diff --git a/.gitignore b/.gitignore index a1feaa08c..f12a5d7f5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,10 @@ .gitignore .vscode/ __*__ -build*/ ignore/ python/src/xstudio.egg-info/ python/test/xstudio.log docs/conf.py python/src/xstudio/version.py .vs/ +.DS_Store diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..387c71ac9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "extern/otio/OpenTimelineIO"] + path = extern/otio/OpenTimelineIO + url = https://github.com/AcademySoftwareFoundation/OpenTimelineIO diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b1378917..e69de29bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +0,0 @@ - diff --git a/CMakeLists.txt b/CMakeLists.txt index f5da236e1..776f4665f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,44 +1,37 @@ -cmake_minimum_required(VERSION 3.12 FATAL_ERROR) - -option(USE_VCPKG "Use Vcpkg for package management" OFF) -if(WIN32) - set(USE_VCPKG ON) -endif() - -if (USE_VCPKG) - include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/vcpkg.cmake) -endif() +cmake_minimum_required(VERSION 3.26 FATAL_ERROR) +cmake_policy(VERSION 3.26) -set(XSTUDIO_GLOBAL_VERSION "0.11.2" CACHE STRING "Version string") +set(XSTUDIO_GLOBAL_VERSION "1.0.0" CACHE STRING "Version string") set(XSTUDIO_GLOBAL_NAME xStudio) -project(${XSTUDIO_GLOBAL_NAME} VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) +set(CMAKE_OSX_DEPLOYMENT_TARGET "14.5" CACHE STRING "Minimum OS X deployment version" FORCE) -cmake_policy(VERSION 3.26) +project(${XSTUDIO_GLOBAL_NAME} VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) option(BUILD_TESTING "Build tests" OFF) option(INSTALL_PYTHON_MODULE "Install python module" ON) option(INSTALL_XSTUDIO "Install xstudio" ON) -option(BUILD_DOCS "Build xStudio documentation" OFF) +option(BUILD_DOCS "Build xStudio documentation" ON) option(ENABLE_CLANG_TIDY "Enable clang-tidy, ninja clang-tidy." OFF) option(ENABLE_CLANG_FORMAT "Enable clang format, ninja clangformat." OFF) option(FORCE_COLORED_OUTPUT "Always produce ANSI-colored output (GNU/Clang only)." TRUE) option(OPTIMIZE_FOR_NATIVE "Build with -march=native" OFF) option(BUILD_RESKIN "Build xstudio reskin binary" ON) - +option(OTIO_SUBMODULE "Automatically build OpenTimelineIO as a submodule" OFF) +option(USE_VCPKG "Use Vcpkg for package management" OFF) +option(BUILD_PYSIDE_WIDGETS "Build xstudio player as PySide widget" OFF) if(WIN32) - set(CMAKE_CXX_FLAGS_DEBUG "/Zi /Ob0 /Od /Oy-") + set(USE_VCPKG ON) + #include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/vcpkg.cmake) + set(CMAKE_CXX_FLAGS_DEBUG "/Zi /Ob0 /Od /Oy-") add_compile_options($<$:/MP>) - # enable UUID System Generator + # enable UUID System Generator add_definitions(-DUUID_SYSTEM_GENERATOR=ON) endif() -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") - set(STUDIO_PLUGINS "" CACHE STRING "Enable compilation of SITE plugins") - if (("${CMAKE_GENERATOR}" MATCHES "Makefiles" OR ("${CMAKE_GENERATOR}" MATCHES "Ninja" AND NOT WIN32)) AND NOT __CHECKED_CXX_FLAGS) set(__CHECKED_CXX_FLAGS TRUE CACHE INTERNAL "Whether we checked the CXX flags already") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${COMMON_GCC}" CACHE STRING "" FORCE) @@ -49,7 +42,12 @@ if (("${CMAKE_GENERATOR}" MATCHES "Makefiles" OR ("${CMAKE_GENERATOR}" MATCHES " endif() set(CXXOPTS_BUILD_TESTS OFF CACHE BOOL "Enable or disable cxxopts' tests") -if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpic -fdiagnostics-color=always") +endif() + +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpic -fmax-errors=5 -fdiagnostics-color=always") endif() @@ -71,9 +69,8 @@ if (NOT ${GCC_MARCH_OVERRIDE} STREQUAL "") endif() endif() -if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fpic") -endif() + +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fpic") set(TEST_RESOURCE "${CMAKE_CURRENT_SOURCE_DIR}/test_resource") set(ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}) @@ -169,7 +166,7 @@ if(WIN32) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) # When moving to Qt6 or greater, we might be able to use qt_generate_deploy_app_script - #set(deploy_script "${Qt5_DIR}/../../../windeployqt.exe ) + #set(deploy_script "${Qt6_DIR}/../../../windeployqt.exe ) endif() if(MSVC) @@ -178,33 +175,46 @@ if(MSVC) endif() -# Add the necessary libraries from Vcpkg if Vcpkg integration is enabled -if(USE_VCPKG) +if (USE_VCPKG) + + # When building with VCPKG, we will use OTIO submodule + set(OTIO_SUBMODULE true) + add_subdirectory("extern/otio") set(VCPKG_INTEGRATION ON) - # Set Python in VCPKG - set(Python_EXECUTABLE "${VCPKG_DIRECTORY}/../vcpkg_installed/x64-windows/tools/python3/python.exe") + # Install pip and sphinx + find_package(Python COMPONENTS Interpreter Development) + + message("Python_RUNTIME_LIBRARY_DIRS ${Python_RUNTIME_LIBRARY_DIRS}") + execute_process( - COMMAND "${CMAKE_COMMAND}" -E env "PATH=${VCPKG_DIRECTORY}/../vcpkg_installed/x64-windows/tools/python3" python.exe -m ensurepip --upgrade + COMMAND "${Python_EXECUTABLE}" -m ensurepip --upgrade RESULT_VARIABLE ENSUREPIP_RESULT ) + if(ENSUREPIP_RESULT) message(FATAL_ERROR "Failed to ensurepip.") else() execute_process( - COMMAND "${CMAKE_COMMAND}" -E env "PATH=${VCPKG_DIRECTORY}/../vcpkg_installed/x64-windows/tools/python3" python.exe -m pip install setuptools sphinx breathe sphinx-rtd-theme OpenTimelineIO importlib_metadata zipp + COMMAND "${Python_EXECUTABLE}" -m pip install setuptools sphinx breathe sphinx-rtd-theme OpenTimelineIO importlib_metadata zipp RESULT_VARIABLE PIP_RESULT ) if(PIP_RESULT) message(FATAL_ERROR "Failed to install Sphinx using pip.") endif() endif() - # append vcpkg packages - list(APPEND CMAKE_PREFIX_PATH "${VCPKG_DIRECTORY}/../vcpkg_installed/x64-windows") - + +else() + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") + if(${OTIO_SUBMODULE}) + add_subdirectory("extern/otio") + endif() + find_package(PkgConfig REQUIRED) endif() +# Add the necessary libraries from Vcpkg if Vcpkg integration is enabled + find_package(nlohmann_json CONFIG REQUIRED) include(CTest) @@ -226,54 +236,55 @@ add_subdirectory(src) if(INSTALL_XSTUDIO) - - # add extern libs that are build-time dependencies of xstudio -if (UNIX) - add_subdirectory("extern/reproc") -endif() - add_subdirectory("extern/quickfuture") - add_subdirectory("extern/quickpromise") - - add_subdirectory(share/preference) - add_subdirectory(share/snippets) - add_subdirectory(share/fonts) + # build quickpromise + add_subdirectory(extern/quickpromise) + add_subdirectory(extern/quickfuture) install(DIRECTORY include/xstudio DESTINATION include) INSTALL(DIRECTORY extern/ DESTINATION extern) + add_subdirectory(share/preference) + add_subdirectory(share/snippets) + add_subdirectory(share/fonts) + if(BUILD_DOCS) - if(NOT INSTALL_PYTHON_MODULE) - add_subdirectory(python) - endif () add_subdirectory(docs) else() - install(DIRECTORY share/docs/ DESTINATION share/xstudio/docs) + if (APPLE) + install(DIRECTORY share/docs/ DESTINATION ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/docs) + else() + install(DIRECTORY share/docs/ DESTINATION share/xstudio/docs) + endif() endif () - include(CMakePackageConfigHelpers) + if (NOT APPLE) - configure_package_config_file(xStudioConfig.cmake.in - ${CMAKE_CURRENT_BINARY_DIR}/xStudioConfig.cmake - INSTALL_DESTINATION lib/cmake/${PROJECT_NAME} - ) - write_basic_package_version_file("xStudioConfigVersion.cmake" - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion - ) + include(CMakePackageConfigHelpers) - install(FILES ${CMAKE_CURRENT_BINARY_DIR}/xStudioConfig.cmake - ${CMAKE_CURRENT_BINARY_DIR}/xStudioConfigVersion.cmake - DESTINATION lib/cmake/${PROJECT_NAME} - ) + configure_package_config_file(xStudioConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/xStudioConfig.cmake + INSTALL_DESTINATION lib/cmake/${PROJECT_NAME} + ) + write_basic_package_version_file("xStudioConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion + ) - install(EXPORT xstudio - DESTINATION lib/cmake/${PROJECT_NAME} - FILE ${PROJECT_NAME}Targets.cmake - NAMESPACE xstudio:: - EXPORT_LINK_INTERFACE_LIBRARIES - ) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/xStudioConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/xStudioConfigVersion.cmake + DESTINATION lib/cmake/${PROJECT_NAME} + ) + + install(EXPORT xstudio + DESTINATION lib/cmake/${PROJECT_NAME} + FILE ${PROJECT_NAME}Targets.cmake + NAMESPACE xstudio:: + EXPORT_LINK_INTERFACE_LIBRARIES + ) + + endif() endif () @@ -281,4 +292,63 @@ if(USE_VCPKG) # To provide reliable ordering, we need to make this install script happen in a subdirectory. # Otherwise, Qt deploy will happen before we have the rest of the application deployed. add_subdirectory("scripts/qt_install") -endif() \ No newline at end of file +endif() + +if (WIN32) + + include(InstallRequiredSystemLibraries) + + set(CPACK_PACKAGE_VERSION "1.0.0") + set(CPACK_PACKAGE_VERSION_MAJOR "1") + set(CPACK_PACKAGE_VERSION_MINOR "0") + set(CPACK_PACKAGE_VERSION_PATCH "0") + set(CPACK_PACKAGE_VENDOR "DNEG / Academy Software Foundation") + set (CPACK_NSIS_MUI_ICON + "${CMAKE_CURRENT_SOURCE_DIR}/ui/icons\\\\xstudio_app.ico") + set (CPACK_PACKAGE_NAME "xSTUDIO") + set (CPACK_NSIS_PACKAGE_NAME "xSTUDIO") + set (CPACK_PACKAGE_ICON + "${CMAKE_CURRENT_SOURCE_DIR}/ui/icons\\\\xstudio_app.ico") + set (CPACK_RESOURCE_FILE_LICENSE + "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE") + set (CPACK_CREATE_DESKTOP_LINKS xstudio) + set (CPACK_NSIS_MENU_LINKS + "share/docs/index.html" + "xSTUDIO Help") + + if("${STUDIO_PLUGINS}" STREQUAL "dneg") + # for dneg deployment, we add some CLI args to pick up our customised facility preferences thus: + set(CPACK_NSIS_CREATE_ICONS_EXTRA + "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\xSTUDIO.lnk' '$INSTDIR\\\\bin\\\\xstudio.exe' '--pref N:\\\\SITE\\\\data\\\\xStudio\\\\dneg_defaults.json --pref N:\\\\SITE\\\\data\\\\xStudio\\\\windows_extra_config\\\\dneg_windows_defaults.json'" + ) + else() + set(CPACK_NSIS_CREATE_ICONS_EXTRA + "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\xSTUDIO.lnk' '$INSTDIR\\\\bin\\\\xstudio.exe'" + ) + endif() + + #set (CPACK_PACKAGE_EXECUTABLES "xstudio" "xSTUDIO" ) + + set (CPACK_NSIS_EXTRA_INSTALL_COMMANDS " + WriteRegStr HKCR '.xst' '' 'XStudioSession' + WriteRegStr HKCR 'XStudioSession' '' 'xSTUDIO Session File' + WriteRegStr HKCR 'XStudioSession\\\\shell' '' 'open' + WriteRegStr HKCR 'XStudioSession\\\\DefaultIcon' \\\\ + '' '$INSTDIR\\\\bin\\\\xstudio.exe,0' + WriteRegStr HKCR 'XStudioSession\\\\shell\\\\open\\\\command' \\\\ + '' '$INSTDIR\\\\bin\\\\xstudio.exe \\"%1\\"' + WriteRegStr HKCR 'XStudioSession\\\\shell\\\\edit' \\\\ + '' 'Edit xSTUDIO Session File' + WriteRegStr HKCR 'XStudioSession\\\\shell\\\\edit\\\\command' \\\\ + '' '$INSTDIR\\\\bin\\\\xstudio.exe \\"%1\\"' + System::Call \\\\ + 'Shell32::SHChangeNotify(i 0x8000000, i 0, i 0, i 0)' + ") + + set (CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " + DeleteRegKey HKCR '.xst' + DeleteRegKey HKCR 'XStudioSession' + ") + + include(CPack) +endif() diff --git a/CMakePresets.json b/CMakePresets.json index fa832be0e..8fd9fb902 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,33 +1,39 @@ { "version": 3, "configurePresets": [ + { + "name": "default", + "hidden": true, + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/../vcpkg/scripts/buildsystems/vcpkg.cmake", + "Qt6_DIR": "C:/Qt6/6.5.3/msvc2019_64/lib/cmake/Qt6", + "CMAKE_INSTALL_PREFIX": "xstudio_install", + "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", + "BUILD_DOCS": "OFF", + "USE_VCPKG": "ON" + } + }, { "name": "windows-base", + "inherits": "default", "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, "hidden": true, - "generator": "Ninja", - "binaryDir": "${sourceDir}/build", - "cacheVariables": { - "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/build/vcpkg/scripts/buildsystems/vcpkg.cmake", - "Qt5_DIR": "C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5/", - "CMAKE_INSTALL_PREFIX": "C:/xstudio_install", - "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON", - "BUILD_DOCS": "OFF" - } + "generator": "Visual Studio 17 2022" }, { - "name": "Release", + "name": "WinRelease", "inherits": ["windows-base"], "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } }, { - "name": "RelWithDebInfo", + "name": "WinRelWithDebInfo", "inherits": ["windows-base"], "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo", @@ -35,13 +41,110 @@ } }, { - "name": "Debug", + "name": "WinDebug", "hidden": true, "inherits": ["windows-base"], "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "USE_SANITIZER": "address" } + }, + { + "name": "macos-base-arm", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "inherits": "default", + "generator": "Unix Makefiles", + "cacheVariables": { + "VCPKG_OVERLAY_TRIPLETS" : "${sourceDir}/cmake/vcpkg_triplets", + "VCPKG_TARGET_TRIPLET": "arm-osx" + } + }, + { + "name": "macos-base-intel", + "inherits": "macos-base-arm", + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "x64-osx" + } + }, + { + "name": "MacOSRelease", + "inherits": ["macos-base-arm"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "MacOSRelWithDebInfo", + "inherits": ["macos-base-arm"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "USE_SANITIZER": "address" + } + }, + { + "name": "MacOSDebug", + "inherits": ["macos-base-arm"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "USE_SANITIZER": "address" + } + }, + { + "name": "MacOSIntelRelease", + "inherits": ["macos-base-intel"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "MacOSIntelRelWithDebInfo", + "inherits": ["macos-base-intel"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "USE_SANITIZER": "address" + } + }, + { + "name": "MacOSIntelDebug", + "inherits": ["macos-base-intel"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "USE_SANITIZER": "address" + } + }, + { + "name": "linux-base", + "inherits": "default", + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "x64-xstudio-linux" + } + }, + { + "name": "LinuxRelease", + "inherits": ["linux-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "LinuxRelWithDebInfo", + "inherits": ["linux-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "USE_SANITIZER": "address" + } + }, + { + "name": "LinuxDebug", + "inherits": ["linux-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "USE_SANITIZER": "address" + } } ] } diff --git a/LICENSE b/LICENSE index 261eeb9e9..32b57b135 100644 --- a/LICENSE +++ b/LICENSE @@ -175,18 +175,7 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] + Copyright 2025 DNEG Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/NOTICE.TXT b/NOTICE.TXT index 07b0f31b4..23681bdb1 100644 --- a/NOTICE.TXT +++ b/NOTICE.TXT @@ -118,4 +118,30 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. + +cpp-colors + +Located in extern/cpp-colors/ + +The MIT License (MIT) + +Copyright (c) 2014 Grigoriy Chudnov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 124f666e4..7e5da55a7 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,22 @@ -# Welcome to xSTUDIO +# Welcome to xSTUDIO - v1.0.0 (alpha) xSTUDIO is a media playback and review application designed for professionals working in the film and TV post production industries, particularly the Visual Effects and Feature Animation sectors. xSTUDIO is focused on providing an intuitive, easy to use interface with a high performance playback engine at its core and C++ and Python APIs for pipeline integration and customisation for total flexibility. +This codebase will build version 1.0.0 (alpha) of xstudio. There are some known issues that are currently being worked on: + +* Moderate audio distortion on playback (Windows only) +* Ueser Documentation and API Documentation is badly out-of-date. +* Saved sessions might not restore media correctly (Windows only) + ## Building xSTUDIO -This release of xSTUDIO can be built on various Linux flavours and Windows 10 and 11. MacOS compatibility is not available yet but this work is on the roadmap for 2024. +This release of xSTUDIO can be built on various Linux flavours and Windows 10 and 11. MacOS compatibility is not available yet but this work is on the roadmap for early 2025. We provide comprehensive build steps for 4 of the most popular distributions. ### Building xSTUDIO for Linux +* [Linux Generic](docs/build_guides/linux_generic.md) * [CentOS 7](docs/build_guides/centos_7.md) * [Rocky Linux 9.1](docs/build_guides/rocky_linux_9_1.md) * [Ubuntu 22.04](docs/build_guides/ubuntu_22_04.md) @@ -20,8 +27,8 @@ We provide comprehensive build steps for 4 of the most popular distributions. ### Building xSTUDIO for MacOS -MacOS compatibility is not yet available. Watch this space! +* [MacOS](docs/build_guides/macos.md) ### Documentation Note -Note that the xSTUDIO user guide is built with Sphinx using the Read-The-Docs theme. The package dependencies for building the docs are somewhat onerous to install and as such we have ommitted these steps from the instructions and instead recommend that you turn off the docs build. Instead, we include the fully built docs (as html pages) as part of this repo and building xSTUDIO will install these pages so that they can be loaded into your browser via the Help menu in the main UI. +Note that the xSTUDIO user guide is built with Sphinx using the Read-The-Docs theme. The package dependencies for building the docs can be challenging to install so we instead include the fully built docs as part of xSTUDIO's repo, as well as the source for building the docs. Our build set-up by default disables the building of the docs to make life easy! \ No newline at end of file diff --git a/cmake/macros.cmake b/cmake/macros.cmake index 53a27b288..ac856e1a4 100644 --- a/cmake/macros.cmake +++ b/cmake/macros.cmake @@ -1,30 +1,39 @@ - macro(default_compile_options name) target_compile_options(${name} # PRIVATE -fvisibility=hidden PRIVATE $<$,$>:-fno-omit-frame-pointer> PRIVATE $<$,$>:/Oy> - PRIVATE $<$,$>:/showIncludes> + PRIVATE $<$:-Wno-deprecated> + PRIVATE $<$:-Wno-deprecated> + # PRIVATE $<$:-Wno-deprecated-declarations> # PRIVATE $<$:-Wno-unused-variable> # PRIVATE $<$:-Wno-unused-but-set-variable> # PRIVATE $<$:-Wno-unused-parameter> PRIVATE $<$,$>:-Wno-unused-function> PRIVATE $<$,$>:-Wextra> + PRIVATE $<$,$>:-Wextra> + PRIVATE $<$:-Wfatal-errors> # Stop after first error PRIVATE $<$,$>:-Wpedantic> PRIVATE $<$,$>:/wd4100> + PRIVATE $<$:/bigobj> # PRIVATE $<$:-Wall> # PRIVATE $<$:-Werror> + # PRIVATE $<$:-Wextra> + # PRIVATE $<$:-Wpedantic> # PRIVATE ${GTEST_CFLAGS} ) target_compile_features(${name} - PUBLIC cxx_std_17 + PUBLIC cxx_std_20 ) target_compile_definitions(${name} PUBLIC $<$:test_private=public> PUBLIC $<$>:test_private=private> + PRIVATE -DSPDLOG_FMT_EXTERNAL $<$:_GNU_SOURCE> # Define _GNU_SOURCE for Linux + $<$:__apple__> # Define __apple__ for MacOS + $<$:__OPENGL_4_1__> # MacOS only supports up to GL4.1 $<$:__linux__> # Define __linux__ for Linux $<$:_WIN32> # Define _WIN32 for Windows PRIVATE XSTUDIO_GLOBAL_VERSION=\"${XSTUDIO_GLOBAL_VERSION}\" @@ -33,8 +42,8 @@ macro(default_compile_options name) PUBLIC BINARY_DIR=\"${CMAKE_BINARY_DIR}/bin\" PRIVATE TEST_RESOURCE=\"${TEST_RESOURCE}\" PRIVATE ROOT_DIR=\"${ROOT_DIR}\" - $<$:WIN32_LEAN_AND_MEAN> PRIVATE $<$:XSTUDIO_DEBUG=1> + $<$:WIN32_LEAN_AND_MEAN> ) endmacro() @@ -43,15 +52,16 @@ if (BUILD_TESTING) target_compile_options(${name} # PRIVATE -fvisibility=hidden PRIVATE $<$:-fno-omit-frame-pointer> + PRIVATE -Wno-deprecated # PRIVATE $<$:-Wno-unused-variable> # PRIVATE $<$:-Wno-unused-but-set-variable> # PRIVATE $<$:-Wno-unused-parameter> - $<$:PRIVATE $<$:-Wno-unused-function>> + PRIVATE $<$,$>:-Wno-unused-function> # PRIVATE $<$:-Wall> # PRIVATE $<$:-Werror> - $<$:PRIVATE $<$:-Wextra>> - $<$:PRIVATE $<$:-Wpedantic>> - $ PRIVATE ${GTEST_CFLAGS} + PRIVATE $<$,$>:-Wextra> + PRIVATE $<$,$>:-Wpedantic> + PRIVATE ${GTEST_CFLAGS} ) target_compile_features(${name} @@ -89,22 +99,36 @@ macro(default_options_local name) ${CMAKE_CURRENT_SOURCE_DIR}/src SYSTEM PUBLIC $ + $ ) - set_target_properties(${name} - PROPERTIES - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/lib" - ) + if (APPLE) + set_target_properties(${name} + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Frameworks" + ) + else() + set_target_properties(${name} + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/lib" + ) + endif() endmacro() macro(default_options name) default_options_local(${name}) - install(TARGETS ${name} EXPORT xstudio - LIBRARY DESTINATION share/xstudio/lib) + + if (NOT APPLE) + install(TARGETS ${name} EXPORT xstudio + LIBRARY DESTINATION share/xstudio/lib) + endif() + target_include_directories(${name} INTERFACE $ $ + $ $ - $) + $ + ) endmacro() macro(default_options_static name) @@ -123,6 +147,7 @@ macro(default_options_static name) ${CMAKE_CURRENT_SOURCE_DIR}/src SYSTEM PUBLIC $ + $ ) set_target_properties(${name} PROPERTIES @@ -135,7 +160,7 @@ macro(default_plugin_options name) find_package(CAF COMPONENTS core io) endif (NOT CAF_FOUND) - find_package(spdlog REQUIRED) + find_package(spdlog CONFIG REQUIRED) default_compile_options(${name}) target_include_directories(${name} PUBLIC @@ -145,28 +170,72 @@ macro(default_plugin_options name) ${CMAKE_CURRENT_SOURCE_DIR}/src SYSTEM PUBLIC $ + $ ) - set_target_properties(${name} - PROPERTIES - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/plugin" - ) - install(TARGETS ${name} - LIBRARY DESTINATION share/xstudio/plugin) + + if (APPLE) + set_target_properties(${name} + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/PlugIns/xstudio" + ) + #InstallSymlink(${CMAKE_INSTALL_PREFIX}/xstudio.bin.app/Contents/Frameworks lib${name}.dylib + # ${CMAKE_INSTALL_PREFIX}/xstudio.bin.app/Contents/Resources/share/xstudio/plugin/lib${name}.dylib + # ) + + else() + set_target_properties(${name} + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/plugin" + ) + + install(TARGETS ${name} + LIBRARY DESTINATION share/xstudio/plugin) + + endif() + if(WIN32) #This will unfortunately also install the plugin in the /bin directory. TODO: Figure out how to omit the plugin itself. - install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin ) + install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin ) # We don't want the vcpkg install because it forces dependences; we just want the plugin. - _install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/plugin) + _install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION share/xstudio/plugin) #For interactive debugging, we want only the output dll to be copied to the build plugins folder. add_custom_command( - TARGET ${PROJECT_NAME} - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy "$" "${CMAKE_CURRENT_BINARY_DIR}/plugin" + TARGET ${PROJECT_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy "$" "${CMAKE_CURRENT_BINARY_DIR}/plugin" ) endif() + +endmacro() + +macro(add_plugin_qml name _dir) + + add_custom_target(${name}_COPY_QML ALL) + file(GLOB DIRS ${CMAKE_CURRENT_SOURCE_DIR}/${_dir}/* LIST_FOLDERS) + + if (APPLE) + foreach(DIR ${DIRS}) + if(IS_DIRECTORY ${DIR}) + cmake_path(GET DIR FILENAME dirname) + add_custom_command(TARGET ${name}_COPY_QML POST_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy_directory ${DIR} ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/PlugIns/xstudio/qml/${dirname}) + endif() + endforeach() + else() + foreach(DIR ${DIRS}) + if(IS_DIRECTORY ${DIR}) + cmake_path(GET DIR FILENAME dirname) + add_custom_command(TARGET ${name}_COPY_QML POST_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy_directory ${DIR} ${CMAKE_BINARY_DIR}/bin/plugin/qml/${dirname}) + install(DIRECTORY ${DIR} DESTINATION share/xstudio/plugin/qml) + endif() + endforeach() + endif() endmacro() @@ -177,7 +246,7 @@ if (BUILD_TESTING) if (NOT CAF_FOUND) find_package(CAF COMPONENTS core io) endif (NOT CAF_FOUND) - find_package(spdlog REQUIRED) + find_package(spdlog CONFIG REQUIRED) default_compile_options_gtest(${name}) target_include_directories(${name} PUBLIC @@ -187,6 +256,7 @@ if (BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/src SYSTEM PUBLIC $ + $ ) endmacro() endif (BUILD_TESTING) @@ -196,7 +266,7 @@ macro(default_options_qt name) if (NOT CAF_FOUND) find_package(CAF COMPONENTS core io) endif (NOT CAF_FOUND) - find_package(spdlog REQUIRED) + find_package(spdlog CONFIG REQUIRED) default_compile_options(${name}) target_include_directories(${name} PUBLIC @@ -206,40 +276,104 @@ macro(default_options_qt name) ${CMAKE_CURRENT_SOURCE_DIR}/src SYSTEM PUBLIC $ - ${Qt5Core_INCLUDE_DIRS} - ${Qt5OpenGL_INCLUDE_DIRS} - ${Qt5Quick_INCLUDE_DIRS} - ${Qt5Gui_INCLUDE_DIRS} - ${Qt5Widgets_INCLUDE_DIRS} - ${Qt5Concurrent_INCLUDE_DIRS} - ${Qt5Qml_INCLUDE_DIRS} - ) - set_target_properties(${name} - PROPERTIES - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/lib" + ${Qt6Core_INCLUDE_DIRS} + ${Qt6OpenGL_INCLUDE_DIRS} + ${Qt6Quick_INCLUDE_DIRS} + ${Qt6Gui_INCLUDE_DIRS} + ${Qt6Widgets_INCLUDE_DIRS} + ${Qt6OpenGLWidgets_INCLUDE_DIRS} + ${Qt6Concurrent_INCLUDE_DIRS} + ${Qt6Qml_INCLUDE_DIRS} ) - install(TARGETS ${name} EXPORT xstudio - LIBRARY DESTINATION share/xstudio/lib) + + if (APPLE) + set_target_properties(${name} + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Frameworks" + ) + else() + set_target_properties(${name} + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/lib" + ) + install(TARGETS ${name} EXPORT xstudio + LIBRARY DESTINATION share/xstudio/lib) + endif() endmacro() -macro(add_src_and_test NAME) - if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${NAME}/CMakeLists.txt) - add_subdirectory(${NAME}) - endif() +macro(add_src_and_test_main NAME INSTALL_BIN INSTALL_PYTHON) + if(${INSTALL_BIN}) + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${NAME}/CMakeLists.txt) + add_subdirectory(${NAME}) + endif() - if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${NAME}/src) - add_subdirectory(${NAME}/src) + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${NAME}/src) + add_subdirectory(${NAME}/src) + endif() + + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${NAME}/share/preference) + file(GLOB PREFFILES ${CMAKE_CURRENT_SOURCE_DIR}/${NAME}/share/preference/*.json) + + STRING(REGEX REPLACE "/" "_" SAFENAME ${NAME}) + + add_custom_target(${SAFENAME}_PREFERENCES ALL) + foreach(PREFFile ${PREFFILES}) + get_filename_component(PREFNAME ${PREFFile} NAME) + add_preference(${PREFNAME} ${CMAKE_CURRENT_SOURCE_DIR}/${NAME}/share/preference ${SAFENAME}_PREFERENCES) + endforeach() + endif () + + if (BUILD_TESTING) + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${NAME}/test) + add_subdirectory(${NAME}/test) + endif() + endif() endif() - if (BUILD_TESTING) - if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${NAME}/test) - add_subdirectory(${NAME}/test) + if(${INSTALL_PYTHON}) + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${NAME}/python) + add_subdirectory(${NAME}/python) endif() endif() + +endmacro() + +macro(add_src_and_test NAME) + add_src_and_test_main(${NAME} ${INSTALL_XSTUDIO} ${INSTALL_PYTHON_MODULE}) +endmacro() + +macro(add_src_and_test_always NAME) + add_src_and_test_main(${NAME} "ON" "ON") endmacro() +macro(add_python_plugin NAME) + + install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${NAME} DESTINATION share/xstudio/plugin-python) + + add_custom_target(COPY_PY_PLUGIN_${NAME} ALL) + + if (APPLE) + + # ensure we have a destination directory + add_custom_command(TARGET COPY_PY_PLUGIN_${NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/plugin-python") + + add_custom_command(TARGET COPY_PY_PLUGIN_${NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/${NAME} ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/plugin-python/${NAME}) + + else() + + add_custom_command(TARGET COPY_PY_PLUGIN_${NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/${NAME} ${CMAKE_BINARY_DIR}/bin/plugin-python/${NAME}) + + endif() + +endmacro() + macro(create_plugin NAME VERSION DEPS) create_plugin_with_alias(${NAME} xstudio::${NAME} ${VERSION} "${DEPS}") endmacro() @@ -250,7 +384,6 @@ macro(create_plugin_with_alias NAME ALIASNAME VERSION DEPS) file(GLOB SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) - add_library(${PROJECT_NAME} SHARED ${SOURCES}) add_library(${ALIASNAME} ALIAS ${PROJECT_NAME}) default_plugin_options(${PROJECT_NAME}) @@ -328,6 +461,92 @@ macro(create_component_static_with_alias NAME ALIASNAME VERSION DEPS STATICDEPS) endmacro() +macro(add_resource name path target resource_type) + + if (APPLE) + + # ensure we have a destination directory + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/${resource_type}") + + # As part of build we copy directly into bundle resources folder + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${path}/${name} + "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/${resource_type}/") + + else() + + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${path}/${name} + ${CMAKE_BINARY_DIR}/bin/${resource_type}/${name}) + + if(INSTALL_XSTUDIO) + install(FILES + ${path}/${name} + DESTINATION share/xstudio/${resource_type}) + endif () + + endif() + +endmacro() + +macro(add_preference name path target) + + if (APPLE) + + # ensure we have a destination directory + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/preference") + + # As part of build we copy directly into bundle resources folder + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${path}/${name} + "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/preference/") + + else() + + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${path}/${name} + ${CMAKE_BINARY_DIR}/bin/preference/${name}) + + if(INSTALL_XSTUDIO) + install(FILES + ${path}/${name} + DESTINATION share/xstudio/preference) + endif () + + endif() + +endmacro() + +macro(add_font name path target) + + if (APPLE) + + # ensure we have a destination directory + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/fonts") + + # As part of build we copy directly into bundle resources folder + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${path}/${name} + "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/fonts/") + + else() + + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${path}/${name} + ${CMAKE_BINARY_DIR}/bin/fonts/${name}) + + if(INSTALL_XSTUDIO) + install(FILES + ${path}/${name} + DESTINATION share/xstudio/fonts) + endif () + + endif() + +endmacro() macro(create_test PATH DEPS) @@ -384,7 +603,7 @@ macro(create_qml_component_with_alias NAME ALIASNAME VERSION DEPS EXTRAMOC) foreach(MOC ${MOCSRC}) get_filename_component(MOCNAME "${MOC}" NAME_WE) - QT5_WRAP_CPP(${MOCNAME}_moc ${MOC}) + QT6_WRAP_CPP(${MOCNAME}_moc ${MOC}) list(APPEND SOURCES ${${MOCNAME}_moc}) endforeach() @@ -396,7 +615,6 @@ macro(create_qml_component_with_alias NAME ALIASNAME VERSION DEPS EXTRAMOC) # Generate export header include(GenerateExportHeader) generate_export_header(${PROJECT_NAME} EXPORT_FILE_NAME "${ROOT_DIR}/include/xstudio/ui/qml/${PROJECT_NAME}_export.h") - target_link_libraries(${PROJECT_NAME} PUBLIC ${DEPS} ) @@ -405,19 +623,15 @@ macro(create_qml_component_with_alias NAME ALIASNAME VERSION DEPS EXTRAMOC) set_property(TARGET ${PROJECT_NAME} PROPERTY AUTOMOC ON) ## Add the directory containing the generated export header to the include directories - #target_include_directories(${PROJECT_NAME} + #target_include_directories(${PROJECT_NAME} # PUBLIC ${CMAKE_BINARY_DIR} # Include the build directory #) - - endmacro() macro(build_studio_plugins STUDIO) if(NOT "${STUDIO}" STREQUAL "") - if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${STUDIO}) - file(GLOB DIRS ${CMAKE_CURRENT_SOURCE_DIR}/${STUDIO}/*) foreach(DIR ${DIRS}) if(IS_DIRECTORY ${DIR}) @@ -425,14 +639,10 @@ macro(build_studio_plugins STUDIO) add_src_and_test(${STUDIO}/${PLUGINNAME}) endif() endforeach() - endif() - endif() - endmacro() - macro(set_python_to_proper_build_type) #TODO Resolve linking error when running debug build: https://github.com/pybind/pybind11/issues/3403 endmacro() diff --git a/cmake/modules/FindCAF.cmake b/cmake/modules/FindCAF.cmake index 8a5255727..413691909 100644 --- a/cmake/modules/FindCAF.cmake +++ b/cmake/modules/FindCAF.cmake @@ -120,25 +120,25 @@ mark_as_advanced(CAF_ROOT_DIR CAF_LIBRARIES CAF_INCLUDE_DIRS) -if (CAF_core_FOUND AND NOT TARGET caf::core) - add_library(caf::core UNKNOWN IMPORTED) - set_target_properties(caf::core PROPERTIES +if (CAF_core_FOUND AND NOT TARGET CAF::core) + add_library(CAF::core UNKNOWN IMPORTED) + set_target_properties(CAF::core PROPERTIES IMPORTED_LOCATION "${CAF_LIBRARY_CORE}" INTERFACE_INCLUDE_DIRECTORIES "${CAF_INCLUDE_DIR_CORE}") endif () -if (CAF_io_FOUND AND NOT TARGET caf::io) - add_library(caf::io UNKNOWN IMPORTED) - set_target_properties(caf::io PROPERTIES +if (CAF_io_FOUND AND NOT TARGET CAF::io) + add_library(CAF::io UNKNOWN IMPORTED) + set_target_properties(CAF::io PROPERTIES IMPORTED_LOCATION "${CAF_LIBRARY_IO}" INTERFACE_INCLUDE_DIRECTORIES "${CAF_INCLUDE_DIR_IO}" - INTERFACE_LINK_LIBRARIES "caf::core") + INTERFACE_LINK_LIBRARIES "CAF::core") endif () if (CAF_openssl_FOUND AND NOT TARGET caf::openssl) add_library(caf::openssl UNKNOWN IMPORTED) set_target_properties(caf::openssl PROPERTIES IMPORTED_LOCATION "${CAF_LIBRARY_OPENSSL}" INTERFACE_INCLUDE_DIRECTORIES "${CAF_INCLUDE_DIR_OPENSSL}" - INTERFACE_LINK_LIBRARIES "caf::core;caf::io") + INTERFACE_LINK_LIBRARIES "CAF::core;CAF::io") if (NOT BUILD_SHARED_LIBS) include(CMakeFindDependencyMacro) set(OPENSSL_USE_STATIC_LIBS TRUE) @@ -151,5 +151,5 @@ if (CAF_test_FOUND AND NOT TARGET caf::test) add_library(caf::test INTERFACE IMPORTED) set_target_properties(caf::test PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${CAF_INCLUDE_DIR_TEST}" - INTERFACE_LINK_LIBRARIES "caf::core") -endif () \ No newline at end of file + INTERFACE_LINK_LIBRARIES "CAF::core") +endif () diff --git a/cmake/modules/FindDbgHelp.cmake b/cmake/modules/FindDbgHelp.cmake deleted file mode 100644 index 022e46034..000000000 --- a/cmake/modules/FindDbgHelp.cmake +++ /dev/null @@ -1,19 +0,0 @@ -# List of possible library names -set(DBGHELP_NAMES dbghelp) - -if(MSVC) - # Try to find library - find_library(DBGHELP_LIBRARY NAMES ${DBGHELP_NAMES}) - - # Try to find include directory - find_path(DBGHELP_INCLUDE_DIR NAMES dbghelp.h PATH_SUFFIXES include) - - include(FindPackageHandleStandardArgs) - FIND_PACKAGE_HANDLE_STANDARD_ARGS(DbgHelp REQUIRED_VARS DBGHELP_LIBRARY DBGHELP_INCLUDE_DIR) - - if(DbgHelp_FOUND) - set(DBGHELP_LIBRARIES ${DBGHELP_LIBRARY}) - else() - message(FATAL_ERROR "DbgHelp not found") - endif() -endif() \ No newline at end of file diff --git a/cmake/modules/FindSphinx.cmake b/cmake/modules/FindSphinx.cmake index 4825e1e14..0243f65fb 100644 --- a/cmake/modules/FindSphinx.cmake +++ b/cmake/modules/FindSphinx.cmake @@ -9,4 +9,4 @@ include(FindPackageHandleStandardArgs) # Handle standard arguments to find_package like REQUIRED and QUIET find_package_handle_standard_args(Sphinx "Failed to find sphinx-build executable" - SPHINX_EXECUTABLE) \ No newline at end of file + SPHINX_EXECUTABLE) diff --git a/cmake/modules/vcpkg.cmake b/cmake/modules/vcpkg.cmake deleted file mode 100644 index daebd6d23..000000000 --- a/cmake/modules/vcpkg.cmake +++ /dev/null @@ -1,615 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. -# -# Copyright (C) 2022, Arne Wendt -# - -# vcpkg examples use 3.0.0, assuming this as minimum version for vcpkg cmake toolchain -cmake_minimum_required(VERSION 3.0.0) -cmake_policy(SET CMP0126 NEW) -set(VCPKG_VERSION "edge") - -# config: -# - VCPKG_VERSION: -# - "latest": latest git tag (undefined or empty treated as "latest") -# - "edge": last commit on master -# - VCPKG_PARENT_DIR: where to place vcpkg -# - VCPKG_FORCE_SYSTEM_BINARIES: use system cmake, zip, unzip, tar, etc. -# may be necessary on some systems as downloaded binaries may be linked against unsupported libraries -# musl-libc based distros (ALPINE)(!) require use of system binaries, but are AUTO DETECTED! -# - VCPKG_FEATURE_FLAGS: modify feature flags; default are "manifests,versions" -# -# - VCPKG_NO_INIT: do not call vcpkg_init() automatically (for use testing) - - -# set default feature flags if not defined -if(NOT DEFINED VCPKG_FEATURE_FLAGS) - set(VCPKG_FEATURE_FLAGS "manifests,versions" CACHE INTERNAL "necessary vcpkg flags for manifest based autoinstall and versioning") -endif() - -# disable metrics by default -if(NOT DEFINED VCPKG_METRICS_FLAG) - set(VCPKG_METRICS_FLAG "-disableMetrics" CACHE INTERNAL "flag to disable telemtry by default") -endif() - -# enable rebuilding of packages if requested by changed configuration -if(NOT DEFINED VCPKG_RECURSE_REBUILD_FLAG) - set(VCPKG_RECURSE_REBUILD_FLAG "--recurse" CACHE INTERNAL "enable rebuilding of packages if requested by changed configuration by default") -endif() - - -# check_conditions and find neccessary packages -find_package(Git REQUIRED) - - - -# get VCPKG -function(vcpkg_init) - # set environment (not cached) - - # mask musl-libc if masked prior - if(VCPKG_MASK_MUSL_LIBC) - vcpkg_mask_if_musl_libc() - endif() - - # use system binaries - if(VCPKG_FORCE_SYSTEM_BINARIES) - set(ENV{VCPKG_FORCE_SYSTEM_BINARIES} "1") - endif() - - # for use in scripting mode - # if(CMAKE_SCRIPT_MODE_FILE) - if(VCPKG_TARGET_TRIPLET) - set(ENV{VCPKG_DEFAULT_TRIPLET} "${VCPKG_DEFAULT_TRIPLET}") - endif() - if(VCPKG_DEFAULT_TRIPLET) - set(ENV{VCPKG_DEFAULT_TRIPLET} "${VCPKG_DEFAULT_TRIPLET}") - endif() - if(VCPKG_HOST_TRIPLET) - set(ENV{VCPKG_DEFAULT_HOST_TRIPLET} "${VCPKG_DEFAULT_HOST_TRIPLET}") - endif() - if(VCPKG_DEFAULT_HOST_TRIPLET) - set(ENV{VCPKG_DEFAULT_HOST_TRIPLET} "${VCPKG_DEFAULT_HOST_TRIPLET}") - endif() - # endif() - # end set environment - - - # test for vcpkg availability - # executable path set ? assume all ok : configure - if(VCPKG_EXECUTABLE EQUAL "" OR NOT DEFINED VCPKG_EXECUTABLE) - # configure vcpkg - - # use system binaries? - # IMPORTANT: we have to use system binaries on musl-libc systems, as vcpkg fetches binaries linked against glibc! - vcpkg_set_use_system_binaries_flag() - - # mask musl-libc if no triplet is provided - if( - ( ENV{VCPKG_DEFAULT_TRIPLET} EQUAL "" OR NOT DEFINED ENV{VCPKG_DEFAULT_TRIPLET}) AND - ( ENV{VCPKG_DEFAULT_HOST_TRIPLET} EQUAL "" OR NOT DEFINED ENV{VCPKG_DEFAULT_HOST_TRIPLET}) AND - ( VCPKG_TARGET_TRIPLET EQUAL "" OR NOT DEFINED VCPKG_TARGET_TRIPLET) - ) - # mask musl-libc from vcpkg - vcpkg_mask_if_musl_libc() - else() - message(WARNING "One of VCPKG_TARGET_TRIPLET, ENV{VCPKG_DEFAULT_TRIPLET} or ENV{VCPKG_DEFAULT_HOST_TRIPLET} has been defined. NOT CHECKING FOR musl-libc MASKING!") - endif() - - - # test options - if(VCPKG_PARENT_DIR EQUAL "" OR NOT DEFINED VCPKG_PARENT_DIR) - if(CMAKE_SCRIPT_MODE_FILE) - message(FATAL_ERROR "Explicitly specify VCPKG_PARENT_DIR when running in script mode!") - else() - message(STATUS "VCPKG from: ${CMAKE_CURRENT_BINARY_DIR}") - set(VCPKG_PARENT_DIR "${CMAKE_CURRENT_BINARY_DIR}/") - endif() - endif() - string(REGEX REPLACE "[/\\]$" "" VCPKG_PARENT_DIR "${VCPKG_PARENT_DIR}") - - # test if VCPKG_PARENT_DIR has to be created in script mode - if(CMAKE_SCRIPT_MODE_FILE AND NOT EXISTS "${VCPKG_PARENT_DIR}") - message(STATUS "Creating vcpkg parent directory") - file(MAKE_DIRECTORY "${VCPKG_PARENT_DIR}") - endif() - - - # set path/location varibles to expected path; necessary to detect after a CMake cache clean - vcpkg_set_vcpkg_directory_from_parent() - vcpkg_set_vcpkg_executable() - - # executable is present ? configuring done : fetch and build - execute_process(COMMAND ${VCPKG_EXECUTABLE} version RESULT_VARIABLE VCPKG_TEST_RETVAL OUTPUT_VARIABLE VCPKG_VERSION_BANNER) - if(NOT VCPKG_TEST_RETVAL EQUAL "0") - # reset executable path to prevent malfunction/wrong assumptions in case of error - set(VCPKG_EXECUTABLE "") - - # getting vcpkg - message(STATUS "No VCPKG executable found; getting new version ready...") - - # select compile script - if(WIN32) - set(VCPKG_BUILD_CMD ".\\bootstrap-vcpkg.bat") - else() - set(VCPKG_BUILD_CMD "./bootstrap-vcpkg.sh") - endif() - - # prepare and clone git sources - # include(FetchContent) - # set(FETCHCONTENT_QUIET on) - # set(FETCHCONTENT_BASE_DIR "${VCPKG_PARENT_DIR}") - # FetchContent_Declare( - # vcpkg - - # GIT_REPOSITORY "https://github.com/microsoft/vcpkg" - # GIT_PROGRESS true - - # SOURCE_DIR "${VCPKG_PARENT_DIR}/vcpkg" - # BINARY_DIR "" - # BUILD_IN_SOURCE true - # CONFIGURE_COMMAND "" - # BUILD_COMMAND "" - # ) - # FetchContent_Populate(vcpkg) - - # check for bootstrap script ? ok : fetch repository - if(NOT EXISTS "${VCPKG_DIRECTORY}/${VCPKG_BUILD_CMD}" AND NOT EXISTS "${VCPKG_DIRECTORY}\\${VCPKG_BUILD_CMD}") - message(STATUS "VCPKG bootstrap script not found; fetching...") - # directory existent ? delete - if(EXISTS "${VCPKG_DIRECTORY}") - file(REMOVE_RECURSE "${VCPKG_DIRECTORY}") - endif() - - # fetch vcpkg repo - execute_process(COMMAND ${GIT_EXECUTABLE} clone https://github.com/microsoft/vcpkg WORKING_DIRECTORY "${VCPKG_PARENT_DIR}" RESULT_VARIABLE VCPKG_GIT_CLONE_OK) - if(NOT VCPKG_GIT_CLONE_OK EQUAL "0") - message(FATAL_ERROR "Cloning VCPKG repository from https://github.com/microsoft/vcpkg failed!") - endif() - endif() - - # compute git checkout target - vcpkg_set_version_checkout() - - # hide detached head notice - execute_process(COMMAND ${GIT_EXECUTABLE} config advice.detachedHead false WORKING_DIRECTORY "${VCPKG_DIRECTORY}" RESULT_VARIABLE VCPKG_GIT_HIDE_DETACHED_HEAD_IGNORED) - # checkout asked version - execute_process(COMMAND ${GIT_EXECUTABLE} checkout ${VCPKG_VERSION_CHECKOUT} WORKING_DIRECTORY "${VCPKG_DIRECTORY}" RESULT_VARIABLE VCPKG_GIT_TAG_CHECKOUT_OK) - if(NOT VCPKG_GIT_TAG_CHECKOUT_OK EQUAL "0") - message(FATAL_ERROR "Checking out VCPKG version/tag ${VCPKG_VERSION} failed!") - endif() - - # wrap -disableMetrics in extra single quotes for windows - # if(WIN32 AND NOT VCPKG_METRICS_FLAG EQUAL "" AND DEFINED VCPKG_METRICS_FLAG) - # set(VCPKG_METRICS_FLAG "'${VCPKG_METRICS_FLAG}'") - # endif() - - # build vcpkg - execute_process(COMMAND ${VCPKG_BUILD_CMD} ${VCPKG_USE_SYSTEM_BINARIES_FLAG} ${VCPKG_METRICS_FLAG} WORKING_DIRECTORY "${VCPKG_DIRECTORY}" RESULT_VARIABLE VCPKG_BUILD_OK) - if(NOT VCPKG_BUILD_OK EQUAL "0") - message(FATAL_ERROR "Bootstrapping VCPKG failed!") - endif() - message(STATUS "Built VCPKG!") - - - # get vcpkg path - vcpkg_set_vcpkg_executable() - - # test vcpkg binary - execute_process(COMMAND ${VCPKG_EXECUTABLE} version RESULT_VARIABLE VCPKG_OK OUTPUT_VARIABLE VCPKG_VERSION_BANNER) - if(NOT VCPKG_OK EQUAL "0") - message(FATAL_ERROR "VCPKG executable failed test!") - endif() - - message(STATUS "VCPKG OK!") - message(STATUS "Install packages using VCPKG:") - message(STATUS " * from your CMakeLists.txt by calling vcpkg_add_package()") - message(STATUS " * by providing a 'vcpkg.json' in your project directory [https://devblogs.microsoft.com/cppblog/take-control-of-your-vcpkg-dependencies-with-versioning-support/]") - - # generate empty manifest on vcpkg installation if none is found - if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg.json") - cmake_language(DEFER DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} CALL vcpkg_manifest_generation_finalize) - message(STATUS "If you need an empty manifest for setting up your project, you will find one in your build directory") - endif() - endif() - - # we have fetched and built, but a clean has been performed - # version banner is set while testing for availability or after build - message(STATUS "VCPKG using:") - string(REGEX REPLACE "\n.*$" "" VCPKG_VERSION_BANNER "${VCPKG_VERSION_BANNER}") - message(STATUS "${VCPKG_VERSION_BANNER}") - - # cache executable path - set(VCPKG_EXECUTABLE ${VCPKG_EXECUTABLE} CACHE STRING "vcpkg executable path" FORCE) - set(VCPKG_DIRECTORY ${VCPKG_DIRECTORY} CACHE STRING "VCPKG directory" FORCE) - message(STATUS "VCPKG_DIRECTORY: ${VCPKG_DIRECTORY}") - - # initialize manifest generation - vcpkg_manifest_generation_init() - - # install from manifest if ran in script mode - #if(CMAKE_SCRIPT_MODE_FILE) - #message(STATUS "Running in script mode to setup environment: trying dependency installation from manifest!") - if(EXISTS "${CMAKE_SOURCE_DIR}/vcpkg.json") - message(STATUS "Found vcpkg.json; installing...") - vcpkg_install_manifest() - else() - message(STATUS "NOT found vcpkg.json; skipping installation") - endif() - #endif() - - # set toolchain - set(CMAKE_TOOLCHAIN_FILE "${VCPKG_DIRECTORY}/scripts/buildsystems/vcpkg.cmake") - set(CMAKE_TOOLCHAIN_FILE ${CMAKE_TOOLCHAIN_FILE} PARENT_SCOPE) - set(CMAKE_TOOLCHAIN_FILE ${CMAKE_TOOLCHAIN_FILE} CACHE STRING "") - endif() -endfunction() - - -# make target triplet from current compiler selection and platform -# set VCPKG_TARGET_TRIPLET in parent scope -function(vcpkg_make_set_triplet) - # get platform: win/linux ONLY - if(WIN32) - set(PLATFORM "windows") - else() - set(PLATFORM "linux") - endif() - - # get bitness: 32/64 ONLY - if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(BITS 64) - else() - set(BITS 86) - endif() - - set(VCPKG_TARGET_TRIPLET "x${BITS}-${PLATFORM}" PARENT_SCOPE) -endfunction() - -# set VCPKG_DIRECTORY to assumed path based on VCPKG_PARENT_DIR -# vcpkg_set_vcpkg_directory_from_parent([VCPKG_PARENT_DIR_EXPLICIT]) -function(vcpkg_set_vcpkg_directory_from_parent) - if(ARGV0 EQUAL "" OR NOT DEFINED ARGV0) - set(VCPKG_DIRECTORY "${VCPKG_PARENT_DIR}/vcpkg" PARENT_SCOPE) - else() - set(VCPKG_DIRECTORY "${ARGV0}/vcpkg" PARENT_SCOPE) - endif() - # set(VCPKG_DIRECTORY ${VCPKG_DIRECTORY} CACHE STRING "vcpkg tool location" FORCE) -endfunction() - - -# set VCPKG_EXECUTABLE to assumed path based on VCPKG_DIRECTORY -# vcpkg_set_vcpkg_executable([VCPKG_DIRECTORY]) -function(vcpkg_set_vcpkg_executable) - if(ARGV0 EQUAL "" OR NOT DEFINED ARGV0) - set(VCPKG_DIRECTORY_EXPLICIT ${VCPKG_DIRECTORY}) - else() - set(VCPKG_DIRECTORY_EXPLICIT ${ARGV0}) - endif() - - if(WIN32) - set(VCPKG_EXECUTABLE "${VCPKG_DIRECTORY_EXPLICIT}/vcpkg.exe" PARENT_SCOPE) - else() - set(VCPKG_EXECUTABLE "${VCPKG_DIRECTORY_EXPLICIT}/vcpkg" PARENT_SCOPE) - endif() -endfunction() - -# determine git checkout target in: VCPKG_VERSION_CHECKOUT -# vcpkg_set_version_checkout([VCPKG_VERSION_EXPLICIT] [VCPKG_DIRECTORY_EXPLICIT]) -function(vcpkg_set_version_checkout) - if(ARGV0 EQUAL "" OR NOT DEFINED ARGV0) - set(VCPKG_VERSION_EXPLICIT ${VCPKG_VERSION}) - else() - set(VCPKG_VERSION_EXPLICIT ${ARGV0}) - endif() - if(ARGV1 EQUAL "" OR NOT DEFINED ARGV1) - set(VCPKG_DIRECTORY_EXPLICIT ${VCPKG_DIRECTORY}) - else() - set(VCPKG_DIRECTORY_EXPLICIT ${ARGV1}) - endif() - - # get latest git tag - execute_process(COMMAND git for-each-ref refs/tags/ --count=1 --sort=-creatordate --format=%\(refname:short\) WORKING_DIRECTORY "${VCPKG_DIRECTORY_EXPLICIT}" OUTPUT_VARIABLE VCPKG_GIT_TAG_LATEST) - string(REGEX REPLACE "\n$" "" VCPKG_GIT_TAG_LATEST "${VCPKG_GIT_TAG_LATEST}") - - # resolve versions - if(EXISTS "./vcpkg.json") - # set hash from vcpkg.json manifest - file(READ "./vcpkg.json" VCPKG_MANIFEST_CONTENTS) - - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.19) - string(JSON VCPKG_BASELINE GET "${VCPKG_MANIFEST_CONTENTS}" "builtin-baseline") - else() - string(REGEX REPLACE "[\n ]" "" VCPKG_MANIFEST_CONTENTS "${VCPKG_MANIFEST_CONTENTS}") - string(REGEX MATCH "\"builtin-baseline\":\"[0-9a-f]+\"" VCPKG_BASELINE "${VCPKG_MANIFEST_CONTENTS}") - string(REPLACE "\"builtin-baseline\":" "" VCPKG_BASELINE "${VCPKG_BASELINE}") - string(REPLACE "\"" "" VCPKG_BASELINE "${VCPKG_BASELINE}") - endif() - - if(NOT "${VCPKG_BASELINE}" EQUAL "") - if(NOT "${VCPKG_VERSION}" EQUAL "" AND DEFINED VCPKG_VERSION) - message(WARNING "VCPKG_VERSION was specified, but vcpkg.json manifest is used and specifies a builtin-baseline; using builtin-baseline: ${VCPKG_BASELINE}") - endif() - set(VCPKG_VERSION_EXPLICIT "${VCPKG_BASELINE}") - message(STATUS "Using VCPKG Version: ") - endif() - endif() - - if("${VCPKG_VERSION_EXPLICIT}" STREQUAL "latest" OR "${VCPKG_VERSION_EXPLICIT}" EQUAL "" OR NOT DEFINED VCPKG_VERSION_EXPLICIT) - set(VCPKG_VERSION_CHECKOUT ${VCPKG_GIT_TAG_LATEST}) - message(STATUS "Using VCPKG Version: ${VCPKG_VERSION_EXPLICIT} (latest)") - elseif("${VCPKG_VERSION_EXPLICIT}" STREQUAL "edge" OR "${VCPKG_VERSION_EXPLICIT}" STREQUAL "master") - set(VCPKG_VERSION_CHECKOUT "master") - message(STATUS "Using VCPKG Version: edge (latest commit)") - else() - message(STATUS "Using VCPKG Version: ${VCPKG_VERSION_EXPLICIT}") - set(VCPKG_VERSION_CHECKOUT ${VCPKG_VERSION_EXPLICIT}) - endif() - - set(VCPKG_VERSION_CHECKOUT ${VCPKG_VERSION_CHECKOUT} PARENT_SCOPE) -endfunction() - -# sets VCPKG_PLATFORM_MUSL_LIBC(ON|OFF) -function(vcpkg_get_set_musl_libc) - if(WIN32) - # is windows - set(VCPKG_PLATFORM_MUSL_LIBC OFF) - else() - execute_process(COMMAND getconf GNU_LIBC_VERSION RESULT_VARIABLE VCPKG_PLATFORM_GLIBC) - if(VCPKG_PLATFORM_GLIBC EQUAL "0") - # has glibc - set(VCPKG_PLATFORM_MUSL_LIBC OFF) - else() - execute_process(COMMAND ldd --version RESULT_VARIABLE VCPKG_PLATFORM_LDD_OK OUTPUT_VARIABLE VCPKG_PLATFORM_LDD_VERSION_STDOUT ERROR_VARIABLE VCPKG_PLATFORM_LDD_VERSION_STDERR) - string(TOLOWER "${VCPKG_PLATFORM_LDD_VERSION_STDOUT}" VCPKG_PLATFORM_LDD_VERSION_STDOUT) - string(TOLOWER "${VCPKG_PLATFORM_LDD_VERSION_STDERR}" VCPKG_PLATFORM_LDD_VERSION_STDERR) - string(FIND "${VCPKG_PLATFORM_LDD_VERSION_STDOUT}" "musl" VCPKG_PLATFORM_LDD_FIND_MUSL_STDOUT) - string(FIND "${VCPKG_PLATFORM_LDD_VERSION_STDERR}" "musl" VCPKG_PLATFORM_LDD_FIND_MUSL_STDERR) - if( - (VCPKG_PLATFORM_LDD_OK EQUAL "0" AND NOT VCPKG_PLATFORM_LDD_FIND_MUSL_STDOUT EQUAL "-1") OR - (NOT VCPKG_PLATFORM_LDD_OK EQUAL "0" AND NOT VCPKG_PLATFORM_LDD_FIND_MUSL_STDERR EQUAL "-1") - ) - # has musl-libc - # use system binaries - set(VCPKG_PLATFORM_MUSL_LIBC ON) - message(STATUS "VCPKG: System is using musl-libc; using system binaries! (e.g. cmake, curl, zip, tar, etc.)") - else() - # has error... - message(FATAL_ERROR "VCPKG: could detect neither glibc nor musl-libc!") - endif() - endif() - endif() - - # propagate back - set(VCPKG_PLATFORM_MUSL_LIBC ${VCPKG_PLATFORM_MUSL_LIBC} PARENT_SCOPE) -endfunction() - - -# configure environment and CMake variables to mask musl-libc from vcpkg triplet checks -function(vcpkg_mask_musl_libc) - # set target triplet without '-musl' - execute_process(COMMAND ldd --version RESULT_VARIABLE VCPKG_PLATFORM_LDD_OK OUTPUT_VARIABLE VCPKG_PLATFORM_LDD_VERSION_STDOUT ERROR_VARIABLE VCPKG_PLATFORM_LDD_VERSION_STDERR) - string(TOLOWER "${VCPKG_PLATFORM_LDD_VERSION_STDOUT}" VCPKG_PLATFORM_LDD_VERSION_STDOUT) - string(TOLOWER "${VCPKG_PLATFORM_LDD_VERSION_STDERR}" VCPKG_PLATFORM_LDD_VERSION_STDERR) - string(FIND "${VCPKG_PLATFORM_LDD_VERSION_STDOUT}" "x86_64" VCPKG_PLATFORM_LDD_FIND_MUSL_BITS_STDOUT) - string(FIND "${VCPKG_PLATFORM_LDD_VERSION_STDERR}" "x86_64" VCPKG_PLATFORM_LDD_FIND_MUSL_BITS_STDERR) - if( - NOT VCPKG_PLATFORM_LDD_FIND_MUSL_BITS_STDOUT EQUAL "-1" OR - NOT VCPKG_PLATFORM_LDD_FIND_MUSL_BITS_STDERR EQUAL "-1" - ) - set(VCPKG_TARGET_TRIPLET "x64-linux") - else() - set(VCPKG_TARGET_TRIPLET "x86-linux") - endif() - - set(ENV{VCPKG_DEFAULT_TRIPLET} "${VCPKG_TARGET_TRIPLET}") - set(ENV{VCPKG_DEFAULT_HOST_TRIPLET} "${VCPKG_TARGET_TRIPLET}") - set(VCPKG_TARGET_TRIPLET "${VCPKG_TARGET_TRIPLET}" CACHE STRING "vcpkg default target triplet (possibly dont change)") - message(STATUS "VCPKG: System is using musl-libc; fixing default target triplet as: ${VCPKG_TARGET_TRIPLET}") - - set(VCPKG_MASK_MUSL_LIBC ON CACHE INTERNAL "masked musl-libc") -endfunction() - -# automate musl-libc masking -function(vcpkg_mask_if_musl_libc) - vcpkg_get_set_musl_libc() - if(VCPKG_PLATFORM_MUSL_LIBC) - vcpkg_mask_musl_libc() - endif() -endfunction() - -# sets VCPKG_USE_SYSTEM_BINARIES_FLAG from VCPKG_PLATFORM_MUSL_LIBC and/or VCPKG_FORCE_SYSTEM_BINARIES -# vcpkg_set_use_system_binaries_flag([VCPKG_FORCE_SYSTEM_BINARIES_EXPLICIT]) -function(vcpkg_set_use_system_binaries_flag) - if(ARGV0 EQUAL "" OR NOT DEFINED ARGV0) - set(VCPKG_FORCE_SYSTEM_BINARIES_EXPLICIT ${VCPKG_FORCE_SYSTEM_BINARIES}) - else() - set(VCPKG_FORCE_SYSTEM_BINARIES_EXPLICIT ${ARGV0}) - endif() - - vcpkg_get_set_musl_libc() - - if(NOT WIN32 AND (VCPKG_FORCE_SYSTEM_BINARIES_EXPLICIT OR VCPKG_PLATFORM_MUSL_LIBC) ) - set(VCPKG_USE_SYSTEM_BINARIES_FLAG "--useSystemBinaries" PARENT_SCOPE) - # has to be propagated to all install calls - set(ENV{VCPKG_FORCE_SYSTEM_BINARIES} "1") - set(VCPKG_FORCE_SYSTEM_BINARIES ON CACHE BOOL "force vcpkg to use system binaries (possibly dont change)") - - message(STATUS "VCPKG: Requested use of system binaries! (e.g. cmake, curl, zip, tar, etc.)") - else() - set(VCPKG_USE_SYSTEM_BINARIES_FLAG "" PARENT_SCOPE) - endif() -endfunction() - - -# install package -function(vcpkg_add_package PKG_NAME) - # if(VCPKG_TARGET_TRIPLET STREQUAL "" OR NOT DEFINED VCPKG_TARGET_TRIPLET) - # vcpkg_make_set_triplet() - # endif() - set(VCPKG_TARGET_TRIPLET_FLAG "") - if(DEFINED VCPKG_TARGET_TRIPLET AND NOT VCPKG_TARGET_TRIPLET EQUAL "") - set(VCPKG_TARGET_TRIPLET_FLAG "--triplet=${VCPKG_TARGET_TRIPLET}") - endif() - - message(STATUS "VCPKG: fetching ${PKG_NAME} via vcpkg_add_package") - execute_process(COMMAND ${VCPKG_EXECUTABLE} ${VCPKG_TARGET_TRIPLET_FLAG} ${VCPKG_RECURSE_REBUILD_FLAG} --feature-flags=-manifests --disable-metrics install "${PKG_NAME}" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} RESULT_VARIABLE VCPKG_INSTALL_OK) - if(NOT VCPKG_INSTALL_OK EQUAL "0") - message(FATAL_ERROR "VCPKG: failed fetching ${PKG_NAME}! Did you call vcpkg_init(<...>)?") - else() - # add package to automatically generated manifest - vcpkg_manifest_generation_add_dependency("${PKG_NAME}") - endif() -endfunction() - - -# install packages from manifest in script mode -function(vcpkg_install_manifest) - if(VCPKG_TARGET_TRIPLET STREQUAL "" OR NOT DEFINED VCPKG_TARGET_TRIPLET) - vcpkg_make_set_triplet() - endif() - get_filename_component(VCPKG_EXECUTABLE_ABS ${VCPKG_EXECUTABLE} ABSOLUTE) - file(COPY "./vcpkg.json" DESTINATION "${VCPKG_PARENT_DIR}") - message(STATUS "VCPKG: install from manifest; using target triplet: ${VCPKG_TARGET_TRIPLET}") - execute_process(COMMAND ${VCPKG_EXECUTABLE_ABS} --triplet=${VCPKG_TARGET_TRIPLET} --feature-flags=manifests,versions --disable-metrics install WORKING_DIRECTORY "${VCPKG_PARENT_DIR}" RESULT_VARIABLE VCPKG_INSTALL_OK) - if(NOT VCPKG_INSTALL_OK EQUAL "0") - message(FATAL_ERROR "VCPKG: install from manifest failed") - endif() -endfunction() - -## manifest generation requires CMake > 3.19 -function(vcpkg_manifest_generation_update_cache VCPKG_GENERATED_MANIFEST) - string(REGEX REPLACE "\n" "" VCPKG_GENERATED_MANIFEST "${VCPKG_GENERATED_MANIFEST}") - set(VCPKG_GENERATED_MANIFEST "${VCPKG_GENERATED_MANIFEST}" CACHE STRING "template for automatically generated manifest by vcpkg-cmake-integration" FORCE) - mark_as_advanced(FORCE VCPKG_GENERATED_MANIFEST) -endfunction() - - -# build empty json manifest and register deferred call to finalize and write -function(vcpkg_manifest_generation_init) - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.19) - # init "empty" json and cache variable - set(VCPKG_GENERATED_MANIFEST "{}") - - # initialize dependencies as empty list - # first vcpkg_add_package will transform to object and install finalization handler - # transform to list in finalization step - string(JSON VCPKG_GENERATED_MANIFEST SET "${VCPKG_GENERATED_MANIFEST}" dependencies "[]") - string(JSON VCPKG_GENERATED_MANIFEST SET "${VCPKG_GENERATED_MANIFEST}" "$schema" "\"https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json\"") - string(JSON VCPKG_GENERATED_MANIFEST SET "${VCPKG_GENERATED_MANIFEST}" version "\"0.1.0-autogenerated\"") - - # write baseline commit - execute_process(COMMAND git log --pretty=format:'%H' -1 WORKING_DIRECTORY "${VCPKG_DIRECTORY}" OUTPUT_VARIABLE VCPKG_GENERATED_MANIFEST_BASELINE) - string(REPLACE "'" "" VCPKG_GENERATED_MANIFEST_BASELINE "${VCPKG_GENERATED_MANIFEST_BASELINE}") - string(JSON VCPKG_GENERATED_MANIFEST SET "${VCPKG_GENERATED_MANIFEST}" builtin-baseline "\"${VCPKG_GENERATED_MANIFEST_BASELINE}\"") - - vcpkg_manifest_generation_update_cache("${VCPKG_GENERATED_MANIFEST}") - - # will be initialized from vcpkg_add_package call - # # defer call to finalize manifest - # # needs to be called later as project variables are not set when initializing - # cmake_language(DEFER CALL vcpkg_manifest_generation_finalize) - endif() -endfunction() - -# add dependency to generated manifest -function(vcpkg_manifest_generation_add_dependency PKG_NAME) - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.19) - # extract features - string(REGEX MATCH "\\[.*\\]" PKG_FEATURES "${PKG_NAME}") - string(REPLACE "${PKG_FEATURES}" "" PKG_BASE_NAME "${PKG_NAME}") - # make comma separated list - string(REPLACE "[" "" PKG_FEATURES "${PKG_FEATURES}") - string(REPLACE "]" "" PKG_FEATURES "${PKG_FEATURES}") - string(REPLACE " " "" PKG_FEATURES "${PKG_FEATURES}") - # build cmake list by separating with ; - string(REPLACE "," ";" PKG_FEATURES "${PKG_FEATURES}") - - if(NOT PKG_FEATURES) - # set package name string only - set(PKG_DEPENDENCY_JSON "\"${PKG_BASE_NAME}\"") - else() - # build dependency object with features - set(PKG_DEPENDENCY_JSON "{}") - string(JSON PKG_DEPENDENCY_JSON SET "${PKG_DEPENDENCY_JSON}" name "\"${PKG_BASE_NAME}\"") - - set(FEATURE_LIST_JSON "[]") - foreach(FEATURE IN LISTS PKG_FEATURES) - if(FEATURE STREQUAL "core") - # set default feature option if special feature "core" is specified - string(JSON PKG_DEPENDENCY_JSON SET "${PKG_DEPENDENCY_JSON}" default-features "false") - else() - # add feature to list - string(JSON FEATURE_LIST_JSON_LEN LENGTH "${FEATURE_LIST_JSON}") - string(JSON FEATURE_LIST_JSON SET "${FEATURE_LIST_JSON}" ${FEATURE_LIST_JSON_LEN} "\"${FEATURE}\"") - endif() - endforeach() - - # build dependency object with feature list - string(JSON PKG_DEPENDENCY_JSON SET "${PKG_DEPENDENCY_JSON}" features "${FEATURE_LIST_JSON}") - endif() - - # add dependency to manifest - # reset to empty object to avoid collissions and track new packages - # defer (new) finalization call - string(JSON VCPKG_GENERATED_MANIFEST_DEPENDENCIES_TYPE TYPE "${VCPKG_GENERATED_MANIFEST}" dependencies) - if(VCPKG_GENERATED_MANIFEST_DEPENDENCIES_TYPE STREQUAL "ARRAY") - string(JSON VCPKG_GENERATED_MANIFEST SET "${VCPKG_GENERATED_MANIFEST}" dependencies "{}") - cmake_language(DEFER CALL vcpkg_manifest_generation_finalize) - endif() - string(JSON VCPKG_GENERATED_MANIFEST SET "${VCPKG_GENERATED_MANIFEST}" dependencies "${PKG_BASE_NAME}" "${PKG_DEPENDENCY_JSON}") - - vcpkg_manifest_generation_update_cache("${VCPKG_GENERATED_MANIFEST}") - endif() -endfunction() - - -# build empty json manifest and register deferred call to finalize and write -function(vcpkg_manifest_generation_finalize) - message(STATUS "VCPKG is creating the manifest") - if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.19) - # populate project information - string(REGEX REPLACE "[^a-z0-9\\.-]" "" VCPKG_GENERATED_MANIFEST_NAME "${PROJECT_NAME}") - string(TOLOWER VCPKG_GENERATED_MANIFEST_NAME "${VCPKG_GENERATED_MANIFEST_NAME}") - string(JSON VCPKG_GENERATED_MANIFEST SET "${VCPKG_GENERATED_MANIFEST}" name "\"${VCPKG_GENERATED_MANIFEST_NAME}\"") - if(NOT PROJECT_VERSION EQUAL "" AND DEFINED PROJECT_VERSION) - string(JSON VCPKG_GENERATED_MANIFEST SET "${VCPKG_GENERATED_MANIFEST}" version "\"${PROJECT_VERSION}\"") - endif() - - vcpkg_manifest_generation_update_cache("${VCPKG_GENERATED_MANIFEST}") - - # make list from dependency dictionary - # cache dependency object - string(JSON VCPKG_GENERATED_DEPENDENCY_OBJECT GET "${VCPKG_GENERATED_MANIFEST}" dependencies) - # initialize dependencies as list - string(JSON VCPKG_GENERATED_MANIFEST SET "${VCPKG_GENERATED_MANIFEST}" dependencies "[]") - - string(JSON VCPKG_GENERATED_DEPENDENCY_COUNT LENGTH "${VCPKG_GENERATED_DEPENDENCY_OBJECT}") - if(VCPKG_GENERATED_DEPENDENCY_COUNT GREATER 0) - # setup range stop for iteration - math(EXPR VCPKG_GENERATED_DEPENDENCY_LOOP_STOP "${VCPKG_GENERATED_DEPENDENCY_COUNT} - 1") - - # make list - foreach(DEPENDENCY_INDEX RANGE ${VCPKG_GENERATED_DEPENDENCY_LOOP_STOP}) - string(JSON DEPENDENCY_NAME MEMBER "${VCPKG_GENERATED_DEPENDENCY_OBJECT}" ${DEPENDENCY_INDEX}) - string(JSON DEPENDENCY_JSON GET "${VCPKG_GENERATED_DEPENDENCY_OBJECT}" "${DEPENDENCY_NAME}") - string(JSON DEPENDENCY_JSON_TYPE ERROR_VARIABLE DEPENDENCY_JSON_TYPE_ERROR_IGNORE TYPE "${DEPENDENCY_JSON}") - if(DEPENDENCY_JSON_TYPE STREQUAL "OBJECT") - string(JSON VCPKG_GENERATED_MANIFEST SET "${VCPKG_GENERATED_MANIFEST}" dependencies ${DEPENDENCY_INDEX} "${DEPENDENCY_JSON}") - else() - string(JSON VCPKG_GENERATED_MANIFEST SET "${VCPKG_GENERATED_MANIFEST}" dependencies ${DEPENDENCY_INDEX} "\"${DEPENDENCY_JSON}\"") - endif() - endforeach() - endif() - - message(STATUS "VCPKG auto-generated manifest (${CMAKE_CURRENT_BINARY_DIR}/vcpkg.json):\n${VCPKG_GENERATED_MANIFEST}") - file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/vcpkg.json" "${VCPKG_GENERATED_MANIFEST}") - endif() -endfunction() - - -# get vcpkg and configure toolchain -if(NOT VCPKG_NO_INIT) - vcpkg_init() -endif() \ No newline at end of file diff --git a/cmake/vcpkg_triplets/arm-osx.cmake b/cmake/vcpkg_triplets/arm-osx.cmake new file mode 100644 index 000000000..d2e41b8c6 --- /dev/null +++ b/cmake/vcpkg_triplets/arm-osx.cmake @@ -0,0 +1,24 @@ +set(VCPKG_TARGET_ARCHITECTURE arm64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) + +set(VCPKG_CMAKE_SYSTEM_NAME Darwin) +set(VCPKG_OSX_ARCHITECTURES arm64) + +# Dynamic libs are preferred, and some cases necessary, +# but VCPKG will not build python3 and its dependencies +# as dynamic libs +if(NOT PORT MATCHES ".*bzip2.*" + AND NOT PORT MATCHES ".*expat.*" + AND NOT PORT MATCHES ".*gettext.*" + AND NOT PORT MATCHES ".*ffi.*" + AND NOT PORT MATCHES ".*iconv.*" + AND NOT PORT MATCHES ".*zma.*" + AND NOT PORT MATCHES ".*openssl.*" + AND NOT PORT MATCHES ".*sqlite.*" + AND NOT PORT MATCHES ".*zlib.*") + set(VCPKG_LIBRARY_LINKAGE dynamic) + message("PORT DYNAMIC " ${PORT}) +else() + message("PORT STATIC " ${PORT}) +endif() diff --git a/cmake/vcpkg_triplets/x64-osx.cmake b/cmake/vcpkg_triplets/x64-osx.cmake new file mode 100644 index 000000000..43e946fcf --- /dev/null +++ b/cmake/vcpkg_triplets/x64-osx.cmake @@ -0,0 +1,24 @@ +set(VCPKG_TARGET_ARCHITECTURE x64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) + +set(VCPKG_CMAKE_SYSTEM_NAME Darwin) +set(VCPKG_OSX_ARCHITECTURES x86_64) + +# Dynamic libs are preferred, and some cases necessary, +# but VCPKG will not build python3 and its dependencies +# as dynamic libs +if(NOT PORT MATCHES ".*bzip2.*" + AND NOT PORT MATCHES ".*expat.*" + AND NOT PORT MATCHES ".*gettext.*" + AND NOT PORT MATCHES ".*ffi.*" + AND NOT PORT MATCHES ".*iconv.*" + AND NOT PORT MATCHES ".*zma.*" + AND NOT PORT MATCHES ".*openssl.*" + AND NOT PORT MATCHES ".*sqlite.*" + AND NOT PORT MATCHES ".*zlib.*") + set(VCPKG_LIBRARY_LINKAGE dynamic) + message("PORT DYNAMIC " ${PORT}) +else() + message("PORT STATIC " ${PORT}) +endif() diff --git a/cmake/vcpkg_triplets/x64-xstudio-linux.cmake b/cmake/vcpkg_triplets/x64-xstudio-linux.cmake new file mode 100644 index 000000000..bb570cff0 --- /dev/null +++ b/cmake/vcpkg_triplets/x64-xstudio-linux.cmake @@ -0,0 +1,6 @@ +set(VCPKG_TARGET_ARCHITECTURE x64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE dynamic) + +set(VCPKG_CMAKE_SYSTEM_NAME Linux) + diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index 799913f96..a56d20a7f 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -51,7 +51,7 @@ add_custom_command(OUTPUT ${DOXYGEN_INDEX_FILE} add_custom_target(Doxygen ALL DEPENDS ${DOXYGEN_INDEX_FILE}) -add_dependencies(Doxygen python_module) +# add_dependencies(Doxygen python_module) set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}) set(SPHINX_BUILD ${CMAKE_CURRENT_BINARY_DIR}/docs/sphinx) @@ -80,6 +80,10 @@ add_custom_target(UserDocs ALL WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMENT "Generating user documentation with Sphinx") +if (APPLE) +install(DIRECTORY ${SPHINX_BUILD}/ DESTINATION ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/docs) +else() install(DIRECTORY ${SPHINX_BUILD}/ DESTINATION share/xstudio/docs) +endif() # https://devblogs.microsoft.com/cppblog/clear-functional-c-documentation-with-sphinx-breathe-doxygen-cmake/ diff --git a/docs/build_guides/centos_7.md b/docs/build_guides/centos_7.md index 32fc7a494..074b33033 100644 --- a/docs/build_guides/centos_7.md +++ b/docs/build_guides/centos_7.md @@ -3,6 +3,11 @@ [Download](https://www.centos.org/download/ "Download") **Default Mesa drivers will not work, as they are to old, NVidia/AMD/Intel custom drivers should be fine. ** + +### Note (May 2025) + +These docs are not accurate and are to be updated soon. Note that xSTUDIO now requires Qt6.5.3, where these notes incorrectly install qt5. You can install Qt6.5.3 using [these instructions](downloading_qt.md) instead. + ### Distro installs sudo yum install -y centos-release-scl sudo yum install -y devtoolset-9 diff --git a/docs/build_guides/downloading_qt.md b/docs/build_guides/downloading_qt.md new file mode 100644 index 000000000..f5832be31 --- /dev/null +++ b/docs/build_guides/downloading_qt.md @@ -0,0 +1,27 @@ +## Downloading Qt SDK + +xSTUDIO's UI is built with the Qt cross-platform GUI development libraries. The Qt SDK is a major dependency that is required to build xSTUDIO but fortunately it is freely available for public use under the GPL license. + +### Running the Qt installer + +First you need to download the [Qt Installer](https://www.qt.io/download-qt-installer). + +Run the installer app. Before you can proceed you must register with Qt if you aren't already and then log-in with your credentials when the installer requests you to do so. Agree to the terms and conditions and hit 'Next'. You are then presented with a panel with options about which developer tools to download. ***Ensure that Custom Installation is checked and all other options are not checked.*** . Choose a destination location on your filesystem for the Qt modules to be installed to (Where it says 'Please specify the directory where QT will be installed'). Make a note of this location as you will need it later. Now hit 'Next'. + +![Qt Installer](qt_inst1.jpg) + +### Select Qt 6.5.3 components + +Now you must select the correct version of Qt to download. The required version is **6.5.3**. Epand the 'Qt' item in the list, then expand the Qt 6.5.3 below that. You only need to check the following option within the list under 6.5.3, depending on your platform: + +* Apple: **macOS** +* Windows: **MSVC 2019 64-bit** +* Linux: **gcc_64** + +You must also expand the 'Addition Libraries' item, and select **'Qt Image Formats'** from that list. + +The following screengrab was done when downloading onto an Apple Mac machine. If you're on Windows it will look slightly different and you must check 'MSVC 2019 64-bit'. + +![Qt Installer](qt_inst2.jpg) + +Now You can hit the Next button and continue. Agree to the license terms and continue to the final page where you must hit the **Install** button. Note that the Qt components that we need are more than 1Gb in size, so may take some time to download depending on you connection and geographical location. \ No newline at end of file diff --git a/docs/build_guides/linux_generic.md b/docs/build_guides/linux_generic.md new file mode 100644 index 000000000..283695f43 --- /dev/null +++ b/docs/build_guides/linux_generic.md @@ -0,0 +1,57 @@ +## Linux Generic Guide + +xSTUDIO can be built in just a few steps on many Linux distros by employing Microsoft's 'vcpkg' open source package build system. Note that the differences between gcc compiler versions can cause some issues where newer compiler versions are more or less strict about syntax. The developers of xSTUDIO are unable to ensure full compatibility with all compiler variations at this point and it's up to individuals following these guides to work through any issues encountered. Please do submit pull requests if you hit build problems and find solutions! + +### Base dependencies + +We assume that you have some knowledge of development on Linux platforms and have git, gcc & cmake installed on your system. + +### Download and install Qt 6.5.3 SDK + +Follow [these instructions](downloading_qt.md) - ensuring that you download the SDK for Linux platforms (rather than the MacOS platform used as an example in the linked guide). + +### Download the VCPKG repo + +To build xSTUDIO we need a number of other open source software packages. We use the VCPKG package manager to do this. All that we need to do is download the repo and run the bootstrap script before we build xstudio. + + git clone https://github.com/microsoft/vcpkg.git + ./vcpkg/bootstrap-vcpkg.sh + +### Download the xSTUDIO repo + +Download from github in the usual manner. Enter the root folder of the repo and ensure you are building from the correct branch. Example terminal commands might be as follows, to build from the develop branch: + + git clone https://github.com/AcademySoftwareFoundation/xstudio.git + cd xstudio + git checkout develop + +You must run these commands to add the OpenTimelineIO submodule to the tree + + git submodule init + git submodule update + +### Modify the CMakePresets.json file + +Open the CMakePresets.json file (which is in the root of the xstudio repo) in a text editor. You must look for the entry "Qt6_DIR" and modify the value that follows it to point to your installation of the Qt SDK. Specifically, you need to point to a directory named 'Qt6' which is in a directory named 'cmake', which is in a directory named 'lib'. For example, if user Mary Jane downloaded Qt into her home folder the entry should look like this: + + "Qt6_DIR": "/home/maryjane/Qt/6.5.3/gcc_64/lib/cmake/Qt6", + +### Build xSTUDIO + +First, we configure for building. Note that this cmake command ***may take several hours to complete*** the first time it is run, though subsequently it will take a few seconds. This is because xSTUDIO's dependencies (particularly ffmpeg) take a long time to download and build from the source code, which is what VCPKG is doing. + + cmake -B build --preset LinuxRelease + +When this has finished, you can build xSTUDIO with this command (in this case, the --parallel flag is set for a machine with 16 cores as an example). + + cmake --build build --parallel 16 + +### Running xSTUDIO in a dev environment + +If the compilation is successfull you will find the xstudio app in ./build/bin/xstudio.bin. To enable the python API, you will need to modify your PYTHONPATH evnironment variable like this, or something similar: + + export PYTHONPATH=$PYTHONPATH:./build/bin/python/lib/python3.10/site-packages + +### Installing xSTUDIO + +Correct packaging and installation of xstudio and its dependencies across various Linux distros is a problem we are still working on! For now, it is up to individual developers to do an effective installation on their system. You can try running 'make install' from the 'build' folder. Use -DCMAKE_BUILD_PREFIX={path} to set a test installation location diff --git a/docs/build_guides/macos.md b/docs/build_guides/macos.md new file mode 100644 index 000000000..65a4d13ac --- /dev/null +++ b/docs/build_guides/macos.md @@ -0,0 +1,73 @@ +## MacOS (Intel/ARM) + +To build xSTUDIO on MacOS you must download and install some build dependencies. Once these are in place xSTUDIO can be built with just two commands. Please read these notes carefully, particulary if you are not experienced in software development. + +### Download Apple XCode + +From the App store, locate and download XCode. Note that it is a large package and may take some time to download. Once downloaded, launch XCode and agree to the license terms & conditions. + +### Download CMake + +CMake is the build system used to compile xSTUDIO. The homepage is [here](https://cmake.org). These build instructions have been tested with CMake version 3.31.6, which you can download directly from [here](https://github.com/Kitware/CMake/releases/download/v3.31.6/cmake-3.31.6-macos-universal.dmg). + +Once downloaded and installed, run CMake and go to the 'Tools' menu and select 'How to install for command line use'. Follow the instructions there so that we can use CMake from a terminal. + +### Install 'homebrew' package manager + +Some of xSTUDIO's dependencies require 'homebrew', the MacOS open source software package manager. The homepage is [here](https://brew.sh) but you can simply run this command in a terminal + + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + +We require 3 packages to be installed to proceed. Run these commands in a terminal: + + brew install pkg-config + brew install nasm + brew install autoconf + +### Download and install Qt 6.5.3 SDK + +Follow [these instructions](downloading_qt.md) + +### Download the VCPKG repo + +To build xSTUDIO we need a number of other open source software packages. We use the VCPKG package manager to do this. All that we need to do is download the repo and run the bootstrap script before we build xstudio. + + git clone https://github.com/microsoft/vcpkg.git + ./vcpkg/bootstrap-vcpkg.sh + +### Download the xSTUDIO repo + +Download from github in the usual manner. Enter the root folder of the repo and ensure you are building from the correct branch. Example terminal commands might be as follows, to build from the develop branch: + + git clone https://github.com/AcademySoftwareFoundation/xstudio.git + cd xstudio + git checkout develop + +You must run these commands to add the OpenTimelineIO submodule to the tree + + git submodule init + git submodule update + +### Modify the CMakePresets.json file + +Open the CMakePresets.json file (which is in the root of the xstudio repo) in a text editor. You must look for the entry "Qt6_DIR" and modify the value that follows it to point to your installation of the Qt SDK. Specifically, you need to point to a directory named 'Qt6' which is in a directory named 'cmake', which is in a directory named 'lib'. For example, on MacOS where user Mary Jane downloaded Qt into her home folder the entry should look like this: + + "Qt6_DIR": "/Users/maryjane/Qt/6.5.3/macos/lib/cmake/Qt6", + +### Build xSTUDIO + +Run the appropriate command for your platform (whether you have an older Intel or a newer Apple Silicon machine) to set-up for building. Note that this cmake command ***may take several hours to complete***. This is because xSTUDIO's dependencies (particularly ffmpeg) take a long time to download and build from the source code, which is what VCPKG is doing. + +**Apple Silicon (ARM) Machines:** + + cmake -B build --preset MacOSRelease + +**Intel Machines:** + + cmake -B build --preset MacOSIntelRelease + +When this has finished, you can build xSTUDIO with this command. + + cmake --build build --parallel 16 --target install + +If the build is successful, you should have an application bundle in the 'build' folder called 'xSTUDIO.app'. This can be drag & dropped into your applications folder, desktop and dock as for any other application. \ No newline at end of file diff --git a/docs/build_guides/media/images/Qt5_select_components.png b/docs/build_guides/media/images/Qt5_select_components.png deleted file mode 100644 index c6c81078c..000000000 Binary files a/docs/build_guides/media/images/Qt5_select_components.png and /dev/null differ diff --git a/docs/build_guides/media/images/setup_Qt5.png b/docs/build_guides/media/images/setup_Qt5.png deleted file mode 100644 index 71e1d6aae..000000000 Binary files a/docs/build_guides/media/images/setup_Qt5.png and /dev/null differ diff --git a/docs/build_guides/media/images/setup_ffmpeg.png b/docs/build_guides/media/images/setup_ffmpeg.png deleted file mode 100644 index a7fb0cf39..000000000 Binary files a/docs/build_guides/media/images/setup_ffmpeg.png and /dev/null differ diff --git a/docs/build_guides/qt_inst1.jpg b/docs/build_guides/qt_inst1.jpg new file mode 100644 index 000000000..80067410b Binary files /dev/null and b/docs/build_guides/qt_inst1.jpg differ diff --git a/docs/build_guides/qt_inst2.jpg b/docs/build_guides/qt_inst2.jpg new file mode 100644 index 000000000..c77dbf5ab Binary files /dev/null and b/docs/build_guides/qt_inst2.jpg differ diff --git a/docs/build_guides/rocky_linux_9_1.md b/docs/build_guides/rocky_linux_9_1.md index 09f9a21e4..4a343b766 100644 --- a/docs/build_guides/rocky_linux_9_1.md +++ b/docs/build_guides/rocky_linux_9_1.md @@ -1,6 +1,10 @@ ## Rocky Linux 9.1 [Download](https://rockylinux.org/download "Download") +### Note (May 2025) + +These docs are not accurate and are to be updated soon. Note that xSTUDIO now requires Qt6.5.3, where these notes incorrectly install qt5. You can install Qt6.5.3 using [these instructions](downloading_qt.md) instead. + ### Distro installs sudo dnf config-manager --set-enabled crb sudo dnf update diff --git a/docs/build_guides/ubuntu_22_04.md b/docs/build_guides/ubuntu_22_04.md index a81486647..ee21963c2 100644 --- a/docs/build_guides/ubuntu_22_04.md +++ b/docs/build_guides/ubuntu_22_04.md @@ -1,6 +1,10 @@ ## Ubuntu 22.04 LTS [Download](https://releases.ubuntu.com/22.04 "Download") +### Note (May 2025) + +Note that xSTUDIO now requires Qt6.5.3. You can install Qt6.5.3 using [these instructions](downloading_qt.md) instead. + ### Distro installs sudo apt install build-essential cmake git python3-pip sudo apt install doxygen sphinx-common sphinx-rtd-theme-common python3-breathe @@ -9,12 +13,8 @@ sudo apt install libglu1-mesa-dev freeglut3-dev mesa-common-dev libglew-dev libfreetype-dev sudo apt install libjpeg-dev libpulse-dev nlohmann-json3-dev sudo apt install yasm nasm libfdk-aac-dev libfdk-aac2 libmp3lame-dev libopus-dev libvpx-dev libx265-dev libx264-dev - sudo apt install qttools5-dev qtbase5-dev qt5-qmake qtdeclarative5-dev qtquickcontrols2-5-dev - sudo apt install qml-module-qtquick* qml-module-qt-labs-* - pip install sphinx_rtd_theme - ### Local installs #### OpenEXR git clone https://github.com/AcademySoftwareFoundation/openexr.git @@ -29,9 +29,9 @@ #### ActorFramework - wget https://github.com/actor-framework/actor-framework/archive/refs/tags/0.18.4.tar.gz - tar -xf 0.18.4.tar.gz - cd actor-framework-0.18.4 + git clone https://github.com/actor-framework/actor-framework + cd actor-framework + git checkout 1.0.2 ./configure cd build make -j $JOBS @@ -39,22 +39,10 @@ cd ../.. -#### OpenTimelineIO - git clone https://github.com/AcademySoftwareFoundation/OpenTimelineIO.git - cd OpenTimelineIO - git checkout cxx17 - mkdir build - cd build - cmake -DOTIO_PYTHON_INSTALL=ON -DOTIO_DEPENDENCIES_INSTALL=OFF -DOTIO_FIND_IMATH=ON .. - make -j $JOBS - sudo make install - cd ../.. - - #### OCIO2 - wget https://github.com/AcademySoftwareFoundation/OpenColorIO/archive/refs/tags/v2.2.0.tar.gz - tar -xf v2.2.0.tar.gz - cd OpenColorIO-2.2.0/ + wget https://github.com/AcademySoftwareFoundation/OpenColorIO/archive/refs/tags/v2.2.1.tar.gz + tar -xf v2.2.1.tar.gz + cd OpenColorIO-2.2.1/ mkdir build cd build cmake -DOCIO_BUILD_APPS=OFF -DOCIO_BUILD_TESTS=OFF -DOCIO_BUILD_GPU_TESTS=OFF ../ @@ -75,15 +63,31 @@ ### xStudio + +You will need to set an environment variable to tell cmake where the Qt6 libraries are located. For example, if Qt6 was installed to **/home/maryjane/Qt6/** then you will need to run this command in your terminal: + + export Qt6_DIR=/home/maryjane/Qt6/6.5.3/gcc_64/lib/cmake/Qt6 + +Now we continue the build commands: + export PKG_CONFIG_PATH=/usr/local/lib64/pkgconfig:/usr/local/lib64/pkgconfig cd xstudio + git submodule init + git submodule update mkdir build cd build - cmake .. -DBUILD_DOCS=Off + cmake .. -DBUILD_DOCS=Off -DOTIO_SUBMODULE=On -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=1 + make -j $JOBS - export QV4_FORCE_INTERPRETER=1 +To run xstudio from your dev environment: + export LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64 export PYTHONPATH=./bin/python/lib/python3.10/site-packages:/home/xstudio/.local/lib/python3.10/site-packages: ./bin/xstudio.bin + +Or you can install to your system and then run. + + sudo make install + xstudio \ No newline at end of file diff --git a/docs/build_guides/windows.md b/docs/build_guides/windows.md index a578812f1..f721ee18e 100644 --- a/docs/build_guides/windows.md +++ b/docs/build_guides/windows.md @@ -1,47 +1,62 @@ # Windows 10/11 -* Enable long path support (if you haven't already) - * Find instructions here: [Maximum File Path Limitation](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry) -* Install git - * Get it here: [Git Download](https://git-scm.com/download/win) -* Install MS Visual Studio 2022 - * Get it here: [Microsoft Visual Studio](https://visualstudio.microsoft.com/vs/) - * Ensure CMake tools for Windows is included on install. [CMake projects in Visual Studio](https://learn.microsoft.com/en-us/cpp/build/cmake-projects-in-visual-studio?view=msvc-170#installation) - * Restart your machine after Visual Studio finishes installing -* Install Ninja (Not required, but highly recommended) - * Find Ninja here [Ninja Website](https://ninja-build.org/) -* Install Qt 5.15 - * Download the online installer here: [qt.io/download-qt-installer](https://www.qt.io/download-qt-installer-oss) - * During installation select the following components: ![Qt Components](/docs/build_guides/media/images/Qt5_select_components.png) - * Qt5.15.2 - * MSVC 2019 64-bit - * Developer and Designer Tools - * Qt Creator 10.0.1 - * Qt Creator 10.0.1 CDB Debugger Support - * Debugging Tools for Windows - * Note: This can take some time; consider manually [setting a mirror if slow](https://wiki.qt.io/Online_Installer_4.x#Selecting_a_mirror_for_opensource). - -* Clone this project to your local drive. Tips to consider: - * The path should not have spaces in it. - * Ideally, keep the path short and uncomplicated (IE `D:\xStudio`) - * Ensure your drive has a decent amount of space free (at least ~40GB) - * The rest of this document will refer to this location as ${CLONE_ROOT} - -* Before loading the project in Visual Studio, consider modifying ${CLONE_ROOT}/CMakePresets.json - * Edit the `Qt5_DIR` if you did not install in C:\Qt - * Edit the `CMAKE_INSTALL_PREFIX` to your desired output location - * This should be outside the build directory in a location where you have permissions to write to. - -* Open VisualStudio 2022 - * Use Open Folder to point at the ${CLONE_ROOT} - * Visual Studio should start configuring the project, including downloading dependencies via VCPKG - * This process will likely take awhile as it obtains the required dependences. - * Once configured, you can switch to the Solution Explorer's solution view to view CMake targets. - * Set your target build to `Release` or `ReleaseWithDeb` - * Double-click `CMake Targets View` - * Right-click on `xStudio Project` and select `Build All` - * Once built, right-click on `xStudio Project` and select `Install` -* If the build succeeds, navigate to your ${CMAKE_INSTALL_PREFIX}/bin and double-click the `xstudio.exe` to run xStudio. +### Enable long path support (if you haven't already) + +Find instructions here: [Maximum File Path Limitation](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry) + +### Install git + +Get it here: [Git Download](https://git-scm.com/download/win) + +### Install MS Visual Studio 2022 + +Get it here: [Microsoft Visual Studio](https://visualstudio.microsoft.com/vs/) +Ensure CMake tools for Windows is included on install. [CMake projects in Visual Studio](https://learn.microsoft.com/en-us/cpp/build/cmake-projects-in-visual-studio?view=msvc-170#installation) +Restart your machine after Visual Studio finishes installing. + +### Download and install Qt 6.5.3 SDK + +Follow [these instructions](downloading_qt.md) + +### Download the VCPKG repo + +Start a Windows Powershell to continue these instructions, where you must run a handfull of powershell commands to build xSTUDIO. Windows Powershell is pre-installed, to start it type Powershell into the Search bar in the Start menu. You will need a location to build xSTUDIO from. We recommend making a folder in your home space, called something like 'dev', as follows: + + mkdir dev + cd dev + +To build xSTUDIO we need a number of other open source software packages. We use the VCPKG package manager to do this. All that we need to do is download the repo and run the bootstrap script before we build xstudio. Run these commands in the Powershell: + + git clone https://github.com/microsoft/vcpkg.git + ./vcpkg/bootstrap-vcpkg.bat + +### Download the xSTUDIO repo + +Download from github in the usual manner. Enter the root folder of the repo and ensure you are building from the correct branch. Example terminal commands might be as follows, to build from the develop branch: + + git clone https://github.com/AcademySoftwareFoundation/xstudio.git + cd xstudio + git checkout develop + +You must run these commands to add the OpenTimelineIO submodule to the tree + + git submodule init + git submodule update + +### Build xSTUDIO + +Run the first cmake command to set-up for building. Note that this cmake command ***may take several hours to complete***. This is because xSTUDIO's dependencies (particularly ffmpeg) take a long time to download and build from the source code, which is what VCPKG is doing. + +First, you may need to find the path to the 'cmake.exe' tool that is part of the VisualStudio install. Substitute as appropriate into the following commands as appropriate. + + 'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe' -B build --preset WinRelease + +When this has finished, you can build xSTUDIO with this command. Note the value after --parallel: change this number to match the number of cores your machine has for best build times. + + 'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe' --build build --parallel 16 --target PACKAGE --config Release + +If the build is successful, you should have an exectuable in the 'build' folder called something like 'xSTUDIO-1.0.0-win64.exe'. This can be executed to start the xSTUDIO installer. + # Questions? diff --git a/docs/conf.py b/docs/conf.py index 811eeb66a..fde0bd85e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -75,9 +75,9 @@ # built documents. # # The short X.Y version. -version = '0.11.2' +version = '0.0.0' # The full version, including alpha/beta/rc tags. -release = '0.11.2' +release = '0.0.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/make.bat b/docs/make.bat index b59201f1a..b20133eb8 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,263 +1,263 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - echo. coverage to run coverage check of the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -REM Check if sphinx-build is available and fallback to Python version if any -%SPHINXBUILD% 2> nul -if errorlevel 9009 goto sphinx_python -goto sphinx_ok - -:sphinx_python - -set SPHINXBUILD=python -m sphinx.__init__ -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -:sphinx_ok - - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\XStudio.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\XStudio.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "coverage" ( - %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage - if errorlevel 1 exit /b 1 - echo. - echo.Testing of coverage in the sources finished, look at the ^ -results in %BUILDDIR%/coverage/python.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 2> nul +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\XStudio.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\XStudio.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/extern/cpp-colors/.travis.yml b/extern/cpp-colors/.travis.yml new file mode 100644 index 000000000..1e19578bd --- /dev/null +++ b/extern/cpp-colors/.travis.yml @@ -0,0 +1,77 @@ +os: + - linux + +language: cpp + +compiler: + - gcc + - clang + +before_install: + # g++4.8.1 + - if [ "$CXX" == "g++" ]; then sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test; fi + + # clang 3.4 + - if [ "$CXX" == "clang++" ]; then sudo add-apt-repository -y ppa:h-rayflood/llvm; fi + + - sudo apt-get update -qq + +install: + # utils + - sudo apt-get install -y curl unzip cmake + + # g++4.8 + - if [ "$CXX" = "g++" ]; then sudo apt-get install -qq gcc-4.8 g++-4.8; fi + - if [ "$CXX" = "g++" ]; then sudo rm /usr/bin/gcc /usr/bin/g++; fi + - if [ "$CXX" = "g++" ]; then sudo ln -s /usr/bin/gcc-4.8 /usr/bin/gcc; fi + - if [ "$CXX" = "g++" ]; then sudo ln -s /usr/bin/g++-4.8 /usr/bin/g++; fi + + # clang 3.4 + - if [ "$CXX" == "clang++" ]; then sudo apt-get install --allow-unauthenticated -qq clang-3.4; fi + - if [ "$CXX" == "clang++" ]; then export CXXFLAGS="-std=c++0x -stdlib=libc++"; fi + + # libc++ + - if [ "$CXX" == "clang++" ]; then svn co --quiet http://llvm.org/svn/llvm-project/libcxx/trunk libcxx; fi + + - if [ "$CXX" == "clang++" ]; then pushd .; fi + - if [ "$CXX" == "clang++" ]; then cd libcxx/lib && bash buildit; fi + - if [ "$CXX" == "clang++" ]; then sudo cp ./libc++.so.1.0 /usr/lib/; fi + - if [ "$CXX" == "clang++" ]; then sudo mkdir /usr/include/c++/v1; fi + - if [ "$CXX" == "clang++" ]; then cd .. && sudo cp -r include/* /usr/include/c++/v1/; fi + - if [ "$CXX" == "clang++" ]; then cd /usr/lib && sudo ln -sf libc++.so.1.0 libc++.so; fi + - if [ "$CXX" == "clang++" ]; then sudo ln -sf libc++.so.1.0 libc++.so.1 && cd $cwd; fi + - if [ "$CXX" == "clang++" ]; then popd; fi + + # libc++abi + - if [ "$CXX" == "clang++" ]; then pushd .; fi + - if [ "$CXX" == "clang++" ]; then svn co --quiet http://llvm.org/svn/llvm-project/libcxxabi/trunk libcxxabi; fi + - if [ "$CXX" == "clang++" ]; then cd libcxxabi/lib && bash buildit; fi + - if [ "$CXX" == "clang++" ]; then sudo cp ./libc++abi.so.1.0 /usr/lib/; fi + - if [ "$CXX" == "clang++" ]; then cd .. && sudo cp -r include/* /usr/include/c++/v1/; fi + - if [ "$CXX" == "clang++" ]; then cd /usr/lib && sudo ln -sf libc++abi.so.1.0 libc++abi.so; fi + - if [ "$CXX" == "clang++" ]; then sudo ln -sf libc++abi.so.1.0 libc++abi.so.1 && cd $cwd; fi + - if [ "$CXX" == "clang++" ]; then popd; fi + + - if [ "$CXX" == "clang++" ]; then export LDFLAGS="-stdlib=libc++ -lc++abi"; fi + + # Boost + - sudo apt-get install -y libboost-all-dev + + # GTest + - curl -OL https://googletest.googlecode.com/files/gtest-1.7.0.zip + - unzip gtest-1.7.0.zip + - cd gtest-1.7.0 + - cmake . + #- if [ "$CXX" = "g++" ]; then cmake .; fi + #- if [ "$CXX" == "clang++" ]; then CC=clang CXX=clang++ cmake .; fi + - sudo cmake --build . + - sudo cp -R ./include /usr + - sudo mv libg* /usr/local/lib/ + - cd - + +script: + - $CXX --version + - cd build + - cmake .. + - make + - ./bin/run_tests \ No newline at end of file diff --git a/extern/cpp-colors/CMakeLists.txt b/extern/cpp-colors/CMakeLists.txt new file mode 100644 index 000000000..c99c5f5b9 --- /dev/null +++ b/extern/cpp-colors/CMakeLists.txt @@ -0,0 +1,62 @@ +cmake_minimum_required(VERSION 2.8.4) +project(cpp_colors) + +enable_testing() + +set(WARNINGS) + +# Platform +if(UNIX) + option(BUILD_TESTS "Build tests." ON) + option(BUILD_EXAMPLES "Build examples." ON) +elseif(WIN32) + option(BUILD_TESTS "Build tests." ON) + option(BUILD_EXAMPLES "Build examples." ON) + + add_definitions(-DUNICODE -D_UNICODE) +else() + message(FATAL_ERROR "-- Unsupported Build Platform.") +endif() + +# Compiler +if(${CMAKE_CXX_COMPILER_ID} STREQUAL Clang) + message("-- Setting clang options") + + set(WARNINGS "-Wall -Wextra") + + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -fno-strict-aliasing") + set(STRICT_CXX_FLAGS ${WARNINGS} "-Werror -pedantic") +elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL GNU) + message("-- Setting gcc options") + + set(WARNINGS "-Wall -Wextra") + + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -fno-strict-aliasing") + set(STRICT_CXX_FLAGS ${WARNINGS} "-Werror -pedantic") +elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC) + message("-- Setting msvc options") +else() + message("-- Unknown compiler, success is doubtful.") +endif() + +# Reconfigure final output directory +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin) +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin) + +include_directories(include) + +if(BUILD_TESTS) + add_subdirectory(test) +endif() + +if(BUILD_EXAMPLES) + add_subdirectory(examples) +endif() + +# Clean the entire build directory +add_custom_target(clean-all + COMMAND ${CMAKE_BUILD_TOOL} clean + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_BINARY_DIR}/../clean-all.cmake +) diff --git a/extern/cpp-colors/LICENSE b/extern/cpp-colors/LICENSE new file mode 100644 index 000000000..b06e92275 --- /dev/null +++ b/extern/cpp-colors/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Grigoriy Chudnov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/extern/cpp-colors/README.md b/extern/cpp-colors/README.md new file mode 100644 index 000000000..5e90ce88e --- /dev/null +++ b/extern/cpp-colors/README.md @@ -0,0 +1,75 @@ +# cpp-colors [![Build Status](https://travis-ci.org/gchudnov/cpp-colors.svg?branch=master)](https://travis-ci.org/gchudnov/cpp-colors) + +A C++ header-only color format & conversion library. +* Parsing and formatting colors: #aarrggbb, argb(a, r, g, b) & rgb(r, g, b) +* Checking whether the given color is light or dark. +* parsing named colors: x11, wpf & .net naming schemes +* conversion between: bgr24, bgra32, bgr32, bgr565, bgr555, bgra5551, rgba32 and rgb32 pixel formats + + +### Directories + +* **build** - directory for tests & examples +* **examples** - examples source code +* **include** - the source code of the library +* **test** - test source code + +### More information + +* [Wiki](https://github.com/gchudnov/cpp-colors/wiki) + +### Tested compilers + +* Linux (x86/64) + * GCC 4.8, Boost 1.54 + * Clang 3.4, Boost 1.54 +* Windows (x86/64) + * MSVC 14, Boost 1.57 + +### Installation + +This is a header only library, in order to use it make the cpp-colors-`include` directory available to your project and include the header file in your source code: + +```c++ +#include "cpp-colors/colors.h" +``` + +### Building Tests & Examples + +This project tests and examples use the Cross-platform Make ([CMake](http://www.cmake.org/)) build system. +Tests depend on [Google Test Framework](https://code.google.com/p/googletest/). gtest-1.7.0 is recommended. + +#### Linux + +The recommended way is to create 'out of source' build: + +```bash +cd cpp-colors/build +cmake .. +make +``` + +#### Windows + +Visual Studio: + + Follow the directions at the link for running CMake on Windows: + http://www.cmake.org/runningcmake/ + + NOTE: Select the "build" folder as the location to build the binaries. + + +### Deleting all files Make & CMake created + +``` +make clean-all +``` + +### Contact + +[Grigoriy Chudnov] (mailto:g.chudnov@gmail.com) + +### License + +Distributed under the [The MIT License (MIT)](https://github.com/gchudnov/cpp-colors/blob/master/LICENSE). + diff --git a/extern/cpp-colors/clean-all.cmake b/extern/cpp-colors/clean-all.cmake new file mode 100644 index 000000000..d983fc342 --- /dev/null +++ b/extern/cpp-colors/clean-all.cmake @@ -0,0 +1,17 @@ +set(cmake_generated + ${CMAKE_BINARY_DIR}/bin + ${CMAKE_BINARY_DIR}/CMakeCache.txt + ${CMAKE_BINARY_DIR}/CMakeFiles + ${CMAKE_BINARY_DIR}/CTestTestfile.cmake + ${CMAKE_BINARY_DIR}/cmake_install.cmake + ${CMAKE_BINARY_DIR}/examples + ${CMAKE_BINARY_DIR}/Makefile + ${CMAKE_BINARY_DIR}/test + ${CMAKE_BINARY_DIR}/Testing + ) + +foreach(file ${cmake_generated}) + if (EXISTS ${file}) + file(REMOVE_RECURSE ${file}) + endif() +endforeach(file) \ No newline at end of file diff --git a/extern/cpp-colors/examples/CMakeLists.txt b/extern/cpp-colors/examples/CMakeLists.txt new file mode 100644 index 000000000..ae4f1fb3f --- /dev/null +++ b/extern/cpp-colors/examples/CMakeLists.txt @@ -0,0 +1 @@ +add_executable(color-conv color-conversion.cpp) diff --git a/extern/cpp-colors/examples/color-conversion.cpp b/extern/cpp-colors/examples/color-conversion.cpp new file mode 100644 index 000000000..8d34d337e --- /dev/null +++ b/extern/cpp-colors/examples/color-conversion.cpp @@ -0,0 +1,27 @@ +#include +#include +#include +#include +#include "cpp-colors/colors.h" + +using namespace std; +using namespace colors; + +void print_color_value(const std::string& name, uint32_t value) { + cout << hex << setfill(' ') << setw(10) << name << " = " << setw(8) << setfill('0') << value << endl; +} + +int main(int argc, char* argv[]) { + + color c(wpf_colors::blue()); + + print_color_value("bgra32", c.value()); + print_color_value("bgr32", c.value()); + print_color_value("rgba32", c.value()); + print_color_value("rgb32", c.value()); + print_color_value("bgr24", c.value()); + print_color_value("bgr565", c.value()); + print_color_value("bgr555", c.value()); + + return EXIT_SUCCESS; +} diff --git a/extern/cpp-colors/include/cpp-colors/color.h b/extern/cpp-colors/include/cpp-colors/color.h new file mode 100644 index 000000000..5103f0841 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/color.h @@ -0,0 +1,433 @@ +#pragma once + +#include +#include +#include +#include +#include "pixel_format.h" + + +namespace colors { + + namespace { + + // BITS-PER-CHANNEL conversion + + // SrcBPC < DstBPC + struct less_converter { + template + static T run(T value) { + if (value == 0) { + return static_cast(0); + } + else if (value == ((static_cast(1) << SrcBPC) - 1)) { + return static_cast((1 << DstBPC) - 1); + } + + return static_cast(value * (1 << DstBPC) / ((1 << SrcBPC) - 1)); + } + }; + + // SrcBPC > DstBPC + struct greater_conveter { + template + static T run(T value) { + return static_cast(value >> (SrcBPC - DstBPC)); + } + }; + + // SrcBPC == DstBPC + struct equal_converter { + template + static T run(T value) { + return value; + } + }; + + // SrcBPC != DstBPC + struct not_equal_converter { + template + static T run(T value) { + typedef typename std::conditional < SrcBPC < DstBPC, less_converter, greater_conveter>::type conv_type; + return conv_type::template run(value); + } + }; + + // converts Value from the Source to Destination bits representation + // SrcBPC ? DstBPC + struct converter { + template + static T run(T value) { + typedef typename std::conditional::type conv_type; + return conv_type::template run(value); + } + }; + + + // represents color channel information + template + struct element_traits; + + + // T is BYTE + template + struct element_traits { + typedef uint8_t value_type; + typedef typename PixelTraits::pixel_type pixel_type; + + static value_type min_value() { return 0x0; } + static value_type max_a() { return (1 << PixelTraits::bits_a) - 1; } + + // Alpha: { 0 -> transparent; 255 -> opaque } + + // Convert color value to the Destination Channel Bits Value + // dstBPC - Destination BitsPerChannel + template + static typename PixelTraits::channel_type to_channel_value(value_type value) { + return static_cast(converter::run(static_cast(value))); + } + + // Convert Source Channel Value into the + template + static value_type from_channel_value(pixel_type value) { + return static_cast(converter::run(value)); + } + }; + + // T is FLOAT + template + struct element_traits { + typedef double value_type; + typedef typename PixelTraits::pixel_type pixel_type; + + static double min_value() { return 0.0; } + static double max_value() { return 1.0; } + static double max_a() { return element_traits::max_value(); } + + // Alpha: { 0.0 -> transparent; 1.0 -> opaque } + + // Convert color value to the Destination Channel Bits Value + // dstBPC - Destination BitsPerChannel + template + static typename PixelTraits::channel_type to_channel_value(value_type value) { + if (value <= min_value()) { + return static_cast(0); + } + else if (value >= max_value()) { + return static_cast((1 << DstBPC) - 1); + } + else { + return static_cast(value * (1 << DstBPC)); + } + } + + // Convert Source Channel Value into the + template + static value_type from_channel_value(pixel_type value) { + return static_cast(static_cast(value) / static_cast((1 << SrcBPC) - 1)); + } + }; + + } // anonymous namespace + + + + // represents a color of a single pixel + // T -- might be an INTEGER or FLOAT type + // PixelTraits -- the format of packed color + template + struct basic_color { + typedef PixelTraits pixel_traits_type; + typedef element_traits element_traits_type; + + typedef typename pixel_traits_type::channel_type channel_type; + typedef typename pixel_traits_type::pixel_type pixel_type; + + typedef T value_type; + + template + friend struct basic_color; + + T a; // Alpha Component + T r; // Red Component + T g; // Green Component + T b; // Blue Component + + // ctor + basic_color() + : a(element_traits_type::max_a()), + r(element_traits_type::min_value()), + g(element_traits_type::min_value()), + b(element_traits_type::min_value()) { + /* no-op */ + } + + basic_color(const T& alpha, const T& red, const T& green, const T& blue) + : a(alpha), + r(red), + g(green), + b(blue) { + /* no-op */ + } + + basic_color(const T& red, const T& green, const T& blue) + : a(element_traits_type::max_a()), // max value --> opaque + r(red), + g(green), + b(blue) { + /* no-op */ + } + + // constructs a color using the packed value + basic_color(typename PixelTraits::pixel_type argb) { + basic_color temp = basic_color::create_from_value(argb); + std::swap(*this, temp); + } + + template + basic_color(const basic_color& other) { + typename PixelTraits::pixel_type packed = other.template traits_value(); + basic_color temp = basic_color::create_from_value(packed); + std::swap(*this, temp); + } + + // Get the packed color value, converted to the destination pixel format + template + uint32_t value() const { + typedef pixel_traits dest_traits_type; + return this->traits_value(); + } + + // Get the packed value of the color + typename PixelTraits::pixel_type value() const { + return this->traits_value(); + } + + private: + // Creates a basic_color object from the packed color value + template + static basic_color create_from_value(typename ST::pixel_type value) { + typedef typename ST::pixel_type src_pixel_type; + + return basic_color( + (element_traits_type::template from_channel_value((value & ST::mask_a) >> ST::shift_a)), + (element_traits_type::template from_channel_value((value & ST::mask_r) >> ST::shift_r)), + (element_traits_type::template from_channel_value((value & ST::mask_g) >> ST::shift_g)), + (element_traits_type::template from_channel_value((value & ST::mask_b) >> ST::shift_b)) + ); + } + + // Gets the packed value of the color using traits type + template + typename DT::pixel_type traits_value() const { + typedef typename DT::pixel_type dest_pixel_type; + typedef typename DT::channel_type dest_channel_type; + + dest_pixel_type value = ( + ((static_cast(element_traits_type::template to_channel_value(a)) << DT::shift_a) & DT::mask_a) | + ((static_cast(element_traits_type::template to_channel_value(r)) << DT::shift_r) & DT::mask_r) | + ((static_cast(element_traits_type::template to_channel_value(g)) << DT::shift_g) & DT::mask_g) | + ((static_cast(element_traits_type::template to_channel_value(b)) << DT::shift_b) & DT::mask_b) + ); + return value; + } + + public: + // add + basic_color& operator +=(const basic_color& other) { + this->r += other.r; + this->g += other.g; + this->b += other.b; + this->a += other.a; + return *this; + } + + // subsctract + basic_color& operator -=(const basic_color& other) { + this->r -= other.r; + this->g -= other.g; + this->b -= other.b; + this->a -= other.a; + return *this; + } + + // modulate + basic_color& operator *=(const basic_color& other) { + this->r *= other.r; + this->g *= other.g; + this->b *= other.b; + this->a *= other.a; + return *this; + } + + // scale + basic_color& operator *=(const T& scalar) { + this->r *= scalar; + this->g *= scalar; + this->b *= scalar; + this->a *= scalar; + return *this; + } + + template + basic_color& operator *=(const U& scalar) { + this->r *= scalar; + this->g *= scalar; + this->b *= scalar; + this->a *= scalar; + return *this; + } + + // modulate + basic_color& operator /=(const basic_color& other) { + this->r /= other.r; + this->g /= other.g; + this->b /= other.b; + this->a /= other.a; + return *this; + } + + // scale + basic_color& operator /=(const T& scalar) { + this->r /= scalar; + this->g /= scalar; + this->b /= scalar; + this->a /= scalar; + return *this; + } + + template + basic_color& operator /=(const U& scalar) { + this->r /= scalar; + this->g /= scalar; + this->b /= scalar; + this->a /= scalar; + return *this; + } + }; + + + + template + bool operator ==(const basic_color& lhs, const basic_color& rhs) { + return (lhs.a == rhs.a && + lhs.r == rhs.r && + lhs.g == rhs.g && + lhs.b == rhs.b + ); + } + + template + bool operator !=(const basic_color& lhs, const basic_color& rhs) { + return !(lhs == rhs); + } + + + + template + bool operator ==(const basic_color& lhs, const basic_color& rhs) { + basic_color other(rhs); + return lhs == other; + } + + template + bool operator !=(const basic_color& lhs, const basic_color& rhs) { + return !(lhs == rhs); + } + + + + template + basic_color operator +(const basic_color& lhs, const basic_color& rhs) { + basic_color result(lhs); + result += rhs; + return result; + } + + template + basic_color operator -(const basic_color& lhs, const basic_color& rhs) { + basic_color result(lhs); + result -= rhs; + return result; + } + + template + basic_color operator *(const basic_color& lhs, const basic_color& rhs) { + basic_color result(lhs); + result *= rhs; + return result; + } + + template + basic_color operator *(const T& lhs, const basic_color& rhs) { + basic_color result(rhs); + result *= lhs; + return result; + } + + template + basic_color operator *(const basic_color& lhs, const T& rhs) { + basic_color result(lhs); + result *= rhs; + return result; + } + + template + basic_color operator *(const U& lhs, const basic_color& rhs) { + basic_color result(rhs); + result *= lhs; + return result; + } + + template + basic_color operator *(const basic_color& lhs, const U& rhs) { + basic_color result(lhs); + result *= rhs; + return result; + } + + template + basic_color operator /(const basic_color& lhs, const basic_color& rhs) { + basic_color result(lhs); + result /= rhs; + return result; + } + + template + basic_color operator /(const basic_color& lhs, const U& rhs) { + basic_color result(lhs); + result /= rhs; + return result; + } + + // Get the color-component by index + template + inline T& get(basic_color& c) { + typedef T* pointer_type; + typedef typename PixelTraits::pixel_type pixel_type; + typedef std::pair pair_type; // shift, element + + static_assert(index < PixelTraits::size, "Color index is out of range."); + + pixel_type shift_a = PixelTraits::shift_a; + pixel_type shift_r = PixelTraits::shift_r; + pixel_type shift_g = PixelTraits::shift_g; + pixel_type shift_b = PixelTraits::shift_b; + + pair_type arr[PixelTraits::size] = { + pair_type(shift_a, &c.a), + pair_type(shift_r, &c.r), + pair_type(shift_g, &c.g), + pair_type(shift_b, &c.b) + }; + std::sort(arr, arr + PixelTraits::size, [](const pair_type& lhs, const pair_type& rhs) -> bool { return lhs.first > rhs.first; }); + + return *(arr[index].second); + } + + typedef basic_color color; /* color_argb */ + typedef basic_color colorF; /* color_argbF */ + + typedef basic_color color_abrg; + +} // namespace colors diff --git a/extern/cpp-colors/include/cpp-colors/color_error.h b/extern/cpp-colors/include/cpp-colors/color_error.h new file mode 100644 index 000000000..4d5fd83fb --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/color_error.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +namespace colors { + + class color_error + : public std::runtime_error { + public: + explicit color_error(const std::string& message) + : std::runtime_error(message) { + /* no-op */ + } + + virtual ~color_error() { + /* no-op */ + } + }; + +} // namespace colors diff --git a/extern/cpp-colors/include/cpp-colors/color_io.h b/extern/cpp-colors/include/cpp-colors/color_io.h new file mode 100644 index 000000000..ca1237391 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/color_io.h @@ -0,0 +1,605 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "color.h" +#include "color_named.h" + + +namespace colors { + + namespace { + + template + struct symbols; + + template <> + struct symbols { + static const char sharp = '#'; + static const char zero = '0'; + + static const char a = 'a'; + static const char r = 'r'; + static const char g = 'g'; + static const char b = 'b'; + + static const char round_bracket_open = '('; + static const char round_bracket_close = ')'; + static const char comma = ','; + + static const char number_0 = '0'; + static const char number_9 = '9'; + static const char char_A = 'A'; + static const char char_F = 'F'; + static const char char_a = 'a'; + static const char char_f = 'f'; + + static const char* const rgb_tag() { return "rgb"; } + static const char* const argb_tag() { return "argb"; } + + static const char to_lower(char ch) { + return static_cast(std::tolower(ch)); + } + }; + + template <> + struct symbols { + static const wchar_t sharp = L'#'; + static const wchar_t zero = L'0'; + + static const wchar_t a = L'a'; + static const wchar_t r = L'r'; + static const wchar_t g = L'g'; + static const wchar_t b = L'b'; + + static const wchar_t number_0 = L'0'; + static const wchar_t number_9 = L'9'; + static const wchar_t char_A = L'A'; + static const wchar_t char_F = L'F'; + static const wchar_t char_a = L'a'; + static const wchar_t char_f = L'f'; + + static const wchar_t round_bracket_open = L'('; + static const wchar_t round_bracket_close = L')'; + static const wchar_t comma = L','; + + static const wchar_t* const rgb_tag() { return L"rgb"; } + static const wchar_t* const argb_tag() { return L"argb"; } + + static const wchar_t to_lower(wchar_t ch) + { + return static_cast(towlower(ch)); + } + }; + + + // Convert to [Color RGB with Alpha argb(a,r,g,b)] + template + struct to_argb_str_dispatch; + + template + struct to_argb_str_dispatch { + typedef basic_color color_type; + typedef std::basic_string string_type; + + color_type c; + + to_argb_str_dispatch(const color_type& ci) + : c(ci) { + } + + operator string_type() { + typedef std::basic_ostringstream ostringstream_type; + ostringstream_type oss; + + oss << symbols::argb_tag() << symbols::round_bracket_open + << static_cast(c.a) << symbols::comma + << static_cast(c.r) << symbols::comma + << static_cast(c.g) << symbols::comma + << static_cast(c.b) + << symbols::round_bracket_close; + + return oss.str(); + } + }; + + template + struct to_argb_str_dispatch { + typedef basic_color color_type; + typedef basic_color color_double_type; + typedef std::basic_string string_type; + + color_type c; + + to_argb_str_dispatch(const color_double_type& cf) + : c(cf) { + } + + operator string_type() { + return to_argb_str_dispatch(c); + } + }; + + + // Convert to [Color RGB rgb(r,g,b)], ignore alpha + template + struct to_rgb_str_dispatch; + + template + struct to_rgb_str_dispatch { + typedef basic_color color_type; + typedef std::basic_string string_type; + + color_type c; + + to_rgb_str_dispatch(const color_type& ci) + : c(ci) { + } + + operator string_type() { + typedef std::basic_ostringstream ostringstream_type; + ostringstream_type oss; + + oss << symbols::rgb_tag() << symbols::round_bracket_open + << static_cast(c.r) << symbols::comma + << static_cast(c.g) << symbols::comma + << static_cast(c.b) + << symbols::round_bracket_close; + + return oss.str(); + } + }; + + template + struct to_rgb_str_dispatch { + typedef basic_color color_type; + typedef basic_color color_double_type; + typedef std::basic_string string_type; + + color_type c; + + to_rgb_str_dispatch(const color_double_type& cf) + : c(cf) { + } + + operator string_type() { + return to_rgb_str_dispatch(c); + } + }; + + + // Convert to [Color HEX with Alpha (#AARRGGBB)] + template + struct to_ahex_str_dispatch; + + template + struct to_ahex_str_dispatch { + typedef basic_color color_type; + typedef std::basic_string string_type; + + color_type c; + + to_ahex_str_dispatch(const color_type& ci) + : c(ci) { + } + + operator string_type() { + typedef std::basic_ostringstream ostringstream_type; + ostringstream_type oss; + + oss << symbols::sharp << std::hex + << std::setw(2) << std::setfill(symbols::zero) << static_cast(c.a) + << std::setw(2) << std::setfill(symbols::zero) << static_cast(c.r) + << std::setw(2) << std::setfill(symbols::zero) << static_cast(c.g) + << std::setw(2) << std::setfill(symbols::zero) << static_cast(c.b); + + return oss.str(); + } + }; + + template + struct to_ahex_str_dispatch { + typedef basic_color color_type; + typedef basic_color color_double_type; + typedef std::basic_string string_type; + + color_type c; + + to_ahex_str_dispatch(const color_double_type& cf) + : c(cf) { + } + + operator string_type() { + return to_ahex_str_dispatch(c); + } + }; + + + // Convert to [Color HEX (#RRGGBB)], ignore alpha + template + struct to_hex_str_dispatch; + + template + struct to_hex_str_dispatch { + typedef basic_color color_type; + typedef std::basic_string string_type; + + color_type c; + + to_hex_str_dispatch(const color_type& ci) + : c(ci) { + } + + operator string_type() { + typedef std::basic_ostringstream ostringstream_type; + ostringstream_type oss; + + oss << symbols::sharp << std::hex + << std::setw(2) << std::setfill(symbols::zero) << static_cast(c.r) + << std::setw(2) << std::setfill(symbols::zero) << static_cast(c.g) + << std::setw(2) << std::setfill(symbols::zero) << static_cast(c.b); + + return oss.str(); + } + }; + + template + struct to_hex_str_dispatch { + typedef basic_color color_type; + typedef basic_color color_double_type; + typedef std::basic_string string_type; + + color_type c; + + to_hex_str_dispatch(const color_double_type& cf) + : c(cf) { + } + + operator string_type() { + return to_hex_str_dispatch(c); + } + }; + + + template + void assign_color_value(basic_color& c, size_t idx, typename basic_color::channel_type value) { + assert(idx < 4 && "IndexOutOfRange"); + + switch (idx) { + case 0: + c.a = value; + break; + case 1: + c.r = value; + break; + case 2: + c.g = value; + break; + case 3: + c.b = value; + break; + }; + } + + + } // end of anonymous namespace + + + + // Write named color + template + inline std::basic_ostream& write_named_color(std::basic_ostream& os, const basic_color& c) { + typedef basic_named_color_converter color_converter; + + if (color_converter::is_named(c.value())) { + os << color_converter::name(c.value()); + } + + return os; + } + + // Read named color + template + inline std::basic_istream& read_named_color(std::basic_istream& is, basic_color& c) { + typedef std::basic_string string_type; + typedef basic_named_color_converter named_converter; + + string_type name; + if (is >> name) { + if (named_converter::is_named(name)) { + c = basic_color(value_of(named_converter::value(name))); + } + else { + is.setstate(std::ios_base::failbit); + } + } + + return is; + } + + + + // Convert to [Color HEX (#RRGGBB)], ignore alpha + template + inline std::basic_string to_hex_str(const basic_color& c) { + typedef basic_color color_type; + return to_hex_str_dispatch::value >(c); + } + + // Convert to [Color HEX with Alpha (#AARRGGBB)] + template + inline std::basic_string to_ahex_str(const basic_color& c) { + typedef basic_color color_type; + return to_ahex_str_dispatch::value >(c); + } + + // Convert to [Color RGB rgb(r,g,b)], ignore alpha + template + inline std::basic_string to_rgb_str(const basic_color& c) { + typedef basic_color color_type; + return to_rgb_str_dispatch::value >(c); + } + + // Convert to [Color RGB with Alpha argb(a,r,g,b)] + template + inline std::basic_string to_argb_str(const basic_color& c) { + typedef basic_color color_type; + return to_argb_str_dispatch::value >(c); + } + + + + template + inline std::basic_ostream& operator<<(std::basic_ostream& os, const basic_color& c) { + std::basic_ostringstream > oss; + + oss.flags(os.flags()); + oss.imbue(os.getloc()); + oss.precision(os.precision()); + + oss << to_argb_str(c); + + return os << oss.str().c_str(); + } + + + template + void parse_hex(std::basic_istream& is, basic_color& c) { + typedef basic_color color_type; + typedef typename color_type::channel_type channel_type; + typedef std::basic_stringstream > stringstream_type; + + // allowed format #RRGGBB or #AARRGGBB + CharT ch; + if (is >> ch && ch != symbols::sharp) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + bool is_argb = true; + const size_t data_len = 4; + for (size_t i = 0; i != data_len; ++i) { + // parse HEX + channel_type chv = 0; + const size_t hex_len = 2; + for (size_t j = 0; j != hex_len; ++j) { + if (is >> ch && ((ch <= symbols::number_9 && symbols::number_0 <= ch) || + (symbols::char_a <= ch && ch <= symbols::char_f) || + (symbols::char_A <= ch && ch <= symbols::char_F))) { + CharT ch1 = symbols::to_lower(ch); + channel_type val = 0; + if (ch1 <= symbols::number_9 && symbols::number_0 <= ch1) { + val = static_cast(ch1 - symbols::number_0); + } + else { // ('a' <= ch1 && ch1 <= 'f') + val = static_cast(10 + (ch1 - symbols::char_a)); + } + + chv <<= 4; + chv |= val; + } + else { + is.putback(ch); + + if (i < data_len - 1) { + is.setstate(std::ios_base::failbit); + } + else { + is.clear(); + is_argb = false; + } + + break; + } + } + + if (!is) + break; + + // #AARRGGBB + assign_color_value(c, i, chv); + } // for + + if (!is_argb) { + // we have RGB instead of ARGB, adjust colors + c.b = c.g; + c.g = c.r; + c.r = c.a; + c.a = color_type::element_traits_type::max_a(); + } + } + } + + + template + void parse_argb(std::basic_istream& is, basic_color& c) { + typedef basic_color color_type; + typedef typename color_type::channel_type channel_type; + typedef std::basic_stringstream > stringstream_type; + + CharT ch; + if (is >> ch && ch != symbols::a) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + if (is >> ch && ch != symbols::r) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + if (is >> ch && ch != symbols::g) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + if (is >> ch && ch != symbols::b) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + if (is >> ch && ch != symbols::round_bracket_open) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + const size_t data_len = 4; + for (size_t i = 0; i != data_len; ++i) { + unsigned int chv; + bool is_stop = false; + if (is >> std::ws >> chv >> ch && ch != symbols::comma) { + is.putback(ch); + if (i < data_len - 1) + is.setstate(std::ios_base::failbit); + + is_stop = true; + } + + assign_color_value(c, i, static_cast(chv)); + + if (is_stop) + break; + } + + if (is >> ch && ch != symbols::round_bracket_close) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + } + } + } + } + } + } + + + template + void parse_rgb(std::basic_istream& is, basic_color& c) { + typedef basic_color color_type; + typedef typename color_type::channel_type channel_type; + typedef std::basic_stringstream > stringstream_type; + + CharT ch; + if (is >> ch && ch != symbols::r) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + if (is >> ch && ch != symbols::g) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + if (is >> ch && ch != symbols::b) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + if (is >> ch && ch != symbols::round_bracket_open) + { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + const size_t data_len = 3; + for (size_t i = 0; i != data_len; ++i) + { + unsigned int chv; + bool is_stop = false; + if (is >> std::ws >> chv >> ch && ch != symbols::comma) { + is.putback(ch); + if (i < data_len - 1) + is.setstate(std::ios_base::failbit); + + is_stop = true; + } + + assign_color_value(c, i + 1, static_cast(chv)); // i==0 --- Alpha + + if (is_stop) + break; + } + + if (is >> ch && ch != symbols::round_bracket_close) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + } + } + } + } + } + + + template + inline std::basic_istream& operator>>(std::basic_istream& is, basic_color& c) { + typedef basic_color color_type; + typedef typename color_type::channel_type channel_type; + typedef std::basic_stringstream > stringstream_type; + + // Parse Formats: + // * Color HEX -- #RRGGBB + // * Color HEX with Alpha(AHEX) -- #AARRGGBB + // * Color RGB -- rgb(r,g,b) + // * Color RGB with Alpha(ARGB) -- argb(a,r,g,b) + // OR + // * We recognize one of the named colors. See: color_named.h + + CharT ch; + if (is >> ch) { + is.putback(ch); + if (ch != symbols::sharp && ch != symbols::r && ch != symbols::a) { + //read_named_color(is, c); + is.setstate(std::ios_base::failbit); + } + else { + if (ch == symbols::sharp) { + parse_hex(is, c); + } + else if (ch == symbols::a) { + parse_argb(is, c); + } + else if (ch == symbols::r) { + parse_rgb(is, c); + } + else { + is.setstate(std::ios_base::failbit); + } + } + } + else { + is.setstate(std::ios_base::failbit); + } + + return is; + } + + +} // namespace colors diff --git a/extern/cpp-colors/include/cpp-colors/color_named.h b/extern/cpp-colors/include/cpp-colors/color_named.h new file mode 100644 index 000000000..51056dc86 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/color_named.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include +#include "impl/named_utils.h" + + +namespace colors { + + // Used to convert named colors to the numeric value + template + class basic_named_color_converter { + private: + typedef ColorMapper color_mapper_type; + + typedef typename ColorMapper::char_type char_type; + typedef typename ColorMapper::string_type string_type; + + typedef typename color_mapper_type::string_color_map string_color_map; + typedef typename color_mapper_type::color_string_map color_string_map; + + typedef typename string_color_map::mapped_type known_color_type; + + private: + basic_named_color_converter(); + ~basic_named_color_converter(); + + public: + // Get the value of the color + static known_color_type value(const string_type& str) { + const string_color_map& sm = color_mapper_type::get_string_to_color_map(); + typename string_color_map::const_iterator it = sm.find(to_lowercase(str)); + if (it == sm.end()) + throw std::invalid_argument("Invalid color name."); + + return it->second; + } + + // Get the name of the color + static string_type name(typename ColorMapper::known_color_type value) { + return basic_named_color_converter::name(static_cast(value)); + } + + // Get the name of the color + static string_type name(int32_t value) { + const color_string_map& vm = color_mapper_type::get_color_to_string_map(); + typename color_string_map::const_iterator it = vm.find(value); + if (it == vm.end()) + throw std::invalid_argument("Invalid color value."); + + return it->second; + } + + // Returns TRUE if the argument is a named color + static bool is_named(const string_type& str) { + const string_color_map& sm = color_mapper_type::get_string_to_color_map(); + return (sm.find(to_lowercase(str)) != sm.end()); + } + + static bool is_named(uint32_t value) { + const color_string_map& vm = color_mapper_type::get_color_to_string_map(); + return (vm.find(value) != vm.end()); + } + + private: + // Convert string to lower case + static string_type to_lowercase(const string_type &str) { + static std::locale loc; + std::string result; + result.reserve(str.size()); + + for (auto elem : str) + result += std::tolower(elem, loc); + + return result; + } + }; + +} // namespace colors diff --git a/extern/cpp-colors/include/cpp-colors/color_utils.h b/extern/cpp-colors/include/cpp-colors/color_utils.h new file mode 100644 index 000000000..36ae6c018 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/color_utils.h @@ -0,0 +1,27 @@ +#pragma once + +#include "color.h" + + +namespace colors { + + enum class brightness_value { + light, + dark + }; + + /** + * Check whether the color is light or dark + * The function converts the RGB color space into YIQ + * + * http://www.w3.org/TR/AERT#color-contrast + */ + template + brightness_value brightness(const basic_color& c) { + color temp(c); + auto value = ((temp.r * 299.0) + (temp.g * 587.0) + (temp.b * 114.0)) / 1000.0; + return (value >= 128.0 ? brightness_value::light : brightness_value::dark); + } + + +} diff --git a/extern/cpp-colors/include/cpp-colors/colors.h b/extern/cpp-colors/include/cpp-colors/colors.h new file mode 100644 index 000000000..5a47114c4 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/colors.h @@ -0,0 +1,18 @@ +#pragma once + +#include "color.h" +#include "color_named.h" +#include "color_io.h" +#include "color_utils.h" +#include "pixel_format.h" +#include "pixel_utils.h" + +#include "impl/colors_impl.h" +#include "impl/constants_impl.h" +#include "impl/named_impl.h" + +namespace colors { + typedef basic_named_color_converter dotnet_named_color_converter; + typedef basic_named_color_converter wpf_named_color_converter; + typedef basic_named_color_converter x11_named_color_converter; +} diff --git a/extern/cpp-colors/include/cpp-colors/impl/colors_impl.h b/extern/cpp-colors/include/cpp-colors/impl/colors_impl.h new file mode 100644 index 000000000..fb12da345 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/impl/colors_impl.h @@ -0,0 +1,13 @@ +#pragma once + +#include "dotnet/dotnet_colors.h" +#include "wpf/wpf_colors.h" +#include "x11/x11_colors.h" + +namespace colors { + + typedef dotnet::basic_colors dotnet_colors; + typedef wpf::basic_colors wpf_colors; + typedef x11::basic_colors x11_colors; + +} // namespace colors diff --git a/extern/cpp-colors/include/cpp-colors/impl/constants_impl.h b/extern/cpp-colors/include/cpp-colors/impl/constants_impl.h new file mode 100644 index 000000000..b37d68319 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/impl/constants_impl.h @@ -0,0 +1,13 @@ +#pragma once + +#include "dotnet/dotnet_constants.h" +#include "wpf/wpf_constants.h" +#include "x11/x11_constants.h" + +namespace colors { + + typedef dotnet::known_color dotnet_known_color; + typedef wpf::known_color wpf_known_color; + typedef x11::known_color x11_known_color; + +} // namespace colors diff --git a/extern/cpp-colors/include/cpp-colors/impl/dotnet/dotnet_colors.h b/extern/cpp-colors/include/cpp-colors/impl/dotnet/dotnet_colors.h new file mode 100644 index 000000000..542f34b4b --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/impl/dotnet/dotnet_colors.h @@ -0,0 +1,585 @@ +#pragma once + +#include "dotnet_constants.h" +#include "../named_utils.h" + + +namespace colors { + namespace dotnet { + + template + struct basic_colors { + typedef ColorType color_type; + + basic_colors() = delete; + ~basic_colors() = delete; + basic_colors(const basic_colors&) = delete; + basic_colors& operator =(const basic_colors&) = delete; + + static color_type alice_blue() { + return color_type(value_of(known_color::alice_blue)); + } + + static color_type antique_white() { + return color_type(value_of(known_color::antique_white)); + } + + static color_type aqua() { + return color_type(value_of(known_color::aqua)); + } + + static color_type aquamarine() { + return color_type(value_of(known_color::aquamarine)); + } + + static color_type azure() { + return color_type(value_of(known_color::azure)); + } + + static color_type beige() { + return color_type(value_of(known_color::beige)); + } + + static color_type bisque() { + return color_type(value_of(known_color::bisque)); + } + + static color_type black() { + return color_type(value_of(known_color::black)); + } + + static color_type blanched_almond() { + return color_type(value_of(known_color::blanched_almond)); + } + + static color_type blue() { + return color_type(value_of(known_color::blue)); + } + + static color_type blue_violet() { + return color_type(value_of(known_color::blue_violet)); + } + + static color_type brown() { + return color_type(value_of(known_color::brown)); + } + + static color_type burly_wood() { + return color_type(value_of(known_color::burly_wood)); + } + + static color_type cadet_blue() { + return color_type(value_of(known_color::cadet_blue)); + } + + static color_type chartreuse() { + return color_type(value_of(known_color::chartreuse)); + } + + static color_type chocolate() { + return color_type(value_of(known_color::chocolate)); + } + + static color_type coral() { + return color_type(value_of(known_color::coral)); + } + + static color_type cornflower_blue() { + return color_type(value_of(known_color::cornflower_blue)); + } + + static color_type cornsilk() { + return color_type(value_of(known_color::cornsilk)); + } + + static color_type crimson() { + return color_type(value_of(known_color::crimson)); + } + + static color_type cyan() { + return color_type(value_of(known_color::cyan)); + } + + static color_type dark_blue() { + return color_type(value_of(known_color::dark_blue)); + } + + static color_type dark_cyan() { + return color_type(value_of(known_color::dark_cyan)); + } + + static color_type dark_goldenrod() { + return color_type(value_of(known_color::dark_goldenrod)); + } + + static color_type dark_gray() { + return color_type(value_of(known_color::dark_gray)); + } + + static color_type dark_green() { + return color_type(value_of(known_color::dark_green)); + } + + static color_type dark_khaki() { + return color_type(value_of(known_color::dark_khaki)); + } + + static color_type dark_magenta() { + return color_type(value_of(known_color::dark_magenta)); + } + + static color_type dark_olive_green() { + return color_type(value_of(known_color::dark_olive_green)); + } + + static color_type dark_orange() { + return color_type(value_of(known_color::dark_orange)); + } + + static color_type dark_orchid() { + return color_type(value_of(known_color::dark_orchid)); + } + + static color_type dark_red() { + return color_type(value_of(known_color::dark_red)); + } + + static color_type dark_salmon() { + return color_type(value_of(known_color::dark_salmon)); + } + + static color_type dark_sea_green() { + return color_type(value_of(known_color::dark_sea_green)); + } + + static color_type dark_slate_blue() { + return color_type(value_of(known_color::dark_slate_blue)); + } + + static color_type dark_slate_gray() { + return color_type(value_of(known_color::dark_slate_gray)); + } + + static color_type dark_turquoise() { + return color_type(value_of(known_color::dark_turquoise)); + } + + static color_type dark_violet() { + return color_type(value_of(known_color::dark_violet)); + } + + static color_type deep_pink() { + return color_type(value_of(known_color::deep_pink)); + } + + static color_type deep_sky_blue() { + return color_type(value_of(known_color::deep_sky_blue)); + } + + static color_type dim_gray() { + return color_type(value_of(known_color::dim_gray)); + } + + static color_type dodger_blue() { + return color_type(value_of(known_color::dodger_blue)); + } + + static color_type firebrick() { + return color_type(value_of(known_color::firebrick)); + } + + static color_type floral_white() { + return color_type(value_of(known_color::floral_white)); + } + + static color_type forest_green() { + return color_type(value_of(known_color::forest_green)); + } + + static color_type fuchsia() { + return color_type(value_of(known_color::fuchsia)); + } + + static color_type gainsboro() { + return color_type(value_of(known_color::gainsboro)); + } + + static color_type ghost_white() { + return color_type(value_of(known_color::ghost_white)); + } + + static color_type gold() { + return color_type(value_of(known_color::gold)); + } + + static color_type goldenrod() { + return color_type(value_of(known_color::goldenrod)); + } + + static color_type gray() { + return color_type(value_of(known_color::gray)); + } + + static color_type green() { + return color_type(value_of(known_color::green)); + } + + static color_type green_yellow() { + return color_type(value_of(known_color::green_yellow)); + } + + static color_type honeydew() { + return color_type(value_of(known_color::honeydew)); + } + + static color_type hot_pink() { + return color_type(value_of(known_color::hot_pink)); + } + + static color_type indian_red() { + return color_type(value_of(known_color::indian_red)); + } + + static color_type indigo() { + return color_type(value_of(known_color::indigo)); + } + + static color_type ivory() { + return color_type(value_of(known_color::ivory)); + } + + static color_type khaki() { + return color_type(value_of(known_color::khaki)); + } + + static color_type lavender() { + return color_type(value_of(known_color::lavender)); + } + + static color_type lavender_blush() { + return color_type(value_of(known_color::lavender_blush)); + } + + static color_type lawn_green() { + return color_type(value_of(known_color::lawn_green)); + } + + static color_type lemon_chiffon() { + return color_type(value_of(known_color::lemon_chiffon)); + } + + static color_type light_blue() { + return color_type(value_of(known_color::light_blue)); + } + + static color_type light_coral() { + return color_type(value_of(known_color::light_coral)); + } + + static color_type light_cyan() { + return color_type(value_of(known_color::light_cyan)); + } + + static color_type light_goldenrod_yellow() { + return color_type(value_of(known_color::light_goldenrod_yellow)); + } + + static color_type light_gray() { + return color_type(value_of(known_color::light_gray)); + } + + static color_type light_green() { + return color_type(value_of(known_color::light_green)); + } + + static color_type light_pink() { + return color_type(value_of(known_color::light_pink)); + } + + static color_type light_salmon() { + return color_type(value_of(known_color::light_salmon)); + } + + static color_type light_sea_green() { + return color_type(value_of(known_color::light_sea_green)); + } + + static color_type light_sky_blue() { + return color_type(value_of(known_color::light_sky_blue)); + } + + static color_type light_slate_gray() { + return color_type(value_of(known_color::light_slate_gray)); + } + + static color_type light_steel_blue() { + return color_type(value_of(known_color::light_steel_blue)); + } + + static color_type light_yellow() { + return color_type(value_of(known_color::light_yellow)); + } + + static color_type lime() { + return color_type(value_of(known_color::lime)); + } + + static color_type lime_green() { + return color_type(value_of(known_color::lime_green)); + } + + static color_type linen() { + return color_type(value_of(known_color::linen)); + } + + static color_type magenta() { + return color_type(value_of(known_color::magenta)); + } + + static color_type maroon() { + return color_type(value_of(known_color::maroon)); + } + + static color_type medium_aquamarine() { + return color_type(value_of(known_color::medium_aquamarine)); + } + + static color_type medium_blue() { + return color_type(value_of(known_color::medium_blue)); + } + + static color_type medium_orchid() { + return color_type(value_of(known_color::medium_orchid)); + } + + static color_type medium_purple() { + return color_type(value_of(known_color::medium_purple)); + } + + static color_type medium_sea_green() { + return color_type(value_of(known_color::medium_sea_green)); + } + + static color_type medium_slate_blue() { + return color_type(value_of(known_color::medium_slate_blue)); + } + + static color_type medium_spring_green() { + return color_type(value_of(known_color::medium_spring_green)); + } + + static color_type medium_turquoise() { + return color_type(value_of(known_color::medium_turquoise)); + } + + static color_type medium_violet_red() { + return color_type(value_of(known_color::medium_violet_red)); + } + + static color_type midnight_blue() { + return color_type(value_of(known_color::midnight_blue)); + } + + static color_type mint_cream() { + return color_type(value_of(known_color::mint_cream)); + } + + static color_type misty_rose() { + return color_type(value_of(known_color::misty_rose)); + } + + static color_type moccasin() { + return color_type(value_of(known_color::moccasin)); + } + + static color_type navajo_white() { + return color_type(value_of(known_color::navajo_white)); + } + + static color_type navy() { + return color_type(value_of(known_color::navy)); + } + + static color_type old_lace() { + return color_type(value_of(known_color::old_lace)); + } + + static color_type olive() { + return color_type(value_of(known_color::olive)); + } + + static color_type olive_drab() { + return color_type(value_of(known_color::olive_drab)); + } + + static color_type orange() { + return color_type(value_of(known_color::orange)); + } + + static color_type orange_red() { + return color_type(value_of(known_color::orange_red)); + } + + static color_type orchid() { + return color_type(value_of(known_color::orchid)); + } + + static color_type pale_goldenrod() { + return color_type(value_of(known_color::pale_goldenrod)); + } + + static color_type pale_green() { + return color_type(value_of(known_color::pale_green)); + } + + static color_type pale_turquoise() { + return color_type(value_of(known_color::pale_turquoise)); + } + + static color_type pale_violet_red() { + return color_type(value_of(known_color::pale_violet_red)); + } + + static color_type papaya_whip() { + return color_type(value_of(known_color::papaya_whip)); + } + + static color_type peach_puff() { + return color_type(value_of(known_color::peach_puff)); + } + + static color_type peru() { + return color_type(value_of(known_color::peru)); + } + + static color_type pink() { + return color_type(value_of(known_color::pink)); + } + + static color_type plum() { + return color_type(value_of(known_color::plum)); + } + + static color_type powder_blue() { + return color_type(value_of(known_color::powder_blue)); + } + + static color_type purple() { + return color_type(value_of(known_color::purple)); + } + + static color_type red() { + return color_type(value_of(known_color::red)); + } + + static color_type rosy_brown() { + return color_type(value_of(known_color::rosy_brown)); + } + + static color_type royal_blue() { + return color_type(value_of(known_color::royal_blue)); + } + + static color_type saddle_brown() { + return color_type(value_of(known_color::saddle_brown)); + } + + static color_type salmon() { + return color_type(value_of(known_color::salmon)); + } + + static color_type sandy_brown() { + return color_type(value_of(known_color::sandy_brown)); + } + + static color_type sea_green() { + return color_type(value_of(known_color::sea_green)); + } + + static color_type sea_shell() { + return color_type(value_of(known_color::sea_shell)); + } + + static color_type sienna() { + return color_type(value_of(known_color::sienna)); + } + + static color_type silver() { + return color_type(value_of(known_color::silver)); + } + + static color_type sky_blue() { + return color_type(value_of(known_color::sky_blue)); + } + + static color_type slate_blue() { + return color_type(value_of(known_color::slate_blue)); + } + + static color_type slate_gray() { + return color_type(value_of(known_color::slate_gray)); + } + + static color_type snow() { + return color_type(value_of(known_color::snow)); + } + + static color_type spring_green() { + return color_type(value_of(known_color::spring_green)); + } + + static color_type steel_blue() { + return color_type(value_of(known_color::steel_blue)); + } + + static color_type tan() { + return color_type(value_of(known_color::tan)); + } + + static color_type teal() { + return color_type(value_of(known_color::teal)); + } + + static color_type thistle() { + return color_type(value_of(known_color::thistle)); + } + + static color_type tomato() { + return color_type(value_of(known_color::tomato)); + } + + static color_type transparent() { + return color_type(value_of(known_color::transparent)); + } + + static color_type turquoise() { + return color_type(value_of(known_color::turquoise)); + } + + static color_type violet() { + return color_type(value_of(known_color::violet)); + } + + static color_type wheat() { + return color_type(value_of(known_color::wheat)); + } + + static color_type white() { + return color_type(value_of(known_color::white)); + } + + static color_type white_smoke() { + return color_type(value_of(known_color::white_smoke)); + } + + static color_type yellow() { + return color_type(value_of(known_color::yellow)); + } + + static color_type yellow_green() { + return color_type(value_of(known_color::yellow_green)); + } + }; + + } // namespace dotnet +} // namespace colors diff --git a/extern/cpp-colors/include/cpp-colors/impl/dotnet/dotnet_constants.h b/extern/cpp-colors/include/cpp-colors/impl/dotnet/dotnet_constants.h new file mode 100644 index 000000000..bc11271cb --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/impl/dotnet/dotnet_constants.h @@ -0,0 +1,153 @@ +#pragma once + +#include + +namespace colors { + namespace dotnet { + + enum class known_color : uint32_t { + alice_blue = 0xFFF0F8FF, // Alice Blue: argb(255, 240, 248, 255) + antique_white = 0xFFFAEBD7, // Antique White: argb(255, 250, 235, 215) + aqua = 0xFF00FFFF, // Aqua: argb(255, 0, 255, 255) + aquamarine = 0xFF7FFFD4, // Aquamarine: argb(255, 127, 255, 212) + azure = 0xFFF0FFFF, // Azure: argb(255, 240, 255, 255) + beige = 0xFFF5F5DC, // Beige: argb(255, 245, 245, 220) + bisque = 0xFFFFE4C4, // Bisque: argb(255, 255, 228, 196) + black = 0xFF000000, // Black: argb(255, 0, 0, 0) + blanched_almond = 0xFFFFEBCD, // Blanched Almond: argb(255, 255, 235, 205) + blue = 0xFF0000FF, // Blue: argb(255, 0, 0, 255) + blue_violet = 0xFF8A2BE2, // Blue Violet: argb(255, 138, 43, 226) + brown = 0xFFA52A2A, // Brown: argb(255, 165, 42, 42) + burly_wood = 0xFFDEB887, // Burly Wood: argb(255, 222, 184, 135) + cadet_blue = 0xFF5F9EA0, // Cadet Blue: argb(255, 95, 158, 160) + chartreuse = 0xFF7FFF00, // Chartreuse: argb(255, 127, 255, 0) + chocolate = 0xFFD2691E, // Chocolate: argb(255, 210, 105, 30) + coral = 0xFFFF7F50, // Coral: argb(255, 255, 127, 80) + cornflower_blue = 0xFF6495ED, // Cornflower Blue: argb(255, 100, 149, 237) + cornsilk = 0xFFFFF8DC, // Cornsilk: argb(255, 255, 248, 220) + crimson = 0xFFDC143C, // Crimson: argb(255, 220, 20, 60) + cyan = 0xFF00FFFF, // Cyan: argb(255, 0, 255, 255) + dark_blue = 0xFF00008B, // Dark Blue: argb(255, 0, 0, 139) + dark_cyan = 0xFF008B8B, // Dark Cyan: argb(255, 0, 139, 139) + dark_goldenrod = 0xFFB8860B, // Dark Goldenrod: argb(255, 184, 134, 11) + dark_gray = 0xFFA9A9A9, // Dark Gray: argb(255, 169, 169, 169) + dark_green = 0xFF006400, // Dark Green: argb(255, 0, 100, 0) + dark_khaki = 0xFFBDB76B, // Dark Khaki: argb(255, 189, 183, 107) + dark_magenta = 0xFF8B008B, // Dark Magenta: argb(255, 139, 0, 139) + dark_olive_green = 0xFF556B2F, // Dark Olive Green: argb(255, 85, 107, 47) + dark_orange = 0xFFFF8C00, // Dark Orange: argb(255, 255, 140, 0) + dark_orchid = 0xFF9932CC, // Dark Orchid: argb(255, 153, 50, 204) + dark_red = 0xFF8B0000, // Dark Red: argb(255, 139, 0, 0) + dark_salmon = 0xFFE9967A, // Dark Salmon: argb(255, 233, 150, 122) + dark_sea_green = 0xFF8FBC8F, // Dark Sea Green: argb(255, 143, 188, 143) + dark_slate_blue = 0xFF483D8B, // Dark Slate Blue: argb(255, 72, 61, 139) + dark_slate_gray = 0xFF2F4F4F, // Dark Slate Gray: argb(255, 47, 79, 79) + dark_turquoise = 0xFF00CED1, // Dark Turquoise: argb(255, 0, 206, 209) + dark_violet = 0xFF9400D3, // Dark Violet: argb(255, 148, 0, 211) + deep_pink = 0xFFFF1493, // Deep Pink: argb(255, 255, 20, 147) + deep_sky_blue = 0xFF00BFFF, // Deep Sky Blue: argb(255, 0, 191, 255) + dim_gray = 0xFF696969, // Dim Gray: argb(255, 105, 105, 105) + dodger_blue = 0xFF1E90FF, // Dodger Blue: argb(255, 30, 144, 255) + firebrick = 0xFFB22222, // Firebrick: argb(255, 178, 34, 34) + floral_white = 0xFFFFFAF0, // Floral White: argb(255, 255, 250, 240) + forest_green = 0xFF228B22, // Forest Green: argb(255, 34, 139, 34) + fuchsia = 0xFFFF00FF, // Fuchsia: argb(255, 255, 0, 255) + gainsboro = 0xFFDCDCDC, // Gainsboro: argb(255, 220, 220, 220) + ghost_white = 0xFFF8F8FF, // Ghost White: argb(255, 248, 248, 255) + gold = 0xFFFFD700, // Gold: argb(255, 255, 215, 0) + goldenrod = 0xFFDAA520, // Goldenrod: argb(255, 218, 165, 32) + gray = 0xFF808080, // Gray: argb(255, 128, 128, 128) + green = 0xFF008000, // Green: argb(255, 0, 128, 0) + green_yellow = 0xFFADFF2F, // Green Yellow: argb(255, 173, 255, 47) + honeydew = 0xFFF0FFF0, // Honeydew: argb(255, 240, 255, 240) + hot_pink = 0xFFFF69B4, // Hot Pink: argb(255, 255, 105, 180) + indian_red = 0xFFCD5C5C, // Indian Red: argb(255, 205, 92, 92) + indigo = 0xFF4B0082, // Indigo: argb(255, 75, 0, 130) + ivory = 0xFFFFFFF0, // Ivory: argb(255, 255, 255, 240) + khaki = 0xFFF0E68C, // Khaki: argb(255, 240, 230, 140) + lavender = 0xFFE6E6FA, // Lavender: argb(255, 230, 230, 250) + lavender_blush = 0xFFFFF0F5, // Lavender Blush: argb(255, 255, 240, 245) + lawn_green = 0xFF7CFC00, // Lawn Green: argb(255, 124, 252, 0) + lemon_chiffon = 0xFFFFFACD, // Lemon Chiffon: argb(255, 255, 250, 205) + light_blue = 0xFFADD8E6, // Light Blue: argb(255, 173, 216, 230) + light_coral = 0xFFF08080, // Light Coral: argb(255, 240, 128, 128) + light_cyan = 0xFFE0FFFF, // Light Cyan: argb(255, 224, 255, 255) + light_goldenrod_yellow = 0xFFFAFAD2, // Light Goldenrod Yellow: argb(255, 250, 250, 210) + light_gray = 0xFFD3D3D3, // Light Gray: argb(255, 211, 211, 211) + light_green = 0xFF90EE90, // Light Green: argb(255, 144, 238, 144) + light_pink = 0xFFFFB6C1, // Light Pink: argb(255, 255, 182, 193) + light_salmon = 0xFFFFA07A, // Light Salmon: argb(255, 255, 160, 122) + light_sea_green = 0xFF20B2AA, // Light Sea Green: argb(255, 32, 178, 170) + light_sky_blue = 0xFF87CEFA, // Light Sky Blue: argb(255, 135, 206, 250) + light_slate_gray = 0xFF778899, // Light Slate Gray: argb(255, 119, 136, 153) + light_steel_blue = 0xFFB0C4DE, // Light Steel Blue: argb(255, 176, 196, 222) + light_yellow = 0xFFFFFFE0, // Light Yellow: argb(255, 255, 255, 224) + lime = 0xFF00FF00, // Lime: argb(255, 0, 255, 0) + lime_green = 0xFF32CD32, // Lime Green: argb(255, 50, 205, 50) + linen = 0xFFFAF0E6, // Linen: argb(255, 250, 240, 230) + magenta = 0xFFFF00FF, // Magenta: argb(255, 255, 0, 255) + maroon = 0xFF800000, // Maroon: argb(255, 128, 0, 0) + medium_aquamarine = 0xFF66CDAA, // Medium Aquamarine: argb(255, 102, 205, 170) + medium_blue = 0xFF0000CD, // Medium Blue: argb(255, 0, 0, 205) + medium_orchid = 0xFFBA55D3, // Medium Orchid: argb(255, 186, 85, 211) + medium_purple = 0xFF9370DB, // Medium Purple: argb(255, 147, 112, 219) + medium_sea_green = 0xFF3CB371, // Medium Sea Green: argb(255, 60, 179, 113) + medium_slate_blue = 0xFF7B68EE, // Medium Slate Blue: argb(255, 123, 104, 238) + medium_spring_green = 0xFF00FA9A, // Medium Spring Green: argb(255, 0, 250, 154) + medium_turquoise = 0xFF48D1CC, // Medium Turquoise: argb(255, 72, 209, 204) + medium_violet_red = 0xFFC71585, // Medium Violet Red: argb(255, 199, 21, 133) + midnight_blue = 0xFF191970, // Midnight Blue: argb(255, 25, 25, 112) + mint_cream = 0xFFF5FFFA, // Mint Cream: argb(255, 245, 255, 250) + misty_rose = 0xFFFFE4E1, // Misty Rose: argb(255, 255, 228, 225) + moccasin = 0xFFFFE4B5, // Moccasin: argb(255, 255, 228, 181) + navajo_white = 0xFFFFDEAD, // Navajo White: argb(255, 255, 222, 173) + navy = 0xFF000080, // Navy: argb(255, 0, 0, 128) + old_lace = 0xFFFDF5E6, // Old Lace: argb(255, 253, 245, 230) + olive = 0xFF808000, // Olive: argb(255, 128, 128, 0) + olive_drab = 0xFF6B8E23, // Olive Drab: argb(255, 107, 142, 35) + orange = 0xFFFFA500, // Orange: argb(255, 255, 165, 0) + orange_red = 0xFFFF4500, // Orange Red: argb(255, 255, 69, 0) + orchid = 0xFFDA70D6, // Orchid: argb(255, 218, 112, 214) + pale_goldenrod = 0xFFEEE8AA, // Pale Goldenrod: argb(255, 238, 232, 170) + pale_green = 0xFF98FB98, // Pale Green: argb(255, 152, 251, 152) + pale_turquoise = 0xFFAFEEEE, // Pale Turquoise: argb(255, 175, 238, 238) + pale_violet_red = 0xFFDB7093, // Pale Violet Red: argb(255, 219, 112, 147) + papaya_whip = 0xFFFFEFD5, // Papaya Whip: argb(255, 255, 239, 213) + peach_puff = 0xFFFFDAB9, // Peach Puff: argb(255, 255, 218, 185) + peru = 0xFFCD853F, // Peru: argb(255, 205, 133, 63) + pink = 0xFFFFC0CB, // Pink: argb(255, 255, 192, 203) + plum = 0xFFDDA0DD, // Plum: argb(255, 221, 160, 221) + powder_blue = 0xFFB0E0E6, // Powder Blue: argb(255, 176, 224, 230) + purple = 0xFF800080, // Purple: argb(255, 128, 0, 128) + red = 0xFFFF0000, // Red: argb(255, 255, 0, 0) + rosy_brown = 0xFFBC8F8F, // Rosy Brown: argb(255, 188, 143, 143) + royal_blue = 0xFF4169E1, // Royal Blue: argb(255, 65, 105, 225) + saddle_brown = 0xFF8B4513, // Saddle Brown: argb(255, 139, 69, 19) + salmon = 0xFFFA8072, // Salmon: argb(255, 250, 128, 114) + sandy_brown = 0xFFF4A460, // Sandy Brown: argb(255, 244, 164, 96) + sea_green = 0xFF2E8B57, // Sea Green: argb(255, 46, 139, 87) + sea_shell = 0xFFFFF5EE, // Sea Shell: argb(255, 255, 245, 238) + sienna = 0xFFA0522D, // Sienna: argb(255, 160, 82, 45) + silver = 0xFFC0C0C0, // Silver: argb(255, 192, 192, 192) + sky_blue = 0xFF87CEEB, // Sky Blue: argb(255, 135, 206, 235) + slate_blue = 0xFF6A5ACD, // Slate Blue: argb(255, 106, 90, 205) + slate_gray = 0xFF708090, // Slate Gray: argb(255, 112, 128, 144) + snow = 0xFFFFFAFA, // Snow: argb(255, 255, 250, 250) + spring_green = 0xFF00FF7F, // Spring Green: argb(255, 0, 255, 127) + steel_blue = 0xFF4682B4, // Steel Blue: argb(255, 70, 130, 180) + tan = 0xFFD2B48C, // Tan: argb(255, 210, 180, 140) + teal = 0xFF008080, // Teal: argb(255, 0, 128, 128) + thistle = 0xFFD8BFD8, // Thistle: argb(255, 216, 191, 216) + tomato = 0xFFFF6347, // Tomato: argb(255, 255, 99, 71) + transparent = 0x00FFFFFF, // Transparent: argb(0, 255, 255, 255) + turquoise = 0xFF40E0D0, // Turquoise: argb(255, 64, 224, 208) + violet = 0xFFEE82EE, // Violet: argb(255, 238, 130, 238) + wheat = 0xFFF5DEB3, // Wheat: argb(255, 245, 222, 179) + white = 0xFFFFFFFF, // White: argb(255, 255, 255, 255) + white_smoke = 0xFFF5F5F5, // White Smoke: argb(255, 245, 245, 245) + yellow = 0xFFFFFF00, // Yellow: argb(255, 255, 255, 0) + yellow_green = 0xFF9ACD32, // Yellow Green: argb(255, 154, 205, 50) + }; + + } // namespace dotnet +} // namespace colors diff --git a/extern/cpp-colors/include/cpp-colors/impl/dotnet/dotnet_named.h b/extern/cpp-colors/include/cpp-colors/impl/dotnet/dotnet_named.h new file mode 100644 index 000000000..d2a31adf6 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/impl/dotnet/dotnet_named.h @@ -0,0 +1,1484 @@ +#pragma once + +#include +#include +#include +#include +#include "dotnet_constants.h" +#include "../named_utils.h" + + +namespace colors { + namespace dotnet { + + template + struct basic_named_color; + + template<> + struct basic_named_color { + static const char *const alice_blue() { + return "AliceBlue"; + } + + static const char *const antique_white() { + return "AntiqueWhite"; + } + + static const char *const aqua() { + return "Aqua"; + } + + static const char *const aquamarine() { + return "Aquamarine"; + } + + static const char *const azure() { + return "Azure"; + } + + static const char *const beige() { + return "Beige"; + } + + static const char *const bisque() { + return "Bisque"; + } + + static const char *const black() { + return "Black"; + } + + static const char *const blanched_almond() { + return "BlanchedAlmond"; + } + + static const char *const blue() { + return "Blue"; + } + + static const char *const blue_violet() { + return "BlueViolet"; + } + + static const char *const brown() { + return "Brown"; + } + + static const char *const burly_wood() { + return "BurlyWood"; + } + + static const char *const cadet_blue() { + return "CadetBlue"; + } + + static const char *const chartreuse() { + return "Chartreuse"; + } + + static const char *const chocolate() { + return "Chocolate"; + } + + static const char *const coral() { + return "Coral"; + } + + static const char *const cornflower_blue() { + return "CornflowerBlue"; + } + + static const char *const cornsilk() { + return "Cornsilk"; + } + + static const char *const crimson() { + return "Crimson"; + } + + static const char *const cyan() { + return "Cyan"; + } + + static const char *const dark_blue() { + return "DarkBlue"; + } + + static const char *const dark_cyan() { + return "DarkCyan"; + } + + static const char *const dark_goldenrod() { + return "DarkGoldenrod"; + } + + static const char *const dark_gray() { + return "DarkGray"; + } + + static const char *const dark_green() { + return "DarkGreen"; + } + + static const char *const dark_khaki() { + return "DarkKhaki"; + } + + static const char *const dark_magenta() { + return "DarkMagenta"; + } + + static const char *const dark_olive_green() { + return "DarkOliveGreen"; + } + + static const char *const dark_orange() { + return "DarkOrange"; + } + + static const char *const dark_orchid() { + return "DarkOrchid"; + } + + static const char *const dark_red() { + return "DarkRed"; + } + + static const char *const dark_salmon() { + return "DarkSalmon"; + } + + static const char *const dark_sea_green() { + return "DarkSeaGreen"; + } + + static const char *const dark_slate_blue() { + return "DarkSlateBlue"; + } + + static const char *const dark_slate_gray() { + return "DarkSlateGray"; + } + + static const char *const dark_turquoise() { + return "DarkTurquoise"; + } + + static const char *const dark_violet() { + return "DarkViolet"; + } + + static const char *const deep_pink() { + return "DeepPink"; + } + + static const char *const deep_sky_blue() { + return "DeepSkyBlue"; + } + + static const char *const dim_gray() { + return "DimGray"; + } + + static const char *const dodger_blue() { + return "DodgerBlue"; + } + + static const char *const firebrick() { + return "Firebrick"; + } + + static const char *const floral_white() { + return "FloralWhite"; + } + + static const char *const forest_green() { + return "ForestGreen"; + } + + static const char *const fuchsia() { + return "Fuchsia"; + } + + static const char *const gainsboro() { + return "Gainsboro"; + } + + static const char *const ghost_white() { + return "GhostWhite"; + } + + static const char *const gold() { + return "Gold"; + } + + static const char *const goldenrod() { + return "Goldenrod"; + } + + static const char *const gray() { + return "Gray"; + } + + static const char *const green() { + return "Green"; + } + + static const char *const green_yellow() { + return "GreenYellow"; + } + + static const char *const honeydew() { + return "Honeydew"; + } + + static const char *const hot_pink() { + return "HotPink"; + } + + static const char *const indian_red() { + return "IndianRed"; + } + + static const char *const indigo() { + return "Indigo"; + } + + static const char *const ivory() { + return "Ivory"; + } + + static const char *const khaki() { + return "Khaki"; + } + + static const char *const lavender() { + return "Lavender"; + } + + static const char *const lavender_blush() { + return "LavenderBlush"; + } + + static const char *const lawn_green() { + return "LawnGreen"; + } + + static const char *const lemon_chiffon() { + return "LemonChiffon"; + } + + static const char *const light_blue() { + return "LightBlue"; + } + + static const char *const light_coral() { + return "LightCoral"; + } + + static const char *const light_cyan() { + return "LightCyan"; + } + + static const char *const light_goldenrod_yellow() { + return "LightGoldenrodYellow"; + } + + static const char *const light_gray() { + return "LightGray"; + } + + static const char *const light_green() { + return "LightGreen"; + } + + static const char *const light_pink() { + return "LightPink"; + } + + static const char *const light_salmon() { + return "LightSalmon"; + } + + static const char *const light_sea_green() { + return "LightSeaGreen"; + } + + static const char *const light_sky_blue() { + return "LightSkyBlue"; + } + + static const char *const light_slate_gray() { + return "LightSlateGray"; + } + + static const char *const light_steel_blue() { + return "LightSteelBlue"; + } + + static const char *const light_yellow() { + return "LightYellow"; + } + + static const char *const lime() { + return "Lime"; + } + + static const char *const lime_green() { + return "LimeGreen"; + } + + static const char *const linen() { + return "Linen"; + } + + static const char *const magenta() { + return "Magenta"; + } + + static const char *const maroon() { + return "Maroon"; + } + + static const char *const medium_aquamarine() { + return "MediumAquamarine"; + } + + static const char *const medium_blue() { + return "MediumBlue"; + } + + static const char *const medium_orchid() { + return "MediumOrchid"; + } + + static const char *const medium_purple() { + return "MediumPurple"; + } + + static const char *const medium_sea_green() { + return "MediumSeaGreen"; + } + + static const char *const medium_slate_blue() { + return "MediumSlateBlue"; + } + + static const char *const medium_spring_green() { + return "MediumSpringGreen"; + } + + static const char *const medium_turquoise() { + return "MediumTurquoise"; + } + + static const char *const medium_violet_red() { + return "MediumVioletRed"; + } + + static const char *const midnight_blue() { + return "MidnightBlue"; + } + + static const char *const mint_cream() { + return "MintCream"; + } + + static const char *const misty_rose() { + return "MistyRose"; + } + + static const char *const moccasin() { + return "Moccasin"; + } + + static const char *const navajo_white() { + return "NavajoWhite"; + } + + static const char *const navy() { + return "Navy"; + } + + static const char *const old_lace() { + return "OldLace"; + } + + static const char *const olive() { + return "Olive"; + } + + static const char *const olive_drab() { + return "OliveDrab"; + } + + static const char *const orange() { + return "Orange"; + } + + static const char *const orange_red() { + return "OrangeRed"; + } + + static const char *const orchid() { + return "Orchid"; + } + + static const char *const pale_goldenrod() { + return "PaleGoldenrod"; + } + + static const char *const pale_green() { + return "PaleGreen"; + } + + static const char *const pale_turquoise() { + return "PaleTurquoise"; + } + + static const char *const pale_violet_red() { + return "PaleVioletRed"; + } + + static const char *const papaya_whip() { + return "PapayaWhip"; + } + + static const char *const peach_puff() { + return "PeachPuff"; + } + + static const char *const peru() { + return "Peru"; + } + + static const char *const pink() { + return "Pink"; + } + + static const char *const plum() { + return "Plum"; + } + + static const char *const powder_blue() { + return "PowderBlue"; + } + + static const char *const purple() { + return "Purple"; + } + + static const char *const red() { + return "Red"; + } + + static const char *const rosy_brown() { + return "RosyBrown"; + } + + static const char *const royal_blue() { + return "RoyalBlue"; + } + + static const char *const saddle_brown() { + return "SaddleBrown"; + } + + static const char *const salmon() { + return "Salmon"; + } + + static const char *const sandy_brown() { + return "SandyBrown"; + } + + static const char *const sea_green() { + return "SeaGreen"; + } + + static const char *const sea_shell() { + return "SeaShell"; + } + + static const char *const sienna() { + return "Sienna"; + } + + static const char *const silver() { + return "Silver"; + } + + static const char *const sky_blue() { + return "SkyBlue"; + } + + static const char *const slate_blue() { + return "SlateBlue"; + } + + static const char *const slate_gray() { + return "SlateGray"; + } + + static const char *const snow() { + return "Snow"; + } + + static const char *const spring_green() { + return "SpringGreen"; + } + + static const char *const steel_blue() { + return "SteelBlue"; + } + + static const char *const tan() { + return "Tan"; + } + + static const char *const teal() { + return "Teal"; + } + + static const char *const thistle() { + return "Thistle"; + } + + static const char *const tomato() { + return "Tomato"; + } + + static const char *const transparent() { + return "Transparent"; + } + + static const char *const turquoise() { + return "Turquoise"; + } + + static const char *const violet() { + return "Violet"; + } + + static const char *const wheat() { + return "Wheat"; + } + + static const char *const white() { + return "White"; + } + + static const char *const white_smoke() { + return "WhiteSmoke"; + } + + static const char *const yellow() { + return "Yellow"; + } + + static const char *const yellow_green() { + return "YellowGreen"; + } + }; + + template<> + struct basic_named_color { + static const wchar_t *const alice_blue() { + return L"AliceBlue"; + } + + static const wchar_t *const antique_white() { + return L"AntiqueWhite"; + } + + static const wchar_t *const aqua() { + return L"Aqua"; + } + + static const wchar_t *const aquamarine() { + return L"Aquamarine"; + } + + static const wchar_t *const azure() { + return L"Azure"; + } + + static const wchar_t *const beige() { + return L"Beige"; + } + + static const wchar_t *const bisque() { + return L"Bisque"; + } + + static const wchar_t *const black() { + return L"Black"; + } + + static const wchar_t *const blanched_almond() { + return L"BlanchedAlmond"; + } + + static const wchar_t *const blue() { + return L"Blue"; + } + + static const wchar_t *const blue_violet() { + return L"BlueViolet"; + } + + static const wchar_t *const brown() { + return L"Brown"; + } + + static const wchar_t *const burly_wood() { + return L"BurlyWood"; + } + + static const wchar_t *const cadet_blue() { + return L"CadetBlue"; + } + + static const wchar_t *const chartreuse() { + return L"Chartreuse"; + } + + static const wchar_t *const chocolate() { + return L"Chocolate"; + } + + static const wchar_t *const coral() { + return L"Coral"; + } + + static const wchar_t *const cornflower_blue() { + return L"CornflowerBlue"; + } + + static const wchar_t *const cornsilk() { + return L"Cornsilk"; + } + + static const wchar_t *const crimson() { + return L"Crimson"; + } + + static const wchar_t *const cyan() { + return L"Cyan"; + } + + static const wchar_t *const dark_blue() { + return L"DarkBlue"; + } + + static const wchar_t *const dark_cyan() { + return L"DarkCyan"; + } + + static const wchar_t *const dark_goldenrod() { + return L"DarkGoldenrod"; + } + + static const wchar_t *const dark_gray() { + return L"DarkGray"; + } + + static const wchar_t *const dark_green() { + return L"DarkGreen"; + } + + static const wchar_t *const dark_khaki() { + return L"DarkKhaki"; + } + + static const wchar_t *const dark_magenta() { + return L"DarkMagenta"; + } + + static const wchar_t *const dark_olive_green() { + return L"DarkOliveGreen"; + } + + static const wchar_t *const dark_orange() { + return L"DarkOrange"; + } + + static const wchar_t *const dark_orchid() { + return L"DarkOrchid"; + } + + static const wchar_t *const dark_red() { + return L"DarkRed"; + } + + static const wchar_t *const dark_salmon() { + return L"DarkSalmon"; + } + + static const wchar_t *const dark_sea_green() { + return L"DarkSeaGreen"; + } + + static const wchar_t *const dark_slate_blue() { + return L"DarkSlateBlue"; + } + + static const wchar_t *const dark_slate_gray() { + return L"DarkSlateGray"; + } + + static const wchar_t *const dark_turquoise() { + return L"DarkTurquoise"; + } + + static const wchar_t *const dark_violet() { + return L"DarkViolet"; + } + + static const wchar_t *const deep_pink() { + return L"DeepPink"; + } + + static const wchar_t *const deep_sky_blue() { + return L"DeepSkyBlue"; + } + + static const wchar_t *const dim_gray() { + return L"DimGray"; + } + + static const wchar_t *const dodger_blue() { + return L"DodgerBlue"; + } + + static const wchar_t *const firebrick() { + return L"Firebrick"; + } + + static const wchar_t *const floral_white() { + return L"FloralWhite"; + } + + static const wchar_t *const forest_green() { + return L"ForestGreen"; + } + + static const wchar_t *const fuchsia() { + return L"Fuchsia"; + } + + static const wchar_t *const gainsboro() { + return L"Gainsboro"; + } + + static const wchar_t *const ghost_white() { + return L"GhostWhite"; + } + + static const wchar_t *const gold() { + return L"Gold"; + } + + static const wchar_t *const goldenrod() { + return L"Goldenrod"; + } + + static const wchar_t *const gray() { + return L"Gray"; + } + + static const wchar_t *const green() { + return L"Green"; + } + + static const wchar_t *const green_yellow() { + return L"GreenYellow"; + } + + static const wchar_t *const honeydew() { + return L"Honeydew"; + } + + static const wchar_t *const hot_pink() { + return L"HotPink"; + } + + static const wchar_t *const indian_red() { + return L"IndianRed"; + } + + static const wchar_t *const indigo() { + return L"Indigo"; + } + + static const wchar_t *const ivory() { + return L"Ivory"; + } + + static const wchar_t *const khaki() { + return L"Khaki"; + } + + static const wchar_t *const lavender() { + return L"Lavender"; + } + + static const wchar_t *const lavender_blush() { + return L"LavenderBlush"; + } + + static const wchar_t *const lawn_green() { + return L"LawnGreen"; + } + + static const wchar_t *const lemon_chiffon() { + return L"LemonChiffon"; + } + + static const wchar_t *const light_blue() { + return L"LightBlue"; + } + + static const wchar_t *const light_coral() { + return L"LightCoral"; + } + + static const wchar_t *const light_cyan() { + return L"LightCyan"; + } + + static const wchar_t *const light_goldenrod_yellow() { + return L"LightGoldenrodYellow"; + } + + static const wchar_t *const light_gray() { + return L"LightGray"; + } + + static const wchar_t *const light_green() { + return L"LightGreen"; + } + + static const wchar_t *const light_pink() { + return L"LightPink"; + } + + static const wchar_t *const light_salmon() { + return L"LightSalmon"; + } + + static const wchar_t *const light_sea_green() { + return L"LightSeaGreen"; + } + + static const wchar_t *const light_sky_blue() { + return L"LightSkyBlue"; + } + + static const wchar_t *const light_slate_gray() { + return L"LightSlateGray"; + } + + static const wchar_t *const light_steel_blue() { + return L"LightSteelBlue"; + } + + static const wchar_t *const light_yellow() { + return L"LightYellow"; + } + + static const wchar_t *const lime() { + return L"Lime"; + } + + static const wchar_t *const lime_green() { + return L"LimeGreen"; + } + + static const wchar_t *const linen() { + return L"Linen"; + } + + static const wchar_t *const magenta() { + return L"Magenta"; + } + + static const wchar_t *const maroon() { + return L"Maroon"; + } + + static const wchar_t *const medium_aquamarine() { + return L"MediumAquamarine"; + } + + static const wchar_t *const medium_blue() { + return L"MediumBlue"; + } + + static const wchar_t *const medium_orchid() { + return L"MediumOrchid"; + } + + static const wchar_t *const medium_purple() { + return L"MediumPurple"; + } + + static const wchar_t *const medium_sea_green() { + return L"MediumSeaGreen"; + } + + static const wchar_t *const medium_slate_blue() { + return L"MediumSlateBlue"; + } + + static const wchar_t *const medium_spring_green() { + return L"MediumSpringGreen"; + } + + static const wchar_t *const medium_turquoise() { + return L"MediumTurquoise"; + } + + static const wchar_t *const medium_violet_red() { + return L"MediumVioletRed"; + } + + static const wchar_t *const midnight_blue() { + return L"MidnightBlue"; + } + + static const wchar_t *const mint_cream() { + return L"MintCream"; + } + + static const wchar_t *const misty_rose() { + return L"MistyRose"; + } + + static const wchar_t *const moccasin() { + return L"Moccasin"; + } + + static const wchar_t *const navajo_white() { + return L"NavajoWhite"; + } + + static const wchar_t *const navy() { + return L"Navy"; + } + + static const wchar_t *const old_lace() { + return L"OldLace"; + } + + static const wchar_t *const olive() { + return L"Olive"; + } + + static const wchar_t *const olive_drab() { + return L"OliveDrab"; + } + + static const wchar_t *const orange() { + return L"Orange"; + } + + static const wchar_t *const orange_red() { + return L"OrangeRed"; + } + + static const wchar_t *const orchid() { + return L"Orchid"; + } + + static const wchar_t *const pale_goldenrod() { + return L"PaleGoldenrod"; + } + + static const wchar_t *const pale_green() { + return L"PaleGreen"; + } + + static const wchar_t *const pale_turquoise() { + return L"PaleTurquoise"; + } + + static const wchar_t *const pale_violet_red() { + return L"PaleVioletRed"; + } + + static const wchar_t *const papaya_whip() { + return L"PapayaWhip"; + } + + static const wchar_t *const peach_puff() { + return L"PeachPuff"; + } + + static const wchar_t *const peru() { + return L"Peru"; + } + + static const wchar_t *const pink() { + return L"Pink"; + } + + static const wchar_t *const plum() { + return L"Plum"; + } + + static const wchar_t *const powder_blue() { + return L"PowderBlue"; + } + + static const wchar_t *const purple() { + return L"Purple"; + } + + static const wchar_t *const red() { + return L"Red"; + } + + static const wchar_t *const rosy_brown() { + return L"RosyBrown"; + } + + static const wchar_t *const royal_blue() { + return L"RoyalBlue"; + } + + static const wchar_t *const saddle_brown() { + return L"SaddleBrown"; + } + + static const wchar_t *const salmon() { + return L"Salmon"; + } + + static const wchar_t *const sandy_brown() { + return L"SandyBrown"; + } + + static const wchar_t *const sea_green() { + return L"SeaGreen"; + } + + static const wchar_t *const sea_shell() { + return L"SeaShell"; + } + + static const wchar_t *const sienna() { + return L"Sienna"; + } + + static const wchar_t *const silver() { + return L"Silver"; + } + + static const wchar_t *const sky_blue() { + return L"SkyBlue"; + } + + static const wchar_t *const slate_blue() { + return L"SlateBlue"; + } + + static const wchar_t *const slate_gray() { + return L"SlateGray"; + } + + static const wchar_t *const snow() { + return L"Snow"; + } + + static const wchar_t *const spring_green() { + return L"SpringGreen"; + } + + static const wchar_t *const steel_blue() { + return L"SteelBlue"; + } + + static const wchar_t *const tan() { + return L"Tan"; + } + + static const wchar_t *const teal() { + return L"Teal"; + } + + static const wchar_t *const thistle() { + return L"Thistle"; + } + + static const wchar_t *const tomato() { + return L"Tomato"; + } + + static const wchar_t *const transparent() { + return L"Transparent"; + } + + static const wchar_t *const turquoise() { + return L"Turquoise"; + } + + static const wchar_t *const violet() { + return L"Violet"; + } + + static const wchar_t *const wheat() { + return L"Wheat"; + } + + static const wchar_t *const white() { + return L"White"; + } + + static const wchar_t *const white_smoke() { + return L"WhiteSmoke"; + } + + static const wchar_t *const yellow() { + return L"Yellow"; + } + + static const wchar_t *const yellow_green() { + return L"YellowGreen"; + } + }; + + template + struct basic_color_mapper { + typedef CharT char_type; + typedef std::basic_string string_type; + + typedef known_color known_color_type; + typedef basic_named_color named_color_type; + + typedef std::map string_color_map; + typedef std::map color_string_map; + + // Build a name -> constant map + static const string_color_map &get_string_to_color_map() { + static string_color_map sm_; // name -> value + if (sm_.empty()) { + sm_.insert(std::make_pair(to_lowercase(named_color_type::alice_blue()), known_color::alice_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::antique_white()), known_color::antique_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::aqua()), known_color::aqua)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::aquamarine()), known_color::aquamarine)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::azure()), known_color::azure)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::beige()), known_color::beige)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::bisque()), known_color::bisque)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::black()), known_color::black)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::blanched_almond()), known_color::blanched_almond)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::blue()), known_color::blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::blue_violet()), known_color::blue_violet)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::brown()), known_color::brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::burly_wood()), known_color::burly_wood)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cadet_blue()), known_color::cadet_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::chartreuse()), known_color::chartreuse)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::chocolate()), known_color::chocolate)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::coral()), known_color::coral)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cornflower_blue()), known_color::cornflower_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cornsilk()), known_color::cornsilk)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::crimson()), known_color::crimson)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cyan()), known_color::cyan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_blue()), known_color::dark_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_cyan()), known_color::dark_cyan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_goldenrod()), known_color::dark_goldenrod)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_gray()), known_color::dark_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_green()), known_color::dark_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_khaki()), known_color::dark_khaki)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_magenta()), known_color::dark_magenta)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_olive_green()), known_color::dark_olive_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_orange()), known_color::dark_orange)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_orchid()), known_color::dark_orchid)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_red()), known_color::dark_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_salmon()), known_color::dark_salmon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_sea_green()), known_color::dark_sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_slate_blue()), known_color::dark_slate_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_slate_gray()), known_color::dark_slate_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_turquoise()), known_color::dark_turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_violet()), known_color::dark_violet)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::deep_pink()), known_color::deep_pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::deep_sky_blue()), known_color::deep_sky_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dim_gray()), known_color::dim_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dodger_blue()), known_color::dodger_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::firebrick()), known_color::firebrick)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::floral_white()), known_color::floral_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::forest_green()), known_color::forest_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::fuchsia()), known_color::fuchsia)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::gainsboro()), known_color::gainsboro)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::ghost_white()), known_color::ghost_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::gold()), known_color::gold)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::goldenrod()), known_color::goldenrod)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::gray()), known_color::gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::green()), known_color::green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::green_yellow()), known_color::green_yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::honeydew()), known_color::honeydew)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::hot_pink()), known_color::hot_pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::indian_red()), known_color::indian_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::indigo()), known_color::indigo)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::ivory()), known_color::ivory)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::khaki()), known_color::khaki)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lavender()), known_color::lavender)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lavender_blush()), known_color::lavender_blush)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lawn_green()), known_color::lawn_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lemon_chiffon()), known_color::lemon_chiffon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_blue()), known_color::light_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_coral()), known_color::light_coral)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_cyan()), known_color::light_cyan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_goldenrod_yellow()), known_color::light_goldenrod_yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_gray()), known_color::light_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_green()), known_color::light_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_pink()), known_color::light_pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_salmon()), known_color::light_salmon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_sea_green()), known_color::light_sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_sky_blue()), known_color::light_sky_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_slate_gray()), known_color::light_slate_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_steel_blue()), known_color::light_steel_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_yellow()), known_color::light_yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lime()), known_color::lime)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lime_green()), known_color::lime_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::linen()), known_color::linen)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::magenta()), known_color::magenta)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::maroon()), known_color::maroon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_aquamarine()), known_color::medium_aquamarine)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_blue()), known_color::medium_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_orchid()), known_color::medium_orchid)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_purple()), known_color::medium_purple)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_sea_green()), known_color::medium_sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_slate_blue()), known_color::medium_slate_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_spring_green()), known_color::medium_spring_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_turquoise()), known_color::medium_turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_violet_red()), known_color::medium_violet_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::midnight_blue()), known_color::midnight_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::mint_cream()), known_color::mint_cream)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::misty_rose()), known_color::misty_rose)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::moccasin()), known_color::moccasin)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::navajo_white()), known_color::navajo_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::navy()), known_color::navy)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::old_lace()), known_color::old_lace)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::olive()), known_color::olive)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::olive_drab()), known_color::olive_drab)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::orange()), known_color::orange)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::orange_red()), known_color::orange_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::orchid()), known_color::orchid)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_goldenrod()), known_color::pale_goldenrod)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_green()), known_color::pale_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_turquoise()), known_color::pale_turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_violet_red()), known_color::pale_violet_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::papaya_whip()), known_color::papaya_whip)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::peach_puff()), known_color::peach_puff)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::peru()), known_color::peru)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pink()), known_color::pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::plum()), known_color::plum)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::powder_blue()), known_color::powder_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::purple()), known_color::purple)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::red()), known_color::red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::rosy_brown()), known_color::rosy_brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::royal_blue()), known_color::royal_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::saddle_brown()), known_color::saddle_brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::salmon()), known_color::salmon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sandy_brown()), known_color::sandy_brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sea_green()), known_color::sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sea_shell()), known_color::sea_shell)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sienna()), known_color::sienna)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::silver()), known_color::silver)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sky_blue()), known_color::sky_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::slate_blue()), known_color::slate_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::slate_gray()), known_color::slate_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::snow()), known_color::snow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::spring_green()), known_color::spring_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::steel_blue()), known_color::steel_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::tan()), known_color::tan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::teal()), known_color::teal)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::thistle()), known_color::thistle)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::tomato()), known_color::tomato)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::transparent()), known_color::transparent)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::turquoise()), known_color::turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::violet()), known_color::violet)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::wheat()), known_color::wheat)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::white()), known_color::white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::white_smoke()), known_color::white_smoke)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::yellow()), known_color::yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::yellow_green()), known_color::yellow_green)); + } + + return sm_; + } + + // Build a name -> constant map + static const color_string_map &get_color_to_string_map() { + static color_string_map vm_; // value -> name + + if (vm_.empty()) { + vm_.insert(std::make_pair(value_of(known_color::alice_blue), named_color_type::alice_blue())); + vm_.insert(std::make_pair(value_of(known_color::antique_white), named_color_type::antique_white())); + vm_.insert(std::make_pair(value_of(known_color::aqua), named_color_type::aqua())); + vm_.insert(std::make_pair(value_of(known_color::aquamarine), named_color_type::aquamarine())); + vm_.insert(std::make_pair(value_of(known_color::azure), named_color_type::azure())); + vm_.insert(std::make_pair(value_of(known_color::beige), named_color_type::beige())); + vm_.insert(std::make_pair(value_of(known_color::bisque), named_color_type::bisque())); + vm_.insert(std::make_pair(value_of(known_color::black), named_color_type::black())); + vm_.insert(std::make_pair(value_of(known_color::blanched_almond), named_color_type::blanched_almond())); + vm_.insert(std::make_pair(value_of(known_color::blue), named_color_type::blue())); + vm_.insert(std::make_pair(value_of(known_color::blue_violet), named_color_type::blue_violet())); + vm_.insert(std::make_pair(value_of(known_color::brown), named_color_type::brown())); + vm_.insert(std::make_pair(value_of(known_color::burly_wood), named_color_type::burly_wood())); + vm_.insert(std::make_pair(value_of(known_color::cadet_blue), named_color_type::cadet_blue())); + vm_.insert(std::make_pair(value_of(known_color::chartreuse), named_color_type::chartreuse())); + vm_.insert(std::make_pair(value_of(known_color::chocolate), named_color_type::chocolate())); + vm_.insert(std::make_pair(value_of(known_color::coral), named_color_type::coral())); + vm_.insert(std::make_pair(value_of(known_color::cornflower_blue), named_color_type::cornflower_blue())); + vm_.insert(std::make_pair(value_of(known_color::cornsilk), named_color_type::cornsilk())); + vm_.insert(std::make_pair(value_of(known_color::crimson), named_color_type::crimson())); + vm_.insert(std::make_pair(value_of(known_color::cyan), named_color_type::cyan())); + vm_.insert(std::make_pair(value_of(known_color::dark_blue), named_color_type::dark_blue())); + vm_.insert(std::make_pair(value_of(known_color::dark_cyan), named_color_type::dark_cyan())); + vm_.insert(std::make_pair(value_of(known_color::dark_goldenrod), named_color_type::dark_goldenrod())); + vm_.insert(std::make_pair(value_of(known_color::dark_gray), named_color_type::dark_gray())); + vm_.insert(std::make_pair(value_of(known_color::dark_green), named_color_type::dark_green())); + vm_.insert(std::make_pair(value_of(known_color::dark_khaki), named_color_type::dark_khaki())); + vm_.insert(std::make_pair(value_of(known_color::dark_magenta), named_color_type::dark_magenta())); + vm_.insert(std::make_pair(value_of(known_color::dark_olive_green), named_color_type::dark_olive_green())); + vm_.insert(std::make_pair(value_of(known_color::dark_orange), named_color_type::dark_orange())); + vm_.insert(std::make_pair(value_of(known_color::dark_orchid), named_color_type::dark_orchid())); + vm_.insert(std::make_pair(value_of(known_color::dark_red), named_color_type::dark_red())); + vm_.insert(std::make_pair(value_of(known_color::dark_salmon), named_color_type::dark_salmon())); + vm_.insert(std::make_pair(value_of(known_color::dark_sea_green), named_color_type::dark_sea_green())); + vm_.insert(std::make_pair(value_of(known_color::dark_slate_blue), named_color_type::dark_slate_blue())); + vm_.insert(std::make_pair(value_of(known_color::dark_slate_gray), named_color_type::dark_slate_gray())); + vm_.insert(std::make_pair(value_of(known_color::dark_turquoise), named_color_type::dark_turquoise())); + vm_.insert(std::make_pair(value_of(known_color::dark_violet), named_color_type::dark_violet())); + vm_.insert(std::make_pair(value_of(known_color::deep_pink), named_color_type::deep_pink())); + vm_.insert(std::make_pair(value_of(known_color::deep_sky_blue), named_color_type::deep_sky_blue())); + vm_.insert(std::make_pair(value_of(known_color::dim_gray), named_color_type::dim_gray())); + vm_.insert(std::make_pair(value_of(known_color::dodger_blue), named_color_type::dodger_blue())); + vm_.insert(std::make_pair(value_of(known_color::firebrick), named_color_type::firebrick())); + vm_.insert(std::make_pair(value_of(known_color::floral_white), named_color_type::floral_white())); + vm_.insert(std::make_pair(value_of(known_color::forest_green), named_color_type::forest_green())); + vm_.insert(std::make_pair(value_of(known_color::fuchsia), named_color_type::fuchsia())); + vm_.insert(std::make_pair(value_of(known_color::gainsboro), named_color_type::gainsboro())); + vm_.insert(std::make_pair(value_of(known_color::ghost_white), named_color_type::ghost_white())); + vm_.insert(std::make_pair(value_of(known_color::gold), named_color_type::gold())); + vm_.insert(std::make_pair(value_of(known_color::goldenrod), named_color_type::goldenrod())); + vm_.insert(std::make_pair(value_of(known_color::gray), named_color_type::gray())); + vm_.insert(std::make_pair(value_of(known_color::green), named_color_type::green())); + vm_.insert(std::make_pair(value_of(known_color::green_yellow), named_color_type::green_yellow())); + vm_.insert(std::make_pair(value_of(known_color::honeydew), named_color_type::honeydew())); + vm_.insert(std::make_pair(value_of(known_color::hot_pink), named_color_type::hot_pink())); + vm_.insert(std::make_pair(value_of(known_color::indian_red), named_color_type::indian_red())); + vm_.insert(std::make_pair(value_of(known_color::indigo), named_color_type::indigo())); + vm_.insert(std::make_pair(value_of(known_color::ivory), named_color_type::ivory())); + vm_.insert(std::make_pair(value_of(known_color::khaki), named_color_type::khaki())); + vm_.insert(std::make_pair(value_of(known_color::lavender), named_color_type::lavender())); + vm_.insert(std::make_pair(value_of(known_color::lavender_blush), named_color_type::lavender_blush())); + vm_.insert(std::make_pair(value_of(known_color::lawn_green), named_color_type::lawn_green())); + vm_.insert(std::make_pair(value_of(known_color::lemon_chiffon), named_color_type::lemon_chiffon())); + vm_.insert(std::make_pair(value_of(known_color::light_blue), named_color_type::light_blue())); + vm_.insert(std::make_pair(value_of(known_color::light_coral), named_color_type::light_coral())); + vm_.insert(std::make_pair(value_of(known_color::light_cyan), named_color_type::light_cyan())); + vm_.insert(std::make_pair(value_of(known_color::light_goldenrod_yellow), named_color_type::light_goldenrod_yellow())); + vm_.insert(std::make_pair(value_of(known_color::light_gray), named_color_type::light_gray())); + vm_.insert(std::make_pair(value_of(known_color::light_green), named_color_type::light_green())); + vm_.insert(std::make_pair(value_of(known_color::light_pink), named_color_type::light_pink())); + vm_.insert(std::make_pair(value_of(known_color::light_salmon), named_color_type::light_salmon())); + vm_.insert(std::make_pair(value_of(known_color::light_sea_green), named_color_type::light_sea_green())); + vm_.insert(std::make_pair(value_of(known_color::light_sky_blue), named_color_type::light_sky_blue())); + vm_.insert(std::make_pair(value_of(known_color::light_slate_gray), named_color_type::light_slate_gray())); + vm_.insert(std::make_pair(value_of(known_color::light_steel_blue), named_color_type::light_steel_blue())); + vm_.insert(std::make_pair(value_of(known_color::light_yellow), named_color_type::light_yellow())); + vm_.insert(std::make_pair(value_of(known_color::lime), named_color_type::lime())); + vm_.insert(std::make_pair(value_of(known_color::lime_green), named_color_type::lime_green())); + vm_.insert(std::make_pair(value_of(known_color::linen), named_color_type::linen())); + vm_.insert(std::make_pair(value_of(known_color::magenta), named_color_type::magenta())); + vm_.insert(std::make_pair(value_of(known_color::maroon), named_color_type::maroon())); + vm_.insert(std::make_pair(value_of(known_color::medium_aquamarine), named_color_type::medium_aquamarine())); + vm_.insert(std::make_pair(value_of(known_color::medium_blue), named_color_type::medium_blue())); + vm_.insert(std::make_pair(value_of(known_color::medium_orchid), named_color_type::medium_orchid())); + vm_.insert(std::make_pair(value_of(known_color::medium_purple), named_color_type::medium_purple())); + vm_.insert(std::make_pair(value_of(known_color::medium_sea_green), named_color_type::medium_sea_green())); + vm_.insert(std::make_pair(value_of(known_color::medium_slate_blue), named_color_type::medium_slate_blue())); + vm_.insert(std::make_pair(value_of(known_color::medium_spring_green), named_color_type::medium_spring_green())); + vm_.insert(std::make_pair(value_of(known_color::medium_turquoise), named_color_type::medium_turquoise())); + vm_.insert(std::make_pair(value_of(known_color::medium_violet_red), named_color_type::medium_violet_red())); + vm_.insert(std::make_pair(value_of(known_color::midnight_blue), named_color_type::midnight_blue())); + vm_.insert(std::make_pair(value_of(known_color::mint_cream), named_color_type::mint_cream())); + vm_.insert(std::make_pair(value_of(known_color::misty_rose), named_color_type::misty_rose())); + vm_.insert(std::make_pair(value_of(known_color::moccasin), named_color_type::moccasin())); + vm_.insert(std::make_pair(value_of(known_color::navajo_white), named_color_type::navajo_white())); + vm_.insert(std::make_pair(value_of(known_color::navy), named_color_type::navy())); + vm_.insert(std::make_pair(value_of(known_color::old_lace), named_color_type::old_lace())); + vm_.insert(std::make_pair(value_of(known_color::olive), named_color_type::olive())); + vm_.insert(std::make_pair(value_of(known_color::olive_drab), named_color_type::olive_drab())); + vm_.insert(std::make_pair(value_of(known_color::orange), named_color_type::orange())); + vm_.insert(std::make_pair(value_of(known_color::orange_red), named_color_type::orange_red())); + vm_.insert(std::make_pair(value_of(known_color::orchid), named_color_type::orchid())); + vm_.insert(std::make_pair(value_of(known_color::pale_goldenrod), named_color_type::pale_goldenrod())); + vm_.insert(std::make_pair(value_of(known_color::pale_green), named_color_type::pale_green())); + vm_.insert(std::make_pair(value_of(known_color::pale_turquoise), named_color_type::pale_turquoise())); + vm_.insert(std::make_pair(value_of(known_color::pale_violet_red), named_color_type::pale_violet_red())); + vm_.insert(std::make_pair(value_of(known_color::papaya_whip), named_color_type::papaya_whip())); + vm_.insert(std::make_pair(value_of(known_color::peach_puff), named_color_type::peach_puff())); + vm_.insert(std::make_pair(value_of(known_color::peru), named_color_type::peru())); + vm_.insert(std::make_pair(value_of(known_color::pink), named_color_type::pink())); + vm_.insert(std::make_pair(value_of(known_color::plum), named_color_type::plum())); + vm_.insert(std::make_pair(value_of(known_color::powder_blue), named_color_type::powder_blue())); + vm_.insert(std::make_pair(value_of(known_color::purple), named_color_type::purple())); + vm_.insert(std::make_pair(value_of(known_color::red), named_color_type::red())); + vm_.insert(std::make_pair(value_of(known_color::rosy_brown), named_color_type::rosy_brown())); + vm_.insert(std::make_pair(value_of(known_color::royal_blue), named_color_type::royal_blue())); + vm_.insert(std::make_pair(value_of(known_color::saddle_brown), named_color_type::saddle_brown())); + vm_.insert(std::make_pair(value_of(known_color::salmon), named_color_type::salmon())); + vm_.insert(std::make_pair(value_of(known_color::sandy_brown), named_color_type::sandy_brown())); + vm_.insert(std::make_pair(value_of(known_color::sea_green), named_color_type::sea_green())); + vm_.insert(std::make_pair(value_of(known_color::sea_shell), named_color_type::sea_shell())); + vm_.insert(std::make_pair(value_of(known_color::sienna), named_color_type::sienna())); + vm_.insert(std::make_pair(value_of(known_color::silver), named_color_type::silver())); + vm_.insert(std::make_pair(value_of(known_color::sky_blue), named_color_type::sky_blue())); + vm_.insert(std::make_pair(value_of(known_color::slate_blue), named_color_type::slate_blue())); + vm_.insert(std::make_pair(value_of(known_color::slate_gray), named_color_type::slate_gray())); + vm_.insert(std::make_pair(value_of(known_color::snow), named_color_type::snow())); + vm_.insert(std::make_pair(value_of(known_color::spring_green), named_color_type::spring_green())); + vm_.insert(std::make_pair(value_of(known_color::steel_blue), named_color_type::steel_blue())); + vm_.insert(std::make_pair(value_of(known_color::tan), named_color_type::tan())); + vm_.insert(std::make_pair(value_of(known_color::teal), named_color_type::teal())); + vm_.insert(std::make_pair(value_of(known_color::thistle), named_color_type::thistle())); + vm_.insert(std::make_pair(value_of(known_color::tomato), named_color_type::tomato())); + vm_.insert(std::make_pair(value_of(known_color::transparent), named_color_type::transparent())); + vm_.insert(std::make_pair(value_of(known_color::turquoise), named_color_type::turquoise())); + vm_.insert(std::make_pair(value_of(known_color::violet), named_color_type::violet())); + vm_.insert(std::make_pair(value_of(known_color::wheat), named_color_type::wheat())); + vm_.insert(std::make_pair(value_of(known_color::white), named_color_type::white())); + vm_.insert(std::make_pair(value_of(known_color::white_smoke), named_color_type::white_smoke())); + vm_.insert(std::make_pair(value_of(known_color::yellow), named_color_type::yellow())); + vm_.insert(std::make_pair(value_of(known_color::yellow_green), named_color_type::yellow_green())); + } + + return vm_; + } + + private: + basic_color_mapper() = delete; + ~basic_color_mapper() = delete; + basic_color_mapper(const basic_color_mapper&) = delete; + basic_color_mapper& operator =(const basic_color_mapper&) = delete; + + private: + // Convert string to lower case + static string_type to_lowercase(const string_type &str) { + static std::locale loc; + std::string result; + result.reserve(str.size()); + + for (auto elem : str) + result += std::tolower(elem, loc); + + return result; + } + }; + + } // namespace dotnet +} // namespace colors diff --git a/extern/cpp-colors/include/cpp-colors/impl/named_impl.h b/extern/cpp-colors/include/cpp-colors/impl/named_impl.h new file mode 100644 index 000000000..878f40dcf --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/impl/named_impl.h @@ -0,0 +1,18 @@ +#pragma once + +#include "dotnet/dotnet_named.h" +#include "wpf/wpf_named.h" +#include "x11/x11_named.h" + +namespace colors { + + typedef dotnet::basic_named_color dotnet_named_color; + typedef dotnet::basic_color_mapper dotnet_color_mapper; + + typedef wpf::basic_named_color wpf_named_color; + typedef wpf::basic_color_mapper wpf_color_mapper; + + typedef x11::basic_named_color x11_named_color; + typedef x11::basic_color_mapper x11_color_mapper; + +} // namespace colors diff --git a/extern/cpp-colors/include/cpp-colors/impl/named_utils.h b/extern/cpp-colors/include/cpp-colors/impl/named_utils.h new file mode 100644 index 000000000..ac0574012 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/impl/named_utils.h @@ -0,0 +1,10 @@ +#pragma once + +namespace colors { + + template + typename std::underlying_type::type value_of(E e) { + return static_cast::type>(e); + } + +} diff --git a/extern/cpp-colors/include/cpp-colors/impl/wpf/wpf_colors.h b/extern/cpp-colors/include/cpp-colors/impl/wpf/wpf_colors.h new file mode 100644 index 000000000..fde395ab3 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/impl/wpf/wpf_colors.h @@ -0,0 +1,81 @@ +#pragma once + +#include "wpf_constants.h" +#include "../named_utils.h" + + +namespace colors { + namespace wpf { + + template + struct basic_colors { + typedef ColorType color_type; + + basic_colors() = delete; + ~basic_colors() = delete; + basic_colors(const basic_colors&) = delete; + basic_colors& operator =(const basic_colors&) = delete; + + static color_type black() { + return color_type(value_of(known_color::black)); + } + + static color_type blue() { + return color_type(value_of(known_color::blue)); + } + + static color_type brown() { + return color_type(value_of(known_color::brown)); + } + + static color_type cyan() { + return color_type(value_of(known_color::cyan)); + } + + static color_type dark_gray() { + return color_type(value_of(known_color::dark_gray)); + } + + static color_type gray() { + return color_type(value_of(known_color::gray)); + } + + static color_type green() { + return color_type(value_of(known_color::green)); + } + + static color_type light_gray() { + return color_type(value_of(known_color::light_gray)); + } + + static color_type magenta() { + return color_type(value_of(known_color::magenta)); + } + + static color_type orange() { + return color_type(value_of(known_color::orange)); + } + + static color_type purple() { + return color_type(value_of(known_color::purple)); + } + + static color_type red() { + return color_type(value_of(known_color::red)); + } + + static color_type transparent() { + return color_type(value_of(known_color::transparent)); + } + + static color_type white() { + return color_type(value_of(known_color::white)); + } + + static color_type yellow() { + return color_type(value_of(known_color::yellow)); + } + }; + + } // namespace wpf +} // namespace colors diff --git a/extern/cpp-colors/include/cpp-colors/impl/wpf/wpf_constants.h b/extern/cpp-colors/include/cpp-colors/impl/wpf/wpf_constants.h new file mode 100644 index 000000000..2b756e4da --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/impl/wpf/wpf_constants.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace colors { + namespace wpf { + + enum class known_color : uint32_t { + black = 0xFF000000, // Black: argb(255, 0, 0, 0) + blue = 0xFF0000FF, // Blue: argb(255, 0, 0, 255) + brown = 0xFFA52A2A, // Brown: argb(255, 165, 42, 42) + cyan = 0xFF00FFFF, // Cyan: argb(255, 0, 255, 255) + dark_gray = 0xFFA9A9A9, // Dark Gray: argb(255, 169, 169, 169) + gray = 0xFF808080, // Gray: argb(255, 128, 128, 128) + green = 0xFF008000, // Green: argb(255, 0, 128, 0) + light_gray = 0xFFD3D3D3, // Light Gray: argb(255, 211, 211, 211) + magenta = 0xFFFF00FF, // Magenta: argb(255, 255, 0, 255) + orange = 0xFFFFA500, // Orange: argb(255, 255, 165, 0) + purple = 0xFF800080, // Purple: argb(255, 128, 0, 128) + red = 0xFFFF0000, // Red: argb(255, 255, 0, 0) + transparent = 0x00FFFFFF, // Transparent: argb(0, 255, 255, 255) + white = 0xFFFFFFFF, // White: argb(255, 255, 255, 255) + yellow = 0xFFFFFF00, // Yellow: argb(255, 255, 255, 0) + }; + + } // namespace wpf +} // namespace colors diff --git a/extern/cpp-colors/include/cpp-colors/impl/wpf/wpf_named.h b/extern/cpp-colors/include/cpp-colors/impl/wpf/wpf_named.h new file mode 100644 index 000000000..eafefeef9 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/impl/wpf/wpf_named.h @@ -0,0 +1,224 @@ +#pragma once + +#include +#include +#include +#include +#include "wpf_constants.h" +#include "../named_utils.h" + + +namespace colors { + namespace wpf { + + template + struct basic_named_color; + + template<> + struct basic_named_color { + static const char *const black() { + return "Black"; + } + + static const char *const blue() { + return "Blue"; + } + + static const char *const brown() { + return "Brown"; + } + + static const char *const cyan() { + return "Cyan"; + } + + static const char *const dark_gray() { + return "DarkGray"; + } + + static const char *const gray() { + return "Gray"; + } + + static const char *const green() { + return "Green"; + } + + static const char *const light_gray() { + return "LightGray"; + } + + static const char *const magenta() { + return "Magenta"; + } + + static const char *const orange() { + return "Orange"; + } + + static const char *const purple() { + return "Purple"; + } + + static const char *const red() { + return "Red"; + } + + static const char *const transparent() { + return "Transparent"; + } + + static const char *const white() { + return "White"; + } + + static const char *const yellow() { + return "Yellow"; + } + }; + + template<> + struct basic_named_color { + static const wchar_t *const black() { + return L"Black"; + } + + static const wchar_t *const blue() { + return L"Blue"; + } + + static const wchar_t *const brown() { + return L"Brown"; + } + + static const wchar_t *const cyan() { + return L"Cyan"; + } + + static const wchar_t *const dark_gray() { + return L"DarkGray"; + } + + static const wchar_t *const gray() { + return L"Gray"; + } + + static const wchar_t *const green() { + return L"Green"; + } + + static const wchar_t *const light_gray() { + return L"LightGray"; + } + + static const wchar_t *const magenta() { + return L"Magenta"; + } + + static const wchar_t *const orange() { + return L"Orange"; + } + + static const wchar_t *const purple() { + return L"Purple"; + } + + static const wchar_t *const red() { + return L"Red"; + } + + static const wchar_t *const transparent() { + return L"Transparent"; + } + + static const wchar_t *const white() { + return L"White"; + } + + static const wchar_t *const yellow() { + return L"Yellow"; + } + }; + + template + struct basic_color_mapper { + typedef CharT char_type; + typedef std::basic_string string_type; + + typedef known_color known_color_type; + typedef basic_named_color named_color_type; + + typedef std::map string_color_map; + typedef std::map color_string_map; + + // Build a name -> constant map + static const string_color_map &get_string_to_color_map() { + static string_color_map sm_; // name -> value + if (sm_.empty()) { + sm_.insert(std::make_pair(to_lowercase(named_color_type::black()), known_color::black)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::blue()), known_color::blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::brown()), known_color::brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cyan()), known_color::cyan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_gray()), known_color::dark_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::gray()), known_color::gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::green()), known_color::green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_gray()), known_color::light_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::magenta()), known_color::magenta)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::orange()), known_color::orange)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::purple()), known_color::purple)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::red()), known_color::red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::transparent()), known_color::transparent)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::white()), known_color::white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::yellow()), known_color::yellow)); + } + + return sm_; + } + + // Build a name -> constant map + static const color_string_map &get_color_to_string_map() { + static color_string_map vm_; // value -> name + + if (vm_.empty()) { + vm_.insert(std::make_pair(value_of(known_color::black), named_color_type::black())); + vm_.insert(std::make_pair(value_of(known_color::blue), named_color_type::blue())); + vm_.insert(std::make_pair(value_of(known_color::brown), named_color_type::brown())); + vm_.insert(std::make_pair(value_of(known_color::cyan), named_color_type::cyan())); + vm_.insert(std::make_pair(value_of(known_color::dark_gray), named_color_type::dark_gray())); + vm_.insert(std::make_pair(value_of(known_color::gray), named_color_type::gray())); + vm_.insert(std::make_pair(value_of(known_color::green), named_color_type::green())); + vm_.insert(std::make_pair(value_of(known_color::light_gray), named_color_type::light_gray())); + vm_.insert(std::make_pair(value_of(known_color::magenta), named_color_type::magenta())); + vm_.insert(std::make_pair(value_of(known_color::orange), named_color_type::orange())); + vm_.insert(std::make_pair(value_of(known_color::purple), named_color_type::purple())); + vm_.insert(std::make_pair(value_of(known_color::red), named_color_type::red())); + vm_.insert(std::make_pair(value_of(known_color::transparent), named_color_type::transparent())); + vm_.insert(std::make_pair(value_of(known_color::white), named_color_type::white())); + vm_.insert(std::make_pair(value_of(known_color::yellow), named_color_type::yellow())); + } + + return vm_; + } + + private: + basic_color_mapper() = delete; + ~basic_color_mapper() = delete; + basic_color_mapper(const basic_color_mapper&) = delete; + basic_color_mapper& operator =(const basic_color_mapper&) = delete; + + private: + // Convert string to lower case + static string_type to_lowercase(const string_type &str) { + static std::locale loc; + std::string result; + result.reserve(str.size()); + + for (auto elem : str) + result += std::tolower(elem, loc); + + return result; + } + }; + + } // namespace wpf +} // namespace colors diff --git a/extern/cpp-colors/include/cpp-colors/impl/x11/x11_colors.h b/extern/cpp-colors/include/cpp-colors/impl/x11/x11_colors.h new file mode 100644 index 000000000..e18149d27 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/impl/x11/x11_colors.h @@ -0,0 +1,580 @@ +#pragma once + +#include "x11_constants.h" +#include "../named_utils.h" + + +namespace colors { + namespace x11 { + + template + struct basic_colors { + typedef ColorType color_type; + + basic_colors() = delete; + ~basic_colors() = delete; + basic_colors(const basic_colors&) = delete; + basic_colors& operator =(const basic_colors&) = delete; + + static color_type pink() { + return color_type(value_of(known_color::pink)); + } + + static color_type light_pink() { + return color_type(value_of(known_color::light_pink)); + } + + static color_type hot_pink() { + return color_type(value_of(known_color::hot_pink)); + } + + static color_type deep_pink() { + return color_type(value_of(known_color::deep_pink)); + } + + static color_type pale_violet_red() { + return color_type(value_of(known_color::pale_violet_red)); + } + + static color_type medium_violet_red() { + return color_type(value_of(known_color::medium_violet_red)); + } + + static color_type light_salmon() { + return color_type(value_of(known_color::light_salmon)); + } + + static color_type salmon() { + return color_type(value_of(known_color::salmon)); + } + + static color_type dark_salmon() { + return color_type(value_of(known_color::dark_salmon)); + } + + static color_type light_coral() { + return color_type(value_of(known_color::light_coral)); + } + + static color_type indian_red() { + return color_type(value_of(known_color::indian_red)); + } + + static color_type crimson() { + return color_type(value_of(known_color::crimson)); + } + + static color_type fire_brick() { + return color_type(value_of(known_color::fire_brick)); + } + + static color_type dark_red() { + return color_type(value_of(known_color::dark_red)); + } + + static color_type red() { + return color_type(value_of(known_color::red)); + } + + static color_type orange_red() { + return color_type(value_of(known_color::orange_red)); + } + + static color_type tomato() { + return color_type(value_of(known_color::tomato)); + } + + static color_type coral() { + return color_type(value_of(known_color::coral)); + } + + static color_type dark_orange() { + return color_type(value_of(known_color::dark_orange)); + } + + static color_type orange() { + return color_type(value_of(known_color::orange)); + } + + static color_type yellow() { + return color_type(value_of(known_color::yellow)); + } + + static color_type light_yellow() { + return color_type(value_of(known_color::light_yellow)); + } + + static color_type lemon_chiffon() { + return color_type(value_of(known_color::lemon_chiffon)); + } + + static color_type light_goldenrod_yellow() { + return color_type(value_of(known_color::light_goldenrod_yellow)); + } + + static color_type papaya_whip() { + return color_type(value_of(known_color::papaya_whip)); + } + + static color_type moccasin() { + return color_type(value_of(known_color::moccasin)); + } + + static color_type peach_puff() { + return color_type(value_of(known_color::peach_puff)); + } + + static color_type pale_goldenrod() { + return color_type(value_of(known_color::pale_goldenrod)); + } + + static color_type khaki() { + return color_type(value_of(known_color::khaki)); + } + + static color_type dark_khaki() { + return color_type(value_of(known_color::dark_khaki)); + } + + static color_type gold() { + return color_type(value_of(known_color::gold)); + } + + static color_type cornsilk() { + return color_type(value_of(known_color::cornsilk)); + } + + static color_type blanched_almond() { + return color_type(value_of(known_color::blanched_almond)); + } + + static color_type bisque() { + return color_type(value_of(known_color::bisque)); + } + + static color_type navajo_white() { + return color_type(value_of(known_color::navajo_white)); + } + + static color_type wheat() { + return color_type(value_of(known_color::wheat)); + } + + static color_type burly_wood() { + return color_type(value_of(known_color::burly_wood)); + } + + static color_type tan() { + return color_type(value_of(known_color::tan)); + } + + static color_type rosy_brown() { + return color_type(value_of(known_color::rosy_brown)); + } + + static color_type sandy_brown() { + return color_type(value_of(known_color::sandy_brown)); + } + + static color_type goldenrod() { + return color_type(value_of(known_color::goldenrod)); + } + + static color_type dark_goldenrod() { + return color_type(value_of(known_color::dark_goldenrod)); + } + + static color_type peru() { + return color_type(value_of(known_color::peru)); + } + + static color_type chocolate() { + return color_type(value_of(known_color::chocolate)); + } + + static color_type saddle_brown() { + return color_type(value_of(known_color::saddle_brown)); + } + + static color_type sienna() { + return color_type(value_of(known_color::sienna)); + } + + static color_type brown() { + return color_type(value_of(known_color::brown)); + } + + static color_type maroon() { + return color_type(value_of(known_color::maroon)); + } + + static color_type dark_olive_green() { + return color_type(value_of(known_color::dark_olive_green)); + } + + static color_type olive() { + return color_type(value_of(known_color::olive)); + } + + static color_type olive_drab() { + return color_type(value_of(known_color::olive_drab)); + } + + static color_type yellow_green() { + return color_type(value_of(known_color::yellow_green)); + } + + static color_type lime_green() { + return color_type(value_of(known_color::lime_green)); + } + + static color_type lime() { + return color_type(value_of(known_color::lime)); + } + + static color_type lawn_green() { + return color_type(value_of(known_color::lawn_green)); + } + + static color_type chartreuse() { + return color_type(value_of(known_color::chartreuse)); + } + + static color_type green_yellow() { + return color_type(value_of(known_color::green_yellow)); + } + + static color_type spring_green() { + return color_type(value_of(known_color::spring_green)); + } + + static color_type medium_spring_green() { + return color_type(value_of(known_color::medium_spring_green)); + } + + static color_type light_green() { + return color_type(value_of(known_color::light_green)); + } + + static color_type pale_green() { + return color_type(value_of(known_color::pale_green)); + } + + static color_type dark_sea_green() { + return color_type(value_of(known_color::dark_sea_green)); + } + + static color_type medium_sea_green() { + return color_type(value_of(known_color::medium_sea_green)); + } + + static color_type sea_green() { + return color_type(value_of(known_color::sea_green)); + } + + static color_type forest_green() { + return color_type(value_of(known_color::forest_green)); + } + + static color_type green() { + return color_type(value_of(known_color::green)); + } + + static color_type dark_green() { + return color_type(value_of(known_color::dark_green)); + } + + static color_type medium_aquamarine() { + return color_type(value_of(known_color::medium_aquamarine)); + } + + static color_type aqua() { + return color_type(value_of(known_color::aqua)); + } + + static color_type cyan() { + return color_type(value_of(known_color::cyan)); + } + + static color_type light_cyan() { + return color_type(value_of(known_color::light_cyan)); + } + + static color_type pale_turquoise() { + return color_type(value_of(known_color::pale_turquoise)); + } + + static color_type aquamarine() { + return color_type(value_of(known_color::aquamarine)); + } + + static color_type turquoise() { + return color_type(value_of(known_color::turquoise)); + } + + static color_type medium_turquoise() { + return color_type(value_of(known_color::medium_turquoise)); + } + + static color_type dark_turquoise() { + return color_type(value_of(known_color::dark_turquoise)); + } + + static color_type light_sea_green() { + return color_type(value_of(known_color::light_sea_green)); + } + + static color_type cadet_blue() { + return color_type(value_of(known_color::cadet_blue)); + } + + static color_type dark_cyan() { + return color_type(value_of(known_color::dark_cyan)); + } + + static color_type teal() { + return color_type(value_of(known_color::teal)); + } + + static color_type light_steel_blue() { + return color_type(value_of(known_color::light_steel_blue)); + } + + static color_type powder_blue() { + return color_type(value_of(known_color::powder_blue)); + } + + static color_type light_blue() { + return color_type(value_of(known_color::light_blue)); + } + + static color_type sky_blue() { + return color_type(value_of(known_color::sky_blue)); + } + + static color_type light_sky_blue() { + return color_type(value_of(known_color::light_sky_blue)); + } + + static color_type deep_sky_blue() { + return color_type(value_of(known_color::deep_sky_blue)); + } + + static color_type dodger_blue() { + return color_type(value_of(known_color::dodger_blue)); + } + + static color_type cornflower_blue() { + return color_type(value_of(known_color::cornflower_blue)); + } + + static color_type steel_blue() { + return color_type(value_of(known_color::steel_blue)); + } + + static color_type royal_blue() { + return color_type(value_of(known_color::royal_blue)); + } + + static color_type blue() { + return color_type(value_of(known_color::blue)); + } + + static color_type medium_blue() { + return color_type(value_of(known_color::medium_blue)); + } + + static color_type dark_blue() { + return color_type(value_of(known_color::dark_blue)); + } + + static color_type navy() { + return color_type(value_of(known_color::navy)); + } + + static color_type midnight_blue() { + return color_type(value_of(known_color::midnight_blue)); + } + + static color_type lavender() { + return color_type(value_of(known_color::lavender)); + } + + static color_type thistle() { + return color_type(value_of(known_color::thistle)); + } + + static color_type plum() { + return color_type(value_of(known_color::plum)); + } + + static color_type violet() { + return color_type(value_of(known_color::violet)); + } + + static color_type orchid() { + return color_type(value_of(known_color::orchid)); + } + + static color_type fuchsia() { + return color_type(value_of(known_color::fuchsia)); + } + + static color_type magenta() { + return color_type(value_of(known_color::magenta)); + } + + static color_type medium_orchid() { + return color_type(value_of(known_color::medium_orchid)); + } + + static color_type medium_purple() { + return color_type(value_of(known_color::medium_purple)); + } + + static color_type blue_violet() { + return color_type(value_of(known_color::blue_violet)); + } + + static color_type dark_violet() { + return color_type(value_of(known_color::dark_violet)); + } + + static color_type dark_orchid() { + return color_type(value_of(known_color::dark_orchid)); + } + + static color_type dark_magenta() { + return color_type(value_of(known_color::dark_magenta)); + } + + static color_type purple() { + return color_type(value_of(known_color::purple)); + } + + static color_type indigo() { + return color_type(value_of(known_color::indigo)); + } + + static color_type dark_slate_blue() { + return color_type(value_of(known_color::dark_slate_blue)); + } + + static color_type slate_blue() { + return color_type(value_of(known_color::slate_blue)); + } + + static color_type medium_slate_blue() { + return color_type(value_of(known_color::medium_slate_blue)); + } + + static color_type white() { + return color_type(value_of(known_color::white)); + } + + static color_type snow() { + return color_type(value_of(known_color::snow)); + } + + static color_type honeydew() { + return color_type(value_of(known_color::honeydew)); + } + + static color_type mint_cream() { + return color_type(value_of(known_color::mint_cream)); + } + + static color_type azure() { + return color_type(value_of(known_color::azure)); + } + + static color_type alice_blue() { + return color_type(value_of(known_color::alice_blue)); + } + + static color_type ghost_white() { + return color_type(value_of(known_color::ghost_white)); + } + + static color_type white_smoke() { + return color_type(value_of(known_color::white_smoke)); + } + + static color_type seashell() { + return color_type(value_of(known_color::seashell)); + } + + static color_type beige() { + return color_type(value_of(known_color::beige)); + } + + static color_type old_lace() { + return color_type(value_of(known_color::old_lace)); + } + + static color_type floral_white() { + return color_type(value_of(known_color::floral_white)); + } + + static color_type ivory() { + return color_type(value_of(known_color::ivory)); + } + + static color_type antique_white() { + return color_type(value_of(known_color::antique_white)); + } + + static color_type linen() { + return color_type(value_of(known_color::linen)); + } + + static color_type lavender_blush() { + return color_type(value_of(known_color::lavender_blush)); + } + + static color_type misty_rose() { + return color_type(value_of(known_color::misty_rose)); + } + + static color_type gainsboro() { + return color_type(value_of(known_color::gainsboro)); + } + + static color_type light_grey() { + return color_type(value_of(known_color::light_grey)); + } + + static color_type silver() { + return color_type(value_of(known_color::silver)); + } + + static color_type dark_gray() { + return color_type(value_of(known_color::dark_gray)); + } + + static color_type gray() { + return color_type(value_of(known_color::gray)); + } + + static color_type dim_gray() { + return color_type(value_of(known_color::dim_gray)); + } + + static color_type light_slate_gray() { + return color_type(value_of(known_color::light_slate_gray)); + } + + static color_type slate_gray() { + return color_type(value_of(known_color::slate_gray)); + } + + static color_type dark_slate_gray() { + return color_type(value_of(known_color::dark_slate_gray)); + } + + static color_type black() { + return color_type(value_of(known_color::black)); + } + }; + } +} \ No newline at end of file diff --git a/extern/cpp-colors/include/cpp-colors/impl/x11/x11_constants.h b/extern/cpp-colors/include/cpp-colors/impl/x11/x11_constants.h new file mode 100644 index 000000000..f7fb531a4 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/impl/x11/x11_constants.h @@ -0,0 +1,152 @@ +#pragma once + +#include + +namespace colors { + namespace x11 { + + enum class known_color : uint32_t { + pink = 0xFFFFC0CB, // Pink: argb(255, 255, 192, 203) + light_pink = 0xFFFFB6C1, // LightPink: argb(255, 255, 182, 193) + hot_pink = 0xFFFF69B4, // HotPink: argb(255, 255, 105, 180) + deep_pink = 0xFFFF1493, // DeepPink: argb(255, 255, 20, 147) + pale_violet_red = 0xFFDB7093, // PaleVioletRed: argb(255, 219, 112, 147) + medium_violet_red = 0xFFC71585, // MediumVioletRed: argb(255, 199, 21, 133) + light_salmon = 0xFFFFA07A, // LightSalmon: argb(255, 255, 160, 122) + salmon = 0xFFFA8072, // Salmon: argb(255, 250, 128, 114) + dark_salmon = 0xFFE9967A, // DarkSalmon: argb(255, 233, 150, 122) + light_coral = 0xFFF08080, // LightCoral: argb(255, 240, 128, 128) + indian_red = 0xFFCD5C5C, // IndianRed: argb(255, 205, 92, 92) + crimson = 0xFFDC143C, // Crimson: argb(255, 220, 20, 60) + fire_brick = 0xFFB22222, // FireBrick: argb(255, 178, 34, 34) + dark_red = 0xFF8B0000, // DarkRed: argb(255, 139, 0, 0) + red = 0xFFFF0000, // Red: argb(255, 255, 0, 0) + orange_red = 0xFFFF4500, // OrangeRed: argb(255, 255, 69, 0) + tomato = 0xFFFF6347, // Tomato: argb(255, 255, 99, 71) + coral = 0xFFFF7F50, // Coral: argb(255, 255, 127, 80) + dark_orange = 0xFFFF8C00, // DarkOrange: argb(255, 255, 140, 0) + orange = 0xFFFFA500, // Orange: argb(255, 255, 165, 0) + yellow = 0xFFFFFF00, // Yellow: argb(255, 255, 255, 0) + light_yellow = 0xFFFFFFE0, // LightYellow: argb(255, 255, 255, 224) + lemon_chiffon = 0xFFFFFACD, // LemonChiffon: argb(255, 255, 250, 205) + light_goldenrod_yellow = 0xFFFAFAD2, // LightGoldenrodYellow: argb(255, 250, 250, 210) + papaya_whip = 0xFFFFEFD5, // PapayaWhip: argb(255, 255, 239, 213) + moccasin = 0xFFFFE4B5, // Moccasin: argb(255, 255, 228, 181) + peach_puff = 0xFFFFDAB9, // PeachPuff: argb(255, 255, 218, 185) + pale_goldenrod = 0xFFEEE8AA, // PaleGoldenrod: argb(255, 238, 232, 170) + khaki = 0xFFF0E68C, // Khaki: argb(255, 240, 230, 140) + dark_khaki = 0xFFBDB76B, // DarkKhaki: argb(255, 189, 183, 107) + gold = 0xFFFFD700, // Gold: argb(255, 255, 215, 0) + cornsilk = 0xFFFFF8DC, // Cornsilk: argb(255, 255, 248, 220) + blanched_almond = 0xFFFFEBCD, // BlanchedAlmond: argb(255, 255, 235, 205) + bisque = 0xFFFFE4C4, // Bisque: argb(255, 255, 228, 196) + navajo_white = 0xFFFFDEAD, // NavajoWhite: argb(255, 255, 222, 173) + wheat = 0xFFF5DEB3, // Wheat: argb(255, 245, 222, 179) + burly_wood = 0xFFDEB887, // BurlyWood: argb(255, 222, 184, 135) + tan = 0xFFD2B48C, // Tan: argb(255, 210, 180, 140) + rosy_brown = 0xFFBC8F8F, // RosyBrown: argb(255, 188, 143, 143) + sandy_brown = 0xFFF4A460, // SandyBrown: argb(255, 244, 164, 96) + goldenrod = 0xFFDAA520, // Goldenrod: argb(255, 218, 165, 32) + dark_goldenrod = 0xFFB8860B, // DarkGoldenrod: argb(255, 184, 134, 11) + peru = 0xFFCD853F, // Peru: argb(255, 205, 133, 63) + chocolate = 0xFFD2691E, // Chocolate: argb(255, 210, 105, 30) + saddle_brown = 0xFF8B4513, // SaddleBrown: argb(255, 139, 69, 19) + sienna = 0xFFA0522D, // Sienna: argb(255, 160, 82, 45) + brown = 0xFFA52A2A, // Brown: argb(255, 165, 42, 42) + maroon = 0xFF800000, // Maroon: argb(255, 128, 0, 0) + dark_olive_green = 0xFF556B2F, // DarkOliveGreen: argb(255, 85, 107, 47) + olive = 0xFF808000, // Olive: argb(255, 128, 128, 0) + olive_drab = 0xFF6B8E23, // OliveDrab: argb(255, 107, 142, 35) + yellow_green = 0xFF9ACD32, // YellowGreen: argb(255, 154, 205, 50) + lime_green = 0xFF32CD32, // LimeGreen: argb(255, 50, 205, 50) + lime = 0xFF00FF00, // Lime: argb(255, 0, 255, 0) + lawn_green = 0xFF7CFC00, // LawnGreen: argb(255, 124, 252, 0) + chartreuse = 0xFF7FFF00, // Chartreuse: argb(255, 127, 255, 0) + green_yellow = 0xFFADFF2F, // GreenYellow: argb(255, 173, 255, 47) + spring_green = 0xFF00FF7F, // SpringGreen: argb(255, 0, 255, 127) + medium_spring_green = 0xFF00FA9A, // MediumSpringGreen: argb(255, 0, 250, 154) + light_green = 0xFF90EE90, // LightGreen: argb(255, 144, 238, 144) + pale_green = 0xFF98FB98, // PaleGreen: argb(255, 152, 251, 152) + dark_sea_green = 0xFF8FBC8F, // DarkSeaGreen: argb(255, 143, 188, 143) + medium_sea_green = 0xFF3CB371, // MediumSeaGreen: argb(255, 60, 179, 113) + sea_green = 0xFF2E8B57, // SeaGreen: argb(255, 46, 139, 87) + forest_green = 0xFF228B22, // ForestGreen: argb(255, 34, 139, 34) + green = 0xFF008000, // Green: argb(255, 0, 128, 0) + dark_green = 0xFF006400, // DarkGreen: argb(255, 0, 100, 0) + medium_aquamarine = 0xFF66CDAA, // MediumAquamarine: argb(255, 102, 205, 170) + aqua = 0xFF00FFFF, // Aqua: argb(255, 0, 255, 255) + cyan = 0xFF00FFFF, // Cyan: argb(255, 0, 255, 255) + light_cyan = 0xFFE0FFFF, // LightCyan: argb(255, 224, 255, 255) + pale_turquoise = 0xFFAFEEEE, // PaleTurquoise: argb(255, 175, 238, 238) + aquamarine = 0xFF7FFFD4, // Aquamarine: argb(255, 127, 255, 212) + turquoise = 0xFF40E0D0, // Turquoise: argb(255, 64, 224, 208) + medium_turquoise = 0xFF48D1CC, // MediumTurquoise: argb(255, 72, 209, 204) + dark_turquoise = 0xFF00CED1, // DarkTurquoise: argb(255, 0, 206, 209) + light_sea_green = 0xFF20B2AA, // LightSeaGreen: argb(255, 32, 178, 170) + cadet_blue = 0xFF5F9EA0, // CadetBlue: argb(255, 95, 158, 160) + dark_cyan = 0xFF008B8B, // DarkCyan: argb(255, 0, 139, 139) + teal = 0xFF008080, // Teal: argb(255, 0, 128, 128) + light_steel_blue = 0xFFB0C4DE, // LightSteelBlue: argb(255, 176, 196, 222) + powder_blue = 0xFFB0E0E6, // PowderBlue: argb(255, 176, 224, 230) + light_blue = 0xFFADD8E6, // LightBlue: argb(255, 173, 216, 230) + sky_blue = 0xFF87CEEB, // SkyBlue: argb(255, 135, 206, 235) + light_sky_blue = 0xFF87CEFA, // LightSkyBlue: argb(255, 135, 206, 250) + deep_sky_blue = 0xFF00BFFF, // DeepSkyBlue: argb(255, 0, 191, 255) + dodger_blue = 0xFF1E90FF, // DodgerBlue: argb(255, 30, 144, 255) + cornflower_blue = 0xFF6495ED, // CornflowerBlue: argb(255, 100, 149, 237) + steel_blue = 0xFF4682B4, // SteelBlue: argb(255, 70, 130, 180) + royal_blue = 0xFF4169E1, // RoyalBlue: argb(255, 65, 105, 225) + blue = 0xFF0000FF, // Blue: argb(255, 0, 0, 255) + medium_blue = 0xFF0000CD, // MediumBlue: argb(255, 0, 0, 205) + dark_blue = 0xFF00008B, // DarkBlue: argb(255, 0, 0, 139) + navy = 0xFF000080, // Navy: argb(255, 0, 0, 128) + midnight_blue = 0xFF191970, // MidnightBlue: argb(255, 25, 25, 112) + lavender = 0xFFE6E6FA, // Lavender: argb(255, 230, 230, 250) + thistle = 0xFFD8BFD8, // Thistle: argb(255, 216, 191, 216) + plum = 0xFFDDA0DD, // Plum: argb(255, 221, 160, 221) + violet = 0xFFEE82EE, // Violet: argb(255, 238, 130, 238) + orchid = 0xFFDA70D6, // Orchid: argb(255, 218, 112, 214) + fuchsia = 0xFFFF00FF, // Fuchsia: argb(255, 255, 0, 255) + magenta = 0xFFFF00FF, // Magenta: argb(255, 255, 0, 255) + medium_orchid = 0xFFBA55D3, // MediumOrchid: argb(255, 186, 85, 211) + medium_purple = 0xFF9370DB, // MediumPurple: argb(255, 147, 112, 219) + blue_violet = 0xFF8A2BE2, // BlueViolet: argb(255, 138, 43, 226) + dark_violet = 0xFF9400D3, // DarkViolet: argb(255, 148, 0, 211) + dark_orchid = 0xFF9932CC, // DarkOrchid: argb(255, 153, 50, 204) + dark_magenta = 0xFF8B008B, // DarkMagenta: argb(255, 139, 0, 139) + purple = 0xFF800080, // Purple: argb(255, 128, 0, 128) + indigo = 0xFF4B0082, // Indigo: argb(255, 75, 0, 130) + dark_slate_blue = 0xFF483D8B, // DarkSlateBlue: argb(255, 72, 61, 139) + slate_blue = 0xFF6A5ACD, // SlateBlue: argb(255, 106, 90, 205) + medium_slate_blue = 0xFF7B68EE, // MediumSlateBlue: argb(255, 123, 104, 238) + white = 0xFFFFFFFF, // White: argb(255, 255, 255, 255) + snow = 0xFFFFFAFA, // Snow: argb(255, 255, 250, 250) + honeydew = 0xFFF0FFF0, // Honeydew: argb(255, 240, 255, 240) + mint_cream = 0xFFF5FFFA, // MintCream: argb(255, 245, 255, 250) + azure = 0xFFF0FFFF, // Azure: argb(255, 240, 255, 255) + alice_blue = 0xFFF0F8FF, // AliceBlue: argb(255, 240, 248, 255) + ghost_white = 0xFFF8F8FF, // GhostWhite: argb(255, 248, 248, 255) + white_smoke = 0xFFF5F5F5, // WhiteSmoke: argb(255, 245, 245, 245) + seashell = 0xFFFFF5EE, // Seashell: argb(255, 255, 245, 238) + beige = 0xFFF5F5DC, // Beige: argb(255, 245, 245, 220) + old_lace = 0xFFFDF5E6, // OldLace: argb(255, 253, 245, 230) + floral_white = 0xFFFFFAF0, // FloralWhite: argb(255, 255, 250, 240) + ivory = 0xFFFFFFF0, // Ivory: argb(255, 255, 255, 240) + antique_white = 0xFFFAEBD7, // AntiqueWhite: argb(255, 250, 235, 215) + linen = 0xFFFAF0E6, // Linen: argb(255, 250, 240, 230) + lavender_blush = 0xFFFFF0F5, // LavenderBlush: argb(255, 255, 240, 245) + misty_rose = 0xFFFFE4E1, // MistyRose: argb(255, 255, 228, 225) + gainsboro = 0xFFDCDCDC, // Gainsboro: argb(255, 220, 220, 220) + light_grey = 0xFFD3D3D3, // LightGrey: argb(255, 211, 211, 211) + silver = 0xFFC0C0C0, // Silver: argb(255, 192, 192, 192) + dark_gray = 0xFFA9A9A9, // DarkGray: argb(255, 169, 169, 169) + gray = 0xFF808080, // Gray: argb(255, 128, 128, 128) + dim_gray = 0xFF696969, // DimGray: argb(255, 105, 105, 105) + light_slate_gray = 0xFF778899, // LightSlateGray: argb(255, 119, 136, 153) + slate_gray = 0xFF708090, // SlateGray: argb(255, 112, 128, 144) + dark_slate_gray = 0xFF2F4F4F, // DarkSlateGray: argb(255, 47, 79, 79) + black = 0xFF000000, // Black: argb(255, 0, 0, 0) + }; + + } +} \ No newline at end of file diff --git a/extern/cpp-colors/include/cpp-colors/impl/x11/x11_named.h b/extern/cpp-colors/include/cpp-colors/impl/x11/x11_named.h new file mode 100644 index 000000000..23bf82a88 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/impl/x11/x11_named.h @@ -0,0 +1,1477 @@ +#pragma once + +#include +#include +#include +#include +#include "x11_constants.h" +#include "../named_utils.h" + + +namespace colors { + namespace x11 { + + template + struct basic_named_color; + + template<> + struct basic_named_color { + static const char* const pink() { + return "Pink"; + } + + static const char* const light_pink() { + return "LightPink"; + } + + static const char* const hot_pink() { + return "HotPink"; + } + + static const char* const deep_pink() { + return "DeepPink"; + } + + static const char* const pale_violet_red() { + return "PaleVioletRed"; + } + + static const char* const medium_violet_red() { + return "MediumVioletRed"; + } + + static const char* const light_salmon() { + return "LightSalmon"; + } + + static const char* const salmon() { + return "Salmon"; + } + + static const char* const dark_salmon() { + return "DarkSalmon"; + } + + static const char* const light_coral() { + return "LightCoral"; + } + + static const char* const indian_red() { + return "IndianRed"; + } + + static const char* const crimson() { + return "Crimson"; + } + + static const char* const fire_brick() { + return "FireBrick"; + } + + static const char* const dark_red() { + return "DarkRed"; + } + + static const char* const red() { + return "Red"; + } + + static const char* const orange_red() { + return "OrangeRed"; + } + + static const char* const tomato() { + return "Tomato"; + } + + static const char* const coral() { + return "Coral"; + } + + static const char* const dark_orange() { + return "DarkOrange"; + } + + static const char* const orange() { + return "Orange"; + } + + static const char* const yellow() { + return "Yellow"; + } + + static const char* const light_yellow() { + return "LightYellow"; + } + + static const char* const lemon_chiffon() { + return "LemonChiffon"; + } + + static const char* const light_goldenrod_yellow() { + return "LightGoldenrodYellow"; + } + + static const char* const papaya_whip() { + return "PapayaWhip"; + } + + static const char* const moccasin() { + return "Moccasin"; + } + + static const char* const peach_puff() { + return "PeachPuff"; + } + + static const char* const pale_goldenrod() { + return "PaleGoldenrod"; + } + + static const char* const khaki() { + return "Khaki"; + } + + static const char* const dark_khaki() { + return "DarkKhaki"; + } + + static const char* const gold() { + return "Gold"; + } + + static const char* const cornsilk() { + return "Cornsilk"; + } + + static const char* const blanched_almond() { + return "BlanchedAlmond"; + } + + static const char* const bisque() { + return "Bisque"; + } + + static const char* const navajo_white() { + return "NavajoWhite"; + } + + static const char* const wheat() { + return "Wheat"; + } + + static const char* const burly_wood() { + return "BurlyWood"; + } + + static const char* const tan() { + return "Tan"; + } + + static const char* const rosy_brown() { + return "RosyBrown"; + } + + static const char* const sandy_brown() { + return "SandyBrown"; + } + + static const char* const goldenrod() { + return "Goldenrod"; + } + + static const char* const dark_goldenrod() { + return "DarkGoldenrod"; + } + + static const char* const peru() { + return "Peru"; + } + + static const char* const chocolate() { + return "Chocolate"; + } + + static const char* const saddle_brown() { + return "SaddleBrown"; + } + + static const char* const sienna() { + return "Sienna"; + } + + static const char* const brown() { + return "Brown"; + } + + static const char* const maroon() { + return "Maroon"; + } + + static const char* const dark_olive_green() { + return "DarkOliveGreen"; + } + + static const char* const olive() { + return "Olive"; + } + + static const char* const olive_drab() { + return "OliveDrab"; + } + + static const char* const yellow_green() { + return "YellowGreen"; + } + + static const char* const lime_green() { + return "LimeGreen"; + } + + static const char* const lime() { + return "Lime"; + } + + static const char* const lawn_green() { + return "LawnGreen"; + } + + static const char* const chartreuse() { + return "Chartreuse"; + } + + static const char* const green_yellow() { + return "GreenYellow"; + } + + static const char* const spring_green() { + return "SpringGreen"; + } + + static const char* const medium_spring_green() { + return "MediumSpringGreen"; + } + + static const char* const light_green() { + return "LightGreen"; + } + + static const char* const pale_green() { + return "PaleGreen"; + } + + static const char* const dark_sea_green() { + return "DarkSeaGreen"; + } + + static const char* const medium_sea_green() { + return "MediumSeaGreen"; + } + + static const char* const sea_green() { + return "SeaGreen"; + } + + static const char* const forest_green() { + return "ForestGreen"; + } + + static const char* const green() { + return "Green"; + } + + static const char* const dark_green() { + return "DarkGreen"; + } + + static const char* const medium_aquamarine() { + return "MediumAquamarine"; + } + + static const char* const aqua() { + return "Aqua"; + } + + static const char* const cyan() { + return "Cyan"; + } + + static const char* const light_cyan() { + return "LightCyan"; + } + + static const char* const pale_turquoise() { + return "PaleTurquoise"; + } + + static const char* const aquamarine() { + return "Aquamarine"; + } + + static const char* const turquoise() { + return "Turquoise"; + } + + static const char* const medium_turquoise() { + return "MediumTurquoise"; + } + + static const char* const dark_turquoise() { + return "DarkTurquoise"; + } + + static const char* const light_sea_green() { + return "LightSeaGreen"; + } + + static const char* const cadet_blue() { + return "CadetBlue"; + } + + static const char* const dark_cyan() { + return "DarkCyan"; + } + + static const char* const teal() { + return "Teal"; + } + + static const char* const light_steel_blue() { + return "LightSteelBlue"; + } + + static const char* const powder_blue() { + return "PowderBlue"; + } + + static const char* const light_blue() { + return "LightBlue"; + } + + static const char* const sky_blue() { + return "SkyBlue"; + } + + static const char* const light_sky_blue() { + return "LightSkyBlue"; + } + + static const char* const deep_sky_blue() { + return "DeepSkyBlue"; + } + + static const char* const dodger_blue() { + return "DodgerBlue"; + } + + static const char* const cornflower_blue() { + return "CornflowerBlue"; + } + + static const char* const steel_blue() { + return "SteelBlue"; + } + + static const char* const royal_blue() { + return "RoyalBlue"; + } + + static const char* const blue() { + return "Blue"; + } + + static const char* const medium_blue() { + return "MediumBlue"; + } + + static const char* const DarkBlue() { + return "DarkBlue"; + } + + static const char* const Navy() { + return "Navy"; + } + + static const char* const MidnightBlue() { + return "MidnightBlue"; + } + + static const char* const Lavender() { + return "Lavender"; + } + + static const char* const Thistle() { + return "Thistle"; + } + + static const char* const Plum() { + return "Plum"; + } + + static const char* const Violet() { + return "Violet"; + } + + static const char* const Orchid() { + return "Orchid"; + } + + static const char* const Fuchsia() { + return "Fuchsia"; + } + + static const char* const Magenta() { + return "Magenta"; + } + + static const char* const MediumOrchid() { + return "MediumOrchid"; + } + + static const char* const MediumPurple() { + return "MediumPurple"; + } + + static const char* const BlueViolet() { + return "BlueViolet"; + } + + static const char* const DarkViolet() { + return "DarkViolet"; + } + + static const char* const DarkOrchid() { + return "DarkOrchid"; + } + + static const char* const DarkMagenta() { + return "DarkMagenta"; + } + + static const char* const Purple() { + return "Purple"; + } + + static const char* const Indigo() { + return "Indigo"; + } + + static const char* const DarkSlateBlue() { + return "DarkSlateBlue"; + } + + static const char* const SlateBlue() { + return "SlateBlue"; + } + + static const char* const MediumSlateBlue() { + return "MediumSlateBlue"; + } + + static const char* const White() { + return "White"; + } + + static const char* const Snow() { + return "Snow"; + } + + static const char* const Honeydew() { + return "Honeydew"; + } + + static const char* const MintCream() { + return "MintCream"; + } + + static const char* const Azure() { + return "Azure"; + } + + static const char* const AliceBlue() { + return "AliceBlue"; + } + + static const char* const GhostWhite() { + return "GhostWhite"; + } + + static const char* const WhiteSmoke() { + return "WhiteSmoke"; + } + + static const char* const Seashell() { + return "Seashell"; + } + + static const char* const Beige() { + return "Beige"; + } + + static const char* const OldLace() { + return "OldLace"; + } + + static const char* const FloralWhite() { + return "FloralWhite"; + } + + static const char* const Ivory() { + return "Ivory"; + } + + static const char* const AntiqueWhite() { + return "AntiqueWhite"; + } + + static const char* const Linen() { + return "Linen"; + } + + static const char* const LavenderBlush() { + return "LavenderBlush"; + } + + static const char* const MistyRose() { + return "MistyRose"; + } + + static const char* const Gainsboro() { + return "Gainsboro"; + } + + static const char* const LightGrey() { + return "LightGrey"; + } + + static const char* const Silver() { + return "Silver"; + } + + static const char* const DarkGray() { + return "DarkGray"; + } + + static const char* const Gray() { + return "Gray"; + } + + static const char* const DimGray() { + return "DimGray"; + } + + static const char* const LightSlateGray() { + return "LightSlateGray"; + } + + static const char* const SlateGray() { + return "SlateGray"; + } + + static const char* const DarkSlateGray() { + return "DarkSlateGray"; + } + + static const char* const Black() { + return "Black"; + } + + }; + + template<> + struct basic_named_color { + static const wchar_t* const pink() { + return L"Pink"; + } + + static const wchar_t* const light_pink() { + return L"LightPink"; + } + + static const wchar_t* const hot_pink() { + return L"HotPink"; + } + + static const wchar_t* const deep_pink() { + return L"DeepPink"; + } + + static const wchar_t* const pale_violet_red() { + return L"PaleVioletRed"; + } + + static const wchar_t* const medium_violet_red() { + return L"MediumVioletRed"; + } + + static const wchar_t* const light_salmon() { + return L"LightSalmon"; + } + + static const wchar_t* const salmon() { + return L"Salmon"; + } + + static const wchar_t* const dark_salmon() { + return L"DarkSalmon"; + } + + static const wchar_t* const light_coral() { + return L"LightCoral"; + } + + static const wchar_t* const indian_red() { + return L"IndianRed"; + } + + static const wchar_t* const crimson() { + return L"Crimson"; + } + + static const wchar_t* const fire_brick() { + return L"FireBrick"; + } + + static const wchar_t* const dark_red() { + return L"DarkRed"; + } + + static const wchar_t* const red() { + return L"Red"; + } + + static const wchar_t* const orange_red() { + return L"OrangeRed"; + } + + static const wchar_t* const tomato() { + return L"Tomato"; + } + + static const wchar_t* const coral() { + return L"Coral"; + } + + static const wchar_t* const dark_orange() { + return L"DarkOrange"; + } + + static const wchar_t* const orange() { + return L"Orange"; + } + + static const wchar_t* const yellow() { + return L"Yellow"; + } + + static const wchar_t* const light_yellow() { + return L"LightYellow"; + } + + static const wchar_t* const lemon_chiffon() { + return L"LemonChiffon"; + } + + static const wchar_t* const light_goldenrod_yellow() { + return L"LightGoldenrodYellow"; + } + + static const wchar_t* const papaya_whip() { + return L"PapayaWhip"; + } + + static const wchar_t* const moccasin() { + return L"Moccasin"; + } + + static const wchar_t* const peach_puff() { + return L"PeachPuff"; + } + + static const wchar_t* const pale_goldenrod() { + return L"PaleGoldenrod"; + } + + static const wchar_t* const khaki() { + return L"Khaki"; + } + + static const wchar_t* const dark_khaki() { + return L"DarkKhaki"; + } + + static const wchar_t* const gold() { + return L"Gold"; + } + + static const wchar_t* const cornsilk() { + return L"Cornsilk"; + } + + static const wchar_t* const blanched_almond() { + return L"BlanchedAlmond"; + } + + static const wchar_t* const bisque() { + return L"Bisque"; + } + + static const wchar_t* const navajo_white() { + return L"NavajoWhite"; + } + + static const wchar_t* const wheat() { + return L"Wheat"; + } + + static const wchar_t* const burly_wood() { + return L"BurlyWood"; + } + + static const wchar_t* const tan() { + return L"Tan"; + } + + static const wchar_t* const rosy_brown() { + return L"RosyBrown"; + } + + static const wchar_t* const sandy_brown() { + return L"SandyBrown"; + } + + static const wchar_t* const goldenrod() { + return L"Goldenrod"; + } + + static const wchar_t* const dark_goldenrod() { + return L"DarkGoldenrod"; + } + + static const wchar_t* const peru() { + return L"Peru"; + } + + static const wchar_t* const chocolate() { + return L"Chocolate"; + } + + static const wchar_t* const saddle_brown() { + return L"SaddleBrown"; + } + + static const wchar_t* const sienna() { + return L"Sienna"; + } + + static const wchar_t* const brown() { + return L"Brown"; + } + + static const wchar_t* const maroon() { + return L"Maroon"; + } + + static const wchar_t* const dark_olive_green() { + return L"DarkOliveGreen"; + } + + static const wchar_t* const olive() { + return L"Olive"; + } + + static const wchar_t* const olive_drab() { + return L"OliveDrab"; + } + + static const wchar_t* const yellow_green() { + return L"YellowGreen"; + } + + static const wchar_t* const lime_green() { + return L"LimeGreen"; + } + + static const wchar_t* const lime() { + return L"Lime"; + } + + static const wchar_t* const lawn_green() { + return L"LawnGreen"; + } + + static const wchar_t* const chartreuse() { + return L"Chartreuse"; + } + + static const wchar_t* const green_yellow() { + return L"GreenYellow"; + } + + static const wchar_t* const spring_green() { + return L"SpringGreen"; + } + + static const wchar_t* const medium_spring_green() { + return L"MediumSpringGreen"; + } + + static const wchar_t* const light_green() { + return L"LightGreen"; + } + + static const wchar_t* const pale_green() { + return L"PaleGreen"; + } + + static const wchar_t* const dark_sea_green() { + return L"DarkSeaGreen"; + } + + static const wchar_t* const medium_sea_green() { + return L"MediumSeaGreen"; + } + + static const wchar_t* const sea_green() { + return L"SeaGreen"; + } + + static const wchar_t* const forest_green() { + return L"ForestGreen"; + } + + static const wchar_t* const green() { + return L"Green"; + } + + static const wchar_t* const dark_green() { + return L"DarkGreen"; + } + + static const wchar_t* const medium_aquamarine() { + return L"MediumAquamarine"; + } + + static const wchar_t* const aqua() { + return L"Aqua"; + } + + static const wchar_t* const cyan() { + return L"Cyan"; + } + + static const wchar_t* const light_cyan() { + return L"LightCyan"; + } + + static const wchar_t* const pale_turquoise() { + return L"PaleTurquoise"; + } + + static const wchar_t* const aquamarine() { + return L"Aquamarine"; + } + + static const wchar_t* const turquoise() { + return L"Turquoise"; + } + + static const wchar_t* const medium_turquoise() { + return L"MediumTurquoise"; + } + + static const wchar_t* const dark_turquoise() { + return L"DarkTurquoise"; + } + + static const wchar_t* const light_sea_green() { + return L"LightSeaGreen"; + } + + static const wchar_t* const cadet_blue() { + return L"CadetBlue"; + } + + static const wchar_t* const dark_cyan() { + return L"DarkCyan"; + } + + static const wchar_t* const teal() { + return L"Teal"; + } + + static const wchar_t* const light_steel_blue() { + return L"LightSteelBlue"; + } + + static const wchar_t* const powder_blue() { + return L"PowderBlue"; + } + + static const wchar_t* const light_blue() { + return L"LightBlue"; + } + + static const wchar_t* const sky_blue() { + return L"SkyBlue"; + } + + static const wchar_t* const light_sky_blue() { + return L"LightSkyBlue"; + } + + static const wchar_t* const deep_sky_blue() { + return L"DeepSkyBlue"; + } + + static const wchar_t* const dodger_blue() { + return L"DodgerBlue"; + } + + static const wchar_t* const cornflower_blue() { + return L"CornflowerBlue"; + } + + static const wchar_t* const steel_blue() { + return L"SteelBlue"; + } + + static const wchar_t* const royal_blue() { + return L"RoyalBlue"; + } + + static const wchar_t* const blue() { + return L"Blue"; + } + + static const wchar_t* const medium_blue() { + return L"MediumBlue"; + } + + static const wchar_t* const DarkBlue() { + return L"DarkBlue"; + } + + static const wchar_t* const Navy() { + return L"Navy"; + } + + static const wchar_t* const MidnightBlue() { + return L"MidnightBlue"; + } + + static const wchar_t* const Lavender() { + return L"Lavender"; + } + + static const wchar_t* const Thistle() { + return L"Thistle"; + } + + static const wchar_t* const Plum() { + return L"Plum"; + } + + static const wchar_t* const Violet() { + return L"Violet"; + } + + static const wchar_t* const Orchid() { + return L"Orchid"; + } + + static const wchar_t* const Fuchsia() { + return L"Fuchsia"; + } + + static const wchar_t* const Magenta() { + return L"Magenta"; + } + + static const wchar_t* const MediumOrchid() { + return L"MediumOrchid"; + } + + static const wchar_t* const MediumPurple() { + return L"MediumPurple"; + } + + static const wchar_t* const BlueViolet() { + return L"BlueViolet"; + } + + static const wchar_t* const DarkViolet() { + return L"DarkViolet"; + } + + static const wchar_t* const DarkOrchid() { + return L"DarkOrchid"; + } + + static const wchar_t* const DarkMagenta() { + return L"DarkMagenta"; + } + + static const wchar_t* const Purple() { + return L"Purple"; + } + + static const wchar_t* const Indigo() { + return L"Indigo"; + } + + static const wchar_t* const DarkSlateBlue() { + return L"DarkSlateBlue"; + } + + static const wchar_t* const SlateBlue() { + return L"SlateBlue"; + } + + static const wchar_t* const MediumSlateBlue() { + return L"MediumSlateBlue"; + } + + static const wchar_t* const White() { + return L"White"; + } + + static const wchar_t* const Snow() { + return L"Snow"; + } + + static const wchar_t* const Honeydew() { + return L"Honeydew"; + } + + static const wchar_t* const MintCream() { + return L"MintCream"; + } + + static const wchar_t* const Azure() { + return L"Azure"; + } + + static const wchar_t* const AliceBlue() { + return L"AliceBlue"; + } + + static const wchar_t* const GhostWhite() { + return L"GhostWhite"; + } + + static const wchar_t* const WhiteSmoke() { + return L"WhiteSmoke"; + } + + static const wchar_t* const Seashell() { + return L"Seashell"; + } + + static const wchar_t* const Beige() { + return L"Beige"; + } + + static const wchar_t* const OldLace() { + return L"OldLace"; + } + + static const wchar_t* const FloralWhite() { + return L"FloralWhite"; + } + + static const wchar_t* const Ivory() { + return L"Ivory"; + } + + static const wchar_t* const AntiqueWhite() { + return L"AntiqueWhite"; + } + + static const wchar_t* const Linen() { + return L"Linen"; + } + + static const wchar_t* const LavenderBlush() { + return L"LavenderBlush"; + } + + static const wchar_t* const MistyRose() { + return L"MistyRose"; + } + + static const wchar_t* const Gainsboro() { + return L"Gainsboro"; + } + + static const wchar_t* const LightGrey() { + return L"LightGrey"; + } + + static const wchar_t* const Silver() { + return L"Silver"; + } + + static const wchar_t* const DarkGray() { + return L"DarkGray"; + } + + static const wchar_t* const Gray() { + return L"Gray"; + } + + static const wchar_t* const DimGray() { + return L"DimGray"; + } + + static const wchar_t* const LightSlateGray() { + return L"LightSlateGray"; + } + + static const wchar_t* const SlateGray() { + return L"SlateGray"; + } + + static const wchar_t* const DarkSlateGray() { + return L"DarkSlateGray"; + } + + static const wchar_t* const Black() { + return L"Black"; + } + + }; + + // + template + struct basic_color_mapper { + typedef CharT char_type; + typedef std::basic_string string_type; + + typedef known_color known_color_type; + typedef basic_named_color named_color_type; + + typedef std::map string_color_map; + typedef std::map color_string_map; + + // Build a name -> constant map + static const string_color_map &get_string_to_color_map() { + static string_color_map sm_; // name -> value + if (sm_.empty()) { + sm_.insert(std::make_pair(to_lowercase(named_color_type::pink()), known_color::pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_pink()), known_color::light_pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::hot_pink()), known_color::hot_pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::deep_pink()), known_color::deep_pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_violet_red()), known_color::pale_violet_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_violet_red()), known_color::medium_violet_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_salmon()), known_color::light_salmon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::salmon()), known_color::salmon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_salmon()), known_color::dark_salmon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_coral()), known_color::light_coral)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::indian_red()), known_color::indian_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::crimson()), known_color::crimson)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::fire_brick()), known_color::fire_brick)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_red()), known_color::dark_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::red()), known_color::red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::orange_red()), known_color::orange_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::tomato()), known_color::tomato)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::coral()), known_color::coral)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_orange()), known_color::dark_orange)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::orange()), known_color::orange)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::yellow()), known_color::yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_yellow()), known_color::light_yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lemon_chiffon()), known_color::lemon_chiffon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_goldenrod_yellow()), known_color::light_goldenrod_yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::papaya_whip()), known_color::papaya_whip)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::moccasin()), known_color::moccasin)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::peach_puff()), known_color::peach_puff)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_goldenrod()), known_color::pale_goldenrod)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::khaki()), known_color::khaki)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_khaki()), known_color::dark_khaki)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::gold()), known_color::gold)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cornsilk()), known_color::cornsilk)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::blanched_almond()), known_color::blanched_almond)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::bisque()), known_color::bisque)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::navajo_white()), known_color::navajo_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::wheat()), known_color::wheat)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::burly_wood()), known_color::burly_wood)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::tan()), known_color::tan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::rosy_brown()), known_color::rosy_brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sandy_brown()), known_color::sandy_brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::goldenrod()), known_color::goldenrod)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_goldenrod()), known_color::dark_goldenrod)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::peru()), known_color::peru)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::chocolate()), known_color::chocolate)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::saddle_brown()), known_color::saddle_brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sienna()), known_color::sienna)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::brown()), known_color::brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::maroon()), known_color::maroon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_olive_green()), known_color::dark_olive_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::olive()), known_color::olive)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::olive_drab()), known_color::olive_drab)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::yellow_green()), known_color::yellow_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lime_green()), known_color::lime_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lime()), known_color::lime)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lawn_green()), known_color::lawn_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::chartreuse()), known_color::chartreuse)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::green_yellow()), known_color::green_yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::spring_green()), known_color::spring_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_spring_green()), known_color::medium_spring_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_green()), known_color::light_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_green()), known_color::pale_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_sea_green()), known_color::dark_sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_sea_green()), known_color::medium_sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sea_green()), known_color::sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::forest_green()), known_color::forest_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::green()), known_color::green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_green()), known_color::dark_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_aquamarine()), known_color::medium_aquamarine)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::aqua()), known_color::aqua)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cyan()), known_color::cyan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_cyan()), known_color::light_cyan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_turquoise()), known_color::pale_turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::aquamarine()), known_color::aquamarine)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::turquoise()), known_color::turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_turquoise()), known_color::medium_turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_turquoise()), known_color::dark_turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_sea_green()), known_color::light_sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cadet_blue()), known_color::cadet_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_cyan()), known_color::dark_cyan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::teal()), known_color::teal)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_steel_blue()), known_color::light_steel_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::powder_blue()), known_color::powder_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_blue()), known_color::light_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sky_blue()), known_color::sky_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_sky_blue()), known_color::light_sky_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::deep_sky_blue()), known_color::deep_sky_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dodger_blue()), known_color::dodger_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cornflower_blue()), known_color::cornflower_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::steel_blue()), known_color::steel_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::royal_blue()), known_color::royal_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::blue()), known_color::blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_blue()), known_color::medium_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_blue()), known_color::dark_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::navy()), known_color::navy)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::midnight_blue()), known_color::midnight_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lavender()), known_color::lavender)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::thistle()), known_color::thistle)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::plum()), known_color::plum)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::violet()), known_color::violet)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::orchid()), known_color::orchid)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::fuchsia()), known_color::fuchsia)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::magenta()), known_color::magenta)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_orchid()), known_color::medium_orchid)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_purple()), known_color::medium_purple)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::blue_violet()), known_color::blue_violet)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_violet()), known_color::dark_violet)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_orchid()), known_color::dark_orchid)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_magenta()), known_color::dark_magenta)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::purple()), known_color::purple)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::indigo()), known_color::indigo)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_slate_blue()), known_color::dark_slate_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::slate_blue()), known_color::slate_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_slate_blue()), known_color::medium_slate_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::white()), known_color::white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::snow()), known_color::snow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::honeydew()), known_color::honeydew)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::mint_cream()), known_color::mint_cream)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::azure()), known_color::azure)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::alice_blue()), known_color::alice_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::ghost_white()), known_color::ghost_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::white_smoke()), known_color::white_smoke)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::seashell()), known_color::seashell)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::beige()), known_color::beige)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::old_lace()), known_color::old_lace)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::floral_white()), known_color::floral_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::ivory()), known_color::ivory)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::antique_white()), known_color::antique_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::linen()), known_color::linen)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lavender_blush()), known_color::lavender_blush)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::misty_rose()), known_color::misty_rose)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::gainsboro()), known_color::gainsboro)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_grey()), known_color::light_grey)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::silver()), known_color::silver)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_gray()), known_color::dark_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::gray()), known_color::gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dim_gray()), known_color::dim_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_slate_gray()), known_color::light_slate_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::slate_gray()), known_color::slate_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_slate_gray()), known_color::dark_slate_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::black()), known_color::black)); + } + + return sm_; + } + + // Build a name -> constant map + static const color_string_map &get_color_to_string_map() { + static color_string_map vm_; // value -> name + + if (vm_.empty()) { + vm_.insert(std::make_pair(value_of(known_color::pink), named_color_type::pink())); + vm_.insert(std::make_pair(value_of(known_color::light_pink), named_color_type::light_pink())); + vm_.insert(std::make_pair(value_of(known_color::hot_pink), named_color_type::hot_pink())); + vm_.insert(std::make_pair(value_of(known_color::deep_pink), named_color_type::deep_pink())); + vm_.insert(std::make_pair(value_of(known_color::pale_violet_red), named_color_type::pale_violet_red())); + vm_.insert(std::make_pair(value_of(known_color::medium_violet_red), named_color_type::medium_violet_red())); + vm_.insert(std::make_pair(value_of(known_color::light_salmon), named_color_type::light_salmon())); + vm_.insert(std::make_pair(value_of(known_color::salmon), named_color_type::salmon())); + vm_.insert(std::make_pair(value_of(known_color::dark_salmon), named_color_type::dark_salmon())); + vm_.insert(std::make_pair(value_of(known_color::light_coral), named_color_type::light_coral())); + vm_.insert(std::make_pair(value_of(known_color::indian_red), named_color_type::indian_red())); + vm_.insert(std::make_pair(value_of(known_color::crimson), named_color_type::crimson())); + vm_.insert(std::make_pair(value_of(known_color::fire_brick), named_color_type::fire_brick())); + vm_.insert(std::make_pair(value_of(known_color::dark_red), named_color_type::dark_red())); + vm_.insert(std::make_pair(value_of(known_color::red), named_color_type::red())); + vm_.insert(std::make_pair(value_of(known_color::orange_red), named_color_type::orange_red())); + vm_.insert(std::make_pair(value_of(known_color::tomato), named_color_type::tomato())); + vm_.insert(std::make_pair(value_of(known_color::coral), named_color_type::coral())); + vm_.insert(std::make_pair(value_of(known_color::dark_orange), named_color_type::dark_orange())); + vm_.insert(std::make_pair(value_of(known_color::orange), named_color_type::orange())); + vm_.insert(std::make_pair(value_of(known_color::yellow), named_color_type::yellow())); + vm_.insert(std::make_pair(value_of(known_color::light_yellow), named_color_type::light_yellow())); + vm_.insert(std::make_pair(value_of(known_color::lemon_chiffon), named_color_type::lemon_chiffon())); + vm_.insert(std::make_pair(value_of(known_color::light_goldenrod_yellow), named_color_type::light_goldenrod_yellow())); + vm_.insert(std::make_pair(value_of(known_color::papaya_whip), named_color_type::papaya_whip())); + vm_.insert(std::make_pair(value_of(known_color::moccasin), named_color_type::moccasin())); + vm_.insert(std::make_pair(value_of(known_color::peach_puff), named_color_type::peach_puff())); + vm_.insert(std::make_pair(value_of(known_color::pale_goldenrod), named_color_type::pale_goldenrod())); + vm_.insert(std::make_pair(value_of(known_color::khaki), named_color_type::khaki())); + vm_.insert(std::make_pair(value_of(known_color::dark_khaki), named_color_type::dark_khaki())); + vm_.insert(std::make_pair(value_of(known_color::gold), named_color_type::gold())); + vm_.insert(std::make_pair(value_of(known_color::cornsilk), named_color_type::cornsilk())); + vm_.insert(std::make_pair(value_of(known_color::blanched_almond), named_color_type::blanched_almond())); + vm_.insert(std::make_pair(value_of(known_color::bisque), named_color_type::bisque())); + vm_.insert(std::make_pair(value_of(known_color::navajo_white), named_color_type::navajo_white())); + vm_.insert(std::make_pair(value_of(known_color::wheat), named_color_type::wheat())); + vm_.insert(std::make_pair(value_of(known_color::burly_wood), named_color_type::burly_wood())); + vm_.insert(std::make_pair(value_of(known_color::tan), named_color_type::tan())); + vm_.insert(std::make_pair(value_of(known_color::rosy_brown), named_color_type::rosy_brown())); + vm_.insert(std::make_pair(value_of(known_color::sandy_brown), named_color_type::sandy_brown())); + vm_.insert(std::make_pair(value_of(known_color::goldenrod), named_color_type::goldenrod())); + vm_.insert(std::make_pair(value_of(known_color::dark_goldenrod), named_color_type::dark_goldenrod())); + vm_.insert(std::make_pair(value_of(known_color::peru), named_color_type::peru())); + vm_.insert(std::make_pair(value_of(known_color::chocolate), named_color_type::chocolate())); + vm_.insert(std::make_pair(value_of(known_color::saddle_brown), named_color_type::saddle_brown())); + vm_.insert(std::make_pair(value_of(known_color::sienna), named_color_type::sienna())); + vm_.insert(std::make_pair(value_of(known_color::brown), named_color_type::brown())); + vm_.insert(std::make_pair(value_of(known_color::maroon), named_color_type::maroon())); + vm_.insert(std::make_pair(value_of(known_color::dark_olive_green), named_color_type::dark_olive_green())); + vm_.insert(std::make_pair(value_of(known_color::olive), named_color_type::olive())); + vm_.insert(std::make_pair(value_of(known_color::olive_drab), named_color_type::olive_drab())); + vm_.insert(std::make_pair(value_of(known_color::yellow_green), named_color_type::yellow_green())); + vm_.insert(std::make_pair(value_of(known_color::lime_green), named_color_type::lime_green())); + vm_.insert(std::make_pair(value_of(known_color::lime), named_color_type::lime())); + vm_.insert(std::make_pair(value_of(known_color::lawn_green), named_color_type::lawn_green())); + vm_.insert(std::make_pair(value_of(known_color::chartreuse), named_color_type::chartreuse())); + vm_.insert(std::make_pair(value_of(known_color::green_yellow), named_color_type::green_yellow())); + vm_.insert(std::make_pair(value_of(known_color::spring_green), named_color_type::spring_green())); + vm_.insert(std::make_pair(value_of(known_color::medium_spring_green), named_color_type::medium_spring_green())); + vm_.insert(std::make_pair(value_of(known_color::light_green), named_color_type::light_green())); + vm_.insert(std::make_pair(value_of(known_color::pale_green), named_color_type::pale_green())); + vm_.insert(std::make_pair(value_of(known_color::dark_sea_green), named_color_type::dark_sea_green())); + vm_.insert(std::make_pair(value_of(known_color::medium_sea_green), named_color_type::medium_sea_green())); + vm_.insert(std::make_pair(value_of(known_color::sea_green), named_color_type::sea_green())); + vm_.insert(std::make_pair(value_of(known_color::forest_green), named_color_type::forest_green())); + vm_.insert(std::make_pair(value_of(known_color::green), named_color_type::green())); + vm_.insert(std::make_pair(value_of(known_color::dark_green), named_color_type::dark_green())); + vm_.insert(std::make_pair(value_of(known_color::medium_aquamarine), named_color_type::medium_aquamarine())); + vm_.insert(std::make_pair(value_of(known_color::aqua), named_color_type::aqua())); + vm_.insert(std::make_pair(value_of(known_color::cyan), named_color_type::cyan())); + vm_.insert(std::make_pair(value_of(known_color::light_cyan), named_color_type::light_cyan())); + vm_.insert(std::make_pair(value_of(known_color::pale_turquoise), named_color_type::pale_turquoise())); + vm_.insert(std::make_pair(value_of(known_color::aquamarine), named_color_type::aquamarine())); + vm_.insert(std::make_pair(value_of(known_color::turquoise), named_color_type::turquoise())); + vm_.insert(std::make_pair(value_of(known_color::medium_turquoise), named_color_type::medium_turquoise())); + vm_.insert(std::make_pair(value_of(known_color::dark_turquoise), named_color_type::dark_turquoise())); + vm_.insert(std::make_pair(value_of(known_color::light_sea_green), named_color_type::light_sea_green())); + vm_.insert(std::make_pair(value_of(known_color::cadet_blue), named_color_type::cadet_blue())); + vm_.insert(std::make_pair(value_of(known_color::dark_cyan), named_color_type::dark_cyan())); + vm_.insert(std::make_pair(value_of(known_color::teal), named_color_type::teal())); + vm_.insert(std::make_pair(value_of(known_color::light_steel_blue), named_color_type::light_steel_blue())); + vm_.insert(std::make_pair(value_of(known_color::powder_blue), named_color_type::powder_blue())); + vm_.insert(std::make_pair(value_of(known_color::light_blue), named_color_type::light_blue())); + vm_.insert(std::make_pair(value_of(known_color::sky_blue), named_color_type::sky_blue())); + vm_.insert(std::make_pair(value_of(known_color::light_sky_blue), named_color_type::light_sky_blue())); + vm_.insert(std::make_pair(value_of(known_color::deep_sky_blue), named_color_type::deep_sky_blue())); + vm_.insert(std::make_pair(value_of(known_color::dodger_blue), named_color_type::dodger_blue())); + vm_.insert(std::make_pair(value_of(known_color::cornflower_blue), named_color_type::cornflower_blue())); + vm_.insert(std::make_pair(value_of(known_color::steel_blue), named_color_type::steel_blue())); + vm_.insert(std::make_pair(value_of(known_color::royal_blue), named_color_type::royal_blue())); + vm_.insert(std::make_pair(value_of(known_color::blue), named_color_type::blue())); + vm_.insert(std::make_pair(value_of(known_color::medium_blue), named_color_type::medium_blue())); + vm_.insert(std::make_pair(value_of(known_color::dark_blue), named_color_type::dark_blue())); + vm_.insert(std::make_pair(value_of(known_color::navy), named_color_type::navy())); + vm_.insert(std::make_pair(value_of(known_color::midnight_blue), named_color_type::midnight_blue())); + vm_.insert(std::make_pair(value_of(known_color::lavender), named_color_type::lavender())); + vm_.insert(std::make_pair(value_of(known_color::thistle), named_color_type::thistle())); + vm_.insert(std::make_pair(value_of(known_color::plum), named_color_type::plum())); + vm_.insert(std::make_pair(value_of(known_color::violet), named_color_type::violet())); + vm_.insert(std::make_pair(value_of(known_color::orchid), named_color_type::orchid())); + vm_.insert(std::make_pair(value_of(known_color::fuchsia), named_color_type::fuchsia())); + vm_.insert(std::make_pair(value_of(known_color::magenta), named_color_type::magenta())); + vm_.insert(std::make_pair(value_of(known_color::medium_orchid), named_color_type::medium_orchid())); + vm_.insert(std::make_pair(value_of(known_color::medium_purple), named_color_type::medium_purple())); + vm_.insert(std::make_pair(value_of(known_color::blue_violet), named_color_type::blue_violet())); + vm_.insert(std::make_pair(value_of(known_color::dark_violet), named_color_type::dark_violet())); + vm_.insert(std::make_pair(value_of(known_color::dark_orchid), named_color_type::dark_orchid())); + vm_.insert(std::make_pair(value_of(known_color::dark_magenta), named_color_type::dark_magenta())); + vm_.insert(std::make_pair(value_of(known_color::purple), named_color_type::purple())); + vm_.insert(std::make_pair(value_of(known_color::indigo), named_color_type::indigo())); + vm_.insert(std::make_pair(value_of(known_color::dark_slate_blue), named_color_type::dark_slate_blue())); + vm_.insert(std::make_pair(value_of(known_color::slate_blue), named_color_type::slate_blue())); + vm_.insert(std::make_pair(value_of(known_color::medium_slate_blue), named_color_type::medium_slate_blue())); + vm_.insert(std::make_pair(value_of(known_color::white), named_color_type::white())); + vm_.insert(std::make_pair(value_of(known_color::snow), named_color_type::snow())); + vm_.insert(std::make_pair(value_of(known_color::honeydew), named_color_type::honeydew())); + vm_.insert(std::make_pair(value_of(known_color::mint_cream), named_color_type::mint_cream())); + vm_.insert(std::make_pair(value_of(known_color::azure), named_color_type::azure())); + vm_.insert(std::make_pair(value_of(known_color::alice_blue), named_color_type::alice_blue())); + vm_.insert(std::make_pair(value_of(known_color::ghost_white), named_color_type::ghost_white())); + vm_.insert(std::make_pair(value_of(known_color::white_smoke), named_color_type::white_smoke())); + vm_.insert(std::make_pair(value_of(known_color::seashell), named_color_type::seashell())); + vm_.insert(std::make_pair(value_of(known_color::beige), named_color_type::beige())); + vm_.insert(std::make_pair(value_of(known_color::old_lace), named_color_type::old_lace())); + vm_.insert(std::make_pair(value_of(known_color::floral_white), named_color_type::floral_white())); + vm_.insert(std::make_pair(value_of(known_color::ivory), named_color_type::ivory())); + vm_.insert(std::make_pair(value_of(known_color::antique_white), named_color_type::antique_white())); + vm_.insert(std::make_pair(value_of(known_color::linen), named_color_type::linen())); + vm_.insert(std::make_pair(value_of(known_color::lavender_blush), named_color_type::lavender_blush())); + vm_.insert(std::make_pair(value_of(known_color::misty_rose), named_color_type::misty_rose())); + vm_.insert(std::make_pair(value_of(known_color::gainsboro), named_color_type::gainsboro())); + vm_.insert(std::make_pair(value_of(known_color::light_grey), named_color_type::light_grey())); + vm_.insert(std::make_pair(value_of(known_color::silver), named_color_type::silver())); + vm_.insert(std::make_pair(value_of(known_color::dark_gray), named_color_type::dark_gray())); + vm_.insert(std::make_pair(value_of(known_color::gray), named_color_type::gray())); + vm_.insert(std::make_pair(value_of(known_color::dim_gray), named_color_type::dim_gray())); + vm_.insert(std::make_pair(value_of(known_color::light_slate_gray), named_color_type::light_slate_gray())); + vm_.insert(std::make_pair(value_of(known_color::slate_gray), named_color_type::slate_gray())); + vm_.insert(std::make_pair(value_of(known_color::dark_slate_gray), named_color_type::dark_slate_gray())); + vm_.insert(std::make_pair(value_of(known_color::black), named_color_type::black())); + } + + return vm_; + } + + private: + basic_color_mapper() = delete; + ~basic_color_mapper() = delete; + basic_color_mapper(const basic_color_mapper&) = delete; + basic_color_mapper& operator =(const basic_color_mapper&) = delete; + + private: + // Convert string to lower case + static string_type to_lowercase(const string_type &str) { + static std::locale loc; + std::string result; + result.reserve(str.size()); + + for (auto elem : str) + result += std::tolower(elem, loc); + + return result; + } + }; + + } +} diff --git a/extern/cpp-colors/include/cpp-colors/pixel_format.h b/extern/cpp-colors/include/cpp-colors/pixel_format.h new file mode 100644 index 000000000..84b2b54a4 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/pixel_format.h @@ -0,0 +1,365 @@ +#pragma once + +#include +#include + + +namespace colors { + + // PixelFormat + namespace pixel_format { + enum pixel_format { + unknown, // Surface format is unknown + + indexed1, // 1-bit Indexed. Specifies that the pixel format is 1 bit per pixel and that it uses indexed color. The color table therefore has two colors in it. + indexed4, // 4-bit Indexed. Specifies that the format is 4 bits per pixel, indexed. + indexed8, // 8-bit Indexed. Specifies that the format is 8 bits per pixel, indexed. The color table therefore has 256 colors in it. + gray16, // 16-bit Grayscale. The pixel format is 16 bits per pixel. The color information specifies 65536 shades of gray. + + bgr24, // 24-bit RGB pixel format with 8 bits per channel. r8g8b8 + color, /* bgra32 */ // 32-bit ARGB pixel format with alpha, using 8 bits per channel. a8r8g8b8 + bgr32, // 32-bit RGB pixel format, where 8 bits are reserved for each color. x8r8g8b8 + bgr565, // 16-bit RGB pixel format with 5 bits for red, 6 bits for green, and 5 bits for blue. r5g6b5 + bgr555, // 16-bit pixel format where 5 bits are reserved for each color. x1r5g5b5 + bgra5551, // 16-bit pixel format where 5 bits are reserved for each color and 1 bit is reserved for alpha. a1r5g5b5 + rgba32, // 32-bit ABGR pixel format with alpha, using 8 bits per channel. a8b8g8r8 + rgb32, // 32-bit BGR pixel format, where 8 bits are reserved for each color. x8b8g8r8 + + bgrap32, // PARGB32. Specifies that the format is 32 bits per pixel; 8 bits each are used for the alpha, red, green, and blue components. The red, green, and blue components are premultiplied, according to the alpha component. + bgrap64, // PARGB64. Specifies that the format is 64 bits per pixel; 16 bits each are used for the alpha, red, green, and blue components. The red, green, and blue components are premultiplied according to the alpha component. + + bgr48, // 48-bit RGB. Specifies that the format is 48 bits per pixel; 16 bits each are used for the red, green, and blue components. + bgra64, // 64-bit ARGB. Specifies that the format is 64 bits per pixel; 16 bits each are used for the alpha, red, green, and blue components. + }; + } + + namespace pixel_format_flags { + enum pixel_format_flags { + has_alpha = 0x00000001, // Format has the Alpha channel + compressed = 0x00000002, // Compressed format + floating = 0x00000004, // Floating point format + depth = 0x00000008, // Depth format (depth textures) + }; + } + + + // Traits of a pixel format + template + struct pixel_traits; + + + // R8G8B8 (Bgr24) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint32_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::bgr24; + + // Tha name of the format + static const char* const name() { return "R8G8B8 (Bgr24)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 3; + + // A combination of one or more flags + static const uint32_t flags = 0; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 8; + static const uint8_t bits_g = 8; + static const uint8_t bits_b = 8; + static const uint8_t bits_a = 0; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0x00FF0000; + static const pixel_type mask_g = 0x0000FF00; + static const pixel_type mask_b = 0x000000FF; + static const pixel_type mask_a = 0x00000000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 16; + static const pixel_type shift_g = 8; + static const pixel_type shift_b = 0; + static const pixel_type shift_a = 0; + }; + + + // A8R8G8B8 (Color; Bgra32) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint32_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::color; + + // Tha name of the format + static const char* const name() { return "A8R8G8B8 (Bgra32)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 4; + + // A combination of one or more flags + static const uint32_t flags = pixel_format_flags::has_alpha; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 8; + static const uint8_t bits_g = 8; + static const uint8_t bits_b = 8; + static const uint8_t bits_a = 8; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0x00FF0000; + static const pixel_type mask_g = 0x0000FF00; + static const pixel_type mask_b = 0x000000FF; + static const pixel_type mask_a = 0xFF000000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 16; + static const pixel_type shift_g = 8; + static const pixel_type shift_b = 0; + static const pixel_type shift_a = 24; + }; + + + // X8R8G8B8 (Bgr32) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint32_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::bgr32; + + // Tha name of the format + static const char* const name() { return "X8R8G8B8 (Bgr32)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 4; + + // A combination of one or more flags + static const uint32_t flags = 0; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 8; + static const uint8_t bits_g = 8; + static const uint8_t bits_b = 8; + static const uint8_t bits_a = 0; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0x00FF0000; + static const pixel_type mask_g = 0x0000FF00; + static const pixel_type mask_b = 0x000000FF; + static const pixel_type mask_a = 0x00000000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 16; + static const pixel_type shift_g = 8; + static const pixel_type shift_b = 0; + static const pixel_type shift_a = 0; + }; + + + // A8B8G8R8 (Rgba32) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint32_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::rgba32; + + // Tha name of the format + static const char* const name() { return "A8B8G8R8 (Rgba32)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 4; + + // A combination of one or more flags + static const uint32_t flags = pixel_format_flags::has_alpha; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 8; + static const uint8_t bits_g = 8; + static const uint8_t bits_b = 8; + static const uint8_t bits_a = 8; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0x000000FF; + static const pixel_type mask_g = 0x0000FF00; + static const pixel_type mask_b = 0x00FF0000; + static const pixel_type mask_a = 0xFF000000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 0; + static const pixel_type shift_g = 8; + static const pixel_type shift_b = 16; + static const pixel_type shift_a = 24; + }; + + + // X8B8G8R8 (Rgb32) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint32_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::rgb32; + + // Tha name of the format + static const char* const name() { return "X8R8G8B8 (Rgb32)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 4; + + // A combination of one or more flags + static const uint32_t flags = 0; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 8; + static const uint8_t bits_g = 8; + static const uint8_t bits_b = 8; + static const uint8_t bits_a = 8; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0x000000FF; + static const pixel_type mask_g = 0x0000FF00; + static const pixel_type mask_b = 0x00FF0000; + static const pixel_type mask_a = 0x00000000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 0; + static const pixel_type shift_g = 8; + static const pixel_type shift_b = 16; + static const pixel_type shift_a = 0; + }; + + + // R5G6B5 (Bgr565) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint16_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::bgr565; + + // Tha name of the format + static const char* const name() { return "R5G6B5 (Bgr565)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 2; + + // A combination of one or more flags + static const uint32_t flags = 0; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 5; + static const uint8_t bits_g = 6; + static const uint8_t bits_b = 5; + static const uint8_t bits_a = 0; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0xF800; + static const pixel_type mask_g = 0x07E0; + static const pixel_type mask_b = 0x001F; + static const pixel_type mask_a = 0x0000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 11; + static const pixel_type shift_g = 5; + static const pixel_type shift_b = 0; + static const pixel_type shift_a = 0; + }; + + + // X1R5G5B5 (Bgr555) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint16_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::bgr555; + + // Tha name of the format + static const char* const name() { return "X1R5G5B5 (Bgr555)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 2; + + // A combination of one or more flags + static const uint32_t flags = 0; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 5; + static const uint8_t bits_g = 5; + static const uint8_t bits_b = 5; + static const uint8_t bits_a = 0; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0x7C00; + static const pixel_type mask_g = 0x03E0; + static const pixel_type mask_b = 0x001F; + static const pixel_type mask_a = 0x0000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 10; + static const pixel_type shift_g = 5; + static const pixel_type shift_b = 0; + static const pixel_type shift_a = 0; + }; + + + // A1R5G5B5 (Bgra5551) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint16_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::bgra5551; + + // Tha name of the format + static const char* const name() { return "A1R5G5B5 (Bgra5551)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 2; + + // A combination of one or more flags + static const uint32_t flags = pixel_format_flags::has_alpha; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 5; + static const uint8_t bits_g = 5; + static const uint8_t bits_b = 5; + static const uint8_t bits_a = 1; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0x7C00; + static const pixel_type mask_g = 0x03E0; + static const pixel_type mask_b = 0x001F; + static const pixel_type mask_a = 0x8000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 10; + static const pixel_type shift_g = 5; + static const pixel_type shift_b = 0; + static const pixel_type shift_a = 15; + }; + + + typedef pixel_traits bgra32_traits; + typedef pixel_traits bgr32_traits; + typedef pixel_traits rgba32_traits; + typedef pixel_traits rgb32_traits; + typedef pixel_traits bgr24_traits; + typedef pixel_traits bgr565_traits; + typedef pixel_traits bgr555_traits; + typedef pixel_traits bgra5551_traits; + + +} // namespace colors diff --git a/extern/cpp-colors/include/cpp-colors/pixel_utils.h b/extern/cpp-colors/include/cpp-colors/pixel_utils.h new file mode 100644 index 000000000..4c9ddf397 --- /dev/null +++ b/extern/cpp-colors/include/cpp-colors/pixel_utils.h @@ -0,0 +1,540 @@ +#pragma once + +#include +#include +#include "pixel_format.h" +#include "color_error.h" + +namespace colors { + + namespace detail { + + // Returns TRUE if the format has alpha channel + template + inline bool has_alpha() { + return ((pixel_traits::flags & pixel_format_flags::has_alpha) > 0); + } + + // Returns TRUE of the format compressed + template + inline bool is_compressed() { + return ((pixel_traits::flags & pixel_format_flags::compressed) > 0); + } + + } // namespace detail + + + + // Stride is the count of bytes between scanlines. + // Generally speaking, the bits that make up the pixels of a bitmap are packed into rows. + // A single row should be long enough to store one row of the bitmap's pixels. + // The stride is the length of a row measured in bytes, rounded up to the nearest DWORD (4 bytes). + // This allows bitmaps with fewer than 32 bits per pixel (bpp) to consume less memory while still + // providing good performance. You can use the following function to calculate the stride for a given bitmap. + // + // width - image width in pixels + // bit_count - bits per pixel + // + inline uint32_t get_stride(uint32_t width, uint32_t bit_count) { + assert(bit_count % 8 == 0); + + const uint32_t byteCount = bit_count / 8; + const uint32_t stride = (width * byteCount + 3) & ~3; + + assert(0 == stride % 4); + + return stride; + } + + + // converts Value from the Source to Destination bits representstion + template + inline T convert_bpc(T value, U srcBPC, U dstBPC) { + T result; + if (dstBPC < srcBPC) { + result = static_cast(value >> (srcBPC - dstBPC)); + } + else if (srcBPC < dstBPC) { + if (value == 0) { + result = static_cast(0); + } + else if (value == ((static_cast(1) << srcBPC) - 1)) { + result = static_cast((1 << dstBPC) - 1); + } + else { + result = static_cast(value * (1 << dstBPC) / ((1 << srcBPC) - 1)); + } + } + else { + result = static_cast(value); + } + + return result; + } + + + // Gets the size of a pixel (in bytes) for a given pixel format + inline uint32_t get_pixel_size(pixel_format::pixel_format pft) { + switch (pft) + { + case pixel_format::color: + return bgra32_traits::size; + + case pixel_format::bgr32: + return bgr32_traits::size; + + case pixel_format::rgba32: + return rgba32_traits::size; + + case pixel_format::rgb32: + return rgb32_traits::size; + + case pixel_format::bgr24: + return bgr24_traits::size; + + case pixel_format::bgr565: + return bgr565_traits::size; + + case pixel_format::bgr555: + return bgr555_traits::size; + + case pixel_format::bgra5551: + return bgra5551_traits::size; + + default: + assert(false && "Cannot get the size of a pixel."); + throw color_error("Cannot get the size of a pixel."); + } + } + + + inline unsigned int get_alpha_mask(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::mask_a; + + case pixel_format::bgr32: + return bgr32_traits::mask_a; + + case pixel_format::rgba32: + return rgba32_traits::mask_a; + + case pixel_format::rgb32: + return rgb32_traits::mask_a; + + case pixel_format::bgr24: + return bgr24_traits::mask_a; + + case pixel_format::bgr565: + return bgr565_traits::mask_a; + + case pixel_format::bgr555: + return bgr555_traits::mask_a; + + case pixel_format::bgra5551: + return bgra5551_traits::mask_a; + + default: + assert(false && "Cannot retreive the pixel alpha mask."); + throw color_error("Cannot retreive the pixel alpha mask."); + } + } + + + inline unsigned int get_red_mask(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::mask_r; + + case pixel_format::bgr32: + return bgr32_traits::mask_r; + + case pixel_format::rgba32: + return rgba32_traits::mask_r; + + case pixel_format::rgb32: + return rgb32_traits::mask_r; + + case pixel_format::bgr24: + return bgr24_traits::mask_r; + + case pixel_format::bgr565: + return bgr565_traits::mask_r; + + case pixel_format::bgr555: + return bgr555_traits::mask_r; + + case pixel_format::bgra5551: + return bgra5551_traits::mask_r; + + default: + assert(false && "Cannot retreive the pixel red mask."); + throw color_error("Cannot retreive the pixel red mask."); + } + } + + + inline unsigned int get_green_mask(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::mask_g; + + case pixel_format::bgr32: + return bgr32_traits::mask_g; + + case pixel_format::rgba32: + return rgba32_traits::mask_g; + + case pixel_format::rgb32: + return rgb32_traits::mask_g; + + case pixel_format::bgr24: + return bgr24_traits::mask_g; + + case pixel_format::bgr565: + return bgr565_traits::mask_g; + + case pixel_format::bgr555: + return bgr555_traits::mask_g; + + case pixel_format::bgra5551: + return bgra5551_traits::mask_g; + + default: + assert(false && "Cannot retreive the pixel green mask."); + throw color_error("Cannot retreive the pixel green mask."); + } + } + + + inline unsigned int get_blue_mask(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::mask_b; + + case pixel_format::bgr32: + return bgr32_traits::mask_b; + + case pixel_format::rgba32: + return rgba32_traits::mask_b; + + case pixel_format::rgb32: + return rgb32_traits::mask_b; + + case pixel_format::bgr24: + return bgr24_traits::mask_b; + + case pixel_format::bgr565: + return bgr565_traits::mask_b; + + case pixel_format::bgr555: + return bgr555_traits::mask_b; + + case pixel_format::bgra5551: + return bgra5551_traits::mask_b; + + default: + assert(false && "Cannot retreive the pixel blue mask."); + throw color_error("Cannot retreive the pixel blue mask."); + } + } + + + inline unsigned int get_alpha_shift(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::shift_a; + + case pixel_format::bgr32: + return bgr32_traits::shift_a; + + case pixel_format::rgba32: + return rgba32_traits::shift_a; + + case pixel_format::rgb32: + return rgb32_traits::shift_a; + + case pixel_format::bgr24: + return bgr24_traits::shift_a; + + case pixel_format::bgr565: + return bgr565_traits::shift_a; + + case pixel_format::bgr555: + return bgr555_traits::shift_a; + + case pixel_format::bgra5551: + return bgra5551_traits::shift_a; + + default: + assert(false && "Cannot retreive the pixel alpha shift."); + throw color_error("Cannot retreive the pixel alpha shift."); + } + } + + + inline unsigned int get_red_shift(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::shift_r; + + case pixel_format::bgr32: + return bgr32_traits::shift_r; + + case pixel_format::rgba32: + return rgba32_traits::shift_r; + + case pixel_format::rgb32: + return rgb32_traits::shift_r; + + case pixel_format::bgr24: + return bgr24_traits::shift_r; + + case pixel_format::bgr565: + return bgr565_traits::shift_r; + + case pixel_format::bgr555: + return bgr555_traits::shift_r; + + case pixel_format::bgra5551: + return bgra5551_traits::shift_r; + + default: + assert(false && "Cannot retreive the pixel red shift."); + throw color_error("Cannot retreive the pixel red shift."); + } + } + + + inline unsigned int get_green_shift(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::shift_g; + + case pixel_format::bgr32: + return bgr32_traits::shift_g; + + case pixel_format::rgba32: + return rgba32_traits::shift_g; + + case pixel_format::rgb32: + return rgb32_traits::shift_g; + + case pixel_format::bgr24: + return bgr24_traits::shift_g; + + case pixel_format::bgr565: + return bgr565_traits::shift_g; + + case pixel_format::bgr555: + return bgr555_traits::shift_g; + + case pixel_format::bgra5551: + return bgra5551_traits::shift_g; + + default: + assert(false && "Cannot retreive the pixel green shift."); + throw color_error("Cannot retreive the pixel green shift."); + } + } + + + inline unsigned int get_blue_shift(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::shift_b; + + case pixel_format::bgr32: + return bgr32_traits::shift_b; + + case pixel_format::rgba32: + return rgba32_traits::shift_b; + + case pixel_format::rgb32: + return rgb32_traits::shift_b; + + case pixel_format::bgr24: + return bgr24_traits::shift_b; + + case pixel_format::bgr565: + return bgr565_traits::shift_b; + + case pixel_format::bgr555: + return bgr555_traits::shift_b; + + case pixel_format::bgra5551: + return bgra5551_traits::shift_b; + + default: + assert(false && "Cannot retreive the pixel blue shift."); + throw color_error("Cannot retreive the pixel blue shift."); + } + } + + + inline unsigned int get_red_bits(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::bits_r; + + case pixel_format::bgr32: + return bgr32_traits::bits_r; + + case pixel_format::rgba32: + return rgba32_traits::bits_r; + + case pixel_format::rgb32: + return rgb32_traits::bits_r; + + case pixel_format::bgr24: + return bgr24_traits::bits_r; + + case pixel_format::bgr565: + return bgr565_traits::bits_r; + + case pixel_format::bgr555: + return bgr555_traits::bits_r; + + case pixel_format::bgra5551: + return bgra5551_traits::bits_r; + + default: + assert(false && "Cannot retreive the pixel red bits."); + throw color_error("Cannot retreive the pixel red bits."); + } + } + + + inline unsigned int get_green_bits(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::bits_g; + + case pixel_format::bgr32: + return bgr32_traits::bits_g; + + case pixel_format::rgba32: + return rgba32_traits::bits_g; + + case pixel_format::rgb32: + return rgb32_traits::bits_g; + + case pixel_format::bgr24: + return bgr24_traits::bits_g; + + case pixel_format::bgr565: + return bgr565_traits::bits_g; + + case pixel_format::bgr555: + return bgr555_traits::bits_g; + + case pixel_format::bgra5551: + return bgra5551_traits::bits_g; + + default: + assert(false && "Cannot retreive the pixel green bits."); + throw color_error("Cannot retreive the pixel green bits."); + } + } + + + inline unsigned int get_blue_bits(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::bits_b; + + case pixel_format::bgr32: + return bgr32_traits::bits_b; + + case pixel_format::rgba32: + return rgba32_traits::bits_b; + + case pixel_format::rgb32: + return rgb32_traits::bits_b; + + case pixel_format::bgr24: + return bgr24_traits::bits_b; + + case pixel_format::bgr565: + return bgr565_traits::bits_b; + + case pixel_format::bgr555: + return bgr555_traits::bits_b; + + case pixel_format::bgra5551: + return bgra5551_traits::bits_b; + + default: + assert(false && "Cannot retreive the pixel blue bits."); + throw color_error("Cannot retreive the pixel blue bits."); + } + } + + + inline unsigned int get_alpha_bits(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::bits_a; + + case pixel_format::bgr32: + return bgr32_traits::bits_a; + + case pixel_format::rgba32: + return rgba32_traits::bits_a; + + case pixel_format::rgb32: + return rgb32_traits::bits_a; + + case pixel_format::bgr24: + return bgr24_traits::bits_a; + + case pixel_format::bgr565: + return bgr565_traits::bits_a; + + case pixel_format::bgr555: + return bgr555_traits::bits_a; + + case pixel_format::bgra5551: + return bgra5551_traits::bits_a; + + default: + assert(false && "Cannot retreive the pixel alpha bits."); + throw color_error("Cannot retreive the pixel alpha bits."); + } + } + + + // Returns if the pixel format is compressed, false otherwise + inline bool is_compressed(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return detail::is_compressed(); + + case pixel_format::bgr32: + return detail::is_compressed(); + + case pixel_format::rgba32: + return detail::is_compressed(); + + case pixel_format::rgb32: + return detail::is_compressed(); + + case pixel_format::bgr24: + return detail::is_compressed(); + + case pixel_format::bgr565: + return detail::is_compressed(); + + case pixel_format::bgr555: + return detail::is_compressed(); + + case pixel_format::bgra5551: + return detail::is_compressed(); + + default: + assert(false && "Cannot determine whether the given pixel format is compressed."); + throw color_error("Cannot determine whether the given pixel format is compressed."); + } + } + +} // namespace colors diff --git a/extern/cpp-colors/test/CMakeLists.txt b/extern/cpp-colors/test/CMakeLists.txt new file mode 100644 index 000000000..02eb03457 --- /dev/null +++ b/extern/cpp-colors/test/CMakeLists.txt @@ -0,0 +1,19 @@ +set(SOURCE_TEST_FILES + suites/test_color.cpp + run_tests.cpp) + +find_package(GTest REQUIRED) +include_directories(${GTEST_INCLUDE_DIRS}) + +find_package(Threads REQUIRED) + +find_package(Boost) +include_directories(${Boost_INCLUDE_DIRS}) + +add_executable(run_tests ${SOURCE_TEST_FILES}) +target_link_libraries(run_tests ${GTEST_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) + +add_test(NAME all_tests + WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + COMMAND run_tests + ) \ No newline at end of file diff --git a/extern/cpp-colors/test/run_tests.cpp b/extern/cpp-colors/test/run_tests.cpp new file mode 100644 index 000000000..0194ab6e1 --- /dev/null +++ b/extern/cpp-colors/test/run_tests.cpp @@ -0,0 +1,7 @@ +#include + +int main(int argc, char* argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/extern/cpp-colors/test/suites/test_color.cpp b/extern/cpp-colors/test/suites/test_color.cpp new file mode 100644 index 000000000..f27318ddf --- /dev/null +++ b/extern/cpp-colors/test/suites/test_color.cpp @@ -0,0 +1,355 @@ +#include +#include +#include +#include +#include + +#include "cpp-colors/colors.h" + +using namespace std; +using namespace colors; + + +TEST(ColorFloat, CanBeCreatedFromWpfConstant) { + colorF c1 = wpf_colors::brown(); + ASSERT_EQ(0xFFA52A2A, c1.value()); +} + + +TEST(ColorInt, CanBeCreatedFromDotNetConstant) { + color c2 = dotnet_colors::brown(); + ASSERT_EQ(0xFFA52A2A, c2.value()); +} + +TEST(ColorInt, CanBeCreatedFromX11Constant) { + color c2 = x11_colors::brown(); + ASSERT_EQ(0xFFA52A2A, c2.value()); +} + + +TEST(ColorInt, CanBeCreatedFromConstant) { + color c2 = wpf_colors::brown(); + ASSERT_EQ(0xFFA52A2A, c2.value()); +} + +TEST(ColorInt, CanBeAssignedFromColorFloat) { + color c1 = wpf_colors::brown(); + colorF c3 = c1; + ASSERT_EQ(c3, c1); +} + +TEST(ColorFloat, CanBeAssignedFromColorInt) { + colorF c2 = wpf_colors::brown(); + color c4 = c2; + ASSERT_EQ(c2, c4); +} + +TEST(ColorARGB, CanBeAssignedFromColorFloat) { + colorF c4 = wpf_colors::brown(); + color_abrg c5 = c4; + ASSERT_EQ(c5, c4); +} + +TEST(ColorRed, CanBeFetchedFromAConstant) { + color cr = wpf_colors::red(); + ASSERT_EQ(0xFFFF0000, cr.value()); +} + +TEST(ColorGreen, CanBeFetchedFromAConstant) { + color cr = wpf_colors::green(); + ASSERT_EQ(0xFF008000, cr.value()); +} + +TEST(ColorBlue, CanBeFetchedFromAConstant) { + color cr = wpf_colors::blue(); + ASSERT_EQ(0xFF0000FF, cr.value()); +} + +TEST(ColorWhite, CanBeFetchedFromAConstant) { + color cr = wpf_colors::white(); + ASSERT_EQ(0xFFFFFFFF, cr.value()); +} + +TEST(ColorBlack, CanBeFetchedFromAConstant) { + color cr = wpf_colors::black(); + ASSERT_EQ(0xFF000000, cr.value()); +} + +TEST(Color, CanBeConverted) { + color c(value_of(wpf_known_color::blue)); + + ASSERT_EQ(0xFF0000FF, c.value()); // color, bgra32 + ASSERT_EQ(0x000000FF, c.value()); + ASSERT_EQ(0xFFFF0000, c.value()); + ASSERT_EQ(0x00FF0000, c.value()); + ASSERT_EQ(0x000000FF, c.value()); + ASSERT_EQ(0x0000001F, c.value()); + ASSERT_EQ(0x0000001F, c.value()); + ASSERT_EQ(0x0000801F, c.value()); + + colorF cf(c); + ASSERT_DOUBLE_EQ(1.0, cf.a); + ASSERT_DOUBLE_EQ(0.0, cf.r); + ASSERT_DOUBLE_EQ(0.0, cf.g); + ASSERT_DOUBLE_EQ(1.0, cf.b); + + c = color(128, 0, 0, 128); + cf = c; + ASSERT_NEAR(0.501, cf.a, 0.001); + ASSERT_NEAR(0.0, cf.r, 0.001); + ASSERT_NEAR(0.0, cf.g, 0.001); + ASSERT_NEAR(0.501, cf.b, 0.001); +} + +TEST(Colors, CanBeAdded) { + color c1 = wpf_colors::black(); + color c2(0x00010101); + + color c3 = c1 + c2; + ASSERT_EQ(0xFF010101, c3.value()); +} + +TEST(Color, Constants) { + color c1 = wpf_colors::red(); + ASSERT_EQ(color(value_of(wpf_known_color::red)), c1); + + color c2 = wpf_colors::blue(); + ASSERT_EQ(color(value_of(wpf_known_color::blue)), c2); + + color c3 = wpf_colors::green(); + ASSERT_EQ(color(value_of(wpf_known_color::green)), c3); +} + +TEST(Color, CanBeRepresentedAsHEXStringValue) { + color c1(0x01020304); + + // HEX + std::string s1 = to_hex_str(c1); + ASSERT_EQ("#020304", s1); + + std::wstring ws1 = to_hex_str(c1); + ASSERT_EQ(L"#020304", ws1); + + std::string s2 = to_ahex_str(c1); + ASSERT_EQ("#01020304", s2); + + std::wstring ws2 = to_ahex_str(c1); + ASSERT_EQ(L"#01020304", ws2); + + + color c2(255, 32, 64, 128); + std::string sb = to_ahex_str(c2); + ASSERT_EQ("#ff204080", sb); +} + +TEST(Color, CanBeRepresentedAsRGBStringValue) { + color c1(0x01020304); + + // RGB + std::string s3 = to_rgb_str(c1); + ASSERT_EQ("rgb(2,3,4)", s3); + + std::wstring ws3 = to_rgb_str(c1); + ASSERT_EQ(L"rgb(2,3,4)", ws3); + + std::string s4 = to_argb_str(c1); + ASSERT_EQ("argb(1,2,3,4)", s4); + + std::wstring ws4 = to_argb_str(c1); + ASSERT_EQ(L"argb(1,2,3,4)", ws4); + + + color c2(255, 32, 64, 128); + std::string sa = to_argb_str(c2); + ASSERT_EQ("argb(255,32,64,128)", sa); +} + +TEST(Color, CanBeSavedToAStream) { + color c1(0x01020304); + + std::ostringstream oss; + oss << c1; + std::string clr_str2 = oss.str(); + + ASSERT_FALSE(clr_str2.empty()); + ASSERT_EQ("argb(1,2,3,4)", clr_str2); +} + +TEST(Color, CanBeCastToString) { + color c1(0x01020304); + + std::string clr_str = boost::lexical_cast(c1); + ASSERT_FALSE(clr_str.empty()); + ASSERT_EQ("argb(1,2,3,4)", clr_str); +} + +TEST(ColorFloat, CanBeRepresentedAsString) { + + colorF c1(0.2, 0.6, 0.8); // r,g,b + color c2(255, 51, 153, 204); + + std::string sa = to_argb_str(c1); + ASSERT_EQ("argb(255,51,153,204)", sa); + ASSERT_EQ(c2, c1); +} + +TEST(ColorString, CanBeConvetedToColor) { + color c2(255, 32, 64, 128); // actual value + + // ARGB + ASSERT_EQ(c2, boost::lexical_cast("#ff204080")); + + // RGB + ASSERT_EQ(c2, boost::lexical_cast("#204080")); + + // ARGB + ASSERT_EQ(c2, boost::lexical_cast("argb(255,32,64,128)")); + + // RGB + ASSERT_EQ(c2, boost::lexical_cast("rgb(32,64,128)")); + + // RGB with spaces + ASSERT_EQ(c2, boost::lexical_cast("rgb(32, 64, 128)")); +} + +TEST(ColorWide, CanBeConvetedToColor) { + color c2(255, 32, 64, 128); // actual value + + // ARGB + ASSERT_EQ(c2, boost::lexical_cast(L"#ff204080")); + + // RGB + ASSERT_EQ(c2, boost::lexical_cast(L"#204080")); + + // ARGB + ASSERT_EQ(c2, boost::lexical_cast(L"argb(255,32,64,128)")); + + // RGB + ASSERT_EQ(c2, boost::lexical_cast(L"rgb(32,64,128)")); + + // RGB with spaces + ASSERT_EQ(c2, boost::lexical_cast(L"rgb(32, 64, 128)")); +} + +TEST(Color, CanBeConvertedToStringAndBack) { + color c1(1, 2, 3, 4); + string s1 = boost::lexical_cast(c1); + + color c2 = boost::lexical_cast(s1); + + ASSERT_EQ(c1, c2); +} + +TEST(ColorComponents, CanBeReadAndWrite) { + color c(255, 51, 153, 204); + + // access + color::value_type a = get<0>(c); + color::value_type r = get<1>(c); + color::value_type g = get<2>(c); + color::value_type b = get<3>(c); + + ASSERT_EQ(255, a); + ASSERT_EQ(51, r); + ASSERT_EQ(153, g); + ASSERT_EQ(204, b); + + // modify + get<0>(c) = 100; + get<1>(c) = 50; + get<2>(c) = 25; + get<3>(c) = 5; + + ASSERT_EQ(100, c.a); + ASSERT_EQ(50, c.r); + ASSERT_EQ(25, c.g); + ASSERT_EQ(5, c.b); + + // bounds test + // get<10>(c); // will issue the compile-time error: `Color index is out of range` +} + +TEST(NamedColors, CanBeCompared) { + ASSERT_EQ(wpf_known_color::green, wpf_named_color_converter::value("Green")); + ASSERT_EQ(wpf_known_color::green, wpf_named_color_converter::value("green")); + ASSERT_EQ(wpf_known_color::green, wpf_named_color_converter::value("GREEN")); +} + +TEST(Color, NameCanBeFetched) { + ASSERT_EQ(wpf_named_color::green(), wpf_named_color_converter::name(wpf_known_color::green)); + ASSERT_EQ(wpf_named_color::white(), wpf_named_color_converter::name(wpf_known_color::white)); +} + +TEST(Color, CanBeVerifiedAsANamedOne) { + uint32_t invalid_value = 0x11223344; + std::string invalid_name = "SuperGreen"; + + ASSERT_FALSE(wpf_named_color_converter::is_named(invalid_value)); + ASSERT_FALSE(wpf_named_color_converter::is_named(invalid_name)); +} + +TEST(Color, CanNotBeParsedFromInvalidNamedValue) { + try { + uint32_t invalid_value = 0x11223344; + std::string dummy = wpf_named_color_converter::name(invalid_value); (void)dummy; + FAIL() << "Must throw 'invalid_argument' exception."; + } + catch (std::invalid_argument& /*ex*/) { + /* no-op */ + } +} + +TEST(Color, CanNotBeParsedFromInvalidName) { + try { + std::string invalid_name = "SuperGreen"; + auto dummy = wpf_named_color_converter::value(invalid_name); (void)dummy; + FAIL() << "Must throw 'invalid_argument' exception."; + } + catch (std::invalid_argument& /*ex*/) { + /* no-op */ + } +} + +TEST(Color, CanBeWrittenAsAName) { + std::ostringstream oss; + color c1 = wpf_colors::yellow(); + write_named_color(oss, c1); + + ASSERT_EQ(std::string(wpf_named_color::yellow()), oss.str()); +}; + +TEST(Color, CanBeReadFromName) { + color c2; + std::istringstream iss(wpf_named_color::yellow()); + read_named_color(iss, c2); + + ASSERT_EQ(value_of(wpf_known_color::yellow), c2.value()); +} + +TEST(Brightness, CanBeMeasured) { + color c1; + + c1 = boost::lexical_cast(L"#ffef4444"); + ASSERT_EQ(brightness_value::dark, brightness(c1)); + + c1 = boost::lexical_cast(L"#ff009f75"); + ASSERT_EQ(brightness_value::dark, brightness(c1)); + + c1 = boost::lexical_cast(L"#fffaa31b"); + ASSERT_EQ(brightness_value::light, brightness(c1)); + + c1 = boost::lexical_cast(L"#ff88c6ed"); + ASSERT_EQ(brightness_value::light, brightness(c1)); + + c1 = boost::lexical_cast(L"#fffff000"); + ASSERT_EQ(brightness_value::light, brightness(c1)); + + c1 = boost::lexical_cast(L"#ff394ba0"); + ASSERT_EQ(brightness_value::dark, brightness(c1)); + + c1 = boost::lexical_cast(L"#ff82c341"); + ASSERT_EQ(brightness_value::light, brightness(c1)); + + c1 = boost::lexical_cast(L"#ffd54799"); + ASSERT_EQ(brightness_value::dark, brightness(c1)); +} \ No newline at end of file diff --git a/extern/include/cpp-colors/color.h b/extern/include/cpp-colors/color.h new file mode 100644 index 000000000..5103f0841 --- /dev/null +++ b/extern/include/cpp-colors/color.h @@ -0,0 +1,433 @@ +#pragma once + +#include +#include +#include +#include +#include "pixel_format.h" + + +namespace colors { + + namespace { + + // BITS-PER-CHANNEL conversion + + // SrcBPC < DstBPC + struct less_converter { + template + static T run(T value) { + if (value == 0) { + return static_cast(0); + } + else if (value == ((static_cast(1) << SrcBPC) - 1)) { + return static_cast((1 << DstBPC) - 1); + } + + return static_cast(value * (1 << DstBPC) / ((1 << SrcBPC) - 1)); + } + }; + + // SrcBPC > DstBPC + struct greater_conveter { + template + static T run(T value) { + return static_cast(value >> (SrcBPC - DstBPC)); + } + }; + + // SrcBPC == DstBPC + struct equal_converter { + template + static T run(T value) { + return value; + } + }; + + // SrcBPC != DstBPC + struct not_equal_converter { + template + static T run(T value) { + typedef typename std::conditional < SrcBPC < DstBPC, less_converter, greater_conveter>::type conv_type; + return conv_type::template run(value); + } + }; + + // converts Value from the Source to Destination bits representation + // SrcBPC ? DstBPC + struct converter { + template + static T run(T value) { + typedef typename std::conditional::type conv_type; + return conv_type::template run(value); + } + }; + + + // represents color channel information + template + struct element_traits; + + + // T is BYTE + template + struct element_traits { + typedef uint8_t value_type; + typedef typename PixelTraits::pixel_type pixel_type; + + static value_type min_value() { return 0x0; } + static value_type max_a() { return (1 << PixelTraits::bits_a) - 1; } + + // Alpha: { 0 -> transparent; 255 -> opaque } + + // Convert color value to the Destination Channel Bits Value + // dstBPC - Destination BitsPerChannel + template + static typename PixelTraits::channel_type to_channel_value(value_type value) { + return static_cast(converter::run(static_cast(value))); + } + + // Convert Source Channel Value into the + template + static value_type from_channel_value(pixel_type value) { + return static_cast(converter::run(value)); + } + }; + + // T is FLOAT + template + struct element_traits { + typedef double value_type; + typedef typename PixelTraits::pixel_type pixel_type; + + static double min_value() { return 0.0; } + static double max_value() { return 1.0; } + static double max_a() { return element_traits::max_value(); } + + // Alpha: { 0.0 -> transparent; 1.0 -> opaque } + + // Convert color value to the Destination Channel Bits Value + // dstBPC - Destination BitsPerChannel + template + static typename PixelTraits::channel_type to_channel_value(value_type value) { + if (value <= min_value()) { + return static_cast(0); + } + else if (value >= max_value()) { + return static_cast((1 << DstBPC) - 1); + } + else { + return static_cast(value * (1 << DstBPC)); + } + } + + // Convert Source Channel Value into the + template + static value_type from_channel_value(pixel_type value) { + return static_cast(static_cast(value) / static_cast((1 << SrcBPC) - 1)); + } + }; + + } // anonymous namespace + + + + // represents a color of a single pixel + // T -- might be an INTEGER or FLOAT type + // PixelTraits -- the format of packed color + template + struct basic_color { + typedef PixelTraits pixel_traits_type; + typedef element_traits element_traits_type; + + typedef typename pixel_traits_type::channel_type channel_type; + typedef typename pixel_traits_type::pixel_type pixel_type; + + typedef T value_type; + + template + friend struct basic_color; + + T a; // Alpha Component + T r; // Red Component + T g; // Green Component + T b; // Blue Component + + // ctor + basic_color() + : a(element_traits_type::max_a()), + r(element_traits_type::min_value()), + g(element_traits_type::min_value()), + b(element_traits_type::min_value()) { + /* no-op */ + } + + basic_color(const T& alpha, const T& red, const T& green, const T& blue) + : a(alpha), + r(red), + g(green), + b(blue) { + /* no-op */ + } + + basic_color(const T& red, const T& green, const T& blue) + : a(element_traits_type::max_a()), // max value --> opaque + r(red), + g(green), + b(blue) { + /* no-op */ + } + + // constructs a color using the packed value + basic_color(typename PixelTraits::pixel_type argb) { + basic_color temp = basic_color::create_from_value(argb); + std::swap(*this, temp); + } + + template + basic_color(const basic_color& other) { + typename PixelTraits::pixel_type packed = other.template traits_value(); + basic_color temp = basic_color::create_from_value(packed); + std::swap(*this, temp); + } + + // Get the packed color value, converted to the destination pixel format + template + uint32_t value() const { + typedef pixel_traits dest_traits_type; + return this->traits_value(); + } + + // Get the packed value of the color + typename PixelTraits::pixel_type value() const { + return this->traits_value(); + } + + private: + // Creates a basic_color object from the packed color value + template + static basic_color create_from_value(typename ST::pixel_type value) { + typedef typename ST::pixel_type src_pixel_type; + + return basic_color( + (element_traits_type::template from_channel_value((value & ST::mask_a) >> ST::shift_a)), + (element_traits_type::template from_channel_value((value & ST::mask_r) >> ST::shift_r)), + (element_traits_type::template from_channel_value((value & ST::mask_g) >> ST::shift_g)), + (element_traits_type::template from_channel_value((value & ST::mask_b) >> ST::shift_b)) + ); + } + + // Gets the packed value of the color using traits type + template + typename DT::pixel_type traits_value() const { + typedef typename DT::pixel_type dest_pixel_type; + typedef typename DT::channel_type dest_channel_type; + + dest_pixel_type value = ( + ((static_cast(element_traits_type::template to_channel_value(a)) << DT::shift_a) & DT::mask_a) | + ((static_cast(element_traits_type::template to_channel_value(r)) << DT::shift_r) & DT::mask_r) | + ((static_cast(element_traits_type::template to_channel_value(g)) << DT::shift_g) & DT::mask_g) | + ((static_cast(element_traits_type::template to_channel_value(b)) << DT::shift_b) & DT::mask_b) + ); + return value; + } + + public: + // add + basic_color& operator +=(const basic_color& other) { + this->r += other.r; + this->g += other.g; + this->b += other.b; + this->a += other.a; + return *this; + } + + // subsctract + basic_color& operator -=(const basic_color& other) { + this->r -= other.r; + this->g -= other.g; + this->b -= other.b; + this->a -= other.a; + return *this; + } + + // modulate + basic_color& operator *=(const basic_color& other) { + this->r *= other.r; + this->g *= other.g; + this->b *= other.b; + this->a *= other.a; + return *this; + } + + // scale + basic_color& operator *=(const T& scalar) { + this->r *= scalar; + this->g *= scalar; + this->b *= scalar; + this->a *= scalar; + return *this; + } + + template + basic_color& operator *=(const U& scalar) { + this->r *= scalar; + this->g *= scalar; + this->b *= scalar; + this->a *= scalar; + return *this; + } + + // modulate + basic_color& operator /=(const basic_color& other) { + this->r /= other.r; + this->g /= other.g; + this->b /= other.b; + this->a /= other.a; + return *this; + } + + // scale + basic_color& operator /=(const T& scalar) { + this->r /= scalar; + this->g /= scalar; + this->b /= scalar; + this->a /= scalar; + return *this; + } + + template + basic_color& operator /=(const U& scalar) { + this->r /= scalar; + this->g /= scalar; + this->b /= scalar; + this->a /= scalar; + return *this; + } + }; + + + + template + bool operator ==(const basic_color& lhs, const basic_color& rhs) { + return (lhs.a == rhs.a && + lhs.r == rhs.r && + lhs.g == rhs.g && + lhs.b == rhs.b + ); + } + + template + bool operator !=(const basic_color& lhs, const basic_color& rhs) { + return !(lhs == rhs); + } + + + + template + bool operator ==(const basic_color& lhs, const basic_color& rhs) { + basic_color other(rhs); + return lhs == other; + } + + template + bool operator !=(const basic_color& lhs, const basic_color& rhs) { + return !(lhs == rhs); + } + + + + template + basic_color operator +(const basic_color& lhs, const basic_color& rhs) { + basic_color result(lhs); + result += rhs; + return result; + } + + template + basic_color operator -(const basic_color& lhs, const basic_color& rhs) { + basic_color result(lhs); + result -= rhs; + return result; + } + + template + basic_color operator *(const basic_color& lhs, const basic_color& rhs) { + basic_color result(lhs); + result *= rhs; + return result; + } + + template + basic_color operator *(const T& lhs, const basic_color& rhs) { + basic_color result(rhs); + result *= lhs; + return result; + } + + template + basic_color operator *(const basic_color& lhs, const T& rhs) { + basic_color result(lhs); + result *= rhs; + return result; + } + + template + basic_color operator *(const U& lhs, const basic_color& rhs) { + basic_color result(rhs); + result *= lhs; + return result; + } + + template + basic_color operator *(const basic_color& lhs, const U& rhs) { + basic_color result(lhs); + result *= rhs; + return result; + } + + template + basic_color operator /(const basic_color& lhs, const basic_color& rhs) { + basic_color result(lhs); + result /= rhs; + return result; + } + + template + basic_color operator /(const basic_color& lhs, const U& rhs) { + basic_color result(lhs); + result /= rhs; + return result; + } + + // Get the color-component by index + template + inline T& get(basic_color& c) { + typedef T* pointer_type; + typedef typename PixelTraits::pixel_type pixel_type; + typedef std::pair pair_type; // shift, element + + static_assert(index < PixelTraits::size, "Color index is out of range."); + + pixel_type shift_a = PixelTraits::shift_a; + pixel_type shift_r = PixelTraits::shift_r; + pixel_type shift_g = PixelTraits::shift_g; + pixel_type shift_b = PixelTraits::shift_b; + + pair_type arr[PixelTraits::size] = { + pair_type(shift_a, &c.a), + pair_type(shift_r, &c.r), + pair_type(shift_g, &c.g), + pair_type(shift_b, &c.b) + }; + std::sort(arr, arr + PixelTraits::size, [](const pair_type& lhs, const pair_type& rhs) -> bool { return lhs.first > rhs.first; }); + + return *(arr[index].second); + } + + typedef basic_color color; /* color_argb */ + typedef basic_color colorF; /* color_argbF */ + + typedef basic_color color_abrg; + +} // namespace colors diff --git a/extern/include/cpp-colors/color_error.h b/extern/include/cpp-colors/color_error.h new file mode 100644 index 000000000..4d5fd83fb --- /dev/null +++ b/extern/include/cpp-colors/color_error.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +namespace colors { + + class color_error + : public std::runtime_error { + public: + explicit color_error(const std::string& message) + : std::runtime_error(message) { + /* no-op */ + } + + virtual ~color_error() { + /* no-op */ + } + }; + +} // namespace colors diff --git a/extern/include/cpp-colors/color_io.h b/extern/include/cpp-colors/color_io.h new file mode 100644 index 000000000..ca1237391 --- /dev/null +++ b/extern/include/cpp-colors/color_io.h @@ -0,0 +1,605 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "color.h" +#include "color_named.h" + + +namespace colors { + + namespace { + + template + struct symbols; + + template <> + struct symbols { + static const char sharp = '#'; + static const char zero = '0'; + + static const char a = 'a'; + static const char r = 'r'; + static const char g = 'g'; + static const char b = 'b'; + + static const char round_bracket_open = '('; + static const char round_bracket_close = ')'; + static const char comma = ','; + + static const char number_0 = '0'; + static const char number_9 = '9'; + static const char char_A = 'A'; + static const char char_F = 'F'; + static const char char_a = 'a'; + static const char char_f = 'f'; + + static const char* const rgb_tag() { return "rgb"; } + static const char* const argb_tag() { return "argb"; } + + static const char to_lower(char ch) { + return static_cast(std::tolower(ch)); + } + }; + + template <> + struct symbols { + static const wchar_t sharp = L'#'; + static const wchar_t zero = L'0'; + + static const wchar_t a = L'a'; + static const wchar_t r = L'r'; + static const wchar_t g = L'g'; + static const wchar_t b = L'b'; + + static const wchar_t number_0 = L'0'; + static const wchar_t number_9 = L'9'; + static const wchar_t char_A = L'A'; + static const wchar_t char_F = L'F'; + static const wchar_t char_a = L'a'; + static const wchar_t char_f = L'f'; + + static const wchar_t round_bracket_open = L'('; + static const wchar_t round_bracket_close = L')'; + static const wchar_t comma = L','; + + static const wchar_t* const rgb_tag() { return L"rgb"; } + static const wchar_t* const argb_tag() { return L"argb"; } + + static const wchar_t to_lower(wchar_t ch) + { + return static_cast(towlower(ch)); + } + }; + + + // Convert to [Color RGB with Alpha argb(a,r,g,b)] + template + struct to_argb_str_dispatch; + + template + struct to_argb_str_dispatch { + typedef basic_color color_type; + typedef std::basic_string string_type; + + color_type c; + + to_argb_str_dispatch(const color_type& ci) + : c(ci) { + } + + operator string_type() { + typedef std::basic_ostringstream ostringstream_type; + ostringstream_type oss; + + oss << symbols::argb_tag() << symbols::round_bracket_open + << static_cast(c.a) << symbols::comma + << static_cast(c.r) << symbols::comma + << static_cast(c.g) << symbols::comma + << static_cast(c.b) + << symbols::round_bracket_close; + + return oss.str(); + } + }; + + template + struct to_argb_str_dispatch { + typedef basic_color color_type; + typedef basic_color color_double_type; + typedef std::basic_string string_type; + + color_type c; + + to_argb_str_dispatch(const color_double_type& cf) + : c(cf) { + } + + operator string_type() { + return to_argb_str_dispatch(c); + } + }; + + + // Convert to [Color RGB rgb(r,g,b)], ignore alpha + template + struct to_rgb_str_dispatch; + + template + struct to_rgb_str_dispatch { + typedef basic_color color_type; + typedef std::basic_string string_type; + + color_type c; + + to_rgb_str_dispatch(const color_type& ci) + : c(ci) { + } + + operator string_type() { + typedef std::basic_ostringstream ostringstream_type; + ostringstream_type oss; + + oss << symbols::rgb_tag() << symbols::round_bracket_open + << static_cast(c.r) << symbols::comma + << static_cast(c.g) << symbols::comma + << static_cast(c.b) + << symbols::round_bracket_close; + + return oss.str(); + } + }; + + template + struct to_rgb_str_dispatch { + typedef basic_color color_type; + typedef basic_color color_double_type; + typedef std::basic_string string_type; + + color_type c; + + to_rgb_str_dispatch(const color_double_type& cf) + : c(cf) { + } + + operator string_type() { + return to_rgb_str_dispatch(c); + } + }; + + + // Convert to [Color HEX with Alpha (#AARRGGBB)] + template + struct to_ahex_str_dispatch; + + template + struct to_ahex_str_dispatch { + typedef basic_color color_type; + typedef std::basic_string string_type; + + color_type c; + + to_ahex_str_dispatch(const color_type& ci) + : c(ci) { + } + + operator string_type() { + typedef std::basic_ostringstream ostringstream_type; + ostringstream_type oss; + + oss << symbols::sharp << std::hex + << std::setw(2) << std::setfill(symbols::zero) << static_cast(c.a) + << std::setw(2) << std::setfill(symbols::zero) << static_cast(c.r) + << std::setw(2) << std::setfill(symbols::zero) << static_cast(c.g) + << std::setw(2) << std::setfill(symbols::zero) << static_cast(c.b); + + return oss.str(); + } + }; + + template + struct to_ahex_str_dispatch { + typedef basic_color color_type; + typedef basic_color color_double_type; + typedef std::basic_string string_type; + + color_type c; + + to_ahex_str_dispatch(const color_double_type& cf) + : c(cf) { + } + + operator string_type() { + return to_ahex_str_dispatch(c); + } + }; + + + // Convert to [Color HEX (#RRGGBB)], ignore alpha + template + struct to_hex_str_dispatch; + + template + struct to_hex_str_dispatch { + typedef basic_color color_type; + typedef std::basic_string string_type; + + color_type c; + + to_hex_str_dispatch(const color_type& ci) + : c(ci) { + } + + operator string_type() { + typedef std::basic_ostringstream ostringstream_type; + ostringstream_type oss; + + oss << symbols::sharp << std::hex + << std::setw(2) << std::setfill(symbols::zero) << static_cast(c.r) + << std::setw(2) << std::setfill(symbols::zero) << static_cast(c.g) + << std::setw(2) << std::setfill(symbols::zero) << static_cast(c.b); + + return oss.str(); + } + }; + + template + struct to_hex_str_dispatch { + typedef basic_color color_type; + typedef basic_color color_double_type; + typedef std::basic_string string_type; + + color_type c; + + to_hex_str_dispatch(const color_double_type& cf) + : c(cf) { + } + + operator string_type() { + return to_hex_str_dispatch(c); + } + }; + + + template + void assign_color_value(basic_color& c, size_t idx, typename basic_color::channel_type value) { + assert(idx < 4 && "IndexOutOfRange"); + + switch (idx) { + case 0: + c.a = value; + break; + case 1: + c.r = value; + break; + case 2: + c.g = value; + break; + case 3: + c.b = value; + break; + }; + } + + + } // end of anonymous namespace + + + + // Write named color + template + inline std::basic_ostream& write_named_color(std::basic_ostream& os, const basic_color& c) { + typedef basic_named_color_converter color_converter; + + if (color_converter::is_named(c.value())) { + os << color_converter::name(c.value()); + } + + return os; + } + + // Read named color + template + inline std::basic_istream& read_named_color(std::basic_istream& is, basic_color& c) { + typedef std::basic_string string_type; + typedef basic_named_color_converter named_converter; + + string_type name; + if (is >> name) { + if (named_converter::is_named(name)) { + c = basic_color(value_of(named_converter::value(name))); + } + else { + is.setstate(std::ios_base::failbit); + } + } + + return is; + } + + + + // Convert to [Color HEX (#RRGGBB)], ignore alpha + template + inline std::basic_string to_hex_str(const basic_color& c) { + typedef basic_color color_type; + return to_hex_str_dispatch::value >(c); + } + + // Convert to [Color HEX with Alpha (#AARRGGBB)] + template + inline std::basic_string to_ahex_str(const basic_color& c) { + typedef basic_color color_type; + return to_ahex_str_dispatch::value >(c); + } + + // Convert to [Color RGB rgb(r,g,b)], ignore alpha + template + inline std::basic_string to_rgb_str(const basic_color& c) { + typedef basic_color color_type; + return to_rgb_str_dispatch::value >(c); + } + + // Convert to [Color RGB with Alpha argb(a,r,g,b)] + template + inline std::basic_string to_argb_str(const basic_color& c) { + typedef basic_color color_type; + return to_argb_str_dispatch::value >(c); + } + + + + template + inline std::basic_ostream& operator<<(std::basic_ostream& os, const basic_color& c) { + std::basic_ostringstream > oss; + + oss.flags(os.flags()); + oss.imbue(os.getloc()); + oss.precision(os.precision()); + + oss << to_argb_str(c); + + return os << oss.str().c_str(); + } + + + template + void parse_hex(std::basic_istream& is, basic_color& c) { + typedef basic_color color_type; + typedef typename color_type::channel_type channel_type; + typedef std::basic_stringstream > stringstream_type; + + // allowed format #RRGGBB or #AARRGGBB + CharT ch; + if (is >> ch && ch != symbols::sharp) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + bool is_argb = true; + const size_t data_len = 4; + for (size_t i = 0; i != data_len; ++i) { + // parse HEX + channel_type chv = 0; + const size_t hex_len = 2; + for (size_t j = 0; j != hex_len; ++j) { + if (is >> ch && ((ch <= symbols::number_9 && symbols::number_0 <= ch) || + (symbols::char_a <= ch && ch <= symbols::char_f) || + (symbols::char_A <= ch && ch <= symbols::char_F))) { + CharT ch1 = symbols::to_lower(ch); + channel_type val = 0; + if (ch1 <= symbols::number_9 && symbols::number_0 <= ch1) { + val = static_cast(ch1 - symbols::number_0); + } + else { // ('a' <= ch1 && ch1 <= 'f') + val = static_cast(10 + (ch1 - symbols::char_a)); + } + + chv <<= 4; + chv |= val; + } + else { + is.putback(ch); + + if (i < data_len - 1) { + is.setstate(std::ios_base::failbit); + } + else { + is.clear(); + is_argb = false; + } + + break; + } + } + + if (!is) + break; + + // #AARRGGBB + assign_color_value(c, i, chv); + } // for + + if (!is_argb) { + // we have RGB instead of ARGB, adjust colors + c.b = c.g; + c.g = c.r; + c.r = c.a; + c.a = color_type::element_traits_type::max_a(); + } + } + } + + + template + void parse_argb(std::basic_istream& is, basic_color& c) { + typedef basic_color color_type; + typedef typename color_type::channel_type channel_type; + typedef std::basic_stringstream > stringstream_type; + + CharT ch; + if (is >> ch && ch != symbols::a) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + if (is >> ch && ch != symbols::r) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + if (is >> ch && ch != symbols::g) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + if (is >> ch && ch != symbols::b) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + if (is >> ch && ch != symbols::round_bracket_open) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + const size_t data_len = 4; + for (size_t i = 0; i != data_len; ++i) { + unsigned int chv; + bool is_stop = false; + if (is >> std::ws >> chv >> ch && ch != symbols::comma) { + is.putback(ch); + if (i < data_len - 1) + is.setstate(std::ios_base::failbit); + + is_stop = true; + } + + assign_color_value(c, i, static_cast(chv)); + + if (is_stop) + break; + } + + if (is >> ch && ch != symbols::round_bracket_close) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + } + } + } + } + } + } + + + template + void parse_rgb(std::basic_istream& is, basic_color& c) { + typedef basic_color color_type; + typedef typename color_type::channel_type channel_type; + typedef std::basic_stringstream > stringstream_type; + + CharT ch; + if (is >> ch && ch != symbols::r) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + if (is >> ch && ch != symbols::g) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + if (is >> ch && ch != symbols::b) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + if (is >> ch && ch != symbols::round_bracket_open) + { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + else if (!is.fail()) { + const size_t data_len = 3; + for (size_t i = 0; i != data_len; ++i) + { + unsigned int chv; + bool is_stop = false; + if (is >> std::ws >> chv >> ch && ch != symbols::comma) { + is.putback(ch); + if (i < data_len - 1) + is.setstate(std::ios_base::failbit); + + is_stop = true; + } + + assign_color_value(c, i + 1, static_cast(chv)); // i==0 --- Alpha + + if (is_stop) + break; + } + + if (is >> ch && ch != symbols::round_bracket_close) { + is.putback(ch); + is.setstate(std::ios_base::failbit); + } + } + } + } + } + } + + + template + inline std::basic_istream& operator>>(std::basic_istream& is, basic_color& c) { + typedef basic_color color_type; + typedef typename color_type::channel_type channel_type; + typedef std::basic_stringstream > stringstream_type; + + // Parse Formats: + // * Color HEX -- #RRGGBB + // * Color HEX with Alpha(AHEX) -- #AARRGGBB + // * Color RGB -- rgb(r,g,b) + // * Color RGB with Alpha(ARGB) -- argb(a,r,g,b) + // OR + // * We recognize one of the named colors. See: color_named.h + + CharT ch; + if (is >> ch) { + is.putback(ch); + if (ch != symbols::sharp && ch != symbols::r && ch != symbols::a) { + //read_named_color(is, c); + is.setstate(std::ios_base::failbit); + } + else { + if (ch == symbols::sharp) { + parse_hex(is, c); + } + else if (ch == symbols::a) { + parse_argb(is, c); + } + else if (ch == symbols::r) { + parse_rgb(is, c); + } + else { + is.setstate(std::ios_base::failbit); + } + } + } + else { + is.setstate(std::ios_base::failbit); + } + + return is; + } + + +} // namespace colors diff --git a/extern/include/cpp-colors/color_named.h b/extern/include/cpp-colors/color_named.h new file mode 100644 index 000000000..51056dc86 --- /dev/null +++ b/extern/include/cpp-colors/color_named.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include +#include "impl/named_utils.h" + + +namespace colors { + + // Used to convert named colors to the numeric value + template + class basic_named_color_converter { + private: + typedef ColorMapper color_mapper_type; + + typedef typename ColorMapper::char_type char_type; + typedef typename ColorMapper::string_type string_type; + + typedef typename color_mapper_type::string_color_map string_color_map; + typedef typename color_mapper_type::color_string_map color_string_map; + + typedef typename string_color_map::mapped_type known_color_type; + + private: + basic_named_color_converter(); + ~basic_named_color_converter(); + + public: + // Get the value of the color + static known_color_type value(const string_type& str) { + const string_color_map& sm = color_mapper_type::get_string_to_color_map(); + typename string_color_map::const_iterator it = sm.find(to_lowercase(str)); + if (it == sm.end()) + throw std::invalid_argument("Invalid color name."); + + return it->second; + } + + // Get the name of the color + static string_type name(typename ColorMapper::known_color_type value) { + return basic_named_color_converter::name(static_cast(value)); + } + + // Get the name of the color + static string_type name(int32_t value) { + const color_string_map& vm = color_mapper_type::get_color_to_string_map(); + typename color_string_map::const_iterator it = vm.find(value); + if (it == vm.end()) + throw std::invalid_argument("Invalid color value."); + + return it->second; + } + + // Returns TRUE if the argument is a named color + static bool is_named(const string_type& str) { + const string_color_map& sm = color_mapper_type::get_string_to_color_map(); + return (sm.find(to_lowercase(str)) != sm.end()); + } + + static bool is_named(uint32_t value) { + const color_string_map& vm = color_mapper_type::get_color_to_string_map(); + return (vm.find(value) != vm.end()); + } + + private: + // Convert string to lower case + static string_type to_lowercase(const string_type &str) { + static std::locale loc; + std::string result; + result.reserve(str.size()); + + for (auto elem : str) + result += std::tolower(elem, loc); + + return result; + } + }; + +} // namespace colors diff --git a/extern/include/cpp-colors/color_utils.h b/extern/include/cpp-colors/color_utils.h new file mode 100644 index 000000000..36ae6c018 --- /dev/null +++ b/extern/include/cpp-colors/color_utils.h @@ -0,0 +1,27 @@ +#pragma once + +#include "color.h" + + +namespace colors { + + enum class brightness_value { + light, + dark + }; + + /** + * Check whether the color is light or dark + * The function converts the RGB color space into YIQ + * + * http://www.w3.org/TR/AERT#color-contrast + */ + template + brightness_value brightness(const basic_color& c) { + color temp(c); + auto value = ((temp.r * 299.0) + (temp.g * 587.0) + (temp.b * 114.0)) / 1000.0; + return (value >= 128.0 ? brightness_value::light : brightness_value::dark); + } + + +} diff --git a/extern/include/cpp-colors/colors.h b/extern/include/cpp-colors/colors.h new file mode 100644 index 000000000..5a47114c4 --- /dev/null +++ b/extern/include/cpp-colors/colors.h @@ -0,0 +1,18 @@ +#pragma once + +#include "color.h" +#include "color_named.h" +#include "color_io.h" +#include "color_utils.h" +#include "pixel_format.h" +#include "pixel_utils.h" + +#include "impl/colors_impl.h" +#include "impl/constants_impl.h" +#include "impl/named_impl.h" + +namespace colors { + typedef basic_named_color_converter dotnet_named_color_converter; + typedef basic_named_color_converter wpf_named_color_converter; + typedef basic_named_color_converter x11_named_color_converter; +} diff --git a/extern/include/cpp-colors/impl/colors_impl.h b/extern/include/cpp-colors/impl/colors_impl.h new file mode 100644 index 000000000..fb12da345 --- /dev/null +++ b/extern/include/cpp-colors/impl/colors_impl.h @@ -0,0 +1,13 @@ +#pragma once + +#include "dotnet/dotnet_colors.h" +#include "wpf/wpf_colors.h" +#include "x11/x11_colors.h" + +namespace colors { + + typedef dotnet::basic_colors dotnet_colors; + typedef wpf::basic_colors wpf_colors; + typedef x11::basic_colors x11_colors; + +} // namespace colors diff --git a/extern/include/cpp-colors/impl/constants_impl.h b/extern/include/cpp-colors/impl/constants_impl.h new file mode 100644 index 000000000..b37d68319 --- /dev/null +++ b/extern/include/cpp-colors/impl/constants_impl.h @@ -0,0 +1,13 @@ +#pragma once + +#include "dotnet/dotnet_constants.h" +#include "wpf/wpf_constants.h" +#include "x11/x11_constants.h" + +namespace colors { + + typedef dotnet::known_color dotnet_known_color; + typedef wpf::known_color wpf_known_color; + typedef x11::known_color x11_known_color; + +} // namespace colors diff --git a/extern/include/cpp-colors/impl/dotnet/dotnet_colors.h b/extern/include/cpp-colors/impl/dotnet/dotnet_colors.h new file mode 100644 index 000000000..542f34b4b --- /dev/null +++ b/extern/include/cpp-colors/impl/dotnet/dotnet_colors.h @@ -0,0 +1,585 @@ +#pragma once + +#include "dotnet_constants.h" +#include "../named_utils.h" + + +namespace colors { + namespace dotnet { + + template + struct basic_colors { + typedef ColorType color_type; + + basic_colors() = delete; + ~basic_colors() = delete; + basic_colors(const basic_colors&) = delete; + basic_colors& operator =(const basic_colors&) = delete; + + static color_type alice_blue() { + return color_type(value_of(known_color::alice_blue)); + } + + static color_type antique_white() { + return color_type(value_of(known_color::antique_white)); + } + + static color_type aqua() { + return color_type(value_of(known_color::aqua)); + } + + static color_type aquamarine() { + return color_type(value_of(known_color::aquamarine)); + } + + static color_type azure() { + return color_type(value_of(known_color::azure)); + } + + static color_type beige() { + return color_type(value_of(known_color::beige)); + } + + static color_type bisque() { + return color_type(value_of(known_color::bisque)); + } + + static color_type black() { + return color_type(value_of(known_color::black)); + } + + static color_type blanched_almond() { + return color_type(value_of(known_color::blanched_almond)); + } + + static color_type blue() { + return color_type(value_of(known_color::blue)); + } + + static color_type blue_violet() { + return color_type(value_of(known_color::blue_violet)); + } + + static color_type brown() { + return color_type(value_of(known_color::brown)); + } + + static color_type burly_wood() { + return color_type(value_of(known_color::burly_wood)); + } + + static color_type cadet_blue() { + return color_type(value_of(known_color::cadet_blue)); + } + + static color_type chartreuse() { + return color_type(value_of(known_color::chartreuse)); + } + + static color_type chocolate() { + return color_type(value_of(known_color::chocolate)); + } + + static color_type coral() { + return color_type(value_of(known_color::coral)); + } + + static color_type cornflower_blue() { + return color_type(value_of(known_color::cornflower_blue)); + } + + static color_type cornsilk() { + return color_type(value_of(known_color::cornsilk)); + } + + static color_type crimson() { + return color_type(value_of(known_color::crimson)); + } + + static color_type cyan() { + return color_type(value_of(known_color::cyan)); + } + + static color_type dark_blue() { + return color_type(value_of(known_color::dark_blue)); + } + + static color_type dark_cyan() { + return color_type(value_of(known_color::dark_cyan)); + } + + static color_type dark_goldenrod() { + return color_type(value_of(known_color::dark_goldenrod)); + } + + static color_type dark_gray() { + return color_type(value_of(known_color::dark_gray)); + } + + static color_type dark_green() { + return color_type(value_of(known_color::dark_green)); + } + + static color_type dark_khaki() { + return color_type(value_of(known_color::dark_khaki)); + } + + static color_type dark_magenta() { + return color_type(value_of(known_color::dark_magenta)); + } + + static color_type dark_olive_green() { + return color_type(value_of(known_color::dark_olive_green)); + } + + static color_type dark_orange() { + return color_type(value_of(known_color::dark_orange)); + } + + static color_type dark_orchid() { + return color_type(value_of(known_color::dark_orchid)); + } + + static color_type dark_red() { + return color_type(value_of(known_color::dark_red)); + } + + static color_type dark_salmon() { + return color_type(value_of(known_color::dark_salmon)); + } + + static color_type dark_sea_green() { + return color_type(value_of(known_color::dark_sea_green)); + } + + static color_type dark_slate_blue() { + return color_type(value_of(known_color::dark_slate_blue)); + } + + static color_type dark_slate_gray() { + return color_type(value_of(known_color::dark_slate_gray)); + } + + static color_type dark_turquoise() { + return color_type(value_of(known_color::dark_turquoise)); + } + + static color_type dark_violet() { + return color_type(value_of(known_color::dark_violet)); + } + + static color_type deep_pink() { + return color_type(value_of(known_color::deep_pink)); + } + + static color_type deep_sky_blue() { + return color_type(value_of(known_color::deep_sky_blue)); + } + + static color_type dim_gray() { + return color_type(value_of(known_color::dim_gray)); + } + + static color_type dodger_blue() { + return color_type(value_of(known_color::dodger_blue)); + } + + static color_type firebrick() { + return color_type(value_of(known_color::firebrick)); + } + + static color_type floral_white() { + return color_type(value_of(known_color::floral_white)); + } + + static color_type forest_green() { + return color_type(value_of(known_color::forest_green)); + } + + static color_type fuchsia() { + return color_type(value_of(known_color::fuchsia)); + } + + static color_type gainsboro() { + return color_type(value_of(known_color::gainsboro)); + } + + static color_type ghost_white() { + return color_type(value_of(known_color::ghost_white)); + } + + static color_type gold() { + return color_type(value_of(known_color::gold)); + } + + static color_type goldenrod() { + return color_type(value_of(known_color::goldenrod)); + } + + static color_type gray() { + return color_type(value_of(known_color::gray)); + } + + static color_type green() { + return color_type(value_of(known_color::green)); + } + + static color_type green_yellow() { + return color_type(value_of(known_color::green_yellow)); + } + + static color_type honeydew() { + return color_type(value_of(known_color::honeydew)); + } + + static color_type hot_pink() { + return color_type(value_of(known_color::hot_pink)); + } + + static color_type indian_red() { + return color_type(value_of(known_color::indian_red)); + } + + static color_type indigo() { + return color_type(value_of(known_color::indigo)); + } + + static color_type ivory() { + return color_type(value_of(known_color::ivory)); + } + + static color_type khaki() { + return color_type(value_of(known_color::khaki)); + } + + static color_type lavender() { + return color_type(value_of(known_color::lavender)); + } + + static color_type lavender_blush() { + return color_type(value_of(known_color::lavender_blush)); + } + + static color_type lawn_green() { + return color_type(value_of(known_color::lawn_green)); + } + + static color_type lemon_chiffon() { + return color_type(value_of(known_color::lemon_chiffon)); + } + + static color_type light_blue() { + return color_type(value_of(known_color::light_blue)); + } + + static color_type light_coral() { + return color_type(value_of(known_color::light_coral)); + } + + static color_type light_cyan() { + return color_type(value_of(known_color::light_cyan)); + } + + static color_type light_goldenrod_yellow() { + return color_type(value_of(known_color::light_goldenrod_yellow)); + } + + static color_type light_gray() { + return color_type(value_of(known_color::light_gray)); + } + + static color_type light_green() { + return color_type(value_of(known_color::light_green)); + } + + static color_type light_pink() { + return color_type(value_of(known_color::light_pink)); + } + + static color_type light_salmon() { + return color_type(value_of(known_color::light_salmon)); + } + + static color_type light_sea_green() { + return color_type(value_of(known_color::light_sea_green)); + } + + static color_type light_sky_blue() { + return color_type(value_of(known_color::light_sky_blue)); + } + + static color_type light_slate_gray() { + return color_type(value_of(known_color::light_slate_gray)); + } + + static color_type light_steel_blue() { + return color_type(value_of(known_color::light_steel_blue)); + } + + static color_type light_yellow() { + return color_type(value_of(known_color::light_yellow)); + } + + static color_type lime() { + return color_type(value_of(known_color::lime)); + } + + static color_type lime_green() { + return color_type(value_of(known_color::lime_green)); + } + + static color_type linen() { + return color_type(value_of(known_color::linen)); + } + + static color_type magenta() { + return color_type(value_of(known_color::magenta)); + } + + static color_type maroon() { + return color_type(value_of(known_color::maroon)); + } + + static color_type medium_aquamarine() { + return color_type(value_of(known_color::medium_aquamarine)); + } + + static color_type medium_blue() { + return color_type(value_of(known_color::medium_blue)); + } + + static color_type medium_orchid() { + return color_type(value_of(known_color::medium_orchid)); + } + + static color_type medium_purple() { + return color_type(value_of(known_color::medium_purple)); + } + + static color_type medium_sea_green() { + return color_type(value_of(known_color::medium_sea_green)); + } + + static color_type medium_slate_blue() { + return color_type(value_of(known_color::medium_slate_blue)); + } + + static color_type medium_spring_green() { + return color_type(value_of(known_color::medium_spring_green)); + } + + static color_type medium_turquoise() { + return color_type(value_of(known_color::medium_turquoise)); + } + + static color_type medium_violet_red() { + return color_type(value_of(known_color::medium_violet_red)); + } + + static color_type midnight_blue() { + return color_type(value_of(known_color::midnight_blue)); + } + + static color_type mint_cream() { + return color_type(value_of(known_color::mint_cream)); + } + + static color_type misty_rose() { + return color_type(value_of(known_color::misty_rose)); + } + + static color_type moccasin() { + return color_type(value_of(known_color::moccasin)); + } + + static color_type navajo_white() { + return color_type(value_of(known_color::navajo_white)); + } + + static color_type navy() { + return color_type(value_of(known_color::navy)); + } + + static color_type old_lace() { + return color_type(value_of(known_color::old_lace)); + } + + static color_type olive() { + return color_type(value_of(known_color::olive)); + } + + static color_type olive_drab() { + return color_type(value_of(known_color::olive_drab)); + } + + static color_type orange() { + return color_type(value_of(known_color::orange)); + } + + static color_type orange_red() { + return color_type(value_of(known_color::orange_red)); + } + + static color_type orchid() { + return color_type(value_of(known_color::orchid)); + } + + static color_type pale_goldenrod() { + return color_type(value_of(known_color::pale_goldenrod)); + } + + static color_type pale_green() { + return color_type(value_of(known_color::pale_green)); + } + + static color_type pale_turquoise() { + return color_type(value_of(known_color::pale_turquoise)); + } + + static color_type pale_violet_red() { + return color_type(value_of(known_color::pale_violet_red)); + } + + static color_type papaya_whip() { + return color_type(value_of(known_color::papaya_whip)); + } + + static color_type peach_puff() { + return color_type(value_of(known_color::peach_puff)); + } + + static color_type peru() { + return color_type(value_of(known_color::peru)); + } + + static color_type pink() { + return color_type(value_of(known_color::pink)); + } + + static color_type plum() { + return color_type(value_of(known_color::plum)); + } + + static color_type powder_blue() { + return color_type(value_of(known_color::powder_blue)); + } + + static color_type purple() { + return color_type(value_of(known_color::purple)); + } + + static color_type red() { + return color_type(value_of(known_color::red)); + } + + static color_type rosy_brown() { + return color_type(value_of(known_color::rosy_brown)); + } + + static color_type royal_blue() { + return color_type(value_of(known_color::royal_blue)); + } + + static color_type saddle_brown() { + return color_type(value_of(known_color::saddle_brown)); + } + + static color_type salmon() { + return color_type(value_of(known_color::salmon)); + } + + static color_type sandy_brown() { + return color_type(value_of(known_color::sandy_brown)); + } + + static color_type sea_green() { + return color_type(value_of(known_color::sea_green)); + } + + static color_type sea_shell() { + return color_type(value_of(known_color::sea_shell)); + } + + static color_type sienna() { + return color_type(value_of(known_color::sienna)); + } + + static color_type silver() { + return color_type(value_of(known_color::silver)); + } + + static color_type sky_blue() { + return color_type(value_of(known_color::sky_blue)); + } + + static color_type slate_blue() { + return color_type(value_of(known_color::slate_blue)); + } + + static color_type slate_gray() { + return color_type(value_of(known_color::slate_gray)); + } + + static color_type snow() { + return color_type(value_of(known_color::snow)); + } + + static color_type spring_green() { + return color_type(value_of(known_color::spring_green)); + } + + static color_type steel_blue() { + return color_type(value_of(known_color::steel_blue)); + } + + static color_type tan() { + return color_type(value_of(known_color::tan)); + } + + static color_type teal() { + return color_type(value_of(known_color::teal)); + } + + static color_type thistle() { + return color_type(value_of(known_color::thistle)); + } + + static color_type tomato() { + return color_type(value_of(known_color::tomato)); + } + + static color_type transparent() { + return color_type(value_of(known_color::transparent)); + } + + static color_type turquoise() { + return color_type(value_of(known_color::turquoise)); + } + + static color_type violet() { + return color_type(value_of(known_color::violet)); + } + + static color_type wheat() { + return color_type(value_of(known_color::wheat)); + } + + static color_type white() { + return color_type(value_of(known_color::white)); + } + + static color_type white_smoke() { + return color_type(value_of(known_color::white_smoke)); + } + + static color_type yellow() { + return color_type(value_of(known_color::yellow)); + } + + static color_type yellow_green() { + return color_type(value_of(known_color::yellow_green)); + } + }; + + } // namespace dotnet +} // namespace colors diff --git a/extern/include/cpp-colors/impl/dotnet/dotnet_constants.h b/extern/include/cpp-colors/impl/dotnet/dotnet_constants.h new file mode 100644 index 000000000..bc11271cb --- /dev/null +++ b/extern/include/cpp-colors/impl/dotnet/dotnet_constants.h @@ -0,0 +1,153 @@ +#pragma once + +#include + +namespace colors { + namespace dotnet { + + enum class known_color : uint32_t { + alice_blue = 0xFFF0F8FF, // Alice Blue: argb(255, 240, 248, 255) + antique_white = 0xFFFAEBD7, // Antique White: argb(255, 250, 235, 215) + aqua = 0xFF00FFFF, // Aqua: argb(255, 0, 255, 255) + aquamarine = 0xFF7FFFD4, // Aquamarine: argb(255, 127, 255, 212) + azure = 0xFFF0FFFF, // Azure: argb(255, 240, 255, 255) + beige = 0xFFF5F5DC, // Beige: argb(255, 245, 245, 220) + bisque = 0xFFFFE4C4, // Bisque: argb(255, 255, 228, 196) + black = 0xFF000000, // Black: argb(255, 0, 0, 0) + blanched_almond = 0xFFFFEBCD, // Blanched Almond: argb(255, 255, 235, 205) + blue = 0xFF0000FF, // Blue: argb(255, 0, 0, 255) + blue_violet = 0xFF8A2BE2, // Blue Violet: argb(255, 138, 43, 226) + brown = 0xFFA52A2A, // Brown: argb(255, 165, 42, 42) + burly_wood = 0xFFDEB887, // Burly Wood: argb(255, 222, 184, 135) + cadet_blue = 0xFF5F9EA0, // Cadet Blue: argb(255, 95, 158, 160) + chartreuse = 0xFF7FFF00, // Chartreuse: argb(255, 127, 255, 0) + chocolate = 0xFFD2691E, // Chocolate: argb(255, 210, 105, 30) + coral = 0xFFFF7F50, // Coral: argb(255, 255, 127, 80) + cornflower_blue = 0xFF6495ED, // Cornflower Blue: argb(255, 100, 149, 237) + cornsilk = 0xFFFFF8DC, // Cornsilk: argb(255, 255, 248, 220) + crimson = 0xFFDC143C, // Crimson: argb(255, 220, 20, 60) + cyan = 0xFF00FFFF, // Cyan: argb(255, 0, 255, 255) + dark_blue = 0xFF00008B, // Dark Blue: argb(255, 0, 0, 139) + dark_cyan = 0xFF008B8B, // Dark Cyan: argb(255, 0, 139, 139) + dark_goldenrod = 0xFFB8860B, // Dark Goldenrod: argb(255, 184, 134, 11) + dark_gray = 0xFFA9A9A9, // Dark Gray: argb(255, 169, 169, 169) + dark_green = 0xFF006400, // Dark Green: argb(255, 0, 100, 0) + dark_khaki = 0xFFBDB76B, // Dark Khaki: argb(255, 189, 183, 107) + dark_magenta = 0xFF8B008B, // Dark Magenta: argb(255, 139, 0, 139) + dark_olive_green = 0xFF556B2F, // Dark Olive Green: argb(255, 85, 107, 47) + dark_orange = 0xFFFF8C00, // Dark Orange: argb(255, 255, 140, 0) + dark_orchid = 0xFF9932CC, // Dark Orchid: argb(255, 153, 50, 204) + dark_red = 0xFF8B0000, // Dark Red: argb(255, 139, 0, 0) + dark_salmon = 0xFFE9967A, // Dark Salmon: argb(255, 233, 150, 122) + dark_sea_green = 0xFF8FBC8F, // Dark Sea Green: argb(255, 143, 188, 143) + dark_slate_blue = 0xFF483D8B, // Dark Slate Blue: argb(255, 72, 61, 139) + dark_slate_gray = 0xFF2F4F4F, // Dark Slate Gray: argb(255, 47, 79, 79) + dark_turquoise = 0xFF00CED1, // Dark Turquoise: argb(255, 0, 206, 209) + dark_violet = 0xFF9400D3, // Dark Violet: argb(255, 148, 0, 211) + deep_pink = 0xFFFF1493, // Deep Pink: argb(255, 255, 20, 147) + deep_sky_blue = 0xFF00BFFF, // Deep Sky Blue: argb(255, 0, 191, 255) + dim_gray = 0xFF696969, // Dim Gray: argb(255, 105, 105, 105) + dodger_blue = 0xFF1E90FF, // Dodger Blue: argb(255, 30, 144, 255) + firebrick = 0xFFB22222, // Firebrick: argb(255, 178, 34, 34) + floral_white = 0xFFFFFAF0, // Floral White: argb(255, 255, 250, 240) + forest_green = 0xFF228B22, // Forest Green: argb(255, 34, 139, 34) + fuchsia = 0xFFFF00FF, // Fuchsia: argb(255, 255, 0, 255) + gainsboro = 0xFFDCDCDC, // Gainsboro: argb(255, 220, 220, 220) + ghost_white = 0xFFF8F8FF, // Ghost White: argb(255, 248, 248, 255) + gold = 0xFFFFD700, // Gold: argb(255, 255, 215, 0) + goldenrod = 0xFFDAA520, // Goldenrod: argb(255, 218, 165, 32) + gray = 0xFF808080, // Gray: argb(255, 128, 128, 128) + green = 0xFF008000, // Green: argb(255, 0, 128, 0) + green_yellow = 0xFFADFF2F, // Green Yellow: argb(255, 173, 255, 47) + honeydew = 0xFFF0FFF0, // Honeydew: argb(255, 240, 255, 240) + hot_pink = 0xFFFF69B4, // Hot Pink: argb(255, 255, 105, 180) + indian_red = 0xFFCD5C5C, // Indian Red: argb(255, 205, 92, 92) + indigo = 0xFF4B0082, // Indigo: argb(255, 75, 0, 130) + ivory = 0xFFFFFFF0, // Ivory: argb(255, 255, 255, 240) + khaki = 0xFFF0E68C, // Khaki: argb(255, 240, 230, 140) + lavender = 0xFFE6E6FA, // Lavender: argb(255, 230, 230, 250) + lavender_blush = 0xFFFFF0F5, // Lavender Blush: argb(255, 255, 240, 245) + lawn_green = 0xFF7CFC00, // Lawn Green: argb(255, 124, 252, 0) + lemon_chiffon = 0xFFFFFACD, // Lemon Chiffon: argb(255, 255, 250, 205) + light_blue = 0xFFADD8E6, // Light Blue: argb(255, 173, 216, 230) + light_coral = 0xFFF08080, // Light Coral: argb(255, 240, 128, 128) + light_cyan = 0xFFE0FFFF, // Light Cyan: argb(255, 224, 255, 255) + light_goldenrod_yellow = 0xFFFAFAD2, // Light Goldenrod Yellow: argb(255, 250, 250, 210) + light_gray = 0xFFD3D3D3, // Light Gray: argb(255, 211, 211, 211) + light_green = 0xFF90EE90, // Light Green: argb(255, 144, 238, 144) + light_pink = 0xFFFFB6C1, // Light Pink: argb(255, 255, 182, 193) + light_salmon = 0xFFFFA07A, // Light Salmon: argb(255, 255, 160, 122) + light_sea_green = 0xFF20B2AA, // Light Sea Green: argb(255, 32, 178, 170) + light_sky_blue = 0xFF87CEFA, // Light Sky Blue: argb(255, 135, 206, 250) + light_slate_gray = 0xFF778899, // Light Slate Gray: argb(255, 119, 136, 153) + light_steel_blue = 0xFFB0C4DE, // Light Steel Blue: argb(255, 176, 196, 222) + light_yellow = 0xFFFFFFE0, // Light Yellow: argb(255, 255, 255, 224) + lime = 0xFF00FF00, // Lime: argb(255, 0, 255, 0) + lime_green = 0xFF32CD32, // Lime Green: argb(255, 50, 205, 50) + linen = 0xFFFAF0E6, // Linen: argb(255, 250, 240, 230) + magenta = 0xFFFF00FF, // Magenta: argb(255, 255, 0, 255) + maroon = 0xFF800000, // Maroon: argb(255, 128, 0, 0) + medium_aquamarine = 0xFF66CDAA, // Medium Aquamarine: argb(255, 102, 205, 170) + medium_blue = 0xFF0000CD, // Medium Blue: argb(255, 0, 0, 205) + medium_orchid = 0xFFBA55D3, // Medium Orchid: argb(255, 186, 85, 211) + medium_purple = 0xFF9370DB, // Medium Purple: argb(255, 147, 112, 219) + medium_sea_green = 0xFF3CB371, // Medium Sea Green: argb(255, 60, 179, 113) + medium_slate_blue = 0xFF7B68EE, // Medium Slate Blue: argb(255, 123, 104, 238) + medium_spring_green = 0xFF00FA9A, // Medium Spring Green: argb(255, 0, 250, 154) + medium_turquoise = 0xFF48D1CC, // Medium Turquoise: argb(255, 72, 209, 204) + medium_violet_red = 0xFFC71585, // Medium Violet Red: argb(255, 199, 21, 133) + midnight_blue = 0xFF191970, // Midnight Blue: argb(255, 25, 25, 112) + mint_cream = 0xFFF5FFFA, // Mint Cream: argb(255, 245, 255, 250) + misty_rose = 0xFFFFE4E1, // Misty Rose: argb(255, 255, 228, 225) + moccasin = 0xFFFFE4B5, // Moccasin: argb(255, 255, 228, 181) + navajo_white = 0xFFFFDEAD, // Navajo White: argb(255, 255, 222, 173) + navy = 0xFF000080, // Navy: argb(255, 0, 0, 128) + old_lace = 0xFFFDF5E6, // Old Lace: argb(255, 253, 245, 230) + olive = 0xFF808000, // Olive: argb(255, 128, 128, 0) + olive_drab = 0xFF6B8E23, // Olive Drab: argb(255, 107, 142, 35) + orange = 0xFFFFA500, // Orange: argb(255, 255, 165, 0) + orange_red = 0xFFFF4500, // Orange Red: argb(255, 255, 69, 0) + orchid = 0xFFDA70D6, // Orchid: argb(255, 218, 112, 214) + pale_goldenrod = 0xFFEEE8AA, // Pale Goldenrod: argb(255, 238, 232, 170) + pale_green = 0xFF98FB98, // Pale Green: argb(255, 152, 251, 152) + pale_turquoise = 0xFFAFEEEE, // Pale Turquoise: argb(255, 175, 238, 238) + pale_violet_red = 0xFFDB7093, // Pale Violet Red: argb(255, 219, 112, 147) + papaya_whip = 0xFFFFEFD5, // Papaya Whip: argb(255, 255, 239, 213) + peach_puff = 0xFFFFDAB9, // Peach Puff: argb(255, 255, 218, 185) + peru = 0xFFCD853F, // Peru: argb(255, 205, 133, 63) + pink = 0xFFFFC0CB, // Pink: argb(255, 255, 192, 203) + plum = 0xFFDDA0DD, // Plum: argb(255, 221, 160, 221) + powder_blue = 0xFFB0E0E6, // Powder Blue: argb(255, 176, 224, 230) + purple = 0xFF800080, // Purple: argb(255, 128, 0, 128) + red = 0xFFFF0000, // Red: argb(255, 255, 0, 0) + rosy_brown = 0xFFBC8F8F, // Rosy Brown: argb(255, 188, 143, 143) + royal_blue = 0xFF4169E1, // Royal Blue: argb(255, 65, 105, 225) + saddle_brown = 0xFF8B4513, // Saddle Brown: argb(255, 139, 69, 19) + salmon = 0xFFFA8072, // Salmon: argb(255, 250, 128, 114) + sandy_brown = 0xFFF4A460, // Sandy Brown: argb(255, 244, 164, 96) + sea_green = 0xFF2E8B57, // Sea Green: argb(255, 46, 139, 87) + sea_shell = 0xFFFFF5EE, // Sea Shell: argb(255, 255, 245, 238) + sienna = 0xFFA0522D, // Sienna: argb(255, 160, 82, 45) + silver = 0xFFC0C0C0, // Silver: argb(255, 192, 192, 192) + sky_blue = 0xFF87CEEB, // Sky Blue: argb(255, 135, 206, 235) + slate_blue = 0xFF6A5ACD, // Slate Blue: argb(255, 106, 90, 205) + slate_gray = 0xFF708090, // Slate Gray: argb(255, 112, 128, 144) + snow = 0xFFFFFAFA, // Snow: argb(255, 255, 250, 250) + spring_green = 0xFF00FF7F, // Spring Green: argb(255, 0, 255, 127) + steel_blue = 0xFF4682B4, // Steel Blue: argb(255, 70, 130, 180) + tan = 0xFFD2B48C, // Tan: argb(255, 210, 180, 140) + teal = 0xFF008080, // Teal: argb(255, 0, 128, 128) + thistle = 0xFFD8BFD8, // Thistle: argb(255, 216, 191, 216) + tomato = 0xFFFF6347, // Tomato: argb(255, 255, 99, 71) + transparent = 0x00FFFFFF, // Transparent: argb(0, 255, 255, 255) + turquoise = 0xFF40E0D0, // Turquoise: argb(255, 64, 224, 208) + violet = 0xFFEE82EE, // Violet: argb(255, 238, 130, 238) + wheat = 0xFFF5DEB3, // Wheat: argb(255, 245, 222, 179) + white = 0xFFFFFFFF, // White: argb(255, 255, 255, 255) + white_smoke = 0xFFF5F5F5, // White Smoke: argb(255, 245, 245, 245) + yellow = 0xFFFFFF00, // Yellow: argb(255, 255, 255, 0) + yellow_green = 0xFF9ACD32, // Yellow Green: argb(255, 154, 205, 50) + }; + + } // namespace dotnet +} // namespace colors diff --git a/extern/include/cpp-colors/impl/dotnet/dotnet_named.h b/extern/include/cpp-colors/impl/dotnet/dotnet_named.h new file mode 100644 index 000000000..d2a31adf6 --- /dev/null +++ b/extern/include/cpp-colors/impl/dotnet/dotnet_named.h @@ -0,0 +1,1484 @@ +#pragma once + +#include +#include +#include +#include +#include "dotnet_constants.h" +#include "../named_utils.h" + + +namespace colors { + namespace dotnet { + + template + struct basic_named_color; + + template<> + struct basic_named_color { + static const char *const alice_blue() { + return "AliceBlue"; + } + + static const char *const antique_white() { + return "AntiqueWhite"; + } + + static const char *const aqua() { + return "Aqua"; + } + + static const char *const aquamarine() { + return "Aquamarine"; + } + + static const char *const azure() { + return "Azure"; + } + + static const char *const beige() { + return "Beige"; + } + + static const char *const bisque() { + return "Bisque"; + } + + static const char *const black() { + return "Black"; + } + + static const char *const blanched_almond() { + return "BlanchedAlmond"; + } + + static const char *const blue() { + return "Blue"; + } + + static const char *const blue_violet() { + return "BlueViolet"; + } + + static const char *const brown() { + return "Brown"; + } + + static const char *const burly_wood() { + return "BurlyWood"; + } + + static const char *const cadet_blue() { + return "CadetBlue"; + } + + static const char *const chartreuse() { + return "Chartreuse"; + } + + static const char *const chocolate() { + return "Chocolate"; + } + + static const char *const coral() { + return "Coral"; + } + + static const char *const cornflower_blue() { + return "CornflowerBlue"; + } + + static const char *const cornsilk() { + return "Cornsilk"; + } + + static const char *const crimson() { + return "Crimson"; + } + + static const char *const cyan() { + return "Cyan"; + } + + static const char *const dark_blue() { + return "DarkBlue"; + } + + static const char *const dark_cyan() { + return "DarkCyan"; + } + + static const char *const dark_goldenrod() { + return "DarkGoldenrod"; + } + + static const char *const dark_gray() { + return "DarkGray"; + } + + static const char *const dark_green() { + return "DarkGreen"; + } + + static const char *const dark_khaki() { + return "DarkKhaki"; + } + + static const char *const dark_magenta() { + return "DarkMagenta"; + } + + static const char *const dark_olive_green() { + return "DarkOliveGreen"; + } + + static const char *const dark_orange() { + return "DarkOrange"; + } + + static const char *const dark_orchid() { + return "DarkOrchid"; + } + + static const char *const dark_red() { + return "DarkRed"; + } + + static const char *const dark_salmon() { + return "DarkSalmon"; + } + + static const char *const dark_sea_green() { + return "DarkSeaGreen"; + } + + static const char *const dark_slate_blue() { + return "DarkSlateBlue"; + } + + static const char *const dark_slate_gray() { + return "DarkSlateGray"; + } + + static const char *const dark_turquoise() { + return "DarkTurquoise"; + } + + static const char *const dark_violet() { + return "DarkViolet"; + } + + static const char *const deep_pink() { + return "DeepPink"; + } + + static const char *const deep_sky_blue() { + return "DeepSkyBlue"; + } + + static const char *const dim_gray() { + return "DimGray"; + } + + static const char *const dodger_blue() { + return "DodgerBlue"; + } + + static const char *const firebrick() { + return "Firebrick"; + } + + static const char *const floral_white() { + return "FloralWhite"; + } + + static const char *const forest_green() { + return "ForestGreen"; + } + + static const char *const fuchsia() { + return "Fuchsia"; + } + + static const char *const gainsboro() { + return "Gainsboro"; + } + + static const char *const ghost_white() { + return "GhostWhite"; + } + + static const char *const gold() { + return "Gold"; + } + + static const char *const goldenrod() { + return "Goldenrod"; + } + + static const char *const gray() { + return "Gray"; + } + + static const char *const green() { + return "Green"; + } + + static const char *const green_yellow() { + return "GreenYellow"; + } + + static const char *const honeydew() { + return "Honeydew"; + } + + static const char *const hot_pink() { + return "HotPink"; + } + + static const char *const indian_red() { + return "IndianRed"; + } + + static const char *const indigo() { + return "Indigo"; + } + + static const char *const ivory() { + return "Ivory"; + } + + static const char *const khaki() { + return "Khaki"; + } + + static const char *const lavender() { + return "Lavender"; + } + + static const char *const lavender_blush() { + return "LavenderBlush"; + } + + static const char *const lawn_green() { + return "LawnGreen"; + } + + static const char *const lemon_chiffon() { + return "LemonChiffon"; + } + + static const char *const light_blue() { + return "LightBlue"; + } + + static const char *const light_coral() { + return "LightCoral"; + } + + static const char *const light_cyan() { + return "LightCyan"; + } + + static const char *const light_goldenrod_yellow() { + return "LightGoldenrodYellow"; + } + + static const char *const light_gray() { + return "LightGray"; + } + + static const char *const light_green() { + return "LightGreen"; + } + + static const char *const light_pink() { + return "LightPink"; + } + + static const char *const light_salmon() { + return "LightSalmon"; + } + + static const char *const light_sea_green() { + return "LightSeaGreen"; + } + + static const char *const light_sky_blue() { + return "LightSkyBlue"; + } + + static const char *const light_slate_gray() { + return "LightSlateGray"; + } + + static const char *const light_steel_blue() { + return "LightSteelBlue"; + } + + static const char *const light_yellow() { + return "LightYellow"; + } + + static const char *const lime() { + return "Lime"; + } + + static const char *const lime_green() { + return "LimeGreen"; + } + + static const char *const linen() { + return "Linen"; + } + + static const char *const magenta() { + return "Magenta"; + } + + static const char *const maroon() { + return "Maroon"; + } + + static const char *const medium_aquamarine() { + return "MediumAquamarine"; + } + + static const char *const medium_blue() { + return "MediumBlue"; + } + + static const char *const medium_orchid() { + return "MediumOrchid"; + } + + static const char *const medium_purple() { + return "MediumPurple"; + } + + static const char *const medium_sea_green() { + return "MediumSeaGreen"; + } + + static const char *const medium_slate_blue() { + return "MediumSlateBlue"; + } + + static const char *const medium_spring_green() { + return "MediumSpringGreen"; + } + + static const char *const medium_turquoise() { + return "MediumTurquoise"; + } + + static const char *const medium_violet_red() { + return "MediumVioletRed"; + } + + static const char *const midnight_blue() { + return "MidnightBlue"; + } + + static const char *const mint_cream() { + return "MintCream"; + } + + static const char *const misty_rose() { + return "MistyRose"; + } + + static const char *const moccasin() { + return "Moccasin"; + } + + static const char *const navajo_white() { + return "NavajoWhite"; + } + + static const char *const navy() { + return "Navy"; + } + + static const char *const old_lace() { + return "OldLace"; + } + + static const char *const olive() { + return "Olive"; + } + + static const char *const olive_drab() { + return "OliveDrab"; + } + + static const char *const orange() { + return "Orange"; + } + + static const char *const orange_red() { + return "OrangeRed"; + } + + static const char *const orchid() { + return "Orchid"; + } + + static const char *const pale_goldenrod() { + return "PaleGoldenrod"; + } + + static const char *const pale_green() { + return "PaleGreen"; + } + + static const char *const pale_turquoise() { + return "PaleTurquoise"; + } + + static const char *const pale_violet_red() { + return "PaleVioletRed"; + } + + static const char *const papaya_whip() { + return "PapayaWhip"; + } + + static const char *const peach_puff() { + return "PeachPuff"; + } + + static const char *const peru() { + return "Peru"; + } + + static const char *const pink() { + return "Pink"; + } + + static const char *const plum() { + return "Plum"; + } + + static const char *const powder_blue() { + return "PowderBlue"; + } + + static const char *const purple() { + return "Purple"; + } + + static const char *const red() { + return "Red"; + } + + static const char *const rosy_brown() { + return "RosyBrown"; + } + + static const char *const royal_blue() { + return "RoyalBlue"; + } + + static const char *const saddle_brown() { + return "SaddleBrown"; + } + + static const char *const salmon() { + return "Salmon"; + } + + static const char *const sandy_brown() { + return "SandyBrown"; + } + + static const char *const sea_green() { + return "SeaGreen"; + } + + static const char *const sea_shell() { + return "SeaShell"; + } + + static const char *const sienna() { + return "Sienna"; + } + + static const char *const silver() { + return "Silver"; + } + + static const char *const sky_blue() { + return "SkyBlue"; + } + + static const char *const slate_blue() { + return "SlateBlue"; + } + + static const char *const slate_gray() { + return "SlateGray"; + } + + static const char *const snow() { + return "Snow"; + } + + static const char *const spring_green() { + return "SpringGreen"; + } + + static const char *const steel_blue() { + return "SteelBlue"; + } + + static const char *const tan() { + return "Tan"; + } + + static const char *const teal() { + return "Teal"; + } + + static const char *const thistle() { + return "Thistle"; + } + + static const char *const tomato() { + return "Tomato"; + } + + static const char *const transparent() { + return "Transparent"; + } + + static const char *const turquoise() { + return "Turquoise"; + } + + static const char *const violet() { + return "Violet"; + } + + static const char *const wheat() { + return "Wheat"; + } + + static const char *const white() { + return "White"; + } + + static const char *const white_smoke() { + return "WhiteSmoke"; + } + + static const char *const yellow() { + return "Yellow"; + } + + static const char *const yellow_green() { + return "YellowGreen"; + } + }; + + template<> + struct basic_named_color { + static const wchar_t *const alice_blue() { + return L"AliceBlue"; + } + + static const wchar_t *const antique_white() { + return L"AntiqueWhite"; + } + + static const wchar_t *const aqua() { + return L"Aqua"; + } + + static const wchar_t *const aquamarine() { + return L"Aquamarine"; + } + + static const wchar_t *const azure() { + return L"Azure"; + } + + static const wchar_t *const beige() { + return L"Beige"; + } + + static const wchar_t *const bisque() { + return L"Bisque"; + } + + static const wchar_t *const black() { + return L"Black"; + } + + static const wchar_t *const blanched_almond() { + return L"BlanchedAlmond"; + } + + static const wchar_t *const blue() { + return L"Blue"; + } + + static const wchar_t *const blue_violet() { + return L"BlueViolet"; + } + + static const wchar_t *const brown() { + return L"Brown"; + } + + static const wchar_t *const burly_wood() { + return L"BurlyWood"; + } + + static const wchar_t *const cadet_blue() { + return L"CadetBlue"; + } + + static const wchar_t *const chartreuse() { + return L"Chartreuse"; + } + + static const wchar_t *const chocolate() { + return L"Chocolate"; + } + + static const wchar_t *const coral() { + return L"Coral"; + } + + static const wchar_t *const cornflower_blue() { + return L"CornflowerBlue"; + } + + static const wchar_t *const cornsilk() { + return L"Cornsilk"; + } + + static const wchar_t *const crimson() { + return L"Crimson"; + } + + static const wchar_t *const cyan() { + return L"Cyan"; + } + + static const wchar_t *const dark_blue() { + return L"DarkBlue"; + } + + static const wchar_t *const dark_cyan() { + return L"DarkCyan"; + } + + static const wchar_t *const dark_goldenrod() { + return L"DarkGoldenrod"; + } + + static const wchar_t *const dark_gray() { + return L"DarkGray"; + } + + static const wchar_t *const dark_green() { + return L"DarkGreen"; + } + + static const wchar_t *const dark_khaki() { + return L"DarkKhaki"; + } + + static const wchar_t *const dark_magenta() { + return L"DarkMagenta"; + } + + static const wchar_t *const dark_olive_green() { + return L"DarkOliveGreen"; + } + + static const wchar_t *const dark_orange() { + return L"DarkOrange"; + } + + static const wchar_t *const dark_orchid() { + return L"DarkOrchid"; + } + + static const wchar_t *const dark_red() { + return L"DarkRed"; + } + + static const wchar_t *const dark_salmon() { + return L"DarkSalmon"; + } + + static const wchar_t *const dark_sea_green() { + return L"DarkSeaGreen"; + } + + static const wchar_t *const dark_slate_blue() { + return L"DarkSlateBlue"; + } + + static const wchar_t *const dark_slate_gray() { + return L"DarkSlateGray"; + } + + static const wchar_t *const dark_turquoise() { + return L"DarkTurquoise"; + } + + static const wchar_t *const dark_violet() { + return L"DarkViolet"; + } + + static const wchar_t *const deep_pink() { + return L"DeepPink"; + } + + static const wchar_t *const deep_sky_blue() { + return L"DeepSkyBlue"; + } + + static const wchar_t *const dim_gray() { + return L"DimGray"; + } + + static const wchar_t *const dodger_blue() { + return L"DodgerBlue"; + } + + static const wchar_t *const firebrick() { + return L"Firebrick"; + } + + static const wchar_t *const floral_white() { + return L"FloralWhite"; + } + + static const wchar_t *const forest_green() { + return L"ForestGreen"; + } + + static const wchar_t *const fuchsia() { + return L"Fuchsia"; + } + + static const wchar_t *const gainsboro() { + return L"Gainsboro"; + } + + static const wchar_t *const ghost_white() { + return L"GhostWhite"; + } + + static const wchar_t *const gold() { + return L"Gold"; + } + + static const wchar_t *const goldenrod() { + return L"Goldenrod"; + } + + static const wchar_t *const gray() { + return L"Gray"; + } + + static const wchar_t *const green() { + return L"Green"; + } + + static const wchar_t *const green_yellow() { + return L"GreenYellow"; + } + + static const wchar_t *const honeydew() { + return L"Honeydew"; + } + + static const wchar_t *const hot_pink() { + return L"HotPink"; + } + + static const wchar_t *const indian_red() { + return L"IndianRed"; + } + + static const wchar_t *const indigo() { + return L"Indigo"; + } + + static const wchar_t *const ivory() { + return L"Ivory"; + } + + static const wchar_t *const khaki() { + return L"Khaki"; + } + + static const wchar_t *const lavender() { + return L"Lavender"; + } + + static const wchar_t *const lavender_blush() { + return L"LavenderBlush"; + } + + static const wchar_t *const lawn_green() { + return L"LawnGreen"; + } + + static const wchar_t *const lemon_chiffon() { + return L"LemonChiffon"; + } + + static const wchar_t *const light_blue() { + return L"LightBlue"; + } + + static const wchar_t *const light_coral() { + return L"LightCoral"; + } + + static const wchar_t *const light_cyan() { + return L"LightCyan"; + } + + static const wchar_t *const light_goldenrod_yellow() { + return L"LightGoldenrodYellow"; + } + + static const wchar_t *const light_gray() { + return L"LightGray"; + } + + static const wchar_t *const light_green() { + return L"LightGreen"; + } + + static const wchar_t *const light_pink() { + return L"LightPink"; + } + + static const wchar_t *const light_salmon() { + return L"LightSalmon"; + } + + static const wchar_t *const light_sea_green() { + return L"LightSeaGreen"; + } + + static const wchar_t *const light_sky_blue() { + return L"LightSkyBlue"; + } + + static const wchar_t *const light_slate_gray() { + return L"LightSlateGray"; + } + + static const wchar_t *const light_steel_blue() { + return L"LightSteelBlue"; + } + + static const wchar_t *const light_yellow() { + return L"LightYellow"; + } + + static const wchar_t *const lime() { + return L"Lime"; + } + + static const wchar_t *const lime_green() { + return L"LimeGreen"; + } + + static const wchar_t *const linen() { + return L"Linen"; + } + + static const wchar_t *const magenta() { + return L"Magenta"; + } + + static const wchar_t *const maroon() { + return L"Maroon"; + } + + static const wchar_t *const medium_aquamarine() { + return L"MediumAquamarine"; + } + + static const wchar_t *const medium_blue() { + return L"MediumBlue"; + } + + static const wchar_t *const medium_orchid() { + return L"MediumOrchid"; + } + + static const wchar_t *const medium_purple() { + return L"MediumPurple"; + } + + static const wchar_t *const medium_sea_green() { + return L"MediumSeaGreen"; + } + + static const wchar_t *const medium_slate_blue() { + return L"MediumSlateBlue"; + } + + static const wchar_t *const medium_spring_green() { + return L"MediumSpringGreen"; + } + + static const wchar_t *const medium_turquoise() { + return L"MediumTurquoise"; + } + + static const wchar_t *const medium_violet_red() { + return L"MediumVioletRed"; + } + + static const wchar_t *const midnight_blue() { + return L"MidnightBlue"; + } + + static const wchar_t *const mint_cream() { + return L"MintCream"; + } + + static const wchar_t *const misty_rose() { + return L"MistyRose"; + } + + static const wchar_t *const moccasin() { + return L"Moccasin"; + } + + static const wchar_t *const navajo_white() { + return L"NavajoWhite"; + } + + static const wchar_t *const navy() { + return L"Navy"; + } + + static const wchar_t *const old_lace() { + return L"OldLace"; + } + + static const wchar_t *const olive() { + return L"Olive"; + } + + static const wchar_t *const olive_drab() { + return L"OliveDrab"; + } + + static const wchar_t *const orange() { + return L"Orange"; + } + + static const wchar_t *const orange_red() { + return L"OrangeRed"; + } + + static const wchar_t *const orchid() { + return L"Orchid"; + } + + static const wchar_t *const pale_goldenrod() { + return L"PaleGoldenrod"; + } + + static const wchar_t *const pale_green() { + return L"PaleGreen"; + } + + static const wchar_t *const pale_turquoise() { + return L"PaleTurquoise"; + } + + static const wchar_t *const pale_violet_red() { + return L"PaleVioletRed"; + } + + static const wchar_t *const papaya_whip() { + return L"PapayaWhip"; + } + + static const wchar_t *const peach_puff() { + return L"PeachPuff"; + } + + static const wchar_t *const peru() { + return L"Peru"; + } + + static const wchar_t *const pink() { + return L"Pink"; + } + + static const wchar_t *const plum() { + return L"Plum"; + } + + static const wchar_t *const powder_blue() { + return L"PowderBlue"; + } + + static const wchar_t *const purple() { + return L"Purple"; + } + + static const wchar_t *const red() { + return L"Red"; + } + + static const wchar_t *const rosy_brown() { + return L"RosyBrown"; + } + + static const wchar_t *const royal_blue() { + return L"RoyalBlue"; + } + + static const wchar_t *const saddle_brown() { + return L"SaddleBrown"; + } + + static const wchar_t *const salmon() { + return L"Salmon"; + } + + static const wchar_t *const sandy_brown() { + return L"SandyBrown"; + } + + static const wchar_t *const sea_green() { + return L"SeaGreen"; + } + + static const wchar_t *const sea_shell() { + return L"SeaShell"; + } + + static const wchar_t *const sienna() { + return L"Sienna"; + } + + static const wchar_t *const silver() { + return L"Silver"; + } + + static const wchar_t *const sky_blue() { + return L"SkyBlue"; + } + + static const wchar_t *const slate_blue() { + return L"SlateBlue"; + } + + static const wchar_t *const slate_gray() { + return L"SlateGray"; + } + + static const wchar_t *const snow() { + return L"Snow"; + } + + static const wchar_t *const spring_green() { + return L"SpringGreen"; + } + + static const wchar_t *const steel_blue() { + return L"SteelBlue"; + } + + static const wchar_t *const tan() { + return L"Tan"; + } + + static const wchar_t *const teal() { + return L"Teal"; + } + + static const wchar_t *const thistle() { + return L"Thistle"; + } + + static const wchar_t *const tomato() { + return L"Tomato"; + } + + static const wchar_t *const transparent() { + return L"Transparent"; + } + + static const wchar_t *const turquoise() { + return L"Turquoise"; + } + + static const wchar_t *const violet() { + return L"Violet"; + } + + static const wchar_t *const wheat() { + return L"Wheat"; + } + + static const wchar_t *const white() { + return L"White"; + } + + static const wchar_t *const white_smoke() { + return L"WhiteSmoke"; + } + + static const wchar_t *const yellow() { + return L"Yellow"; + } + + static const wchar_t *const yellow_green() { + return L"YellowGreen"; + } + }; + + template + struct basic_color_mapper { + typedef CharT char_type; + typedef std::basic_string string_type; + + typedef known_color known_color_type; + typedef basic_named_color named_color_type; + + typedef std::map string_color_map; + typedef std::map color_string_map; + + // Build a name -> constant map + static const string_color_map &get_string_to_color_map() { + static string_color_map sm_; // name -> value + if (sm_.empty()) { + sm_.insert(std::make_pair(to_lowercase(named_color_type::alice_blue()), known_color::alice_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::antique_white()), known_color::antique_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::aqua()), known_color::aqua)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::aquamarine()), known_color::aquamarine)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::azure()), known_color::azure)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::beige()), known_color::beige)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::bisque()), known_color::bisque)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::black()), known_color::black)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::blanched_almond()), known_color::blanched_almond)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::blue()), known_color::blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::blue_violet()), known_color::blue_violet)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::brown()), known_color::brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::burly_wood()), known_color::burly_wood)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cadet_blue()), known_color::cadet_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::chartreuse()), known_color::chartreuse)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::chocolate()), known_color::chocolate)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::coral()), known_color::coral)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cornflower_blue()), known_color::cornflower_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cornsilk()), known_color::cornsilk)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::crimson()), known_color::crimson)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cyan()), known_color::cyan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_blue()), known_color::dark_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_cyan()), known_color::dark_cyan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_goldenrod()), known_color::dark_goldenrod)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_gray()), known_color::dark_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_green()), known_color::dark_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_khaki()), known_color::dark_khaki)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_magenta()), known_color::dark_magenta)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_olive_green()), known_color::dark_olive_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_orange()), known_color::dark_orange)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_orchid()), known_color::dark_orchid)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_red()), known_color::dark_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_salmon()), known_color::dark_salmon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_sea_green()), known_color::dark_sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_slate_blue()), known_color::dark_slate_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_slate_gray()), known_color::dark_slate_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_turquoise()), known_color::dark_turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_violet()), known_color::dark_violet)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::deep_pink()), known_color::deep_pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::deep_sky_blue()), known_color::deep_sky_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dim_gray()), known_color::dim_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dodger_blue()), known_color::dodger_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::firebrick()), known_color::firebrick)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::floral_white()), known_color::floral_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::forest_green()), known_color::forest_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::fuchsia()), known_color::fuchsia)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::gainsboro()), known_color::gainsboro)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::ghost_white()), known_color::ghost_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::gold()), known_color::gold)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::goldenrod()), known_color::goldenrod)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::gray()), known_color::gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::green()), known_color::green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::green_yellow()), known_color::green_yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::honeydew()), known_color::honeydew)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::hot_pink()), known_color::hot_pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::indian_red()), known_color::indian_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::indigo()), known_color::indigo)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::ivory()), known_color::ivory)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::khaki()), known_color::khaki)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lavender()), known_color::lavender)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lavender_blush()), known_color::lavender_blush)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lawn_green()), known_color::lawn_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lemon_chiffon()), known_color::lemon_chiffon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_blue()), known_color::light_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_coral()), known_color::light_coral)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_cyan()), known_color::light_cyan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_goldenrod_yellow()), known_color::light_goldenrod_yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_gray()), known_color::light_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_green()), known_color::light_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_pink()), known_color::light_pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_salmon()), known_color::light_salmon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_sea_green()), known_color::light_sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_sky_blue()), known_color::light_sky_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_slate_gray()), known_color::light_slate_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_steel_blue()), known_color::light_steel_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_yellow()), known_color::light_yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lime()), known_color::lime)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lime_green()), known_color::lime_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::linen()), known_color::linen)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::magenta()), known_color::magenta)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::maroon()), known_color::maroon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_aquamarine()), known_color::medium_aquamarine)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_blue()), known_color::medium_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_orchid()), known_color::medium_orchid)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_purple()), known_color::medium_purple)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_sea_green()), known_color::medium_sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_slate_blue()), known_color::medium_slate_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_spring_green()), known_color::medium_spring_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_turquoise()), known_color::medium_turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_violet_red()), known_color::medium_violet_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::midnight_blue()), known_color::midnight_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::mint_cream()), known_color::mint_cream)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::misty_rose()), known_color::misty_rose)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::moccasin()), known_color::moccasin)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::navajo_white()), known_color::navajo_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::navy()), known_color::navy)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::old_lace()), known_color::old_lace)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::olive()), known_color::olive)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::olive_drab()), known_color::olive_drab)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::orange()), known_color::orange)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::orange_red()), known_color::orange_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::orchid()), known_color::orchid)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_goldenrod()), known_color::pale_goldenrod)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_green()), known_color::pale_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_turquoise()), known_color::pale_turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_violet_red()), known_color::pale_violet_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::papaya_whip()), known_color::papaya_whip)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::peach_puff()), known_color::peach_puff)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::peru()), known_color::peru)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pink()), known_color::pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::plum()), known_color::plum)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::powder_blue()), known_color::powder_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::purple()), known_color::purple)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::red()), known_color::red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::rosy_brown()), known_color::rosy_brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::royal_blue()), known_color::royal_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::saddle_brown()), known_color::saddle_brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::salmon()), known_color::salmon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sandy_brown()), known_color::sandy_brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sea_green()), known_color::sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sea_shell()), known_color::sea_shell)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sienna()), known_color::sienna)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::silver()), known_color::silver)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sky_blue()), known_color::sky_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::slate_blue()), known_color::slate_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::slate_gray()), known_color::slate_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::snow()), known_color::snow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::spring_green()), known_color::spring_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::steel_blue()), known_color::steel_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::tan()), known_color::tan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::teal()), known_color::teal)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::thistle()), known_color::thistle)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::tomato()), known_color::tomato)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::transparent()), known_color::transparent)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::turquoise()), known_color::turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::violet()), known_color::violet)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::wheat()), known_color::wheat)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::white()), known_color::white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::white_smoke()), known_color::white_smoke)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::yellow()), known_color::yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::yellow_green()), known_color::yellow_green)); + } + + return sm_; + } + + // Build a name -> constant map + static const color_string_map &get_color_to_string_map() { + static color_string_map vm_; // value -> name + + if (vm_.empty()) { + vm_.insert(std::make_pair(value_of(known_color::alice_blue), named_color_type::alice_blue())); + vm_.insert(std::make_pair(value_of(known_color::antique_white), named_color_type::antique_white())); + vm_.insert(std::make_pair(value_of(known_color::aqua), named_color_type::aqua())); + vm_.insert(std::make_pair(value_of(known_color::aquamarine), named_color_type::aquamarine())); + vm_.insert(std::make_pair(value_of(known_color::azure), named_color_type::azure())); + vm_.insert(std::make_pair(value_of(known_color::beige), named_color_type::beige())); + vm_.insert(std::make_pair(value_of(known_color::bisque), named_color_type::bisque())); + vm_.insert(std::make_pair(value_of(known_color::black), named_color_type::black())); + vm_.insert(std::make_pair(value_of(known_color::blanched_almond), named_color_type::blanched_almond())); + vm_.insert(std::make_pair(value_of(known_color::blue), named_color_type::blue())); + vm_.insert(std::make_pair(value_of(known_color::blue_violet), named_color_type::blue_violet())); + vm_.insert(std::make_pair(value_of(known_color::brown), named_color_type::brown())); + vm_.insert(std::make_pair(value_of(known_color::burly_wood), named_color_type::burly_wood())); + vm_.insert(std::make_pair(value_of(known_color::cadet_blue), named_color_type::cadet_blue())); + vm_.insert(std::make_pair(value_of(known_color::chartreuse), named_color_type::chartreuse())); + vm_.insert(std::make_pair(value_of(known_color::chocolate), named_color_type::chocolate())); + vm_.insert(std::make_pair(value_of(known_color::coral), named_color_type::coral())); + vm_.insert(std::make_pair(value_of(known_color::cornflower_blue), named_color_type::cornflower_blue())); + vm_.insert(std::make_pair(value_of(known_color::cornsilk), named_color_type::cornsilk())); + vm_.insert(std::make_pair(value_of(known_color::crimson), named_color_type::crimson())); + vm_.insert(std::make_pair(value_of(known_color::cyan), named_color_type::cyan())); + vm_.insert(std::make_pair(value_of(known_color::dark_blue), named_color_type::dark_blue())); + vm_.insert(std::make_pair(value_of(known_color::dark_cyan), named_color_type::dark_cyan())); + vm_.insert(std::make_pair(value_of(known_color::dark_goldenrod), named_color_type::dark_goldenrod())); + vm_.insert(std::make_pair(value_of(known_color::dark_gray), named_color_type::dark_gray())); + vm_.insert(std::make_pair(value_of(known_color::dark_green), named_color_type::dark_green())); + vm_.insert(std::make_pair(value_of(known_color::dark_khaki), named_color_type::dark_khaki())); + vm_.insert(std::make_pair(value_of(known_color::dark_magenta), named_color_type::dark_magenta())); + vm_.insert(std::make_pair(value_of(known_color::dark_olive_green), named_color_type::dark_olive_green())); + vm_.insert(std::make_pair(value_of(known_color::dark_orange), named_color_type::dark_orange())); + vm_.insert(std::make_pair(value_of(known_color::dark_orchid), named_color_type::dark_orchid())); + vm_.insert(std::make_pair(value_of(known_color::dark_red), named_color_type::dark_red())); + vm_.insert(std::make_pair(value_of(known_color::dark_salmon), named_color_type::dark_salmon())); + vm_.insert(std::make_pair(value_of(known_color::dark_sea_green), named_color_type::dark_sea_green())); + vm_.insert(std::make_pair(value_of(known_color::dark_slate_blue), named_color_type::dark_slate_blue())); + vm_.insert(std::make_pair(value_of(known_color::dark_slate_gray), named_color_type::dark_slate_gray())); + vm_.insert(std::make_pair(value_of(known_color::dark_turquoise), named_color_type::dark_turquoise())); + vm_.insert(std::make_pair(value_of(known_color::dark_violet), named_color_type::dark_violet())); + vm_.insert(std::make_pair(value_of(known_color::deep_pink), named_color_type::deep_pink())); + vm_.insert(std::make_pair(value_of(known_color::deep_sky_blue), named_color_type::deep_sky_blue())); + vm_.insert(std::make_pair(value_of(known_color::dim_gray), named_color_type::dim_gray())); + vm_.insert(std::make_pair(value_of(known_color::dodger_blue), named_color_type::dodger_blue())); + vm_.insert(std::make_pair(value_of(known_color::firebrick), named_color_type::firebrick())); + vm_.insert(std::make_pair(value_of(known_color::floral_white), named_color_type::floral_white())); + vm_.insert(std::make_pair(value_of(known_color::forest_green), named_color_type::forest_green())); + vm_.insert(std::make_pair(value_of(known_color::fuchsia), named_color_type::fuchsia())); + vm_.insert(std::make_pair(value_of(known_color::gainsboro), named_color_type::gainsboro())); + vm_.insert(std::make_pair(value_of(known_color::ghost_white), named_color_type::ghost_white())); + vm_.insert(std::make_pair(value_of(known_color::gold), named_color_type::gold())); + vm_.insert(std::make_pair(value_of(known_color::goldenrod), named_color_type::goldenrod())); + vm_.insert(std::make_pair(value_of(known_color::gray), named_color_type::gray())); + vm_.insert(std::make_pair(value_of(known_color::green), named_color_type::green())); + vm_.insert(std::make_pair(value_of(known_color::green_yellow), named_color_type::green_yellow())); + vm_.insert(std::make_pair(value_of(known_color::honeydew), named_color_type::honeydew())); + vm_.insert(std::make_pair(value_of(known_color::hot_pink), named_color_type::hot_pink())); + vm_.insert(std::make_pair(value_of(known_color::indian_red), named_color_type::indian_red())); + vm_.insert(std::make_pair(value_of(known_color::indigo), named_color_type::indigo())); + vm_.insert(std::make_pair(value_of(known_color::ivory), named_color_type::ivory())); + vm_.insert(std::make_pair(value_of(known_color::khaki), named_color_type::khaki())); + vm_.insert(std::make_pair(value_of(known_color::lavender), named_color_type::lavender())); + vm_.insert(std::make_pair(value_of(known_color::lavender_blush), named_color_type::lavender_blush())); + vm_.insert(std::make_pair(value_of(known_color::lawn_green), named_color_type::lawn_green())); + vm_.insert(std::make_pair(value_of(known_color::lemon_chiffon), named_color_type::lemon_chiffon())); + vm_.insert(std::make_pair(value_of(known_color::light_blue), named_color_type::light_blue())); + vm_.insert(std::make_pair(value_of(known_color::light_coral), named_color_type::light_coral())); + vm_.insert(std::make_pair(value_of(known_color::light_cyan), named_color_type::light_cyan())); + vm_.insert(std::make_pair(value_of(known_color::light_goldenrod_yellow), named_color_type::light_goldenrod_yellow())); + vm_.insert(std::make_pair(value_of(known_color::light_gray), named_color_type::light_gray())); + vm_.insert(std::make_pair(value_of(known_color::light_green), named_color_type::light_green())); + vm_.insert(std::make_pair(value_of(known_color::light_pink), named_color_type::light_pink())); + vm_.insert(std::make_pair(value_of(known_color::light_salmon), named_color_type::light_salmon())); + vm_.insert(std::make_pair(value_of(known_color::light_sea_green), named_color_type::light_sea_green())); + vm_.insert(std::make_pair(value_of(known_color::light_sky_blue), named_color_type::light_sky_blue())); + vm_.insert(std::make_pair(value_of(known_color::light_slate_gray), named_color_type::light_slate_gray())); + vm_.insert(std::make_pair(value_of(known_color::light_steel_blue), named_color_type::light_steel_blue())); + vm_.insert(std::make_pair(value_of(known_color::light_yellow), named_color_type::light_yellow())); + vm_.insert(std::make_pair(value_of(known_color::lime), named_color_type::lime())); + vm_.insert(std::make_pair(value_of(known_color::lime_green), named_color_type::lime_green())); + vm_.insert(std::make_pair(value_of(known_color::linen), named_color_type::linen())); + vm_.insert(std::make_pair(value_of(known_color::magenta), named_color_type::magenta())); + vm_.insert(std::make_pair(value_of(known_color::maroon), named_color_type::maroon())); + vm_.insert(std::make_pair(value_of(known_color::medium_aquamarine), named_color_type::medium_aquamarine())); + vm_.insert(std::make_pair(value_of(known_color::medium_blue), named_color_type::medium_blue())); + vm_.insert(std::make_pair(value_of(known_color::medium_orchid), named_color_type::medium_orchid())); + vm_.insert(std::make_pair(value_of(known_color::medium_purple), named_color_type::medium_purple())); + vm_.insert(std::make_pair(value_of(known_color::medium_sea_green), named_color_type::medium_sea_green())); + vm_.insert(std::make_pair(value_of(known_color::medium_slate_blue), named_color_type::medium_slate_blue())); + vm_.insert(std::make_pair(value_of(known_color::medium_spring_green), named_color_type::medium_spring_green())); + vm_.insert(std::make_pair(value_of(known_color::medium_turquoise), named_color_type::medium_turquoise())); + vm_.insert(std::make_pair(value_of(known_color::medium_violet_red), named_color_type::medium_violet_red())); + vm_.insert(std::make_pair(value_of(known_color::midnight_blue), named_color_type::midnight_blue())); + vm_.insert(std::make_pair(value_of(known_color::mint_cream), named_color_type::mint_cream())); + vm_.insert(std::make_pair(value_of(known_color::misty_rose), named_color_type::misty_rose())); + vm_.insert(std::make_pair(value_of(known_color::moccasin), named_color_type::moccasin())); + vm_.insert(std::make_pair(value_of(known_color::navajo_white), named_color_type::navajo_white())); + vm_.insert(std::make_pair(value_of(known_color::navy), named_color_type::navy())); + vm_.insert(std::make_pair(value_of(known_color::old_lace), named_color_type::old_lace())); + vm_.insert(std::make_pair(value_of(known_color::olive), named_color_type::olive())); + vm_.insert(std::make_pair(value_of(known_color::olive_drab), named_color_type::olive_drab())); + vm_.insert(std::make_pair(value_of(known_color::orange), named_color_type::orange())); + vm_.insert(std::make_pair(value_of(known_color::orange_red), named_color_type::orange_red())); + vm_.insert(std::make_pair(value_of(known_color::orchid), named_color_type::orchid())); + vm_.insert(std::make_pair(value_of(known_color::pale_goldenrod), named_color_type::pale_goldenrod())); + vm_.insert(std::make_pair(value_of(known_color::pale_green), named_color_type::pale_green())); + vm_.insert(std::make_pair(value_of(known_color::pale_turquoise), named_color_type::pale_turquoise())); + vm_.insert(std::make_pair(value_of(known_color::pale_violet_red), named_color_type::pale_violet_red())); + vm_.insert(std::make_pair(value_of(known_color::papaya_whip), named_color_type::papaya_whip())); + vm_.insert(std::make_pair(value_of(known_color::peach_puff), named_color_type::peach_puff())); + vm_.insert(std::make_pair(value_of(known_color::peru), named_color_type::peru())); + vm_.insert(std::make_pair(value_of(known_color::pink), named_color_type::pink())); + vm_.insert(std::make_pair(value_of(known_color::plum), named_color_type::plum())); + vm_.insert(std::make_pair(value_of(known_color::powder_blue), named_color_type::powder_blue())); + vm_.insert(std::make_pair(value_of(known_color::purple), named_color_type::purple())); + vm_.insert(std::make_pair(value_of(known_color::red), named_color_type::red())); + vm_.insert(std::make_pair(value_of(known_color::rosy_brown), named_color_type::rosy_brown())); + vm_.insert(std::make_pair(value_of(known_color::royal_blue), named_color_type::royal_blue())); + vm_.insert(std::make_pair(value_of(known_color::saddle_brown), named_color_type::saddle_brown())); + vm_.insert(std::make_pair(value_of(known_color::salmon), named_color_type::salmon())); + vm_.insert(std::make_pair(value_of(known_color::sandy_brown), named_color_type::sandy_brown())); + vm_.insert(std::make_pair(value_of(known_color::sea_green), named_color_type::sea_green())); + vm_.insert(std::make_pair(value_of(known_color::sea_shell), named_color_type::sea_shell())); + vm_.insert(std::make_pair(value_of(known_color::sienna), named_color_type::sienna())); + vm_.insert(std::make_pair(value_of(known_color::silver), named_color_type::silver())); + vm_.insert(std::make_pair(value_of(known_color::sky_blue), named_color_type::sky_blue())); + vm_.insert(std::make_pair(value_of(known_color::slate_blue), named_color_type::slate_blue())); + vm_.insert(std::make_pair(value_of(known_color::slate_gray), named_color_type::slate_gray())); + vm_.insert(std::make_pair(value_of(known_color::snow), named_color_type::snow())); + vm_.insert(std::make_pair(value_of(known_color::spring_green), named_color_type::spring_green())); + vm_.insert(std::make_pair(value_of(known_color::steel_blue), named_color_type::steel_blue())); + vm_.insert(std::make_pair(value_of(known_color::tan), named_color_type::tan())); + vm_.insert(std::make_pair(value_of(known_color::teal), named_color_type::teal())); + vm_.insert(std::make_pair(value_of(known_color::thistle), named_color_type::thistle())); + vm_.insert(std::make_pair(value_of(known_color::tomato), named_color_type::tomato())); + vm_.insert(std::make_pair(value_of(known_color::transparent), named_color_type::transparent())); + vm_.insert(std::make_pair(value_of(known_color::turquoise), named_color_type::turquoise())); + vm_.insert(std::make_pair(value_of(known_color::violet), named_color_type::violet())); + vm_.insert(std::make_pair(value_of(known_color::wheat), named_color_type::wheat())); + vm_.insert(std::make_pair(value_of(known_color::white), named_color_type::white())); + vm_.insert(std::make_pair(value_of(known_color::white_smoke), named_color_type::white_smoke())); + vm_.insert(std::make_pair(value_of(known_color::yellow), named_color_type::yellow())); + vm_.insert(std::make_pair(value_of(known_color::yellow_green), named_color_type::yellow_green())); + } + + return vm_; + } + + private: + basic_color_mapper() = delete; + ~basic_color_mapper() = delete; + basic_color_mapper(const basic_color_mapper&) = delete; + basic_color_mapper& operator =(const basic_color_mapper&) = delete; + + private: + // Convert string to lower case + static string_type to_lowercase(const string_type &str) { + static std::locale loc; + std::string result; + result.reserve(str.size()); + + for (auto elem : str) + result += std::tolower(elem, loc); + + return result; + } + }; + + } // namespace dotnet +} // namespace colors diff --git a/extern/include/cpp-colors/impl/named_impl.h b/extern/include/cpp-colors/impl/named_impl.h new file mode 100644 index 000000000..878f40dcf --- /dev/null +++ b/extern/include/cpp-colors/impl/named_impl.h @@ -0,0 +1,18 @@ +#pragma once + +#include "dotnet/dotnet_named.h" +#include "wpf/wpf_named.h" +#include "x11/x11_named.h" + +namespace colors { + + typedef dotnet::basic_named_color dotnet_named_color; + typedef dotnet::basic_color_mapper dotnet_color_mapper; + + typedef wpf::basic_named_color wpf_named_color; + typedef wpf::basic_color_mapper wpf_color_mapper; + + typedef x11::basic_named_color x11_named_color; + typedef x11::basic_color_mapper x11_color_mapper; + +} // namespace colors diff --git a/extern/include/cpp-colors/impl/named_utils.h b/extern/include/cpp-colors/impl/named_utils.h new file mode 100644 index 000000000..ac0574012 --- /dev/null +++ b/extern/include/cpp-colors/impl/named_utils.h @@ -0,0 +1,10 @@ +#pragma once + +namespace colors { + + template + typename std::underlying_type::type value_of(E e) { + return static_cast::type>(e); + } + +} diff --git a/extern/include/cpp-colors/impl/wpf/wpf_colors.h b/extern/include/cpp-colors/impl/wpf/wpf_colors.h new file mode 100644 index 000000000..fde395ab3 --- /dev/null +++ b/extern/include/cpp-colors/impl/wpf/wpf_colors.h @@ -0,0 +1,81 @@ +#pragma once + +#include "wpf_constants.h" +#include "../named_utils.h" + + +namespace colors { + namespace wpf { + + template + struct basic_colors { + typedef ColorType color_type; + + basic_colors() = delete; + ~basic_colors() = delete; + basic_colors(const basic_colors&) = delete; + basic_colors& operator =(const basic_colors&) = delete; + + static color_type black() { + return color_type(value_of(known_color::black)); + } + + static color_type blue() { + return color_type(value_of(known_color::blue)); + } + + static color_type brown() { + return color_type(value_of(known_color::brown)); + } + + static color_type cyan() { + return color_type(value_of(known_color::cyan)); + } + + static color_type dark_gray() { + return color_type(value_of(known_color::dark_gray)); + } + + static color_type gray() { + return color_type(value_of(known_color::gray)); + } + + static color_type green() { + return color_type(value_of(known_color::green)); + } + + static color_type light_gray() { + return color_type(value_of(known_color::light_gray)); + } + + static color_type magenta() { + return color_type(value_of(known_color::magenta)); + } + + static color_type orange() { + return color_type(value_of(known_color::orange)); + } + + static color_type purple() { + return color_type(value_of(known_color::purple)); + } + + static color_type red() { + return color_type(value_of(known_color::red)); + } + + static color_type transparent() { + return color_type(value_of(known_color::transparent)); + } + + static color_type white() { + return color_type(value_of(known_color::white)); + } + + static color_type yellow() { + return color_type(value_of(known_color::yellow)); + } + }; + + } // namespace wpf +} // namespace colors diff --git a/extern/include/cpp-colors/impl/wpf/wpf_constants.h b/extern/include/cpp-colors/impl/wpf/wpf_constants.h new file mode 100644 index 000000000..2b756e4da --- /dev/null +++ b/extern/include/cpp-colors/impl/wpf/wpf_constants.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace colors { + namespace wpf { + + enum class known_color : uint32_t { + black = 0xFF000000, // Black: argb(255, 0, 0, 0) + blue = 0xFF0000FF, // Blue: argb(255, 0, 0, 255) + brown = 0xFFA52A2A, // Brown: argb(255, 165, 42, 42) + cyan = 0xFF00FFFF, // Cyan: argb(255, 0, 255, 255) + dark_gray = 0xFFA9A9A9, // Dark Gray: argb(255, 169, 169, 169) + gray = 0xFF808080, // Gray: argb(255, 128, 128, 128) + green = 0xFF008000, // Green: argb(255, 0, 128, 0) + light_gray = 0xFFD3D3D3, // Light Gray: argb(255, 211, 211, 211) + magenta = 0xFFFF00FF, // Magenta: argb(255, 255, 0, 255) + orange = 0xFFFFA500, // Orange: argb(255, 255, 165, 0) + purple = 0xFF800080, // Purple: argb(255, 128, 0, 128) + red = 0xFFFF0000, // Red: argb(255, 255, 0, 0) + transparent = 0x00FFFFFF, // Transparent: argb(0, 255, 255, 255) + white = 0xFFFFFFFF, // White: argb(255, 255, 255, 255) + yellow = 0xFFFFFF00, // Yellow: argb(255, 255, 255, 0) + }; + + } // namespace wpf +} // namespace colors diff --git a/extern/include/cpp-colors/impl/wpf/wpf_named.h b/extern/include/cpp-colors/impl/wpf/wpf_named.h new file mode 100644 index 000000000..eafefeef9 --- /dev/null +++ b/extern/include/cpp-colors/impl/wpf/wpf_named.h @@ -0,0 +1,224 @@ +#pragma once + +#include +#include +#include +#include +#include "wpf_constants.h" +#include "../named_utils.h" + + +namespace colors { + namespace wpf { + + template + struct basic_named_color; + + template<> + struct basic_named_color { + static const char *const black() { + return "Black"; + } + + static const char *const blue() { + return "Blue"; + } + + static const char *const brown() { + return "Brown"; + } + + static const char *const cyan() { + return "Cyan"; + } + + static const char *const dark_gray() { + return "DarkGray"; + } + + static const char *const gray() { + return "Gray"; + } + + static const char *const green() { + return "Green"; + } + + static const char *const light_gray() { + return "LightGray"; + } + + static const char *const magenta() { + return "Magenta"; + } + + static const char *const orange() { + return "Orange"; + } + + static const char *const purple() { + return "Purple"; + } + + static const char *const red() { + return "Red"; + } + + static const char *const transparent() { + return "Transparent"; + } + + static const char *const white() { + return "White"; + } + + static const char *const yellow() { + return "Yellow"; + } + }; + + template<> + struct basic_named_color { + static const wchar_t *const black() { + return L"Black"; + } + + static const wchar_t *const blue() { + return L"Blue"; + } + + static const wchar_t *const brown() { + return L"Brown"; + } + + static const wchar_t *const cyan() { + return L"Cyan"; + } + + static const wchar_t *const dark_gray() { + return L"DarkGray"; + } + + static const wchar_t *const gray() { + return L"Gray"; + } + + static const wchar_t *const green() { + return L"Green"; + } + + static const wchar_t *const light_gray() { + return L"LightGray"; + } + + static const wchar_t *const magenta() { + return L"Magenta"; + } + + static const wchar_t *const orange() { + return L"Orange"; + } + + static const wchar_t *const purple() { + return L"Purple"; + } + + static const wchar_t *const red() { + return L"Red"; + } + + static const wchar_t *const transparent() { + return L"Transparent"; + } + + static const wchar_t *const white() { + return L"White"; + } + + static const wchar_t *const yellow() { + return L"Yellow"; + } + }; + + template + struct basic_color_mapper { + typedef CharT char_type; + typedef std::basic_string string_type; + + typedef known_color known_color_type; + typedef basic_named_color named_color_type; + + typedef std::map string_color_map; + typedef std::map color_string_map; + + // Build a name -> constant map + static const string_color_map &get_string_to_color_map() { + static string_color_map sm_; // name -> value + if (sm_.empty()) { + sm_.insert(std::make_pair(to_lowercase(named_color_type::black()), known_color::black)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::blue()), known_color::blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::brown()), known_color::brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cyan()), known_color::cyan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_gray()), known_color::dark_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::gray()), known_color::gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::green()), known_color::green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_gray()), known_color::light_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::magenta()), known_color::magenta)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::orange()), known_color::orange)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::purple()), known_color::purple)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::red()), known_color::red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::transparent()), known_color::transparent)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::white()), known_color::white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::yellow()), known_color::yellow)); + } + + return sm_; + } + + // Build a name -> constant map + static const color_string_map &get_color_to_string_map() { + static color_string_map vm_; // value -> name + + if (vm_.empty()) { + vm_.insert(std::make_pair(value_of(known_color::black), named_color_type::black())); + vm_.insert(std::make_pair(value_of(known_color::blue), named_color_type::blue())); + vm_.insert(std::make_pair(value_of(known_color::brown), named_color_type::brown())); + vm_.insert(std::make_pair(value_of(known_color::cyan), named_color_type::cyan())); + vm_.insert(std::make_pair(value_of(known_color::dark_gray), named_color_type::dark_gray())); + vm_.insert(std::make_pair(value_of(known_color::gray), named_color_type::gray())); + vm_.insert(std::make_pair(value_of(known_color::green), named_color_type::green())); + vm_.insert(std::make_pair(value_of(known_color::light_gray), named_color_type::light_gray())); + vm_.insert(std::make_pair(value_of(known_color::magenta), named_color_type::magenta())); + vm_.insert(std::make_pair(value_of(known_color::orange), named_color_type::orange())); + vm_.insert(std::make_pair(value_of(known_color::purple), named_color_type::purple())); + vm_.insert(std::make_pair(value_of(known_color::red), named_color_type::red())); + vm_.insert(std::make_pair(value_of(known_color::transparent), named_color_type::transparent())); + vm_.insert(std::make_pair(value_of(known_color::white), named_color_type::white())); + vm_.insert(std::make_pair(value_of(known_color::yellow), named_color_type::yellow())); + } + + return vm_; + } + + private: + basic_color_mapper() = delete; + ~basic_color_mapper() = delete; + basic_color_mapper(const basic_color_mapper&) = delete; + basic_color_mapper& operator =(const basic_color_mapper&) = delete; + + private: + // Convert string to lower case + static string_type to_lowercase(const string_type &str) { + static std::locale loc; + std::string result; + result.reserve(str.size()); + + for (auto elem : str) + result += std::tolower(elem, loc); + + return result; + } + }; + + } // namespace wpf +} // namespace colors diff --git a/extern/include/cpp-colors/impl/x11/x11_colors.h b/extern/include/cpp-colors/impl/x11/x11_colors.h new file mode 100644 index 000000000..e18149d27 --- /dev/null +++ b/extern/include/cpp-colors/impl/x11/x11_colors.h @@ -0,0 +1,580 @@ +#pragma once + +#include "x11_constants.h" +#include "../named_utils.h" + + +namespace colors { + namespace x11 { + + template + struct basic_colors { + typedef ColorType color_type; + + basic_colors() = delete; + ~basic_colors() = delete; + basic_colors(const basic_colors&) = delete; + basic_colors& operator =(const basic_colors&) = delete; + + static color_type pink() { + return color_type(value_of(known_color::pink)); + } + + static color_type light_pink() { + return color_type(value_of(known_color::light_pink)); + } + + static color_type hot_pink() { + return color_type(value_of(known_color::hot_pink)); + } + + static color_type deep_pink() { + return color_type(value_of(known_color::deep_pink)); + } + + static color_type pale_violet_red() { + return color_type(value_of(known_color::pale_violet_red)); + } + + static color_type medium_violet_red() { + return color_type(value_of(known_color::medium_violet_red)); + } + + static color_type light_salmon() { + return color_type(value_of(known_color::light_salmon)); + } + + static color_type salmon() { + return color_type(value_of(known_color::salmon)); + } + + static color_type dark_salmon() { + return color_type(value_of(known_color::dark_salmon)); + } + + static color_type light_coral() { + return color_type(value_of(known_color::light_coral)); + } + + static color_type indian_red() { + return color_type(value_of(known_color::indian_red)); + } + + static color_type crimson() { + return color_type(value_of(known_color::crimson)); + } + + static color_type fire_brick() { + return color_type(value_of(known_color::fire_brick)); + } + + static color_type dark_red() { + return color_type(value_of(known_color::dark_red)); + } + + static color_type red() { + return color_type(value_of(known_color::red)); + } + + static color_type orange_red() { + return color_type(value_of(known_color::orange_red)); + } + + static color_type tomato() { + return color_type(value_of(known_color::tomato)); + } + + static color_type coral() { + return color_type(value_of(known_color::coral)); + } + + static color_type dark_orange() { + return color_type(value_of(known_color::dark_orange)); + } + + static color_type orange() { + return color_type(value_of(known_color::orange)); + } + + static color_type yellow() { + return color_type(value_of(known_color::yellow)); + } + + static color_type light_yellow() { + return color_type(value_of(known_color::light_yellow)); + } + + static color_type lemon_chiffon() { + return color_type(value_of(known_color::lemon_chiffon)); + } + + static color_type light_goldenrod_yellow() { + return color_type(value_of(known_color::light_goldenrod_yellow)); + } + + static color_type papaya_whip() { + return color_type(value_of(known_color::papaya_whip)); + } + + static color_type moccasin() { + return color_type(value_of(known_color::moccasin)); + } + + static color_type peach_puff() { + return color_type(value_of(known_color::peach_puff)); + } + + static color_type pale_goldenrod() { + return color_type(value_of(known_color::pale_goldenrod)); + } + + static color_type khaki() { + return color_type(value_of(known_color::khaki)); + } + + static color_type dark_khaki() { + return color_type(value_of(known_color::dark_khaki)); + } + + static color_type gold() { + return color_type(value_of(known_color::gold)); + } + + static color_type cornsilk() { + return color_type(value_of(known_color::cornsilk)); + } + + static color_type blanched_almond() { + return color_type(value_of(known_color::blanched_almond)); + } + + static color_type bisque() { + return color_type(value_of(known_color::bisque)); + } + + static color_type navajo_white() { + return color_type(value_of(known_color::navajo_white)); + } + + static color_type wheat() { + return color_type(value_of(known_color::wheat)); + } + + static color_type burly_wood() { + return color_type(value_of(known_color::burly_wood)); + } + + static color_type tan() { + return color_type(value_of(known_color::tan)); + } + + static color_type rosy_brown() { + return color_type(value_of(known_color::rosy_brown)); + } + + static color_type sandy_brown() { + return color_type(value_of(known_color::sandy_brown)); + } + + static color_type goldenrod() { + return color_type(value_of(known_color::goldenrod)); + } + + static color_type dark_goldenrod() { + return color_type(value_of(known_color::dark_goldenrod)); + } + + static color_type peru() { + return color_type(value_of(known_color::peru)); + } + + static color_type chocolate() { + return color_type(value_of(known_color::chocolate)); + } + + static color_type saddle_brown() { + return color_type(value_of(known_color::saddle_brown)); + } + + static color_type sienna() { + return color_type(value_of(known_color::sienna)); + } + + static color_type brown() { + return color_type(value_of(known_color::brown)); + } + + static color_type maroon() { + return color_type(value_of(known_color::maroon)); + } + + static color_type dark_olive_green() { + return color_type(value_of(known_color::dark_olive_green)); + } + + static color_type olive() { + return color_type(value_of(known_color::olive)); + } + + static color_type olive_drab() { + return color_type(value_of(known_color::olive_drab)); + } + + static color_type yellow_green() { + return color_type(value_of(known_color::yellow_green)); + } + + static color_type lime_green() { + return color_type(value_of(known_color::lime_green)); + } + + static color_type lime() { + return color_type(value_of(known_color::lime)); + } + + static color_type lawn_green() { + return color_type(value_of(known_color::lawn_green)); + } + + static color_type chartreuse() { + return color_type(value_of(known_color::chartreuse)); + } + + static color_type green_yellow() { + return color_type(value_of(known_color::green_yellow)); + } + + static color_type spring_green() { + return color_type(value_of(known_color::spring_green)); + } + + static color_type medium_spring_green() { + return color_type(value_of(known_color::medium_spring_green)); + } + + static color_type light_green() { + return color_type(value_of(known_color::light_green)); + } + + static color_type pale_green() { + return color_type(value_of(known_color::pale_green)); + } + + static color_type dark_sea_green() { + return color_type(value_of(known_color::dark_sea_green)); + } + + static color_type medium_sea_green() { + return color_type(value_of(known_color::medium_sea_green)); + } + + static color_type sea_green() { + return color_type(value_of(known_color::sea_green)); + } + + static color_type forest_green() { + return color_type(value_of(known_color::forest_green)); + } + + static color_type green() { + return color_type(value_of(known_color::green)); + } + + static color_type dark_green() { + return color_type(value_of(known_color::dark_green)); + } + + static color_type medium_aquamarine() { + return color_type(value_of(known_color::medium_aquamarine)); + } + + static color_type aqua() { + return color_type(value_of(known_color::aqua)); + } + + static color_type cyan() { + return color_type(value_of(known_color::cyan)); + } + + static color_type light_cyan() { + return color_type(value_of(known_color::light_cyan)); + } + + static color_type pale_turquoise() { + return color_type(value_of(known_color::pale_turquoise)); + } + + static color_type aquamarine() { + return color_type(value_of(known_color::aquamarine)); + } + + static color_type turquoise() { + return color_type(value_of(known_color::turquoise)); + } + + static color_type medium_turquoise() { + return color_type(value_of(known_color::medium_turquoise)); + } + + static color_type dark_turquoise() { + return color_type(value_of(known_color::dark_turquoise)); + } + + static color_type light_sea_green() { + return color_type(value_of(known_color::light_sea_green)); + } + + static color_type cadet_blue() { + return color_type(value_of(known_color::cadet_blue)); + } + + static color_type dark_cyan() { + return color_type(value_of(known_color::dark_cyan)); + } + + static color_type teal() { + return color_type(value_of(known_color::teal)); + } + + static color_type light_steel_blue() { + return color_type(value_of(known_color::light_steel_blue)); + } + + static color_type powder_blue() { + return color_type(value_of(known_color::powder_blue)); + } + + static color_type light_blue() { + return color_type(value_of(known_color::light_blue)); + } + + static color_type sky_blue() { + return color_type(value_of(known_color::sky_blue)); + } + + static color_type light_sky_blue() { + return color_type(value_of(known_color::light_sky_blue)); + } + + static color_type deep_sky_blue() { + return color_type(value_of(known_color::deep_sky_blue)); + } + + static color_type dodger_blue() { + return color_type(value_of(known_color::dodger_blue)); + } + + static color_type cornflower_blue() { + return color_type(value_of(known_color::cornflower_blue)); + } + + static color_type steel_blue() { + return color_type(value_of(known_color::steel_blue)); + } + + static color_type royal_blue() { + return color_type(value_of(known_color::royal_blue)); + } + + static color_type blue() { + return color_type(value_of(known_color::blue)); + } + + static color_type medium_blue() { + return color_type(value_of(known_color::medium_blue)); + } + + static color_type dark_blue() { + return color_type(value_of(known_color::dark_blue)); + } + + static color_type navy() { + return color_type(value_of(known_color::navy)); + } + + static color_type midnight_blue() { + return color_type(value_of(known_color::midnight_blue)); + } + + static color_type lavender() { + return color_type(value_of(known_color::lavender)); + } + + static color_type thistle() { + return color_type(value_of(known_color::thistle)); + } + + static color_type plum() { + return color_type(value_of(known_color::plum)); + } + + static color_type violet() { + return color_type(value_of(known_color::violet)); + } + + static color_type orchid() { + return color_type(value_of(known_color::orchid)); + } + + static color_type fuchsia() { + return color_type(value_of(known_color::fuchsia)); + } + + static color_type magenta() { + return color_type(value_of(known_color::magenta)); + } + + static color_type medium_orchid() { + return color_type(value_of(known_color::medium_orchid)); + } + + static color_type medium_purple() { + return color_type(value_of(known_color::medium_purple)); + } + + static color_type blue_violet() { + return color_type(value_of(known_color::blue_violet)); + } + + static color_type dark_violet() { + return color_type(value_of(known_color::dark_violet)); + } + + static color_type dark_orchid() { + return color_type(value_of(known_color::dark_orchid)); + } + + static color_type dark_magenta() { + return color_type(value_of(known_color::dark_magenta)); + } + + static color_type purple() { + return color_type(value_of(known_color::purple)); + } + + static color_type indigo() { + return color_type(value_of(known_color::indigo)); + } + + static color_type dark_slate_blue() { + return color_type(value_of(known_color::dark_slate_blue)); + } + + static color_type slate_blue() { + return color_type(value_of(known_color::slate_blue)); + } + + static color_type medium_slate_blue() { + return color_type(value_of(known_color::medium_slate_blue)); + } + + static color_type white() { + return color_type(value_of(known_color::white)); + } + + static color_type snow() { + return color_type(value_of(known_color::snow)); + } + + static color_type honeydew() { + return color_type(value_of(known_color::honeydew)); + } + + static color_type mint_cream() { + return color_type(value_of(known_color::mint_cream)); + } + + static color_type azure() { + return color_type(value_of(known_color::azure)); + } + + static color_type alice_blue() { + return color_type(value_of(known_color::alice_blue)); + } + + static color_type ghost_white() { + return color_type(value_of(known_color::ghost_white)); + } + + static color_type white_smoke() { + return color_type(value_of(known_color::white_smoke)); + } + + static color_type seashell() { + return color_type(value_of(known_color::seashell)); + } + + static color_type beige() { + return color_type(value_of(known_color::beige)); + } + + static color_type old_lace() { + return color_type(value_of(known_color::old_lace)); + } + + static color_type floral_white() { + return color_type(value_of(known_color::floral_white)); + } + + static color_type ivory() { + return color_type(value_of(known_color::ivory)); + } + + static color_type antique_white() { + return color_type(value_of(known_color::antique_white)); + } + + static color_type linen() { + return color_type(value_of(known_color::linen)); + } + + static color_type lavender_blush() { + return color_type(value_of(known_color::lavender_blush)); + } + + static color_type misty_rose() { + return color_type(value_of(known_color::misty_rose)); + } + + static color_type gainsboro() { + return color_type(value_of(known_color::gainsboro)); + } + + static color_type light_grey() { + return color_type(value_of(known_color::light_grey)); + } + + static color_type silver() { + return color_type(value_of(known_color::silver)); + } + + static color_type dark_gray() { + return color_type(value_of(known_color::dark_gray)); + } + + static color_type gray() { + return color_type(value_of(known_color::gray)); + } + + static color_type dim_gray() { + return color_type(value_of(known_color::dim_gray)); + } + + static color_type light_slate_gray() { + return color_type(value_of(known_color::light_slate_gray)); + } + + static color_type slate_gray() { + return color_type(value_of(known_color::slate_gray)); + } + + static color_type dark_slate_gray() { + return color_type(value_of(known_color::dark_slate_gray)); + } + + static color_type black() { + return color_type(value_of(known_color::black)); + } + }; + } +} \ No newline at end of file diff --git a/extern/include/cpp-colors/impl/x11/x11_constants.h b/extern/include/cpp-colors/impl/x11/x11_constants.h new file mode 100644 index 000000000..f7fb531a4 --- /dev/null +++ b/extern/include/cpp-colors/impl/x11/x11_constants.h @@ -0,0 +1,152 @@ +#pragma once + +#include + +namespace colors { + namespace x11 { + + enum class known_color : uint32_t { + pink = 0xFFFFC0CB, // Pink: argb(255, 255, 192, 203) + light_pink = 0xFFFFB6C1, // LightPink: argb(255, 255, 182, 193) + hot_pink = 0xFFFF69B4, // HotPink: argb(255, 255, 105, 180) + deep_pink = 0xFFFF1493, // DeepPink: argb(255, 255, 20, 147) + pale_violet_red = 0xFFDB7093, // PaleVioletRed: argb(255, 219, 112, 147) + medium_violet_red = 0xFFC71585, // MediumVioletRed: argb(255, 199, 21, 133) + light_salmon = 0xFFFFA07A, // LightSalmon: argb(255, 255, 160, 122) + salmon = 0xFFFA8072, // Salmon: argb(255, 250, 128, 114) + dark_salmon = 0xFFE9967A, // DarkSalmon: argb(255, 233, 150, 122) + light_coral = 0xFFF08080, // LightCoral: argb(255, 240, 128, 128) + indian_red = 0xFFCD5C5C, // IndianRed: argb(255, 205, 92, 92) + crimson = 0xFFDC143C, // Crimson: argb(255, 220, 20, 60) + fire_brick = 0xFFB22222, // FireBrick: argb(255, 178, 34, 34) + dark_red = 0xFF8B0000, // DarkRed: argb(255, 139, 0, 0) + red = 0xFFFF0000, // Red: argb(255, 255, 0, 0) + orange_red = 0xFFFF4500, // OrangeRed: argb(255, 255, 69, 0) + tomato = 0xFFFF6347, // Tomato: argb(255, 255, 99, 71) + coral = 0xFFFF7F50, // Coral: argb(255, 255, 127, 80) + dark_orange = 0xFFFF8C00, // DarkOrange: argb(255, 255, 140, 0) + orange = 0xFFFFA500, // Orange: argb(255, 255, 165, 0) + yellow = 0xFFFFFF00, // Yellow: argb(255, 255, 255, 0) + light_yellow = 0xFFFFFFE0, // LightYellow: argb(255, 255, 255, 224) + lemon_chiffon = 0xFFFFFACD, // LemonChiffon: argb(255, 255, 250, 205) + light_goldenrod_yellow = 0xFFFAFAD2, // LightGoldenrodYellow: argb(255, 250, 250, 210) + papaya_whip = 0xFFFFEFD5, // PapayaWhip: argb(255, 255, 239, 213) + moccasin = 0xFFFFE4B5, // Moccasin: argb(255, 255, 228, 181) + peach_puff = 0xFFFFDAB9, // PeachPuff: argb(255, 255, 218, 185) + pale_goldenrod = 0xFFEEE8AA, // PaleGoldenrod: argb(255, 238, 232, 170) + khaki = 0xFFF0E68C, // Khaki: argb(255, 240, 230, 140) + dark_khaki = 0xFFBDB76B, // DarkKhaki: argb(255, 189, 183, 107) + gold = 0xFFFFD700, // Gold: argb(255, 255, 215, 0) + cornsilk = 0xFFFFF8DC, // Cornsilk: argb(255, 255, 248, 220) + blanched_almond = 0xFFFFEBCD, // BlanchedAlmond: argb(255, 255, 235, 205) + bisque = 0xFFFFE4C4, // Bisque: argb(255, 255, 228, 196) + navajo_white = 0xFFFFDEAD, // NavajoWhite: argb(255, 255, 222, 173) + wheat = 0xFFF5DEB3, // Wheat: argb(255, 245, 222, 179) + burly_wood = 0xFFDEB887, // BurlyWood: argb(255, 222, 184, 135) + tan = 0xFFD2B48C, // Tan: argb(255, 210, 180, 140) + rosy_brown = 0xFFBC8F8F, // RosyBrown: argb(255, 188, 143, 143) + sandy_brown = 0xFFF4A460, // SandyBrown: argb(255, 244, 164, 96) + goldenrod = 0xFFDAA520, // Goldenrod: argb(255, 218, 165, 32) + dark_goldenrod = 0xFFB8860B, // DarkGoldenrod: argb(255, 184, 134, 11) + peru = 0xFFCD853F, // Peru: argb(255, 205, 133, 63) + chocolate = 0xFFD2691E, // Chocolate: argb(255, 210, 105, 30) + saddle_brown = 0xFF8B4513, // SaddleBrown: argb(255, 139, 69, 19) + sienna = 0xFFA0522D, // Sienna: argb(255, 160, 82, 45) + brown = 0xFFA52A2A, // Brown: argb(255, 165, 42, 42) + maroon = 0xFF800000, // Maroon: argb(255, 128, 0, 0) + dark_olive_green = 0xFF556B2F, // DarkOliveGreen: argb(255, 85, 107, 47) + olive = 0xFF808000, // Olive: argb(255, 128, 128, 0) + olive_drab = 0xFF6B8E23, // OliveDrab: argb(255, 107, 142, 35) + yellow_green = 0xFF9ACD32, // YellowGreen: argb(255, 154, 205, 50) + lime_green = 0xFF32CD32, // LimeGreen: argb(255, 50, 205, 50) + lime = 0xFF00FF00, // Lime: argb(255, 0, 255, 0) + lawn_green = 0xFF7CFC00, // LawnGreen: argb(255, 124, 252, 0) + chartreuse = 0xFF7FFF00, // Chartreuse: argb(255, 127, 255, 0) + green_yellow = 0xFFADFF2F, // GreenYellow: argb(255, 173, 255, 47) + spring_green = 0xFF00FF7F, // SpringGreen: argb(255, 0, 255, 127) + medium_spring_green = 0xFF00FA9A, // MediumSpringGreen: argb(255, 0, 250, 154) + light_green = 0xFF90EE90, // LightGreen: argb(255, 144, 238, 144) + pale_green = 0xFF98FB98, // PaleGreen: argb(255, 152, 251, 152) + dark_sea_green = 0xFF8FBC8F, // DarkSeaGreen: argb(255, 143, 188, 143) + medium_sea_green = 0xFF3CB371, // MediumSeaGreen: argb(255, 60, 179, 113) + sea_green = 0xFF2E8B57, // SeaGreen: argb(255, 46, 139, 87) + forest_green = 0xFF228B22, // ForestGreen: argb(255, 34, 139, 34) + green = 0xFF008000, // Green: argb(255, 0, 128, 0) + dark_green = 0xFF006400, // DarkGreen: argb(255, 0, 100, 0) + medium_aquamarine = 0xFF66CDAA, // MediumAquamarine: argb(255, 102, 205, 170) + aqua = 0xFF00FFFF, // Aqua: argb(255, 0, 255, 255) + cyan = 0xFF00FFFF, // Cyan: argb(255, 0, 255, 255) + light_cyan = 0xFFE0FFFF, // LightCyan: argb(255, 224, 255, 255) + pale_turquoise = 0xFFAFEEEE, // PaleTurquoise: argb(255, 175, 238, 238) + aquamarine = 0xFF7FFFD4, // Aquamarine: argb(255, 127, 255, 212) + turquoise = 0xFF40E0D0, // Turquoise: argb(255, 64, 224, 208) + medium_turquoise = 0xFF48D1CC, // MediumTurquoise: argb(255, 72, 209, 204) + dark_turquoise = 0xFF00CED1, // DarkTurquoise: argb(255, 0, 206, 209) + light_sea_green = 0xFF20B2AA, // LightSeaGreen: argb(255, 32, 178, 170) + cadet_blue = 0xFF5F9EA0, // CadetBlue: argb(255, 95, 158, 160) + dark_cyan = 0xFF008B8B, // DarkCyan: argb(255, 0, 139, 139) + teal = 0xFF008080, // Teal: argb(255, 0, 128, 128) + light_steel_blue = 0xFFB0C4DE, // LightSteelBlue: argb(255, 176, 196, 222) + powder_blue = 0xFFB0E0E6, // PowderBlue: argb(255, 176, 224, 230) + light_blue = 0xFFADD8E6, // LightBlue: argb(255, 173, 216, 230) + sky_blue = 0xFF87CEEB, // SkyBlue: argb(255, 135, 206, 235) + light_sky_blue = 0xFF87CEFA, // LightSkyBlue: argb(255, 135, 206, 250) + deep_sky_blue = 0xFF00BFFF, // DeepSkyBlue: argb(255, 0, 191, 255) + dodger_blue = 0xFF1E90FF, // DodgerBlue: argb(255, 30, 144, 255) + cornflower_blue = 0xFF6495ED, // CornflowerBlue: argb(255, 100, 149, 237) + steel_blue = 0xFF4682B4, // SteelBlue: argb(255, 70, 130, 180) + royal_blue = 0xFF4169E1, // RoyalBlue: argb(255, 65, 105, 225) + blue = 0xFF0000FF, // Blue: argb(255, 0, 0, 255) + medium_blue = 0xFF0000CD, // MediumBlue: argb(255, 0, 0, 205) + dark_blue = 0xFF00008B, // DarkBlue: argb(255, 0, 0, 139) + navy = 0xFF000080, // Navy: argb(255, 0, 0, 128) + midnight_blue = 0xFF191970, // MidnightBlue: argb(255, 25, 25, 112) + lavender = 0xFFE6E6FA, // Lavender: argb(255, 230, 230, 250) + thistle = 0xFFD8BFD8, // Thistle: argb(255, 216, 191, 216) + plum = 0xFFDDA0DD, // Plum: argb(255, 221, 160, 221) + violet = 0xFFEE82EE, // Violet: argb(255, 238, 130, 238) + orchid = 0xFFDA70D6, // Orchid: argb(255, 218, 112, 214) + fuchsia = 0xFFFF00FF, // Fuchsia: argb(255, 255, 0, 255) + magenta = 0xFFFF00FF, // Magenta: argb(255, 255, 0, 255) + medium_orchid = 0xFFBA55D3, // MediumOrchid: argb(255, 186, 85, 211) + medium_purple = 0xFF9370DB, // MediumPurple: argb(255, 147, 112, 219) + blue_violet = 0xFF8A2BE2, // BlueViolet: argb(255, 138, 43, 226) + dark_violet = 0xFF9400D3, // DarkViolet: argb(255, 148, 0, 211) + dark_orchid = 0xFF9932CC, // DarkOrchid: argb(255, 153, 50, 204) + dark_magenta = 0xFF8B008B, // DarkMagenta: argb(255, 139, 0, 139) + purple = 0xFF800080, // Purple: argb(255, 128, 0, 128) + indigo = 0xFF4B0082, // Indigo: argb(255, 75, 0, 130) + dark_slate_blue = 0xFF483D8B, // DarkSlateBlue: argb(255, 72, 61, 139) + slate_blue = 0xFF6A5ACD, // SlateBlue: argb(255, 106, 90, 205) + medium_slate_blue = 0xFF7B68EE, // MediumSlateBlue: argb(255, 123, 104, 238) + white = 0xFFFFFFFF, // White: argb(255, 255, 255, 255) + snow = 0xFFFFFAFA, // Snow: argb(255, 255, 250, 250) + honeydew = 0xFFF0FFF0, // Honeydew: argb(255, 240, 255, 240) + mint_cream = 0xFFF5FFFA, // MintCream: argb(255, 245, 255, 250) + azure = 0xFFF0FFFF, // Azure: argb(255, 240, 255, 255) + alice_blue = 0xFFF0F8FF, // AliceBlue: argb(255, 240, 248, 255) + ghost_white = 0xFFF8F8FF, // GhostWhite: argb(255, 248, 248, 255) + white_smoke = 0xFFF5F5F5, // WhiteSmoke: argb(255, 245, 245, 245) + seashell = 0xFFFFF5EE, // Seashell: argb(255, 255, 245, 238) + beige = 0xFFF5F5DC, // Beige: argb(255, 245, 245, 220) + old_lace = 0xFFFDF5E6, // OldLace: argb(255, 253, 245, 230) + floral_white = 0xFFFFFAF0, // FloralWhite: argb(255, 255, 250, 240) + ivory = 0xFFFFFFF0, // Ivory: argb(255, 255, 255, 240) + antique_white = 0xFFFAEBD7, // AntiqueWhite: argb(255, 250, 235, 215) + linen = 0xFFFAF0E6, // Linen: argb(255, 250, 240, 230) + lavender_blush = 0xFFFFF0F5, // LavenderBlush: argb(255, 255, 240, 245) + misty_rose = 0xFFFFE4E1, // MistyRose: argb(255, 255, 228, 225) + gainsboro = 0xFFDCDCDC, // Gainsboro: argb(255, 220, 220, 220) + light_grey = 0xFFD3D3D3, // LightGrey: argb(255, 211, 211, 211) + silver = 0xFFC0C0C0, // Silver: argb(255, 192, 192, 192) + dark_gray = 0xFFA9A9A9, // DarkGray: argb(255, 169, 169, 169) + gray = 0xFF808080, // Gray: argb(255, 128, 128, 128) + dim_gray = 0xFF696969, // DimGray: argb(255, 105, 105, 105) + light_slate_gray = 0xFF778899, // LightSlateGray: argb(255, 119, 136, 153) + slate_gray = 0xFF708090, // SlateGray: argb(255, 112, 128, 144) + dark_slate_gray = 0xFF2F4F4F, // DarkSlateGray: argb(255, 47, 79, 79) + black = 0xFF000000, // Black: argb(255, 0, 0, 0) + }; + + } +} \ No newline at end of file diff --git a/extern/include/cpp-colors/impl/x11/x11_named.h b/extern/include/cpp-colors/impl/x11/x11_named.h new file mode 100644 index 000000000..23bf82a88 --- /dev/null +++ b/extern/include/cpp-colors/impl/x11/x11_named.h @@ -0,0 +1,1477 @@ +#pragma once + +#include +#include +#include +#include +#include "x11_constants.h" +#include "../named_utils.h" + + +namespace colors { + namespace x11 { + + template + struct basic_named_color; + + template<> + struct basic_named_color { + static const char* const pink() { + return "Pink"; + } + + static const char* const light_pink() { + return "LightPink"; + } + + static const char* const hot_pink() { + return "HotPink"; + } + + static const char* const deep_pink() { + return "DeepPink"; + } + + static const char* const pale_violet_red() { + return "PaleVioletRed"; + } + + static const char* const medium_violet_red() { + return "MediumVioletRed"; + } + + static const char* const light_salmon() { + return "LightSalmon"; + } + + static const char* const salmon() { + return "Salmon"; + } + + static const char* const dark_salmon() { + return "DarkSalmon"; + } + + static const char* const light_coral() { + return "LightCoral"; + } + + static const char* const indian_red() { + return "IndianRed"; + } + + static const char* const crimson() { + return "Crimson"; + } + + static const char* const fire_brick() { + return "FireBrick"; + } + + static const char* const dark_red() { + return "DarkRed"; + } + + static const char* const red() { + return "Red"; + } + + static const char* const orange_red() { + return "OrangeRed"; + } + + static const char* const tomato() { + return "Tomato"; + } + + static const char* const coral() { + return "Coral"; + } + + static const char* const dark_orange() { + return "DarkOrange"; + } + + static const char* const orange() { + return "Orange"; + } + + static const char* const yellow() { + return "Yellow"; + } + + static const char* const light_yellow() { + return "LightYellow"; + } + + static const char* const lemon_chiffon() { + return "LemonChiffon"; + } + + static const char* const light_goldenrod_yellow() { + return "LightGoldenrodYellow"; + } + + static const char* const papaya_whip() { + return "PapayaWhip"; + } + + static const char* const moccasin() { + return "Moccasin"; + } + + static const char* const peach_puff() { + return "PeachPuff"; + } + + static const char* const pale_goldenrod() { + return "PaleGoldenrod"; + } + + static const char* const khaki() { + return "Khaki"; + } + + static const char* const dark_khaki() { + return "DarkKhaki"; + } + + static const char* const gold() { + return "Gold"; + } + + static const char* const cornsilk() { + return "Cornsilk"; + } + + static const char* const blanched_almond() { + return "BlanchedAlmond"; + } + + static const char* const bisque() { + return "Bisque"; + } + + static const char* const navajo_white() { + return "NavajoWhite"; + } + + static const char* const wheat() { + return "Wheat"; + } + + static const char* const burly_wood() { + return "BurlyWood"; + } + + static const char* const tan() { + return "Tan"; + } + + static const char* const rosy_brown() { + return "RosyBrown"; + } + + static const char* const sandy_brown() { + return "SandyBrown"; + } + + static const char* const goldenrod() { + return "Goldenrod"; + } + + static const char* const dark_goldenrod() { + return "DarkGoldenrod"; + } + + static const char* const peru() { + return "Peru"; + } + + static const char* const chocolate() { + return "Chocolate"; + } + + static const char* const saddle_brown() { + return "SaddleBrown"; + } + + static const char* const sienna() { + return "Sienna"; + } + + static const char* const brown() { + return "Brown"; + } + + static const char* const maroon() { + return "Maroon"; + } + + static const char* const dark_olive_green() { + return "DarkOliveGreen"; + } + + static const char* const olive() { + return "Olive"; + } + + static const char* const olive_drab() { + return "OliveDrab"; + } + + static const char* const yellow_green() { + return "YellowGreen"; + } + + static const char* const lime_green() { + return "LimeGreen"; + } + + static const char* const lime() { + return "Lime"; + } + + static const char* const lawn_green() { + return "LawnGreen"; + } + + static const char* const chartreuse() { + return "Chartreuse"; + } + + static const char* const green_yellow() { + return "GreenYellow"; + } + + static const char* const spring_green() { + return "SpringGreen"; + } + + static const char* const medium_spring_green() { + return "MediumSpringGreen"; + } + + static const char* const light_green() { + return "LightGreen"; + } + + static const char* const pale_green() { + return "PaleGreen"; + } + + static const char* const dark_sea_green() { + return "DarkSeaGreen"; + } + + static const char* const medium_sea_green() { + return "MediumSeaGreen"; + } + + static const char* const sea_green() { + return "SeaGreen"; + } + + static const char* const forest_green() { + return "ForestGreen"; + } + + static const char* const green() { + return "Green"; + } + + static const char* const dark_green() { + return "DarkGreen"; + } + + static const char* const medium_aquamarine() { + return "MediumAquamarine"; + } + + static const char* const aqua() { + return "Aqua"; + } + + static const char* const cyan() { + return "Cyan"; + } + + static const char* const light_cyan() { + return "LightCyan"; + } + + static const char* const pale_turquoise() { + return "PaleTurquoise"; + } + + static const char* const aquamarine() { + return "Aquamarine"; + } + + static const char* const turquoise() { + return "Turquoise"; + } + + static const char* const medium_turquoise() { + return "MediumTurquoise"; + } + + static const char* const dark_turquoise() { + return "DarkTurquoise"; + } + + static const char* const light_sea_green() { + return "LightSeaGreen"; + } + + static const char* const cadet_blue() { + return "CadetBlue"; + } + + static const char* const dark_cyan() { + return "DarkCyan"; + } + + static const char* const teal() { + return "Teal"; + } + + static const char* const light_steel_blue() { + return "LightSteelBlue"; + } + + static const char* const powder_blue() { + return "PowderBlue"; + } + + static const char* const light_blue() { + return "LightBlue"; + } + + static const char* const sky_blue() { + return "SkyBlue"; + } + + static const char* const light_sky_blue() { + return "LightSkyBlue"; + } + + static const char* const deep_sky_blue() { + return "DeepSkyBlue"; + } + + static const char* const dodger_blue() { + return "DodgerBlue"; + } + + static const char* const cornflower_blue() { + return "CornflowerBlue"; + } + + static const char* const steel_blue() { + return "SteelBlue"; + } + + static const char* const royal_blue() { + return "RoyalBlue"; + } + + static const char* const blue() { + return "Blue"; + } + + static const char* const medium_blue() { + return "MediumBlue"; + } + + static const char* const DarkBlue() { + return "DarkBlue"; + } + + static const char* const Navy() { + return "Navy"; + } + + static const char* const MidnightBlue() { + return "MidnightBlue"; + } + + static const char* const Lavender() { + return "Lavender"; + } + + static const char* const Thistle() { + return "Thistle"; + } + + static const char* const Plum() { + return "Plum"; + } + + static const char* const Violet() { + return "Violet"; + } + + static const char* const Orchid() { + return "Orchid"; + } + + static const char* const Fuchsia() { + return "Fuchsia"; + } + + static const char* const Magenta() { + return "Magenta"; + } + + static const char* const MediumOrchid() { + return "MediumOrchid"; + } + + static const char* const MediumPurple() { + return "MediumPurple"; + } + + static const char* const BlueViolet() { + return "BlueViolet"; + } + + static const char* const DarkViolet() { + return "DarkViolet"; + } + + static const char* const DarkOrchid() { + return "DarkOrchid"; + } + + static const char* const DarkMagenta() { + return "DarkMagenta"; + } + + static const char* const Purple() { + return "Purple"; + } + + static const char* const Indigo() { + return "Indigo"; + } + + static const char* const DarkSlateBlue() { + return "DarkSlateBlue"; + } + + static const char* const SlateBlue() { + return "SlateBlue"; + } + + static const char* const MediumSlateBlue() { + return "MediumSlateBlue"; + } + + static const char* const White() { + return "White"; + } + + static const char* const Snow() { + return "Snow"; + } + + static const char* const Honeydew() { + return "Honeydew"; + } + + static const char* const MintCream() { + return "MintCream"; + } + + static const char* const Azure() { + return "Azure"; + } + + static const char* const AliceBlue() { + return "AliceBlue"; + } + + static const char* const GhostWhite() { + return "GhostWhite"; + } + + static const char* const WhiteSmoke() { + return "WhiteSmoke"; + } + + static const char* const Seashell() { + return "Seashell"; + } + + static const char* const Beige() { + return "Beige"; + } + + static const char* const OldLace() { + return "OldLace"; + } + + static const char* const FloralWhite() { + return "FloralWhite"; + } + + static const char* const Ivory() { + return "Ivory"; + } + + static const char* const AntiqueWhite() { + return "AntiqueWhite"; + } + + static const char* const Linen() { + return "Linen"; + } + + static const char* const LavenderBlush() { + return "LavenderBlush"; + } + + static const char* const MistyRose() { + return "MistyRose"; + } + + static const char* const Gainsboro() { + return "Gainsboro"; + } + + static const char* const LightGrey() { + return "LightGrey"; + } + + static const char* const Silver() { + return "Silver"; + } + + static const char* const DarkGray() { + return "DarkGray"; + } + + static const char* const Gray() { + return "Gray"; + } + + static const char* const DimGray() { + return "DimGray"; + } + + static const char* const LightSlateGray() { + return "LightSlateGray"; + } + + static const char* const SlateGray() { + return "SlateGray"; + } + + static const char* const DarkSlateGray() { + return "DarkSlateGray"; + } + + static const char* const Black() { + return "Black"; + } + + }; + + template<> + struct basic_named_color { + static const wchar_t* const pink() { + return L"Pink"; + } + + static const wchar_t* const light_pink() { + return L"LightPink"; + } + + static const wchar_t* const hot_pink() { + return L"HotPink"; + } + + static const wchar_t* const deep_pink() { + return L"DeepPink"; + } + + static const wchar_t* const pale_violet_red() { + return L"PaleVioletRed"; + } + + static const wchar_t* const medium_violet_red() { + return L"MediumVioletRed"; + } + + static const wchar_t* const light_salmon() { + return L"LightSalmon"; + } + + static const wchar_t* const salmon() { + return L"Salmon"; + } + + static const wchar_t* const dark_salmon() { + return L"DarkSalmon"; + } + + static const wchar_t* const light_coral() { + return L"LightCoral"; + } + + static const wchar_t* const indian_red() { + return L"IndianRed"; + } + + static const wchar_t* const crimson() { + return L"Crimson"; + } + + static const wchar_t* const fire_brick() { + return L"FireBrick"; + } + + static const wchar_t* const dark_red() { + return L"DarkRed"; + } + + static const wchar_t* const red() { + return L"Red"; + } + + static const wchar_t* const orange_red() { + return L"OrangeRed"; + } + + static const wchar_t* const tomato() { + return L"Tomato"; + } + + static const wchar_t* const coral() { + return L"Coral"; + } + + static const wchar_t* const dark_orange() { + return L"DarkOrange"; + } + + static const wchar_t* const orange() { + return L"Orange"; + } + + static const wchar_t* const yellow() { + return L"Yellow"; + } + + static const wchar_t* const light_yellow() { + return L"LightYellow"; + } + + static const wchar_t* const lemon_chiffon() { + return L"LemonChiffon"; + } + + static const wchar_t* const light_goldenrod_yellow() { + return L"LightGoldenrodYellow"; + } + + static const wchar_t* const papaya_whip() { + return L"PapayaWhip"; + } + + static const wchar_t* const moccasin() { + return L"Moccasin"; + } + + static const wchar_t* const peach_puff() { + return L"PeachPuff"; + } + + static const wchar_t* const pale_goldenrod() { + return L"PaleGoldenrod"; + } + + static const wchar_t* const khaki() { + return L"Khaki"; + } + + static const wchar_t* const dark_khaki() { + return L"DarkKhaki"; + } + + static const wchar_t* const gold() { + return L"Gold"; + } + + static const wchar_t* const cornsilk() { + return L"Cornsilk"; + } + + static const wchar_t* const blanched_almond() { + return L"BlanchedAlmond"; + } + + static const wchar_t* const bisque() { + return L"Bisque"; + } + + static const wchar_t* const navajo_white() { + return L"NavajoWhite"; + } + + static const wchar_t* const wheat() { + return L"Wheat"; + } + + static const wchar_t* const burly_wood() { + return L"BurlyWood"; + } + + static const wchar_t* const tan() { + return L"Tan"; + } + + static const wchar_t* const rosy_brown() { + return L"RosyBrown"; + } + + static const wchar_t* const sandy_brown() { + return L"SandyBrown"; + } + + static const wchar_t* const goldenrod() { + return L"Goldenrod"; + } + + static const wchar_t* const dark_goldenrod() { + return L"DarkGoldenrod"; + } + + static const wchar_t* const peru() { + return L"Peru"; + } + + static const wchar_t* const chocolate() { + return L"Chocolate"; + } + + static const wchar_t* const saddle_brown() { + return L"SaddleBrown"; + } + + static const wchar_t* const sienna() { + return L"Sienna"; + } + + static const wchar_t* const brown() { + return L"Brown"; + } + + static const wchar_t* const maroon() { + return L"Maroon"; + } + + static const wchar_t* const dark_olive_green() { + return L"DarkOliveGreen"; + } + + static const wchar_t* const olive() { + return L"Olive"; + } + + static const wchar_t* const olive_drab() { + return L"OliveDrab"; + } + + static const wchar_t* const yellow_green() { + return L"YellowGreen"; + } + + static const wchar_t* const lime_green() { + return L"LimeGreen"; + } + + static const wchar_t* const lime() { + return L"Lime"; + } + + static const wchar_t* const lawn_green() { + return L"LawnGreen"; + } + + static const wchar_t* const chartreuse() { + return L"Chartreuse"; + } + + static const wchar_t* const green_yellow() { + return L"GreenYellow"; + } + + static const wchar_t* const spring_green() { + return L"SpringGreen"; + } + + static const wchar_t* const medium_spring_green() { + return L"MediumSpringGreen"; + } + + static const wchar_t* const light_green() { + return L"LightGreen"; + } + + static const wchar_t* const pale_green() { + return L"PaleGreen"; + } + + static const wchar_t* const dark_sea_green() { + return L"DarkSeaGreen"; + } + + static const wchar_t* const medium_sea_green() { + return L"MediumSeaGreen"; + } + + static const wchar_t* const sea_green() { + return L"SeaGreen"; + } + + static const wchar_t* const forest_green() { + return L"ForestGreen"; + } + + static const wchar_t* const green() { + return L"Green"; + } + + static const wchar_t* const dark_green() { + return L"DarkGreen"; + } + + static const wchar_t* const medium_aquamarine() { + return L"MediumAquamarine"; + } + + static const wchar_t* const aqua() { + return L"Aqua"; + } + + static const wchar_t* const cyan() { + return L"Cyan"; + } + + static const wchar_t* const light_cyan() { + return L"LightCyan"; + } + + static const wchar_t* const pale_turquoise() { + return L"PaleTurquoise"; + } + + static const wchar_t* const aquamarine() { + return L"Aquamarine"; + } + + static const wchar_t* const turquoise() { + return L"Turquoise"; + } + + static const wchar_t* const medium_turquoise() { + return L"MediumTurquoise"; + } + + static const wchar_t* const dark_turquoise() { + return L"DarkTurquoise"; + } + + static const wchar_t* const light_sea_green() { + return L"LightSeaGreen"; + } + + static const wchar_t* const cadet_blue() { + return L"CadetBlue"; + } + + static const wchar_t* const dark_cyan() { + return L"DarkCyan"; + } + + static const wchar_t* const teal() { + return L"Teal"; + } + + static const wchar_t* const light_steel_blue() { + return L"LightSteelBlue"; + } + + static const wchar_t* const powder_blue() { + return L"PowderBlue"; + } + + static const wchar_t* const light_blue() { + return L"LightBlue"; + } + + static const wchar_t* const sky_blue() { + return L"SkyBlue"; + } + + static const wchar_t* const light_sky_blue() { + return L"LightSkyBlue"; + } + + static const wchar_t* const deep_sky_blue() { + return L"DeepSkyBlue"; + } + + static const wchar_t* const dodger_blue() { + return L"DodgerBlue"; + } + + static const wchar_t* const cornflower_blue() { + return L"CornflowerBlue"; + } + + static const wchar_t* const steel_blue() { + return L"SteelBlue"; + } + + static const wchar_t* const royal_blue() { + return L"RoyalBlue"; + } + + static const wchar_t* const blue() { + return L"Blue"; + } + + static const wchar_t* const medium_blue() { + return L"MediumBlue"; + } + + static const wchar_t* const DarkBlue() { + return L"DarkBlue"; + } + + static const wchar_t* const Navy() { + return L"Navy"; + } + + static const wchar_t* const MidnightBlue() { + return L"MidnightBlue"; + } + + static const wchar_t* const Lavender() { + return L"Lavender"; + } + + static const wchar_t* const Thistle() { + return L"Thistle"; + } + + static const wchar_t* const Plum() { + return L"Plum"; + } + + static const wchar_t* const Violet() { + return L"Violet"; + } + + static const wchar_t* const Orchid() { + return L"Orchid"; + } + + static const wchar_t* const Fuchsia() { + return L"Fuchsia"; + } + + static const wchar_t* const Magenta() { + return L"Magenta"; + } + + static const wchar_t* const MediumOrchid() { + return L"MediumOrchid"; + } + + static const wchar_t* const MediumPurple() { + return L"MediumPurple"; + } + + static const wchar_t* const BlueViolet() { + return L"BlueViolet"; + } + + static const wchar_t* const DarkViolet() { + return L"DarkViolet"; + } + + static const wchar_t* const DarkOrchid() { + return L"DarkOrchid"; + } + + static const wchar_t* const DarkMagenta() { + return L"DarkMagenta"; + } + + static const wchar_t* const Purple() { + return L"Purple"; + } + + static const wchar_t* const Indigo() { + return L"Indigo"; + } + + static const wchar_t* const DarkSlateBlue() { + return L"DarkSlateBlue"; + } + + static const wchar_t* const SlateBlue() { + return L"SlateBlue"; + } + + static const wchar_t* const MediumSlateBlue() { + return L"MediumSlateBlue"; + } + + static const wchar_t* const White() { + return L"White"; + } + + static const wchar_t* const Snow() { + return L"Snow"; + } + + static const wchar_t* const Honeydew() { + return L"Honeydew"; + } + + static const wchar_t* const MintCream() { + return L"MintCream"; + } + + static const wchar_t* const Azure() { + return L"Azure"; + } + + static const wchar_t* const AliceBlue() { + return L"AliceBlue"; + } + + static const wchar_t* const GhostWhite() { + return L"GhostWhite"; + } + + static const wchar_t* const WhiteSmoke() { + return L"WhiteSmoke"; + } + + static const wchar_t* const Seashell() { + return L"Seashell"; + } + + static const wchar_t* const Beige() { + return L"Beige"; + } + + static const wchar_t* const OldLace() { + return L"OldLace"; + } + + static const wchar_t* const FloralWhite() { + return L"FloralWhite"; + } + + static const wchar_t* const Ivory() { + return L"Ivory"; + } + + static const wchar_t* const AntiqueWhite() { + return L"AntiqueWhite"; + } + + static const wchar_t* const Linen() { + return L"Linen"; + } + + static const wchar_t* const LavenderBlush() { + return L"LavenderBlush"; + } + + static const wchar_t* const MistyRose() { + return L"MistyRose"; + } + + static const wchar_t* const Gainsboro() { + return L"Gainsboro"; + } + + static const wchar_t* const LightGrey() { + return L"LightGrey"; + } + + static const wchar_t* const Silver() { + return L"Silver"; + } + + static const wchar_t* const DarkGray() { + return L"DarkGray"; + } + + static const wchar_t* const Gray() { + return L"Gray"; + } + + static const wchar_t* const DimGray() { + return L"DimGray"; + } + + static const wchar_t* const LightSlateGray() { + return L"LightSlateGray"; + } + + static const wchar_t* const SlateGray() { + return L"SlateGray"; + } + + static const wchar_t* const DarkSlateGray() { + return L"DarkSlateGray"; + } + + static const wchar_t* const Black() { + return L"Black"; + } + + }; + + // + template + struct basic_color_mapper { + typedef CharT char_type; + typedef std::basic_string string_type; + + typedef known_color known_color_type; + typedef basic_named_color named_color_type; + + typedef std::map string_color_map; + typedef std::map color_string_map; + + // Build a name -> constant map + static const string_color_map &get_string_to_color_map() { + static string_color_map sm_; // name -> value + if (sm_.empty()) { + sm_.insert(std::make_pair(to_lowercase(named_color_type::pink()), known_color::pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_pink()), known_color::light_pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::hot_pink()), known_color::hot_pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::deep_pink()), known_color::deep_pink)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_violet_red()), known_color::pale_violet_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_violet_red()), known_color::medium_violet_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_salmon()), known_color::light_salmon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::salmon()), known_color::salmon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_salmon()), known_color::dark_salmon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_coral()), known_color::light_coral)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::indian_red()), known_color::indian_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::crimson()), known_color::crimson)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::fire_brick()), known_color::fire_brick)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_red()), known_color::dark_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::red()), known_color::red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::orange_red()), known_color::orange_red)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::tomato()), known_color::tomato)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::coral()), known_color::coral)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_orange()), known_color::dark_orange)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::orange()), known_color::orange)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::yellow()), known_color::yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_yellow()), known_color::light_yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lemon_chiffon()), known_color::lemon_chiffon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_goldenrod_yellow()), known_color::light_goldenrod_yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::papaya_whip()), known_color::papaya_whip)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::moccasin()), known_color::moccasin)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::peach_puff()), known_color::peach_puff)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_goldenrod()), known_color::pale_goldenrod)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::khaki()), known_color::khaki)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_khaki()), known_color::dark_khaki)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::gold()), known_color::gold)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cornsilk()), known_color::cornsilk)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::blanched_almond()), known_color::blanched_almond)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::bisque()), known_color::bisque)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::navajo_white()), known_color::navajo_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::wheat()), known_color::wheat)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::burly_wood()), known_color::burly_wood)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::tan()), known_color::tan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::rosy_brown()), known_color::rosy_brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sandy_brown()), known_color::sandy_brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::goldenrod()), known_color::goldenrod)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_goldenrod()), known_color::dark_goldenrod)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::peru()), known_color::peru)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::chocolate()), known_color::chocolate)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::saddle_brown()), known_color::saddle_brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sienna()), known_color::sienna)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::brown()), known_color::brown)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::maroon()), known_color::maroon)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_olive_green()), known_color::dark_olive_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::olive()), known_color::olive)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::olive_drab()), known_color::olive_drab)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::yellow_green()), known_color::yellow_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lime_green()), known_color::lime_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lime()), known_color::lime)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lawn_green()), known_color::lawn_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::chartreuse()), known_color::chartreuse)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::green_yellow()), known_color::green_yellow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::spring_green()), known_color::spring_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_spring_green()), known_color::medium_spring_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_green()), known_color::light_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_green()), known_color::pale_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_sea_green()), known_color::dark_sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_sea_green()), known_color::medium_sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sea_green()), known_color::sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::forest_green()), known_color::forest_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::green()), known_color::green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_green()), known_color::dark_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_aquamarine()), known_color::medium_aquamarine)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::aqua()), known_color::aqua)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cyan()), known_color::cyan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_cyan()), known_color::light_cyan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::pale_turquoise()), known_color::pale_turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::aquamarine()), known_color::aquamarine)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::turquoise()), known_color::turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_turquoise()), known_color::medium_turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_turquoise()), known_color::dark_turquoise)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_sea_green()), known_color::light_sea_green)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cadet_blue()), known_color::cadet_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_cyan()), known_color::dark_cyan)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::teal()), known_color::teal)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_steel_blue()), known_color::light_steel_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::powder_blue()), known_color::powder_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_blue()), known_color::light_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::sky_blue()), known_color::sky_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_sky_blue()), known_color::light_sky_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::deep_sky_blue()), known_color::deep_sky_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dodger_blue()), known_color::dodger_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::cornflower_blue()), known_color::cornflower_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::steel_blue()), known_color::steel_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::royal_blue()), known_color::royal_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::blue()), known_color::blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_blue()), known_color::medium_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_blue()), known_color::dark_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::navy()), known_color::navy)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::midnight_blue()), known_color::midnight_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lavender()), known_color::lavender)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::thistle()), known_color::thistle)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::plum()), known_color::plum)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::violet()), known_color::violet)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::orchid()), known_color::orchid)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::fuchsia()), known_color::fuchsia)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::magenta()), known_color::magenta)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_orchid()), known_color::medium_orchid)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_purple()), known_color::medium_purple)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::blue_violet()), known_color::blue_violet)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_violet()), known_color::dark_violet)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_orchid()), known_color::dark_orchid)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_magenta()), known_color::dark_magenta)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::purple()), known_color::purple)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::indigo()), known_color::indigo)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_slate_blue()), known_color::dark_slate_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::slate_blue()), known_color::slate_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::medium_slate_blue()), known_color::medium_slate_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::white()), known_color::white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::snow()), known_color::snow)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::honeydew()), known_color::honeydew)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::mint_cream()), known_color::mint_cream)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::azure()), known_color::azure)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::alice_blue()), known_color::alice_blue)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::ghost_white()), known_color::ghost_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::white_smoke()), known_color::white_smoke)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::seashell()), known_color::seashell)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::beige()), known_color::beige)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::old_lace()), known_color::old_lace)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::floral_white()), known_color::floral_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::ivory()), known_color::ivory)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::antique_white()), known_color::antique_white)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::linen()), known_color::linen)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::lavender_blush()), known_color::lavender_blush)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::misty_rose()), known_color::misty_rose)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::gainsboro()), known_color::gainsboro)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_grey()), known_color::light_grey)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::silver()), known_color::silver)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_gray()), known_color::dark_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::gray()), known_color::gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dim_gray()), known_color::dim_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::light_slate_gray()), known_color::light_slate_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::slate_gray()), known_color::slate_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::dark_slate_gray()), known_color::dark_slate_gray)); + sm_.insert(std::make_pair(to_lowercase(named_color_type::black()), known_color::black)); + } + + return sm_; + } + + // Build a name -> constant map + static const color_string_map &get_color_to_string_map() { + static color_string_map vm_; // value -> name + + if (vm_.empty()) { + vm_.insert(std::make_pair(value_of(known_color::pink), named_color_type::pink())); + vm_.insert(std::make_pair(value_of(known_color::light_pink), named_color_type::light_pink())); + vm_.insert(std::make_pair(value_of(known_color::hot_pink), named_color_type::hot_pink())); + vm_.insert(std::make_pair(value_of(known_color::deep_pink), named_color_type::deep_pink())); + vm_.insert(std::make_pair(value_of(known_color::pale_violet_red), named_color_type::pale_violet_red())); + vm_.insert(std::make_pair(value_of(known_color::medium_violet_red), named_color_type::medium_violet_red())); + vm_.insert(std::make_pair(value_of(known_color::light_salmon), named_color_type::light_salmon())); + vm_.insert(std::make_pair(value_of(known_color::salmon), named_color_type::salmon())); + vm_.insert(std::make_pair(value_of(known_color::dark_salmon), named_color_type::dark_salmon())); + vm_.insert(std::make_pair(value_of(known_color::light_coral), named_color_type::light_coral())); + vm_.insert(std::make_pair(value_of(known_color::indian_red), named_color_type::indian_red())); + vm_.insert(std::make_pair(value_of(known_color::crimson), named_color_type::crimson())); + vm_.insert(std::make_pair(value_of(known_color::fire_brick), named_color_type::fire_brick())); + vm_.insert(std::make_pair(value_of(known_color::dark_red), named_color_type::dark_red())); + vm_.insert(std::make_pair(value_of(known_color::red), named_color_type::red())); + vm_.insert(std::make_pair(value_of(known_color::orange_red), named_color_type::orange_red())); + vm_.insert(std::make_pair(value_of(known_color::tomato), named_color_type::tomato())); + vm_.insert(std::make_pair(value_of(known_color::coral), named_color_type::coral())); + vm_.insert(std::make_pair(value_of(known_color::dark_orange), named_color_type::dark_orange())); + vm_.insert(std::make_pair(value_of(known_color::orange), named_color_type::orange())); + vm_.insert(std::make_pair(value_of(known_color::yellow), named_color_type::yellow())); + vm_.insert(std::make_pair(value_of(known_color::light_yellow), named_color_type::light_yellow())); + vm_.insert(std::make_pair(value_of(known_color::lemon_chiffon), named_color_type::lemon_chiffon())); + vm_.insert(std::make_pair(value_of(known_color::light_goldenrod_yellow), named_color_type::light_goldenrod_yellow())); + vm_.insert(std::make_pair(value_of(known_color::papaya_whip), named_color_type::papaya_whip())); + vm_.insert(std::make_pair(value_of(known_color::moccasin), named_color_type::moccasin())); + vm_.insert(std::make_pair(value_of(known_color::peach_puff), named_color_type::peach_puff())); + vm_.insert(std::make_pair(value_of(known_color::pale_goldenrod), named_color_type::pale_goldenrod())); + vm_.insert(std::make_pair(value_of(known_color::khaki), named_color_type::khaki())); + vm_.insert(std::make_pair(value_of(known_color::dark_khaki), named_color_type::dark_khaki())); + vm_.insert(std::make_pair(value_of(known_color::gold), named_color_type::gold())); + vm_.insert(std::make_pair(value_of(known_color::cornsilk), named_color_type::cornsilk())); + vm_.insert(std::make_pair(value_of(known_color::blanched_almond), named_color_type::blanched_almond())); + vm_.insert(std::make_pair(value_of(known_color::bisque), named_color_type::bisque())); + vm_.insert(std::make_pair(value_of(known_color::navajo_white), named_color_type::navajo_white())); + vm_.insert(std::make_pair(value_of(known_color::wheat), named_color_type::wheat())); + vm_.insert(std::make_pair(value_of(known_color::burly_wood), named_color_type::burly_wood())); + vm_.insert(std::make_pair(value_of(known_color::tan), named_color_type::tan())); + vm_.insert(std::make_pair(value_of(known_color::rosy_brown), named_color_type::rosy_brown())); + vm_.insert(std::make_pair(value_of(known_color::sandy_brown), named_color_type::sandy_brown())); + vm_.insert(std::make_pair(value_of(known_color::goldenrod), named_color_type::goldenrod())); + vm_.insert(std::make_pair(value_of(known_color::dark_goldenrod), named_color_type::dark_goldenrod())); + vm_.insert(std::make_pair(value_of(known_color::peru), named_color_type::peru())); + vm_.insert(std::make_pair(value_of(known_color::chocolate), named_color_type::chocolate())); + vm_.insert(std::make_pair(value_of(known_color::saddle_brown), named_color_type::saddle_brown())); + vm_.insert(std::make_pair(value_of(known_color::sienna), named_color_type::sienna())); + vm_.insert(std::make_pair(value_of(known_color::brown), named_color_type::brown())); + vm_.insert(std::make_pair(value_of(known_color::maroon), named_color_type::maroon())); + vm_.insert(std::make_pair(value_of(known_color::dark_olive_green), named_color_type::dark_olive_green())); + vm_.insert(std::make_pair(value_of(known_color::olive), named_color_type::olive())); + vm_.insert(std::make_pair(value_of(known_color::olive_drab), named_color_type::olive_drab())); + vm_.insert(std::make_pair(value_of(known_color::yellow_green), named_color_type::yellow_green())); + vm_.insert(std::make_pair(value_of(known_color::lime_green), named_color_type::lime_green())); + vm_.insert(std::make_pair(value_of(known_color::lime), named_color_type::lime())); + vm_.insert(std::make_pair(value_of(known_color::lawn_green), named_color_type::lawn_green())); + vm_.insert(std::make_pair(value_of(known_color::chartreuse), named_color_type::chartreuse())); + vm_.insert(std::make_pair(value_of(known_color::green_yellow), named_color_type::green_yellow())); + vm_.insert(std::make_pair(value_of(known_color::spring_green), named_color_type::spring_green())); + vm_.insert(std::make_pair(value_of(known_color::medium_spring_green), named_color_type::medium_spring_green())); + vm_.insert(std::make_pair(value_of(known_color::light_green), named_color_type::light_green())); + vm_.insert(std::make_pair(value_of(known_color::pale_green), named_color_type::pale_green())); + vm_.insert(std::make_pair(value_of(known_color::dark_sea_green), named_color_type::dark_sea_green())); + vm_.insert(std::make_pair(value_of(known_color::medium_sea_green), named_color_type::medium_sea_green())); + vm_.insert(std::make_pair(value_of(known_color::sea_green), named_color_type::sea_green())); + vm_.insert(std::make_pair(value_of(known_color::forest_green), named_color_type::forest_green())); + vm_.insert(std::make_pair(value_of(known_color::green), named_color_type::green())); + vm_.insert(std::make_pair(value_of(known_color::dark_green), named_color_type::dark_green())); + vm_.insert(std::make_pair(value_of(known_color::medium_aquamarine), named_color_type::medium_aquamarine())); + vm_.insert(std::make_pair(value_of(known_color::aqua), named_color_type::aqua())); + vm_.insert(std::make_pair(value_of(known_color::cyan), named_color_type::cyan())); + vm_.insert(std::make_pair(value_of(known_color::light_cyan), named_color_type::light_cyan())); + vm_.insert(std::make_pair(value_of(known_color::pale_turquoise), named_color_type::pale_turquoise())); + vm_.insert(std::make_pair(value_of(known_color::aquamarine), named_color_type::aquamarine())); + vm_.insert(std::make_pair(value_of(known_color::turquoise), named_color_type::turquoise())); + vm_.insert(std::make_pair(value_of(known_color::medium_turquoise), named_color_type::medium_turquoise())); + vm_.insert(std::make_pair(value_of(known_color::dark_turquoise), named_color_type::dark_turquoise())); + vm_.insert(std::make_pair(value_of(known_color::light_sea_green), named_color_type::light_sea_green())); + vm_.insert(std::make_pair(value_of(known_color::cadet_blue), named_color_type::cadet_blue())); + vm_.insert(std::make_pair(value_of(known_color::dark_cyan), named_color_type::dark_cyan())); + vm_.insert(std::make_pair(value_of(known_color::teal), named_color_type::teal())); + vm_.insert(std::make_pair(value_of(known_color::light_steel_blue), named_color_type::light_steel_blue())); + vm_.insert(std::make_pair(value_of(known_color::powder_blue), named_color_type::powder_blue())); + vm_.insert(std::make_pair(value_of(known_color::light_blue), named_color_type::light_blue())); + vm_.insert(std::make_pair(value_of(known_color::sky_blue), named_color_type::sky_blue())); + vm_.insert(std::make_pair(value_of(known_color::light_sky_blue), named_color_type::light_sky_blue())); + vm_.insert(std::make_pair(value_of(known_color::deep_sky_blue), named_color_type::deep_sky_blue())); + vm_.insert(std::make_pair(value_of(known_color::dodger_blue), named_color_type::dodger_blue())); + vm_.insert(std::make_pair(value_of(known_color::cornflower_blue), named_color_type::cornflower_blue())); + vm_.insert(std::make_pair(value_of(known_color::steel_blue), named_color_type::steel_blue())); + vm_.insert(std::make_pair(value_of(known_color::royal_blue), named_color_type::royal_blue())); + vm_.insert(std::make_pair(value_of(known_color::blue), named_color_type::blue())); + vm_.insert(std::make_pair(value_of(known_color::medium_blue), named_color_type::medium_blue())); + vm_.insert(std::make_pair(value_of(known_color::dark_blue), named_color_type::dark_blue())); + vm_.insert(std::make_pair(value_of(known_color::navy), named_color_type::navy())); + vm_.insert(std::make_pair(value_of(known_color::midnight_blue), named_color_type::midnight_blue())); + vm_.insert(std::make_pair(value_of(known_color::lavender), named_color_type::lavender())); + vm_.insert(std::make_pair(value_of(known_color::thistle), named_color_type::thistle())); + vm_.insert(std::make_pair(value_of(known_color::plum), named_color_type::plum())); + vm_.insert(std::make_pair(value_of(known_color::violet), named_color_type::violet())); + vm_.insert(std::make_pair(value_of(known_color::orchid), named_color_type::orchid())); + vm_.insert(std::make_pair(value_of(known_color::fuchsia), named_color_type::fuchsia())); + vm_.insert(std::make_pair(value_of(known_color::magenta), named_color_type::magenta())); + vm_.insert(std::make_pair(value_of(known_color::medium_orchid), named_color_type::medium_orchid())); + vm_.insert(std::make_pair(value_of(known_color::medium_purple), named_color_type::medium_purple())); + vm_.insert(std::make_pair(value_of(known_color::blue_violet), named_color_type::blue_violet())); + vm_.insert(std::make_pair(value_of(known_color::dark_violet), named_color_type::dark_violet())); + vm_.insert(std::make_pair(value_of(known_color::dark_orchid), named_color_type::dark_orchid())); + vm_.insert(std::make_pair(value_of(known_color::dark_magenta), named_color_type::dark_magenta())); + vm_.insert(std::make_pair(value_of(known_color::purple), named_color_type::purple())); + vm_.insert(std::make_pair(value_of(known_color::indigo), named_color_type::indigo())); + vm_.insert(std::make_pair(value_of(known_color::dark_slate_blue), named_color_type::dark_slate_blue())); + vm_.insert(std::make_pair(value_of(known_color::slate_blue), named_color_type::slate_blue())); + vm_.insert(std::make_pair(value_of(known_color::medium_slate_blue), named_color_type::medium_slate_blue())); + vm_.insert(std::make_pair(value_of(known_color::white), named_color_type::white())); + vm_.insert(std::make_pair(value_of(known_color::snow), named_color_type::snow())); + vm_.insert(std::make_pair(value_of(known_color::honeydew), named_color_type::honeydew())); + vm_.insert(std::make_pair(value_of(known_color::mint_cream), named_color_type::mint_cream())); + vm_.insert(std::make_pair(value_of(known_color::azure), named_color_type::azure())); + vm_.insert(std::make_pair(value_of(known_color::alice_blue), named_color_type::alice_blue())); + vm_.insert(std::make_pair(value_of(known_color::ghost_white), named_color_type::ghost_white())); + vm_.insert(std::make_pair(value_of(known_color::white_smoke), named_color_type::white_smoke())); + vm_.insert(std::make_pair(value_of(known_color::seashell), named_color_type::seashell())); + vm_.insert(std::make_pair(value_of(known_color::beige), named_color_type::beige())); + vm_.insert(std::make_pair(value_of(known_color::old_lace), named_color_type::old_lace())); + vm_.insert(std::make_pair(value_of(known_color::floral_white), named_color_type::floral_white())); + vm_.insert(std::make_pair(value_of(known_color::ivory), named_color_type::ivory())); + vm_.insert(std::make_pair(value_of(known_color::antique_white), named_color_type::antique_white())); + vm_.insert(std::make_pair(value_of(known_color::linen), named_color_type::linen())); + vm_.insert(std::make_pair(value_of(known_color::lavender_blush), named_color_type::lavender_blush())); + vm_.insert(std::make_pair(value_of(known_color::misty_rose), named_color_type::misty_rose())); + vm_.insert(std::make_pair(value_of(known_color::gainsboro), named_color_type::gainsboro())); + vm_.insert(std::make_pair(value_of(known_color::light_grey), named_color_type::light_grey())); + vm_.insert(std::make_pair(value_of(known_color::silver), named_color_type::silver())); + vm_.insert(std::make_pair(value_of(known_color::dark_gray), named_color_type::dark_gray())); + vm_.insert(std::make_pair(value_of(known_color::gray), named_color_type::gray())); + vm_.insert(std::make_pair(value_of(known_color::dim_gray), named_color_type::dim_gray())); + vm_.insert(std::make_pair(value_of(known_color::light_slate_gray), named_color_type::light_slate_gray())); + vm_.insert(std::make_pair(value_of(known_color::slate_gray), named_color_type::slate_gray())); + vm_.insert(std::make_pair(value_of(known_color::dark_slate_gray), named_color_type::dark_slate_gray())); + vm_.insert(std::make_pair(value_of(known_color::black), named_color_type::black())); + } + + return vm_; + } + + private: + basic_color_mapper() = delete; + ~basic_color_mapper() = delete; + basic_color_mapper(const basic_color_mapper&) = delete; + basic_color_mapper& operator =(const basic_color_mapper&) = delete; + + private: + // Convert string to lower case + static string_type to_lowercase(const string_type &str) { + static std::locale loc; + std::string result; + result.reserve(str.size()); + + for (auto elem : str) + result += std::tolower(elem, loc); + + return result; + } + }; + + } +} diff --git a/extern/include/cpp-colors/pixel_format.h b/extern/include/cpp-colors/pixel_format.h new file mode 100644 index 000000000..84b2b54a4 --- /dev/null +++ b/extern/include/cpp-colors/pixel_format.h @@ -0,0 +1,365 @@ +#pragma once + +#include +#include + + +namespace colors { + + // PixelFormat + namespace pixel_format { + enum pixel_format { + unknown, // Surface format is unknown + + indexed1, // 1-bit Indexed. Specifies that the pixel format is 1 bit per pixel and that it uses indexed color. The color table therefore has two colors in it. + indexed4, // 4-bit Indexed. Specifies that the format is 4 bits per pixel, indexed. + indexed8, // 8-bit Indexed. Specifies that the format is 8 bits per pixel, indexed. The color table therefore has 256 colors in it. + gray16, // 16-bit Grayscale. The pixel format is 16 bits per pixel. The color information specifies 65536 shades of gray. + + bgr24, // 24-bit RGB pixel format with 8 bits per channel. r8g8b8 + color, /* bgra32 */ // 32-bit ARGB pixel format with alpha, using 8 bits per channel. a8r8g8b8 + bgr32, // 32-bit RGB pixel format, where 8 bits are reserved for each color. x8r8g8b8 + bgr565, // 16-bit RGB pixel format with 5 bits for red, 6 bits for green, and 5 bits for blue. r5g6b5 + bgr555, // 16-bit pixel format where 5 bits are reserved for each color. x1r5g5b5 + bgra5551, // 16-bit pixel format where 5 bits are reserved for each color and 1 bit is reserved for alpha. a1r5g5b5 + rgba32, // 32-bit ABGR pixel format with alpha, using 8 bits per channel. a8b8g8r8 + rgb32, // 32-bit BGR pixel format, where 8 bits are reserved for each color. x8b8g8r8 + + bgrap32, // PARGB32. Specifies that the format is 32 bits per pixel; 8 bits each are used for the alpha, red, green, and blue components. The red, green, and blue components are premultiplied, according to the alpha component. + bgrap64, // PARGB64. Specifies that the format is 64 bits per pixel; 16 bits each are used for the alpha, red, green, and blue components. The red, green, and blue components are premultiplied according to the alpha component. + + bgr48, // 48-bit RGB. Specifies that the format is 48 bits per pixel; 16 bits each are used for the red, green, and blue components. + bgra64, // 64-bit ARGB. Specifies that the format is 64 bits per pixel; 16 bits each are used for the alpha, red, green, and blue components. + }; + } + + namespace pixel_format_flags { + enum pixel_format_flags { + has_alpha = 0x00000001, // Format has the Alpha channel + compressed = 0x00000002, // Compressed format + floating = 0x00000004, // Floating point format + depth = 0x00000008, // Depth format (depth textures) + }; + } + + + // Traits of a pixel format + template + struct pixel_traits; + + + // R8G8B8 (Bgr24) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint32_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::bgr24; + + // Tha name of the format + static const char* const name() { return "R8G8B8 (Bgr24)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 3; + + // A combination of one or more flags + static const uint32_t flags = 0; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 8; + static const uint8_t bits_g = 8; + static const uint8_t bits_b = 8; + static const uint8_t bits_a = 0; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0x00FF0000; + static const pixel_type mask_g = 0x0000FF00; + static const pixel_type mask_b = 0x000000FF; + static const pixel_type mask_a = 0x00000000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 16; + static const pixel_type shift_g = 8; + static const pixel_type shift_b = 0; + static const pixel_type shift_a = 0; + }; + + + // A8R8G8B8 (Color; Bgra32) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint32_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::color; + + // Tha name of the format + static const char* const name() { return "A8R8G8B8 (Bgra32)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 4; + + // A combination of one or more flags + static const uint32_t flags = pixel_format_flags::has_alpha; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 8; + static const uint8_t bits_g = 8; + static const uint8_t bits_b = 8; + static const uint8_t bits_a = 8; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0x00FF0000; + static const pixel_type mask_g = 0x0000FF00; + static const pixel_type mask_b = 0x000000FF; + static const pixel_type mask_a = 0xFF000000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 16; + static const pixel_type shift_g = 8; + static const pixel_type shift_b = 0; + static const pixel_type shift_a = 24; + }; + + + // X8R8G8B8 (Bgr32) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint32_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::bgr32; + + // Tha name of the format + static const char* const name() { return "X8R8G8B8 (Bgr32)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 4; + + // A combination of one or more flags + static const uint32_t flags = 0; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 8; + static const uint8_t bits_g = 8; + static const uint8_t bits_b = 8; + static const uint8_t bits_a = 0; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0x00FF0000; + static const pixel_type mask_g = 0x0000FF00; + static const pixel_type mask_b = 0x000000FF; + static const pixel_type mask_a = 0x00000000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 16; + static const pixel_type shift_g = 8; + static const pixel_type shift_b = 0; + static const pixel_type shift_a = 0; + }; + + + // A8B8G8R8 (Rgba32) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint32_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::rgba32; + + // Tha name of the format + static const char* const name() { return "A8B8G8R8 (Rgba32)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 4; + + // A combination of one or more flags + static const uint32_t flags = pixel_format_flags::has_alpha; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 8; + static const uint8_t bits_g = 8; + static const uint8_t bits_b = 8; + static const uint8_t bits_a = 8; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0x000000FF; + static const pixel_type mask_g = 0x0000FF00; + static const pixel_type mask_b = 0x00FF0000; + static const pixel_type mask_a = 0xFF000000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 0; + static const pixel_type shift_g = 8; + static const pixel_type shift_b = 16; + static const pixel_type shift_a = 24; + }; + + + // X8B8G8R8 (Rgb32) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint32_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::rgb32; + + // Tha name of the format + static const char* const name() { return "X8R8G8B8 (Rgb32)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 4; + + // A combination of one or more flags + static const uint32_t flags = 0; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 8; + static const uint8_t bits_g = 8; + static const uint8_t bits_b = 8; + static const uint8_t bits_a = 8; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0x000000FF; + static const pixel_type mask_g = 0x0000FF00; + static const pixel_type mask_b = 0x00FF0000; + static const pixel_type mask_a = 0x00000000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 0; + static const pixel_type shift_g = 8; + static const pixel_type shift_b = 16; + static const pixel_type shift_a = 0; + }; + + + // R5G6B5 (Bgr565) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint16_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::bgr565; + + // Tha name of the format + static const char* const name() { return "R5G6B5 (Bgr565)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 2; + + // A combination of one or more flags + static const uint32_t flags = 0; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 5; + static const uint8_t bits_g = 6; + static const uint8_t bits_b = 5; + static const uint8_t bits_a = 0; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0xF800; + static const pixel_type mask_g = 0x07E0; + static const pixel_type mask_b = 0x001F; + static const pixel_type mask_a = 0x0000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 11; + static const pixel_type shift_g = 5; + static const pixel_type shift_b = 0; + static const pixel_type shift_a = 0; + }; + + + // X1R5G5B5 (Bgr555) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint16_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::bgr555; + + // Tha name of the format + static const char* const name() { return "X1R5G5B5 (Bgr555)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 2; + + // A combination of one or more flags + static const uint32_t flags = 0; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 5; + static const uint8_t bits_g = 5; + static const uint8_t bits_b = 5; + static const uint8_t bits_a = 0; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0x7C00; + static const pixel_type mask_g = 0x03E0; + static const pixel_type mask_b = 0x001F; + static const pixel_type mask_a = 0x0000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 10; + static const pixel_type shift_g = 5; + static const pixel_type shift_b = 0; + static const pixel_type shift_a = 0; + }; + + + // A1R5G5B5 (Bgra5551) + template <> + struct pixel_traits { + typedef uint8_t channel_type; // type to fit the channel value + typedef uint16_t pixel_type; // Compact representation of a pixel + + // Format that traits describe + static const pixel_format::pixel_format format = pixel_format::bgra5551; + + // Tha name of the format + static const char* const name() { return "A1R5G5B5 (Bgra5551)"; } + + // The number of bytes one element takes (size per element) + static const uint8_t size = 2; + + // A combination of one or more flags + static const uint32_t flags = pixel_format_flags::has_alpha; + + // Bits per component (R, G, B, A) + static const uint8_t bits_r = 5; + static const uint8_t bits_g = 5; + static const uint8_t bits_b = 5; + static const uint8_t bits_a = 1; + + // Mask of a component (R, G, B, A) + static const pixel_type mask_r = 0x7C00; + static const pixel_type mask_g = 0x03E0; + static const pixel_type mask_b = 0x001F; + static const pixel_type mask_a = 0x8000; + + // Shift of the component (R, G, B, A) + static const pixel_type shift_r = 10; + static const pixel_type shift_g = 5; + static const pixel_type shift_b = 0; + static const pixel_type shift_a = 15; + }; + + + typedef pixel_traits bgra32_traits; + typedef pixel_traits bgr32_traits; + typedef pixel_traits rgba32_traits; + typedef pixel_traits rgb32_traits; + typedef pixel_traits bgr24_traits; + typedef pixel_traits bgr565_traits; + typedef pixel_traits bgr555_traits; + typedef pixel_traits bgra5551_traits; + + +} // namespace colors diff --git a/extern/include/cpp-colors/pixel_utils.h b/extern/include/cpp-colors/pixel_utils.h new file mode 100644 index 000000000..4c9ddf397 --- /dev/null +++ b/extern/include/cpp-colors/pixel_utils.h @@ -0,0 +1,540 @@ +#pragma once + +#include +#include +#include "pixel_format.h" +#include "color_error.h" + +namespace colors { + + namespace detail { + + // Returns TRUE if the format has alpha channel + template + inline bool has_alpha() { + return ((pixel_traits::flags & pixel_format_flags::has_alpha) > 0); + } + + // Returns TRUE of the format compressed + template + inline bool is_compressed() { + return ((pixel_traits::flags & pixel_format_flags::compressed) > 0); + } + + } // namespace detail + + + + // Stride is the count of bytes between scanlines. + // Generally speaking, the bits that make up the pixels of a bitmap are packed into rows. + // A single row should be long enough to store one row of the bitmap's pixels. + // The stride is the length of a row measured in bytes, rounded up to the nearest DWORD (4 bytes). + // This allows bitmaps with fewer than 32 bits per pixel (bpp) to consume less memory while still + // providing good performance. You can use the following function to calculate the stride for a given bitmap. + // + // width - image width in pixels + // bit_count - bits per pixel + // + inline uint32_t get_stride(uint32_t width, uint32_t bit_count) { + assert(bit_count % 8 == 0); + + const uint32_t byteCount = bit_count / 8; + const uint32_t stride = (width * byteCount + 3) & ~3; + + assert(0 == stride % 4); + + return stride; + } + + + // converts Value from the Source to Destination bits representstion + template + inline T convert_bpc(T value, U srcBPC, U dstBPC) { + T result; + if (dstBPC < srcBPC) { + result = static_cast(value >> (srcBPC - dstBPC)); + } + else if (srcBPC < dstBPC) { + if (value == 0) { + result = static_cast(0); + } + else if (value == ((static_cast(1) << srcBPC) - 1)) { + result = static_cast((1 << dstBPC) - 1); + } + else { + result = static_cast(value * (1 << dstBPC) / ((1 << srcBPC) - 1)); + } + } + else { + result = static_cast(value); + } + + return result; + } + + + // Gets the size of a pixel (in bytes) for a given pixel format + inline uint32_t get_pixel_size(pixel_format::pixel_format pft) { + switch (pft) + { + case pixel_format::color: + return bgra32_traits::size; + + case pixel_format::bgr32: + return bgr32_traits::size; + + case pixel_format::rgba32: + return rgba32_traits::size; + + case pixel_format::rgb32: + return rgb32_traits::size; + + case pixel_format::bgr24: + return bgr24_traits::size; + + case pixel_format::bgr565: + return bgr565_traits::size; + + case pixel_format::bgr555: + return bgr555_traits::size; + + case pixel_format::bgra5551: + return bgra5551_traits::size; + + default: + assert(false && "Cannot get the size of a pixel."); + throw color_error("Cannot get the size of a pixel."); + } + } + + + inline unsigned int get_alpha_mask(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::mask_a; + + case pixel_format::bgr32: + return bgr32_traits::mask_a; + + case pixel_format::rgba32: + return rgba32_traits::mask_a; + + case pixel_format::rgb32: + return rgb32_traits::mask_a; + + case pixel_format::bgr24: + return bgr24_traits::mask_a; + + case pixel_format::bgr565: + return bgr565_traits::mask_a; + + case pixel_format::bgr555: + return bgr555_traits::mask_a; + + case pixel_format::bgra5551: + return bgra5551_traits::mask_a; + + default: + assert(false && "Cannot retreive the pixel alpha mask."); + throw color_error("Cannot retreive the pixel alpha mask."); + } + } + + + inline unsigned int get_red_mask(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::mask_r; + + case pixel_format::bgr32: + return bgr32_traits::mask_r; + + case pixel_format::rgba32: + return rgba32_traits::mask_r; + + case pixel_format::rgb32: + return rgb32_traits::mask_r; + + case pixel_format::bgr24: + return bgr24_traits::mask_r; + + case pixel_format::bgr565: + return bgr565_traits::mask_r; + + case pixel_format::bgr555: + return bgr555_traits::mask_r; + + case pixel_format::bgra5551: + return bgra5551_traits::mask_r; + + default: + assert(false && "Cannot retreive the pixel red mask."); + throw color_error("Cannot retreive the pixel red mask."); + } + } + + + inline unsigned int get_green_mask(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::mask_g; + + case pixel_format::bgr32: + return bgr32_traits::mask_g; + + case pixel_format::rgba32: + return rgba32_traits::mask_g; + + case pixel_format::rgb32: + return rgb32_traits::mask_g; + + case pixel_format::bgr24: + return bgr24_traits::mask_g; + + case pixel_format::bgr565: + return bgr565_traits::mask_g; + + case pixel_format::bgr555: + return bgr555_traits::mask_g; + + case pixel_format::bgra5551: + return bgra5551_traits::mask_g; + + default: + assert(false && "Cannot retreive the pixel green mask."); + throw color_error("Cannot retreive the pixel green mask."); + } + } + + + inline unsigned int get_blue_mask(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::mask_b; + + case pixel_format::bgr32: + return bgr32_traits::mask_b; + + case pixel_format::rgba32: + return rgba32_traits::mask_b; + + case pixel_format::rgb32: + return rgb32_traits::mask_b; + + case pixel_format::bgr24: + return bgr24_traits::mask_b; + + case pixel_format::bgr565: + return bgr565_traits::mask_b; + + case pixel_format::bgr555: + return bgr555_traits::mask_b; + + case pixel_format::bgra5551: + return bgra5551_traits::mask_b; + + default: + assert(false && "Cannot retreive the pixel blue mask."); + throw color_error("Cannot retreive the pixel blue mask."); + } + } + + + inline unsigned int get_alpha_shift(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::shift_a; + + case pixel_format::bgr32: + return bgr32_traits::shift_a; + + case pixel_format::rgba32: + return rgba32_traits::shift_a; + + case pixel_format::rgb32: + return rgb32_traits::shift_a; + + case pixel_format::bgr24: + return bgr24_traits::shift_a; + + case pixel_format::bgr565: + return bgr565_traits::shift_a; + + case pixel_format::bgr555: + return bgr555_traits::shift_a; + + case pixel_format::bgra5551: + return bgra5551_traits::shift_a; + + default: + assert(false && "Cannot retreive the pixel alpha shift."); + throw color_error("Cannot retreive the pixel alpha shift."); + } + } + + + inline unsigned int get_red_shift(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::shift_r; + + case pixel_format::bgr32: + return bgr32_traits::shift_r; + + case pixel_format::rgba32: + return rgba32_traits::shift_r; + + case pixel_format::rgb32: + return rgb32_traits::shift_r; + + case pixel_format::bgr24: + return bgr24_traits::shift_r; + + case pixel_format::bgr565: + return bgr565_traits::shift_r; + + case pixel_format::bgr555: + return bgr555_traits::shift_r; + + case pixel_format::bgra5551: + return bgra5551_traits::shift_r; + + default: + assert(false && "Cannot retreive the pixel red shift."); + throw color_error("Cannot retreive the pixel red shift."); + } + } + + + inline unsigned int get_green_shift(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::shift_g; + + case pixel_format::bgr32: + return bgr32_traits::shift_g; + + case pixel_format::rgba32: + return rgba32_traits::shift_g; + + case pixel_format::rgb32: + return rgb32_traits::shift_g; + + case pixel_format::bgr24: + return bgr24_traits::shift_g; + + case pixel_format::bgr565: + return bgr565_traits::shift_g; + + case pixel_format::bgr555: + return bgr555_traits::shift_g; + + case pixel_format::bgra5551: + return bgra5551_traits::shift_g; + + default: + assert(false && "Cannot retreive the pixel green shift."); + throw color_error("Cannot retreive the pixel green shift."); + } + } + + + inline unsigned int get_blue_shift(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::shift_b; + + case pixel_format::bgr32: + return bgr32_traits::shift_b; + + case pixel_format::rgba32: + return rgba32_traits::shift_b; + + case pixel_format::rgb32: + return rgb32_traits::shift_b; + + case pixel_format::bgr24: + return bgr24_traits::shift_b; + + case pixel_format::bgr565: + return bgr565_traits::shift_b; + + case pixel_format::bgr555: + return bgr555_traits::shift_b; + + case pixel_format::bgra5551: + return bgra5551_traits::shift_b; + + default: + assert(false && "Cannot retreive the pixel blue shift."); + throw color_error("Cannot retreive the pixel blue shift."); + } + } + + + inline unsigned int get_red_bits(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::bits_r; + + case pixel_format::bgr32: + return bgr32_traits::bits_r; + + case pixel_format::rgba32: + return rgba32_traits::bits_r; + + case pixel_format::rgb32: + return rgb32_traits::bits_r; + + case pixel_format::bgr24: + return bgr24_traits::bits_r; + + case pixel_format::bgr565: + return bgr565_traits::bits_r; + + case pixel_format::bgr555: + return bgr555_traits::bits_r; + + case pixel_format::bgra5551: + return bgra5551_traits::bits_r; + + default: + assert(false && "Cannot retreive the pixel red bits."); + throw color_error("Cannot retreive the pixel red bits."); + } + } + + + inline unsigned int get_green_bits(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::bits_g; + + case pixel_format::bgr32: + return bgr32_traits::bits_g; + + case pixel_format::rgba32: + return rgba32_traits::bits_g; + + case pixel_format::rgb32: + return rgb32_traits::bits_g; + + case pixel_format::bgr24: + return bgr24_traits::bits_g; + + case pixel_format::bgr565: + return bgr565_traits::bits_g; + + case pixel_format::bgr555: + return bgr555_traits::bits_g; + + case pixel_format::bgra5551: + return bgra5551_traits::bits_g; + + default: + assert(false && "Cannot retreive the pixel green bits."); + throw color_error("Cannot retreive the pixel green bits."); + } + } + + + inline unsigned int get_blue_bits(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::bits_b; + + case pixel_format::bgr32: + return bgr32_traits::bits_b; + + case pixel_format::rgba32: + return rgba32_traits::bits_b; + + case pixel_format::rgb32: + return rgb32_traits::bits_b; + + case pixel_format::bgr24: + return bgr24_traits::bits_b; + + case pixel_format::bgr565: + return bgr565_traits::bits_b; + + case pixel_format::bgr555: + return bgr555_traits::bits_b; + + case pixel_format::bgra5551: + return bgra5551_traits::bits_b; + + default: + assert(false && "Cannot retreive the pixel blue bits."); + throw color_error("Cannot retreive the pixel blue bits."); + } + } + + + inline unsigned int get_alpha_bits(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return bgra32_traits::bits_a; + + case pixel_format::bgr32: + return bgr32_traits::bits_a; + + case pixel_format::rgba32: + return rgba32_traits::bits_a; + + case pixel_format::rgb32: + return rgb32_traits::bits_a; + + case pixel_format::bgr24: + return bgr24_traits::bits_a; + + case pixel_format::bgr565: + return bgr565_traits::bits_a; + + case pixel_format::bgr555: + return bgr555_traits::bits_a; + + case pixel_format::bgra5551: + return bgra5551_traits::bits_a; + + default: + assert(false && "Cannot retreive the pixel alpha bits."); + throw color_error("Cannot retreive the pixel alpha bits."); + } + } + + + // Returns if the pixel format is compressed, false otherwise + inline bool is_compressed(pixel_format::pixel_format pft) { + switch (pft) { + case pixel_format::color: + return detail::is_compressed(); + + case pixel_format::bgr32: + return detail::is_compressed(); + + case pixel_format::rgba32: + return detail::is_compressed(); + + case pixel_format::rgb32: + return detail::is_compressed(); + + case pixel_format::bgr24: + return detail::is_compressed(); + + case pixel_format::bgr565: + return detail::is_compressed(); + + case pixel_format::bgr555: + return detail::is_compressed(); + + case pixel_format::bgra5551: + return detail::is_compressed(); + + default: + assert(false && "Cannot determine whether the given pixel format is compressed."); + throw color_error("Cannot determine whether the given pixel format is compressed."); + } + } + +} // namespace colors diff --git a/extern/include/cpp-httplib/httplib.h b/extern/include/cpp-httplib/httplib.h index 0f86fbf61..1dcb41bfd 100644 --- a/extern/include/cpp-httplib/httplib.h +++ b/extern/include/cpp-httplib/httplib.h @@ -153,7 +153,7 @@ using ssize_t = long; #endif // NOMINMAX #include -//#include +#include #include #ifndef WSA_FLAG_NO_HANDLE_INHERIT diff --git a/extern/include/stduuid/uuid.h b/extern/include/stduuid/uuid.h index 8e06b97e9..34f59e5f8 100644 --- a/extern/include/stduuid/uuid.h +++ b/extern/include/stduuid/uuid.h @@ -20,9 +20,8 @@ #ifdef _WIN32 #include -#ifndef NOMINMAX +#define WIN32_LEAN_AND_MEAN #define NOMINMAX -#endif #include #include #include diff --git a/extern/otio/CMakeLists.txt b/extern/otio/CMakeLists.txt new file mode 100644 index 000000000..5c612aeea --- /dev/null +++ b/extern/otio/CMakeLists.txt @@ -0,0 +1,13 @@ +set(OTIO_PYTHON_INSTALL ON) +set(OTIO_DEPENDENCIES_INSTALL ON) +set(OTIO_INSTALL_PYTHON_MODULES ON) +set(OTIO_INSTALL_COMMANDLINE_TOOLS OFF) +set(OTIO_FIND_IMATH ON) +set(OTIO_PYTHON_INSTALL_DIR python) +set(CMAKE_INSTALL_PREFIX "bin/..") + +# Build options +set(OTIO_SHARED_LIBS OFF) +set(OTIO_AUTOMATIC_SUBMODULES ON) + +add_subdirectory("OpenTimelineIO") \ No newline at end of file diff --git a/extern/otio/OpenTimelineIO b/extern/otio/OpenTimelineIO new file mode 160000 index 000000000..4440afaa2 --- /dev/null +++ b/extern/otio/OpenTimelineIO @@ -0,0 +1 @@ +Subproject commit 4440afaa27b16f81cdf81215ce4d0b08e1424148 diff --git a/extern/quickfuture/CMakeLists.txt b/extern/quickfuture/CMakeLists.txt index 6bc949b57..c59299b02 100644 --- a/extern/quickfuture/CMakeLists.txt +++ b/extern/quickfuture/CMakeLists.txt @@ -4,14 +4,14 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -find_package(Qt5 COMPONENTS Core Quick REQUIRED) +find_package(Qt6 COMPONENTS Core Quick REQUIRED) FILE(GLOB_RECURSE SOURCES src/*.cpp src/*.h) add_library(quickfuture SHARED ${SOURCES}) target_compile_definitions(quickfuture PUBLIC QUICK_FUTURE_BUILD_PLUGIN) -target_link_libraries(quickfuture PUBLIC Qt5::Core Qt5::Quick) +target_link_libraries(quickfuture PUBLIC Qt6::Core Qt6::Quick) target_include_directories(quickfuture PUBLIC src) set(QML_FILES @@ -19,37 +19,48 @@ set(QML_FILES src/quickfuture.qmltypes ) -if(WIN32) - install(FILES ${QML_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/plugin/qml/QuickFuture) - install(TARGETS quickfuture RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/plugin/qml) -else() - +if(APPLE) set_target_properties(quickfuture PROPERTIES - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickFuture" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/PlugIns/xstudio/qml/QuickFuture" ) +else() + set_target_properties(quickfuture + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickFuture") +endif() - install(FILES ${QML_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/share/xstudio/plugin/qml/QuickFuture) - install(TARGETS quickfuture LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/share/xstudio/plugin/qml/QuickFuture) +install(FILES ${QML_FILES} DESTINATION share/xstudio/plugin/qml/QuickFuture) +install(TARGETS quickfuture LIBRARY DESTINATION share/xstudio/plugin/qml/QuickFuture) - # This may not be the best solution. We need to install the quickfuture qml and - # library at build time into the ./bin/plugin/qml/QuickFuture folder. This allows - # us to run xstudio directly from the build target without doing an install - add_custom_target(COPY_FUTURE_QML DEPENDS copy-cmds) - set(QML_FUTURE_FILES - ${CMAKE_CURRENT_SOURCE_DIR}/src/qmldir - ${CMAKE_CURRENT_SOURCE_DIR}/src/quickfuture.qmltypes - ) - - add_custom_command(OUTPUT copy-cmds POST_BUILD - COMMAND ${CMAKE_COMMAND} -E - make_directory ${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickFuture) +if(WIN32) + # On Windows the dll must be present in the qml/QuickFuture folder + _install(TARGETS quickfuture RUNTIME DESTINATION share/xstudio/plugin/qml/QuickFuture) +endif() - foreach(QMLFile ${QML_FUTURE_FILES}) - add_custom_command(OUTPUT copy-cmds APPEND PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E - copy ${QMLFile} ${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickFuture/) - endforeach() - add_dependencies(quickfuture COPY_FUTURE_QML) +# This may not be the best solution. We need to install the quickfuture qml and +# library at build time into the ./bin/plugin/qml/QuickFuture folder. This allows +# us to run xstudio directly from the build target without doing an install +add_custom_target(COPY_FUTURE_QML DEPENDS copy-cmds) +set(QML_FUTURE_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/src/qmldir + ${CMAKE_CURRENT_SOURCE_DIR}/src/quickfuture.qmltypes +) +if (APPLE) + set(QML_DEST_DIR ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/PlugIns/xstudio/qml/QuickFuture) +else() + set(QML_DEST_DIR ${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickFuture) endif() + +add_custom_command(OUTPUT copy-cmds POST_BUILD + COMMAND ${CMAKE_COMMAND} -E + make_directory ${QML_DEST_DIR}) + +foreach(QMLFile ${QML_FUTURE_FILES}) + add_custom_command(OUTPUT copy-cmds APPEND PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy ${QMLFile} ${QML_DEST_DIR}/) +endforeach() + +add_dependencies(quickfuture COPY_FUTURE_QML) diff --git a/extern/quickpromise/CMakeLists.txt b/extern/quickpromise/CMakeLists.txt index 2ed083cd7..a3efcafa0 100644 --- a/extern/quickpromise/CMakeLists.txt +++ b/extern/quickpromise/CMakeLists.txt @@ -12,7 +12,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(INSTALL_ROOT ${CMAKE_INSTALL_PREFIX}) -find_package(Qt5 COMPONENTS Core Quick REQUIRED) +find_package(Qt6 COMPONENTS Core Quick REQUIRED) if(NOT DEFINED STATIC) set(STATIC OFF) @@ -29,7 +29,7 @@ set(QML_FILES set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${PROJECT_SOURCE_DIR}/qml) # Approximates RESOURCES -qt5_add_resources(RESOURCES ${PROJECT_SOURCE_DIR}/qml/quickpromise.qrc) +qt6_add_resources(RESOURCES ${PROJECT_SOURCE_DIR}/qml/quickpromise.qrc) # Add the library if(${STATIC}) @@ -38,18 +38,15 @@ else() add_library(quickpromise SHARED) endif() -target_link_libraries(quickpromise PRIVATE Qt5::Core Qt5::Quick) +target_link_libraries(quickpromise PRIVATE Qt6::Core Qt6::Quick) # Add resource and header files to the library target_sources(quickpromise PRIVATE ${QML_FILES} ${RESOURCES}) -# Set install paths -set(QML_INSTALL_DIR ${INSTALL_ROOT}/bin/QuickPromise) - # Install the library and qml files if(WIN32) - install(TARGETS quickpromise DESTINATION ${INSTALL_ROOT}/bin) - install(FILES ${QML_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/plugin/qml/QuickPromise) + install(TARGETS quickpromise DESTINATION bin) + install(FILES ${QML_FILES} DESTINATION share/xstudio/plugin/qml/QuickPromise) else() install(TARGETS quickpromise DESTINATION ${INSTALL_ROOT}) install(FILES ${QML_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/share/xstudio/plugin/qml/QuickPromise) @@ -61,7 +58,14 @@ endif() # do a 'make install' moving it to the build destination allows us to # run xstudio without an install for development environment. add_custom_target(COPY_PROMISE_QML) + +if (APPLE) + set(QML_DEST_DIR ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/PlugIns/xstudio/qml/QuickPromise) +else() + set(QML_DEST_DIR ${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickPromise) +endif() + add_custom_command(TARGET COPY_PROMISE_QML POST_BUILD COMMAND ${CMAKE_COMMAND} -E - copy_directory ${PROJECT_SOURCE_DIR}/qml/QuickPromise ${CMAKE_BINARY_DIR}/bin/plugin/qml/QuickPromise) + copy_directory ${PROJECT_SOURCE_DIR}/qml/QuickPromise ${QML_DEST_DIR}) add_dependencies(quickpromise COPY_PROMISE_QML) \ No newline at end of file diff --git a/extern/quickpromise/qml/QuickPromise/Promise.qml b/extern/quickpromise/qml/QuickPromise/Promise.qml index 9b80eab27..8466f6ee7 100644 --- a/extern/quickpromise/qml/QuickPromise/Promise.qml +++ b/extern/quickpromise/qml/QuickPromise/Promise.qml @@ -1,5 +1,5 @@ // Author: Ben Lau (https://github.com/benlau) -import QtQuick 2.0 +import QtQuick import QtQml 2.2 import QuickPromise 1.0 import "promise.js" as PromiseJS diff --git a/extern/quickpromise/qml/QuickPromise/PromiseTimer.qml b/extern/quickpromise/qml/QuickPromise/PromiseTimer.qml index dbc4e549e..b72e2fd55 100644 --- a/extern/quickpromise/qml/QuickPromise/PromiseTimer.qml +++ b/extern/quickpromise/qml/QuickPromise/PromiseTimer.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick Timer { running: false diff --git a/extern/stduuid/include/uuid.h b/extern/stduuid/include/uuid.h index fb5875762..34f59e5f8 100644 --- a/extern/stduuid/include/uuid.h +++ b/extern/stduuid/include/uuid.h @@ -20,9 +20,8 @@ #ifdef _WIN32 #include -#ifndef NOMINMAX +#define WIN32_LEAN_AND_MEAN #define NOMINMAX -#endif #include #include #include diff --git a/include/xstudio/atoms.hpp b/include/xstudio/atoms.hpp index 04de419cd..bb17189cb 100644 --- a/include/xstudio/atoms.hpp +++ b/include/xstudio/atoms.hpp @@ -11,6 +11,10 @@ #include #include +#ifdef __apple__ +#undef nil +#endif + #include "flicks.hpp" #include "xstudio/enums.hpp" #include "xstudio/caf_error.hpp" @@ -28,7 +32,6 @@ const std::string colour_cache_registry{"COLOURCACHE"}; const std::string colour_pipeline_registry{"COLOURPIPELINE"}; const std::string conform_registry{"CONFORM"}; const std::string embedded_python_registry{"EMBEDDEDPYTHON"}; -const std::string global_event_group{"XSTUDIO_EVENTS"}; const std::string global_registry{"GLOBAL"}; const std::string global_playhead_events_actor{"GLOBALPLAYHEADEVENTS"}; const std::string global_store_registry{"GLOBALSTORE"}; @@ -37,7 +40,6 @@ const std::string keyboard_events{"KEYBOARDEVENTS"}; const std::string media_hook_registry{"MEDIAHOOK"}; const std::string media_metadata_registry{"MEDIAMETADATA"}; const std::string media_reader_registry{"MEDIAREADER"}; -const std::string module_events_registry{"MODULE_EVENTS"}; const std::string offscreen_viewport_registry{"OFFSCREEN_VIEWPORT"}; const std::string plugin_manager_registry{"PLUGINMNGR"}; const std::string scanner_registry{"SCANNER"}; @@ -47,6 +49,10 @@ const std::string sync_gateway_manager_registry{"SYNCGATEMAN"}; const std::string sync_gateway_registry{"SYNCGATE"}; const std::string thumbnail_manager_registry{"THUMBNAIL"}; const std::string global_ui_model_data_registry{"GLOBALUIMODELDATA"}; +const std::string global_media_metadata_manager_registry{"MEDIAMETADATAMANAGER"}; +const std::string pc_audio_output_registry{"PC_AUDIO_OUTPUT"}; +const std::string viewport_layouts_manager{"GLOBALVIEWPORT_LAYOUTS_MGR"}; + namespace bookmark { class AnnotationBase; @@ -55,31 +61,24 @@ namespace bookmark { typedef std::shared_ptr BookmarkAndAnnotationPtr; } // namespace bookmark -namespace event { - class Event; -} - -namespace tag { - class Tag; -} - namespace timeline { class Item; -} + class Marker; +} // namespace timeline namespace utility { class BlindDataObject; - class ContainerTree; - class EditList; class FrameList; class FrameRange; class FrameRate; class FrameRateDuration; class JsonStore; class MediaReference; + class Notification; class PlaylistTree; class Timecode; class UuidActor; + struct absolute_receive_timeout; struct ContainerDetail; struct CopyResult; @@ -98,7 +97,8 @@ namespace media { class MediaKey; class AVFrameID; class StreamDetail; - typedef std::map> FrameTimeMap; + typedef std::shared_ptr>> + FrameTimeMapPtr; typedef std::vector>> AVFrameIDsAndTimePoints; typedef std::vector> AVFrameIDs; @@ -106,6 +106,7 @@ namespace media { } // namespace media namespace conform { + struct ConformRequestItem; struct ConformRequest; struct ConformReply; } // namespace conform @@ -114,6 +115,10 @@ namespace media_reader { class AudioBuffer; class AudioBufPtr; class ImageBufPtr; + class ImageBufDisplaySet; + class ImageSetLayoutData; + typedef std::shared_ptr ImageBufDisplaySetPtr; + typedef std::shared_ptr ImageSetLayoutDataPtr; class MediaReaderManager; class PixelInfo; } // namespace media_reader @@ -135,6 +140,9 @@ namespace ui { namespace viewport { class GPUShader; typedef std::shared_ptr GPUShaderPtr; + class ViewportRenderer; + typedef std::shared_ptr ViewportRendererPtr; + } // namespace viewport } // namespace ui @@ -179,15 +187,19 @@ CAF_ALLOW_UNSAFE_MESSAGE_TYPE(std::shared_ptr) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::bookmark::BookmarkAndAnnotationPtr) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::colour_pipeline::ColourOperationDataPtr) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::colour_pipeline::ColourPipelineDataPtr) -CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::media::FrameTimeMap) +CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::media::FrameTimeMapPtr) +CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::media::AVFrameID) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::media::AVFrameIDs) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::media::AVFrameIDsAndTimePoints) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::media_reader::AudioBuffer) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::media_reader::AudioBufPtr) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::media_reader::ImageBufPtr) +CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::media_reader::ImageBufDisplaySetPtr) +CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::media_reader::ImageSetLayoutDataPtr) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::media_reader::PixelInfo) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::ui::Hotkey) CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::ui::viewport::GPUShaderPtr) +CAF_ALLOW_UNSAFE_MESSAGE_TYPE(xstudio::ui::viewport::ViewportRendererPtr) // clang-format off // offset first_custom_type_id by first custom qt event @@ -206,13 +218,12 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_simple_types, FIRST_CUSTOM_ID) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::bookmark::BookmarkAndAnnotationPtr)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::colour_pipeline::ColourOperationDataPtr)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::colour_pipeline::ColourPipelineDataPtr)) - CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::event::Event)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::global::StatusType)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::http_client::http_client_error)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::AVFrameID)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::AVFrameIDs)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::AVFrameIDsAndTimePoints)) - CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::FrameTimeMap)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::FrameTimeMapPtr)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::media_error)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::MediaDetail)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media::MediaKey)) @@ -222,17 +233,20 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_simple_types, FIRST_CUSTOM_ID) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_metadata::MMCertainty)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_reader::AudioBufPtr)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_reader::ImageBufPtr)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_reader::ImageBufDisplaySetPtr)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_reader::ImageSetLayoutDataPtr)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_reader::MRCertainty)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::media_reader::PixelInfo)) - CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::playhead::CompareMode)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::playhead::AssemblyMode)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::playhead::AutoAlignMode)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::playhead::LoopMode)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::playhead::OverflowMode)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::plugin_manager::PluginDetail)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::plugin::HUDElementPosition)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::session::ExportFormat)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::shotgun_client::AuthenticateShotgun)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::shotgun_client::AUTHENTICATION_METHOD)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::shotgun_client::shotgun_client_error)) - CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::tag::Tag)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::thumbnail::DiskCacheStat)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::thumbnail::THUMBNAIL_FORMAT)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::thumbnail::ThumbnailBuffer)) @@ -242,11 +256,13 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_simple_types, FIRST_CUSTOM_ID) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::PointerEvent)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::Signature)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::FitMode)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::MirrorMode)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::GPUShaderPtr)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::GraphicsAPI)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::ViewportRendererPtr)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::absolute_receive_timeout)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::ContainerDetail)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::CopyResult)) - CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::EditList)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::FrameList)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::FrameRange)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::FrameRate)) @@ -255,12 +271,23 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_simple_types, FIRST_CUSTOM_ID) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::MediaReference)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::PlaylistTree)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::time_point)) +#ifndef __linux__ + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::sys_time_point)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::sys_time_duration)) +#endif CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::Timecode)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::TimeSourceMode)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::Uuid)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::UuidActorMap)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::xstudio_error)) CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::ui::viewport::ImageFormat)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::timeline::Marker)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::NotificationType)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::Notification)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::playhead::SelectionMode)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::timeline::ItemType)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (xstudio::utility::ColourTriplet)) + CAF_ADD_TYPE_ID(xstudio_simple_types, (spdlog::level::level_enum)) CAF_END_TYPE_ID_BLOCK(xstudio_simple_types) @@ -273,7 +300,7 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_complex_types, FIRST_CUSTOM_ID + 200) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::map)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::optional)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::optional)) - CAF_ADD_TYPE_ID(xstudio_complex_types, (std::pair)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::tuple)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::pair)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::pair)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::pair)) @@ -311,11 +338,10 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_complex_types, FIRST_CUSTOM_ID + 200) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) - CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) + // CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector>)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector>)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector>)) - CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector>)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector>)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector>)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector>)) @@ -332,11 +358,9 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_complex_types, FIRST_CUSTOM_ID + 200) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) - CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) - CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) @@ -347,11 +371,30 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_complex_types, FIRST_CUSTOM_ID + 200) CAF_ADD_TYPE_ID(xstudio_complex_types, (xstudio::conform::ConformRequest)) CAF_ADD_TYPE_ID(xstudio_complex_types, (xstudio::conform::ConformReply)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (xstudio::conform::ConformRequestItem)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::set)) CAF_ADD_TYPE_ID(xstudio_complex_types, (std::shared_ptr)) -CAF_END_TYPE_ID_BLOCK(xstudio_complex_types) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::pair>)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector>>)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::pair)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector>>)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::pair>)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector>>)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::pair>)) + + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::pair)) + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::pair>)) + + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector>>)) + + CAF_ADD_TYPE_ID(xstudio_complex_types, (std::vector)) + + +CAF_END_TYPE_ID_BLOCK(xstudio_complex_types) CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, FIRST_CUSTOM_ID + (200 * 2)) @@ -363,7 +406,6 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, FIRST_CUSTOM_ID + (200 * 2)) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, create_studio_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, exit_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_actor_from_registry_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_api_mode_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_application_mode_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_global_audio_cache_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, get_global_image_cache_atom) @@ -404,7 +446,6 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, FIRST_CUSTOM_ID + (200 * 2)) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, current_viewport_playhead_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, deserialise_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, disconnect_from_ui_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, full_attributes_description_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, get_ui_focus_events_group_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, grab_all_keyboard_input_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, grab_all_mouse_input_atom) @@ -415,13 +456,8 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, FIRST_CUSTOM_ID + (200 * 2)) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, module_ui_events_group_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, redraw_viewport_group_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, release_ui_focus_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, remove_attrs_from_ui_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, request_full_attributes_description_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, request_menu_attributes_description_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, update_attribute_in_preferences_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::sync, authorise_connection_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::sync, get_sync_atom) - CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::sync, request_connection_atom) + CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::thumbnail, cache_path_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::thumbnail, cache_stats_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::utility, change_atom) @@ -442,8 +478,16 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_framework_atoms, FIRST_CUSTOM_ID + (200 * 2)) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::utility, uuid_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::utility, version_atom) + CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::json_store, sync_atom) + CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, reset_module_atom) + CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, module_add_menu_item_atom) + CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, module_remove_menu_item_atom) CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::module, remove_attribute_atom) + CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::utility, notification_atom) + + CAF_ADD_ATOM(xstudio_framework_atoms, xstudio::global, authenticate_atom) + CAF_END_TYPE_ID_BLOCK(xstudio_framework_atoms) @@ -473,6 +517,7 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_plugin_atoms, FIRST_CUSTOM_ID + (200 * 3)) CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::media_hook, check_media_hook_plugin_versions_atom) CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::media_hook, gather_media_sources_atom) CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::media_hook, get_media_hook_atom) + CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::media_hook, detect_display_atom) CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::plugin_manager, add_path_atom) CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::plugin_manager, enable_atom) CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::plugin_manager, get_resident_atom) @@ -508,6 +553,7 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_plugin_atoms, FIRST_CUSTOM_ID + (200 * 3)) // **************** add new entries here ****************** CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::conform, conform_atom) CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::conform, conform_tasks_atom) + CAF_ADD_ATOM(xstudio_plugin_atoms, xstudio::media_hook, get_clip_hook_atom) CAF_END_TYPE_ID_BLOCK(xstudio_plugin_atoms) @@ -548,8 +594,10 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_session_atoms, FIRST_CUSTOM_ID + (200 * 4)) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, relink_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, rescan_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, source_offset_frames_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, pixel_aspect_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media_metadata, get_metadata_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, add_media_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, add_media_with_subsets_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, convert_to_contact_sheet_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, convert_to_subset_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, convert_to_timeline_atom) @@ -564,6 +612,7 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_session_atoms, FIRST_CUSTOM_ID + (200 * 4)) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, create_subset_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, create_timeline_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, duplicate_container_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, filter_media_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, get_change_event_group_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, get_container_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, get_media_atom) @@ -573,6 +622,7 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_session_atoms, FIRST_CUSTOM_ID + (200 * 4)) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, insert_container_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, loading_media_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, media_content_changed_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, media_filter_string) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, move_container_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, move_container_to_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, move_media_atom) @@ -585,12 +635,14 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_session_atoms, FIRST_CUSTOM_ID + (200 * 4)) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, select_media_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, selection_actor_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, set_playlist_in_viewer_atom) - CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, sort_alphabetically_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, sort_by_media_display_info_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::session, add_playlist_atom) - CAF_ADD_ATOM(xstudio_session_atoms, xstudio::session, current_playlist_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::session, active_media_container_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::session, viewport_active_media_container_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::session, export_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::session, get_playlist_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::session, get_playlists_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::session, get_push_playlist_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::session, import_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::session, load_uris_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::session, media_rate_atom) @@ -600,10 +652,6 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_session_atoms, FIRST_CUSTOM_ID + (200 * 4)) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::session, remove_serialise_target_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::session, session_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::session, session_request_atom) - CAF_ADD_ATOM(xstudio_session_atoms, xstudio::tag, add_tag_atom) - CAF_ADD_ATOM(xstudio_session_atoms, xstudio::tag, get_tag_atom) - CAF_ADD_ATOM(xstudio_session_atoms, xstudio::tag, get_tags_atom) - CAF_ADD_ATOM(xstudio_session_atoms, xstudio::tag, remove_tag_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, active_range_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, available_range_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, duration_atom) @@ -625,6 +673,19 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_session_atoms, FIRST_CUSTOM_ID + (200 * 4)) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, item_flag_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, metadata_selection_atom) CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, focus_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, item_lock_atom) + + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, media_display_info_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, human_readable_info_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, bake_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, item_prop_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, item_marker_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, media_frame_to_timeline_frames_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::playlist, expanded_atom) + + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::media, current_media_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, item_selection_atom) + CAF_ADD_ATOM(xstudio_session_atoms, xstudio::timeline, item_type_atom) CAF_END_TYPE_ID_BLOCK(xstudio_session_atoms) @@ -633,6 +694,8 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_playback_atoms, FIRST_CUSTOM_ID + (200 * 5)) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::audio, get_samples_for_soundcard_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::audio, push_samples_atom) + CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::audio, set_override_volume_atom) + CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::audio, audio_samples_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, colour_operation_uniforms_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, colour_pipeline_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, connect_to_viewport_atom) @@ -651,7 +714,6 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_playback_atoms, FIRST_CUSTOM_ID + (200 * 5)) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::media_cache, size_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::media_cache, store_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::media_cache, unpreserve_atom) - CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::media_reader, cancel_thumbnail_request_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::media_reader, clear_precache_queue_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::media_reader, do_precache_work_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::media_reader, get_audio_atom) @@ -699,6 +761,7 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_playback_atoms, FIRST_CUSTOM_ID + (200 * 5)) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, media_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, media_events_group_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, media_frame_to_flicks_atom) + CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, media_frame_ranges_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, media_logical_frame_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, media_source_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, monitored_atom) @@ -710,13 +773,12 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_playback_atoms, FIRST_CUSTOM_ID + (200 * 5)) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, position_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, precache_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, redraw_viewport_atom) - CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, scrub_frame_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, select_next_media_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, selection_changed_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, show_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, simple_loop_end_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, simple_loop_start_atom) - CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, skip_through_sources_atom) + CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, skip_to_clip_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, sound_audio_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, source_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, step_atom) @@ -726,6 +788,9 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_playback_atoms, FIRST_CUSTOM_ID + (200 * 5)) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, viewport_events_group_atom) CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::playhead, media_frame_atom) + CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, global_ocio_controls_atom) + CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, colour_pipe_linearise_data_atom) + CAF_ADD_ATOM(xstudio_playback_atoms, xstudio::colour_pipeline, colour_pipe_display_data_atom) CAF_END_TYPE_ID_BLOCK(xstudio_playback_atoms) @@ -748,39 +813,49 @@ CAF_BEGIN_TYPE_ID_BLOCK(xstudio_ui_atoms, FIRST_CUSTOM_ID + (200 * 6)) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, register_hotkey_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, skipped_mouse_event_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, text_entry_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::keypress_monitor, watch_hotkey_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, insert_or_update_menu_node_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, insert_rows_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, menu_node_activated_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, model_data_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, register_model_data_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, deregister_model_data_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, remove_node_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, remove_rows_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, set_node_data_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::qml, backend_atom) - CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, enable_hud_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, hud_settings_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, fit_mode_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, other_viewport_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, active_viewport_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, overlay_render_function_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, prepare_overlay_render_data_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, render_viewport_to_image_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, screen_info_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, viewport_cursor_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, viewport_get_next_frames_for_display_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, viewport_pan_atom) - CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, viewport_pixel_zoom_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, viewport_playhead_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, viewport_scale_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, viewport_set_scene_coordinates_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, quickview_media_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, connect_to_viewport_toolbar_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, viewport_visibility_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, viewport_renderer_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, viewport_layout_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui, open_quickview_window_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui, show_message_box_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, viewport_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, pre_render_gpu_hook_atom) - CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui, offscreen_viewport_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, reset_model_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::model_data, set_row_ordering_role_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, register_viewport_atom) + + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui, offscreen_viewport_atom) CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui, video_output_actor_atom) - CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, aux_shader_uniforms_atom) + CAF_ADD_ATOM(xstudio_ui_atoms, xstudio::ui::viewport, turn_off_overlay_interaction_atom) CAF_END_TYPE_ID_BLOCK(xstudio_ui_atoms) @@ -791,6 +866,196 @@ CAF_ERROR_CODE_ENUM(xstudio::http_client::http_client_error) CAF_ERROR_CODE_ENUM(xstudio::media::media_error) CAF_ERROR_CODE_ENUM(xstudio::shotgun_client::shotgun_client_error) +namespace xstudio::global { +template bool inspect(Inspector &f, StatusType &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} +} // namespace xstudio::global + +namespace xstudio::ui { +template bool inspect(Inspector &f, EventType &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} +} // namespace xstudio::ui + +namespace xstudio::utility { +template bool inspect(Inspector &f, NotificationType &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} + +template bool inspect(Inspector &f, TimeSourceMode &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} +} // namespace xstudio::utility + +namespace xstudio::timeline { +template bool inspect(Inspector &f, ItemType &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} +} // namespace xstudio::timeline + +namespace xstudio::playhead { +template bool inspect(Inspector &f, LoopMode &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} + +template bool inspect(Inspector &f, AssemblyMode &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} + +template bool inspect(Inspector &f, AutoAlignMode &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} + +template bool inspect(Inspector &f, OverflowMode &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} + +template bool inspect(Inspector &f, SelectionMode &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} +} // namespace xstudio::playhead + +namespace xstudio::media { +template bool inspect(Inspector &f, MediaStatus &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} + +template bool inspect(Inspector &f, MediaType &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} +} // namespace xstudio::media + +namespace xstudio::ui::viewport { +template bool inspect(Inspector &f, FitMode &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} + +template bool inspect(Inspector &f, MirrorMode &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} + +template bool inspect(Inspector &f, GraphicsAPI &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} + +template bool inspect(Inspector &f, ImageFormat &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} +} // namespace xstudio::ui::viewport + +namespace xstudio::shotgun_client { +template bool inspect(Inspector &f, AUTHENTICATION_METHOD &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} +} // namespace xstudio::shotgun_client + +namespace xstudio::session { +template bool inspect(Inspector &f, ExportFormat &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} +} // namespace xstudio::session + +namespace xstudio::thumbnail { +template bool inspect(Inspector &f, THUMBNAIL_FORMAT &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} +} // namespace xstudio::thumbnail + +namespace spdlog::level { +template bool inspect(Inspector &f, level_enum &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} +} // namespace spdlog::level + +namespace xstudio::media_metadata { +template bool inspect(Inspector &f, MMCertainty &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} +} // namespace xstudio::media_metadata + +namespace xstudio::media_reader { +template bool inspect(Inspector &f, MRCertainty &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} +} // namespace xstudio::media_reader + +namespace xstudio::plugin { +template bool inspect(Inspector &f, HUDElementPosition &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} +} // namespace xstudio::plugin + + namespace xstudio::utility { inline auto make_get_version_handler() { return [=](version_atom) -> std::string { return PROJECT_VERSION; }; @@ -798,6 +1063,14 @@ inline auto make_get_version_handler() { } // namespace xstudio::utility namespace semver { + +template bool inspect(Inspector &f, prerelease &x) { + using int_t = std::underlying_type_t; + auto getter = [&x] { return static_cast(x); }; + auto setter = [&x](int_t val) { x = static_cast(val); }; + return f.apply(getter, setter); +} + template bool inspect(Inspector &f, version &x) { return f.object(x).fields( f.field("major", x.major), @@ -825,6 +1098,33 @@ template bool inspect(Inspector &f, xstudio::utility::time_poi return f.object(x).fields(f.field("ts", get_ts, set_ts)); } +template bool inspect(Inspector &f, xstudio::utility::sys_time_point &x) { + using Clock = xstudio::utility::sysclock; + using Time_point = Clock::time_point; + using Duration = Clock::duration; + + auto get_ts = [&x]() -> decltype(auto) { return x.time_since_epoch().count(); }; + auto set_ts = [&x](Duration::rep value) { + Duration d(value); + Time_point tp1(d); + x = tp1; + return true; + }; + + return f.object(x).fields(f.field("ts", get_ts, set_ts)); +} + +template bool inspect(Inspector &f, xstudio::utility::sys_time_duration &x) { + + auto get_ts = [&x]() -> decltype(auto) { return x.count(); }; + auto set_ts = [&x](xstudio::utility::sys_time_duration::rep value) { + x = xstudio::utility::sys_time_duration(value); + return true; + }; + + return f.object(x).fields(f.field("ts", get_ts, set_ts)); +} + template bool inspect(Inspector &f, std::filesystem::file_time_type &x) { using TP = std::filesystem::file_time_type; using D = std::filesystem::file_time_type::duration; @@ -893,17 +1193,21 @@ class XStudioError : public std::runtime_error { : runtime_error(msg.c_str()), error_type_(xstudio_error_type::xstudio_error) {} XStudioError(const std::runtime_error &err) : runtime_error(err) {} XStudioError(const caf::error &err) : runtime_error(to_string(err)), caf_error_(err) { - if (err.category() == caf::type_id_v) { - error_type_ = xstudio_error_type::http_client_error; - } else if (err.category() == caf::type_id_v) { - error_type_ = xstudio_error_type::shotgun_client_error; - } else if (err.category() == caf::type_id_v) { - error_type_ = xstudio_error_type::media_error; - } else if (err.category() == caf::type_id_v) { - error_type_ = xstudio_error_type::xstudio_error; - } else { + if (caf_error_) { + if (caf_error_.category() == caf::type_id_v) { + error_type_ = xstudio_error_type::http_client_error; + } else if ( + caf_error_.category() == caf::type_id_v) { + error_type_ = xstudio_error_type::shotgun_client_error; + } else if (caf_error_.category() == caf::type_id_v) { + error_type_ = xstudio_error_type::media_error; + } else if (caf_error_.category() == caf::type_id_v) { + error_type_ = xstudio_error_type::xstudio_error; + } else { + error_type_ = xstudio_error_type::caf_error; + } + } else error_type_ = xstudio_error_type::caf_error; - } } xstudio_error_type type() const { return error_type_; } diff --git a/include/xstudio/audio/audio_output.hpp b/include/xstudio/audio/audio_output.hpp index 70338b38b..3be07db56 100644 --- a/include/xstudio/audio/audio_output.hpp +++ b/include/xstudio/audio/audio_output.hpp @@ -34,17 +34,25 @@ class AudioOutputControl { * audio frames. * */ - void prepare_samples_for_soundcard( + void prepare_samples_for_soundcard_playback( std::vector &samples, const long num_samps_to_push, const long microseconds_delay, const int num_channels, const int sample_rate); + /** + * @brief Pick audio samples based on the current playhead position to sound + * audio during timeline scrubbing. + * + */ + long copy_samples_to_buffer_for_scrubbing( + std::vector &samples, const long num_samps_to_push); + /** * @brief The audio volume (range is 0-1) */ - [[nodiscard]] float volume() const { return volume_; } + [[nodiscard]] float volume() const { return volume_ * override_volume_ / 100.0f; } /** * @brief The audio volume muted @@ -54,11 +62,25 @@ class AudioOutputControl { /** * @brief Queue audio buffer for streaming to the soundcard */ - void queue_samples_for_playing( + void queue_samples_for_playing(const std::vector &audio_buffers); + + /** + * @brief Queue audio buffer for streaming to the soundcard during + * timeline scrubbing + */ + void prepare_samples_for_audio_scrubbing( const std::vector &audio_buffers, + const timebase::flicks playhead_position); + + /** + * @brief Fine grained update of playhead position + */ + void playhead_position_changed( + const timebase::flicks playhead_position, + const bool forward, + const float velocity, const bool playing, - const bool forwards, - const float velocity); + utility::time_point when_position_changed); /** * @brief Clear all queued audio buffers to immediately stop audio playback @@ -82,7 +104,11 @@ class AudioOutputControl { audio_scrubbing_ = audio_scrubbing; } - private: + void set_override_volume(const float override_volume) { + override_volume_ = override_volume; + } + + protected: media_reader::AudioBufPtr pick_audio_buffer(const utility::clock::time_point &tp, const bool drop_old_buffers); @@ -91,17 +117,34 @@ class AudioOutputControl { const media_reader::AudioBufPtr &next_buf, const media_reader::AudioBufPtr &previous_buf_); - std::map sample_data_; + // the actual sound samples that we are about to play, measured against + // their timestamp in the xstudio plyhead timeline + std::map sample_data_; + + // a dynamic buffer of samples to be streamed to soundcard during + // scrubbing. + std::vector scrubbing_samples_buf_; + media_reader::AudioBufPtr current_buf_; media_reader::AudioBufPtr previous_buf_; + media_reader::AudioBufPtr next_buf_; long current_buf_pos_; float playback_velocity_ = {1.0f}; int fade_in_out_ = {NoFade}; - bool audio_repitch_ = {false}; - bool audio_scrubbing_ = {false}; - float volume_ = {100.0f}; - bool muted_ = {false}; + timebase::flicks playhead_position_; + bool playing_forward_ = {true}; + utility::time_point playhead_position_update_tp_; + timebase::flicks last_buffer_pts_; + + bool audio_repitch_ = {false}; + bool audio_scrubbing_ = {false}; + float volume_ = {100.0f}; + bool muted_ = {false}; + bool playing_ = {false}; + float override_volume_ = {100.0f}; + float last_volume_ = {100.0f}; + float scrub_chunk_duration_frames_ = {1.0f}; }; } // namespace xstudio::audio diff --git a/include/xstudio/audio/audio_output_actor.hpp b/include/xstudio/audio/audio_output_actor.hpp index 0ff730b0b..1eae78696 100644 --- a/include/xstudio/audio/audio_output_actor.hpp +++ b/include/xstudio/audio/audio_output_actor.hpp @@ -8,27 +8,30 @@ namespace xstudio::audio { -template class AudioOutputDeviceActor : public caf::event_based_actor { public: - AudioOutputDeviceActor(caf::actor_config &cfg, caf::actor samples_actor) + AudioOutputDeviceActor( + caf::actor_config &cfg, + caf::actor samples_actor, + std::shared_ptr output_device) : caf::event_based_actor(cfg), playing_(false), waiting_for_samples_(false), - audio_samples_actor_(samples_actor) { + audio_samples_actor_(samples_actor), + output_device_(output_device) { - // spdlog::info("Created {} {}", "AudioOutputDeviceActor", OutputClassType::name()); + // spdlog::debug("Created {} {}", "AudioOutputDeviceActor", OutputClassType::name()); // utility::print_on_exit(this, OutputClassType::name()); - try { + /*try { auto prefs = global_store::GlobalStoreHelper(system()); utility::JsonStore j; utility::join_broadcast(this, prefs.get_group(j)); open_output_device(j); } catch (...) { open_output_device(utility::JsonStore()); - } + }*/ behavior_.assign( @@ -38,7 +41,8 @@ class AudioOutputDeviceActor : public caf::event_based_actor { const utility::JsonStore & /*change*/, const std::string & /*path*/, const utility::JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full) + .delegate(actor_cast(this)); }, [=](json_store::update_atom, const utility::JsonStore & /*j*/) { // TODO: restart soundcard connection with new prefs @@ -46,51 +50,103 @@ class AudioOutputDeviceActor : public caf::event_based_actor { output_device_->initialize_sound_card(); } }, + [=](utility::event_atom, playhead::play_atom) { + // we get this message every time the AudioOutputActor has + // received samples to play. + // connect to the sound output device if necessary + if (output_device_) { + try { + output_device_->connect_to_soundcard(); + } catch (std::exception &err) { + spdlog::critical("Failed to connect to audio device: {}", err.what()); + output_device_.reset(); + return; + } + } else { + return; + } + + if (!waiting_for_samples_) { + // start playback loop + anon_mail(push_samples_atom_v).send(actor_cast(this)); + } + }, [=](utility::event_atom, playhead::play_atom, const bool is_playing) { if (!is_playing && output_device_) { // this stops the loop pushing samples to the soundcard + if (playing_) { + // we've stopped playing, so clear samples in the + // buffer + output_device_->disconnect_from_soundcard(); + } playing_ = false; - output_device_->disconnect_from_soundcard(); + // } else if (is_playing && !playing_) { // start loop playing_ = true; - if (output_device_) - output_device_->connect_to_soundcard(); - anon_send(actor_cast(this), push_samples_atom_v); } }, [=](push_samples_atom) { if (!output_device_) return; + // The 'waiting_for_samples_' flag allows us to ensure that we // don't have multiple requests for samples to play in flight - // since each response to a request then sends another // 'push_samples_atom' atom (to keep playback running), having multiple // requests in flight completely messes up the audio playback as // essentially we have two loops running within the single actor. - if (waiting_for_samples_ || !playing_) + if (waiting_for_samples_) return; + const long num_samps_soundcard_wants = (long)output_device_->desired_samples(); + if (!num_samps_soundcard_wants) { + // soundcard buffer is probably full. Wait 2ms, and continue + // continue the loop. Why 1ms ? for really low latency, we might + // have just a few 100 samples in the soundcard buffer. At 48Khz + // (common sample rate) + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + anon_mail(push_samples_atom_v).send(actor_cast(this)); + return; + } + waiting_for_samples_ = true; - const long num_samps_soundcard_wants = (long)output_device_->desired_samples(); - auto tt = utility::clock::now(); - request( - audio_samples_actor_, - infinite, + if (!num_samps_soundcard_wants) { + // soundcard doesn't want any more samples yet, it's buffer + // must be full. + if (playing_) { + // continue the loop, but sleep for 5ms so we don't create + // a tight loop and the soundcard can drain some samples + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + anon_mail(push_samples_atom_v) + .send(actor_cast(this)); + } + + return; + } + + waiting_for_samples_ = true; + + auto tt = utility::clock::now(); + mail( get_samples_for_soundcard_atom_v, num_samps_soundcard_wants, (long)output_device_->latency_microseconds(), (int)output_device_->num_channels(), (int)output_device_->sample_rate()) + .request(audio_samples_actor_, infinite) .then( [=](const std::vector &samples_to_play) mutable { waiting_for_samples_ = false; - output_device_->push_samples( - (const void *)samples_to_play.data(), samples_to_play.size()); - - - if (playing_) { - anon_send(actor_cast(this), push_samples_atom_v); + if (samples_to_play.size()) { + if (output_device_->push_samples( + (const void *)samples_to_play.data(), + samples_to_play.size())) { + + // continue the loop + anon_mail(push_samples_atom_v) + .send(actor_cast(this)); + } } }, [=](caf::error &err) mutable { waiting_for_samples_ = false; }); @@ -99,15 +155,6 @@ class AudioOutputDeviceActor : public caf::event_based_actor { ); } - void open_output_device(const utility::JsonStore &prefs) { - try { - output_device_ = std::make_unique(prefs); - } catch (std::exception &e) { - spdlog::error( - "{} Failed to connect to an audio device: {}", __PRETTY_FUNCTION__, e.what()); - } - } - ~AudioOutputDeviceActor() override = default; caf::behavior make_behavior() override { return behavior_; } @@ -115,7 +162,7 @@ class AudioOutputDeviceActor : public caf::event_based_actor { const char *name() const override { return name_.c_str(); } protected: - std::unique_ptr output_device_; + std::shared_ptr output_device_; private: caf::behavior behavior_; @@ -125,11 +172,18 @@ class AudioOutputDeviceActor : public caf::event_based_actor { bool waiting_for_samples_; }; -template class AudioOutputActor : public caf::event_based_actor, AudioOutputControl { public: - AudioOutputActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { init(); } + AudioOutputActor( + caf::actor_config &cfg, + std::shared_ptr output_device, + bool subscribe_to_global_audio_stream = true) + : caf::event_based_actor(cfg), + output_device_(output_device), + is_global_(subscribe_to_global_audio_stream) { + init(); + } ~AudioOutputActor() override = default; @@ -144,11 +198,14 @@ class AudioOutputActor : public caf::event_based_actor, AudioOutputControl { caf::behavior behavior_; const utility::JsonStore params_; - bool playing_ = {false}; int video_frame_ = {0}; int retry_on_error_ = {0}; utility::Uuid uuid_ = {utility::Uuid::generate()}; utility::Uuid sub_playhead_uuid_; + std::shared_ptr output_device_; + caf::actor playhead_; + bool is_global_; + utility::time_point last_audio_sounding_tp_; }; /* Singleton class that receives audio sample buffers from the current @@ -162,87 +219,33 @@ class GlobalAudioOutputActor : public caf::event_based_actor, module::Module { void on_exit() override; - void attribute_changed(const utility::Uuid &attr_uuid, const int role); + void attribute_changed(const utility::Uuid &attr_uuid, const int role) override; caf::behavior make_behavior() override { return behavior_.or_else(module::Module::message_handler()); } + void hotkey_pressed( + const utility::Uuid &hotkey_uuid, + const std::string &context, + const std::string &window) override; + + const char *name() const override { + return dynamic_cast(this)->name().c_str(); + } + + private: + caf::actor independent_output(const utility::Uuid &playhead_uuid); + caf::actor event_group_; caf::message_handler behavior_; module::BooleanAttribute *audio_repitch_; module::BooleanAttribute *audio_scrubbing_; module::FloatAttribute *volume_; module::BooleanAttribute *muted_; + utility::Uuid mute_hotkey_; + std::map independent_outputs_; }; -template void AudioOutputActor::init() { - - // spdlog::debug("Created AudioOutputControlActor {}", OutputClassType::name()); - utility::print_on_exit(this, "AudioOutputControlActor"); - - audio_output_device_ = - spawn>(caf::actor_cast(this)); - link_to(audio_output_device_); - - auto global_audio_actor = - system().registry().template get(audio_output_registry); - utility::join_event_group(this, global_audio_actor); - - behavior_.assign( - - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - - [=](utility::event_atom, playhead::play_atom, const bool is_playing) { - send( - audio_output_device_, utility::event_atom_v, playhead::play_atom_v, is_playing); - }, - - [=](get_samples_for_soundcard_atom, - const long num_samps_to_push, - const long microseconds_delay, - const int num_channels, - const int sample_rate) -> result> { - std::vector samples; - try { - - prepare_samples_for_soundcard( - samples, num_samps_to_push, microseconds_delay, num_channels, sample_rate); - - } catch (std::exception &e) { - - return caf::make_error(xstudio_error::error, e.what()); - } - return samples; - }, - [=](utility::event_atom, - module::change_attribute_event_atom, - const float volume, - const bool muted, - const bool repitch, - const bool scrubbing) { set_attrs(volume, muted, repitch, scrubbing); }, - [=](utility::event_atom, - playhead::sound_audio_atom, - const std::vector &audio_buffers, - const utility::Uuid &sub_playhead, - const bool playing, - const bool forwards, - const float velocity) { - if (!playing) { - clear_queued_samples(); - } else { - if (sub_playhead != sub_playhead_uuid_) { - // sound is coming from a different source to - // previous time - clear_queued_samples(); - sub_playhead_uuid_ = sub_playhead; - } - queue_samples_for_playing(audio_buffers, playing, forwards, velocity); - } - } - - ); -} - } // namespace xstudio::audio diff --git a/include/xstudio/audio/audio_output_device.hpp b/include/xstudio/audio/audio_output_device.hpp index c0b86f8b1..58beac8fa 100644 --- a/include/xstudio/audio/audio_output_device.hpp +++ b/include/xstudio/audio/audio_output_device.hpp @@ -3,7 +3,7 @@ namespace xstudio::audio { -enum class SampleFormat { UNSET, UINT8 = 1, INT16, INT32, FLOAT32, INT64, DOUBLE64 }; +enum class SampleFormat { UNSET, UINT8 = 1, INT16, SFINT32, FLOAT32, INT64, DOUBLE64 }; /** * @brief AudioOutputDevice class, low level interface with audio output @@ -65,8 +65,11 @@ class AudioOutputDevice { * number of channels and sample rate that the soundcard expects. This function may * block while the soundcard consumes samples, depending on the implementation of * the subclass. + * + * @return (bool) returns true if samples were able to be consumed. Returning + * false will stop audio samples playback loop. */ - virtual void push_samples(const void *sample_data, const long num_samples) = 0; + virtual bool push_samples(const void *sample_data, const long num_samples) = 0; /** * @brief Query the audio pipeline delay from the last sample in the soundcard diff --git a/include/xstudio/audio/linux_audio_output_device.hpp b/include/xstudio/audio/linux_audio_output_device.hpp index 2ff0005d1..2808c8b14 100644 --- a/include/xstudio/audio/linux_audio_output_device.hpp +++ b/include/xstudio/audio/linux_audio_output_device.hpp @@ -27,7 +27,7 @@ namespace audio { long desired_samples() override; - void push_samples(const void *sample_data, const long num_samples) override; + bool push_samples(const void *sample_data, const long num_samples) override; long latency_microseconds() override; diff --git a/include/xstudio/audio/macos_audio_output_device.hpp b/include/xstudio/audio/macos_audio_output_device.hpp new file mode 100644 index 000000000..53caa1a04 --- /dev/null +++ b/include/xstudio/audio/macos_audio_output_device.hpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/audio/audio_output_device.hpp" +#include "xstudio/utility/json_store.hpp" + +namespace xstudio { +namespace audio { + + class MacOSAudioOutData; + + /** + * @brief MacOSAudioOutputDevice class, low level interface with audio output + * + * @details + * See header for AudioOutputDevice + */ + class MacOSAudioOutputDevice : public AudioOutputDevice { + + public: + + MacOSAudioOutputDevice(const utility::JsonStore &prefs); + + ~MacOSAudioOutputDevice() override; + + void initialize_sound_card() override {} + + void connect_to_soundcard() override; + + void disconnect_from_soundcard() override; + + long desired_samples() override; + + bool push_samples(const void *sample_data, const long num_samples) override; + + long latency_microseconds() override; + + [[nodiscard]] long sample_rate() const override { return sample_rate_; } + + [[nodiscard]] int num_channels() const override { return num_channels_; } + + [[nodiscard]] SampleFormat sample_format() const override { return sample_format_; } + + private: + + long aprx_buffer_water_level_microsecs_ = {80000}; + long sample_rate_ = {48000}; + int num_channels_ = {2}; + long buffer_size_ = {2048}; + SampleFormat sample_format_ = {SampleFormat::INT16}; + + const utility::JsonStore prefs_; + + std::unique_ptr audio_out_data_; + + }; +} // namespace audio +} // namespace xstudio diff --git a/include/xstudio/audio/windows_audio_output_device.hpp b/include/xstudio/audio/windows_audio_output_device.hpp index ec06390b7..c9750a386 100644 --- a/include/xstudio/audio/windows_audio_output_device.hpp +++ b/include/xstudio/audio/windows_audio_output_device.hpp @@ -29,7 +29,7 @@ namespace audio { long desired_samples() override; - void push_samples(const void *sample_data, const long num_samples) override; + bool push_samples(const void *sample_data, const long num_samples) override; long latency_microseconds() override; diff --git a/include/xstudio/bookmark/bookmark.hpp b/include/xstudio/bookmark/bookmark.hpp index 8dbaff21d..093789b77 100644 --- a/include/xstudio/bookmark/bookmark.hpp +++ b/include/xstudio/bookmark/bookmark.hpp @@ -31,6 +31,8 @@ namespace bookmark { } utility::Uuid bookmark_uuid_; + virtual size_t hash() const { return 0; } + private: utility::JsonStore store_; }; @@ -84,6 +86,7 @@ namespace bookmark { BookmarkDetail(const Bookmark &bm) { *this = bm; } utility::Uuid uuid_; + caf::actor_addr actor_addr_; std::optional owner_; std::optional enabled_; @@ -91,6 +94,8 @@ namespace bookmark { std::optional visible_; std::optional start_; std::optional duration_; + std::optional user_type_; + std::optional user_data_; std::optional author_; std::optional subject_; @@ -101,6 +106,7 @@ namespace bookmark { std::optional has_note_; std::optional has_annotation_; + std::optional annotation_hash_; std::optional media_reference_; std::optional media_flag_; @@ -111,13 +117,17 @@ namespace bookmark { template friend bool inspect(Inspector &f, BookmarkDetail &x) { return f.object(x).fields( f.field("uui", x.uuid_), + f.field("act", x.actor_addr_), f.field("own", x.owner_), f.field("ena", x.enabled_), f.field("foc", x.has_focus_), f.field("vis", x.visible_), f.field("sta", x.start_), f.field("dur", x.duration_), + f.field("utp", x.user_type_), + f.field("udt", x.user_data_), f.field("hasa", x.has_annotation_), + f.field("anh", x.annotation_hash_), f.field("hasn", x.has_note_), f.field("aut", x.author_), f.field("cat", x.category_), @@ -206,7 +216,7 @@ namespace bookmark { double get_second(const int frame) const { if (media_reference_) { - return timebase::to_seconds((*(media_reference_)).rate() * frame); + return timebase::to_seconds((*(media_reference_)).rate().to_flicks() * frame); } return 0.0; } @@ -237,11 +247,7 @@ namespace bookmark { std::string created() const { -#ifdef _WIN32 - auto dt = (created_ ? *created_ : std::chrono::high_resolution_clock::now()); -#else auto dt = (created_ ? *created_ : std::chrono::system_clock::now()); -#endif return utility::to_string(dt); } @@ -282,6 +288,7 @@ namespace bookmark { auto has_note() const { return static_cast(note_); } auto has_annotation() const { return static_cast(annotation_); } + size_t annotation_hash() const { return annotation_ ? annotation_->hash() : 0; } void create_note(); void create_annotation(); @@ -291,6 +298,9 @@ namespace bookmark { auto visible() const { return visible_; } auto start() const { return start_; } auto duration() const { return duration_; } + auto user_type() const { return user_type_; } + auto user_data() const { return user_data_; } + auto created() const { return created_; }; auto owner() const { return owner_; } @@ -304,6 +314,9 @@ namespace bookmark { void set_duration(const timebase::flicks duration = timebase::k_flicks_max) { duration_ = duration; } + void set_user_type(const std::string &user_type) { user_type_ = user_type; } + void set_user_data(const utility::JsonStore &user_data) { user_data_ = user_data; } + void set_created(const utility::sys_time_point &created) { created_ = created; } friend BookmarkDetail &BookmarkDetail::operator=(const Bookmark &bookmark); @@ -317,6 +330,9 @@ namespace bookmark { bool visible_{true}; timebase::flicks start_{timebase::k_flicks_low}; timebase::flicks duration_{timebase::k_flicks_max}; + std::string user_type_; + utility::JsonStore user_data_; + utility::sys_time_point created_{utility::sysclock::now()}; std::shared_ptr note_{nullptr}; std::shared_ptr annotation_{nullptr}; diff --git a/include/xstudio/bookmark/bookmark_actor.hpp b/include/xstudio/bookmark/bookmark_actor.hpp index 3e4757520..5363f5959 100644 --- a/include/xstudio/bookmark/bookmark_actor.hpp +++ b/include/xstudio/bookmark/bookmark_actor.hpp @@ -22,10 +22,16 @@ namespace bookmark { const char *name() const override { return NAME.c_str(); } + void on_exit() override; + private: inline static const std::string NAME = "BookmarkActor"; void init(); - caf::behavior make_behavior() override { return behavior_; } + caf::message_handler message_handler(); + + caf::behavior make_behavior() override { + return message_handler().or_else(base_.container_message_handler(this)); + } void build_annotation_via_plugin(const utility::JsonStore &anno_data); @@ -33,11 +39,10 @@ namespace bookmark { private: - caf::behavior behavior_; Bookmark base_; - caf::actor event_group_; caf::actor_addr owner_; caf::actor json_store_; + caf::disposable monitor_; }; } // namespace bookmark diff --git a/include/xstudio/bookmark/bookmarks_actor.hpp b/include/xstudio/bookmark/bookmarks_actor.hpp index ad805bca4..a4e7cd1de 100644 --- a/include/xstudio/bookmark/bookmarks_actor.hpp +++ b/include/xstudio/bookmark/bookmarks_actor.hpp @@ -22,18 +22,24 @@ namespace bookmark { static caf::message_handler default_event_handler(); + void on_exit() override; + private: inline static const std::string NAME = "BookmarksActor"; void init(); - caf::behavior make_behavior() override { return behavior_; } + caf::message_handler message_handler(); + + caf::behavior make_behavior() override { + return message_handler().or_else(base_.container_message_handler(this)); + } void csv_export( caf::typed_response_promise>> rp); + void monitor_bookmark(const caf::actor &actor); + private: - caf::behavior behavior_; Bookmarks base_; - caf::actor event_group_; std::string default_category_; std::map bookmarks_; diff --git a/include/xstudio/broadcast/broadcast_actor.hpp b/include/xstudio/broadcast/broadcast_actor.hpp index ad13db096..80ab3ea5e 100644 --- a/include/xstudio/broadcast/broadcast_actor.hpp +++ b/include/xstudio/broadcast/broadcast_actor.hpp @@ -22,10 +22,13 @@ namespace broadcast { caf::skippable_result broadcast_message(caf::scheduled_actor *, caf::message &); + void monitor_subscriber(const caf::actor &actor); + private: caf::behavior behavior_; caf::actor_addr owner_; std::set subscribers_; + std::map monitor_; }; } // namespace broadcast diff --git a/include/xstudio/caf_error.hpp b/include/xstudio/caf_error.hpp index 8842075f9..7ead7ea07 100644 --- a/include/xstudio/caf_error.hpp +++ b/include/xstudio/caf_error.hpp @@ -17,7 +17,7 @@ inline std::string to_string(xstudio_error x) { } } -inline bool from_string(caf::string_view in, xstudio_error &out) { +inline bool from_string(std::string_view in, xstudio_error &out) { if (in == "error") { out = xstudio_error::error; return true; diff --git a/include/xstudio/colour_pipeline/colour_pipeline.hpp b/include/xstudio/colour_pipeline/colour_pipeline.hpp index f00898e31..780f81822 100644 --- a/include/xstudio/colour_pipeline/colour_pipeline.hpp +++ b/include/xstudio/colour_pipeline/colour_pipeline.hpp @@ -31,13 +31,18 @@ namespace colour_pipeline { ColourOperationData(const std::string name) : name_(name) {} utility::Uuid uuid_; std::string name_; - std::string cache_id_; std::vector luts_; std::vector textures_; ui::viewport::GPUShaderPtr shader_; float order_index_; [[nodiscard]] size_t size() const; std::any user_data_; + + void set_cache_id(const std::string &id) { cache_id_ = id; } + [[nodiscard]] const std::string &cache_id() const { return cache_id_; } + + private: + std::string cache_id_; }; typedef std::shared_ptr ColourOperationDataPtr; @@ -47,8 +52,6 @@ namespace colour_pipeline { ColourPipelineData() = default; ColourPipelineData(const ColourPipelineData &o) = default; - std::string cache_id_; - void add_operation(const ColourOperationDataPtr &op) { auto p = std::lower_bound( ordered_colour_operations_.begin(), @@ -87,7 +90,12 @@ namespace colour_pipeline { return ordered_colour_operations_; } + void set_cache_id(const std::string &id) { cache_id_ = id; } + [[nodiscard]] const std::string &cache_id() const { return cache_id_; } + private: + std::string cache_id_; + /*Apply grades and other colour manipulations after stage_zero_operation_*/ std::vector ordered_colour_operations_; }; @@ -106,34 +114,18 @@ namespace colour_pipeline { virtual ~ColourPipeline(); - /* Given the colour related metadata of a media source, evaluate a hash - that is unique for any unique set of LUTs and/or GPU shaders necessary - to display the given source on the screen. You must also vary the hash - with the attributes/state of the ColourPipeline itself when this will - change the GPU shaders or LUTs required for the display of an image - coming from the given source. For example, if your display shader applies - a simple Exposure operation you would probably omit this from the hash - evaluation. However, the 'Display' attribute will probably affect the - shader/LUTs so it should be factored into the hash computation. - - This function should be as fast as possible as it is called every time - an image is displayed. Implement your own cacheing system if necessary - to avoid expensive evaluations that can otherwise be stored. - */ - [[nodiscard]] virtual std::string linearise_op_hash( - const utility::Uuid &source_uuid, - const utility::JsonStore &media_source_colour_metadata) = 0; - - [[nodiscard]] virtual std::string linear_to_display_op_hash( - const utility::Uuid &source_uuid, - const utility::JsonStore &media_source_colour_metadata) = 0; - /* Create the ColourOperationDataPtr containing the necessary LUT and shader data for linearising the source colourspace RGB data from the - given media source on the screen */ - virtual ColourOperationDataPtr linearise_op_data( - const utility::Uuid &source_uuid, - const utility::JsonStore &media_source_colour_metadata) = 0; + given media source on the screen. + + You MUST add an appropriate cache_id key value that is unqique for a + given transform for efficient cache retrieval to prevent computing this + data again unnecessarily. + + See OCIO plugin for rederence implementation. */ + virtual void linearise_op_data( + caf::typed_response_promise &rp, + const media::AVFrameID &media_ptr) = 0; /* If desired, override to return one or more colour operations that are applied to the linear colour value before the display space is applied. @@ -145,10 +137,17 @@ namespace colour_pipeline { } /* Create the ColourOperationDataPtr containing the necessary LUT and - shader data for transforming linear colour values into display space */ - virtual ColourOperationDataPtr linear_to_display_op_data( - const utility::Uuid &source_uuid, - const utility::JsonStore &media_source_colour_metadata) = 0; + shader data for transforming linear colour values into display space. + rp.deliver() MUST be called by your function to deliver the resulting + ColourOperationDataPtr OR the rp must be passed to a worker actor that + will call rp.deliver(). See OCIO plugin for reference implementation. + + You MUST add an appropriate cache_id key value that is unqique for a + given transform for efficient cache retrieval to prevent computing this + data again unnecessarily. */ + virtual void linear_to_display_op_data( + caf::typed_response_promise &rp, + const media::AVFrameID &media_ptr) = 0; /* For the given image build a dictionary of shader uniform names and their corresponding values to be used to set the uniform values in your @@ -168,52 +167,62 @@ namespace colour_pipeline { const std::string &manufacturer, const std::string &serialNumber) = 0; - /* Re-implement and return true if your class supports a design that - allows for 'workers'. Workers are clones of your main class instance - that expensive 'work' can be delegated to. In this case the following - five functions could be called on the workers instead of the main class: - - linearise_op_hash - - linear_to_display_op_hash - - linearise_op_data - - linear_to_display_op_data - - intermediate_operations - The mechanism relies on (automatic) syncing the Attributes on the - workers with the main instance: if the above five functions will always - return the same result for a given state of the Attributes belonging to - your class then workers can be used to paralellise colour operations. - If these functions in your ColourPipeline rely on state data other than - Attributes, however, you cannot use workers. */ - virtual bool allow_workers() const { return false; } - - /* In conjunction with the allow_wokers method, you will also need to - re-implement this function. If your class is called MyColourPipe the - reimplementation would look exactly like this: - caf::actor self_spawn(const utility::JsonStore &s) override { return - spawn(s); } - */ - virtual caf::actor self_spawn(const utility::JsonStore &s) { return caf::actor(); } - /* implement this method to extend information about a pixel that is under the mouse pointer. For example, pixel_info might include linear RGB information, but we want to add RGB information after a viewing transform has been applied, say. */ virtual void extend_pixel_info( media_reader::PixelInfo &pixel_info, const media::AVFrameID &frame_id) {} - virtual thumbnail::ThumbnailBufferPtr process_thumbnail( - const media::AVFrameID &media_ptr, const thumbnail::ThumbnailBufferPtr &buf) = 0; + /* Implement this method for converting the colourspace of a thumbnail + buffer from media source colourspace to a DISPLAY colourspace. + + The incoming buffer is RGB float 32 pixel format and in the native + colourspace of the source media. + + Information about the source colourspace should be available in the json + dict returned by 'params()' method on the media_ptr (as well as other + info provided by AVFrameID such as the URI, frame number etc iuf you + need it.) - virtual std::string fast_display_transform_hash(const media::AVFrameID &media_ptr) = 0; + For example, a thumbnail from an EXR source would most likely be in a + linear colourspace which we want to convert into a display space. + + The resulting buffer, or an error if one occurs, MUST be delivered by + calling rp.deliver(). You can call rp.deliver within your function or + delegate the work to a worker actor which then MUST call rp.deliver. + + See the OCIO plugin for a reference implementation. */ + virtual void process_thumbnail( + caf::typed_response_promise &rp, + const media::AVFrameID &media_ptr, + const thumbnail::ThumbnailBufferPtr &buf) = 0; + + /* This function should return a unique string based on the current + statue of the plugin plus RELEVANT properties of the media_ptr that + influence its colourpsace transforms to display the corresponding image + on screen correctly. + + For example, you can make a hash from the OCIO View & Display values + plus the source colourspace and optionally grading data attached to + the media. This should be sufficient to tell the calling class when it + needs to compute new colour opration data and when it can use cached + data. + + This function must be as fast as possible as it is called on every frame + refresh. + + See the OCIO plugin for a reference implementation. */ + virtual size_t fast_display_transform_hash(const media::AVFrameID &media_ptr) = 0; protected: void make_pre_draw_gpu_hook( - caf::typed_response_promise rp, const int viewer_index); + const std::string &viewport_name, + caf::typed_response_promise rp); void attribute_changed(const utility::Uuid &attr_uuid, const int role) override; caf::message_handler message_handler_extensions() override; - bool is_worker() const { return is_worker_; } - utility::Uuid uuid_; private: @@ -252,17 +261,15 @@ namespace colour_pipeline { in_flight_requests_; std::map> cache_keys_cache_; - utility::JsonStore init_data_; - caf::actor worker_pool_; caf::actor thumbnail_processor_pool_; caf::actor pixel_probe_worker_; caf::actor cache_; std::vector workers_; - bool is_worker_ = false; bool colour_ops_loaded_ = false; std::vector colour_op_plugins_; - std::vector, int>> + std::vector< + std::pair>> hook_requests_; }; diff --git a/include/xstudio/conform/conform_manager_actor.hpp b/include/xstudio/conform/conform_manager_actor.hpp index 7c2a059b6..0fbbcb771 100644 --- a/include/xstudio/conform/conform_manager_actor.hpp +++ b/include/xstudio/conform/conform_manager_actor.hpp @@ -4,6 +4,8 @@ #include #include "xstudio/media_reader/media_reader.hpp" +#include "xstudio/module/module.hpp" +#include "xstudio/utility/json_store_sync.hpp" namespace xstudio::conform { class ConformWorkerActor : public caf::event_based_actor { @@ -15,26 +17,122 @@ class ConformWorkerActor : public caf::event_based_actor { const char *name() const override { return NAME.c_str(); } private: + void process_request( + caf::typed_response_promise rp, const ConformRequest &request); + + void process_task_request( + caf::typed_response_promise rp, + const std::string &conform_task, + const ConformRequest &request); + + void get_media_sequences( + caf::typed_response_promise>>> rp, + const utility::UuidActorVector &media); + + void prepare_sequence( + caf::typed_response_promise rp, + const utility::UuidActor &timeline, + const bool only_create_conform_track); + + void conform_to_media( + caf::typed_response_promise rp, + const std::string &conform_task, + const utility::JsonStore &conform_operations, + const utility::UuidActor &playlist, + const utility::UuidActor &container, + const std::string &item_type, + const utility::UuidActorVector &items, + const utility::UuidVector &insert_before); + + void conform_tracks_to_sequence( + caf::typed_response_promise rp, + const utility::UuidActor &source_playlist, + const utility::UuidActor &source_timeline, + const utility::UuidActorVector &tracks, + const utility::UuidActor &target_playlist, + const utility::UuidActor &target_timeline, + const utility::UuidActor &conform_track); + + void conform_to_timeline( + caf::typed_response_promise rp, + const utility::JsonStore &conform_operations, + const utility::UuidActor &playlist, + const utility::UuidActor &timeline, + const utility::UuidActor &conform_track, + const utility::UuidActorVector &media); + + void conform_step_get_playlist_json( + caf::typed_response_promise rp, + const std::string &conform_task, + ConformRequest conform_request); + + void conform_step_get_clip_json( + caf::typed_response_promise rp, + const std::string &conform_task, + ConformRequest &conform_request); + + void conform_step_get_clip_media( + caf::typed_response_promise rp, + const std::string &conform_task, + ConformRequest &conform_request); + + void conform_step_get_media_json( + caf::typed_response_promise rp, + const std::string &conform_task, + ConformRequest &conform_request); + + void conform_step_get_media_source( + caf::typed_response_promise rp, + const std::string &conform_task, + ConformRequest &conform_request); + + void conform_chain( + caf::typed_response_promise rp, ConformRequest &conform_request); + + + void find_matched( + caf::typed_response_promise rp, + const std::string &key, + const utility::UuidActor &clip, + const utility::UuidActor &timeline); + inline static const std::string NAME = "ConformWorkerActor"; caf::behavior behavior_; + std::vector conformers_; + bool initialised_{false}; }; -class ConformManagerActor : public caf::event_based_actor { +class ConformManagerActor : public caf::event_based_actor, public module::Module { public: ConformManagerActor( caf::actor_config &cfg, const utility::Uuid uuid = utility::Uuid::generate()); ~ConformManagerActor() override = default; - caf::behavior make_behavior() override { return behavior_; } + caf::behavior make_behavior() override { + set_parent_actor_addr(actor_cast(this)); + return message_handler_extensions().or_else( + message_handler_.or_else(module::Module::message_handler())); + } + void on_exit() override; const char *name() const override { return NAME.c_str(); } + protected: + caf::message_handler message_handler_; + + caf::message_handler message_handler_extensions(); + private: inline static const std::string NAME = "ConformManagerActor"; - caf::behavior behavior_; utility::Uuid uuid_; caf::actor event_group_; - std::vector tasks_; + caf::actor pool_; + size_t worker_count_{5}; + + // stores information on conforming actions. + utility::JsonStoreSync data_; + utility::Uuid data_uuid_{utility::Uuid::generate()}; }; } // namespace xstudio::conform diff --git a/include/xstudio/conform/conformer.hpp b/include/xstudio/conform/conformer.hpp index 07ab65975..cfc18ac5b 100644 --- a/include/xstudio/conform/conformer.hpp +++ b/include/xstudio/conform/conformer.hpp @@ -12,6 +12,7 @@ #include "xstudio/global_store/global_store.hpp" #include "xstudio/plugin_manager/plugin_factory.hpp" +#include "xstudio/timeline/item.hpp" #include "xstudio/utility/media_reference.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/helpers.hpp" @@ -21,47 +22,157 @@ namespace conform { // item json, we might need to expand this with more detail, may need to support clips. // might need a custom handler on items to generate more usful hints. - typedef std::tuple ConformRequestItem; + + // item, metadata, insert before this in media list. + const auto ConformOperationsJSON = R"({ + "match_only": false, + "create_media": false, + "remove_media": false, + "insert_media": false, + "new_track_name": "", + "remove_failed_clip": false, + "replace_clip": false, + "only_one_clip_match": false + })"_json; + + + // item, media, before + struct ConformRequestItem { + ConformRequestItem() {} + + ConformRequestItem( + const utility::UuidActor item, + const utility::UuidActor media, + const utility::Uuid before = utility::Uuid()) + : item_(std::move(item)), media_(std::move(media)), before_(std::move(before)) {} + + ConformRequestItem( + const utility::UuidActor item, + const utility::UuidActor media, + const timeline::Item clip, + const utility::Uuid clip_track_uuid) + : item_(std::move(item)), + media_(std::move(media)), + clip_(std::move(clip)), + clip_track_uuid_(std::move(clip_track_uuid)) {} + + template friend bool inspect(Inspector &f, ConformRequestItem &x) { + return f.object(x).fields( + f.field("it", x.item_), + f.field("me", x.media_), + f.field("be", x.before_), + f.field("cl", x.clip_), + f.field("ctu", x.clip_track_uuid_)); + } + + utility::UuidActor item_; + utility::UuidActor media_; + utility::Uuid before_; + timeline::Item clip_; + utility::Uuid clip_track_uuid_; + }; struct ConformRequest { ConformRequest( const utility::UuidActor playlist, - const utility::JsonStore playlist_json, + const utility::UuidActor container, + const std::string item_type, const std::vector items) : playlist_(std::move(playlist)), - playlist_json_(std::move(playlist_json)), - items_(std::move(items)) {} - ConformRequest() = default; + container_(std::move(container)), + item_type_(std::move(item_type)), + items_(std::move(items)), + operations_(ConformOperationsJSON) {} + ConformRequest( + const utility::UuidActor playlist, + const utility::UuidActor container, + const timeline::Item template_track, + const std::vector items) + : playlist_(std::move(playlist)), + container_(std::move(container)), + template_tracks_({std::move(template_track)}), + item_type_("Clip"), + items_(std::move(items)), + operations_(ConformOperationsJSON) {} + + ConformRequest( + const utility::UuidActor playlist, + const utility::UuidActor container, + const std::vector template_tracks, + const std::vector items) + : playlist_(std::move(playlist)), + container_(std::move(container)), + template_tracks_(std::move(template_tracks)), + item_type_("Clip"), + items_(std::move(items)), + operations_(ConformOperationsJSON) {} + + ConformRequest() : operations_(ConformOperationsJSON) {} ~ConformRequest() = default; utility::UuidActor playlist_; - utility::JsonStore playlist_json_; + utility::UuidActor container_; + std::string item_type_; std::vector< // request item ConformRequestItem> items_; + utility::JsonStore operations_; + utility::JsonStore detail_; + std::map metadata_; + std::vector template_tracks_; + + template friend bool inspect(Inspector &f, ConformRequest &x) { return f.object(x).fields( f.field("pl", x.playlist_), - f.field("plj", x.playlist_json_), + f.field("ct", x.container_), + f.field("op", x.operations_), + f.field("dt", x.detail_), + f.field("cm", x.metadata_), + f.field("it", x.item_type_), + f.field("t", x.template_tracks_), f.field("items", x.items_)); } + + void dump() const { + spdlog::warn("playlist {}", to_string(playlist_)); + spdlog::warn("container {}", to_string(container_)); + spdlog::warn("item type {}", item_type_); + for (const auto &i : items_) { + spdlog::warn( + "item {} media {} before {} item {} track {}", + to_string(i.item_), + to_string(i.media_), + to_string(i.before_), + i.clip_.name(), + to_string(i.clip_track_uuid_)); + } + for (const auto &i : metadata_) { + spdlog::warn("metadata {} {}", to_string(i.first), i.second.dump(2)); + } + } }; - typedef std::tuple< - bool, // exists in playlist - utility::MediaReference, // media json - utility::UuidActor // reference to media actor - > + typedef std::tuple ConformReplyItem; struct ConformReply { - ConformReply() = default; + ConformReply() : operations_(ConformOperationsJSON) {} + ConformReply(ConformRequest request) + : operations_(ConformOperationsJSON), request_(std::move(request)) {} ~ConformReply() = default; + + ConformRequest request_; + utility::JsonStore operations_; std::vector>> items_; template friend bool inspect(Inspector &f, ConformReply &x) { - return f.object(x).fields(f.field("items", x.items_)); + return f.object(x).fields( + f.field("req", x.request_), + f.field("op", x.operations_), + f.field("items", x.items_)); } }; @@ -71,12 +182,23 @@ namespace conform { virtual ~Conformer() = default; virtual void update_preferences(const utility::JsonStore &prefs); - virtual ConformReply conform_request( - const std::string &conform_task, - const utility::JsonStore &conform_detail, - const ConformRequest &request); + virtual ConformReply + conform_request(const std::string &conform_task, const ConformRequest &request); + + virtual ConformReply conform_request(const ConformRequest &request); + virtual bool conform_prepare_timeline( + const utility::UuidActor &timeline, const bool only_create_conform_track); + virtual std::vector< + std::optional>> + conform_find_timeline( + const std::vector> &media); virtual std::vector conform_tasks(); + + virtual utility::UuidActorVector find_matching( + const std::string &key, + const std::pair &needle, + const std::vector> &haystack); }; template @@ -121,9 +243,35 @@ namespace conform { [=](conform_atom, const std::string &conform_task, - const utility::JsonStore &conform_detail, const ConformRequest &request) -> ConformReply { - return conform_.conform_request(conform_task, conform_detail, request); + return conform_.conform_request(conform_task, request); + }, + + // find sequence related to media. + [=](conform_atom, + const std::vector> &media) + -> std::vector< + std::optional>> { + return conform_.conform_find_timeline(media); + }, + + [=](conform_atom, const ConformRequest &request) -> ConformReply { + return conform_.conform_request(request); + }, + + [=](conform_atom, + const std::string &key, + const std::pair &needle, + const std::vector> + &haystack) -> utility::UuidActorVector { + return conform_.find_matching(key, needle, haystack); + }, + + [=](conform_atom, + const utility::UuidActor &timeline, + const bool only_create_conform_track) -> bool { + return conform_.conform_prepare_timeline( + timeline, only_create_conform_track); }, [=](conform_tasks_atom) -> std::vector { @@ -134,7 +282,8 @@ namespace conform { const utility::JsonStore & /*change*/, const std::string & /*path*/, const utility::JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full) + .delegate(actor_cast(this)); }, [=](json_store::update_atom, const utility::JsonStore &js) { diff --git a/include/xstudio/contact_sheet/contact_sheet.hpp b/include/xstudio/contact_sheet/contact_sheet.hpp index af0833085..f5d3932d4 100644 --- a/include/xstudio/contact_sheet/contact_sheet.hpp +++ b/include/xstudio/contact_sheet/contact_sheet.hpp @@ -7,7 +7,6 @@ #include "xstudio/playhead/playhead.hpp" #include "xstudio/utility/container.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" @@ -48,15 +47,10 @@ namespace contact_sheet { [[nodiscard]] utility::FrameRate playhead_rate() const { return playhead_rate_; } void set_playhead_rate(const utility::FrameRate &rate) { playhead_rate_ = rate; } - void set_compare_mode(const playhead::CompareMode compare_mode = playhead::CM_GRID) { - compare_mode_ = compare_mode; - } - [[nodiscard]] playhead::CompareMode compare_mode() const { return compare_mode_; } private: utility::UuidListContainer media_list_; utility::FrameRate playhead_rate_; - playhead::CompareMode compare_mode_ = {playhead::CM_GRID}; utility::UuidActor playhead_; }; } // namespace contact_sheet diff --git a/include/xstudio/contact_sheet/contact_sheet_actor.hpp b/include/xstudio/contact_sheet/contact_sheet_actor.hpp index 94cd71a43..0e3f84bbb 100644 --- a/include/xstudio/contact_sheet/contact_sheet_actor.hpp +++ b/include/xstudio/contact_sheet/contact_sheet_actor.hpp @@ -3,13 +3,30 @@ #include -#include "xstudio/contact_sheet/contact_sheet.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/uuid.hpp" +#include "xstudio/subset/subset_actor.hpp" namespace xstudio { namespace contact_sheet { - class ContactSheetActor : public caf::event_based_actor { + + class ContactSheetActor : public subset::SubsetActor { + + public: + ContactSheetActor( + caf::actor_config &cfg, caf::actor playlist, const utility::JsonStore &jsn); + ContactSheetActor(caf::actor_config &cfg, caf::actor playlist, const std::string &name); + + caf::behavior make_behavior() override { + return override_behaviour_.or_else(subset::SubsetActor::make_behavior()); + } + + private: + void init(); + inline static const std::string NAME = "ContactSheetActor"; + + caf::message_handler override_behaviour_; + }; + + /*class ContactSheetActor : public caf::event_based_actor { public: ContactSheetActor( caf::actor_config &cfg, caf::actor playlist, const utility::JsonStore &jsn); @@ -27,15 +44,22 @@ namespace contact_sheet { const utility::Uuid &uuid, const utility::Uuid &before_uuid = utility::Uuid()); bool remove_media(caf::actor actor, const utility::Uuid &uuid); - caf::behavior make_behavior() override { return behavior_; } - void sort_alphabetically(); + caf::message_handler message_handler(); + + caf::behavior make_behavior() override { + return message_handler().or_else(base_.container_message_handler(this)); + } + void sort_by_media_display_info(const int sort_column_index, const bool ascending); private: - caf::behavior behavior_; caf::actor_addr playlist_; ContactSheet base_; + + caf::actor change_event_group_; + caf::actor selection_actor_; + utility::UuidActorMap actors_; utility::UuidActor playhead_; - }; + };*/ } // namespace contact_sheet } // namespace xstudio diff --git a/include/xstudio/data_source/data_source.hpp b/include/xstudio/data_source/data_source.hpp index 10599236b..bda9398f0 100644 --- a/include/xstudio/data_source/data_source.hpp +++ b/include/xstudio/data_source/data_source.hpp @@ -92,37 +92,40 @@ namespace data_source { return utility::JsonStore(); }, - [=](data_source::use_data_atom, + [=](use_data_atom, const caf::uri &) -> utility::UuidActorVector { + return utility::UuidActorVector(); + }, + + [=](use_data_atom, const caf::actor &media, const utility::FrameRate &media_rate) -> result { return utility::UuidActorVector(); }, - [=](put_data_atom, const utility::JsonStore &js) -> result { + [=](use_data_atom, const utility::JsonStore &js) -> result { try { - return data_source_.put_data(js); + return data_source_.use_data(js); } catch (const std::exception &err) { return make_error(xstudio_error::error, err.what()); } return utility::JsonStore(); }, - [=](post_data_atom, - const utility::JsonStore &js) -> result { + + [=](use_data_atom, const utility::JsonStore &, const bool) + -> result { return utility::UuidActorVector(); }, + + [=](put_data_atom, const utility::JsonStore &js) -> result { try { - return data_source_.post_data(js); + return data_source_.put_data(js); } catch (const std::exception &err) { return make_error(xstudio_error::error, err.what()); } return utility::JsonStore(); }, - [=](use_data_atom, const caf::uri &) -> utility::UuidActorVector { - return utility::UuidActorVector(); - }, - [=](use_data_atom, const utility::JsonStore &, const bool) - -> result { return utility::UuidActorVector(); }, - [=](use_data_atom, const utility::JsonStore &js) -> result { + [=](post_data_atom, + const utility::JsonStore &js) -> result { try { - return data_source_.use_data(js); + return data_source_.post_data(js); } catch (const std::exception &err) { return make_error(xstudio_error::error, err.what()); } diff --git a/include/xstudio/embedded_python/embedded_python.hpp b/include/xstudio/embedded_python/embedded_python.hpp index 83e06f247..ddd7212c0 100644 --- a/include/xstudio/embedded_python/embedded_python.hpp +++ b/include/xstudio/embedded_python/embedded_python.hpp @@ -4,6 +4,8 @@ #include #include #include +#define PYBIND11_DETAILED_ERROR_MESSAGES + #include // everything needed for embedding #include @@ -52,17 +54,11 @@ namespace embedded_python { bool input_session(const utility::Uuid &session_uuid, const std::string &input); bool input_ctrl_c_session(const utility::Uuid &session_uuid); - void push_caf_message_to_py_callbacks(caf::actor sender, caf::message &m); - - void add_message_callback(const py::tuple &xs); - - static void s_add_message_callback(const py::tuple &xs); - void finalize(); private: // py::function cb_; - std::map> message_handler_callbacks_; + std::map plugin_registry_; EmbeddedPythonActor *parent_; diff --git a/include/xstudio/embedded_python/embedded_python_actor.hpp b/include/xstudio/embedded_python/embedded_python_actor.hpp index 46825f748..66bff62de 100644 --- a/include/xstudio/embedded_python/embedded_python_actor.hpp +++ b/include/xstudio/embedded_python/embedded_python_actor.hpp @@ -2,10 +2,11 @@ #pragma once #include +#include #include "xstudio/embedded_python/embedded_python.hpp" #include "xstudio/utility/exports.hpp" -#include "xstudio/utility/json_store.hpp" +#include "xstudio/utility/json_store_sync.hpp" #include "xstudio/utility/uuid.hpp" namespace xstudio { @@ -21,6 +22,10 @@ namespace embedded_python { void join_broadcast(caf::actor act, const std::string &plugin_name); void join_broadcast(caf::actor act); + void leave_broadcast(caf::actor act); + void delayed_callback(utility::Uuid &cb_id, const int microseconds_delay); + + void main_loop(); private: inline static const std::string NAME = "EmbeddedPythonActor"; @@ -28,9 +33,21 @@ namespace embedded_python { void act() override; void init(); + void refresh_snippets(const std::vector &paths); + nlohmann::json refresh_snippet( + const std::filesystem::path &path, + const std::string &menu_path = "", + const std::string &snippet_type = ""); + void update_preferences(const utility::JsonStore &); + private: EmbeddedPython base_; caf::actor event_group_; + + // stores information on conforming actions. + utility::JsonStoreSync data_; + utility::Uuid data_uuid_{utility::Uuid::generate()}; + std::vector snippet_paths_; }; } // namespace embedded_python } // namespace xstudio diff --git a/include/xstudio/embedded_python/python_thread_locker.hpp b/include/xstudio/embedded_python/python_thread_locker.hpp new file mode 100644 index 000000000..c800b1e74 --- /dev/null +++ b/include/xstudio/embedded_python/python_thread_locker.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "xstudio/utility/blind_data.hpp" + +namespace xstudio { +namespace embedded_python { + + class PythonThreadLocker : public utility::BlindDataObject { + + public: + // Mutex shared between actors needing to execute python and the main + // embedded python actor + std::mutex mutex_; + }; + +} // end namespace embedded_python +} // namespace xstudio diff --git a/include/xstudio/enums.hpp b/include/xstudio/enums.hpp index 712188f2b..26416c0b1 100644 --- a/include/xstudio/enums.hpp +++ b/include/xstudio/enums.hpp @@ -9,6 +9,8 @@ #include "xstudio/plugin_manager/enums.hpp" #include "xstudio/session/enums.hpp" #include "xstudio/shotgun_client/enums.hpp" -#include "xstudio/ui/viewport/enums.hpp" #include "xstudio/thumbnail/enums.hpp" +#include "xstudio/timeline/enums.hpp" +#include "xstudio/ui/enums.hpp" +#include "xstudio/ui/viewport/enums.hpp" #include "xstudio/utility/enums.hpp" \ No newline at end of file diff --git a/include/xstudio/event/event.hpp b/include/xstudio/event/event.hpp deleted file mode 100644 index 306e25e14..000000000 --- a/include/xstudio/event/event.hpp +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include -#include - -#include -#include -#include - -#include "xstudio/utility/container.hpp" -#include "xstudio/utility/uuid.hpp" - -namespace xstudio { -namespace event { - - class Event { - public: - Event() = default; - - Event( - const std::string progress_text, - const int progress = 0, - const int progress_minimum = 0, - const int progress_maximum = 100, - const std::set targets = std::set(), - const utility::Uuid event = utility::Uuid::generate()) - : progress_text_(std::move(progress_text)), - progress_(progress), - progress_minimum_(progress_minimum), - progress_maximum_(progress_maximum), - event_(std::move(event)), - targets_(std::move(targets)) {} - - virtual ~Event() = default; - - [[nodiscard]] auto progress() const { return progress_; } - [[nodiscard]] auto progress_minimum() const { return progress_minimum_; } - [[nodiscard]] auto progress_maximum() const { return progress_maximum_; } - [[nodiscard]] auto complete() const { return progress_ == progress_maximum_; } - [[nodiscard]] auto event() const { return event_; } - [[nodiscard]] auto targets() const { return targets_; } - [[nodiscard]] auto contains(const utility::Uuid &value) const { - return static_cast(targets_.count(value)); - } - [[nodiscard]] auto progress_percentage() const { - float percentage = 100.0; - auto range = progress_maximum_ - progress_minimum_; - if (range > 0) { - auto div = 100.0f / static_cast(range); - percentage = div * static_cast(progress_ - progress_minimum_); - } - - return percentage; - } - - [[nodiscard]] auto progress_text() const { return progress_text_; } - [[nodiscard]] auto progress_text_percentage() const { - return std::string(fmt::format( - progress_text_, std::string(fmt::format("{:>5.1f}%", progress_percentage())))); - } - [[nodiscard]] auto progress_text_range() const { - return std::string(fmt::format( - progress_text_, - std::string(fmt::format( - "{}/{}", - progress_ - progress_minimum_, - progress_maximum_ - progress_minimum_)))); - } - - template friend bool inspect(Inspector &f, Event &x) { - return f.object(x).fields( - f.field("txt", x.progress_text_), - f.field("val", x.progress_), - f.field("min", x.progress_minimum_), - f.field("max", x.progress_maximum_), - f.field("evt", x.event_), - f.field("tar", x.targets_)); - } - - void set_progress_text(const std::string value) { progress_text_ = std::move(value); } - void set_progress(const int value) { progress_ = value; } - void increment_progress(const int value = 1) { - progress_ = std::min(progress_ + value, progress_maximum_); - } - void set_complete() { progress_ = progress_maximum_; } - void set_progress_minimum(const int value) { progress_minimum_ = value; } - void set_progress_maximum(const int value) { progress_maximum_ = value; } - void set_event(const utility::Uuid value) { event_ = std::move(value); } - void set_targets(const std::set value) { targets_ = std::move(value); } - void add_target(const utility::Uuid value) { targets_.emplace(value); } - - private: - std::string progress_text_{"{}"}; - int progress_{0}; - int progress_minimum_{0}; - int progress_maximum_{0}; - utility::Uuid event_; - std::set targets_; - }; - - inline utility::Uuid send_event(caf::event_based_actor *act, const Event &event) { - act->send( - act->system().groups().get_local(global_event_group), utility::event_atom_v, event); - return event.event(); - } - inline utility::Uuid send_event(caf::blocking_actor *act, const Event &event) { - act->send( - act->system().groups().get_local(global_event_group), utility::event_atom_v, event); - return event.event(); - } - -} // namespace event -} // namespace xstudio diff --git a/include/xstudio/global/global_actor.hpp b/include/xstudio/global/global_actor.hpp index ad78df71e..a8e0ff8f0 100644 --- a/include/xstudio/global/global_actor.hpp +++ b/include/xstudio/global/global_actor.hpp @@ -5,6 +5,7 @@ #include #include +#include "xstudio/audio/audio_output_actor.hpp" #include "xstudio/global/enums.hpp" #include "xstudio/utility/exports.hpp" #include "xstudio/utility/remote_session_file.hpp" @@ -13,12 +14,30 @@ namespace xstudio { namespace global { + class DLL_PUBLIC APIActor : public caf::event_based_actor { + public: + APIActor(caf::actor_config &cfg, const caf::actor &global); + ~APIActor() override = default; + const char *name() const override { return NAME.c_str(); } + caf::behavior make_behavior() override { return behavior_; } + + private: + inline static const std::string NAME = "APIActor"; + caf::behavior behavior_; + caf::actor global_; + + bool allow_unauthenticated_{false}; + utility::JsonStore authentication_passwords_; + utility::JsonStore authentication_keys_; + }; class DLL_PUBLIC GlobalActor : public caf::event_based_actor { public: GlobalActor( - caf::actor_config &cfg, const utility::JsonStore &prefs = utility::JsonStore()); - ~GlobalActor() override = default; + caf::actor_config &cfg, + const utility::JsonStore &prefs = utility::JsonStore(), + const bool embedded_python = true); + ~GlobalActor(); const char *name() const override { return NAME.c_str(); } void on_exit() override; @@ -30,18 +49,29 @@ namespace global { const int maximum, const std::string &bind_address, caf::actor a); - void init(const utility::JsonStore &prefs = utility::JsonStore()); + void init( + const utility::JsonStore &prefs = utility::JsonStore(), + const bool embedded_python = true); void connect_api(const caf::actor &embedded_python); - void connect_sync_api(); void disconnect_api(const caf::actor &embedded_python, const bool force = false); - void disconnect_sync_api(const bool force = false); + + template + caf::actor spawn_audio_output_actor(const utility::JsonStore &prefs) { + static_assert( + std::is_base_of::value, + "Not derived from audio::AudioOutputDevice"); + return spawn( + std::shared_ptr(new AudioOutputDev(prefs)), true); + } inline static const std::string NAME = "GlobalActor"; caf::behavior behavior_; caf::actor studio_; caf::actor ui_studio_; caf::actor event_group_; + caf::actor apia_; + caf::actor gsa_; bool python_enabled_; bool api_enabled_; @@ -51,14 +81,7 @@ namespace global { std::string bind_address_; bool connected_; - bool sync_api_enabled_; - int sync_port_minimum_; - int sync_port_; - int sync_port_maximum_; - std::string sync_bind_address_; - bool sync_connected_; std::string remote_api_session_name_; - std::string remote_sync_session_name_; utility::RemoteSessionManager rsm_; bool session_autosave_{false}; @@ -67,6 +90,7 @@ namespace global { size_t session_autosave_hash_{0}; StatusType status_{StatusType::ST_NONE}; std::set busy_; + utility::JsonStore file_map_regex_; }; } // namespace global } // namespace xstudio diff --git a/include/xstudio/global/xstudio_actor_system.hpp b/include/xstudio/global/xstudio_actor_system.hpp new file mode 100644 index 000000000..eb66b9cef --- /dev/null +++ b/include/xstudio/global/xstudio_actor_system.hpp @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include "xstudio/utility/json_store.hpp" + +namespace xstudio::caf_utility { +class caf_config; +} + +namespace xstudio { + +// description: Helper class to set-up and manage an instance of a caf actor +// system and the xstudio 'global' actor that manages the instances of the +// various core components of xSTUDIO. +// +// Note that any application embedding xSTUDIO (and the main xSTUDIO applicaiton +// itself) will need to call the statuc 'instance' method of this class to +// intialise the actor system and create the global actor. Other set-up like +// loading preferences, starting the logging etc. is handled by this class. +class CafActorSystem { + + public: + static void exit(); + static CafActorSystem *instance(); + static caf::actor_system &system() { return instance()->__system(); } + static caf::actor global_actor( + const bool embedded_python, + const std::string &name = "XStudio", + const utility::JsonStore &prefs = utility::JsonStore()) { + return instance()->__global_actor(name, prefs, embedded_python); + } + ~CafActorSystem(); + + private: + caf::actor_system &__system() { return *the_system_; } + caf::actor __global_actor( + const std::string &name, const utility::JsonStore &prefs, const bool embedded_python); + + CafActorSystem(); + caf::actor_system *the_system_; + caf_utility::caf_config *config_; + caf::actor global_actor_; +}; + +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/global_store/global_store.hpp b/include/xstudio/global_store/global_store.hpp index 1fea8d83b..a7f097af8 100644 --- a/include/xstudio/global_store/global_store.hpp +++ b/include/xstudio/global_store/global_store.hpp @@ -38,6 +38,8 @@ namespace global_store { virtual ~GlobalStoreDef() = default; std::string path_; + std::string display_name_; + std::string category_; nlohmann::json value_; nlohmann::json default_value_; std::string datatype_; @@ -45,8 +47,11 @@ namespace global_store { std::string overridden_path_; nlohmann::json minimum_value_; nlohmann::json maximum_value_; + nlohmann::json options_; [[nodiscard]] std::string path() const { return path_; } + [[nodiscard]] std::string display_name() const { return display_name_; } + [[nodiscard]] std::string category() const { return category_; } [[nodiscard]] std::string datatype() const { return datatype_; } [[nodiscard]] std::string description() const { return description_; } [[nodiscard]] std::string overridden_path() const { return overridden_path_; } @@ -56,6 +61,7 @@ namespace global_store { [[nodiscard]] nlohmann::json default_value() const { return default_value_; } [[nodiscard]] nlohmann::json minimum_value() const { return minimum_value_; } [[nodiscard]] nlohmann::json maximum_value() const { return maximum_value_; } + [[nodiscard]] nlohmann::json options() const { return options_; } operator std::string() const { return path_; } operator nlohmann::json() const { @@ -65,8 +71,11 @@ namespace global_store { {"default_value", default_value_}, {"description", description_}, {"datatype", datatype_}, + {"category", category_}, {"minimum", minimum_value_}, {"maximum", maximum_value_}, + {"display_name", display_name_}, + {"options", options_}, {"overridden_path", overridden_path_}}; } }; @@ -117,11 +126,27 @@ namespace global_store { return preference_property(js, path, "default_value"); } + template + inline result_type + preference_options(const utility::JsonStore &js, const std::string &path) { + return preference_property(js, path, "options"); + } + + inline std::string + preference_category(const utility::JsonStore &js, const std::string &path) { + return preference_property(js, path, "category"); + } + inline std::string preference_description(const utility::JsonStore &js, const std::string &path) { return preference_property(js, path, "description"); } + inline std::string + preference_display_name(const utility::JsonStore &js, const std::string &path) { + return preference_property(js, path, "display_name"); + } + inline std::string preference_overridden_path(const utility::JsonStore &js, const std::string &path) { return preference_property(js, path, "overridden_path"); @@ -174,7 +199,20 @@ namespace global_store { const std::string &override_path = ""); void set_global_store_def(utility::JsonStore &js, const GlobalStoreDef &gsd); + // note: extra_prefs_paths allows us to add preference files (or folders) that + // are loaded BEFORE the user's own preference files are loaded. The + // override_prefs_paths will be loaded AFTER the user's own prefs, providing + // a way to override user settings if required (e.g. on special machines like + // in a playback suite we may want to force certain xstudio settings to suit + // the machine and ignore the user's own settings) + bool load_preferences( + utility::JsonStore &js, + const bool load_user_prefs = true, + const std::vector &extra_prefs_paths = std::vector(), + const std::vector &override_prefs_paths = std::vector()); + bool preference_load_defaults(utility::JsonStore &js, const std::string &path); + void preference_load_overrides(utility::JsonStore &js, const std::vector &paths); @@ -233,6 +271,15 @@ namespace global_store { JsonStoreHelper::set(value, path + "/value", async, broacast_change); } + template + inline void set_overridden_path( + const value_type &value, + const std::string &path, + const bool async = true, + const bool broacast_change = true) { + JsonStoreHelper::set(value, path + "/overridden_path", async, broacast_change); + } + /*If a preference is found at path return the value. Otherwise build a preference at path and return default.*/ utility::JsonStore get_existing_or_create_new_preference( diff --git a/include/xstudio/global_store/global_store_actor.hpp b/include/xstudio/global_store/global_store_actor.hpp index 415ca5680..1a65e7d4d 100644 --- a/include/xstudio/global_store/global_store_actor.hpp +++ b/include/xstudio/global_store/global_store_actor.hpp @@ -29,8 +29,11 @@ namespace global_store { std::string reg_value = global_store_registry); void on_exit() override; const char *name() const override { return NAME.c_str(); } + caf::message_handler message_handler(); - caf::behavior make_behavior() override { return behavior_; } + caf::behavior make_behavior() override { + return message_handler().or_else(base_.container_message_handler(this)); + } private: inline static const std::string NAME = "GlobalStoreActor"; @@ -38,8 +41,9 @@ namespace global_store { private: const std::string reg_value_; - caf::behavior behavior_; GlobalStore base_; + caf::actor jsonactor_; + caf::actor ioactor_; }; } // namespace global_store } // namespace xstudio diff --git a/include/xstudio/history/history_actor.hpp b/include/xstudio/history/history_actor.hpp index 886681bb9..172d2eaa1 100644 --- a/include/xstudio/history/history_actor.hpp +++ b/include/xstudio/history/history_actor.hpp @@ -27,10 +27,13 @@ namespace history { private: inline static const std::string NAME = "HistoryActor"; void init(); - caf::behavior make_behavior() override { return behavior_; } + caf::message_handler message_handler(); + + caf::behavior make_behavior() override { + return message_handler().or_else(base_.container_message_handler(this)); + } private: - caf::behavior behavior_; History base_; }; @@ -49,16 +52,8 @@ namespace history { init(); } - template void HistoryActor::init() { - print_on_create(this, "HistoryActor"); - print_on_exit(this, "HistoryActor"); - - behavior_.assign( - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(caf::actor()), - base_.make_get_detail_handler(this, caf::actor()), - + template caf::message_handler HistoryActor::message_handler() { + return caf::message_handler{ [=](plugin_manager::enable_atom) -> bool { return base_.enabled(); }, [=](plugin_manager::enable_atom, const bool enabled) -> bool { @@ -104,7 +99,13 @@ namespace history { utility::JsonStore jsn; jsn["base"] = base_.serialise(); return jsn; - }); + }}; + } + + + template void HistoryActor::init() { + print_on_create(this, "HistoryActor"); + print_on_exit(this, "HistoryActor"); } template class HistoryMapActor : public caf::event_based_actor { @@ -118,7 +119,11 @@ namespace history { private: inline static const std::string NAME = "HistoryActor"; void init(); - caf::behavior make_behavior() override { return behavior_; } + caf::message_handler message_handler(); + + caf::behavior make_behavior() override { + return message_handler().or_else(base_.container_message_handler(this)); + } private: caf::behavior behavior_; @@ -141,16 +146,9 @@ namespace history { init(); } - template void HistoryMapActor::init() { - print_on_create(this, "HistoryActor"); - print_on_exit(this, "HistoryActor"); - - behavior_.assign( - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(caf::actor()), - base_.make_get_detail_handler(this, caf::actor()), - + template + caf::message_handler HistoryMapActor::message_handler() { + return caf::message_handler{ [=](plugin_manager::enable_atom) -> bool { return base_.enabled(); }, [=](plugin_manager::enable_atom, const bool enabled) -> bool { @@ -210,19 +208,19 @@ namespace history { utility::JsonStore jsn; jsn["base"] = base_.serialise(); return jsn; - }); + }}; } - template <> void HistoryMapActor::init() { + + template void HistoryMapActor::init() { print_on_create(this, "HistoryActor"); print_on_exit(this, "HistoryActor"); + } - behavior_.assign( - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(caf::actor()), - base_.make_get_detail_handler(this, caf::actor()), - + template <> + caf::message_handler + HistoryMapActor::message_handler() { + return caf::message_handler{ [=](plugin_manager::enable_atom) -> bool { return base_.enabled(); }, [=](plugin_manager::enable_atom, const bool enabled) -> bool { @@ -332,7 +330,13 @@ namespace history { utility::JsonStore jsn; jsn["base"] = base_.serialise(); return jsn; - }); + }}; + } + + + template <> void HistoryMapActor::init() { + print_on_create(this, "HistoryActor"); + print_on_exit(this, "HistoryActor"); } diff --git a/include/xstudio/http_client/caf_http_client_error.hpp b/include/xstudio/http_client/caf_http_client_error.hpp index ecd4497a6..2c45b4f20 100644 --- a/include/xstudio/http_client/caf_http_client_error.hpp +++ b/include/xstudio/http_client/caf_http_client_error.hpp @@ -18,7 +18,7 @@ namespace http_client { } } - inline bool from_string(caf::string_view in, http_client_error &out) { + inline bool from_string(std::string_view in, http_client_error &out) { if (in == "connection_error") { out = http_client_error::connection_error; return true; diff --git a/include/xstudio/json_store/json_store_actor.hpp b/include/xstudio/json_store/json_store_actor.hpp index 46d682f2c..da6f9fcc8 100644 --- a/include/xstudio/json_store/json_store_actor.hpp +++ b/include/xstudio/json_store/json_store_actor.hpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include "xstudio/utility/json_store.hpp" diff --git a/include/xstudio/json_store/json_store_handler.hpp b/include/xstudio/json_store/json_store_handler.hpp new file mode 100644 index 000000000..8b288486c --- /dev/null +++ b/include/xstudio/json_store/json_store_handler.hpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/utility/json_store.hpp" +#include +#include +#include + +namespace xstudio::json_store { + +class JsonStoreHandler { + public: + JsonStoreHandler() = default; + JsonStoreHandler( + caf::event_based_actor *act, + caf::actor event_group, + const utility::Uuid &uuid = utility::Uuid::generate(), + const utility::JsonStore &json = utility::JsonStore(), + const std::chrono::milliseconds &delay = std::chrono::milliseconds()); + + virtual ~JsonStoreHandler() = default; + + virtual caf::message_handler message_handler(); + static caf::message_handler default_event_handler(); + + caf::actor json_actor() const { return json_store_; } + + private: + caf::event_based_actor *actor_{nullptr}; + caf::actor event_group_; + caf::actor json_store_; +}; + + +} // namespace xstudio::json_store diff --git a/include/xstudio/json_store/json_store_helper.hpp b/include/xstudio/json_store/json_store_helper.hpp index 84a67c634..2ebff7191 100644 --- a/include/xstudio/json_store/json_store_helper.hpp +++ b/include/xstudio/json_store/json_store_helper.hpp @@ -39,6 +39,7 @@ namespace json_store { const bool broadcast_change = true); caf::actor get_group(utility::JsonStore &V) const; + caf::actor get_jsonactor() const; [[nodiscard]] caf::actor get_actor() const { return caf::actor_cast(store_actor_); diff --git a/include/xstudio/media/caf_media_error.hpp b/include/xstudio/media/caf_media_error.hpp index 58cdae850..dae7cd2c4 100644 --- a/include/xstudio/media/caf_media_error.hpp +++ b/include/xstudio/media/caf_media_error.hpp @@ -22,7 +22,7 @@ namespace media { } } - inline bool from_string(caf::string_view in, media_error &out) { + inline bool from_string(std::string_view in, media_error &out) { if (in == "File missing") { out = media_error::missing; return true; diff --git a/include/xstudio/media/enums.hpp b/include/xstudio/media/enums.hpp index 80daeb573..adeeb9bd0 100644 --- a/include/xstudio/media/enums.hpp +++ b/include/xstudio/media/enums.hpp @@ -13,5 +13,13 @@ namespace media { MS_UNREADABLE = 0x04L } MediaStatus; + typedef enum { + PS_DONT_HOLD_FRAME = 0, + PS_HOLD_FRAME, + PS_COLLAPSE_TO_ON_DISK_FRAMES + } PartialSeqBehaviour; + + typedef enum { FS_ON_DISK = 0, FS_NOT_ON_DISK, FS_HELD_FRAME, FS_UNKNOWN } FrameStatus; + } // namespace media } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/media/media.hpp b/include/xstudio/media/media.hpp index f03e2c9cf..3517c4028 100644 --- a/include/xstudio/media/media.hpp +++ b/include/xstudio/media/media.hpp @@ -10,7 +10,6 @@ #include "xstudio/enums.hpp" #include "xstudio/utility/container.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/frame_list.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/media_reference.hpp" @@ -22,6 +21,11 @@ namespace media { typedef std::vector> LogicalFrameRanges; + static inline std::map partialSeqNameMap = { + {"Insert Blank Frames", PS_DONT_HOLD_FRAME}, + {"Hold Frames", PS_HOLD_FRAME}, + {"Skip Missing Frames", PS_COLLAPSE_TO_ON_DISK_FRAMES}}; + MediaType media_type_from_string(const std::string &str); inline std::string to_readable_string(const MediaType mt) { std::string str; @@ -108,7 +112,8 @@ namespace media { inline std::string to_string(const StreamDetail &v) { return v.name_ + " " + to_readable_string(v.media_type_) + " " + v.key_format_ + " " + - to_string(v.duration_); + to_string(v.duration_) + " " + std::to_string(v.resolution_.x) + "x" + + std::to_string(v.resolution_.y) + " " + std::to_string(v.pixel_aspect_); } class MediaDetail { @@ -144,39 +149,44 @@ namespace media { public: MediaKey() = default; MediaKey(const MediaKey &o) = default; - MediaKey(const std::string &o) : std::string(o) {} + MediaKey(const std::string &o); MediaKey( const std::string &key_format, const caf::uri &uri, const int frame, - const std::string &stream_id) - : std::string(fmt::format( - key_format, - to_string(uri), - (frame == std::numeric_limits::min() ? 0 : frame), - stream_id)) {} + const std::string &stream_id); - bool operator==(const MediaKey &o) const { - return static_cast(o) == - static_cast(*this); + bool operator!=(const MediaKey &o) const { + return (hash_ == o.hash_) ? static_cast(*this) != + static_cast(o) + : true; } - bool operator!=(const MediaKey &o) const { - return static_cast(o) != - static_cast(*this); + bool operator==(const MediaKey &o) const { + return (hash_ == o.hash_) ? static_cast(*this) == + static_cast(o) + : false; } bool operator<(const MediaKey &o) const { - return static_cast(o) < - static_cast(*this); + return (hash_ != o.hash_) ? hash_ < o.hash_ + : static_cast(*this) < + static_cast(o); } + [[nodiscard]] size_t hash() const { return hash_; } + friend std::string to_string(const MediaKey &value); friend fmt::string_view to_string_view(const MediaKey &value); template friend bool inspect(Inspector &f, MediaKey &x) { return f.object(x).fields(f.field("data", static_cast(x))); } + + bool is_null() const { return empty(); } + + private: + size_t hash_; }; inline std::string to_string(const MediaKey &v) { @@ -193,101 +203,175 @@ namespace media { const int frame, const std::string &stream_id) { return MediaKey(fmt::format( - key_format, + fmt::runtime(key_format), to_string(uri), (frame == std::numeric_limits::min() ? 0 : frame), stream_id)); } + // class AVFrameID + // + // This class contains all detail needed to read a single video/audio frame + // into an Image or Audio data buffer. It is used primarily by the playhead, + // media reader and media components of xstudio. + // + // AVFrameIDs are evaluated and stored for each frame in any timeline that + // will be played. As such, we use shared_ptr both internally and to manage + // instances that are passed around the messaging system to minimise + // copy and data footprint class AVFrameID { public: AVFrameID( - const caf::uri &uri = caf::uri(), - const int frame = std::numeric_limits::min(), - const int first_frame = std::numeric_limits::min(), - const utility::FrameRate rate = utility::FrameRate(timebase::k_flicks_24fps), - const std::string &stream_id = "", - const std::string &key_format = "{0}@{1}/{2}", - std::string reader = "", - const caf::actor_addr addr = caf::actor_addr(), - const utility::JsonStore ¶ms = utility::JsonStore(), - const utility::Uuid &source_uuid = utility::Uuid(), - const utility::Uuid &media_uuid = utility::Uuid(), - const MediaType media_type = MT_IMAGE, - const int playhead_logical_frame = 0, + const AVFrameID &shared, + const caf::uri &uri, + const int frame, + const int key_frame, + const std::string &key_format, + const FrameStatus frame_status, const utility::Timecode time_code = utility::Timecode()) + : fixed_media_data_(shared.fixed_media_data_), + uri_(uri == shared.fixed_media_data_->fixed_uri_ ? caf::uri() : uri), + frame_(frame), + key_(key_format, uri, key_frame, shared.fixed_media_data_->stream_id_), + frame_status_(frame_status), + timecode_(time_code) {} + + AVFrameID( + const caf::uri &uri = caf::uri(), + const int frame = std::numeric_limits::min(), + const int first_frame = std::numeric_limits::min(), + const FrameStatus frame_status = FS_UNKNOWN, + const float pixel_aspect = 1.0f, + const utility::FrameRate rate = utility::FrameRate(timebase::k_flicks_24fps), + const std::string &stream_id = "", + const std::string &key_format = "{0}@{1}/{2}", + std::string reader = "", + const caf::actor_addr media_addr = caf::actor_addr(), + const caf::actor_addr media_source_addr = caf::actor_addr(), + const utility::JsonStore ¶ms = utility::JsonStore(), + const utility::Uuid &source_uuid = utility::Uuid(), + const utility::Uuid &media_uuid = utility::Uuid(), + const utility::Uuid &clip_uuid = utility::Uuid(), + const MediaType media_type = MT_IMAGE, + const utility::Timecode time_code = utility::Timecode()) : uri_(uri), frame_(frame), - first_frame_(first_frame), - rate_(std::move(rate)), - stream_id_(stream_id), key_(key_format, uri, frame, stream_id), - reader_(std::move(reader)), - actor_addr_(addr), - params_(params), - source_uuid_(source_uuid), - media_uuid_(media_uuid), - media_type_(media_type), - playhead_logical_frame_(playhead_logical_frame), - timecode_(time_code) {} + frame_status_(frame_status), + timecode_(time_code) { + FixedMediaData *md = new FixedMediaData; + md->first_frame_ = first_frame; + md->pixel_aspect_ = pixel_aspect; + md->rate_ = rate; + md->stream_id_ = stream_id; + md->reader_ = reader; + md->media_addr_ = media_addr; + md->media_source_addr_ = media_source_addr; + md->params_ = params; + md->source_uuid_ = source_uuid; + md->media_uuid_ = media_uuid; + md->clip_uuid_ = clip_uuid; + md->media_type_ = media_type; + fixed_media_data_.reset(md); + } virtual ~AVFrameID() = default; AVFrameID(const AVFrameID &o) = default; - [[nodiscard]] bool is_nil() const { return uri_.empty(); } + [[nodiscard]] bool is_nil() const { return uri().empty(); } bool operator==(const AVFrameID &other) const { return ( uri_ == other.uri_ and frame_ == other.frame_ and - first_frame_ == other.first_frame_ and rate_ == other.rate_ and - stream_id_ == other.stream_id_ and key_ == other.key_ and - reader_ == other.reader_ and actor_addr_ == other.actor_addr_ and - params_ == other.params_ and source_uuid_ == other.source_uuid_ and - media_uuid_ == other.media_uuid_ and media_type_ == other.media_type_ and - playhead_logical_frame_ == other.playhead_logical_frame_ and + fixed_media_data_ == other.fixed_media_data_ and key_ == other.key_ and timecode_ == other.timecode_ and error_ == other.error_); } - template friend bool inspect(Inspector &f, AVFrameID &x) { - return f.object(x).fields( - f.field("uri", x.uri_), - f.field("frm", x.frame_), - f.field("ffrm", x.first_frame_), - f.field("rat", x.rate_), - f.field("strid", x.stream_id_), - f.field("key", x.key_), - f.field("rdr", x.reader_), - f.field("actad", x.actor_addr_), - f.field("prms", x.params_), - f.field("suuid", x.source_uuid_), - f.field("skpky", x.media_uuid_), - f.field("mt", x.media_type_), - f.field("plc", x.playhead_logical_frame_), - f.field("tc", x.timecode_), - f.field("err", x.error_)); + + bool operator!=(const AVFrameID &other) const { return !(*this == other); } + + [[nodiscard]] const caf::uri &uri() const { + return uri_.empty() ? fixed_media_data_->fixed_uri_ : uri_; + } + [[nodiscard]] int frame() const { return frame_; } + [[nodiscard]] int first_frame() const { return fixed_media_data_->first_frame_; } + [[nodiscard]] FrameStatus frame_status() const { return frame_status_; } + [[nodiscard]] float pixel_aspect() const { return fixed_media_data_->pixel_aspect_; } + [[nodiscard]] const utility::FrameRate &rate() const { + return fixed_media_data_->rate_; + } + [[nodiscard]] const std::string &stream_id() const { + return fixed_media_data_->stream_id_; + } + [[nodiscard]] const MediaKey &key() const { return key_; } + [[nodiscard]] const std::string &reader() const { return fixed_media_data_->reader_; } + [[nodiscard]] const caf::actor_addr &media_source_addr() const { + return fixed_media_data_->media_source_addr_; + } + [[nodiscard]] const caf::actor_addr &media_addr() const { + return fixed_media_data_->media_addr_; + } + [[nodiscard]] const utility::JsonStore ¶ms() const { + return fixed_media_data_->params_; + } + [[nodiscard]] const utility::Uuid &source_uuid() const { + return fixed_media_data_->source_uuid_; + } + [[nodiscard]] const utility::Uuid &media_uuid() const { + return fixed_media_data_->media_uuid_; + } + [[nodiscard]] const utility::Uuid &clip_uuid() const { + return fixed_media_data_->clip_uuid_; } + [[nodiscard]] MediaType media_type() const { return fixed_media_data_->media_type_; } + [[nodiscard]] const utility::Timecode &timecode() const { return timecode_; } + [[nodiscard]] const std::string &error() const { return error_; } + utility::UuidActor media_actor() const { + return utility::UuidActor(media_uuid(), caf::actor_cast(media_addr())); + } + + utility::UuidActor media_source_actor() const { + return utility::UuidActor( + source_uuid(), caf::actor_cast(media_source_addr())); + } + + void set_frame_status(const FrameStatus fs) { frame_status_ = fs; } + + private: caf::uri uri_; int frame_; - int first_frame_; - utility::FrameRate rate_; - std::string stream_id_; MediaKey key_; - std::string reader_; - caf::actor_addr actor_addr_; - utility::JsonStore params_; - utility::Uuid source_uuid_; - utility::Uuid media_uuid_; - MediaType media_type_; - int playhead_logical_frame_; + FrameStatus frame_status_; utility::Timecode timecode_; std::string error_; + + struct FixedMediaData { + caf::uri fixed_uri_; + int first_frame_; + float pixel_aspect_; + utility::FrameRate rate_; + std::string stream_id_; + std::string reader_; + caf::actor_addr media_source_addr_; + caf::actor_addr media_addr_; + utility::JsonStore params_; + utility::Uuid source_uuid_; + utility::Uuid media_uuid_; + utility::Uuid clip_uuid_; + MediaType media_type_; + }; + + std::shared_ptr fixed_media_data_; }; typedef std::pair> MediaPointerAndTimePoint; typedef std::vector AVFrameIDsAndTimePoints; + typedef std::map> FrameTimeMap; + typedef std::shared_ptr>> + FrameTimeMapPtr; class Media : public utility::Container { public: @@ -319,7 +403,6 @@ namespace media { [[nodiscard]] std::string flag_text() const { return flag_text_; } void set_flag_text(const std::string &flag_text) { flag_text_ = flag_text; } - private: // will need extending.., tagging ? utility::Uuid current_image_source_; @@ -375,11 +458,17 @@ namespace media { void set_error_detail(const std::string error_detail) { error_detail_ = error_detail; } + void set_partial_seq_behaviour(const PartialSeqBehaviour _partial_seq_behaviour) { + partial_seq_behaviour_ = _partial_seq_behaviour; + } + [[nodiscard]] bool has_type(const MediaType media_type) const; [[nodiscard]] MediaStatus media_status() const { return media_status_; } [[nodiscard]] bool online() const { return media_status_ == MediaStatus::MS_ONLINE; } [[nodiscard]] const std::string &error_detail() const { return error_detail_; } - + [[nodiscard]] const PartialSeqBehaviour partial_seq_behaviour() const { + return partial_seq_behaviour_; + } [[nodiscard]] const std::list &streams(const MediaType media_type) const; @@ -398,6 +487,7 @@ namespace media { private: utility::MediaReference ref_; + PartialSeqBehaviour partial_seq_behaviour_ = {PS_COLLAPSE_TO_ON_DISK_FRAMES}; utility::Uuid current_audio_; utility::Uuid current_image_; std::list image_streams_; @@ -432,7 +522,8 @@ namespace media { StreamDetail detail_; }; - inline std::shared_ptr make_blank_frame(const MediaType media_type) { + inline std::shared_ptr + make_blank_frame(const utility::FrameRate rate, const MediaType media_type) { utility::JsonStore js; js["BLANK_FRAME"] = true; @@ -440,14 +531,47 @@ namespace media { *caf::make_uri("xstudio://blank/?colour=gray"), 0, 0, - timebase::k_flicks_24fps, + FS_UNKNOWN, + 1.0f, + rate, "", "{0}@{1}/{2}", "Blank", - actor_addr(), + caf::actor_addr(), + caf::actor_addr(), js, utility::Uuid(), utility::Uuid(), + utility::Uuid(), + media_type)); + } + + inline std::shared_ptr make_blank_frame( + const MediaType media_type, + const utility::Uuid media_uuid = utility::Uuid(), + const utility::Uuid source_uuid = utility::Uuid(), + const utility::Uuid clip_uuid = utility::Uuid(), + const caf::actor_addr media_actor_addr = caf::actor_addr(), + const caf::actor_addr media_source_actor_addr = caf::actor_addr()) { + utility::JsonStore js; + js["BLANK_FRAME"] = true; + + return std::shared_ptr(new AVFrameID( + *caf::make_uri("xstudio://blank/?colour=gray"), + 0, + 0, + FS_UNKNOWN, + 1.0f, + timebase::k_flicks_24fps, + "", + "{0}@{1}/{2}", + "Blank", + media_actor_addr, + media_source_actor_addr, + js, + source_uuid, + media_uuid, + clip_uuid, media_type)); } } // namespace media @@ -455,8 +579,6 @@ namespace media { namespace std { template <> struct hash { - size_t operator()(const xstudio::media::MediaKey &k) const { - return hash()(to_string(k)); - } + size_t operator()(const xstudio::media::MediaKey &k) const { return k.hash(); } }; } // namespace std \ No newline at end of file diff --git a/include/xstudio/media/media_actor.hpp b/include/xstudio/media/media_actor.hpp index 7fe34ac0b..6fb8956dc 100644 --- a/include/xstudio/media/media_actor.hpp +++ b/include/xstudio/media/media_actor.hpp @@ -7,6 +7,8 @@ #include "xstudio/media/media.hpp" #include "xstudio/utility/container.hpp" #include "xstudio/utility/json_store.hpp" +#include "xstudio/json_store/json_store_handler.hpp" +#include "xstudio/utility/tree.hpp" #include "xstudio/utility/uuid.hpp" namespace xstudio { @@ -30,7 +32,11 @@ namespace media { caf::actor_config &cfg, const utility::JsonStore &jsn, const bool async = false); ~MediaActor() override = default; - caf::behavior make_behavior() override { return behavior_; } + caf::message_handler message_handler(); + + caf::behavior make_behavior() override { + return message_handler().or_else(base_.container_message_handler(this)); + } const char *name() const override { return NAME.c_str(); } static caf::message_handler default_event_handler(); @@ -47,7 +53,10 @@ namespace media { const bool set_as_current_source); void clone_bookmarks_to( - const utility::UuidActor &ua, caf::actor src_bookmark, caf::actor dst_bookmark); + caf::typed_response_promise rp, + const utility::UuidUuidActor &uua, + caf::actor src_bookmark, + caf::actor dst_bookmark); void switch_current_source_to_named_source( caf::typed_response_promise rp, @@ -55,14 +64,30 @@ namespace media { const media::MediaType mt, const bool auto_select_source_if_failed = false); - void auto_set_current_source(const media::MediaType media_type); + void auto_set_current_source( + const media::MediaType media_type, caf::typed_response_promise rp); + + void update_human_readable_details(caf::typed_response_promise rp); + + void build_media_list_info(caf::typed_response_promise rp); + + void display_info_item( + const utility::JsonStore item_query_info, + caf::typed_response_promise rp); + + void duplicate( + caf::typed_response_promise rp, + caf::actor src_bookmarks, + caf::actor dst_bookmarks); - caf::behavior behavior_; Media base_; caf::actor json_store_; - caf::actor event_group_; std::map media_sources_; + utility::UuidList bookmark_uuids_; bool pending_change_{false}; + utility::JsonTree media_list_columns_config_; + utility::JsonStore human_readable_info_; + utility::JsonStore media_list_columns_info_; }; class MediaSourceActor : public caf::event_based_actor { @@ -91,25 +116,42 @@ namespace media { static caf::message_handler default_event_handler(); - caf::behavior make_behavior() override { return behavior_; } + caf::message_handler message_handler(); + + caf::behavior make_behavior() override { + return message_handler().or_else(base_.container_message_handler(this)); + } const char *name() const override { return NAME.c_str(); } private: void update_media_status(); + void update_media_detail(); void acquire_detail(const utility::FrameRate &rate, caf::typed_response_promise rp); - void deliver_frames_media_keys( - caf::typed_response_promise rp, + + void get_media_pointers_for_frames( const MediaType media_type, - const std::vector logical_frames); + const LogicalFrameRanges &ranges, + caf::typed_response_promise rp, + const utility::Uuid clip_uuid); void get_media_pointers_for_frames( const MediaType media_type, const LogicalFrameRanges &ranges, - caf::typed_response_promise rp); + caf::typed_response_promise rp, + const utility::Uuid clip_uuid, + const utility::JsonStore &colour_mgmt_data, + const StreamDetail &media_detail); + + caf::uri uri_for_logical_frame( + const MediaType media_type, + const int logical_frame, + int &frame, + int &keyframe, + FrameStatus &frame_status); void update_stream_media_reference( StreamDetail &stream_detail, @@ -117,16 +159,32 @@ namespace media { const utility::FrameRate &rate, const utility::Timecode &timecode); + void send_stream_metadata_to_stream_actors(const utility::JsonStore &meta); + + void duplicate(caf::typed_response_promise rp); + inline static const std::string NAME = "MediaSourceActor"; void init(); - caf::behavior behavior_; MediaSource base_; caf::actor json_store_; std::map media_streams_; caf::actor_addr parent_; utility::Uuid parent_uuid_; - caf::actor event_group_; std::vector> pending_stream_detail_requests_; + bool media_metadata_up_to_date_ = {false}; + std::set all_requested_frames_; + + struct UriStatus { + UriStatus(const UriStatus &o) = default; + UriStatus() = default; + UriStatus(const caf::uri &_uri, const FrameStatus &status, const int f) + : uri_(_uri), status_(status), frame_(f) {} + caf::uri uri_; + FrameStatus status_; + int frame_; + }; + + std::map uri_status_cache_; }; class MediaStreamActor : public caf::event_based_actor { @@ -134,20 +192,25 @@ namespace media { MediaStreamActor( caf::actor_config &cfg, const StreamDetail &detail, - const utility::Uuid &uuid = utility::Uuid()); + const utility::Uuid &uuid = utility::Uuid(), + const utility::JsonStore &meta = utility::JsonStore()); MediaStreamActor(caf::actor_config &cfg, const utility::JsonStore &jsn); ~MediaStreamActor() override = default; + caf::message_handler message_handler(); - caf::behavior make_behavior() override { return behavior_; } + caf::behavior make_behavior() override { + return message_handler() + .or_else(base_.container_message_handler(this)) + .or_else(jsn_handler_.message_handler()); + } const char *name() const override { return NAME.c_str(); } private: inline static const std::string NAME = "MediaStreamActor"; void init(); - caf::behavior behavior_; MediaStream base_; - caf::actor json_store_; + json_store::JsonStoreHandler jsn_handler_; }; } // namespace media } // namespace xstudio diff --git a/include/xstudio/media/media_metadata_manager_actor.hpp b/include/xstudio/media/media_metadata_manager_actor.hpp new file mode 100644 index 000000000..43ee79743 --- /dev/null +++ b/include/xstudio/media/media_metadata_manager_actor.hpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include +#include + +#include "xstudio/utility/tree.hpp" +#include "xstudio/utility/uuid.hpp" + +namespace xstudio { +namespace media { + class GlobalMetadataManager : public caf::event_based_actor { + public: + GlobalMetadataManager(caf::actor_config &cfg); + ~GlobalMetadataManager() override = default; + + caf::behavior make_behavior() override { return behavior_; } + + void on_exit() override; + + private: + void config_updated(); + + caf::actor event_group_; + utility::JsonTree metadata_config_; + utility::JsonStore metadata_extraction_config_; + caf::behavior behavior_; + }; +} // namespace media +} // namespace xstudio diff --git a/include/xstudio/media_cache/media_cache_actor.hpp b/include/xstudio/media_cache/media_cache_actor.hpp index 105a27a8e..d346e0235 100644 --- a/include/xstudio/media_cache/media_cache_actor.hpp +++ b/include/xstudio/media_cache/media_cache_actor.hpp @@ -8,6 +8,7 @@ #include "xstudio/media_reader/media_reader.hpp" #include "xstudio/utility/time_cache.hpp" +#include "xstudio/utility/chrono.hpp" namespace xstudio { namespace media_cache { @@ -29,9 +30,15 @@ namespace media_cache { caf::behavior behavior_; utility::TimeCache cache_; - std::set new_keys_; - std::set erased_keys_; + std::unordered_set new_keys_; + std::unordered_set erased_keys_; + + std::vector good_images_in_cache_; + std::vector error_images_in_cache_; + bool update_pending_; + std::chrono::minutes reset_idle_{0}; + utility::time_point last_activity_{utility::clock::now()}; }; class GlobalAudioCacheActor : public caf::event_based_actor { @@ -52,9 +59,11 @@ namespace media_cache { caf::behavior behavior_; utility::TimeCache cache_; - std::set new_keys_; - std::set erased_keys_; + std::unordered_set new_keys_; + std::unordered_set erased_keys_; bool update_pending_; + std::chrono::minutes reset_idle_{0}; + utility::time_point last_activity_{utility::clock::now()}; }; } // namespace media_cache } // namespace xstudio diff --git a/include/xstudio/media_hook/media_hook.hpp b/include/xstudio/media_hook/media_hook.hpp index fb54b55c5..56dd3356a 100644 --- a/include/xstudio/media_hook/media_hook.hpp +++ b/include/xstudio/media_hook/media_hook.hpp @@ -34,6 +34,11 @@ namespace media_hook { return utility::JsonStore{}; } + virtual utility::JsonStore modify_clip_metadata( + const utility::JsonStore &clip_metadata, const utility::JsonStore &media_metadata) { + return utility::JsonStore{}; + } + virtual std::optional modify_media_reference(const utility::MediaReference &, const utility::JsonStore &) { return {}; @@ -54,6 +59,18 @@ namespace media_hook { virtual void update_prefs(const utility::JsonStore &full_prefs_dict) {} + /* Allow custom display selection logic to be defined in MediaHook plugins. + This can be used to assign a specific OCIO display to the detected + monitor where the xStudio viewport is being shown. */ + virtual std::string detect_display( + const std::string &name, + const std::string &model, + const std::string &manufacturer, + const std::string &serialNumber, + const utility::JsonStore &colour_params) { + return {}; + } + protected: MediaHook(std::string name) : module::Module(name), name_(std::move(name)) {} std::string default_path() const { return "/metadata/external/" + name_; } @@ -142,12 +159,10 @@ namespace media_hook { // { // } else { - // anon_send( - // media_actor, - // media::add_media_source_atom_v, + // anon_mail(// media::add_media_source_atom_v, // media_source_references, // media_source_names, - // preferred_source); + // preferred_source).send(// media_actor); // } // } // return true; @@ -161,12 +176,28 @@ namespace media_hook { auto ref = media_hook_.modify_media_reference(mr, jsn); if (ref) { mr = *ref; - anon_send(ua.actor(), media::media_reference_atom_v, mr); + anon_mail(media::media_reference_atom_v, mr).send(ua.actor()); } return media_hook_.modify_metadata(mr, jsn); }, + [=](media_hook::get_clip_hook_atom, + const utility::JsonStore &clip, + const utility::JsonStore &media) -> utility::JsonStore { + return media_hook_.modify_clip_metadata(clip, media); + }, + + [=](media_hook::detect_display_atom, + const std::string &name, + const std::string &model, + const std::string &manufacturer, + const std::string &serialNumber, + const utility::JsonStore &jsn) -> std::string { + return media_hook_.detect_display( + name, model, manufacturer, serialNumber, jsn); + }, + [=](json_store::update_atom, const utility::JsonStore & /*change*/, const std::string & /*path*/, @@ -199,20 +230,19 @@ namespace media_hook { if (p != main_instances.end()) { auto main_instance = caf::actor_cast(p->second); if (main_instance) { - anon_send( - main_instance, + anon_mail( module::link_module_atom_v, actor_cast(this), true, false, - true); + true) + .send(main_instance); } } else { main_instances[hook_type_id] = actor_cast(this); - delayed_anon_send( - actor_cast(this), - std::chrono::milliseconds(250), - module::connect_to_ui_atom_v); + anon_mail(module::connect_to_ui_atom_v) + .delay(std::chrono::milliseconds(250)) + .send(actor_cast(this)); } m.unlock(); } diff --git a/include/xstudio/media_hook/media_hook_actor.hpp b/include/xstudio/media_hook/media_hook_actor.hpp index 7ac3882c3..2e1b7c346 100644 --- a/include/xstudio/media_hook/media_hook_actor.hpp +++ b/include/xstudio/media_hook/media_hook_actor.hpp @@ -19,6 +19,13 @@ namespace media_hook { private: inline static const std::string NAME = "MediaHookWorkerActor"; caf::behavior behavior_; + std::vector hooks_; + + void do_clip_hook( + caf::typed_response_promise rp, + const caf::actor &clip_actor, + const utility::JsonStore &clip_meta, + const utility::JsonStore &media_meta); }; class GlobalMediaHookActor : public caf::event_based_actor { diff --git a/include/xstudio/media_reader/audio_buffer.hpp b/include/xstudio/media_reader/audio_buffer.hpp index 48f2cec73..aecb32842 100644 --- a/include/xstudio/media_reader/audio_buffer.hpp +++ b/include/xstudio/media_reader/audio_buffer.hpp @@ -33,7 +33,7 @@ namespace media_reader { case audio::SampleFormat::INT16: samp_size = 2; break; - case audio::SampleFormat::INT32: + case audio::SampleFormat::SFINT32: samp_size = 4; break; case audio::SampleFormat::FLOAT32: @@ -104,12 +104,15 @@ namespace media_reader { AudioBufPtr() = default; AudioBufPtr(AudioBuffer *imbuf) : Base(imbuf) {} AudioBufPtr(const AudioBufPtr &o) - : Base(static_cast(o)), when_to_display_(o.when_to_display_) {} + : Base(static_cast(o)), + when_to_display_(o.when_to_display_), + tts_(o.tts_) {} AudioBufPtr &operator=(const AudioBufPtr &o) { Base &b = static_cast(*this); b = static_cast(o); when_to_display_ = o.when_to_display_; + tts_ = o.tts_; return *this; } @@ -125,6 +128,12 @@ namespace media_reader { bool operator<(const utility::time_point &t) const { return when_to_display_ < t; } utility::time_point when_to_display_; + + [[nodiscard]] const timebase::flicks &timeline_timestamp() const { return tts_; } + void set_timline_timestamp(const timebase::flicks tts) { tts_ = tts; } + + private: + timebase::flicks tts_ = timebase::flicks{0}; }; } // namespace media_reader diff --git a/include/xstudio/media_reader/buffer.hpp b/include/xstudio/media_reader/buffer.hpp index 7bd8b0c5e..3f35739bc 100644 --- a/include/xstudio/media_reader/buffer.hpp +++ b/include/xstudio/media_reader/buffer.hpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #define UNSET_DTS -1e6 @@ -19,6 +18,8 @@ namespace media_reader { enum class byte : unsigned char {}; + class ImageBufferRecyclerCache; + class Buffer { public: @@ -80,6 +81,8 @@ namespace media_reader { }; typedef std::shared_ptr BufferDataPtr; + static std::shared_ptr s_buf_cache; + private: BufferDataPtr buffer_; // use long long to get 16 byte alignment size_t size_{0}; @@ -89,5 +92,21 @@ namespace media_reader { double dts_ = {UNSET_DTS}; }; + /* Lower level dumb cache that hangs onto BufferDataPtrs after deletion + of parent Buffer class for re-use.*/ + class ImageBufferRecyclerCache { + public: + void store_unwanted_buffer(Buffer::BufferDataPtr &buf, const size_t size); + Buffer::BufferDataPtr fetch_recycled_buffer(const size_t required_size); + size_t max_size_ = {512 * 1024 * 1024}; // 0.5GB - probably doesn't need to be that big, + // and need to set this with a pref + size_t total_size_ = {0}; + typedef std::vector Buffers; + std::map recycle_buffer_bin_; + std::map size_by_time_; + std::mutex mutex_; + }; + + } // namespace media_reader } // namespace xstudio diff --git a/include/xstudio/media_reader/cacheing_media_reader_actor.hpp b/include/xstudio/media_reader/cacheing_media_reader_actor.hpp index 9c9a93574..67f272e34 100644 --- a/include/xstudio/media_reader/cacheing_media_reader_actor.hpp +++ b/include/xstudio/media_reader/cacheing_media_reader_actor.hpp @@ -28,10 +28,8 @@ namespace media_reader { struct ImmediateImageReqest { ImmediateImageReqest( - const media::AVFrameID mptr, - caf::actor &playhead, - const utility::time_point &time) - : mptr_(mptr), playhead_(playhead), time_point_(time) {} + const media::AVFrameID mptr, caf::typed_response_promise &rp) + : mptr_(mptr), response_promise_(rp) {} ImmediateImageReqest(const ImmediateImageReqest &) = default; ImmediateImageReqest() = default; @@ -39,16 +37,14 @@ namespace media_reader { ImmediateImageReqest &operator=(const ImmediateImageReqest &o) = default; media::AVFrameID mptr_; - caf::actor playhead_; - utility::time_point time_point_; + caf::typed_response_promise response_promise_; }; void do_urgent_get_image(); - void receive_image_buffer_request( - const media::AVFrameID &mptr, - caf::actor playhead, - const utility::Uuid playhead_uuid, - const utility::time_point &tp); + caf::typed_response_promise receive_image_buffer_request( + const media::AVFrameID &mptr, const utility::Uuid playhead_uuid); + + ImageBufPtr make_error_buffer(const caf::error &err, const media::AVFrameID &mptr); std::map pending_get_image_requests_; @@ -64,6 +60,8 @@ namespace media_reader { caf::actor urgent_worker_; caf::actor precache_worker_; caf::actor audio_worker_; + + ImageBufPtr blank_image_; }; } // namespace media_reader } // namespace xstudio diff --git a/include/xstudio/media_reader/image_buffer.hpp b/include/xstudio/media_reader/image_buffer.hpp index cc7d6349c..1c25896c5 100644 --- a/include/xstudio/media_reader/image_buffer.hpp +++ b/include/xstudio/media_reader/image_buffer.hpp @@ -34,6 +34,9 @@ namespace media_reader { void set_shader_params(const utility::JsonStore ¶ms) { shader_params_ = params; } [[nodiscard]] const utility::JsonStore &shader_params() const { return shader_params_; } + void set_metadata(const utility::JsonStore &metadata) { metadata_ = metadata; } + [[nodiscard]] const utility::JsonStore &metadata() const { return metadata_; } + [[nodiscard]] Imath::V2i image_size_in_pixels() const { return image_size_in_pixels_; } [[nodiscard]] Imath::Box2i image_pixels_bounding_box() const { return pixels_bounds_; } void set_image_dimensions( @@ -46,15 +49,9 @@ namespace media_reader { } } - [[nodiscard]] float pixel_aspect() const { return pixel_aspect_; } - void set_pixel_aspect(const float aspect) { pixel_aspect_ = aspect; } - [[nodiscard]] const media::MediaKey &media_key() const { return media_key_; } void set_media_key(const media::MediaKey &key) { media_key_ = key; } - [[nodiscard]] double duration_seconds() const { return frame_duration_; } - void set_duration_seconds(const double d) { frame_duration_ = d; } - [[nodiscard]] int decoder_frame_number() const { return frame_num_; } void set_decoder_frame_number(const int f) { frame_num_ = f; } @@ -72,17 +69,14 @@ namespace media_reader { return PixelInfo(pixel_location); } - AudioBufPtr audio_; - private: utility::Uuid shader_id_; utility::JsonStore shader_params_; + utility::JsonStore metadata_; Imath::V2i image_size_in_pixels_; Imath::Box2i pixels_bounds_; - float pixel_aspect_ = {1.0f}; media::MediaKey media_key_; - double frame_duration_ = {1.0}; - int frame_num_ = -1; + int frame_num_ = -1; ui::viewport::GPUShaderPtr shader_; PixelPickerFunc pixel_picker_; bool has_alpha_ = false; @@ -106,18 +100,26 @@ namespace media_reader { plugin_blind_data_(o.plugin_blind_data_), tts_(o.tts_), frame_id_(o.frame_id_), - bookmarks_(o.bookmarks_) {} + bookmarks_(o.bookmarks_), + intrinsic_transform_(o.intrinsic_transform_), + layout_transform_(o.layout_transform_), + playhead_logical_frame_(o.playhead_logical_frame_), + error_details_(o.error_details_) {} ImageBufPtr &operator=(const ImageBufPtr &o) { - Base &b = static_cast(*this); - b = static_cast(o); - colour_pipe_data_ = o.colour_pipe_data_; - colour_pipe_uniforms_ = o.colour_pipe_uniforms_; - when_to_display_ = o.when_to_display_; - plugin_blind_data_ = o.plugin_blind_data_; - tts_ = o.tts_; - frame_id_ = o.frame_id_; - bookmarks_ = o.bookmarks_; + Base &b = static_cast(*this); + b = static_cast(o); + colour_pipe_data_ = o.colour_pipe_data_; + colour_pipe_uniforms_ = o.colour_pipe_uniforms_; + when_to_display_ = o.when_to_display_; + plugin_blind_data_ = o.plugin_blind_data_; + tts_ = o.tts_; + frame_id_ = o.frame_id_; + bookmarks_ = o.bookmarks_; + intrinsic_transform_ = o.intrinsic_transform_; + layout_transform_ = o.layout_transform_; + playhead_logical_frame_ = o.playhead_logical_frame_; + error_details_ = o.error_details_; return *this; } @@ -129,7 +131,7 @@ namespace media_reader { } if (colour_pipe_data_ && o.colour_pipe_data_) { - if (colour_pipe_data_->cache_id_ != o.colour_pipe_data_->cache_id_) { + if (colour_pipe_data_->cache_id() != o.colour_pipe_data_->cache_id()) { return false; } } else if (colour_pipe_data_ || o.colour_pipe_data_) { @@ -144,6 +146,10 @@ namespace media_reader { return false; } + if (bookmarks_ != o.bookmarks_) { + return false; + } + return true; } @@ -164,38 +170,27 @@ namespace media_reader { // of add_plugin_blind_data2 instead void add_plugin_blind_data( const utility::Uuid &plugin_uuid, const utility::BlindDataObjectPtr &data) { - plugin_blind_data_[plugin_uuid].first = data; - } - - void add_plugin_blind_data2( - const utility::Uuid &plugin_uuid, const utility::BlindDataObjectPtr &data) { - plugin_blind_data_[plugin_uuid].second = data; + plugin_blind_data_[plugin_uuid] = data; } [[nodiscard]] utility::BlindDataObjectPtr plugin_blind_data(const utility::Uuid plugin_uuid) const { auto p = plugin_blind_data_.find(plugin_uuid); if (p != plugin_blind_data_.end()) - return p->second.first; + return p->second; return utility::BlindDataObjectPtr(); } - [[nodiscard]] utility::BlindDataObjectPtr - plugin_blind_data2(const utility::Uuid plugin_uuid) const { - auto p = plugin_blind_data_.find(plugin_uuid); - if (p != plugin_blind_data_.end()) - return p->second.second; - return utility::BlindDataObjectPtr(); - } - - std::map< - utility::Uuid, - std::pair> - plugin_blind_data_; + std::map plugin_blind_data_; [[nodiscard]] const timebase::flicks &timeline_timestamp() const { return tts_; } void set_timline_timestamp(const timebase::flicks tts) { tts_ = tts; } + [[nodiscard]] const int &playhead_logical_frame() const { + return playhead_logical_frame_; + } + void set_playhead_logical_frame(const int frame) { playhead_logical_frame_ = frame; } + [[nodiscard]] const bookmark::BookmarkAndAnnotations &bookmarks() const { return bookmarks_; } @@ -206,11 +201,42 @@ namespace media_reader { [[nodiscard]] const media::AVFrameID &frame_id() const { return frame_id_; } void set_frame_id(const media::AVFrameID &id) { frame_id_ = id; } + [[nodiscard]] const Imath::M44f &intrinsic_transform() const { + return intrinsic_transform_; + } + void set_intrinsic_transform(const Imath::M44f &t) { intrinsic_transform_ = t; } + + [[nodiscard]] const Imath::M44f &layout_transform() const { return layout_transform_; } + void set_layout_transform(const Imath::M44f &t) { layout_transform_ = t; } + + [[nodiscard]] const std::string &error_details() const { return error_details_; } + void set_error_details(const std::string &err) { error_details_ = err; } + + friend float image_aspect(const ImageBufPtr &value); + + utility::JsonStore metadata() const; + private: + Imath::M44f intrinsic_transform_; + Imath::M44f layout_transform_; + + std::string error_details_; + timebase::flicks tts_ = timebase::flicks{0}; media::AVFrameID frame_id_; bookmark::BookmarkAndAnnotations bookmarks_; + int playhead_logical_frame_ = 0; }; + inline float image_aspect(const ImageBufPtr &v) { + + return v ? v->image_size_in_pixels().y + ? v.frame_id_.pixel_aspect() * v->image_size_in_pixels().x / + v->image_size_in_pixels().y + : 16.0f / 9.0f + : 16.0f / 9.0f; + } + + } // namespace media_reader } // namespace xstudio diff --git a/include/xstudio/media_reader/image_buffer_set.hpp b/include/xstudio/media_reader/image_buffer_set.hpp new file mode 100644 index 000000000..f33709b1d --- /dev/null +++ b/include/xstudio/media_reader/image_buffer_set.hpp @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/media_reader/image_buffer.hpp" + +namespace xstudio { +namespace media_reader { + + // A simple struct that contains matrices for transforming a set of images, + // a vector of indexes providing the draw order of images in a set, the + // overall display aspect of the layout and a json store for any custom + // layout data. Layout data is attached to the ImageBufDisplaySet just + // before the viewport is rendererd. + struct ImageSetLayoutData { + std::vector image_transforms_; + std::vector image_draw_order_hint_; + float layout_aspect_; + utility::JsonStore custom_layout_data_; + bool draw_hero_overlays_only_; + }; + typedef std::shared_ptr ImageSetLayoutDataPtr; + + // A set of ImageBufPtrs for display. When xSTUDIO displays multiple images + // on-screen, this is achieved by the playhead having multiple 'subPlayheads'. + // Each sub-playhead is attached to a source, and requests image reads from + // the media readers and then broadcasts the images to the parent playhead. + // The parent playhead then re-broadcasts the images to the viewport. + // The ImageBufDisplaySet is used to gather the images coming from the + // multiple sub-playheads into one object which is finally used at draw- + // time for rendering the images to screen. The sub-playheads are somewhat + // independent of each other, so the synchronisation at display time of + // which images should be on-screen is handled by the ViewportFrameQueueActor + class ImageBufDisplaySet { + public: + ImageBufDisplaySet() = default; + ImageBufDisplaySet(const ImageBufDisplaySet &o) = default; + ImageBufDisplaySet(const utility::UuidVector &sub_playhead_ids) + : sub_playhead_ids_(sub_playhead_ids) {} + + // For a given sub-playhead, set the image that should be on-screen for the next + // viewport redraw + void + add_on_screen_image(const utility::Uuid &sub_playhead_id, const ImageBufPtr &image) { + image_set(sub_playhead_id)->on_screen_image = image; + } + + // Overwrite the on-screen image at sub_playhead_idx + void set_on_screen_image(const int &sub_playhead_idx, ImageBufPtr &image) { + std::swap(onscreen_image_m(sub_playhead_idx), image); + } + + // For a given sub-playhead, add and image that is expected to go on screen soon. These + // should be added in the order that they are expected to hit the screen + void + append_future_image(const utility::Uuid &sub_playhead_id, const ImageBufPtr &image) { + image_set(sub_playhead_id)->future_images.push_back(image); + } + + // Loop through all images in the set to get the *earliest* display + // timestamp in the set. This gives us the overall 'when_to_display' + // estimate for the set. It's used by ViewportFrameQueueActor to + // purge stale images from it's cache. + [[nodiscard]] utility::time_point when_to_display() const { + utility::time_point t = utility::time_point::max(); + for (const auto &p : sub_playhead_sets_) { + for (const auto &d : p.second->future_images) { + t = std::min(d.when_to_display_, t); + } + } + return t; + } + + [[nodiscard]] const std::vector & + future_images(const int sub_playhead_index) const { + return image_set(sub_playhead_index)->future_images; + } + [[nodiscard]] int num_future_images(const utility::Uuid &playhead_id) const { + return int(image_set(playhead_id)->future_images.size()); + } + [[nodiscard]] int num_future_images(const int sub_playhead_index) const { + return int(image_set(sub_playhead_index)->future_images.size()); + } + [[nodiscard]] size_t images_keys_hash() const { return images_hash_; } + [[nodiscard]] int num_onscreen_images() const { return int(sub_playhead_ids_.size()); } + [[nodiscard]] bool empty() const { return sub_playhead_ids_.empty(); } + [[nodiscard]] float layout_aspect() const { + return layout_data_ ? layout_data_->layout_aspect_ : 16.0f / 9.0f; + } + [[nodiscard]] int hero_sub_playhead_index() const { return hero_sub_playhead_index_; } + [[nodiscard]] int previous_hero_sub_playhead_index() const { + return previous_hero_sub_playhead_index_; + } + [[nodiscard]] const ImageSetLayoutDataPtr &layout_data() const { return layout_data_; } + [[nodiscard]] const ImageBufPtr &onscreen_image(const int sub_playhead_index) const { + return image_set(sub_playhead_index)->on_screen_image; + } + [[nodiscard]] const ImageBufPtr &hero_image() const { + return image_set(hero_sub_playhead_index_)->on_screen_image; + } + + [[nodiscard]] ImageBufPtr &onscreen_image_m(const int sub_playhead_index) { + return image_set(sub_playhead_index)->on_screen_image; + } + [[nodiscard]] const utility::JsonStore &as_json() const { return as_json_; } + [[nodiscard]] size_t images_layout_hash() const { return hash_; } + + void set_layout_data(const ImageSetLayoutDataPtr &layout_data) { + layout_data_ = layout_data; + } + void set_hero_sub_playhead_index(const int idx) { hero_sub_playhead_index_ = idx; } + void set_previous_hero_sub_playhead_index(const int idx) { + previous_hero_sub_playhead_index_ = idx; + } + + // this is called after an ImageBufDisplaySet has been built to set + // up some internal read-only data + void finalise(); + + private: + struct ImageSet { + ImageBufPtr on_screen_image; + std::vector future_images; + }; + + const std::shared_ptr &image_set(const int sub_playhead_index) const { + if (sub_playhead_index >= (int)sub_playhead_ids_.size()) + return null_set_; + return image_set(sub_playhead_ids_[sub_playhead_index]); + } + + std::shared_ptr &image_set(const int &sub_playhead_index) { + if (sub_playhead_index >= (int)sub_playhead_ids_.size()) + return null_set_; + return image_set(sub_playhead_ids_[sub_playhead_index]); + } + + const std::shared_ptr &image_set(const utility::Uuid &playhead_id) const { + auto p = sub_playhead_sets_.find(playhead_id); + if (p == sub_playhead_sets_.end()) + return null_set_; + return p->second; + } + + std::shared_ptr &image_set(const utility::Uuid &playhead_id) { + if (!sub_playhead_sets_[playhead_id]) { + sub_playhead_sets_[playhead_id].reset(new ImageSet); + } + return sub_playhead_sets_[playhead_id]; + } + + // using shared_ptr to reduce overhead of map rearranging itself + // and copying elements + std::map> sub_playhead_sets_; + + // ordered vec of the IDs of the sub-playheads that are suppling + // the image sets. The first in the vec is the 'key' subplayhead + // corresponding to the first item in the playhead selection. + utility::UuidVector sub_playhead_ids_; + + int hero_sub_playhead_index_ = {0}; + int previous_hero_sub_playhead_index_ = {-1}; + + std::shared_ptr null_set_ = {std::shared_ptr(new ImageSet)}; + + ImageSetLayoutDataPtr layout_data_; + + utility::JsonStore as_json_; + size_t hash_ = {0}; + size_t images_hash_ = {0}; + }; + + typedef std::shared_ptr ImageBufDisplaySetPtr; + +} // namespace media_reader +} // namespace xstudio diff --git a/include/xstudio/media_reader/media_detail_and_thumbnail_reader_actor.hpp b/include/xstudio/media_reader/media_detail_and_thumbnail_reader_actor.hpp index c3ce7a204..83a058aff 100644 --- a/include/xstudio/media_reader/media_detail_and_thumbnail_reader_actor.hpp +++ b/include/xstudio/media_reader/media_detail_and_thumbnail_reader_actor.hpp @@ -34,15 +34,7 @@ namespace media_reader { private: void send_error_to_source(const caf::actor_addr &addr, const caf::error &err); void process_get_media_detail_queue(); - void process_get_thumbnail_queue(); - bool queues_empty() const { - return media_detail_request_queue_.empty() && thumbnail_request_queue_.empty(); - } - void get_thumbnail_from_reader_plugin( - caf::actor &, - const media::AVFrameID, - const size_t, - caf::typed_response_promise); + bool queues_empty() const { return media_detail_request_queue_.empty(); } void continue_processing_queue(); private: @@ -61,29 +53,24 @@ namespace media_reader { caf::typed_response_promise rp_; }; - struct ThumbnailRequest { - - ThumbnailRequest(const ThumbnailRequest &o) = default; - ThumbnailRequest() = default; - ThumbnailRequest( - const media::AVFrameID media_pointer, - const size_t size, - caf::typed_response_promise rp) - : media_pointer_(std::move(media_pointer)), size_(size), rp_(std::move(rp)) {} + private: + void get_thumbnail( + caf::typed_response_promise rp, + const media::AVFrameID &mptr, + const size_t size); - media::AVFrameID media_pointer_; - size_t size_; - caf::typed_response_promise rp_; - }; + void get_thumbnail_from_reader_plugin( + caf::actor &reader_plugin, + caf::typed_response_promise rp, + const media::AVFrameID &mptr, + const size_t size); - private: inline static const std::string NAME = "MediaDetailAndThumbnailReaderActor"; caf::behavior behavior_; int num_detail_requests_since_thumbnail_request_ = {0}; std::queue media_detail_request_queue_; - std::queue thumbnail_request_queue_; std::map media_detail_cache_; std::map media_detail_cache_age_; diff --git a/include/xstudio/media_reader/media_reader.hpp b/include/xstudio/media_reader/media_reader.hpp index c1278aa63..13da0febe 100644 --- a/include/xstudio/media_reader/media_reader.hpp +++ b/include/xstudio/media_reader/media_reader.hpp @@ -136,7 +136,7 @@ namespace media_reader { try { mb = media_reader_.audio(mptr); if (mb) { - mb->set_media_key(mptr.key_); + mb->set_media_key(mptr.key()); } } catch (const std::exception &e) { return make_error(xstudio_error::error, e.what()); @@ -147,16 +147,14 @@ namespace media_reader { [=](get_image_atom, const media::AVFrameID &mptr) -> result { ImageBufPtr mb; try { - std::string path = utility::uri_to_posix_path(mptr.uri_); + std::string path = utility::uri_to_posix_path(mptr.uri()); mb = media_reader_.image(mptr); if (mb) { - mb->set_media_key(mptr.key_); + if (mb->media_key().is_null()) + mb->set_media_key(mptr.key()); mb->set_pixel_picker_func(media_reader_.pixel_picker_func()); - if (mb->audio_) { - mb->audio_->set_media_key(mptr.key_); - } mb->params()["path"] = path; - mb->params()["frame"] = mptr.frame_; + mb->params()["frame"] = mptr.frame(); mb->params()["reader"] = media_reader_.name(); } } catch (const media_missing_error &e) { @@ -228,7 +226,8 @@ namespace media_reader { const utility::JsonStore & /*change*/, const std::string & /*path*/, const utility::JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full) + .delegate(actor_cast(this)); }, [=](json_store::update_atom, const utility::JsonStore &js) { diff --git a/include/xstudio/media_reader/media_reader_actor.hpp b/include/xstudio/media_reader/media_reader_actor.hpp index 7c50f7b30..c85af04ad 100644 --- a/include/xstudio/media_reader/media_reader_actor.hpp +++ b/include/xstudio/media_reader/media_reader_actor.hpp @@ -95,6 +95,16 @@ namespace media_reader { FrameRequestQueue playback_precache_request_queue_; FrameRequestQueue background_precache_request_queue_; + + struct ImmediateFrameRequest { + media::AVFrameID mptr; + caf::actor playhead; + utility::Uuid playhead_uuid; + utility::time_point tp; + timebase::flicks playhead_position; + }; + std::map>> + immediate_frame_requests_; }; } // namespace media_reader diff --git a/include/xstudio/module/attribute.hpp b/include/xstudio/module/attribute.hpp index 929bfb429..c788719b8 100644 --- a/include/xstudio/module/attribute.hpp +++ b/include/xstudio/module/attribute.hpp @@ -22,6 +22,8 @@ namespace module { IntegerAttribute, StringChoiceAttribute, ColourAttribute, + Vec4fAttribute, + FloatVectorAttribute, JsonAttribute }; @@ -34,6 +36,8 @@ namespace module { {FloatAttribute, "FloatScrubber"}, {ActionAttribute, "Action"}, {ColourAttribute, "ColourAttribute"}, + {Vec4fAttribute, "Vec4fAttribute"}, + {FloatVectorAttribute, "FloatVectorAttribute"}, {JsonAttribute, "JsonAttribute"}}; enum Role { @@ -44,6 +48,7 @@ namespace module { AbbrTitle, StringChoices, AbbrStringChoices, + StringChoicesIds, StringChoicesEnabled, ToolTip, CustomMessage, @@ -59,12 +64,15 @@ namespace module { DefaultValue, AbbrValue, UuidRole, - Groups, + UIDataModels, MenuPaths, ToolbarPosition, OverrideValue, SerializeKey, QmlCode, + ModuleUuid, + LeftRightDockWidgetQmlCode, + TopBottomDockWidgetQmlCode, PreferencePath, // use this to set a pref path that means the attribute always // tracks the preference InitOnlyPreferencePath, // use this to set a pref path that doesn't update the @@ -75,7 +83,9 @@ namespace module { TextContainerBox, Colour, HotkeyUuid, - UserData + UserData, + IconPath, + CallbackData }; inline static const std::map role_names = { @@ -86,6 +96,7 @@ namespace module { {AbbrTitle, "abbr_title"}, {StringChoices, "combo_box_options"}, {AbbrStringChoices, "combo_box_abbr_options"}, + {StringChoicesIds, "combo_box_options_ids"}, {StringChoicesEnabled, "combo_box_options_enabled"}, {ToolTip, "tooltip"}, {CustomMessage, "custom_message"}, @@ -101,12 +112,15 @@ namespace module { {AbbrValue, "short_value"}, {DisabledValue, "disabled_value"}, {UuidRole, "attr_uuid"}, - {Groups, "groups"}, + {UIDataModels, "groups"}, {MenuPaths, "menu_paths"}, {ToolbarPosition, "toolbar_position"}, {OverrideValue, "override_value"}, {SerializeKey, "serialize_key"}, {QmlCode, "qml_code"}, + {ModuleUuid, "module_uuid"}, + {LeftRightDockWidgetQmlCode, "left_right_dock_widget_qml_code"}, + {TopBottomDockWidgetQmlCode, "top_bottom_dock_widget_qml_code"}, {PreferencePath, "preference_path"}, {InitOnlyPreferencePath, "init_only_preference_path"}, {FontSize, "font_size"}, @@ -115,7 +129,9 @@ namespace module { {TextContainerBox, "text_alignment_box"}, {Colour, "attr_colour"}, {HotkeyUuid, "hotkey_uuid"}, - {UserData, "user_data"}}; + {UserData, "user_data"}, + {IconPath, "icon_path"}, + {CallbackData, "callback_data"}}; ~Attribute() = default; @@ -141,6 +157,9 @@ namespace module { [[nodiscard]] nlohmann::json role_data_as_json(const int role) const; + void update_role_data_from_json( + const int role, const nlohmann::json &data, const bool notify); + [[nodiscard]] nlohmann::json as_json() const; void update_from_json(const nlohmann::json &data, const bool notify); diff --git a/include/xstudio/module/attribute_role_data.hpp b/include/xstudio/module/attribute_role_data.hpp index 94d7c0760..b561fe665 100644 --- a/include/xstudio/module/attribute_role_data.hpp +++ b/include/xstudio/module/attribute_role_data.hpp @@ -40,7 +40,8 @@ namespace module { _any_to_json{ to_any_to_json( [](nlohmann::json x) -> nlohmann::json { return x; }), - to_any_to_json([](int x) -> nlohmann::json { return nlohmann::json(x); }), + to_any_to_json( + [](int64_t x) -> nlohmann::json { return nlohmann::json(x); }), to_any_to_json( [](float x) -> nlohmann::json { return nlohmann::json(x); }), to_any_to_json( @@ -56,18 +57,27 @@ namespace module { to_any_to_json([](Imath::V3f x) -> nlohmann::json { return nlohmann::json{"vec3", 1, x.x, x.y, x.z}; }), + to_any_to_json([](Imath::V4f x) -> nlohmann::json { + return nlohmann::json{"vec4", 1, x.x, x.y, x.z, x.w}; + }), to_any_to_json( [](utility::ColourTriplet x) -> nlohmann::json { return nlohmann::json{"colour", 1, x.r, x.g, x.b}; }), to_any_to_json>( [](std::vector x) -> nlohmann::json { return nlohmann::json(x); }), + to_any_to_json>( + [](std::vector x) -> nlohmann::json { return nlohmann::json(x); }), to_any_to_json>( [](std::vector x) -> nlohmann::json { return nlohmann::json(x); }), to_any_to_json>( [](std::vector x) -> nlohmann::json { return nlohmann::json(x); }), + to_any_to_json>( + [](std::vector x) -> nlohmann::json { + return nlohmann::json(x); + }), }; return _any_to_json; } @@ -98,6 +108,7 @@ namespace module { template bool __set(const T &v) { // NOLINT if (data_.has_value() && typeid(v) != data_.type()) { + spdlog::warn( "{} Attempt to set AttributeData with type {} with data of type {} and " "value {}", @@ -105,7 +116,9 @@ namespace module { data_.type().name(), typeid(v).name(), to_json().dump()); + return false; + } else if ((data_.has_value() && get() != v) || !data_.has_value()) { data_ = v; return true; @@ -144,12 +157,27 @@ namespace module { rt = set(data.get()); + } else if ( + data.is_array() && data.size() == 6 && data.begin().value().is_string() && + data.begin().value().get() == "vec4") { + + rt = set(data.get()); + } else if ( data.is_array() && data.size() == 5 && data.begin().value().is_string() && data.begin().value().get() == "colour") { rt = set(data.get()); + } else if ( + (data.is_array() || data.is_object()) && + data_.type() == typeid(nlohmann::json)) { + + if (get() != data) { + data_ = data; + rt = true; + } + } else if (data.is_array() && data.size() && data.begin().value().is_string()) { std::vector v; @@ -170,12 +198,11 @@ namespace module { } rt = set(v); - } else if ( - data.is_array() && data.size() && data.begin().value().is_number_float()) { + } else if (data.is_array() && data.size() && data.begin().value().is_number()) { std::vector v; for (auto p = data.begin(); p != data.end(); p++) { - if (p.value().is_number_float()) { + if (p.value().is_number()) { v.push_back(p.value().get()); } } @@ -184,7 +211,7 @@ namespace module { } else if (data.is_boolean()) { rt = set(data.get()); } else if (data.is_number_integer()) { - rt = set(data.get()); + rt = set(data.get()); } else if (data.is_number_float()) { rt = set(data.get()); } else if ( @@ -200,17 +227,18 @@ namespace module { return rt; } - // not pretty, must be a better way of letting an int set a float and vice versa + // not pretty, must be a better way of letting an int64_t set a float and vice versa // without violating type checking bool set(const float &v) { if (data_.has_value()) { if (data_.type() == typeid(float) && get() != v) { data_ = v; return true; - } else if (data_.type() == typeid(int) && get() != (int)v) { - data_ = (int)v; + } else if (data_.type() == typeid(int64_t) && get() != (int64_t)v) { + data_ = (int64_t)v; return true; - } else if (!(data_.type() == typeid(int) || data_.type() == typeid(float))) { + } else if (!(data_.type() == typeid(int64_t) || + data_.type() == typeid(float))) { spdlog::warn( "{} Attempt to set AttributeData with type {} with data of type {} " "and " @@ -225,15 +253,18 @@ namespace module { return true; } - bool set(const int &v) { + bool set(const int v) { return set(int64_t(v)); } + + bool set(const int64_t &v) { if (data_.has_value()) { if (data_.type() == typeid(float) && get() != (float)v) { data_ = (float)v; return true; - } else if (data_.type() == typeid(int) && get() != v) { + } else if (data_.type() == typeid(int64_t) && get() != v) { data_ = v; return true; - } else if (!(data_.type() == typeid(int) || data_.type() == typeid(float))) { + } else if (!(data_.type() == typeid(int64_t) || + data_.type() == typeid(float))) { spdlog::warn( "{} Attempt to set AttributeData with type {} with data of type {} " "and " diff --git a/include/xstudio/module/global_module_events_actor.hpp b/include/xstudio/module/global_module_events_actor.hpp deleted file mode 100644 index 033cdb623..000000000 --- a/include/xstudio/module/global_module_events_actor.hpp +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "xstudio/module/module.hpp" -#include - -namespace xstudio { - -namespace module { - - class GlobalModuleAttrEventsActor : public caf::event_based_actor { - - public: - GlobalModuleAttrEventsActor(caf::actor_config &cfg); - ~GlobalModuleAttrEventsActor() override = default; - - caf::behavior make_behavior() override { return behavior_; } - const char *name() const override { return NAME.c_str(); } - - void on_exit() override; - - private: - inline static const std::string NAME = "GlobalModuleAttrEventsActor"; - caf::behavior behavior_; - caf::actor module_backend_events_group_; - caf::actor playhead_group_; - caf::actor module_ui_events_group_; - - std::vector attr_owners_; - }; - -} // namespace module -} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/module/module.hpp b/include/xstudio/module/module.hpp index d538dd9e0..6af01b70d 100644 --- a/include/xstudio/module/module.hpp +++ b/include/xstudio/module/module.hpp @@ -1,6 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#ifdef __apple__ +#undef nil +#endif + #include #include "xstudio/module/typed_attributes.hpp" @@ -24,6 +28,8 @@ namespace module { virtual ~Module(); + void parent_actor_exiting(); + FloatAttribute *add_float_attribute( const std::string &title, const std::string &abbr_title = "", @@ -52,6 +58,9 @@ namespace module { // enabled ); + IntegerVecAttribute * + add_int_vec_attribute(const std::string &title, const std::string &abbr_title = ""); + JsonAttribute *add_json_attribute( const std::string &title, const std::string &abbr_title = "", @@ -69,9 +78,9 @@ namespace module { IntegerAttribute *add_integer_attribute( const std::string &title, const std::string &abbr_title, - const int value, - const int int_min = std::numeric_limits::lowest(), - const int int_max = std::numeric_limits::max()); + const int64_t value, + const int64_t int_min = std::numeric_limits::lowest(), + const int64_t int_max = std::numeric_limits::max()); ActionAttribute * add_action_attribute(const std::string &title, const std::string &abbr_title); @@ -81,10 +90,29 @@ namespace module { const std::string &abbr_title, const utility::ColourTriplet &value); + Vec4fAttribute *add_vec4f_attribute( + const std::string &title, const std::string &abbr_title, const Imath::V4f &value); + + Vec4fAttribute *add_vec4f_attribute( + const std::string &title, + const std::string &abbr_title, + const Imath::V4f &value, + const Imath::V4f &min, + const Imath::V4f &max, + const Imath::V4f &step); + + FloatVectorAttribute *add_float_vector_attribute( + const std::string &title, + const std::string &abbr_title, + const std::vector &value, + const std::vector &min, + const std::vector &max, + const std::vector &step); + Attribute *add_attribute( const std::string &title, - const utility::JsonStore &value, - const utility::JsonStore &role_data); + const utility::JsonStore &value = utility::JsonStore(), + const utility::JsonStore &role_data = utility::JsonStore()); [[nodiscard]] virtual AttributeSet full_module( const std::vector &attr_groups = std::vector()) const; @@ -96,7 +124,7 @@ namespace module { virtual void update_attrs_from_preferences(const utility::JsonStore &); - virtual bool remove_attribute(const utility::Uuid &attribute_uuid); + virtual void remove_attribute(const utility::Uuid &attribute_uuid); [[nodiscard]] virtual utility::JsonStore serialise() const; @@ -146,6 +174,12 @@ namespace module { const bool redraw_viewport = false, const bool self_notify = true); + utility::Uuid register_hotkey( + const std::string sequence, + const std::string &hotkey_name, + const std::string &description, + const bool autorepeat = false, + const std::string &component = "MODULE_NAME"); utility::Uuid register_hotkey( int default_keycode, @@ -153,18 +187,16 @@ namespace module { const std::string &hotkey_name, const std::string &description, const bool autorepeat = false, - const std::string &component = "MODULE_NAME", - const std::string &context = "any" // context "any" means hotkey will be activated - // regardless of which window it came from - ); + const std::string &component = "MODULE_NAME"); void remove_hotkey(const utility::Uuid &hotkey_uuid); // re-implement to handle viewport hotkey events. context argument indicates where the // hotkey was pressed (e.g. 'main_viewport', 'popout_viewport' etc.) - virtual void - hotkey_pressed(const utility::Uuid & /*hotkey_uuid*/, const std::string & /*context*/) { - } + virtual void hotkey_pressed( + const utility::Uuid & /*hotkey_uuid*/, + const std::string & /*context*/, + const std::string & /*window*/) {} // re-implement to handle viewport hotkey release events virtual void hotkey_released( @@ -182,7 +214,7 @@ namespace module { caf::scoped_actor home_system() { return self()->home_system(); } - caf::actor self() { return caf::actor_cast(parent_actor_addr_); } + caf::actor self() const { return caf::actor_cast(parent_actor_addr_); } // re-implement to receive callback when the on-screen media changes. To virtual void on_screen_media_changed(caf::actor media) {} @@ -202,7 +234,14 @@ namespace module { virtual void connect_to_viewport( const std::string &viewport_name, const std::string &viewport_toolbar_name, - bool connect); + bool connect, + caf::actor viewport); + + // re-implement to execute useful state reset for your module when the + // user hits the 'reset-viewport' hotkey or button. For example, a + // colour pipeline plugin might reset exposure / gamma values in this + // function. + virtual void reset() {} protected: /* Call this method with your StringChoiceAttribute to expose it in @@ -230,6 +269,43 @@ namespace module { const std::string top_level_menu, const std::string before = std::string{}); + utility::Uuid insert_menu_item( + const std::string &menu_model_name, + const std::string &menu_text, + const std::string &menu_path, + const float menu_item_position, + Attribute *attr = nullptr, + const bool divider = false, + const utility::Uuid &hotkey = utility::Uuid(), + const std::string &user_data = std::string()); + + utility::Uuid insert_hotkey_into_menu( + const std::string &menu_model_name, + const std::string &menu_path, + const float menu_item_position, + const utility::Uuid &hotkey); + + utility::Uuid insert_menu_divider( + const std::string &menu_model_name, + const std::string &menu_path, + const float menu_item_position); + + void set_submenu_position_in_parent( + const std::string &menu_model_name, + const std::string &submenu, + const float submenu_position); + + void remove_all_menu_items(const std::string &menu_model_name); + + void remove_menu_item(const std::string &menu_model_name, const utility::Uuid item_id); + + virtual void menu_item_activated( + const utility::JsonStore &menu_item_data, const std::string &user_data) {} + + utility::JsonStore attribute_menu_item_data(Attribute *attr); + + void update_attribute_menu_item_data(Attribute *attr); + void make_attribute_visible_in_viewport_toolbar( Attribute *attr, const bool make_visible = true); @@ -253,12 +329,12 @@ namespace module { caf::actor_addr parent_actor_addr_; void connect_to_ui(); - void disconnect_from_ui(); void grab_mouse_focus(); void release_mouse_focus(); void grab_keyboard_focus(); void release_keyboard_focus(); - void listen_to_playhead_events(const bool listen = true); + + virtual void disconnect_from_ui(); [[nodiscard]] bool connected_to_ui() const { return connected_to_ui_; } virtual void connected_to_ui_changed() {} @@ -268,16 +344,62 @@ namespace module { std::vector attributes_; + // Call this code to expose a UI panel in xSTUDIO's panels menu. See + // annotations tool for an example. To allow the panel to be shown in + // a floating window, provide an incon path and a position within + // the pop-out button shelf (top left of viewport panels) for it to + // appear in. + void register_ui_panel_qml( + const std::string &panel_name, + const std::string &qml_code, + const float position_in_menu, + const std::string &viewport_popout_button_icon_path = "", + const float &viewport_popout_button_position = -1.0f, + const utility::Uuid toggle_hotkey_id = utility::Uuid()); + + // Call this code to expose a widget that can dock to the left/right or + // top/bottom of the viewport + Attribute *register_viewport_dockable_widget( + const std::string &widget_name, + const std::string &button_icon_qrc_path, + const std::string &button_tooltip, + const float button_position, + const bool enabled, + const std::string &left_right_dockable_widget_qml, + const std::string &top_bottom_dockable_widget_qml, + const utility::Uuid toggle_widget_visible_hotkey = utility::Uuid()); + + std::set dock_widget_attributes_; + + // re-implement to get a callback when your dockable widget has been + // made visible by the user clicking on the toggle button in the + // viewport action bar + virtual void viewport_dockable_widget_activated(std::string &widget_name) {} + + // re-implement to get a callback when your dockable widget has been + // hidden by the user clicking on the toggle button in the + // viewport action bar. Also this callback is made when the dockable + // widget is hidden or goes off screen. + // If you have a tool that captures mouse events, for example, you may + // want to stop grabbing mouse events when the widget is hidden + virtual void viewport_dockable_widget_deactivated(std::string &widget_name) {} + + // To instance a single global UI item on startup (which might, for + // example, insert menu items into the xstudio interface) pass the + // necessary qml to this function. + void register_singleton_qml(const std::string &qml_code); + + // This method can be overriden to receive a callback when the number of viewports + // connected to the module changes. This is used by the Playhead class, for example + virtual void connected_viewports_changed(std::set &connected_viewports) {} + private: void notify_attribute_destroyed(Attribute *); void attribute_changed(const utility::Uuid &attr_uuid, const int role_id, bool notify); void add_attribute(Attribute *attr); - caf::actor global_module_events_actor_; caf::actor keypress_monitor_actor_; caf::actor keyboard_and_mouse_group_; - caf::actor ui_attribute_events_group_; - caf::actor module_events_group_; caf::actor attribute_events_group_; caf::actor_addr attr_sync_source_adress_; @@ -285,15 +407,18 @@ namespace module { std::set fully_linked_modules_; std::set linked_attrs_; std::set attrs_in_toolbar_; - std::set connected_viewports_; + std::set connected_viewport_names_; + std::set connected_viewports_; bool connected_to_ui_ = {false}; bool linking_disabled_ = {false}; utility::Uuid module_uuid_; std::string name_; std::set attrs_waiting_to_update_prefs_; + std::map> menu_items_; + - std::vector unregistered_hotkeys_; + std::map monitor_; }; template diff --git a/include/xstudio/module/typed_attributes.hpp b/include/xstudio/module/typed_attributes.hpp index 2097add56..0043350b1 100644 --- a/include/xstudio/module/typed_attributes.hpp +++ b/include/xstudio/module/typed_attributes.hpp @@ -75,14 +75,14 @@ namespace module { QmlCodeAttribute(const std::string &title, const std::string &code); }; - class IntegerAttribute : public TypeAttribute { + class IntegerAttribute : public TypeAttribute { public: IntegerAttribute( const std::string &title, const std::string &abbr_title, - const int value, - const int int_min = std::numeric_limits::lowest(), - const int int_max = std::numeric_limits::max()); + const int64_t value, + const int64_t int_min = std::numeric_limits::lowest(), + const int64_t int_max = std::numeric_limits::max()); }; class ColourAttribute : public TypeAttribute { @@ -93,6 +93,20 @@ namespace module { const utility::ColourTriplet &value); }; + class Vec4fAttribute : public TypeAttribute { + public: + Vec4fAttribute( + const std::string &title, const std::string &abbr_title, const Imath::V4f &value); + }; + + class FloatVectorAttribute : public TypeAttribute> { + public: + FloatVectorAttribute( + const std::string &title, + const std::string &abbr_title, + const std::vector &value); + }; + class JsonAttribute : public TypeAttribute { public: JsonAttribute( @@ -101,5 +115,13 @@ namespace module { const nlohmann::json &value); }; + class IntegerVecAttribute : public TypeAttribute> { + public: + IntegerVecAttribute( + const std::string &title, + const std::string &abbr_title, + const std::vector &value); + }; + } // namespace module } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/playhead/edit_list_actor.hpp b/include/xstudio/playhead/edit_list_actor.hpp deleted file mode 100644 index 2b714b0b8..000000000 --- a/include/xstudio/playhead/edit_list_actor.hpp +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include - -#include "xstudio/media/media.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/uuid.hpp" - -namespace xstudio { -namespace playhead { - class EditListActor : public caf::event_based_actor { - public: - // SelectionStringActor( - // caf::actor_config& cfg, - // const utility::JsonStore &jsn - // ); - EditListActor( - caf::actor_config &cfg, - const std::string &name, - const std::vector &media_clip_list, - const media::MediaType mt); - ~EditListActor() override = default; - - const char *name() const override { return NAME.c_str(); } - - private: - inline static const std::string NAME = "EditListActor"; - - caf::behavior make_behavior() override { return behavior_; } - - void full_media_key_list(caf::typed_response_promise rp); - - caf::actor media_source_actor(const utility::Uuid &source_uuid); - - void recursive_deliver_all_media_pointers( - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate, - const media::MediaType media_type, - const int clip_index, - const timebase::flicks clip_start_time_point, - std::shared_ptr result, - caf::typed_response_promise rp); - - private: - caf::behavior behavior_; - caf::actor event_group_; - std::map source_actors_per_uuid_; - std::vector source_actors_; - utility::EditList edit_list_; - int frames_offset_; - }; -} // namespace playhead -} // namespace xstudio diff --git a/include/xstudio/playhead/enums.hpp b/include/xstudio/playhead/enums.hpp index c09ff3173..84a5beb6a 100644 --- a/include/xstudio/playhead/enums.hpp +++ b/include/xstudio/playhead/enums.hpp @@ -3,6 +3,7 @@ namespace xstudio { namespace playhead { + typedef enum { CM_STRING = 0, CM_AB, @@ -12,6 +13,8 @@ namespace playhead { CM_OFF } CompareMode; + typedef enum { AM_STRING = 0, AM_ONE, AM_ALL, AM_TEN } AssemblyMode; + typedef enum { AAM_ALIGN_OFF = 0, AAM_ALIGN_FRAMES, AAM_ALIGN_TRIM } AutoAlignMode; typedef enum { LM_PLAY_ONCE = 0, LM_LOOP, LM_PING_PONG } LoopMode; @@ -22,5 +25,15 @@ namespace playhead { OM_HOLD = 0x2L, OM_LOOP = 0x3L } OverflowMode; + + typedef enum { + SM_NO_UPDATE = 0x0L, // NO OP + SM_CLEAR = 0x1L, + SM_SELECT = 0x2L, + SM_CLEAR_AND_SELECT = 0x3L, + SM_DESELECT = 0x4L, + SM_TOGGLE = 0x8L + } SelectionMode; + } // namespace playhead -} // namespace xstudio \ No newline at end of file +} // namespace xstudio diff --git a/include/xstudio/playhead/playhead.hpp b/include/xstudio/playhead/playhead.hpp index 69bf68e23..51daf0c7b 100644 --- a/include/xstudio/playhead/playhead.hpp +++ b/include/xstudio/playhead/playhead.hpp @@ -11,7 +11,6 @@ #include "xstudio/media/media.hpp" #include "xstudio/utility/chrono.hpp" #include "xstudio/utility/container.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/timecode.hpp" @@ -20,29 +19,46 @@ namespace xstudio { namespace playhead { - class PlayheadBase : public utility::Container, public module::Module { + class PlayheadBase : public module::Module { public: typedef std::optional OptionalTimePoint; PlayheadBase( const std::string &name = "PlayheadBase", const utility::Uuid uuid = utility::Uuid::generate()); - PlayheadBase(const utility::JsonStore &jsn); - ~PlayheadBase() override = default; + virtual ~PlayheadBase(); [[nodiscard]] utility::JsonStore serialise() const override; + void deserialise(const utility::JsonStore &jsn); + OptionalTimePoint play_step(); [[nodiscard]] timebase::flicks position() const { return position_; } + + // adjusts the playhead position during playback by audio_delay_millisecs_ + // to allow for timing difference between audio and video playback + [[nodiscard]] timebase::flicks adjusted_position() const; + void set_position(const timebase::flicks p); + inline static const std::map loop_modes_ = { + {"Play Once", playhead::LoopMode::LM_PLAY_ONCE}, + {"Loop", playhead::LoopMode::LM_LOOP}, + {"Ping Pong", playhead::LoopMode::LM_PING_PONG}}; + [[nodiscard]] bool playing() const { return playing_->value(); } [[nodiscard]] bool forward() const { return forward_->value(); } [[nodiscard]] AutoAlignMode auto_align_mode() const; - [[nodiscard]] int loop() const { return loop_mode_->value(); } - [[nodiscard]] CompareMode compare_mode() const; + [[nodiscard]] playhead::LoopMode loop() const { + const auto p = loop_modes_.find(loop_mode_->value()); + if (p != loop_modes_.end()) { + return p->second; + } + return playhead::LoopMode::LM_LOOP; + } + [[nodiscard]] AssemblyMode assemply_mode() const; [[nodiscard]] float velocity() const { return velocity_->value(); } [[nodiscard]] float velocity_multiplier() const { return velocity_multiplier_->value(); @@ -59,15 +75,25 @@ namespace playhead { ? loop_end_ : timebase::flicks(std::numeric_limits::max()); } - [[nodiscard]] bool use_loop_range() const { return do_looping_->value(); } + [[nodiscard]] bool use_loop_range() const { return loop_range_enabled_->value(); } [[nodiscard]] timebase::flicks duration() const { return duration_; } [[nodiscard]] timebase::flicks effective_frame_period() const; + [[nodiscard]] AssemblyMode assembly_mode() const { return assembly_mode_; } + [[nodiscard]] const utility::time_point &last_playhead_set_timepoint() const { + return position_set_tp_; + } + timebase::flicks clamp_timepoint_to_loop_range(const timebase::flicks pos) const; void set_forward(const bool forward = true) { forward_->set_value(forward); } - void set_loop(const LoopMode loop = LM_LOOP) { loop_mode_->set_value(loop); } + void set_loop(const LoopMode loop = LM_LOOP) { + for (const auto &p : loop_modes_) { + if (p.second == loop) { + loop_mode_->set_value(p.first); + } + } + } void set_playing(const bool play = true); - timebase::flicks adjusted_position() const; void set_play_rate_mode(const utility::TimeSourceMode play_rate_mode) { play_rate_mode_ = play_rate_mode; } @@ -75,9 +101,13 @@ namespace playhead { void set_velocity_multiplier(const float velocity_multiplier = 1.0f) { velocity_multiplier_->set_value(velocity_multiplier); } - void set_playhead_rate(const utility::FrameRate &rate) { playhead_rate_ = rate; } + void set_playhead_rate(const utility::FrameRate &rate) { + playhead_rate_ = rate; + playhead_rate_attr_->set_value(rate.to_seconds()); + } void set_duration(const timebase::flicks duration); - void set_compare_mode(const CompareMode mode); + void set_assembly_mode(const AssemblyMode mode); + void set_auto_align_mode(const AutoAlignMode mode); bool set_use_loop_range(const bool use_loop_range); bool set_loop_start(const timebase::flicks loop_start); @@ -86,28 +116,31 @@ namespace playhead { void throttle(); void revert_throttle(); + void disconnect_from_ui() override; + bool pointer_event(const ui::PointerEvent &) override; - void - hotkey_pressed(const utility::Uuid &hotkey_uuid, const std::string &context) override; + void hotkey_pressed( + const utility::Uuid &hotkey_uuid, + const std::string &context, + const std::string &window) override; void connect_to_viewport( const std::string &viewport_name, const std::string &viewport_toolbar_name, - bool connect) override; + bool connect, + caf::actor viewport) override; + + void menu_item_activated( + const utility::JsonStore &menu_item_data, const std::string &user_data) override; inline static const std::chrono::milliseconds playback_step_increment = std::chrono::milliseconds(5); - inline static const std::vector> - compare_mode_names = { - {CM_STRING, "String", "Str", true}, - {CM_AB, "A/B", "A/B", true}, - {CM_VERTICAL, "Vertical", "Vert", false}, - {CM_HORIZONTAL, "Horizontal", "Horiz", false}, - {CM_GRID, "Grid", "Grid", false}, - {CM_OFF, "Off", "Off", true}}; + void reset() override; - private: + void register_hotkeys() override; + + protected: void play_faster(const bool forwards); inline static const std::vector< @@ -129,45 +162,75 @@ namespace playhead { utility::Uuid play_forwards_hotkey_; utility::Uuid play_backwards_hotkey_; utility::Uuid stop_play_hotkey_; - utility::Uuid reset_hotkey_; - + utility::Uuid toggle_loop_range_; + utility::Uuid set_loop_in_; + utility::Uuid set_loop_out_; + utility::Uuid step_forward_; + utility::Uuid step_backward_; + utility::Uuid jump_to_first_frame_; + utility::Uuid jump_to_last_frame_; + utility::Uuid cycle_image_layer_up_; + utility::Uuid cycle_image_layer_down_; + + bool deserialised_ = {false}; float drag_start_x_; timebase::flicks drag_start_playhead_position_; + utility::time_point click_timepoint_; - protected: void add_attributes(); - utility::time_point last_step_; + utility::time_point last_step_, position_set_tp_; float throttle_{1.0f}; + AssemblyMode assembly_mode_ = {AssemblyMode::AM_ONE}; module::FloatAttribute *velocity_; - module::StringChoiceAttribute *compare_mode_; module::QmlCodeAttribute *source_; + module::StringAttribute *image_source_name_; module::StringChoiceAttribute *image_source_; + module::StringChoiceAttribute *image_stream_; module::StringChoiceAttribute *audio_source_; - module::FloatAttribute *velocity_multiplier_; + module::IntegerAttribute *velocity_multiplier_; module::BooleanAttribute *playing_; module::BooleanAttribute *forward_; module::StringChoiceAttribute *auto_align_mode_; + module::IntegerAttribute *audio_delay_millisecs_; + module::JsonAttribute *cached_frames_; + module::IntegerVecAttribute *bookmarked_frames_; + module::IntegerVecAttribute *media_transition_frames_; module::IntegerAttribute *max_compare_sources_; module::BooleanAttribute *restore_play_state_after_scrub_; + module::BooleanAttribute *click_to_toggle_play_; + module::BooleanAttribute *timeline_mode_; module::IntegerAttribute *viewport_scrub_sensitivity_; + module::IntegerAttribute *source_offset_frames_; + module::BooleanAttribute *connect_to_ui_attr_; - module::IntegerAttribute *loop_mode_; + module::StringChoiceAttribute *loop_mode_; module::IntegerAttribute *loop_start_frame_; module::IntegerAttribute *loop_end_frame_; module::IntegerAttribute *playhead_logical_frame_; + module::FloatAttribute *playhead_position_seconds_; + module::IntegerAttribute *playhead_position_flicks_; + module::FloatAttribute *playhead_rate_attr_; module::IntegerAttribute *playhead_media_logical_frame_; module::IntegerAttribute *playhead_media_frame_; module::IntegerAttribute *duration_frames_; - module::StringAttribute *current_source_frame_timecode_; + module::IntegerAttribute *key_playhead_index_; + module::IntegerAttribute *num_sub_playheads_; + module::FloatAttribute *duration_seconds_; + module::StringAttribute *current_frame_timecode_; + module::IntegerAttribute *current_frame_timecode_as_frame_; module::StringAttribute *current_media_uuid_; module::StringAttribute *current_media_source_uuid_; - module::BooleanAttribute *do_looping_; - module::IntegerAttribute *audio_delay_millisecs_; + module::BooleanAttribute *loop_range_enabled_; + module::BooleanAttribute *user_is_frame_scrubbing_; + module::BooleanAttribute *pinned_source_mode_; + module::StringAttribute *compare_mode_; + module::IntegerVecAttribute *source_alignment_values_; bool was_playing_when_scrub_started_ = {false}; + std::set active_viewports_; }; } // namespace playhead } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/playhead/playhead_actor.hpp b/include/xstudio/playhead/playhead_actor.hpp index e90136ee4..3709a1b45 100644 --- a/include/xstudio/playhead/playhead_actor.hpp +++ b/include/xstudio/playhead/playhead_actor.hpp @@ -8,7 +8,6 @@ #include "xstudio/media/media.hpp" #include "xstudio/media_reader/media_reader.hpp" #include "xstudio/playhead/playhead.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/timecode.hpp" #include "xstudio/utility/uuid.hpp" @@ -16,18 +15,23 @@ namespace xstudio { namespace playhead { + enum AudioPath { GLOBAL_AUDIO, INDEPENDENT_AUDIO, NO_AUDIO }; + class PlayheadActor : public caf::event_based_actor, public PlayheadBase { public: - PlayheadActor( - caf::actor_config &cfg, - const utility::JsonStore &jsn, - caf::actor playlist_selection = caf::actor()); + // Constructor for special 'offscreen only' playhead that doesn't need + // to connect to the UI to function + PlayheadActor(caf::actor_config &cfg, const std::string &name); + PlayheadActor( caf::actor_config &cfg, const std::string &name, - caf::actor playlist_selection = caf::actor(), - const utility::Uuid uuid = utility::Uuid::generate()); - ~PlayheadActor() override = default; + const AudioPath, + caf::actor playlist_selection = caf::actor(), + const utility::Uuid uuid = utility::Uuid::generate(), + caf::actor_addr parent_playlist = caf::actor_addr()); + + virtual ~PlayheadActor() = default; const char *name() const override { return NAME.c_str(); } @@ -44,15 +48,17 @@ namespace playhead { void clear_all_precache_requests(caf::typed_response_promise rp); void clear_child_playheads(); - caf::actor make_child_playhead(caf::actor source); + caf::actor make_child_playhead(utility::UuidActor source); void make_audio_child_playhead(const int source_index); - void rebuild(); - void connect_to_playlist_selection_actor(caf::actor playlist_selection); - void new_source_list(const std::vector &sl); + void rebuild_from_timeline_sources(); + void rebuild_from_dynamic_sources(); + + void connect_to_playlist_selection_actor( + caf::actor playlist_selection, caf::typed_response_promise rp); + void new_source_list(); void switch_key_playhead(int idx); void calculate_duration(); - void update_child_playhead_positions( - const bool force_broadcast = false, const bool playhead_scrubbing = false); + void update_child_playhead_positions(const bool force_broadcast); void notify_loop_end_changed(); void notify_loop_start_changed(); void update_duration(caf::typed_response_promise rp); @@ -69,6 +75,8 @@ namespace playhead { void rebuild_cached_frames_status(); void select_media(const utility::UuidList &selection, caf::typed_response_promise &rp); + void match_video_track_durations(); + void align_audio_playhead(); void align_clip_frame_numbers(); void move_playhead_to_last_viewed_frame_of_current_source(); void @@ -76,56 +84,102 @@ namespace playhead { void current_media_changed(caf::actor media_actor, const bool force = false); void update_source_multichoice( module::StringChoiceAttribute *attr, const media::MediaType mt); + void update_stream_multichoice( + module::StringChoiceAttribute *streams_attr, const media::MediaType mt); + void restart_readahead_cacheing(const bool all_child_playheads, const bool force = false); void switch_media_source(const std::string new_source_name, const media::MediaType mt); - bool has_selection_changed(); + void switch_media_stream( + caf::actor media_actor, + const std::string new_stream_name, + const media::MediaType mt, + bool apply_to_selected); int previous_selected_sources_count_ = {-1}; - void manage_playback_video_refresh_sync( - const utility::time_point &when_video_framebuffer_was_swapped_to_screen, - const timebase::flicks video_refresh_rate_hint, - const int viewer_index); + void on_exit() override; protected: void attribute_changed(const utility::Uuid &attr_uuid, const int /*role*/) override; + void hotkey_pressed( + const utility::Uuid &hotkey_uuid, + const std::string &context, + const std::string &window) override; + void + hotkey_released(const utility::Uuid &hotkey_uuid, const std::string &context) override; + bool timeline_mode() const { return pinned_source_mode_->value() && timeline_actor_; } + bool contact_sheet_mode() const { return contact_sheet_mode_; } void connected_to_ui_changed() override; void check_if_loop_range_makes_sense(); + void make_source_menu_model(); + void apply_compare_prefs(); + void step_to_next_media(const bool forwards); + + void connected_viewports_changed(std::set &connected_viewports) override; caf::message_handler behavior_; + caf::actor playlist_selection_; - caf::actor empty_clip_; + utility::UuidActor empty_clip_; caf::actor broadcast_; - caf::actor event_group_; caf::actor fps_moniotor_group_; caf::actor viewport_events_group_; caf::actor playhead_media_events_group_; - std::vector sub_playheads_; - std::vector source_wrappers_; - std::vector source_actors_; - caf::actor key_playhead_; - caf::actor audio_output_actor_; - caf::actor playhead_events_actor_; + caf::actor event_group_; + caf::actor media_actor_; + + utility::UuidActor hero_sub_playhead_; + utility::UuidActorVector sub_playheads_; + + utility::UuidActor video_string_out_actor_; + utility::UuidActor timeline_actor_; + utility::UuidActorVector source_actors_; + utility::UuidActorVector previous_source_actors_; + utility::UuidActorVector timeline_track_actors_; + utility::UuidActorVector dynamic_source_actors_; + + utility::UuidActorVector string_audio_sources_; + utility::UuidActor audio_src_; caf::actor audio_playhead_; caf::actor audio_playhead_retimer_; + + caf::actor audio_output_actor_; + caf::actor playhead_events_actor_; caf::actor image_cache_; caf::actor pre_reader_; caf::actor_addr playlist_selection_addr_; + caf::actor_addr parent_playlist_; + utility::Uuid previous_source_uuid_; utility::Uuid current_source_uuid_; - utility::Uuid key_playhead_uuid_; std::map media_frame_per_media_uuid_; + std::map switch_key_playhead_hotkeys_; + utility::Uuid move_selection_up_hotkey_; + utility::Uuid move_selection_down_hotkey_; + utility::Uuid jump_to_previous_note_hotkey_; + utility::Uuid jump_to_next_note_hotkey_; + + std::map source_offsets_; + // std::map media_frame_per_media_uuid_; - std::vector> cached_frames_ranges_; std::vector> bookmark_frames_ranges_; std::set frames_cached_; - media::MediaKeyVector all_frames_keys_; + media::AVFrameIDs all_frame_ids_; bool updating_source_list_ = {false}; bool child_playhead_changed_ = {false}; timebase::flicks vid_refresh_sync_phase_adjust_ = timebase::flicks{0}; int media_logical_frame_ = {0}; + float step_keypress_event_id_ = {0}; + bool precacheing_enabled_ = {true}; + bool wrap_sources_ = {false}; + bool contact_sheet_mode_ = {false}; + int sub_playhead_precache_idx_ = {0}; + bool offscreen_only_ = {false}; + const AudioPath audio_path_; + + utility::UuidActorVector to_uuid_actor_vec(const std::vector &actors); }; } // namespace playhead } // namespace xstudio diff --git a/include/xstudio/playhead/playhead_global_events_actor.hpp b/include/xstudio/playhead/playhead_global_events_actor.hpp index e485d50e6..922b14283 100644 --- a/include/xstudio/playhead/playhead_global_events_actor.hpp +++ b/include/xstudio/playhead/playhead_global_events_actor.hpp @@ -8,7 +8,6 @@ #include "xstudio/media/media.hpp" #include "xstudio/media_reader/media_reader.hpp" #include "xstudio/playhead/playhead.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/timecode.hpp" #include "xstudio/utility/uuid.hpp" @@ -19,7 +18,6 @@ namespace playhead { class PlayheadGlobalEventsActor : public caf::event_based_actor { public: PlayheadGlobalEventsActor(caf::actor_config &cfg); - ~PlayheadGlobalEventsActor() override = default; const char *name() const override { return NAME.c_str(); } @@ -37,10 +35,19 @@ namespace playhead { caf::behavior make_behavior() override { return behavior_; } protected: + void on_exit() override; + void monitor_it(const caf::actor &actor); + caf::behavior behavior_; caf::actor event_group_; - caf::actor on_screen_playhead_; - std::map viewports_; + caf::actor fine_grain_events_group_; + caf::actor global_active_playhead_; + struct ViewportAndPlayhead { + caf::actor viewport; + caf::actor playhead; + }; + std::map viewports_; + std::map monitor_; }; } // namespace playhead } // namespace xstudio diff --git a/include/xstudio/playhead/playhead_selection.hpp b/include/xstudio/playhead/playhead_selection.hpp index 7b5f13aa1..8dfa0871c 100644 --- a/include/xstudio/playhead/playhead_selection.hpp +++ b/include/xstudio/playhead/playhead_selection.hpp @@ -7,7 +7,6 @@ #include "xstudio/media/media.hpp" #include "xstudio/utility/container.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" diff --git a/include/xstudio/playhead/playhead_selection_actor.hpp b/include/xstudio/playhead/playhead_selection_actor.hpp index a8fd2b7b0..9668e4f0e 100644 --- a/include/xstudio/playhead/playhead_selection_actor.hpp +++ b/include/xstudio/playhead/playhead_selection_actor.hpp @@ -18,7 +18,7 @@ namespace playhead { caf::actor_config &cfg, const utility::JsonStore &jsn, caf::actor playlist); PlayheadSelectionActor( caf::actor_config &cfg, const std::string &name, caf::actor playlist); - ~PlayheadSelectionActor() override = default; + virtual ~PlayheadSelectionActor() = default; const char *name() const override { return NAME.c_str(); } @@ -26,15 +26,23 @@ namespace playhead { inline static const std::string NAME = "PlayheadSelectionActor"; void init(); - caf::behavior make_behavior() override { return behavior_; } + void on_exit() override; - void select_media(const utility::UuidList &media_uuids); + + caf::message_handler message_handler(); + + caf::behavior make_behavior() override { + return message_handler().or_else(base_.container_message_handler(this)); + } + + void select_media( + const utility::UuidVector &media_uuids = utility::UuidVector(), + const bool retry = true, + const SelectionMode mode = SM_CLEAR_AND_SELECT); void insert_actor( caf::actor actor, const utility::Uuid media_uuid, const utility::Uuid &before_uuid); - void remove_dead_actor(caf::actor_addr actor); - void select_all(); void select_one(); @@ -43,10 +51,10 @@ namespace playhead { private: PlayheadSelection base_; - caf::behavior behavior_; - caf::actor event_group_; caf::actor playlist_; std::map source_actors_; + std::string filter_string_; + std::map monitor_; }; } // namespace playhead } // namespace xstudio diff --git a/include/xstudio/playhead/retime_actor.hpp b/include/xstudio/playhead/retime_actor.hpp deleted file mode 100644 index ba83ba9c8..000000000 --- a/include/xstudio/playhead/retime_actor.hpp +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include - -#include "xstudio/utility/uuid.hpp" -#include "xstudio/utility/edit_list.hpp" -#include "xstudio/media/media.hpp" - -namespace xstudio { -namespace playhead { - class RetimeActor : public caf::event_based_actor { - public: - RetimeActor( - caf::actor_config &cfg, - const std::string &name, - caf::actor &source, - const media::MediaType); - ~RetimeActor() override = default; - - const char *name() const override { return NAME.c_str(); } - - private: - inline static const std::string NAME = "RetimeActor"; - - caf::behavior make_behavior() override { return behavior_; } - - private: - enum RetimeFrameResult { FAIL, OUT_OF_RANGE, HELD_FRAME, LOOPED_RANGE, IN_RANGE }; - - void set_duration(const timebase::flicks &new_duration); - int apply_retime(const int logical_frame, RetimeFrameResult &retime_result); - void recursive_deliver_all_media_pointers( - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate, - const media::MediaType media_type, - const int clip_index, - const timebase::flicks clip_start_time_point, - std::shared_ptr result, - caf::typed_response_promise rp); - void get_source_edit_list(const media::MediaType mt); - - caf::behavior behavior_; - caf::actor event_group_; - caf::actor source_; - int frames_offset_; - utility::Uuid uuid_; - utility::EditList source_edit_list_, retime_edit_list_; - OverflowMode overflow_mode_ = {OM_HOLD}; - timebase::flicks forced_duration_ = {timebase::k_flicks_zero_seconds}; - }; -} // namespace playhead -} // namespace xstudio diff --git a/include/xstudio/playhead/string_out_actor.hpp b/include/xstudio/playhead/string_out_actor.hpp new file mode 100644 index 000000000..c8f651089 --- /dev/null +++ b/include/xstudio/playhead/string_out_actor.hpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +// #include + +#include "xstudio/utility/uuid.hpp" +#include "xstudio/media/media.hpp" + +namespace xstudio { +namespace playhead { + + /* This simple actor allows a number of sources to be 'strung' together + so that they can be played in a sequence. This supports the 'String' + compare mode. It provides the one message handler required to be + playable by a SubPlayhead, plug it handles event messages emitted by + any of its sources. + + This handles mixed frame rate sources. + + */ + class StringOutActor : public caf::event_based_actor { + public: + StringOutActor(caf::actor_config &cfg, const utility::UuidActorVector &sources); + + virtual ~StringOutActor() = default; + + const char *name() const override { return NAME.c_str(); } + + private: + void build_frame_map( + const media::MediaType media_type, + const utility::TimeSourceMode tsm, + const utility::FrameRate &override_rate, + caf::typed_response_promise rp); + + + void finalise_frame_map(caf::typed_response_promise rp); + + inline static const std::string NAME = "StringOutActor"; + + caf::behavior make_behavior() override { return behavior_; } + + utility::UuidActorVector source_actors_; + caf::actor event_group_; + std::map source_frames_; + caf::behavior behavior_; + }; +} // namespace playhead +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/playhead/sub_playhead.hpp b/include/xstudio/playhead/sub_playhead.hpp index 76c145a1a..30b6be201 100644 --- a/include/xstudio/playhead/sub_playhead.hpp +++ b/include/xstudio/playhead/sub_playhead.hpp @@ -8,7 +8,6 @@ #include "xstudio/media/media.hpp" #include "xstudio/media_reader/media_reader.hpp" #include "xstudio/utility/chrono.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/timecode.hpp" #include "xstudio/utility/uuid.hpp" @@ -21,14 +20,16 @@ namespace playhead { SubPlayhead( caf::actor_config &cfg, const std::string &name, - caf::actor source, + utility::UuidActor source, caf::actor parent, + const bool source_is_timeline, const timebase::flicks loop_in_point_, const timebase::flicks loop_out_point_, const utility::TimeSourceMode time_source_mode_, const utility::FrameRate override_frame_rate_, - const media::MediaType media_type); - ~SubPlayhead() override = default; + const media::MediaType media_type, + const utility::Uuid &uuid = utility::Uuid::generate()); + ~SubPlayhead(); const char *name() const override { return NAME.c_str(); } @@ -38,10 +39,13 @@ namespace playhead { void set_position( const timebase::flicks time, const bool forwards, - const bool playing = false, - const float velocity = 1.0f, - const bool force_updates = false, - const bool timeline_scrubbing = false); + const bool playing, + const float velocity, + const bool force_updates, + const bool active_in_ui, + const bool scrubbing); + + void on_exit() override; void init(); @@ -58,6 +62,8 @@ namespace playhead { std::shared_ptr frame, const bool is_future_frame); + void broadcast_audio_samples(); + std::vector get_lookahead_frame_pointers( media::AVFrameIDsAndTimePoints &result, const int max_num_frames); @@ -74,7 +80,8 @@ namespace playhead { void receive_image_from_cache( media_reader::ImageBufPtr image_buffer, const media::AVFrameID mptr, - const utility::time_point tp); + const utility::time_point tp, + const timebase::flicks timeline_pts); void get_full_timeline_frame_list(caf::typed_response_promise rp); @@ -89,6 +96,13 @@ namespace playhead { int step_frames, const bool loop); + timebase::flicks get_next_or_previous_clip_start_position( + const timebase::flicks start_position, const bool next_clip); + + void store_media_frame_ranges(); + + void update_retiming(); + void set_in_and_out_frames(); typedef std::vector> BookmarkRanges; @@ -98,9 +112,10 @@ namespace playhead { const int logical_playhead_frame, BookmarkRanges &bookmark_ranges); - void full_bookmarks_update(); + void full_bookmarks_update(caf::typed_response_promise done); - void fetch_bookmark_annotations(BookmarkRanges bookmark_ranges); + void fetch_bookmark_annotations( + BookmarkRanges bookmark_ranges, caf::typed_response_promise done); void add_annotations_data_to_frame(media_reader::ImageBufPtr &frame); @@ -109,24 +124,45 @@ namespace playhead { void bookmark_changed(const utility::UuidActor bookmark); protected: + media::FrameTimeMap::iterator current_frame_iterator(); + media::FrameTimeMap::iterator current_frame_iterator(const timebase::flicks t); + + inline int logical_frame_from_pts(const timebase::flicks t) const { + // we use logical_frames_ as a lookup to get the corresponding logical + // frame at time t - this is much more efficient than looking up from + // an iterator from retimed_frames_ and using std::distance to get + // the position. + auto p = logical_frames_.lower_bound(t); + if (p == logical_frames_.end()) { + return logical_frames_.size() ? logical_frames_.rbegin()->second : 0; + } + return p->second; + } + + const std::string name_; + const utility::Uuid uuid_; + int logical_frame_ = {0}; timebase::flicks position_flicks_ = timebase::k_flicks_zero_seconds; - - bool playing_forwards_ = {true}; - float playback_velocity_ = {1.0f}; - int read_ahead_frames_ = {0}; - int precache_start_frame_ = {std::numeric_limits::lowest()}; + bool playing_forwards_ = {true}; + float playback_velocity_ = {1.0f}; + int read_ahead_frames_ = {0}; + int precache_start_frame_ = {std::numeric_limits::lowest()}; + int64_t frame_offset_ = {0}; + timebase::flicks forced_duration_ = timebase::k_flicks_zero_seconds; + utility::FrameRate default_rate_ = utility::FrameRate(timebase::k_flicks_24fps); + const bool source_is_timeline_; int pre_cache_read_ahead_frames_ = {32}; std::chrono::milliseconds static_cache_delay_milliseconds_ = { std::chrono::milliseconds(500)}; caf::behavior behavior_; - utility::Container base_; caf::actor pre_reader_; - caf::actor source_; + utility::UuidActor source_; caf::actor parent_; - caf::actor event_group_; caf::actor current_media_actor_; + caf::actor global_prefs_actor_; + caf::actor event_group_; utility::Uuid current_media_source_uuid_; utility::time_point last_image_timepoint_; @@ -135,19 +171,24 @@ namespace playhead { timebase::flicks loop_out_point_; utility::TimeSourceMode time_source_mode_; utility::FrameRate override_frame_rate_; - const media::MediaType media_type_; + media::MediaType media_type_; std::shared_ptr previous_frame_; utility::UuidSet all_media_uuids_; + std::map logical_frames_; media::FrameTimeMap full_timeline_frames_; + media::FrameTimeMap retimed_frames_; media::FrameTimeMap::iterator in_frame_, out_frame_, first_frame_, last_frame_; xstudio::bookmark::BookmarkAndAnnotations bookmarks_; BookmarkRanges bookmark_ranges_; + std::vector media_ranges_; typedef std::pair ImageAndLut; - bool content_changed_{false}; bool up_to_date_{false}; + bool full_precache_activated_{false}; + utility::time_point last_change_timepoint_; + std::vector> inflight_update_requests_; }; } // namespace playhead } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/playlist/playlist.hpp b/include/xstudio/playlist/playlist.hpp index 50b1bdc32..6b7c6c225 100644 --- a/include/xstudio/playlist/playlist.hpp +++ b/include/xstudio/playlist/playlist.hpp @@ -6,7 +6,7 @@ #include #include "xstudio/utility/container.hpp" -#include "xstudio/utility/edit_list.hpp" +#include "xstudio/utility/frame_rate.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" @@ -79,12 +79,16 @@ namespace playlist { [[nodiscard]] utility::FrameRate playhead_rate() const { return playhead_rate_; } void set_playhead_rate(const utility::FrameRate &rate) { playhead_rate_ = rate; } + [[nodiscard]] bool expanded() const { return expanded_; } + void set_expanded(const bool expanded) { expanded_ = expanded; } + private: utility::UuidListContainer media_list_; utility::PlaylistTree container_tree_; utility::FrameRate media_rate_; utility::FrameRate playhead_rate_; + bool expanded_ = {false}; }; } // namespace playlist } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/playlist/playlist_actor.hpp b/include/xstudio/playlist/playlist_actor.hpp index c9824761a..a62167b64 100644 --- a/include/xstudio/playlist/playlist_actor.hpp +++ b/include/xstudio/playlist/playlist_actor.hpp @@ -6,6 +6,7 @@ #include "xstudio/playlist/playlist.hpp" #include "xstudio/utility/uuid.hpp" +#include "xstudio/utility/notification_handler.hpp" namespace xstudio { @@ -26,7 +27,8 @@ namespace playlist { const std::string &name, const utility::Uuid &uuid = utility::Uuid(), const caf::actor &session = caf::actor()); - ~PlaylistActor() override = default; + + ~PlaylistActor(); void on_exit() override; const char *name() const override { return NAME.c_str(); } @@ -38,13 +40,26 @@ namespace playlist { void init(); - caf::behavior make_behavior() override { return behavior_; } + caf::message_handler message_handler(); + + caf::behavior make_behavior() override { + return message_handler() + .or_else(base_.container_message_handler(this)) + .or_else(notification_.message_handler(this, base_.event_group())) + .or_else(utility::NotificationHandler::default_event_handler()); + } void add_media( utility::UuidActor &ua, const utility::Uuid &uuid_before, + const bool delayed, caf::typed_response_promise rp); + void recursive_add_media_with_subsets( + caf::typed_response_promise> rp, + const caf::uri &path, + const utility::Uuid &uuid_before); + void create_container( caf::actor actor, caf::typed_response_promise rp, @@ -71,14 +86,27 @@ namespace playlist { void open_media_readers(); void open_media_reader(caf::actor media_actor); void send_content_changed_event(const bool queue = true); - void sort_alphabetically(); + void sort_by_media_display_info(const int sort_column_index, const bool ascending); + + void duplicate( + caf::typed_response_promise rp, + caf::actor src_bookmarks, + caf::actor dst_bookmarks); + + void duplicate_containers( + caf::typed_response_promise rp, + const utility::UuidActor &new_playlist, + const utility::UuidUuidMap &media_map); private: - caf::behavior behavior_; Playlist base_; + utility::NotificationHandler notification_; + utility::JsonStore playhead_serialisation_; + caf::actor_addr session_; utility::UuidActor playhead_; - caf::actor event_group_, change_event_group_; + caf::actor change_event_group_; + caf::actor global_prefs_actor_; std::map media_; std::map container_; bool is_in_viewer_ = {false}; @@ -87,6 +115,8 @@ namespace playlist { caf::actor playlist_broadcast_; caf::actor selection_actor_; bool auto_gather_sources_{false}; + + utility::UuidActorVector delayed_add_media_; }; } // namespace playlist } // namespace xstudio diff --git a/include/xstudio/plugin_manager/enums.hpp b/include/xstudio/plugin_manager/enums.hpp index 8a14c8335..20813dd00 100644 --- a/include/xstudio/plugin_manager/enums.hpp +++ b/include/xstudio/plugin_manager/enums.hpp @@ -12,13 +12,26 @@ namespace plugin_manager { PF_COLOUR_OPERATION = 1 << 5, PF_DATA_SOURCE = 1 << 6, PF_VIEWPORT_OVERLAY = 1 << 7, - PF_HEAD_UP_DISPLAY = 1 << 8, - PF_UTILITY = 1 << 9, - PF_CONFORM = 1 << 10, - PF_VIDEO_OUTPUT = 1 << 11, + PF_VIEWPORT_RENDERER = 1 << 8, + PF_HEAD_UP_DISPLAY = 1 << 9, + PF_UTILITY = 1 << 10, + PF_CONFORM = 1 << 11, + PF_VIDEO_OUTPUT = 1 << 12, } PluginFlags; typedef unsigned int PluginType; } // namespace plugin_manager + +namespace plugin { + typedef enum { + BottomLeft, + BottomCenter, + BottomRight, + TopLeft, + TopCenter, + TopRight, + FullScreen + } HUDElementPosition; +} // namespace plugin } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/plugin_manager/hud_plugin.hpp b/include/xstudio/plugin_manager/hud_plugin.hpp new file mode 100644 index 000000000..14bad1be7 --- /dev/null +++ b/include/xstudio/plugin_manager/hud_plugin.hpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + + +// #include "xstudio/colour_pipeline/colour_pipeline.hpp" +#include "xstudio/plugin_manager/plugin_base.hpp" + +#define NO_FRAME INT_MIN + + +namespace xstudio { +namespace plugin { + + /** + * @brief HUDPluginBase class. + * + * @details + * Subclass to create custom HUDs in xStudio - which can be activated and configured + * from the 'HUD' toolbar button. The HUD graphics/text overlay can be implemented + * either as QML or an OpenGL renderer. Refer to the PixelProbe plugin example for + * a reference implementation. + */ + + class HUDPluginBase : public plugin::StandardPlugin { + public: + HUDPluginBase( + caf::actor_config &cfg, + std::string name, + const utility::JsonStore &init_settings, + const float toolbar_position = -1.0f); + ~HUDPluginBase() override = default; + + /** + * @brief Declare the name of the HUD element + * + * @details This string value is used to populate the list of hud elements shown on + * the HUD pop-up menu in the UI. Can be overridden + */ + virtual const std::string &hud_name() const { return Module::name(); } + + protected: + void attribute_changed(const utility::Uuid &attribute_uuid, const int role) override; + + /** + * @brief Make a given attribute visible in the settings panel for the HUD plugin + * + * @details Calling this with an attribte will ensure that the attribute will + * appear in the settings pop-up dialog for the plugin + */ + void add_hud_settings_attribute(module::Attribute *attr); + + /** + * @brief Provide a QML string to instance a dialog that is launched when the + * user clicks on the settings button (the cog) for this HUD plugin + * + * @details If this is not called, a default settings dialog is created based + * on attributes that have been passed to 'add_hud_settings_attribute'. This + * method is provided to allow for complete flexibility on having a custom + * interface for configuring the HUD plugin as desired. + */ + void set_custom_settings_qml(const std::string &code); + + /** + * @brief Declare qml code that adds to the HUD. Plugins are responsible for + * providing attribute data and linking up with the qml code etc. See pixel_probe + * plugin for a reference example. + */ + void hud_element_qml( + const std::string qml_code, const HUDElementPosition position = FullScreen); + + caf::message_handler message_handler_extensions() override { return message_handler_; } + + /** + * @brief Determines if HUD should be drawn to viewport at given moment + */ + bool visible() const { return globally_enabled_ && hud_data_->value(); } + + caf::message_handler message_handler_; + + module::BooleanAttribute *hud_data_; + module::StringChoiceAttribute *hud_item_position_ = {nullptr}; + bool globally_enabled_ = {false}; + std::string plugin_underscore_name_; + + static inline std::map position_names_ = { + {BottomLeft, "HUD Bottom Left"}, + {BottomCenter, "HUD Bottom Centre"}, + {BottomRight, "HUD Bottom Right"}, + {TopLeft, "HUD Top Left"}, + {TopCenter, "HUD Top Centre"}, + {TopRight, "HUD Top Right"}}; + }; +} // namespace plugin +} // namespace xstudio diff --git a/include/xstudio/plugin_manager/plugin_base.hpp b/include/xstudio/plugin_manager/plugin_base.hpp index 3189e2ff2..53d39fd43 100644 --- a/include/xstudio/plugin_manager/plugin_base.hpp +++ b/include/xstudio/plugin_manager/plugin_base.hpp @@ -50,12 +50,20 @@ namespace plugin { the overlay can render ontop of the image, after it is drawn. If 'have_alpha_buffer' is false, the BeforeImage pass is not executed. */ - virtual void render_opengl( + virtual void render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, const xstudio::media_reader::ImageBufPtr &frame, - const bool have_alpha_buffer) {}; + const bool have_alpha_buffer){}; + + /* An overlay can render visuals to the viewport without an associated + image via this method. */ + virtual void render_viewport_overlay( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_normalised_coords, + const float viewport_du_dpixel, + const bool have_alpha_buffer){}; [[nodiscard]] virtual RenderPass preferred_render_pass() const { return AfterImage; } }; @@ -76,41 +84,45 @@ namespace plugin { message_handler_.or_else(module::Module::message_handler())); } + const char *name() const override { + return dynamic_cast(this)->name().c_str(); + } + + protected: + void on_exit() override; + virtual caf::message_handler message_handler_extensions() { return caf::message_handler(); } caf::message_handler message_handler_; - virtual utility::BlindDataObjectPtr prepare_overlay_data( - const media_reader::ImageBufPtr & /*image*/, const bool /*offscreen*/ - ) const { - return utility::BlindDataObjectPtr(); - } - // TODO: deprecate prepare_render_data and use this everywhere virtual utility::BlindDataObjectPtr onscreen_render_data( - const media_reader::ImageBufPtr & /*image*/, const std::string & /*viewport_name*/ - ) const { + const media_reader::ImageBufPtr & /*image*/, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid, + const bool is_hero_image) const { return utility::BlindDataObjectPtr(); } // reimpliment this function to receive the image buffer(s) that are // currently being displayed on the given viewport virtual void images_going_on_screen( - const std::vector & /*images*/, + const media_reader::ImageBufDisplaySetPtr & /*image_set*/, const std::string /*viewport_name*/, const bool /*playhead_playing*/ ) {} - virtual ViewportOverlayRendererPtr make_overlay_renderer(const int /*viewer_index*/) { + virtual ViewportOverlayRendererPtr + make_overlay_renderer(const std::string &viewport_name) { return ViewportOverlayRendererPtr(); } // Override this and return your own subclass of GPUPreDrawHook to allow // arbitrary GPU rendering (e.g. when in the viewport OpenGL context) - virtual GPUPreDrawHookPtr make_pre_draw_gpu_hook(const int /*viewer_index*/) { + virtual GPUPreDrawHookPtr make_pre_draw_gpu_hook(const std::string &viewport_name) { return GPUPreDrawHookPtr(); } @@ -121,32 +133,43 @@ namespace plugin { return bookmark::AnnotationBasePtr(); } - /* Function signature for on screen frame change callback - reimplement to - receive this event */ - virtual void on_screen_frame_changed( - const timebase::flicks, // playhead position - const int, // playhead logical frame - const int, // media frame - const int, // media logical frame - const utility::Timecode & // media frame timecode - ) {} + // reimplement this function to get a list of frame IDs of media that + // is going to be going on screen in the near future during playtback. + // This gives the opportunity to do asynchronous fetching of data from + // media or media sources before it's needed at draw time. + virtual void media_due_on_screen_soon(const media::AVFrameIDsAndTimePoints &) {} /* Function signature for on screen annotation change - reimplement to - receive this event */ + receive this event. Call join_playhead_events() to activate. */ virtual void on_screen_media_changed( caf::actor, // media item actor const utility::MediaReference &, // media reference - const std::string) {} + const utility::JsonStore & // colour params + ) {} /* Function signature for current playhead playing status change - reimplement to receive this event */ virtual void on_playhead_playing_changed(const bool // is playing ) {} + /* Reimplement to receive this notification telling us when the playhead driving a given + named viewport has changed */ + virtual void + viewport_playhead_changed(const std::string &viewport_name, caf::actor playhead) {} + + /* Use this function to define the qml code that draws information over the xstudio viewport. See basic_viewport_masking and pixel_probe plugin examples. */ void qml_viewport_overlay_code(const std::string &code); + /* Use this function to create a new bookmark on the given frame (as + per frame_details). See annotations_tool.cpp for example useage. */ + utility::Uuid create_bookmark_on_frame( + const media::AVFrameID &frame_details, + const std::string &bookmark_subject, + const bookmark::BookmarkDetail &detail, + const bool bookmark_entire_duration = false); + /* Use this function to create a new bookmark on the current (on screen) frame of for the entire duration for the media currently showing on the given named viewport. */ @@ -156,17 +179,48 @@ namespace plugin { const bookmark::BookmarkDetail &detail, const bool bookmark_entire_duratio = false); + /* Call this function to turn off any other tools that have direct, interactive + drawing in the viewport. This will allow your drawing plugin to exclusively + do interactive drawing. */ + void cancel_other_drawing_tools(); + + /* Override this function and take necessary action to disable any interactive + (e.g.) drawing state of your plugin. */ + virtual void turn_off_overlay_interaction() {} + + utility::UuidList get_bookmarks_on_current_media(const std::string &viewport_name); + bookmark::BookmarkDetail get_bookmark_detail(const utility::Uuid &bookmark_id); + bookmark::AnnotationBasePtr get_bookmark_annotation(const utility::Uuid &bookmark_id); /* Call this function to update the annotation data attached to the given bookmark */ void update_bookmark_annotation( const utility::Uuid bookmark_id, - std::shared_ptr annotation_data, + bookmark::AnnotationBasePtr annotation_data, const bool annotation_is_empty); void update_bookmark_detail( const utility::Uuid bookmark_id, const bookmark::BookmarkDetail &bmd); + void remove_bookmark(const utility::Uuid &bookmark_id); + + /* set playback state for playhead attached to the named viewport */ + void start_stop_playback(const std::string viewport_name, bool play); + + /* set the cursor (mouse pointer) shape for all viewports. An empty + string will return to defaul (arrow) pointer. See XsViewport.qml + for possible cursor names - the are simply stringified versions of the + Qt cursorshape enumerator. For example "Qt.WaitCursor" would be a + valid cursor name. If you have an image resource declared in a qrc + file this can also be used for fully custom cursor. To see an example + string-search for 'magnifier_cursor' in the xstudio code base.*/ + void set_viewport_cursor(const std::string cusor_name); + + /* Call this function to start listening to events related to the + current global (active) playhead. This must be called if you want to + make use of the on_screen_frame_changed, on_screen_media_changed and + on_playhead_playing_changed callbacks. */ + void listen_to_playhead_events(const bool listen = true); private: // re-implement to receive callback when the on-screen media changes. To @@ -174,17 +228,23 @@ namespace plugin { void session_changed(caf::actor session); - void current_viewed_playhead_changed(caf::actor_addr playhead_addr); + void current_viewed_playhead_changed(caf::actor playhead); void join_studio_events(); - int playhead_logical_frame_ = {-1}; + void __images_going_on_screen( + const media_reader::ImageBufDisplaySetPtr &image_set, + const std::string viewport_name, + const bool playhead_playing); caf::actor_addr active_viewport_playhead_; caf::actor_addr playhead_media_events_group_; caf::actor bookmark_manager_; + caf::actor playhead_events_actor_; + bool joined_playhead_events_ = {false}; + std::map last_source_uuid_; - module::QmlCodeAttribute *viewport_overlay_qml_code_ = nullptr; + module::BooleanAttribute *viewport_overlay_qml_code_ = nullptr; }; diff --git a/include/xstudio/plugin_manager/plugin_factory.hpp b/include/xstudio/plugin_manager/plugin_factory.hpp index 0e479a93f..b92658b66 100644 --- a/include/xstudio/plugin_manager/plugin_factory.hpp +++ b/include/xstudio/plugin_manager/plugin_factory.hpp @@ -33,6 +33,9 @@ namespace plugin_manager { spawn(caf::blocking_actor &, const utility::JsonStore & = utility::JsonStore()) { return caf::actor(); } + + virtual void *instance_q_object(void *parent_q_object) { return nullptr; } + [[nodiscard]] virtual std::string spawn_widget_ui() { return ""; } [[nodiscard]] virtual std::string spawn_menu_ui() { return ""; } }; @@ -90,6 +93,51 @@ namespace plugin_manager { }; + template class PluginFactoryTemplate2 : public PluginFactory { + public: + PluginFactoryTemplate2( + utility::Uuid uuid, + std::string name = "", + PluginType type = PluginFlags::PF_CUSTOM, + bool resident = false, + std::string author = "", + std::string description = "", + semver::version version = semver::version("0.0.0")) + : uuid_(std::move(uuid)), + name_(std::move(name)), + type_(type), + resident_(resident), + author_(std::move(author)), + description_(std::move(description)), + version_(std::move(version)) {} + ~PluginFactoryTemplate2() override = default; + + [[nodiscard]] std::string name() const override { return name_; } + [[nodiscard]] utility::Uuid uuid() const override { return uuid_; } + [[nodiscard]] PluginType type() const override { return type_; } + [[nodiscard]] bool resident() const override { return resident_; } + [[nodiscard]] std::string author() const override { return author_; } + [[nodiscard]] std::string description() const override { return description_; } + [[nodiscard]] semver::version version() const override { return version_; } + [[nodiscard]] std::string spawn_widget_ui() override { return ui_widget_string_; } + [[nodiscard]] std::string spawn_menu_ui() override { return ui_menu_string_; } + + void *instance_q_object(void *parent_q_object) override { + return T::instance_q_object(parent_q_object); + } + + utility::Uuid uuid_; + std::string name_; + PluginType type_; + bool resident_; + std::string author_; + std::string description_; + semver::version version_; + std::string ui_widget_string_; + std::string ui_menu_string_; + }; + + typedef PluginFactory *(*plugin_factory_ptr)(); class PluginFactoryCollection { diff --git a/include/xstudio/plugin_manager/plugin_utility.hpp b/include/xstudio/plugin_manager/plugin_utility.hpp index 494f65f89..7bc2b0427 100644 --- a/include/xstudio/plugin_manager/plugin_utility.hpp +++ b/include/xstudio/plugin_manager/plugin_utility.hpp @@ -84,7 +84,8 @@ namespace plugin_manager { const utility::JsonStore & /*change*/, const std::string & /*path*/, const utility::JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full) + .delegate(actor_cast(this)); }, [=](json_store::update_atom, const utility::JsonStore &js) { diff --git a/include/xstudio/session/session.hpp b/include/xstudio/session/session.hpp index b9463a387..ac87158b5 100644 --- a/include/xstudio/session/session.hpp +++ b/include/xstudio/session/session.hpp @@ -8,10 +8,10 @@ #include "xstudio/utility/container.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" #include "xstudio/utility/lock_file.hpp" +#include "xstudio/utility/frame_rate.hpp" namespace xstudio { namespace session { @@ -66,10 +66,34 @@ namespace session { } [[nodiscard]] utility::FrameRate media_rate() const { return media_rate_; } [[nodiscard]] utility::FrameRate playhead_rate() const { return playhead_rate_; } + [[nodiscard]] bool push_to_current_playlist() const { return push_to_current_; } + [[nodiscard]] const std::string &push_playlist_name() const { + return push_playlist_name_; + } + + [[nodiscard]] utility::Uuid current_playlist_uuid() const { + return current_playlist_uuid_; + } + void set_current_playlist_uuid(const utility::Uuid &uuid) { + current_playlist_uuid_ = uuid; + } + + [[nodiscard]] utility::Uuid viewed_playlist_uuid() const { + return viewed_playlist_uuid_; + } + void set_viewed_playlist_uuid(const utility::Uuid &uuid) { + viewed_playlist_uuid_ = uuid; + } void set_media_rate(const utility::FrameRate &rate) { media_rate_ = rate; } void set_playhead_rate(const utility::FrameRate &rate) { playhead_rate_ = rate; } void set_filepath(const caf::uri &path); + void set_push_to_current_playlist(const bool push_to_current) { + push_to_current_ = push_to_current; + } + void set_push_playlist_name(const std::string &push_playlist_name) { + push_playlist_name_ = push_playlist_name; + } [[nodiscard]] caf::uri filepath() const { return filepath_; } [[nodiscard]] fs::file_time_type session_file_mtime() const { @@ -82,8 +106,12 @@ namespace session { utility::PlaylistTree playlists_; utility::FrameRate media_rate_; utility::FrameRate playhead_rate_; + utility::Uuid current_playlist_uuid_; + utility::Uuid viewed_playlist_uuid_; caf::uri filepath_; fs::file_time_type session_file_mtime_{fs::file_time_type::min()}; + bool push_to_current_ = {true}; + std::string push_playlist_name_ = {"Pushed Media"}; }; } // namespace session } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/session/session_actor.hpp b/include/xstudio/session/session_actor.hpp index 034865e79..f9bca374f 100644 --- a/include/xstudio/session/session_actor.hpp +++ b/include/xstudio/session/session_actor.hpp @@ -8,6 +8,7 @@ #include "xstudio/session/session.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" +#include "xstudio/utility/notification_handler.hpp" namespace xstudio { namespace session { @@ -28,9 +29,10 @@ namespace session { inline static const std::string NAME = "SessionActor"; void init(); - caf::message_handler message_handler(); + caf::message_handler message_handler(); caf::behavior make_behavior() override { return behavior_; } + void create_container( caf::actor actor, caf::typed_response_promise> rp, @@ -78,17 +80,11 @@ namespace session { const utility::Uuid &uuid_before = utility::Uuid(), const bool into = false); - void save_json_to( - caf::typed_response_promise &rp, - const utility::JsonStore &js, - const caf::uri &path, - const bool update_path = true, - const size_t hash = 0); - void associate_bookmarks(caf::typed_response_promise &rp); void sync_to_json_store(caf::typed_response_promise &rp); + [[nodiscard]] std::string get_next_name(const std::string &name_template) const; void gather_media_sources( caf::typed_response_promise &rp, @@ -121,16 +117,28 @@ namespace session { void check_media_hook_plugin_version(const utility::JsonStore &jsn, const caf::uri &path); + utility::NotificationHandler notification_; + caf::behavior behavior_; - caf::actor event_group_; Session base_; caf::actor json_store_; caf::actor bookmarks_; - caf::actor tags_; + caf::actor ioactor_; std::map playlists_; - caf::actor_addr current_playlist_; std::map serialise_targets_; // std::map players_; + + // store gui conext information + utility::UuidActor viewedContainer_; + utility::UuidActor inspectedContainer_; + + std::vector selectedMedia_; + + utility::UuidActorVector selection_; + + caf::disposable viewed_monitor_; + caf::disposable inspected_monitor_; + std::map serialise_monitor_; }; } // namespace session } // namespace xstudio diff --git a/include/xstudio/shotgun_client/caf_shotgun_client_error.hpp b/include/xstudio/shotgun_client/caf_shotgun_client_error.hpp index 7893388b6..0ca32fe10 100644 --- a/include/xstudio/shotgun_client/caf_shotgun_client_error.hpp +++ b/include/xstudio/shotgun_client/caf_shotgun_client_error.hpp @@ -27,7 +27,7 @@ namespace shotgun_client { } } - inline bool from_string(caf::string_view in, shotgun_client_error &out) { + inline bool from_string(std::string_view in, shotgun_client_error &out) { if (in == "connection_error") { out = shotgun_client_error::connection_error; return true; diff --git a/include/xstudio/shotgun_client/shotgun_client.hpp b/include/xstudio/shotgun_client/shotgun_client.hpp index 0da8b3007..2b97e8965 100644 --- a/include/xstudio/shotgun_client/shotgun_client.hpp +++ b/include/xstudio/shotgun_client/shotgun_client.hpp @@ -1265,6 +1265,7 @@ namespace shotgun_client { using StdList::const_iterator; using StdList::crbegin; using StdList::crend; + using StdList::emplace_back; using StdList::empty; using StdList::end; using StdList::erase; diff --git a/include/xstudio/shotgun_client/shotgun_client_actor.hpp b/include/xstudio/shotgun_client/shotgun_client_actor.hpp index 546451284..dfbb495a9 100644 --- a/include/xstudio/shotgun_client/shotgun_client_actor.hpp +++ b/include/xstudio/shotgun_client/shotgun_client_actor.hpp @@ -6,6 +6,7 @@ #include "xstudio/shotgun_client/shotgun_client.hpp" #include "xstudio/utility/uuid.hpp" +#include "xstudio/thumbnail/thumbnail.hpp" namespace xstudio { namespace shotgun_client { @@ -21,8 +22,177 @@ namespace shotgun_client { void init(); caf::behavior make_behavior() override { return behavior_; } + template void authenticate(T rp, std::function lambda) { + if (not base_.authenticated()) { + // spdlog::error("NOT AUTH REQUEST IT {}", + // to_string(caf::actor_cast(this))); + + mail(shotgun_acquire_token_atom_v) + .request(actor_cast(this), infinite) + .then( + [=](const std::pair &) mutable { lambda(); }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + } else + lambda(); + } + + template + bool check_failed_authenticate( + const utility::JsonStore &jsn, T rp, std::function lambda) { + const static auto path = nlohmann::json::json_pointer("/errors/0/status"); + auto result = false; + try { + if (jsn.contains(path) and not jsn.at(path).is_null() and + jsn.at(path).get() == 401) { + result = true; + // spdlog::error("FAILED REQUEST NOT AUTH"); + + // try and authorise.. + mail(shotgun_acquire_token_atom_v) + .request(actor_cast(this), infinite) + .then( + [=](const std::pair &) mutable { + lambda(); + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(utility::JsonStore(std::move(jsn))); + }); + } + } catch (const std::exception &err) { + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), jsn.dump(2)); + } + + return result; + } + + void acquire_token_primary( + caf::typed_response_promise> rp); void acquire_token(caf::typed_response_promise> rp); + void request_image( + const std::string &entity, + const int record_id, + const bool thumbnail, + caf::typed_response_promise rp); + + void request_attachment( + const std::string &entity, + const int record_id, + const std::string &property, + caf::typed_response_promise rp); + + void request_text_search( + const std::string &text, + const utility::JsonStore &conditions, + const int page, + const int page_size, + caf::typed_response_promise rp); + + void request_schema_entity_fields( + const std::string &entity, + const std::string &field, + const int id, + caf::typed_response_promise rp); + + void request_entity_filter( + const std::string &entity, + const utility::JsonStore &filter, + const std::vector &fields, + const std::vector &sort, + const int page, + const int page_size, + caf::typed_response_promise rp); + + void request_entity( + const std::string &entity, + const int record_id, + const std::vector &fields, + caf::typed_response_promise rp); + + void request_entity_search( + const std::string &entity, + const utility::JsonStore &conditions, + const std::vector &fields, + const std::vector &sort, + const int page, + const int page_size, + caf::typed_response_promise rp); + + void request_schema_entity( + const std::string &entity, + const int record_id, + caf::typed_response_promise rp); + + void + request_schema(const int record_id, caf::typed_response_promise rp); + + void request_link( + const std::string &link, caf::typed_response_promise rp); + + void update_entity( + const std::string &entity, + const int record_id, + const utility::JsonStore &body, + const std::vector &fields, + caf::typed_response_promise rp); + + void delete_entity( + const std::string &entity, + const int record_id, + caf::typed_response_promise rp); + + void create_entity( + const std::string &entity, + const utility::JsonStore &body, + caf::typed_response_promise rp); + + void request_info(caf::typed_response_promise rp); + + void request_preference(caf::typed_response_promise rp); + + void upload( + const std::string &entity, + const int record_id, + const std::string &field, + const std::string &name, + const std::vector &data, + const std::string &content_type, + caf::typed_response_promise rp); + + void upload_start( + const std::string &entity, + const int record_id, + const std::string &field, + const std::string &name, + caf::typed_response_promise rp); + + void upload_transfer( + const std::string &url, + const std::string &content_type, + const std::vector &data, + caf::typed_response_promise rp); + + void upload_finish( + const std::string &path, + const utility::JsonStore &info, + caf::typed_response_promise rp); + + + void refresh_token( + const bool force, + caf::typed_response_promise> rp); + + void request_image_buffer( + const std::string &entity, + const int record_id, + const bool thumbnail, + const bool as_buffer, + caf::typed_response_promise rp); + private: std::queue>> request_refresh_queue_; diff --git a/include/xstudio/studio/studio_actor.hpp b/include/xstudio/studio/studio_actor.hpp index 8d693485c..a96d97d36 100644 --- a/include/xstudio/studio/studio_actor.hpp +++ b/include/xstudio/studio/studio_actor.hpp @@ -13,7 +13,11 @@ namespace studio { StudioActor(caf::actor_config &cfg, const std::string &name); ~StudioActor() override = default; - caf::behavior make_behavior() override { return behavior_; } + caf::message_handler message_handler(); + + caf::behavior make_behavior() override { + return message_handler().or_else(base_.container_message_handler(this)); + } void on_exit() override; const char *name() const override { return NAME.c_str(); } @@ -21,13 +25,14 @@ namespace studio { inline static const std::string NAME = "StudioActor"; void init(); - caf::behavior behavior_; Studio base_; caf::actor session_; struct QuickviewRequest { utility::UuidActorVector media_actors; std::string compare_mode; + utility::JsonStore in_point; + utility::JsonStore out_point; }; std::vector quickview_requests_; }; diff --git a/include/xstudio/subset/subset.hpp b/include/xstudio/subset/subset.hpp index 44978e8b3..3632f47c8 100644 --- a/include/xstudio/subset/subset.hpp +++ b/include/xstudio/subset/subset.hpp @@ -6,7 +6,7 @@ #include #include "xstudio/utility/container.hpp" -#include "xstudio/utility/edit_list.hpp" +#include "xstudio/utility/frame_rate.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" @@ -14,7 +14,10 @@ namespace xstudio { namespace subset { class Subset : public utility::Container { public: - Subset(const std::string &name = "Subset", const std::string &type = "Subset"); + Subset( + const std::string &name = "Subset", + const std::string &type = "Subset", + const utility::Uuid &uuid = utility::Uuid::generate()); Subset(const utility::JsonStore &jsn); ~Subset() override = default; diff --git a/include/xstudio/subset/subset_actor.hpp b/include/xstudio/subset/subset_actor.hpp index dd1ab8f42..1bef72e52 100644 --- a/include/xstudio/subset/subset_actor.hpp +++ b/include/xstudio/subset/subset_actor.hpp @@ -6,27 +6,40 @@ #include "xstudio/subset/subset.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" +#include "xstudio/json_store/json_store_handler.hpp" namespace xstudio { namespace subset { class SubsetActor : public caf::event_based_actor { public: SubsetActor(caf::actor_config &cfg, caf::actor playlist, const utility::JsonStore &jsn); - SubsetActor(caf::actor_config &cfg, caf::actor playlist, const std::string &name); + SubsetActor( + caf::actor_config &cfg, + caf::actor playlist, + const std::string &name, + const utility::Uuid &uuid = utility::Uuid::generate(), + const std::string &override_type = "Subset"); ~SubsetActor() override = default; const char *name() const override { return NAME.c_str(); } + caf::behavior make_behavior() override { + return message_handler() + .or_else(base_.container_message_handler(this)) + .or_else(jsn_handler_.message_handler()); + } + private: inline static const std::string NAME = "SubsetActor"; void init(); - caf::behavior make_behavior() override { return behavior_; } + caf::message_handler message_handler(); + void deliver_media_pointer( const int logical_frame, caf::typed_response_promise rp); - void sort_alphabetically(); + void sort_by_media_display_info(const int sort_column_index, const bool ascending); void add_media( const utility::UuidActor &ua, @@ -38,13 +51,23 @@ namespace subset { const utility::Uuid &before_uuid = utility::Uuid()); bool remove_media(caf::actor actor, const utility::Uuid &uuid); - private: + protected: + utility::JsonStore serialise() const { return base_.serialise(); } + void monitor_media(const caf::actor &actor); + + std::map monitor_; + + utility::JsonStore playhead_serialisation_; + caf::behavior behavior_; caf::actor_addr playlist_; - caf::actor event_group_, change_event_group_; + caf::actor change_event_group_; + caf::actor selection_actor_; Subset base_; utility::UuidActorMap actors_; utility::UuidActor playhead_; + + json_store::JsonStoreHandler jsn_handler_; }; } // namespace subset } // namespace xstudio diff --git a/include/xstudio/sync/sync_actor.hpp b/include/xstudio/sync/sync_actor.hpp deleted file mode 100644 index be2cd86e0..000000000 --- a/include/xstudio/sync/sync_actor.hpp +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include - -#include -#include -#include - -#include "xstudio/utility/uuid.hpp" - -namespace xstudio { -namespace sync { - class SyncActor : public caf::event_based_actor { - public: - SyncActor(caf::actor_config &cfg); - const char *name() const override { return NAME.c_str(); } - void on_exit() override; - - caf::behavior make_behavior() override { return behavior_; } - - private: - void init(); - inline static const std::string NAME = "SyncActor"; - caf::behavior behavior_; - }; - - class SyncGatewayActor : public caf::event_based_actor { - public: - SyncGatewayActor(caf::actor_config &cfg); - const char *name() const override { return NAME.c_str(); } - void on_exit() override; - - caf::behavior make_behavior() override { return behavior_; } - - private: - void init(); - inline static const std::string NAME = "SyncGatewayActor"; - caf::behavior behavior_; - }; - - class SyncGatewayManagerActor : public caf::event_based_actor { - public: - SyncGatewayManagerActor(caf::actor_config &cfg); - const char *name() const override { return NAME.c_str(); } - void on_exit() override; - - caf::behavior make_behavior() override { return behavior_; } - - private: - void init(); - inline static const std::string NAME = "SyncGatewayManagerActor"; - caf::behavior behavior_; - std::map lock_key_; - std::map clients_; - }; -} // namespace sync -} // namespace xstudio diff --git a/include/xstudio/tag/tag.hpp b/include/xstudio/tag/tag.hpp deleted file mode 100644 index b479dede6..000000000 --- a/include/xstudio/tag/tag.hpp +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include -#include - -#include -#include -#include - -#include "xstudio/utility/container.hpp" -#include "xstudio/utility/uuid.hpp" - -namespace xstudio { -namespace tag { - - class Tag { - public: - Tag() : type_(to_string(id_)) {} - Tag(const utility::JsonStore &); - virtual ~Tag() = default; - - [[nodiscard]] utility::JsonStore serialise() const; - - [[nodiscard]] auto id() const { return id_; } - [[nodiscard]] auto persistent() const { return persistent_; } - [[nodiscard]] auto link() const { return link_; } - [[nodiscard]] auto data() const { return data_; } - [[nodiscard]] auto type() const { return type_; } - [[nodiscard]] auto unique() const { return unique_; } - - void set_id(const utility::Uuid &value) { id_ = value; } - void set_link(const utility::Uuid &value) { link_ = value; } - void set_type(const std::string &value) { type_ = value; } - void set_persistent(const bool value) { persistent_ = value; } - void set_data(const std::string &value) { data_ = value; } - void set_unique(const size_t value) { unique_ = value; } - void set_unique(const std::string &value) { unique_ = std::hash{}(value); } - - template friend bool inspect(Inspector &f, Tag &x) { - return f.object(x).fields( - f.field("id", x.id_), - f.field("link", x.link_), - f.field("persist", x.persistent_), - f.field("type", x.type_), - f.field("uniq", x.unique_), - f.field("data", x.data_)); - } - - private: - utility::Uuid id_{utility::Uuid::generate()}; - utility::Uuid link_; - std::string type_; - bool persistent_{false}; - std::string data_; - size_t unique_{0}; - }; - - class TagBase : public utility::Container { - public: - TagBase(const std::string &name = "TagBase"); - TagBase(const utility::JsonStore &jsn); - ~TagBase() override = default; - - [[nodiscard]] utility::JsonStore serialise() const override; - [[nodiscard]] std::optional get_tag(const utility::Uuid &id) const; - [[nodiscard]] std::vector get_tags(const utility::Uuid &link) const; - [[nodiscard]] std::vector get_tags() const; - - std::optional add_tag(const Tag tag); - bool remove_tag(const utility::Uuid &id); - - private: - std::map link_map_; - std::map tag_map_; - }; - -} // namespace tag -} // namespace xstudio diff --git a/include/xstudio/tag/tag_actor.hpp b/include/xstudio/tag/tag_actor.hpp deleted file mode 100644 index c188acbe6..000000000 --- a/include/xstudio/tag/tag_actor.hpp +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include - -#include "xstudio/tag/tag.hpp" -#include "xstudio/utility/uuid.hpp" - - -namespace xstudio { -namespace tag { - - class TagActor : public caf::event_based_actor { - public: - TagActor(caf::actor_config &cfg, const utility::JsonStore &jsn); - TagActor(caf::actor_config &cfg, const utility::Uuid &uuid = utility::Uuid::generate()); - - ~TagActor() override = default; - - const char *name() const override { return NAME.c_str(); } - - static caf::message_handler default_event_handler(); - - private: - inline static const std::string NAME = "TagActor"; - void init(); - caf::behavior make_behavior() override { return behavior_; } - - private: - caf::behavior behavior_; - TagBase base_; - caf::actor event_group_; - }; - -} // namespace tag -} // namespace xstudio diff --git a/include/xstudio/thumbnail/thumbnail.hpp b/include/xstudio/thumbnail/thumbnail.hpp index 3b9b8cbe7..6bbabd0ea 100644 --- a/include/xstudio/thumbnail/thumbnail.hpp +++ b/include/xstudio/thumbnail/thumbnail.hpp @@ -29,7 +29,7 @@ namespace thumbnail { : std::string(fmt::format("{}/{}", o, size)) {} ThumbnailKey( const media::AVFrameID &mptr, const size_t hash = 0, const size_t size = 256) - : std::string(fmt::format("{}/{}/{}", mptr.key_, std::to_string(hash), size)) {} + : std::string(fmt::format("{}/{}/{}", mptr.key(), std::to_string(hash), size)) {} using std::string::empty; using std::string::substr; @@ -57,7 +57,7 @@ namespace thumbnail { } [[nodiscard]] std::string hash_str() const { return fmt::format( - fmt_format(), + fmt::runtime(fmt_format()), std::hash{}(static_cast(*this))); } [[nodiscard]] size_t hash() const { @@ -71,7 +71,8 @@ namespace thumbnail { }; inline std::string to_hash_string(const size_t hash) { - return fmt::format("{:0" + std::to_string(sizeof(size_t) * 2) + "x}", hash); + return fmt::format( + fmt::runtime("{:0" + std::to_string(sizeof(size_t) * 2) + "x}"), hash); } inline std::string to_string(const ThumbnailKey &v) { return static_cast(v); diff --git a/include/xstudio/thumbnail/thumbnail_disk_cache_actor.hpp b/include/xstudio/thumbnail/thumbnail_disk_cache_actor.hpp index 550f3e1fd..cb9d888e3 100644 --- a/include/xstudio/thumbnail/thumbnail_disk_cache_actor.hpp +++ b/include/xstudio/thumbnail/thumbnail_disk_cache_actor.hpp @@ -31,7 +31,8 @@ namespace thumbnail { // std::vector read_thumb(const std::string &path); - std::vector encode_thumb(const ThumbnailBufferPtr &buffer); + std::vector + encode_thumb(const ThumbnailBufferPtr &buffer, const int quality = 75); ThumbnailBufferPtr decode_thumb(const std::vector &buffer); inline static const std::string NAME = "TDCHelperActor"; diff --git a/include/xstudio/thumbnail/thumbnail_manager_actor.hpp b/include/xstudio/thumbnail/thumbnail_manager_actor.hpp index 7cec3367b..54d58709f 100644 --- a/include/xstudio/thumbnail/thumbnail_manager_actor.hpp +++ b/include/xstudio/thumbnail/thumbnail_manager_actor.hpp @@ -28,8 +28,7 @@ class ThumbnailManagerActor : public caf::event_based_actor { const media::AVFrameID &mptr, const size_t thumb_size, const size_t hash, - const bool cache_to_disk, - const utility::Uuid &job_uuid); + const bool cache_to_disk); void request_buffer( caf::typed_response_promise rp, @@ -38,20 +37,38 @@ class ThumbnailManagerActor : public caf::event_based_actor { void process_queue(); + void queue_thumbnail_request( + caf::typed_response_promise rp, + const media::AVFrameID &mptr, + const size_t thumb_size, + const size_t hash, + const bool cache_to_disk); + inline static const std::string NAME = "ThumbnailManagerActor"; caf::behavior behavior_; caf::actor mem_cache_; caf::actor dsk_cache_; - std::deque, - media::AVFrameID, - size_t, - size_t, - bool, - utility::Uuid>> - request_queue_; + struct ThumbnailRequest { + std::vector> response_promises; + media::AVFrameID mptr; + size_t size; + size_t hash; + bool cache_to_disk; + bool in_flight = false; + void deliver(const ThumbnailBufferPtr &buf) { + for (auto &rp : response_promises) + rp.deliver(buf); + } + void deliver(const caf::error &err) { + for (auto &rp : response_promises) + rp.deliver(err); + } + }; + + typedef std::shared_ptr ThumbnailRequestPtr; + std::vector request_queue_; }; } // namespace xstudio::thumbnail diff --git a/include/xstudio/timeline/clip.hpp b/include/xstudio/timeline/clip.hpp index 6dc47b7c3..88158c449 100644 --- a/include/xstudio/timeline/clip.hpp +++ b/include/xstudio/timeline/clip.hpp @@ -1,16 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include #include -#include "xstudio/media/media.hpp" #include "xstudio/timeline/item.hpp" #include "xstudio/utility/container.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/media_reference.hpp" #include "xstudio/utility/uuid.hpp" namespace xstudio { @@ -24,6 +20,7 @@ namespace timeline { const caf::actor &actor = caf::actor(), const utility::Uuid media_uuid = utility::Uuid()); Clip(const utility::JsonStore &jsn); + Clip(const Item &item, const caf::actor &actor); ~Clip() override = default; @@ -38,11 +35,11 @@ namespace timeline { } [[nodiscard]] const utility::Uuid &media_uuid() const { return media_uuid_; } - void set_media_uuid(const utility::Uuid &media_uuid) { + utility::JsonStore set_media_uuid(const utility::Uuid &media_uuid) { auto jsn = item_.prop(); jsn["media_uuid"] = media_uuid; - item_.set_prop(jsn); - media_uuid_ = media_uuid; + media_uuid_ = media_uuid; + return item_.set_prop(jsn); } private: diff --git a/include/xstudio/timeline/clip_actor.hpp b/include/xstudio/timeline/clip_actor.hpp index 77eb682ea..fe3eaba2b 100644 --- a/include/xstudio/timeline/clip_actor.hpp +++ b/include/xstudio/timeline/clip_actor.hpp @@ -4,11 +4,6 @@ #include #include "xstudio/timeline/clip.hpp" -#include "xstudio/timeline/stack.hpp" -#include "xstudio/timeline/track.hpp" -#include "xstudio/media/media.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/uuid.hpp" namespace xstudio { namespace timeline { @@ -21,6 +16,9 @@ namespace timeline { const utility::UuidActor &media, const std::string &name = "ClipActor", const utility::Uuid &uuid = utility::Uuid::generate()); + ClipActor(caf::actor_config &cfg, const Item &item); + ClipActor(caf::actor_config &cfg, const Item &item, Item &nitem); + ~ClipActor() override = default; const char *name() const override { return NAME.c_str(); } @@ -29,13 +27,21 @@ namespace timeline { inline static const std::string NAME = "ClipActor"; void init(); - caf::behavior make_behavior() override { return behavior_; } + caf::message_handler message_handler(); + + caf::behavior make_behavior() override { + return message_handler().or_else(base_.container_message_handler(this)); + } + + void link_media( + caf::typed_response_promise rp, + const utility::UuidActor &media, + const bool refresh = true); private: - caf::behavior behavior_; Clip base_; - caf::actor event_group_; caf::actor_addr media_; + caf::disposable monitor_; std::map> audio_ptr_cache_; std::map> image_ptr_cache_; diff --git a/include/xstudio/timeline/gap.hpp b/include/xstudio/timeline/gap.hpp index 9d3bfc10e..c34692d76 100644 --- a/include/xstudio/timeline/gap.hpp +++ b/include/xstudio/timeline/gap.hpp @@ -1,15 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include #include +#include "xstudio/timeline/item.hpp" #include "xstudio/utility/container.hpp" +#include "xstudio/utility/frame_rate_and_duration.hpp" #include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/frame_range.hpp" #include "xstudio/utility/uuid.hpp" -#include "xstudio/timeline/item.hpp" namespace xstudio { namespace timeline { @@ -20,6 +19,7 @@ namespace timeline { const utility::Uuid &uuid = utility::Uuid::generate(), const caf::actor &actor = caf::actor()); Gap(const utility::JsonStore &jsn); + Gap(const Item &item, const caf::actor &actor); ~Gap() override = default; diff --git a/include/xstudio/timeline/gap_actor.hpp b/include/xstudio/timeline/gap_actor.hpp index ad73c5124..a73288cbb 100644 --- a/include/xstudio/timeline/gap_actor.hpp +++ b/include/xstudio/timeline/gap_actor.hpp @@ -4,8 +4,6 @@ #include #include "xstudio/timeline/gap.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/frame_rate_and_duration.hpp" namespace xstudio { namespace timeline { @@ -18,6 +16,10 @@ namespace timeline { const std::string &name = "Gap", const utility::FrameRateDuration &duration = utility::FrameRateDuration(), const utility::Uuid &uuid = utility::Uuid::generate()); + + GapActor(caf::actor_config &cfg, const Item &item); + GapActor(caf::actor_config &cfg, const Item &item, Item &nitem); + ~GapActor() override = default; const char *name() const override { return NAME.c_str(); } @@ -25,12 +27,15 @@ namespace timeline { private: inline static const std::string NAME = "GapActor"; void init(); - caf::behavior make_behavior() override { return behavior_; } + + caf::message_handler message_handler(); + + caf::behavior make_behavior() override { + return message_handler().or_else(base_.container_message_handler(this)); + } private: - caf::behavior behavior_; Gap base_; - caf::actor event_group_; }; } // namespace timeline } // namespace xstudio diff --git a/include/xstudio/timeline/item.hpp b/include/xstudio/timeline/item.hpp index 5327659cb..1146a2b54 100644 --- a/include/xstudio/timeline/item.hpp +++ b/include/xstudio/timeline/item.hpp @@ -10,39 +10,63 @@ #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" #include "xstudio/timeline/enums.hpp" -#include "xstudio/media/enums.hpp" +#include "xstudio/timeline/marker.hpp" +#include "xstudio/media/media.hpp" namespace xstudio { namespace timeline { typedef enum { IA_NONE = 0x0L, - IT_ENABLE = 0x1L, - IT_ADDR = 0x2L, - IT_ACTIVE = 0x3L, - IT_AVAIL = 0x4L, - IT_INSERT = 0x5L, - IT_REMOVE = 0x6L, - IT_SPLICE = 0x7L, - IT_NAME = 0x8L, - IT_FLAG = 0x9L, - IT_PROP = 0x10L, + IA_ENABLE = 0x1L, + IA_ADDR = 0x2L, + IA_ACTIVE = 0x3L, + IA_AVAIL = 0x4L, + IA_RANGE = 0x5L, + IA_INSERT = 0x6L, + IA_REMOVE = 0x7L, + IA_SPLICE = 0x8L, + IA_NAME = 0x9L, + IA_FLAG = 0x10L, + IA_PROP = 0x11L, + IA_LOCK = 0x12L, + IA_MARKER = 0x13L, + IA_DIRTY = 0x14L, } ItemAction; class Item; using Items = std::list; - using ResolvedItem = std::tuple; + using Point = std::pair; + using Box = std::pair; + + using ResolvedItem = std::pair; typedef std::function ItemEventFunc; class Item : private Items { public: + Item(const ItemType item_type, const std::string name) + : Items(), + item_type_(item_type), + name_(std::move(name)), + uuid_addr_(utility::UuidActorAddr(utility::Uuid::generate(), caf::actor_addr())) { + } + + Item(const ItemType item_type, const std::string name, const utility::FrameRate &rate) + : Items(), + item_type_(item_type), + name_(std::move(name)), + uuid_addr_(utility::UuidActorAddr(utility::Uuid::generate(), caf::actor_addr())) { + has_available_range_ = true; + active_range_ = utility::FrameRange(utility::FrameRateDuration(0, rate)); + } + Item( const ItemType item_type, - const utility::Uuid uuid = utility::Uuid::generate(), - const std::optional active_range = {}, + const utility::Uuid uuid, + const std::optional active_range = {}, const std::optional available_range = {}) : Items(), item_type_(item_type), @@ -75,6 +99,10 @@ namespace timeline { } Item(const utility::JsonStore &jsn, caf::actor_system *system = nullptr); + + // Item(const Item& a); + // Item& operator=(const Item&); + using Items::clear; using Items::empty; using Items::size; @@ -102,9 +130,20 @@ namespace timeline { using Items::splice; + [[nodiscard]] Item clone(const bool reset_uuids = false) const { + Item item = *this; + item.unbind(); + if (reset_uuids) + item.reset_uuid(true); + return item; + } + [[nodiscard]] const Items &children() const { return *this; } [[nodiscard]] Items &children() { return *this; } + [[nodiscard]] const Markers &markers() const { return markers_; } + [[nodiscard]] Markers &markers() { return markers_; } + [[nodiscard]] bool valid_child(const Item &child) const; [[nodiscard]] bool valid() const; @@ -131,6 +170,13 @@ namespace timeline { [[nodiscard]] utility::FrameRateDuration trimmed_frame_duration() const { return trimmed_range().frame_duration(); } + + [[nodiscard]] std::optional available_frame_start() const { + if (has_available_range_) + return available_range()->frame_start(); + return {}; + } + [[nodiscard]] std::optional active_frame_duration() const; [[nodiscard]] std::optional available_frame_duration() const; @@ -144,6 +190,18 @@ namespace timeline { [[nodiscard]] int frame_at_index(const int item_index) const; [[nodiscard]] int frame_at_index(const int item_index, const int item_frame) const; + [[nodiscard]] std::optional frame_at_item_frame( + const utility::Uuid &item_uuid, + const int item_local_frame, + const bool skip_disabled) const; + + [[nodiscard]] std::optional top_left(const utility::Uuid &uuid) const; + [[nodiscard]] std::optional bottom_right(const utility::Uuid &uuid) const; + [[nodiscard]] std::optional box(const utility::Uuid &uuid) const; + [[nodiscard]] Point top_left() const; + [[nodiscard]] Point bottom_right() const; + [[nodiscard]] Box box() const; + [[nodiscard]] caf::actor_addr actor_addr() const { return uuid_addr_.second; } [[nodiscard]] caf::actor actor() const { return caf::actor_cast(uuid_addr_.second); @@ -152,30 +210,45 @@ namespace timeline { return utility::UuidActor(uuid(), actor()); } [[nodiscard]] bool enabled() const { return enabled_; } + [[nodiscard]] bool locked() const { return locked_; } [[nodiscard]] std::string name() const { return name_; } [[nodiscard]] std::string flag() const { return flag_; } [[nodiscard]] utility::JsonStore prop() const { return prop_; } - [[nodiscard]] bool transparent() const { - if (item_type_ == ItemType::IT_GAP) - return true; - return not enabled_; - } - [[nodiscard]] utility::UuidActorVector - find_all_uuid_actors(const ItemType item_type) const; + [[nodiscard]] bool transparent() const; + [[nodiscard]] utility::UuidActorVector find_all_uuid_actors( + const ItemType item_type, const bool only_enabled_items = false) const; + + [[nodiscard]] std::vector> + find_all_items(const ItemType item_type, const ItemType track_type = IT_NONE) const; + + [[nodiscard]] std::vector> + find_all_items(const ItemType item_type, const ItemType track_type = IT_NONE); + [[nodiscard]] utility::JsonStore serialise(const int depth = std::numeric_limits::max()) const; bool replace_child(const Item &child); - bool update(const utility::JsonStore &event); + std::set update(const utility::JsonStore &event); + void merge_gaps(const bool purge_empty_clips = false); utility::JsonStore refresh(const int depth = std::numeric_limits::max()); void set_uuid(const utility::Uuid &uuid) { uuid_addr_.first = uuid; } - utility::JsonStore set_enabled(const bool &value); + void reset_uuid(const bool recursive = false); + void reset_media_uuid(); + void reset_actor(const bool recursive = false); + + utility::JsonStore set_enabled(const bool value); + utility::JsonStore set_locked(const bool value); utility::JsonStore set_name(const std::string &value); utility::JsonStore set_flag(const std::string &value); utility::JsonStore set_prop(const utility::JsonStore &value); + utility::JsonStore set_markers(const Markers &value); + utility::JsonStore set_markers(const std::vector &value) { + return set_markers(Markers(value.begin(), value.end())); + } + void set_system(caf::actor_system *value) { the_system_ = value; } utility::JsonStore set_actor_addr(const caf::actor_addr &value); @@ -185,6 +258,9 @@ namespace timeline { utility::JsonStore set_active_range(const utility::FrameRange &value); utility::JsonStore set_available_range(const utility::FrameRange &value); + utility::JsonStore + set_range(const utility::FrameRange &avail, const utility::FrameRange &active); + utility::JsonStore insert( Items::iterator position, const Item &val, @@ -204,9 +280,11 @@ namespace timeline { f.field("app_rng", x.active_range_), f.field("ava_rng", x.available_range_), f.field("enabled", x.enabled_), + f.field("locked", x.locked_), f.field("name", x.name_), f.field("flag", x.flag_), f.field("prop", x.prop_), + f.field("markers", x.markers_), f.field("has_av", x.has_available_range_), f.field("has_ac", x.has_active_range_), f.field("children", x.children())); @@ -217,21 +295,44 @@ namespace timeline { uuid_addr_.first == other.uuid_addr_.first and available_range_ == other.available_range_ and active_range_ == other.active_range_ and enabled_ == other.enabled_ and - flag_ == other.flag_ and prop_ == other.prop_ and name_ == other.name_; + locked_ == other.locked_ and flag_ == other.flag_ and + prop_ == other.prop_ and name_ == other.name_; } [[nodiscard]] std::optional resolve_time( const utility::FrameRate &time, const media::MediaType mt = media::MediaType::MT_IMAGE, - const utility::UuidSet &focus = utility::UuidSet()) const; + const utility::UuidSet &focus = utility::UuidSet(), + const bool must_have_focus = false) const; + + // doesn't bake tracks + [[nodiscard]] std::vector resolve_time_raw( + const utility::FrameRate &time, + const media::MediaType mt = media::MediaType::MT_IMAGE, + const utility::UuidSet &focus = utility::UuidSet(), + const bool ignore_disabled = true) const; void undo(const utility::JsonStore &event); void redo(const utility::JsonStore &event); - void bind_item_event_func(ItemEventFunc fn, const bool recursive = false); + void bind_item_pre_event_func(ItemEventFunc fn, const bool recursive = false); + void bind_item_post_event_func(ItemEventFunc fn, const bool recursive = false); + + // rest binds, required for copies.. + void unbind(); [[nodiscard]] utility::JsonStore make_actor_addr_update() const; + bool has_dirty(const utility::JsonStore &event); + + // This method allows an Item to produce a 'FrameTimeMap' which is the + // data a playhead needs to do playback + caf::typed_response_promise get_all_frame_IDs( + const media::MediaType media_type, + const utility::TimeSourceMode tsm, + const utility::FrameRate &override_rate, + const utility::UuidSet &focus_list = utility::UuidSet()); + private: bool process_event(const utility::JsonStore &event); void splice_direct( @@ -243,11 +344,15 @@ namespace timeline { Items::iterator erase_direct(Items::iterator position); void set_active_range_direct(const utility::FrameRange &value); void set_available_range_direct(const utility::FrameRange &value); + void + set_range_direct(const utility::FrameRange &avail, const utility::FrameRange &active); void set_actor_addr_direct(const caf::actor_addr &value); - void set_enabled_direct(const bool &value); + void set_enabled_direct(const bool value); + void set_locked_direct(const bool value); void set_name_direct(const std::string &value); void set_flag_direct(const std::string &value); void set_prop_direct(const utility::JsonStore &value); + void set_markers_direct(const Markers &value); [[nodiscard]] std::string actor_addr_to_string(const caf::actor_addr &addr) const; [[nodiscard]] caf::actor_addr string_to_actor_addr(const std::string &addr) const; @@ -255,6 +360,8 @@ namespace timeline { [[nodiscard]] caf::actor_system &system() const { return *the_system_; } private: + friend class BuildFrameIDsHelper; + ItemType item_type_{IT_NONE}; utility::UuidActorAddr uuid_addr_; utility::FrameRange available_range_; @@ -262,16 +369,29 @@ namespace timeline { bool has_available_range_{false}; bool has_active_range_{false}; bool enabled_{true}; + bool locked_{false}; std::string name_{}; std::string flag_{}; - utility::JsonStore prop_{}; + utility::JsonStore prop_{R"({})"_json}; + Markers markers_; // not sure if this is safe.. caf::actor_system *the_system_{nullptr}; - ItemEventFunc item_event_callback_{nullptr}; - bool recursive_bind_{false}; + ItemEventFunc item_pre_event_callback_{nullptr}; + ItemEventFunc item_post_event_callback_{nullptr}; + bool recursive_bind_pre_{false}; + bool recursive_bind_post_{false}; }; + inline utility::UuidVector get_event_id(const utility::JsonStore &event) { + auto result = utility::UuidVector(); + + for (const auto &i : event) + result.push_back(i.at("redo").at("event_id")); + + return result; + } + inline std::optional find_item(const Items &items, const utility::Uuid &uuid) { auto it = std::find_if(items.cbegin(), items.cend(), [uuid](Item const &obj) { @@ -296,6 +416,75 @@ namespace timeline { return it; } + inline std::optional + find_track_from_item(const Items &items, const utility::Uuid &uuid, const bool top = true) { + auto it = std::find_if(items.cbegin(), items.cend(), [uuid](Item const &obj) { + return obj.uuid() == uuid; + }); + + // search children + if (it == items.cend()) { + for (auto i = items.cbegin(); i != items.cend(); ++i) { + auto ii = find_track_from_item(i->children(), uuid, false); + + if (ii) { + if (i->item_type() == IT_AUDIO_TRACK or i->item_type() == IT_VIDEO_TRACK) + it = i; + else + it = *ii; + break; + } + } + } + + if (it == items.cend()) + return {}; + + if (top and (it->item_type() != IT_AUDIO_TRACK and it->item_type() != IT_VIDEO_TRACK)) + return {}; + + return it; + } + + inline utility::UuidVector + find_media_clips(const Items &items, const utility::Uuid &media_uuid) { + auto result = utility::UuidVector(); + + for (const auto &i : items) { + switch (i.item_type()) { + case ItemType::IT_CLIP: + if (i.prop().value("media_uuid", utility::Uuid()) == media_uuid) + result.emplace_back(i.uuid()); + break; + + case ItemType::IT_TIMELINE: + case ItemType::IT_AUDIO_TRACK: + case ItemType::IT_VIDEO_TRACK: + case ItemType::IT_STACK: { + const auto more = find_media_clips(i.children(), media_uuid); + if (not more.empty()) + result.insert(result.end(), more.begin(), more.end()); + } + + break; + + default: + break; + } + } + + return result; + } + + inline std::set get_event_ids(const utility::JsonStore &events) { + auto result = std::set(); + + for (const auto &i : events) + result.insert(utility::Uuid(i.at("undo").at("event_id"))); + + return result; + } + inline auto find_uuid(const Items &items, const utility::Uuid &uuid) { return std::find_if(items.cbegin(), items.cend(), [uuid](Item const &obj) { return obj.uuid() == uuid; @@ -307,6 +496,37 @@ namespace timeline { items.begin(), items.end(), [uuid](Item const &obj) { return obj.uuid() == uuid; }); } + // Sure there's a better way of doing all this stuff.. + inline caf::actor find_parent_actor(const Item &item, const utility::Uuid &uuid) { + auto result = caf::actor(); + + auto item_it = find_uuid(item.children(), uuid); + if (item_it != std::end(item.children())) { + result = item.actor(); + } else { + for (const auto &i : item.children()) { + result = find_parent_actor(i, uuid); + if (result) + break; + } + } + + return result; + } + + inline auto + find_indexes(const Items &items, const ItemType type, const bool ignore_disabled = false) { + auto result = std::vector(); + auto c = 0; + for (const auto &i : items) { + if (i.item_type() == type and (i.enabled() or not ignore_disabled)) + result.push_back(c); + c++; + } + + return result; + } + inline auto find_actor(const Items &items, const caf::actor &actor) { return std::find_if(items.cbegin(), items.cend(), [actor](Item const &obj) { return obj.actor() == actor; @@ -375,5 +595,6 @@ namespace timeline { return str; } + } // namespace timeline } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/timeline/marker.hpp b/include/xstudio/timeline/marker.hpp new file mode 100644 index 000000000..6b896e568 --- /dev/null +++ b/include/xstudio/timeline/marker.hpp @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include "xstudio/utility/frame_range.hpp" +#include "xstudio/utility/json_store.hpp" +#include "xstudio/utility/uuid.hpp" + +namespace xstudio { +namespace timeline { + + class Marker; + using Markers = std::list; + + class Marker { + public: + Marker(const std::string name = "Marker") + : name_(std::move(name)), uuid_(std::move(utility::Uuid::generate())) {} + + Marker(const utility::JsonStore &jsn); + + [[nodiscard]] utility::Uuid uuid() const { return uuid_; } + + [[nodiscard]] utility::FrameRange range() const { return range_; } + [[nodiscard]] utility::FrameRate rate() const { return range_.rate(); } + + [[nodiscard]] utility::FrameRate duration() const { return range_.duration(); } + [[nodiscard]] utility::FrameRate start() const { return range_.start(); } + [[nodiscard]] utility::FrameRateDuration frame_start() const { + return range_.frame_start(); + } + [[nodiscard]] utility::FrameRateDuration frame_duration() const { + return range_.frame_duration(); + } + + [[nodiscard]] std::string name() const { return name_; } + [[nodiscard]] std::string flag() const { return flag_; } + [[nodiscard]] utility::JsonStore prop() const { return prop_; } + + [[nodiscard]] utility::JsonStore serialise() const; + + void set_uuid(const utility::Uuid &uuid) { uuid_ = uuid; } + void reset_uuid() { uuid_.generate_in_place(); } + + void set_name(const std::string &value) { name_ = value; } + void set_flag(const std::string &value) { flag_ = value; } + void set_prop(const utility::JsonStore &value) { prop_ = value; } + void set_range(const utility::FrameRange &value) { range_ = value; } + + template friend bool inspect(Inspector &f, Marker &x) { + return f.object(x).fields( + f.field("u", x.uuid_), + f.field("rng", x.range_), + f.field("name", x.name_), + f.field("flag", x.flag_), + f.field("prop", x.prop_)); + } + + bool operator==(const Marker &other) const { + return uuid_ == other.uuid_ and range_ == other.range_ and name_ == other.name_ and + flag_ == other.flag_ and prop_ == other.prop_; + } + + // private: + + private: + utility::Uuid uuid_{}; + utility::FrameRange range_{}; + std::string name_{}; + std::string flag_{}; + utility::JsonStore prop_{}; + }; + + inline utility::JsonStore serialise_markers(const Markers &markers) { + auto result = R"([])"_json; + for (const auto &i : markers) + result.emplace_back(i.serialise()); + + return result; + } +} // namespace timeline +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/timeline/stack.hpp b/include/xstudio/timeline/stack.hpp index 1b40324a2..1fbabd860 100644 --- a/include/xstudio/timeline/stack.hpp +++ b/include/xstudio/timeline/stack.hpp @@ -1,26 +1,27 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include #include +#include "xstudio/timeline/item.hpp" #include "xstudio/utility/container.hpp" -#include "xstudio/utility/edit_list.hpp" +#include "xstudio/utility/frame_rate.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" -#include "xstudio/timeline/item.hpp" namespace xstudio { namespace timeline { class Stack : public utility::Container { public: Stack( - const std::string &name = "Stack", - const utility::Uuid &uuid = utility::Uuid::generate(), - const caf::actor &actor = caf::actor()); + const std::string &name = "Stack", + const utility::FrameRate &rate = utility::FrameRate(), + const utility::Uuid &uuid = utility::Uuid::generate(), + const caf::actor &actor = caf::actor()); Stack(const utility::JsonStore &jsn); + Stack(const Item &item, const caf::actor &actor); ~Stack() override = default; [[nodiscard]] utility::JsonStore serialise() const override; diff --git a/include/xstudio/timeline/stack_actor.hpp b/include/xstudio/timeline/stack_actor.hpp index 5b70d4de1..1677a01cc 100644 --- a/include/xstudio/timeline/stack_actor.hpp +++ b/include/xstudio/timeline/stack_actor.hpp @@ -4,8 +4,6 @@ #include #include "xstudio/timeline/stack.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/uuid.hpp" namespace xstudio { namespace timeline { @@ -15,8 +13,12 @@ namespace timeline { StackActor(caf::actor_config &cfg, const utility::JsonStore &jsn, Item &item); StackActor( caf::actor_config &cfg, - const std::string &name = "Stack", - const utility::Uuid &uuid = utility::Uuid::generate()); + const std::string &name = "Stack", + const utility::FrameRate &rate = utility::FrameRate(), + const utility::Uuid &uuid = utility::Uuid::generate()); + + StackActor(caf::actor_config &cfg, const Item &item); + StackActor(caf::actor_config &cfg, const Item &item, Item &nitem); ~StackActor() override = default; const char *name() const override { return NAME.c_str(); } @@ -25,40 +27,50 @@ namespace timeline { inline static const std::string NAME = "StackActor"; void init(); void on_exit() override; + caf::message_handler message_handler(); - caf::behavior make_behavior() override { return behavior_; } + caf::behavior make_behavior() override { + return message_handler().or_else(base_.container_message_handler(this)); + } void add_item(const utility::UuidActor &ua); caf::actor deserialise(const utility::JsonStore &value, const bool replace_item = false); + void deserialise(); void item_event_callback(const utility::JsonStore &event, Item &item); + + std::pair> + remove_items(const int index, const int count = 1); + void insert_items( + caf::typed_response_promise rp, const int index, - const utility::UuidActorVector &uav, - caf::typed_response_promise rp); + const utility::UuidActorVector &uav); void remove_items( - const int index, - const int count, caf::typed_response_promise< - std::pair>> rp); + std::pair>> rp, + const int index, + const int count = 1); void erase_items( + caf::typed_response_promise rp, const int index, - const int count, - caf::typed_response_promise rp); + const int count = 1); void move_items( + caf::typed_response_promise rp, const int src_index, const int count, - const int dst_index, - caf::typed_response_promise rp); + const int dst_index); private: - caf::behavior behavior_; Stack base_; - caf::actor event_group_; std::map actors_; + // might need to prune.. ? + std::set events_processed_; + + std::map monitor_; }; } // namespace timeline } // namespace xstudio diff --git a/include/xstudio/timeline/timeline.hpp b/include/xstudio/timeline/timeline.hpp index b7c39a897..8c88da827 100644 --- a/include/xstudio/timeline/timeline.hpp +++ b/include/xstudio/timeline/timeline.hpp @@ -6,7 +6,6 @@ #include #include "xstudio/utility/container.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" #include "xstudio/utility/frame_range.hpp" @@ -28,9 +27,10 @@ namespace timeline { class Timeline : public utility::Container { public: Timeline( - const std::string &name = "Timeline", - const utility::Uuid &uuid = utility::Uuid::generate(), - const caf::actor &actor = caf::actor()); + const std::string &name = "Timeline", + const utility::FrameRate &rate = utility::FrameRate(), + const utility::Uuid &uuid = utility::Uuid::generate(), + const caf::actor &actor = caf::actor()); Timeline(const utility::JsonStore &jsn); ~Timeline() override = default; diff --git a/include/xstudio/timeline/timeline_actor.hpp b/include/xstudio/timeline/timeline_actor.hpp index 0f57b7920..be7189878 100644 --- a/include/xstudio/timeline/timeline_actor.hpp +++ b/include/xstudio/timeline/timeline_actor.hpp @@ -4,8 +4,8 @@ #include #include "xstudio/timeline/timeline.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/uuid.hpp" +#include "xstudio/utility/notification_handler.hpp" +#include "xstudio/json_store/json_store_handler.hpp" namespace xstudio { namespace timeline { @@ -17,9 +17,11 @@ namespace timeline { const caf::actor &playlist = caf::actor()); TimelineActor( caf::actor_config &cfg, - const std::string &name = "Timeline", - const utility::Uuid &uuid = utility::Uuid::generate(), - const caf::actor &playlist = caf::actor()); + const std::string &name = "Timeline", + const utility::FrameRate &rate = utility::FrameRate(), + const utility::Uuid &uuid = utility::Uuid::generate(), + const caf::actor &playlist = caf::actor(), + const bool with_tracks = false); ~TimelineActor() override = default; const char *name() const override { return NAME.c_str(); } @@ -30,64 +32,105 @@ namespace timeline { inline static const std::string NAME = "TimelineActor"; void init(); - caf::behavior make_behavior() override { return behavior_; } + caf::message_handler message_handler(); - void deliver_media_pointer( - const int logical_frame, - const media::MediaType media_type, - caf::typed_response_promise rp); + caf::behavior make_behavior() override { + return message_handler() + .or_else(base_.container_message_handler(this)) + .or_else(notification_.message_handler(this, base_.event_group())) + .or_else(jsn_handler_.message_handler()); + } void add_item(const utility::UuidActor &ua); - void add_media( - const utility::UuidActor &ua, - const utility::Uuid &uuid_before, - caf::typed_response_promise rp); void add_media( caf::actor actor, const utility::Uuid &uuid, const utility::Uuid &before_uuid = utility::Uuid()); + + void deliver_media_pointer( + caf::typed_response_promise rp, + const int logical_frame, + const media::MediaType media_type); + + void add_media( + caf::typed_response_promise rp, + const utility::UuidActor &ua, + const utility::Uuid &uuid_before); bool remove_media(caf::actor actor, const utility::Uuid &uuid); void insert_items( + caf::typed_response_promise rp, const int index, - const utility::UuidActorVector &uav, - caf::typed_response_promise rp); + const utility::UuidActorVector &uav); void remove_items( - const int index, - const int count, caf::typed_response_promise< - std::pair>> rp); + std::pair>> rp, + const int index, + const int count = 1); void erase_items( + caf::typed_response_promise rp, const int index, - const int count, - caf::typed_response_promise rp); + const int count = 1); + + std::pair> + remove_items(const int index, const int count = 1); - void sort_alphabetically(); + void sort_by_media_display_info(const int sort_column_index, const bool ascending); + + void + bake(caf::typed_response_promise rp, const utility::UuidSet &uuids); void on_exit() override; caf::actor deserialise(const utility::JsonStore &value, const bool replace_item = false); - void item_event_callback(const utility::JsonStore &event, Item &item); + void item_pre_event_callback(const utility::JsonStore &event, Item &item); + void item_post_event_callback(const utility::JsonStore &event, Item &item); + + void export_otio( + caf::typed_response_promise rp, + const std::string &otio_str, + const caf::uri &path, + const std::string &type, + const std::string &target_schema = ""); + + void export_otio_as_string(caf::typed_response_promise rp); private: - caf::behavior behavior_; + void monitor_media(const caf::actor &actor); + Timeline base_; - caf::actor event_group_; + caf::actor change_event_group_; + + utility::Uuid history_uuid_; + caf::actor history_; + + caf::actor selection_actor_; - utility::UuidActorMap actors_; + utility::UuidActorMap item_actors_; utility::UuidActorMap media_actors_; + utility::JsonStore playhead_serialisation_; + caf::actor_addr playlist_; bool content_changed_{false}; utility::UuidActor playhead_; - caf::actor history_; - // bool update_edit_list_; - // utility::EditList edit_list_; + // might need to prune.. ? + std::set events_processed_; + utility::UuidActorVector video_tracks_; + + // current selection + utility::UuidActorVector selection_; + + utility::NotificationHandler notification_; + + std::map monitor_; + + json_store::JsonStoreHandler jsn_handler_; }; } // namespace timeline diff --git a/include/xstudio/timeline/track.hpp b/include/xstudio/timeline/track.hpp index 61e312fae..50bb6705f 100644 --- a/include/xstudio/timeline/track.hpp +++ b/include/xstudio/timeline/track.hpp @@ -1,17 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include #include -#include "xstudio/media/media.hpp" +#include "xstudio/media/enums.hpp" +#include "xstudio/timeline/item.hpp" #include "xstudio/utility/container.hpp" -#include "xstudio/utility/frame_range.hpp" -#include "xstudio/utility/edit_list.hpp" +#include "xstudio/utility/frame_rate.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" -#include "xstudio/timeline/item.hpp" namespace xstudio { namespace timeline { @@ -20,10 +18,12 @@ namespace timeline { public: Track( const std::string &name = "Track", + const utility::FrameRate &rate = utility::FrameRate(), const media::MediaType media_type = media::MediaType::MT_IMAGE, const utility::Uuid &uuid = utility::Uuid::generate(), const caf::actor &actor = caf::actor()); Track(const utility::JsonStore &jsn); + Track(const Item &item, const caf::actor &actor); ~Track() override = default; @@ -46,6 +46,8 @@ namespace timeline { utility::JsonStore refresh_item() { return item_.refresh(); } + utility::FrameRate rate() const { return item_.rate(); } + private: media::MediaType media_type_; Item item_; diff --git a/include/xstudio/timeline/track_actor.hpp b/include/xstudio/timeline/track_actor.hpp index 8b9083c8c..a095c7313 100644 --- a/include/xstudio/timeline/track_actor.hpp +++ b/include/xstudio/timeline/track_actor.hpp @@ -6,8 +6,7 @@ #include "xstudio/timeline/clip.hpp" #include "xstudio/timeline/stack.hpp" #include "xstudio/timeline/track.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/uuid.hpp" +#include "xstudio/utility/notification_handler.hpp" namespace xstudio { namespace timeline { @@ -18,83 +17,118 @@ namespace timeline { TrackActor( caf::actor_config &cfg, const std::string &name = "Track", + const utility::FrameRate &rate = utility::FrameRate(), const media::MediaType media_type = media::MediaType::MT_IMAGE, const utility::Uuid &uuid = utility::Uuid::generate()); + TrackActor(caf::actor_config &cfg, const Item &item); + TrackActor(caf::actor_config &cfg, const Item &item, Item &nitem); + ~TrackActor() override = default; const char *name() const override { return NAME.c_str(); } void on_exit() override; + static caf::message_handler default_event_handler(); + private: inline static const std::string NAME = "TrackActor"; void init(); - caf::behavior make_behavior() override { return behavior_; } + caf::message_handler message_handler(); + + caf::behavior make_behavior() override { + return message_handler() + .or_else(base_.container_message_handler(this)) + .or_else(notification_.message_handler(this, base_.event_group())); + } + // void deliver_media_pointer( // const int logical_frame, caf::typed_response_promise rp); void add_item(const utility::UuidActor &ua); caf::actor deserialise(const utility::JsonStore &value, const bool replace_item = false); + void deserialise(); void item_event_callback(const utility::JsonStore &event, Item &item); void split_item( + caf::typed_response_promise rp, const Items::const_iterator &item, - const int frame, - caf::typed_response_promise rp); + const int frame); void insert_items( + caf::typed_response_promise rp, const int index, - const utility::UuidActorVector &uav, - caf::typed_response_promise rp); + + const utility::UuidActorVector &uav); void insert_items_at_frame( + caf::typed_response_promise rp, const int frame, - const utility::UuidActorVector &uav, - caf::typed_response_promise rp); + const utility::UuidActorVector &uav); void remove_items_at_frame( + caf::typed_response_promise< + std::pair>> rp, const int frame, const int duration, - caf::typed_response_promise< - std::pair>> rp); + const bool add_gap, + const bool collapse_gaps); void remove_items( + caf::typed_response_promise< + std::pair>> rp, const int index, const int count, - caf::typed_response_promise< - std::pair>> rp); + const bool add_gap, + const bool collapse_gaps); void erase_items_at_frame( + caf::typed_response_promise rp, const int frame, const int duration, - caf::typed_response_promise rp); + const bool add_gap, + const bool collapse_gaps); void erase_items( + caf::typed_response_promise rp, const int index, const int count, - caf::typed_response_promise rp); + const bool add_gap, + const bool collapse_gaps); void move_items( + caf::typed_response_promise rp, const int src_index, const int count, const int dst_index, - caf::typed_response_promise rp); + const bool add_gap, + const bool replace_with_gap = false); void move_items_at_frame( + caf::typed_response_promise rp, const int frame, const int duration, const int dest_frame, const bool insert, - caf::typed_response_promise rp); + const bool add_gap, + const bool replace_with_gap = false); + + void merge_gaps(caf::typed_response_promise rp); + utility::JsonStore merge_gaps(); + + std::pair> remove_items( + const int index, const int count, const bool add_gap, const bool collapse_gaps); + + // void merge_gaps(caf::typed_response_promise rp); private: - caf::behavior behavior_; Track base_; - caf::actor event_group_; std::map actors_; + // might need to prune.. ? + std::set events_processed_; + utility::NotificationHandler notification_; - // bool update_edit_list_; - // utility::EditList edit_list_; + std::map monitor_; }; } // namespace timeline } // namespace xstudio diff --git a/include/xstudio/ui/canvas/canvas.hpp b/include/xstudio/ui/canvas/canvas.hpp index 5c82c4a9a..9aecc36ba 100644 --- a/include/xstudio/ui/canvas/canvas.hpp +++ b/include/xstudio/ui/canvas/canvas.hpp @@ -6,6 +6,7 @@ #include #include +#include "xstudio/ui/canvas/shapes.hpp" #include "xstudio/ui/canvas/stroke.hpp" #include "xstudio/ui/canvas/caption.hpp" #include "xstudio/ui/canvas/handle.hpp" @@ -47,7 +48,7 @@ namespace ui { */ class Canvas { - using Item = std::variant; + using Item = std::variant; using ItemVec = std::vector; public: @@ -57,8 +58,13 @@ namespace ui { current_item_(o.current_item_), undo_stack_(o.undo_stack_), redo_stack_(o.redo_stack_), - last_change_time_(o.last_change_time_), - uuid_(o.uuid_) {} + uuid_(o.uuid_), + next_shape_id_(o.next_shape_id_) { + // make sure current_item_ is pushed into finished + // strokes/captions on copy + end_draw(); + last_change_time_ = o.last_change_time_; + } bool operator==(const Canvas &o) const { std::shared_lock l(mutex_); @@ -67,12 +73,17 @@ namespace ui { Canvas &operator=(const Canvas &o) { std::unique_lock l(mutex_); - items_ = o.items_; - current_item_ = o.current_item_; - undo_stack_ = o.undo_stack_; - redo_stack_ = o.redo_stack_; + items_ = o.items_; + current_item_ = o.current_item_; + undo_stack_ = o.undo_stack_; + redo_stack_ = o.redo_stack_; + uuid_ = o.uuid_; + next_shape_id_ = o.next_shape_id_; + // make sure current_item_ is pushed into finished + // strokes/captions on copy + changed(); + end_draw_no_lock(); last_change_time_ = o.last_change_time_; - uuid_ = o.uuid_; return *this; } @@ -114,7 +125,46 @@ namespace ui { // Delete the strokes when reaching 0 opacity. bool fade_all_strokes(float opacity); - // Shapes + // Shapes (filled) + + uint32_t start_quad( + const utility::ColourTriplet &colour, const std::vector &corners); + void update_quad( + uint32_t id, + const utility::ColourTriplet &colour, + const std::vector &corners, + float softness, + float opacity, + bool invert); + + uint32_t start_polygon( + const utility::ColourTriplet &colour, const std::vector &points); + void update_polygon( + uint32_t id, + const utility::ColourTriplet &colour, + const std::vector &points, + float softness, + float opacity, + bool invert); + + uint32_t start_ellipse( + const utility::ColourTriplet &colour, + const Imath::V2f ¢er, + const Imath::V2f &radius, + float angle); + void update_ellipse( + uint32_t id, + const utility::ColourTriplet &colour, + const Imath::V2f ¢er, + const Imath::V2f &radius, + float angle, + float softness, + float opacity, + bool invert); + + bool remove_shape(uint32_t id); + + // Shapes (outlined using strokes) void start_square(const utility::ColourTriplet &colour, float thickness, float opacity); @@ -159,6 +209,8 @@ namespace ui { std::array caption_cursor_position() const; Imath::V2f caption_cursor_bottom() const; + size_t hash() const { return hash_; } + void update_caption_text(const std::string &text); void update_caption_position(const Imath::V2f &position); void update_caption_width(float wrap_width); @@ -217,7 +269,6 @@ namespace ui { } template T get_current() const { - std::shared_lock l(mutex_); return std::get(current_item_.value()); } @@ -255,6 +306,9 @@ namespace ui { std::string::const_iterator cursor_position_; + uint32_t next_shape_id_{0}; + size_t hash_{0}; + mutable std::shared_mutex mutex_; }; diff --git a/include/xstudio/ui/canvas/caption.hpp b/include/xstudio/ui/canvas/caption.hpp index 080372b1c..f3439bb5b 100644 --- a/include/xstudio/ui/canvas/caption.hpp +++ b/include/xstudio/ui/canvas/caption.hpp @@ -64,3 +64,16 @@ namespace ui { } // end namespace canvas } // end namespace ui } // end namespace xstudio + +namespace std +{ + template <> + struct hash + { + std::size_t operator()(const xstudio::ui::canvas::Caption &item) const + { + std::hash hasher; + return hasher(item.hash()); + } + }; +} \ No newline at end of file diff --git a/include/xstudio/ui/canvas/shapes.hpp b/include/xstudio/ui/canvas/shapes.hpp new file mode 100644 index 000000000..f19c8a34f --- /dev/null +++ b/include/xstudio/ui/canvas/shapes.hpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include "xstudio/utility/json_store.hpp" + + +namespace xstudio { +namespace ui { + namespace canvas { + + // Generic 4 sided polygon + struct Quad { + + Imath::V2f bl; + Imath::V2f tl; + Imath::V2f tr; + Imath::V2f br; + + utility::ColourTriplet colour; + + float softness{0.0f}; + float opacity{1.0f}; + + bool invert{false}; + + // Field used by Canvas to track shapes + // Do not participate in equality tests + uint32_t _id; + + bool operator==(const Quad &o) const; + + std::string hash() const; + }; + + void from_json(const nlohmann::json &j, Quad &s); + void to_json(nlohmann::json &j, const Quad &s); + + struct Polygon { + + std::vector points; + + utility::ColourTriplet colour; + + float softness{0.0f}; + float opacity{1.0f}; + + bool invert{false}; + + // Field used by Canvas to track shapes + // Do not participate in equality tests + uint32_t _id; + + bool operator==(const Polygon &o) const; + + std::string hash() const; + }; + + void from_json(const nlohmann::json &j, Polygon &s); + void to_json(nlohmann::json &j, const Polygon &s); + + struct Ellipse { + + Imath::V2f center; + Imath::V2f radius; + float angle{0.0f}; + + utility::ColourTriplet colour; + + float softness{0.0f}; + float opacity{1.0f}; + + bool invert{false}; + + // Field used by Canvas to track shapes + // Do not participate in equality tests + uint32_t _id; + + bool operator==(const Ellipse &o) const; + + std::string hash() const; + }; + + void from_json(const nlohmann::json &j, Ellipse &s); + void to_json(nlohmann::json &j, const Ellipse &s); + + } // end namespace canvas +} // end namespace ui +} // end namespace xstudio + +namespace std +{ + template <> + struct hash + { + std::size_t operator()(const xstudio::ui::canvas::Quad &item) const + { + std::hash hasher; + return hasher(item.hash()); + } + }; + + template <> + struct hash + { + std::size_t operator()(const xstudio::ui::canvas::Polygon &item) const + { + std::hash hasher; + return hasher(item.hash()); + } + }; + + template <> + struct hash + { + std::size_t operator()(const xstudio::ui::canvas::Ellipse &item) const + { + std::hash hasher; + return hasher(item.hash()); + } + }; +} \ No newline at end of file diff --git a/include/xstudio/ui/canvas/stroke.hpp b/include/xstudio/ui/canvas/stroke.hpp index f2a8ca0dc..1c83f88f5 100644 --- a/include/xstudio/ui/canvas/stroke.hpp +++ b/include/xstudio/ui/canvas/stroke.hpp @@ -34,6 +34,8 @@ namespace ui { bool operator==(const Stroke &o) const; + std::string hash() const; + // TODO: Below are shapes and should be extracted to dedicated types // Rendering them as stroke seems like an implementation details and // will probably not hold if we need filled shape for example. @@ -58,4 +60,17 @@ namespace ui { } // end namespace canvas } // end namespace ui -} // end namespace xstudio \ No newline at end of file +} // end namespace xstudio + +namespace std +{ + template <> + struct hash + { + std::size_t operator()(const xstudio::ui::canvas::Stroke &item) const + { + std::hash hasher; + return hasher(item.hash()); + } + }; +} \ No newline at end of file diff --git a/include/xstudio/ui/enums.hpp b/include/xstudio/ui/enums.hpp new file mode 100644 index 000000000..e5a445616 --- /dev/null +++ b/include/xstudio/ui/enums.hpp @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +namespace xstudio::ui { +enum class EventType { Move, Drag, ButtonDown, ButtonRelease, MouseWheel, DoubleClick }; +} // namespace xstudio::ui diff --git a/include/xstudio/ui/font.hpp b/include/xstudio/ui/font.hpp index 00bf9ef13..5a20be2d6 100644 --- a/include/xstudio/ui/font.hpp +++ b/include/xstudio/ui/font.hpp @@ -9,6 +9,7 @@ namespace xstudio { namespace ui { enum Justification { JustifyLeft, JustifyRight, JustifyCentre }; + enum VerticalJustification { JustifyTopLine, JustifyTop, JustifyBottom, JustifyVCentre }; class Fonts { @@ -80,7 +81,9 @@ namespace ui { const float wrap_width, const float text_size, const Justification &just, - const float line_spacing) const; + const float line_spacing, + const bool align_around_position = true, + const VerticalJustification v_just = JustifyTopLine) const; /** * @brief Given a position in the viewport, does it correspond to a character diff --git a/include/xstudio/ui/keyboard.hpp b/include/xstudio/ui/keyboard.hpp index f82ed4945..fd10a04a1 100644 --- a/include/xstudio/ui/keyboard.hpp +++ b/include/xstudio/ui/keyboard.hpp @@ -35,15 +35,14 @@ namespace ui { const std::string name, const std::string component, const std::string description, - const std::string context, + const std::string window_name, const bool auto_repeat, - caf::actor_addr watcher); + caf::actor_addr watcher, + const utility::Uuid uuid = utility::Uuid()); Hotkey(const Hotkey &o) = default; Hotkey() = default; - void add_watcher(caf::actor_addr); - [[nodiscard]] const std::string &hotkey_name() const { return name_; } [[nodiscard]] const std::string &hotkey_origin() const { return component_; } [[nodiscard]] const std::string &hotkey_description() const { return description_; } @@ -51,18 +50,35 @@ namespace ui { [[nodiscard]] int modifiers() const { return modifiers_; } [[nodiscard]] const utility::Uuid &uuid() const { return uuid_; } [[nodiscard]] std::string hotkey_sequence() const; + [[nodiscard]] const std::vector &watchers() const { return watchers_; } bool update(const Hotkey &o); + void add_watcher(caf::actor_addr w); + + void watcher_died(caf::actor_addr &watcher); + + // setting an exclusive watcher means it is the only actor to get the + // hotkey event. Hotkey events are not broadcast to 'regular' watchers + // or to the general event_group of the global keyboard_events actor. + // The most recent actor to set itself exclusive is the one that gets + // the events. + void exclusive_watcher(caf::actor_addr w, bool watch); + void update_state( const std::set ¤t_keys, const std::string &context, - const bool auto_repeat); + const std::string &window, + const bool auto_repeat, + caf::actor keypress_monitor); - static std::map key_names; + static void + sequence_to_key_and_modifier(const std::string &sequence, int &keycode, int &modifier); private: - void notifty_watchers(const std::string &context); + void notify( + const std::string &context, const std::string &window, caf::actor keypress_monitor); + void notify_watchers(const std::string &context, const std::string &window); int key_ = 0; int modifiers_ = 0; @@ -71,142 +87,143 @@ namespace ui { std::string name_; std::string component_; std::string description_; - std::string context_; bool auto_repeat_; - std::vector> watchers_; - }; + std::vector watchers_; + std::vector exclusive_watchers_; - // This is a straight clone of the Qt::Key enums but instead we provide string - // names for each key. The reason is that the actual key press event comes from - // qt and we pass the qt key ID - here in xSTUDIO backend we don't want - // any qt dependency hence this map. - inline std::map Hotkey::key_names = { - {0x01000000, "Escape"}, - {0x01000001, "Tab"}, - {0x01000002, "Backtab"}, - {0x01000003, "Backspace"}, - {0x01000004, "Return"}, - {0x01000005, "Enter"}, - {0x01000006, "Insert"}, - {0x01000007, "Delete"}, - {0x01000008, "Pause"}, - {0x01000009, "Print"}, - {0x0100000a, "SysReq"}, - {0x0100000b, "Clear"}, - {0x01000010, "Home"}, - {0x01000011, "End"}, - {0x01000012, "Left"}, - {0x01000013, "Up"}, - {0x01000014, "Right"}, - {0x01000015, "Down"}, - {0x01000016, "PageUp"}, - {0x01000017, "PageDown"}, - {0x01000020, "Shift"}, - {0x01000021, "Control"}, - {0x01000022, "Meta"}, - {0x01000023, "Alt"}, - {0x01001103, "AltGr"}, - {0x01000024, "CapsLock"}, - {0x01000025, "NumLock"}, - {0x01000026, "ScrollLock"}, - {0x01000030, "F1"}, - {0x01000031, "F2"}, - {0x01000032, "F3"}, - {0x01000033, "F4"}, - {0x01000034, "F5"}, - {0x01000035, "F6"}, - {0x01000036, "F7"}, - {0x01000037, "F8"}, - {0x01000038, "F9"}, - {0x01000039, "F10"}, - {0x0100003a, "F11"}, - {0x0100003b, "F12"}, - {0x0100003c, "F13"}, - {0x0100003d, "F14"}, - {0x0100003e, "F15"}, - {0x20, "Space Bar"}, - {0x21, "Exclam"}, - {0x22, "\""}, - {0x23, "#"}, - {0x24, "$"}, - {0x25, "%"}, - {0x26, "&"}, - {0x27, "'"}, - {0x28, "("}, - {0x29, ")"}, - {0x2a, "*"}, - {0x2b, "+"}, - {0x2c, ","}, - {0x2d, "-"}, - {0x2e, "."}, - {0x2f, "/"}, - {0x30, "0"}, - {0x31, "1"}, - {0x32, "2"}, - {0x33, "3"}, - {0x34, "4"}, - {0x35, "5"}, - {0x36, "6"}, - {0x37, "7"}, - {0x38, "8"}, - {0x39, "9"}, - {0x3a, ":"}, - {0x3b, ";"}, - {0x3c, "<"}, - {0x3d, "="}, - {0x3e, ">"}, - {0x3f, "?"}, - {0x40, "@"}, - {0x41, "A"}, - {0x42, "B"}, - {0x43, "C"}, - {0x44, "D"}, - {0x45, "E"}, - {0x46, "F"}, - {0x47, "G"}, - {0x48, "H"}, - {0x49, "I"}, - {0x4a, "J"}, - {0x4b, "K"}, - {0x4c, "L"}, - {0x4d, "M"}, - {0x4e, "N"}, - {0x4f, "O"}, - {0x50, "P"}, - {0x51, "Q"}, - {0x52, "R"}, - {0x53, "S"}, - {0x54, "T"}, - {0x55, "U"}, - {0x56, "V"}, - {0x57, "W"}, - {0x58, "X"}, - {0x59, "Y"}, - {0x5a, "Z"}, - {0x5b, "["}, - {0x5c, "\\"}, - {0x5d, "]"}, - {0x5f, "_"}, - {0x60, "`"}, - {0x7b, "{"}, - //{0x7c - {0x7d, "}"}, - {0x7e, "~"}, - {93, "numpad 0"}, - {96, "numpad 1"}, - {97, "numpad 2"}, - {98, "numpad 3"}, - {99, "numpad 4"}, - {100, "numpad 5"}, - {101, "numpad 6"}, - {102, "numpad 7"}, - {103, "numpad 8"}, - {104, "numpad 9"}, - {105, "numpad multiply"}, - {106, "numpad add"}, - {107, "numpad subtract"}, - {109, "numpad decimal point"}, - {110, "numpad divide"}}; + public: + // This is a straight clone of the Qt::Key enums but instead we provide string + // names for each key. The reason is that the actual key press event comes from + // qt and we pass the qt key ID - here in xSTUDIO backend we don't want + // any qt dependency hence this map. + inline static const std::map key_names = { + {0x01000000, "Escape"}, + {0x01000001, "Tab"}, + {0x01000002, "Backtab"}, + {0x01000003, "Backspace"}, + {0x01000004, "Return"}, + {0x01000005, "Enter"}, + {0x01000006, "Insert"}, + {0x01000007, "Delete"}, + {0x01000008, "Pause"}, + {0x01000009, "Print"}, + {0x0100000a, "SysReq"}, + {0x0100000b, "Clear"}, + {0x01000010, "Home"}, + {0x01000011, "End"}, + {0x01000012, "Left"}, + {0x01000013, "Up"}, + {0x01000014, "Right"}, + {0x01000015, "Down"}, + {0x01000016, "PageUp"}, + {0x01000017, "PageDown"}, + {0x01000020, "Shift"}, + {0x01000021, "Control"}, + {0x01000022, "Meta"}, + {0x01000023, "Alt"}, + {0x01001103, "AltGr"}, + {0x01000024, "CapsLock"}, + {0x01000025, "NumLock"}, + {0x01000026, "ScrollLock"}, + {0x01000030, "F1"}, + {0x01000031, "F2"}, + {0x01000032, "F3"}, + {0x01000033, "F4"}, + {0x01000034, "F5"}, + {0x01000035, "F6"}, + {0x01000036, "F7"}, + {0x01000037, "F8"}, + {0x01000038, "F9"}, + {0x01000039, "F10"}, + {0x0100003a, "F11"}, + {0x0100003b, "F12"}, + {0x0100003c, "F13"}, + {0x0100003d, "F14"}, + {0x0100003e, "F15"}, + {0x20, "Space Bar"}, + {0x21, "Exclam"}, + {0x22, "\""}, + {0x23, "#"}, + {0x24, "$"}, + {0x25, "%"}, + {0x26, "&"}, + {0x27, "'"}, + {0x28, "("}, + {0x29, ")"}, + {0x2a, "*"}, + {0x2b, "+"}, + {0x2c, ","}, + {0x2d, "-"}, + {0x2e, "."}, + {0x2f, "/"}, + {0x30, "0"}, + {0x31, "1"}, + {0x32, "2"}, + {0x33, "3"}, + {0x34, "4"}, + {0x35, "5"}, + {0x36, "6"}, + {0x37, "7"}, + {0x38, "8"}, + {0x39, "9"}, + {0x3a, ":"}, + {0x3b, ";"}, + {0x3c, "<"}, + {0x3d, "="}, + {0x3e, ">"}, + {0x3f, "?"}, + {0x40, "@"}, + {0x41, "A"}, + {0x42, "B"}, + {0x43, "C"}, + {0x44, "D"}, + {0x45, "E"}, + {0x46, "F"}, + {0x47, "G"}, + {0x48, "H"}, + {0x49, "I"}, + {0x4a, "J"}, + {0x4b, "K"}, + {0x4c, "L"}, + {0x4d, "M"}, + {0x4e, "N"}, + {0x4f, "O"}, + {0x50, "P"}, + {0x51, "Q"}, + {0x52, "R"}, + {0x53, "S"}, + {0x54, "T"}, + {0x55, "U"}, + {0x56, "V"}, + {0x57, "W"}, + {0x58, "X"}, + {0x59, "Y"}, + {0x5a, "Z"}, + {0x5b, "["}, + {0x5c, "\\"}, + {0x5d, "]"}, + {0x5f, "_"}, + {0x60, "`"}, + {0x7b, "{"}, + //{0x7c + {0x7d, "}"}, + {0x7e, "~"}, + {93, "numpad 0"}, + {96, "numpad 1"}, + {97, "numpad 2"}, + {98, "numpad 3"}, + {99, "numpad 4"}, + {100, "numpad 5"}, + {101, "numpad 6"}, + {102, "numpad 7"}, + {103, "numpad 8"}, + {104, "numpad 9"}, + {105, "numpad multiply"}, + {106, "numpad add"}, + {107, "numpad subtract"}, + {109, "numpad decimal point"}, + {110, "numpad divide"}}; + }; } // namespace ui } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/model_data/model_data_actor.hpp b/include/xstudio/ui/model_data/model_data_actor.hpp index 91f509932..e2e057105 100644 --- a/include/xstudio/ui/model_data/model_data_actor.hpp +++ b/include/xstudio/ui/model_data/model_data_actor.hpp @@ -71,10 +71,13 @@ namespace ui { const utility::Uuid &attribute_uuid, caf::actor client); + void stop_watching_model(const std::string &model_name, caf::actor client); + void register_model( const std::string &model_name, const utility::JsonStore &model_data, caf::actor client, + bool force_resend = false, const std::string &preference_path = std::string()); void check_model_is_registered( @@ -95,12 +98,16 @@ namespace ui { int count, caf::actor requester = caf::actor()); - void push_to_prefs(const std::string &model_name, const bool actually_push = false); - void remove_attribute_from_model( const std::string &model_name, const utility::Uuid &attr_uuid); - void node_activated(const std::string &model_name, const std::string &path); + void push_to_prefs(const std::string &model_name, const bool actually_push = false); + + void node_activated( + const std::string &model_name, + const std::string &path, + const std::string &user_data, + const bool from_hotkey); void insert_into_menu_model( const std::string &model_name, @@ -112,6 +119,28 @@ namespace ui { void broadcast_whole_model_data(const std::string &model_name); + void set_menu_node_position( + const std::string &model_name, + const std::string &menu_path, + const float position_in_menu); + + void update_hotkeys_model_data(const Hotkey &hotkey); + + void hotkey_pressed( + const utility::Uuid &hotkey_uuid, + const std::string &context, + const std::string &window); + + void + reset_model(const std::string &model_name, const std::string &preferences_path); + + void monitor_item(const caf::actor &actor); + + void reset_model( + const std::string &model_name, + const utility::JsonStore &data, + caf::actor excluded_client); + struct ModelData { ModelData() = default; ModelData(const ModelData &o) = default; @@ -126,14 +155,23 @@ namespace ui { clients_.push_back(client); } std::string name_; + std::string sort_key_; utility::JsonTree data_; std::string preference_path_; std::vector clients_; std::map> menu_watchers_; bool pending_prefs_update_ = {false}; + std::set hotkeys_; }; typedef std::shared_ptr ModelDataPtr; + typedef std::vector ModelDataVec; + + ModelDataVec get_menu_models(const std::string model_name); + + void do_ordering( + utility::JsonTree *node, + const std::string &ordering_key = "menu_item_position"); std::map models_; std::set models_to_be_fully_broadcasted_; diff --git a/include/xstudio/ui/mouse.hpp b/include/xstudio/ui/mouse.hpp index a07ceef97..b001f61cf 100644 --- a/include/xstudio/ui/mouse.hpp +++ b/include/xstudio/ui/mouse.hpp @@ -7,13 +7,15 @@ #include #include "xstudio/utility/chrono.hpp" +#include "xstudio/ui/enums.hpp" namespace xstudio { namespace ui { + // enum class EventType { Move, Drag, ButtonDown, ButtonRelease, MouseWheel, DoubleClick }; + struct Signature { - enum class EventType { Move, Drag, ButtonDown, ButtonRelease, MouseWheel, DoubleClick }; enum Button { None = 0, @@ -91,7 +93,7 @@ namespace ui { PointerEvent(const PointerEvent &o) = default; PointerEvent( - Signature::EventType t = Signature::EventType::Move, + EventType t = EventType::Move, Signature::Button b = Signature::Button::None, int x = 0, int y = 0, @@ -157,7 +159,7 @@ namespace ui { [[nodiscard]] int height() const { return height_; } [[nodiscard]] std::pair angle_delta() const { return angle_delta_; } [[nodiscard]] std::pair pixel_delta() const { return pixel_delta_; } - [[nodiscard]] Signature::EventType type() const { return signature_.type_; } + [[nodiscard]] EventType type() const { return signature_.type_; } [[nodiscard]] int buttons() const { return signature_.buttons_; } [[nodiscard]] int modifiers() const { return signature_.modifiers_; } [[nodiscard]] const Signature &signature() const { return signature_; } diff --git a/include/xstudio/ui/opengl/gl_debug_utils.hpp b/include/xstudio/ui/opengl/gl_debug_utils.hpp new file mode 100644 index 000000000..4f45e53af --- /dev/null +++ b/include/xstudio/ui/opengl/gl_debug_utils.hpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +namespace xstudio { +namespace ui { + namespace opengl { + + // reads the entire GL viewport into a buffer and writes out + // to EXR in /user_data/.tmp/xstudio_viewport.%04d.exr + // incrementing frame number on every draw from 1 + void grab_framebuffer_to_disk(); + + void debug_message_callback( + GLenum source, + GLenum type, + GLuint id, + GLenum severity, + GLsizei length, + const GLchar *msg, + const void *data); + + } // namespace opengl +} // namespace ui +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/opengl/opengl_canvas_renderer.hpp b/include/xstudio/ui/opengl/opengl_canvas_renderer.hpp index a851e9f5a..afe03bf0f 100644 --- a/include/xstudio/ui/opengl/opengl_canvas_renderer.hpp +++ b/include/xstudio/ui/opengl/opengl_canvas_renderer.hpp @@ -6,6 +6,7 @@ #include "xstudio/ui/opengl/shader_program_base.hpp" #include "xstudio/ui/opengl/opengl_caption_renderer.hpp" #include "xstudio/ui/opengl/opengl_stroke_renderer.hpp" +#include "xstudio/ui/opengl/opengl_shape_renderer.hpp" #include "xstudio/ui/canvas/canvas.hpp" @@ -24,7 +25,8 @@ namespace ui { const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, - const bool have_alpha_buffer); + const bool have_alpha_buffer, + const float image_aspectratio); private: template @@ -46,8 +48,9 @@ namespace ui { private: std::unique_ptr stroke_renderer_; std::unique_ptr caption_renderer_; + std::unique_ptr shape_renderer_; }; } // end namespace opengl } // end namespace ui -} // end namespace xstudio \ No newline at end of file +} // end namespace xstudio diff --git a/include/xstudio/ui/opengl/opengl_colour_lut_texture.hpp b/include/xstudio/ui/opengl/opengl_colour_lut_texture.hpp new file mode 100644 index 000000000..764e036a4 --- /dev/null +++ b/include/xstudio/ui/opengl/opengl_colour_lut_texture.hpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +// clang-format off +#ifdef __apple__ +#include +#else +#include +#include +#endif +// clang-format on + +#include "opengl_texture_base.hpp" + +// #define USE_SSBO + +namespace xstudio { +namespace ui { + namespace opengl { + + class GLColourLutTexture { + + public: + GLColourLutTexture( + const colour_pipeline::LUTDescriptor desc, const std::string texture_name); + virtual ~GLColourLutTexture(); + + void bind(int tex_index); + void release(); + void upload_texture_data(const colour_pipeline::ColourLUTPtr &lut); + + [[nodiscard]] GLuint texture_id() const { return tex_id_; } + [[nodiscard]] GLenum target() const; + [[nodiscard]] const std::string &texture_name() const { return texture_name_; } + + private: + GLint interpolation(); + GLint internal_format(); + GLint data_type(); + GLint format(); + + GLuint tex_id_ = {0}; + GLuint pbo_ = {0}; + const colour_pipeline::LUTDescriptor descriptor_; + std::size_t lut_cache_id_; + const std::string texture_name_; + }; + } // namespace opengl +} // namespace ui +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/opengl/opengl_multi_buffered_texture.hpp b/include/xstudio/ui/opengl/opengl_multi_buffered_texture.hpp new file mode 100644 index 000000000..8d0ec6bd3 --- /dev/null +++ b/include/xstudio/ui/opengl/opengl_multi_buffered_texture.hpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "opengl_texture_base.hpp" +#include "xstudio/media_reader/image_buffer_set.hpp" + +namespace xstudio { +namespace ui { + namespace opengl { + + class TextureTransferWorker; + + class GLDoubleBufferedTexture { + + public: + typedef std::shared_ptr GLBlindTexturePtr; + + template + static GLDoubleBufferedTexture *create(const int num_textures) { + // we are creating 'num_textures' textures. This allows us to do asynchronous + // uploads, so that some of the textures can start uploading pixel data for + // upcoming redraws while others are being used to draw the current frame. + // TODO: some rigorous testing for best number of textures and upload threads. + // Also provide user preferences to manually tweak these settings if needed. + auto result = new GLDoubleBufferedTexture(); + for (int i = 0; i < num_textures; ++i) { + result->textures_.emplace_back(new TexType()); + } + return result; + } + + virtual ~GLDoubleBufferedTexture() = default; + + void bind(const media_reader::ImageBufPtr &image, int &tex_index, Imath::V2i &dims); + + void release(const media_reader::ImageBufPtr &image); + + void queue_image_set_for_upload(const media_reader::ImageBufDisplaySetPtr &images); + + private: + GLDoubleBufferedTexture(); + + GLBlindTexturePtr get_free_texture(); + + class TexSet : public std::vector { + public: + TexSet::iterator find(const media_reader::ImageBufPtr &image) { + return std::find_if(begin(), end(), [=](const auto &t) { + return t->media_key() == image->media_key(); + }); + } + TexSet::iterator find_pending(const media_reader::ImageBufPtr &image) { + return std::find_if(begin(), end(), [=](const auto &t) { + return t->pending_media_key() == image->media_key(); + }); + } + }; + + void queue_for_upload( + TexSet &available_textures, const media_reader::ImageBufPtr &image); + + TexSet textures_; + + std::deque image_queue_; + + TextureTransferWorker *worker_; + }; + } // namespace opengl +} // namespace ui +} // namespace xstudio diff --git a/include/xstudio/ui/opengl/opengl_offscreen_renderer.hpp b/include/xstudio/ui/opengl/opengl_offscreen_renderer.hpp index 1d09c7c91..8a32fd713 100644 --- a/include/xstudio/ui/opengl/opengl_offscreen_renderer.hpp +++ b/include/xstudio/ui/opengl/opengl_offscreen_renderer.hpp @@ -35,6 +35,7 @@ namespace ui { unsigned int rbo_id_{0}; unsigned int fbo_id_{0}; + GLint restore_fbo_id_{0}; std::array vp_state_; }; diff --git a/include/xstudio/ui/opengl/opengl_rgba8bit_image_texture.hpp b/include/xstudio/ui/opengl/opengl_rgba8bit_image_texture.hpp new file mode 100644 index 000000000..56874d1f0 --- /dev/null +++ b/include/xstudio/ui/opengl/opengl_rgba8bit_image_texture.hpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +// clang-format off +#ifdef __apple__ +#include +#else +#include +#include +#endif +// clang-format on + +#include "opengl_texture_base.hpp" + +// #define USE_SSBO + +namespace xstudio { +namespace ui { + namespace opengl { + + class GLBlindRGBA8bitTex : public GLBlindTex { + + public: + GLBlindRGBA8bitTex() = default; + virtual ~GLBlindRGBA8bitTex(); + + uint8_t *map_buffer_for_upload(const size_t buffer_size) override; + void __bind(int tex_index, Imath::V2i &dims) override; + + private: + bool resize(const size_t required_size_bytes); + void pixel_upload(); + + [[nodiscard]] size_t tex_size_bytes() const override { + return tex_width_ * tex_height_ * bytes_per_pixel_; + } + + GLuint bytes_per_pixel_ = {0}; + GLuint tex_id_ = {0}; + GLuint pixel_buf_object_id_ = {0}; + + uint8_t *mapped_address_ = {nullptr}; + + int tex_width_ = {0}; + int tex_height_ = {0}; + }; + + } // namespace opengl +} // namespace ui +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/opengl/opengl_shape_renderer.hpp b/include/xstudio/ui/opengl/opengl_shape_renderer.hpp new file mode 100644 index 000000000..69410eca3 --- /dev/null +++ b/include/xstudio/ui/opengl/opengl_shape_renderer.hpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +// clang-format off +#include +#include +// clang-format on + +#include "xstudio/ui/opengl/shader_program_base.hpp" +#include "xstudio/ui/canvas/shapes.hpp" + +namespace { +struct GLQuad; +struct GLEllipse; +struct GLPolygon; +struct GLPoint; +} // namespace + +namespace xstudio { +namespace ui { + namespace opengl { + + class OpenGLShapeRenderer { + public: + ~OpenGLShapeRenderer(); + + void render_shapes( + const std::vector &quads, + const std::vector &polygons, + const std::vector &ellipses, + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + float viewport_du_dx, + float image_aspectratio); + + private: + void init_gl(); + void cleanup_gl(); + +// if we are GL 4.2 or above, we can use SSBO approach. +#ifdef __OPENGL_4_1__ + void upload_shape_data_to_tex(const std::vector &data_buf); + GLuint tex_id_; + GLuint vao_, vbo_; +#else + void upload_ssbo( + const std::vector &quads, + const std::vector &ellipses, + const std::vector &polygons, + const std::vector &points); + std::array ssbo_id_{0, 0, 0, 0}; +#endif + + std::unique_ptr shader_; + }; + + } // namespace opengl +} // namespace ui +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/opengl/opengl_ssbo_image_texture.hpp b/include/xstudio/ui/opengl/opengl_ssbo_image_texture.hpp new file mode 100644 index 000000000..c9043deae --- /dev/null +++ b/include/xstudio/ui/opengl/opengl_ssbo_image_texture.hpp @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +// SSBO not supported on MacOS +#ifndef __OPENGL_4_1__ +// clang-format off +#include +#include +// clang-format on + +#include "opengl_texture_base.hpp" + +// #define USE_SSBO + +namespace xstudio { +namespace ui { + namespace opengl { + + class GLSsboTex : public GLBlindTex { + + public: + GLSsboTex() = default; + virtual ~GLSsboTex(); + + uint8_t *map_buffer_for_upload(const size_t buffer_size) override; + void __bind(int /*tex_index*/, Imath::V2i & /*dims*/) override; + + private: + void compute_size(const size_t required_size_bytes); + void pixel_upload(); + void wait_on_upload(); + + GLuint ssbo_id_ = {0}; + GLuint bytes_per_pixel_ = 4; + + [[nodiscard]] size_t tex_size_bytes() const override { return tex_data_size_; } + + size_t tex_data_size_ = {0}; + }; + + } // namespace opengl +} // namespace ui +} // namespace xstudio + +#endif diff --git a/include/xstudio/ui/opengl/opengl_stroke_renderer.hpp b/include/xstudio/ui/opengl/opengl_stroke_renderer.hpp index ed0721b8a..e35135ea1 100644 --- a/include/xstudio/ui/opengl/opengl_stroke_renderer.hpp +++ b/include/xstudio/ui/opengl/opengl_stroke_renderer.hpp @@ -29,15 +29,12 @@ namespace ui { private: void init_gl(); void cleanup_gl(); - void resize_ssbo(std::size_t size); - void upload_ssbo(const std::vector &points); - const void *last_data_{nullptr}; + GLuint vbo_; + GLuint vao_; + const void *last_data_{nullptr}; std::unique_ptr shader_; - GLuint ssbo_id_{0}; - GLuint ssbo_size_{0}; - std::size_t ssbo_data_hash_{0}; }; } // namespace opengl diff --git a/include/xstudio/ui/opengl/opengl_text_rendering.hpp b/include/xstudio/ui/opengl/opengl_text_rendering.hpp index 929f5cf2f..698179543 100644 --- a/include/xstudio/ui/opengl/opengl_text_rendering.hpp +++ b/include/xstudio/ui/opengl/opengl_text_rendering.hpp @@ -50,7 +50,7 @@ namespace ui { const float opacity) const override; private: - unsigned int vao_, vbo_, texture_; + unsigned int vao_, vbo_, texture_, buf_size_; std::unique_ptr shader_; }; diff --git a/include/xstudio/ui/opengl/opengl_texture_base.hpp b/include/xstudio/ui/opengl/opengl_texture_base.hpp new file mode 100644 index 000000000..05389b31e --- /dev/null +++ b/include/xstudio/ui/opengl/opengl_texture_base.hpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +// clang-format off +#ifdef __apple__ +#include +#else +#include +#include +#endif +// clang-format on + +#include "xstudio/media_reader/media_reader.hpp" +#include "xstudio/colour_pipeline/colour_pipeline.hpp" +#include "xstudio/utility/uuid.hpp" + +// #define USE_SSBO + +namespace xstudio { +namespace ui { + namespace opengl { + + class GLBlindTex { + + public: + GLBlindTex(); + ~GLBlindTex(); + + void release(); + + virtual void bind(int tex_index, Imath::V2i &dims) { + wait_on_upload_pixels(); + __bind(tex_index, dims); + } + + + [[nodiscard]] const media::MediaKey &pending_media_key() const { + return pending_media_key_; + } + [[nodiscard]] const media::MediaKey &media_key() const { return media_key_; } + [[nodiscard]] const utility::time_point &when_last_used() const { + return when_last_used_; + } + + void prepare_for_upload(const media_reader::ImageBufPtr &frame); + + void do_pixel_upload(); + + void cancel_upload(); + + protected: + virtual uint8_t *map_buffer_for_upload(const size_t buffer_size) = 0; + virtual void __bind(int tex_index, Imath::V2i &dims) = 0; + virtual size_t tex_size_bytes() const = 0; + + void wait_on_upload_pixels(); + + utility::time_point when_last_used_; + + media::MediaKey media_key_; + media_reader::ImageBufPtr source_frame_; + + media::MediaKey pending_media_key_; + media_reader::ImageBufPtr pending_source_frame_; + + uint8_t *gpu_mapped_mem_ = nullptr; + + std::thread upload_thread_; + std::mutex mutex_; + std::condition_variable cv_; + bool pending_upload_ = {false}; + bool in_progress_ = {false}; + }; + } // namespace opengl +} // namespace ui +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/opengl/opengl_viewport_renderer.hpp b/include/xstudio/ui/opengl/opengl_viewport_renderer.hpp index 123924a51..2149b9758 100644 --- a/include/xstudio/ui/opengl/opengl_viewport_renderer.hpp +++ b/include/xstudio/ui/opengl/opengl_viewport_renderer.hpp @@ -6,6 +6,7 @@ #include #include "xstudio/colour_pipeline/colour_pipeline.hpp" +#include "xstudio/media_reader/image_buffer_set.hpp" #include "xstudio/ui/viewport/viewport.hpp" #include "xstudio/utility/uuid.hpp" #include @@ -18,6 +19,7 @@ namespace ui { class GLDoubleBufferedTexture; class GLColourLutTexture; class GLShaderProgram; + class OpenGLTextRendererSDF; typedef std::shared_ptr GLShaderProgramPtr; @@ -42,52 +44,99 @@ namespace ui { class OpenGLViewportRenderer : public viewport::ViewportRenderer { public: - OpenGLViewportRenderer(const int viewer_index, const bool gl_context_shared); + OpenGLViewportRenderer( + const std::string &window_id, const utility::JsonStore &prefs); - ~OpenGLViewportRenderer() override = default; + virtual ~OpenGLViewportRenderer() override; void render( - const std::vector &next_images, - const Imath::M44f &to_scene_matrix, - const Imath::M44f &projection_matrix, - const Imath::M44f &fit_mode_matrix) override; + const media_reader::ImageBufDisplaySetPtr &images, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_matrix, + const Imath::V2i &window_size, + const std::map + &overlay_renderers) override; + + virtual void draw_image( + const media_reader::ImageBufPtr &image_to_be_drawn, + const media_reader::ImageSetLayoutDataPtr &layout_data, + const int index, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx); + + protected: + const std::array &viewport_coords_in_window() { + return viewport_coords_in_window_; + } + + void __draw_image( + const media_reader::ImageBufDisplaySetPtr &all_images, + const int index, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx); + + void __draw_per_image_overlays( + const media_reader::ImageBufDisplaySetPtr &all_images, + const int index, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx, + const std::map + &overlay_renderers); - utility::JsonStore default_prefs() override; - - void set_prefs(const utility::JsonStore &prefs) override; - - private: void pre_init() override; bool activate_shader( const viewport::GPUShaderPtr &image_buffer_unpack_shader, const std::vector &operations); - void - upload_image_and_colour_data(std::vector &next_images); - void bind_textures(); + void upload_image_and_colour_data(const media_reader::ImageBufPtr &image); + void bind_textures(const media_reader::ImageBufPtr &image); void release_textures(); - void clear_viewport_area(const Imath::M44f &to_scene_matrix); + void clear_viewport_area( + const Imath::M44f &to_scene_matrix, const Imath::V2i &window_size); typedef std::shared_ptr GLTexturePtr; - std::vector textures_; - - std::map programs_; - - ColourPipeLutCollection colour_pipe_textures_; - - unsigned int vbo_, vao_; + struct SharedResources { + std::vector textures_; + std::map programs_; + ColourPipeLutCollection colour_pipe_textures_; + std::unique_ptr text_renderer_; + unsigned int vbo_ = 0; + unsigned int vao_ = 0; + GLShaderProgramPtr no_image_shader_program_; + void init(); + SharedResources(const utility::JsonStore &prefs); + ~SharedResources(); + bool use_ssbo = false; + int num_textures = {4}; + }; + + typedef std::shared_ptr SharedResourcesPtr; + std::vector &textures() { return resources_->textures_; } + std::map &shader_programs() { + return resources_->programs_; + } + ColourPipeLutCollection &colour_pipe_textures() { + return resources_->colour_pipe_textures_; + } + GLShaderProgramPtr &no_image_shader_program() { + return resources_->no_image_shader_program_; + } + unsigned int &vbo() { return resources_->vbo_; } + unsigned int &vao() { return resources_->vao_; } + + static std::map s_resources_store_; + + SharedResourcesPtr resources_; GLShaderProgramPtr active_shader_program_; - GLShaderProgramPtr no_image_shader_program_; std::string latest_colour_pipe_data_cacheid_; - bool gl_context_shared_; - bool use_ssbo_; - - media_reader::ImageBufPtr onscreen_frame_; - - int viewport_index_; - bool has_alpha_ = {false}; + const std::string window_id_; + bool use_ssbo_ = {false}; + std::array viewport_coords_in_window_; }; } // namespace opengl } // namespace ui diff --git a/include/xstudio/ui/opengl/shader_program_base.hpp b/include/xstudio/ui/opengl/shader_program_base.hpp index aef18fd1a..73338f011 100644 --- a/include/xstudio/ui/opengl/shader_program_base.hpp +++ b/include/xstudio/ui/opengl/shader_program_base.hpp @@ -4,8 +4,12 @@ // clang-format off #include #include +#ifdef __apple__ +#include +#else #include #include +#endif // clang-format on #include "xstudio/utility/json_store.hpp" @@ -46,7 +50,7 @@ namespace ui { void inject_colour_op_shader(const std::string &colour_op_shader); - void compile(); + void compile(const bool force_combine_frag_shaders = false); void use() const; void stop_using() const; void set_shader_parameters(const utility::JsonStore &shader_params); diff --git a/include/xstudio/ui/opengl/texture.hpp b/include/xstudio/ui/opengl/texture.hpp deleted file mode 100644 index 30b0671a4..000000000 --- a/include/xstudio/ui/opengl/texture.hpp +++ /dev/null @@ -1,158 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -// clang-format off -#include -#include -// clang-format on - -#include "xstudio/media_reader/media_reader.hpp" -#include "xstudio/colour_pipeline/colour_pipeline.hpp" -#include "xstudio/utility/uuid.hpp" - -// #define USE_SSBO - -namespace xstudio { -namespace ui { - namespace opengl { - - class GLBlindTex { - - public: - GLBlindTex() = default; - ~GLBlindTex(); - - void release(); - - virtual void map_buffer_for_upload(media_reader::ImageBufPtr &frame) = 0; - virtual void start_pixel_upload() = 0; - virtual void bind(int tex_index, Imath::V2i &dims) = 0; - - [[nodiscard]] media_reader::ImageBufPtr current_frame() const { - return current_source_frame_; - } - [[nodiscard]] const media::MediaKey &media_key() const { return media_key_; } - [[nodiscard]] const utility::time_point &when_last_used() const { - return when_last_used_; - } - - protected: - media::MediaKey media_key_; - - media_reader::ImageBufPtr new_source_frame_; - media_reader::ImageBufPtr current_source_frame_; - - uint8_t *buffer_io_ptr_ = {nullptr}; - std::thread upload_thread_; - std::mutex mutex_; - utility::time_point when_last_used_; - }; - - class GLSsboTex : public GLBlindTex { - - public: - GLSsboTex(); - virtual ~GLSsboTex(); - - void map_buffer_for_upload(media_reader::ImageBufPtr &frame) override; - void start_pixel_upload() override; - void bind(int /*tex_index*/, Imath::V2i & /*dims*/) override { wait_on_upload(); } - - private: - void compute_size(const size_t required_size_bytes); - void pixel_upload(); - void wait_on_upload(); - - GLuint ssbo_id_ = {0}; - GLuint bytes_per_pixel_ = 4; - - [[nodiscard]] size_t tex_size_bytes() const { return tex_data_size_; } - - size_t tex_data_size_ = {0}; - }; - - class GLBlindRGBA8bitTex : public GLBlindTex { - - public: - GLBlindRGBA8bitTex() = default; - virtual ~GLBlindRGBA8bitTex(); - - void map_buffer_for_upload(media_reader::ImageBufPtr &frame) override; - void start_pixel_upload() override; - void bind(int tex_index, Imath::V2i &dims) override; - - private: - void resize(const size_t required_size_bytes); - void pixel_upload(); - - [[nodiscard]] size_t tex_size_bytes() const { - return tex_width_ * tex_height_ * bytes_per_pixel_; - } - - GLuint bytes_per_pixel_ = {0}; - GLuint tex_id_ = {0}; - GLuint pixel_buf_object_id_ = {0}; - - int tex_width_ = {0}; - int tex_height_ = {0}; - }; - - - class GLDoubleBufferedTexture { - - public: - GLDoubleBufferedTexture(); - virtual ~GLDoubleBufferedTexture() = default; - - void bind(int &tex_index, Imath::V2i &dims); - void release(); - - void upload_next(std::vector); - - void set_use_ssbo(const bool using_ssbo); - - /*[[nodiscard]] int width() const ; - [[nodiscard]] int height() const;*/ - [[nodiscard]] media_reader::ImageBufPtr current_frame() const { - return current_ ? current_->current_frame() : media_reader::ImageBufPtr(); - } - - private: - typedef std::shared_ptr GLBlindTexturePtr; - GLBlindTexturePtr current_; - std::vector textures_; - media::MediaKey active_media_key_; - bool using_ssbo_ = {false}; - }; - - - class GLColourLutTexture { - - public: - GLColourLutTexture( - const colour_pipeline::LUTDescriptor desc, const std::string texture_name); - virtual ~GLColourLutTexture(); - - void bind(int tex_index); - void release(); - void upload_texture_data(const colour_pipeline::ColourLUTPtr &lut); - - [[nodiscard]] GLuint texture_id() const { return tex_id_; } - [[nodiscard]] GLenum target() const; - [[nodiscard]] const std::string &texture_name() const { return texture_name_; } - - private: - GLint interpolation(); - GLint internal_format(); - GLint data_type(); - GLint format(); - - GLuint tex_id_ = {0}; - GLuint pbo_ = {0}; - const colour_pipeline::LUTDescriptor descriptor_; - std::size_t lut_cache_id_; - const std::string texture_name_; - }; - } // namespace opengl -} // namespace ui -} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/qml/QTreeModelToTableModel.hpp b/include/xstudio/ui/qml/QTreeModelToTableModel.hpp new file mode 100644 index 000000000..83e6d4099 --- /dev/null +++ b/include/xstudio/ui/qml/QTreeModelToTableModel.hpp @@ -0,0 +1,212 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR +// GPL-3.0-only + +#ifndef QTreeModelToTableModel_H +#define QTreeModelToTableModel_H + +#include +#include +#include +#include +#include + +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/helper_qml_export.h" + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +// #include "qtqmlmodelsglobal_p.h" + +// #include +// #include +// #include +// #include + +// QT_BEGIN_NAMESPACE + +class QAbstractItemModel; + +class HELPER_QML_EXPORT QTreeModelToTableModel : public QAbstractItemModel { + Q_OBJECT + Q_PROPERTY(QAbstractItemModel *model READ model WRITE setModel NOTIFY modelChanged FINAL) + Q_PROPERTY(QModelIndex rootIndex READ rootIndex WRITE setRootIndex RESET resetRootIndex + NOTIFY rootIndexChanged FINAL) + Q_PROPERTY(int count READ length NOTIFY lengthChanged) + Q_PROPERTY(int length READ length NOTIFY lengthChanged) + + struct TreeItem; + + public: + explicit QTreeModelToTableModel(QObject *parent = nullptr); + + QAbstractItemModel *model() const; + QModelIndex rootIndex() const; + void setRootIndex(const QModelIndex &idx); + void resetRootIndex(); + + QModelIndex + index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + enum { + DepthRole = Qt::UserRole - 5, + ExpandedRole, + HasChildrenRole, + HasSiblingRole, + ModelIndexRole + }; + + QHash roleNames() const override; + QVariant data(const QModelIndex &, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + [[nodiscard]] int length() const { return rowCount(); } + + void clearModelData(); + + bool isVisible(const QModelIndex &index); + bool childrenVisible(const QModelIndex &index); + + Q_INVOKABLE QModelIndex mapToModel(const QModelIndex &index) const; + Q_INVOKABLE QModelIndex mapRowToModel(int row) const { return mapToModel(row); } + Q_INVOKABLE QModelIndex mapFromModel(const QModelIndex &index) const; + QModelIndex mapToModel(int row) const; + + Q_INVOKABLE QItemSelection + selectionForRowRange(const QModelIndex &fromIndex, const QModelIndex &toIndex) const; + + void showModelTopLevelItems(bool doInsertRows = true); + void showModelChildItems( + const TreeItem &parent, + int start, + int end, + bool doInsertRows = true, + bool doExpandPendingRows = true); + + int itemIndex(const QModelIndex &index) const; + void expandPendingRows(bool doInsertRows = true); + int lastChildIndex(const QModelIndex &index) const; + void removeVisibleRows(int startIndex, int endIndex, bool doRemoveRows = true); + + void dump() const; + bool testConsistency(bool dumpOnFail = false) const; + + using QAbstractItemModel::hasChildren; + + Q_SIGNALS: + void modelChanged(QAbstractItemModel *model); + void lengthChanged(); + void rootIndexChanged(); + void expanded(const QModelIndex &index); + void collapsed(const QModelIndex &index); + + public Q_SLOTS: + void expand(const QModelIndex &); + void collapse(const QModelIndex &); + void setModel(QAbstractItemModel *model); + bool isExpanded(const QModelIndex &) const; + bool isExpanded(int row) const; + bool hasChildren(int row) const; + bool hasSiblings(int row) const; + int depthAtRow(int row) const; + void expandRow(int n); + void expandRecursively(int row, int depth); + void collapseRow(int n); + void collapseRecursively(int row); + void expandAll(int depth); + + private Q_SLOTS: + void modelHasBeenDestroyed(); + void modelHasBeenReset(); + void modelDataChanged( + const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); + void modelLayoutAboutToBeChanged( + const QList &parents, QAbstractItemModel::LayoutChangeHint hint); + void modelLayoutChanged( + const QList &parents, QAbstractItemModel::LayoutChangeHint hint); + void modelRowsAboutToBeInserted(const QModelIndex &parent, int start, int end); + void modelRowsAboutToBeMoved( + const QModelIndex &sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex &destinationParent, + int destinationRow); + void modelRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); + void modelRowsInserted(const QModelIndex &parent, int start, int end); + void modelRowsMoved( + const QModelIndex &sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex &destinationParent, + int destinationRow); + void modelRowsRemoved(const QModelIndex &parent, int start, int end); + + private: + struct TreeItem { + QPersistentModelIndex index; + int depth; + bool expanded; + + explicit TreeItem(const QModelIndex &idx = QModelIndex(), int d = 0, int e = false) + : index(idx), depth(d), expanded(e) {} + + inline bool operator==(const TreeItem &other) const { + return this->index == other.index; + } + }; + + struct DataChangedParams { + QModelIndex topLeft; + QModelIndex bottomRight; + QVector roles; + }; + + struct SignalFreezer { + SignalFreezer(QTreeModelToTableModel *parent) : m_parent(parent) { + m_parent->enableSignalAggregation(); + } + ~SignalFreezer() { m_parent->disableSignalAggregation(); } + + private: + QTreeModelToTableModel *m_parent; + }; + + void enableSignalAggregation(); + void disableSignalAggregation(); + bool isAggregatingSignals() const { return m_signalAggregatorStack > 0; } + void queueDataChanged( + const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); + void emitQueuedSignals(); + + QPointer m_model = nullptr; + QPersistentModelIndex m_rootIndex; + QList m_items; + QSet m_expandedItems; + QList m_itemsToExpand; + mutable int m_lastItemIndex = 0; + bool m_visibleRowsMoved = false; + bool m_modelLayoutChanged = false; + int m_signalAggregatorStack = 0; + QVector m_queuedDataChanged; + int m_column = 0; + bool m_expanding_all = false; +}; + +// QT_END_NAMESPACE + +#endif // QTreeModelToTableModel_H diff --git a/include/xstudio/ui/qml/actor_object.hpp b/include/xstudio/ui/qml/actor_object.hpp index 2c5f6e943..f39463fdf 100644 --- a/include/xstudio/ui/qml/actor_object.hpp +++ b/include/xstudio/ui/qml/actor_object.hpp @@ -24,9 +24,15 @@ #include #include #include -#include "caf/detail/shared_spinlock.hpp" +// #include "caf/detail/shared_spinlock.hpp" -#include +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/helper_qml_export.h" + +// #include + +#include "xstudio/atoms.hpp" +#include "xstudio/utility/logging.hpp" CAF_PUSH_WARNINGS #include @@ -34,8 +40,6 @@ CAF_PUSH_WARNINGS #include CAF_POP_WARNINGS -#include "xstudio/atoms.hpp" -#include "xstudio/utility/logging.hpp" namespace caf::mixin { @@ -44,7 +48,7 @@ template (FIRST_CUSTOM_ID)> class actor_object : public Base { public: /// A shared lockable. - using lock_type = detail::shared_spinlock; + // using lock_type = detail::shared_spinlock; struct event_type : public QEvent { mailbox_element_ptr mptr; @@ -56,7 +60,7 @@ class actor_object : public Base { // TODO: Ahead This is a bad hack for windows to make it compile currently, possible // solution is to pass - // JsonTreeModel as a reference or a pointer. + // JsonTreeModel as a reference or a pointer. template < typename... Ts, std::enable_if_t<(std::is_move_constructible_v && ...), int> = 0> @@ -68,16 +72,19 @@ class actor_object : public Base { alive_ = false; this->disconnect(); if (companion_) - self()->cleanup(error{}, &execution_unit_); + self()->cleanup(error{}, &(sys_->scheduler())); + // if (companion_) + // self()->cleanup(error{}, &execution_unit_); } void init(actor_system &system) { + sys_ = &system; alive_ = true; - execution_unit_.system_ptr(&system); + // execution_unit_.system_ptr(&system); companion_ = actor_cast(system.spawn()); self()->on_enqueue([=](mailbox_element_ptr ptr) { - std::lock_guard guard(lock_); + // std::lock_guard guard(lock_); if (self() == nullptr or not alive_) { // spdlog::warn("{} companion is null on_enqueue", __PRETTY_FUNCTION__); @@ -91,7 +98,7 @@ class actor_object : public Base { }); self()->on_exit([=] { - spdlog::warn("{}", __PRETTY_FUNCTION__); + // spdlog::warn("{}", __PRETTY_FUNCTION__); // close widget if actor companion dies // this->close(); }); @@ -110,7 +117,7 @@ class actor_object : public Base { auto ptr = dynamic_cast(event); if (ptr && alive_) { try { - switch (self()->activate(&execution_unit_, *(ptr->mptr))) { + switch (self()->activate(&(sys_->scheduler()), *(ptr->mptr))) { case caf::scheduled_actor::activation_result::success: case caf::scheduled_actor::activation_result::terminated: case caf::scheduled_actor::activation_result::skipped: @@ -138,11 +145,32 @@ class actor_object : public Base { } private: - scoped_execution_unit execution_unit_; + actor_system *sys_ = nullptr; + // scoped_execution_unit execution_unit_; strong_actor_ptr companion_{nullptr}; bool alive_{false}; // guards access to handler_ - lock_type lock_; + // lock_type lock_; }; } // namespace caf::mixin + +namespace xstudio::ui::qml { +using namespace caf; + +class HELPER_QML_EXPORT QMLActor : public caf::mixin::actor_object { + Q_OBJECT + + public: + using super = caf::mixin::actor_object; + explicit QMLActor(QObject *parent = nullptr) : super(parent) {} + + virtual ~QMLActor() = default; + virtual void init(caf::actor_system &system) { super::init(system); } + + public: + caf::actor_system &system() const { + return const_cast(self())->home_system(); + } +}; +} // namespace xstudio::ui::qml diff --git a/include/xstudio/ui/qml/bookmark_model_ui.hpp b/include/xstudio/ui/qml/bookmark_model_ui.hpp index 79216f9e5..45acc0c38 100644 --- a/include/xstudio/ui/qml/bookmark_model_ui.hpp +++ b/include/xstudio/ui/qml/bookmark_model_ui.hpp @@ -56,17 +56,45 @@ class BOOKMARK_QML_EXPORT BookmarkFilterModel : public QSortFilterProxyModel { currentMediaChanged) Q_PROPERTY(int depth READ depth WRITE setDepth NOTIFY depthChanged) + Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged) + Q_PROPERTY( + QString showUserType READ showUserType WRITE setShowUserType NOTIFY showUserTypeChanged) + Q_PROPERTY(QStringList excludedCategories READ excludedCategories WRITE + setExcludedCategories NOTIFY excludedCategoriesChanged) + Q_PROPERTY(QStringList includedCategories READ includedCategories WRITE + setIncludedCategories NOTIFY includedCategoriesChanged) + Q_PROPERTY(bool sortbyCreated READ sortbyCreated WRITE setsortbyCreated NOTIFY + sortbyCreatedChanged) + Q_PROPERTY(int length READ length NOTIFY lengthChanged) + public: BookmarkFilterModel(QObject *parent = nullptr); + Q_INVOKABLE [[nodiscard]] QModelIndex + nextBookmark(const int mediaFrame, const QUuid &ownerUuid) const; + + Q_INVOKABLE [[nodiscard]] QModelIndex + previousBookmark(const int mediaFrame, const QUuid &ownerUuid) const; + [[nodiscard]] QVariantMap mediaOrder() const { return media_order_; } [[nodiscard]] int depth() const { return depth_; } [[nodiscard]] QVariant currentMedia() const { return QVariant::fromValue(current_media_); } + [[nodiscard]] bool showHidden() const { return showHidden_; } + [[nodiscard]] QString showUserType() const { return showUserType_; } + [[nodiscard]] QStringList excludedCategories() const { return excluded_categories_; } + [[nodiscard]] QStringList includedCategories() const { return included_categories_; } + [[nodiscard]] bool sortbyCreated() const { return sortbyCreated_; } + [[nodiscard]] int length() const { return rowCount(); } void setMediaOrder(const QVariantMap &mo); void setDepth(const int value); void setCurrentMedia(const QVariant &value); + void setShowHidden(const bool value); + void setShowUserType(const QString &value); + void setExcludedCategories(const QStringList value); + void setIncludedCategories(const QStringList value); + void setsortbyCreated(const bool value); Q_INVOKABLE bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override { @@ -87,12 +115,23 @@ class BOOKMARK_QML_EXPORT BookmarkFilterModel : public QSortFilterProxyModel { signals: void mediaOrderChanged(); void depthChanged(); + void lengthChanged(); void currentMediaChanged(); + void showHiddenChanged(); + void showUserTypeChanged(); + void excludedCategoriesChanged(); + void includedCategoriesChanged(); + void sortbyCreatedChanged(); private: QVariantMap media_order_; QUuid current_media_; int depth_{0}; + bool showHidden_{false}; + QString showUserType_; + QStringList excluded_categories_; + QStringList included_categories_; + bool sortbyCreated_{false}; }; @@ -105,31 +144,35 @@ class BOOKMARK_QML_EXPORT BookmarkModel : public caf::mixin::actor_object; @@ -163,6 +206,12 @@ class BOOKMARK_QML_EXPORT BookmarkModel : public caf::mixin::actor_object getJSONFuture(const QModelIndex &index, const QString &path) const; + Q_INVOKABLE QVariant getJSONObject(const QModelIndex &index, const QString &path) const { + return getJSONObjectFuture(index, path).result(); + } + Q_INVOKABLE QFuture + getJSONObjectFuture(const QModelIndex &index, const QString &path) const; + signals: void bookmarkActorAddrChanged(); void lengthChanged(); @@ -183,5 +232,7 @@ class BOOKMARK_QML_EXPORT BookmarkModel : public caf::mixin::actor_object bookmarks_; + std::map thumbnail_cache_; + std::set out_of_date_thumbnails_; }; } // namespace xstudio::ui::qml diff --git a/include/xstudio/ui/qml/caf_response_ui.hpp b/include/xstudio/ui/qml/caf_response_ui.hpp index 1546eeb73..a08cc924a 100644 --- a/include/xstudio/ui/qml/caf_response_ui.hpp +++ b/include/xstudio/ui/qml/caf_response_ui.hpp @@ -22,6 +22,7 @@ class CafResponse : public QObject { void received(QVariant, int, QPersistentModelIndex, int, QString); // Search value, search role, set role void finished(QVariant, int, int); + void started(QVariant, int, int); public: CafResponse( @@ -33,18 +34,10 @@ class CafResponse : public QObject { const std::string &role_name, QThreadPool *pool); - CafResponse( - const QVariant search_value, - const int search_role, - const QPersistentModelIndex search_hint, - const nlohmann::json &data, - int role, - const std::string &role_name, - const std::map &metadata_paths, - QThreadPool *pool); - + ~CafResponse(); private: + void handleStarted(); void handleFinished(); QFutureWatcher> watcher_; diff --git a/include/xstudio/ui/qml/conform_ui.hpp b/include/xstudio/ui/qml/conform_ui.hpp new file mode 100644 index 000000000..8ee2e1b27 --- /dev/null +++ b/include/xstudio/ui/qml/conform_ui.hpp @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/conform_qml_export.h" + +CAF_PUSH_WARNINGS +#include +#include +#include +#include +#include +CAF_POP_WARNINGS + +#include "xstudio/ui/qml/helper_ui.hpp" +#include "xstudio/ui/qml/json_tree_model_ui.hpp" + +namespace xstudio { +namespace ui { + namespace qml { + + class CONFORM_QML_EXPORT ConformEngineUI + : public caf::mixin::actor_object { + Q_OBJECT + + public: + using super = caf::mixin::actor_object; + enum Roles { nameRole = JSONTreeModel::Roles::LASTROLE }; + + ConformEngineUI(QObject *parent = nullptr); + ~ConformEngineUI() override = default; + + void init(caf::actor_system &system); + caf::actor_system &system() const { + return const_cast(self())->home_system(); + } + + [[nodiscard]] QVariant + data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + + Q_INVOKABLE QFuture> conformItemsFuture( + const QString &task, + const QModelIndex &container, + const QModelIndex &item, + const bool fanOut = false, + const bool removeSource = false) const; + + Q_INVOKABLE QFuture> conformToSequenceFuture( + const QModelIndex &playlistIndex, + const QModelIndexList &mediaIndexes, + const QModelIndex &sequenceIndex, + const QModelIndex &conformTrackIndex, + const bool replace, + const QString &newTrackName = "") const; + + Q_INVOKABLE QFuture> conformToNewSequenceFuture( + const QModelIndexList &mediaIndexes, + const QString &task = "", + const int siblings = -1, + const QModelIndex &playlistIndex = QModelIndex()) const; + + Q_INVOKABLE QFuture conformPrepareSequenceFuture( + const QModelIndex &sequenceIndex, const bool onlyCreateConfrom = true) const; + + Q_INVOKABLE QFuture> conformTracksToSequenceFuture( + const QModelIndexList &trackIndexes, const QModelIndex &sequenceIndex) const; + + Q_INVOKABLE QFuture> conformFindRelatedFuture( + const QString &key, + const QModelIndex &clipIndex, + const QModelIndex &sequenceIndex) const; + + signals: + + private: + QFuture> conformItemsFuture( + const std::string &task, + const utility::UuidActorVector &items, + const utility::UuidActor &playlist, + const utility::UuidActor &container, + const std::string &item_type, + const utility::UuidVector &before, + const bool removeSource, + const QPersistentModelIndex ¬ifyIndex = QPersistentModelIndex(), + const QUuid ¬ifyUuid = QUuid()) const; + + utility::Uuid conform_uuid_; + caf::actor conform_events_; + }; + + } // namespace qml +} // namespace ui +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/qml/embedded_python_ui.hpp b/include/xstudio/ui/qml/embedded_python_ui.hpp index 5166a46f6..cb08180e6 100644 --- a/include/xstudio/ui/qml/embedded_python_ui.hpp +++ b/include/xstudio/ui/qml/embedded_python_ui.hpp @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once - // include CMake auto-generated export hpp #include "xstudio/ui/qml/embedded_python_qml_export.h" @@ -9,140 +8,189 @@ #include // CAF_PUSH_WARNINGS -// #include -// #include -// #include +// #include +// #include +// #include +// #include // CAF_POP_WARNINGS #include "xstudio/ui/qml/helper_ui.hpp" +#include "xstudio/ui/qml/json_tree_model_ui.hpp" namespace xstudio { namespace ui { namespace qml { - // class Snippet { - - // }; + class EMBEDDED_PYTHON_QML_EXPORT SnippetFilterModel : public QSortFilterProxyModel { - class SnippetUI : public QObject { Q_OBJECT - Q_PROPERTY(QString name READ name NOTIFY nameChanged) - Q_PROPERTY(QString menuModelName READ menuModelName NOTIFY menuNameChanged) - Q_PROPERTY(QString script READ script NOTIFY scriptChanged) - Q_PROPERTY(QString description READ description NOTIFY descriptionChanged) + // Q_PROPERTY(int length READ length NOTIFY lengthChanged) + // Q_PROPERTY(int count READ length NOTIFY lengthChanged) + + Q_PROPERTY(QString snippetType READ snippetType WRITE setSnippetType NOTIFY + snippetTypeChanged) public: - explicit SnippetUI( - const QString menu_name, - const QString name, - const QString script, - QObject *parent = nullptr) - : QObject(parent), - menu_name_(std::move(menu_name)), - name_(std::move(name)), - script_(std::move(script)) {} - explicit SnippetUI(const utility::JsonStore &json, QObject *parent = nullptr); - - ~SnippetUI() override = default; - - [[nodiscard]] QString name() const { return name_; } - [[nodiscard]] QString menuModelName() const { return menu_name_; } - [[nodiscard]] QString script() const { return script_; } - [[nodiscard]] QString description() const { return description_; } + using super = QSortFilterProxyModel; - signals: - void nameChanged(); - void menuNameChanged(); - void scriptChanged(); - void descriptionChanged(); + SnippetFilterModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) { + // setDynamicSortFilter(true); + // sort(0); + } - private: - QString menu_name_ = {}; - QString name_ = {}; - QString script_ = {}; - QString description_ = {}; - }; - class SnippetMenuUI : public QObject { - Q_OBJECT - Q_PROPERTY(QString name READ name NOTIFY nameChanged) - Q_PROPERTY(QList snippets READ snippets NOTIFY snippetsChanged) + const QString &snippetType() const { return snippet_type_; } - public: - explicit SnippetMenuUI(const QString name, QObject *parent = nullptr) - : QObject(parent), name_(std::move(name)) {} - ~SnippetMenuUI() override = default; + void setSnippetType(const QString &value) { + if (value != snippet_type_) { + snippet_type_ = value; + emit snippetTypeChanged(); + invalidateFilter(); + } + } - [[nodiscard]] QString name() const { return name_; } - [[nodiscard]] QList snippets() const { return snippets_; } + // [[nodiscard]] int length() const { return rowCount(); } - void addSnippet(QObject *snippet) { - snippets_.push_back(snippet); - emit snippetsChanged(); - } + protected: + [[nodiscard]] bool + filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; signals: - void nameChanged(); - void snippetsChanged(); + void snippetTypeChanged(); + // void lengthChanged(); private: - QString name_; - QList snippets_; + QString snippet_type_; }; - class EMBEDDED_PYTHON_QML_EXPORT EmbeddedPythonUI : public QMLActor { + + class EMBEDDED_PYTHON_QML_EXPORT EmbeddedPythonUI + : public caf::mixin::actor_object { Q_OBJECT Q_PROPERTY(bool waiting READ waiting NOTIFY waitingChanged) - Q_PROPERTY( - QList snippetMenus READ snippetMenus NOTIFY snippetMenusChanged) - Q_PROPERTY(QList snippets READ snippets NOTIFY snippetsChanged) + Q_PROPERTY(QUuid sessionId READ sessionId NOTIFY sessionIdChanged) + + Q_PROPERTY(QObject *applicationMenuModel READ applicationMenuModel NOTIFY + applicationMenuModelChanged) + Q_PROPERTY(QObject *playlistMenuModel READ playlistMenuModel NOTIFY + playlistMenuModelChanged) + Q_PROPERTY(QObject *mediaMenuModel READ mediaMenuModel NOTIFY mediaMenuModelChanged) + Q_PROPERTY(QObject *sequenceMenuModel READ sequenceMenuModel NOTIFY + sequenceMenuModelChanged) + Q_PROPERTY(QObject *trackMenuModel READ trackMenuModel NOTIFY trackMenuModelChanged) + Q_PROPERTY(QObject *clipMenuModel READ clipMenuModel NOTIFY clipMenuModelChanged) public: + using super = caf::mixin::actor_object; + enum Roles { + nameRole = JSONTreeModel::Roles::LASTROLE, + menuPathRole, + scriptPathRole, + snippetTypeRole, + typeRole + }; + explicit EmbeddedPythonUI(QObject *parent = nullptr); ~EmbeddedPythonUI() override = default; - void init(caf::actor_system &system) override; + caf::actor_system &system() const { + return const_cast(self())->home_system(); + } + + void init(caf::actor_system &system); void set_backend(caf::actor backend); caf::actor backend() { return backend_; } [[nodiscard]] bool waiting() const { return waiting_; } + [[nodiscard]] QUuid sessionId() const { return QUuidFromUuid(event_uuid_); } - [[nodiscard]] QList snippetMenus() const { return snippet_menus_; } - [[nodiscard]] QList snippets() const { return snippets_; } + [[nodiscard]] QVariant + data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + [[nodiscard]] QObject *applicationMenuModel() { + if (not application_menu_model_) + application_menu_model_ = makeFilterModel("Application"); + return application_menu_model_; + } + + [[nodiscard]] QObject *playlistMenuModel() { + if (not playlist_menu_model_) + playlist_menu_model_ = makeFilterModel("Playlist"); + return playlist_menu_model_; + } + + [[nodiscard]] QObject *mediaMenuModel() { + if (not media_menu_model_) + media_menu_model_ = makeFilterModel("Media"); + return media_menu_model_; + } + + [[nodiscard]] QObject *sequenceMenuModel() { + if (not sequence_menu_model_) + sequence_menu_model_ = makeFilterModel("Sequence"); + return sequence_menu_model_; + } + + [[nodiscard]] QObject *trackMenuModel() { + if (not track_menu_model_) + track_menu_model_ = makeFilterModel("Track"); + return track_menu_model_; + } + + [[nodiscard]] QObject *clipMenuModel() { + if (not clip_menu_model_) + clip_menu_model_ = makeFilterModel("Clip"); + return clip_menu_model_; + } public slots: void pyEvalFile(const QUrl &path); - void pyExec(const QString &str); + void pyExec(const QString &str) const; QVariant pyEval(const QString &str); QUuid createSession(); bool sendInput(const QString &str); bool sendInterrupt(); - - // QVariant pyEval(const QString &str, const QVariant &locals); - // QVariant pyEvalLocals(const QString &str); - // QVariant pyEvalLocals(const QString &str, const QVariant &locals); - - void addSnippet(SnippetUI *snippet); + void reloadSnippets() const; + bool saveSnippet(const QUrl &path, const QString &content) const; signals: - void snippetMenusChanged(); - void snippetsChanged(); void waitingChanged(); void backendChanged(); + void sessionIdChanged(); void stdoutEvent(const QString &str); void stderrEvent(const QString &str); + void applicationMenuModelChanged(); + void playlistMenuModelChanged(); + void mediaMenuModelChanged(); + void sequenceMenuModelChanged(); + void trackMenuModelChanged(); + void clipMenuModelChanged(); + private: + SnippetFilterModel *makeFilterModel(const QString &filter) { + SnippetFilterModel *result = new SnippetFilterModel(this); + result->setSnippetType(filter); + result->setSourceModel(this); + return result; + } + caf::actor backend_; caf::actor backend_events_; bool waiting_{false}; - QList snippet_menus_; - QList snippets_; utility::Uuid event_uuid_; + + SnippetFilterModel *application_menu_model_{nullptr}; + SnippetFilterModel *playlist_menu_model_{nullptr}; + SnippetFilterModel *media_menu_model_{nullptr}; + SnippetFilterModel *sequence_menu_model_{nullptr}; + SnippetFilterModel *track_menu_model_{nullptr}; + SnippetFilterModel *clip_menu_model_{nullptr}; + + utility::Uuid snippet_uuid_{utility::Uuid::generate()}; }; } // namespace qml } // namespace ui -} // namespace xstudio +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/qml/event_ui.hpp b/include/xstudio/ui/qml/event_ui.hpp deleted file mode 100644 index caf5c9380..000000000 --- a/include/xstudio/ui/qml/event_ui.hpp +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - - -// include CMake auto-generated export hpp -#include "xstudio/ui/qml/event_qml_export.h" - -#include -#include - -CAF_PUSH_WARNINGS -#include -#include -#include -#include -CAF_POP_WARNINGS - -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/event/event.hpp" - -namespace xstudio { -namespace ui { - namespace qml { - - class EVENT_QML_EXPORT EventUI : public QObject { - Q_OBJECT - - Q_PROPERTY(int progress READ progress NOTIFY progressChanged) - Q_PROPERTY(int progressMinimum READ progressMinimum NOTIFY progressMinimumChanged) - Q_PROPERTY(int progressMaximum READ progressMaximum NOTIFY progressMaximumChanged) - Q_PROPERTY(float progressPercentage READ progressPercentage NOTIFY - progressPercentageChanged) - Q_PROPERTY(bool complete READ complete NOTIFY completeChanged) - Q_PROPERTY(QUuid event READ event NOTIFY eventChanged) - - Q_PROPERTY(QString text READ text NOTIFY textChanged) - Q_PROPERTY(QString textRange READ textRange NOTIFY textRangeChanged) - Q_PROPERTY(QString textPercentage READ textPercentage NOTIFY textPercentageChanged) - - public: - EventUI(const event::Event &event, QObject *parent = nullptr); - ~EventUI() override = default; - - void update(const event::Event &event); - - [[nodiscard]] int progress() const { return event_.progress(); } - [[nodiscard]] int progressMinimum() const { return event_.progress_minimum(); } - [[nodiscard]] int progressMaximum() const { return event_.progress_maximum(); } - [[nodiscard]] float progressPercentage() const { - return event_.progress_percentage(); - } - [[nodiscard]] bool complete() const { return event_.complete(); } - [[nodiscard]] QUuid event() const { return QUuidFromUuid(event_.event()); } - - [[nodiscard]] QString text() const { - return QStringFromStd(event_.progress_text()); - } - [[nodiscard]] QString textRange() const { - return QStringFromStd(event_.progress_text_range()); - } - [[nodiscard]] QString textPercentage() const { - return QStringFromStd(event_.progress_text_percentage()); - } - - signals: - void progressChanged(); - void progressMinimumChanged(); - void progressMaximumChanged(); - void progressPercentageChanged(); - void completeChanged(); - void eventChanged(); - void textChanged(); - void textRangeChanged(); - void textPercentageChanged(); - - private: - event::Event event_; - }; - - class EVENT_QML_EXPORT EventAttrs : public QQmlPropertyMap { - - Q_OBJECT - - public: - EventAttrs(QObject *parent = nullptr); - ~EventAttrs() override = default; - void addEvent(const event::Event &); - }; - - class EVENT_QML_EXPORT EventManagerUI : public QMLActor { - - Q_OBJECT - - public: - EventManagerUI(EventAttrs *attrs_map); - ~EventManagerUI() override = default; - - void init(caf::actor_system &system) override; - - private: - EventAttrs *attrs_map_ = {nullptr}; - }; - - } // namespace qml -} // namespace ui -} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/qml/global_store_model_ui.hpp b/include/xstudio/ui/qml/global_store_model_ui.hpp index e09ba4c8d..dc94a5002 100644 --- a/include/xstudio/ui/qml/global_store_model_ui.hpp +++ b/include/xstudio/ui/qml/global_store_model_ui.hpp @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once - // include CMake auto-generated export hpp #include "xstudio/ui/qml/global_store_qml_export.h" @@ -71,4 +70,62 @@ class GLOBAL_STORE_QML_EXPORT GlobalStoreModel bool autosave_{false}; }; +/* A specialised version of the GlobalStoreModel where only 'public' data +entries from the GlobalStore are exposed in a flattened tree. The purpose of +this is to drive the preferences panel in the front end. */ +class GLOBAL_STORE_QML_EXPORT PublicPreferencesModel + : public caf::mixin::actor_object { + Q_OBJECT + + Q_PROPERTY(bool autosave READ autosave WRITE setAutosave NOTIFY autosaveChanged) + + enum Roles { + nameRole = JSONTreeModel::Roles::LASTROLE, + pathRole, + datatypeRole, + contextRole, + valueRole, + descriptionRole, + defaultValueRole, + overriddenValueRole, + overriddenPathRole, + displayNameRole, + categoryRole, + optionsRole + }; + + public: + using super = caf::mixin::actor_object; + explicit PublicPreferencesModel(QObject *parent = nullptr); + virtual void init(caf::actor_system &system); + + [[nodiscard]] QVariant + data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + bool + setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + [[nodiscard]] bool autosave() const { return autosave_; } + + void setAutosave(const bool enabled = true); + + signals: + void autosaveChanged(); + + public: + caf::actor_system &system() { return self()->home_system(); } + + private: + void buildModel(const utility::JsonStore &entireGlobalStore); + + void storeToTree(const nlohmann::json &src, nlohmann::json &tree); + + bool updateProperty(const std::string &path, const utility::JsonStore &change); + + + std::shared_ptr gsh_; + bool autosave_{false}; +}; + + } // namespace xstudio::ui::qml diff --git a/include/xstudio/ui/qml/helper_ui.hpp b/include/xstudio/ui/qml/helper_ui.hpp index 42c03f22a..a4a5b53ac 100644 --- a/include/xstudio/ui/qml/helper_ui.hpp +++ b/include/xstudio/ui/qml/helper_ui.hpp @@ -1,14 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once - #include #include #include +#include #include "xstudio/ui/qml/actor_object.hpp" #include "xstudio/ui/qml/json_tree_model_ui.hpp" #include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/timecode.hpp" #include "xstudio/utility/string_helpers.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" @@ -17,35 +18,169 @@ #include "xstudio/ui/qml/helper_qml_export.h" CAF_PUSH_WARNINGS +#include #include #include -#include +#include +#include +#include +#include #include +#include #include +#include +#include #include -#include #include -#include +#include +#include #include +#include +#include #include -#include +#include #include #include -#include -#include -#include CAF_POP_WARNINGS + namespace xstudio { namespace ui { namespace qml { using namespace caf; + namespace fs = std::filesystem; QVariant mapFromValue(const nlohmann::json &value); nlohmann::json mapFromValue(const QVariant &value); + inline QString QStringFromStd(const std::string &str) { + return QString::fromUtf8(str.c_str()); + } + inline std::string StdFromQString(const QString &str) { + return str.toUtf8().constData(); + } + + /* Get the name of the window that this QObject belongs to (if any)*/ + inline QString item_window_name(QObject *obj) { + + if (qmlContext(obj)) { + QQmlContext *c = qmlContext(obj); + QObject *pobj = obj; + while (c) { + QObject *cobj = c->contextObject(); + if (cobj && cobj->isWindowType()) { + return cobj->objectName(); + } + pobj = cobj; + c = c->parentContext(); + } + } + return QString(); + } + + class HELPER_QML_EXPORT TimeCode : public QObject { + Q_OBJECT + + Q_PROPERTY(unsigned int hours READ hours WRITE setHours NOTIFY timeCodeChanged) + Q_PROPERTY( + unsigned int minutes READ minutes WRITE setMinutes NOTIFY timeCodeChanged) + Q_PROPERTY( + unsigned int seconds READ seconds WRITE setSeconds NOTIFY timeCodeChanged) + Q_PROPERTY(unsigned int frames READ frames WRITE setFrames NOTIFY timeCodeChanged) + Q_PROPERTY( + double frameRate READ frameRate WRITE setFrameRate NOTIFY timeCodeChanged) + Q_PROPERTY(bool dropFrame READ dropFrame WRITE setDropFrame NOTIFY timeCodeChanged) + Q_PROPERTY(unsigned int totalFrames READ totalFrames WRITE setTotalFrames NOTIFY + timeCodeChanged) + Q_PROPERTY(QString timeCode READ timeCode NOTIFY timeCodeChanged) + + public: + explicit TimeCode(QObject *parent = nullptr) : QObject(parent) {} + + [[nodiscard]] unsigned int hours() const { return timecode_.hours(); } + [[nodiscard]] unsigned int minutes() const { return timecode_.minutes(); } + [[nodiscard]] unsigned int seconds() const { return timecode_.seconds(); } + [[nodiscard]] unsigned int frames() const { return timecode_.frames(); } + [[nodiscard]] double frameRate() const { return timecode_.framerate(); } + [[nodiscard]] bool dropFrame() const { return timecode_.dropframe(); } + [[nodiscard]] unsigned int totalFrames() const { return timecode_.total_frames(); } + [[nodiscard]] QString timeCode() const { + return QStringFromStd(timecode_.to_string()); + } + + void setHours(const unsigned int value) { + timecode_.hours(value); + emit timeCodeChanged(); + } + + void setMinutes(const unsigned int value) { + timecode_.minutes(value); + emit timeCodeChanged(); + } + + void setSeconds(const unsigned int value) { + timecode_.seconds(value); + emit timeCodeChanged(); + } + + void setFrames(const unsigned int value) { + timecode_.frames(value); + emit timeCodeChanged(); + } + + void setFrameRate(const double value) { + timecode_.framerate(value); + emit timeCodeChanged(); + } + + void setDropFrame(const bool value) { + timecode_.dropframe(value); + emit timeCodeChanged(); + } + + void setTotalFrames(const unsigned int value) { + timecode_.total_frames(value); + emit timeCodeChanged(); + } + + Q_INVOKABLE void setTimeCodeFromString( + const QString &code, + const double frameRate = 30.0, + const bool dropFrame = false) { + timecode_ = utility::Timecode(StdFromQString(code), frameRate, dropFrame); + emit timeCodeChanged(); + } + + Q_INVOKABLE void setTimeCodeFromFrames( + const unsigned int frames, + const double frameRate = 30.0, + const bool dropFrame = false) { + timecode_ = utility::Timecode(frames, frameRate, dropFrame); + emit timeCodeChanged(); + } + + Q_INVOKABLE void setTimeCode( + const unsigned int hour, + const unsigned int minute, + const unsigned int second, + const unsigned int frame, + const double frameRate = 30.0, + const bool dropFrame = false) { + timecode_ = + utility::Timecode(hour, minute, second, frame, frameRate, dropFrame); + emit timeCodeChanged(); + } + + + signals: + void timeCodeChanged(); + + private: + utility::Timecode timecode_; + }; + class HELPER_QML_EXPORT ModelRowCount : public QObject { Q_OBJECT @@ -112,6 +247,8 @@ namespace ui { const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()); + void removed(const QModelIndex &parent, int first, int last); + private: bool updateValue(); @@ -180,6 +317,7 @@ namespace ui { signals: void indexChanged(); void valuesChanged(); + void contentChanged(); protected slots: void dataChanged( @@ -190,13 +328,53 @@ namespace ui { protected: virtual void valueChanged(const QString &key, const QVariant &value); - virtual void updateValues(const QVector &roles = {}); + virtual bool updateValues(const QVector &roles = {}); [[nodiscard]] int getRoleId(const QString &role) const; QPersistentModelIndex index_; QQmlPropertyMap *values_{nullptr}; }; + class HELPER_QML_EXPORT PreferencePropertyMap : public ModelPropertyMap { + Q_OBJECT + + Q_PROPERTY(QVariant value READ value WRITE setMyValue NOTIFY myValueChanged) + + Q_PROPERTY(QVariant dataType READ dataType NOTIFY dataTypeChanged) + Q_PROPERTY(QVariant context READ context NOTIFY contextChanged) + Q_PROPERTY(QVariant name READ name NOTIFY nameChanged) + Q_PROPERTY(QVariant defaultValue READ defaultValue NOTIFY defaultValueChanged) + Q_PROPERTY(QVariant jsonString READ jsonString NOTIFY jsonStringChanged) + + public: + explicit PreferencePropertyMap(QObject *parent = nullptr) + : ModelPropertyMap(parent) {} + [[nodiscard]] QVariant value() const { return values_->value("valueRole"); } + [[nodiscard]] QVariant dataType() const { return values_->value("datatypeRole"); } + [[nodiscard]] QVariant context() const { return values_->value("contextRole"); } + [[nodiscard]] QVariant name() const { return values_->value("nameRole"); } + [[nodiscard]] QVariant defaultValue() const { + return values_->value("defaultValueRole"); + } + [[nodiscard]] QVariant jsonString() const { return values_->value("jsonTextRole"); } + + void setMyValue(const QVariant &value); + + signals: + void myValueChanged(); + void dataTypeChanged(); + void contextChanged(); + void nameChanged(); + void defaultValueChanged(); + void jsonStringChanged(); + + protected: + void valueChanged(const QString &key, const QVariant &value) override; + + private: + void emitChange(const QString &key); + }; + class HELPER_QML_EXPORT ModelNestedPropertyMap : public ModelPropertyMap { Q_OBJECT @@ -206,7 +384,7 @@ namespace ui { protected: - void updateValues(const QVector &roles = {}) override; + bool updateValues(const QVector &roles = {}) override; void valueChanged(const QString &key, const QVariant &value) override; QString data_role_ = {"valueRole"}; @@ -230,27 +408,6 @@ namespace ui { std::reference_wrapper system_ref_; }; - class HELPER_QML_EXPORT QMLActor : public caf::mixin::actor_object { - Q_OBJECT - - public: - using super = caf::mixin::actor_object; - explicit QMLActor(QObject *parent = nullptr); - - virtual ~QMLActor(); - virtual void init(caf::actor_system &system) { super::init(system); } - - public: - caf::actor_system &system() { return self()->home_system(); } - }; - - inline QString QStringFromStd(const std::string &str) { - return QString::fromUtf8(str.c_str()); - } - inline std::string StdFromQString(const QString &str) { - return str.toUtf8().constData(); - } - inline QUrl QUrlFromUri(const caf::uri &uri) { if (uri.empty()) return QUrl(); @@ -319,7 +476,11 @@ namespace ui { auto jsn = qvariant_to_json(drop); std::regex rgx(R"(\r\n|\n)"); //, std::regex::extended); for (auto i : jsn.items()) { - jsn[i.key()] = utility::resplit(i.value(), rgx); + if (i.value().is_string()) { + jsn[i.key()] = utility::resplit(i.value(), rgx); + } else { + jsn[i.key()] = i.value(); + } } return jsn; } @@ -333,13 +494,69 @@ namespace ui { ~Helpers() override = default; Q_INVOKABLE [[nodiscard]] bool openURL(const QUrl &url) const { - return QDesktopServices::openUrl(url); + return openURLFuture(url).result(); + } + + Q_INVOKABLE [[nodiscard]] QFuture openURLFuture(const QUrl &url) const { + return QtConcurrent::run([=]() { return QDesktopServices::openUrl(url); }); } - Q_INVOKABLE [[nodiscard]] QString ShowURIS(const QList &urls) const { - std::vector uris; + + Q_INVOKABLE [[nodiscard]] bool showURIS(const QList &urls) const { + auto uris = QStringList(); for (const auto &i : urls) - uris.emplace_back(UriFromQUrl(i)); - return QStringFromStd(utility::filemanager_show_uris(uris)); + uris.push_back(i.toString()); + + auto arguments = QStringList( + {"--session", + "--print-reply", + "--dest=org.freedesktop.FileManager1", + "--type=method_call", + "/org/freedesktop/FileManager1", + "org.freedesktop.FileManager1.ShowItems", + QString("array:string:") + uris.join(','), + "string:"}); + + return startDetachedProcess("dbus-send", arguments); + } + + Q_INVOKABLE void setOverrideCursor(const QString &name = "") { + const static auto cursors = std::map( + {{"Qt.ArrowCursor", Qt::ArrowCursor}, + {"Qt.UpArrowCursor", Qt::UpArrowCursor}, + {"Qt.CrossCursor", Qt::CrossCursor}, + {"Qt.WaitCursor", Qt::WaitCursor}, + {"Qt.IBeamCursor", Qt::IBeamCursor}, + {"Qt.SizeVerCursor", Qt::SizeVerCursor}, + {"Qt.SizeHorCursor", Qt::SizeHorCursor}, + {"Qt.SizeBDiagCursor", Qt::SizeBDiagCursor}, + {"Qt.SizeFDiagCursor", Qt::SizeFDiagCursor}, + {"Qt.SizeAllCursor", Qt::SizeAllCursor}, + {"Qt.BlankCursor", Qt::BlankCursor}, + {"Qt.SplitVCursor", Qt::SplitVCursor}, + {"Qt.SplitHCursor", Qt::SplitHCursor}, + {"Qt.PointingHandCursor", Qt::PointingHandCursor}, + {"Qt.ForbiddenCursor", Qt::ForbiddenCursor}, + {"Qt.OpenHandCursor", Qt::OpenHandCursor}, + {"Qt.ClosedHandCursor", Qt::ClosedHandCursor}, + {"Qt.WhatsThisCursor", Qt::WhatsThisCursor}, + {"Qt.BusyCursor", Qt::BusyCursor}, + {"Qt.DragMoveCursor", Qt::DragMoveCursor}, + {"Qt.DragCopyCursor", Qt::DragCopyCursor}, + {"Qt.DragLinkCursor", Qt::DragLinkCursor}}); + + if (name == "") + restoreOverrideCursor(); + else { + if (cursors.count(name)) + QGuiApplication::setOverrideCursor(QCursor(cursors.at(name))); + else + QGuiApplication::setOverrideCursor( + QCursor(QPixmap(name).scaledToWidth(32, Qt::SmoothTransformation))); + } + } + + Q_INVOKABLE void restoreOverrideCursor() { + QGuiApplication::restoreOverrideCursor(); } Q_INVOKABLE [[nodiscard]] QString getUserName() const { @@ -350,12 +567,54 @@ namespace ui { return url.path().replace("//", "/"); } + Q_INVOKABLE [[nodiscard]] QModelIndex qModelIndex() const { return QModelIndex(); } + Q_INVOKABLE [[nodiscard]] QModelIndex + qModelIndex(const QPersistentModelIndex &index) const { + return QModelIndex(index); + } + Q_INVOKABLE [[nodiscard]] QModelIndex qModelIndex(const QModelIndex &index) const { + return index; + } + Q_INVOKABLE [[nodiscard]] QPersistentModelIndex - makePersistent(const QModelIndex &index) const { + qPersistentModelIndex(const QPersistentModelIndex &index) const { + return index; + } + Q_INVOKABLE [[nodiscard]] QPersistentModelIndex + qPersistentModelIndex(const QModelIndex &index) const { return QPersistentModelIndex(index); } + Q_INVOKABLE [[nodiscard]] QPersistentModelIndex + makePersistent(const QModelIndex &index) const { + return qPersistentModelIndex(index); + } + + Q_INVOKABLE [[nodiscard]] bool + compareIndex(const QModelIndex &a, const QPersistentModelIndex &b) const { + return a == b; + } + + Q_INVOKABLE [[nodiscard]] bool + compareIndex(const QPersistentModelIndex &a, const QModelIndex &b) const { + return a == b; + } + + Q_INVOKABLE [[nodiscard]] QUrl QUrlFromQString(const QString &str) const { + return QUrl(str); + } + + Q_INVOKABLE [[nodiscard]] bool urlExists(const QUrl &url) const { + auto path = fs::path(utility::uri_to_posix_path(UriFromQUrl(url))); + auto result = false; + try { + result = fs::exists(path); + } catch (...) { + } + return result; + } + Q_INVOKABLE [[nodiscard]] QString fileFromURL(const QUrl &url) const { static const std::regex as_hash_pad(R"(\{:0(\d+)d\})"); auto tmp = utility::uri_to_posix_path(UriFromQUrl(url)); @@ -363,7 +622,8 @@ namespace ui { // convert padding spec to #'s // {:04d} try { - tmp = fmt::format(std::regex_replace(tmp, as_hash_pad, R"({:#<$1})"), ""); + tmp = fmt::format( + fmt::runtime(std::regex_replace(tmp, as_hash_pad, R"({:#<$1})")), ""); } catch (...) { tmp = std::regex_replace(tmp, as_hash_pad, "#"); } @@ -397,7 +657,7 @@ namespace ui { &name); Q_INVOKABLE QQuickItem* findItemByName(const QString& name) { return findItemByName(engine_->rootObjects(), name); }*/ - Q_INVOKABLE [[nodiscard]] [[nodiscard]] QString + Q_INVOKABLE [[nodiscard]] QString getEnv(const QString &key, const QString &fallback = "") const { QString result = fallback; auto value = utility::get_env(StdFromQString(key)); @@ -406,6 +666,10 @@ namespace ui { return result; } + Q_INVOKABLE [[nodiscard]] QString expandEnvVars(const QString &value) const { + return QStringFromStd(xstudio::utility::expand_envvars(StdFromQString(value))); + } + Q_INVOKABLE [[nodiscard]] bool startDetachedProcess( const QString &program, const QStringList &arguments, @@ -418,6 +682,15 @@ namespace ui { QVariantFromUuidString(const QString &uuid) const { return QVariant::fromValue(QUuidFromUuid(utility::Uuid(StdFromQString(uuid)))); } + + Q_INVOKABLE [[nodiscard]] QUuid makeQUuid() const { + return QUuidFromUuid(utility::Uuid::generate()); + } + + Q_INVOKABLE [[nodiscard]] QUuid QUuidFromUuidString(const QString &uuid) const { + return QUuidFromUuid(utility::Uuid(StdFromQString(uuid))); + } + Q_INVOKABLE [[nodiscard]] QString QUuidToQString(const QUuid &uuid) const { return uuid.toString(QUuid::WithoutBraces); } @@ -429,6 +702,12 @@ namespace ui { createItemSelection(const QModelIndex &tl, const QModelIndex &br) const { return QItemSelection(tl, br); } + + Q_INVOKABLE [[nodiscard]] QModelIndexList + createListFromRange(const QItemSelectionRange &r) const { + return r.indexes(); + } + Q_INVOKABLE [[nodiscard]] QItemSelection createItemSelection(const QModelIndexList &l) const { auto s = QItemSelection(); @@ -436,6 +715,31 @@ namespace ui { s.select(i, i); return s; } + + Q_INVOKABLE [[nodiscard]] QItemSelection + intersectItemSelection(const QItemSelection &a, const QItemSelection &b) const { + auto s = QItemSelection(); + for (const auto &i : a.indexes()) { + if (b.contains(i)) + s.select(i, i); + } + return s; + } + + Q_INVOKABLE [[nodiscard]] QItemSelection + createItemSelectionFromList(const QVariantList &l) const { + auto s = QItemSelection(); + for (const auto &i : l) + s.select(i.toModelIndex(), i.toModelIndex()); + return s; + } + + Q_INVOKABLE [[nodiscard]] QModelIndexList + getParentIndexes(const QModelIndexList &l) const; + + Q_INVOKABLE [[nodiscard]] QModelIndexList + getParentIndexesFromRange(const QItemSelection &l) const; + Q_INVOKABLE [[nodiscard]] bool itemSelectionContains( const QItemSelection &selection, const QModelIndex &item) const { return selection.contains(item); @@ -443,7 +747,7 @@ namespace ui { Q_INVOKABLE [[nodiscard]] QColor saturate(const QColor &color, const double factor = 1.5) const { - double h, s, l, a; + float h, s, l, a; color.getHslF(&h, &s, &l, &a); s = std::max(0.0, std::min(1.0, s * factor)); return QColor::fromHslF(h, s, l, a); @@ -451,7 +755,7 @@ namespace ui { Q_INVOKABLE [[nodiscard]] QColor luminate(const QColor &color, const double factor = 1.5) const { - double h, s, l, a; + float h, s, l, a; color.getHslF(&h, &s, &l, &a); l = std::max(0.0, std::min(1.0, l * factor)); return QColor::fromHslF(h, s, l, a); @@ -468,27 +772,71 @@ namespace ui { const QColor &color, const double sfactor = 1.0, const double lfactor = 1.0) const { - double h, s, l, a; + float h, s, l, a; color.getHslF(&h, &s, &l, &a); s = std::max(0.0, std::min(1.0, s * sfactor)); l = std::max(0.0, std::min(1.0, l * lfactor)); return QColor::fromHslF(h, s, l, a); } + Q_INVOKABLE [[nodiscard]] QObject *contextPanel(QObject *obj) const; + + Q_INVOKABLE [[nodiscard]] QString contextPanelAddress(QObject *obj) const { + return objPtrTostring(contextPanel(obj)); + } + + Q_INVOKABLE void setMenuPathPosition( + const QString &menu_path, const QString &menu_name, const float position) const; + + static inline QString objPtrTostring(QObject *obj) { + if (!obj) + return QString(); + return QString("0x%1").arg( + reinterpret_cast(obj), QT_POINTER_SIZE * 2, 16, QChar('0')); + } + + Q_INVOKABLE void inhibitScreenSaver(const bool inhibit = true) const; + + Q_INVOKABLE QVariant python_callback( + QString method_name, + QUuid python_plugin_uuid, + const QVariant args = QVariant()) const; + private: QQmlEngine *engine_; }; - class HELPER_QML_EXPORT CursorPosProvider : public QObject { + class HELPER_QML_EXPORT KeyEventsItem : public QQuickItem { Q_OBJECT + Q_PROPERTY(QString context READ context WRITE setContext NOTIFY contextChanged) + public: - CursorPosProvider(QObject *parent = nullptr) : QObject(parent) {} - ~CursorPosProvider() override = default; + explicit KeyEventsItem(QQuickItem *parent = nullptr); + + [[nodiscard]] QString context() const { return QStringFromStd(context_); } + + void setContext(const QString &context) { + if (context_ != StdFromQString(context)) { + context_ = StdFromQString(context); + emit contextChanged(); + } + } + signals: + void contextChanged(); - Q_INVOKABLE QPointF cursorPos() { return QCursor::pos(); } + protected: + bool event(QEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *event) override; + + private: + std::string context_; + caf::actor keypress_monitor_; + std::string window_name_; }; + class HELPER_QML_EXPORT QMLUuid : public QObject { Q_OBJECT Q_PROPERTY(QString asString READ asString WRITE setFromString NOTIFY changed) @@ -677,6 +1025,133 @@ namespace ui { caf::actor_id backend_id_; }; + class HELPER_QML_EXPORT ImagePainter : public QQuickPaintedItem { + + Q_OBJECT + + Q_PROPERTY(QVariant image READ image WRITE setImage NOTIFY imageChanged) + Q_PROPERTY(bool fill READ fill WRITE setFill NOTIFY fillChanged) + + public: + ImagePainter(QQuickItem *parent = nullptr); + ~ImagePainter() override = default; + + [[nodiscard]] const QVariant &image() const { return image_property_; } + [[nodiscard]] bool fill() const { return fill_; } + + void setImage(QVariant &image) { + image_property_ = image; + if (image_property_.canConvert()) { + image_ = image_property_.value(); + } else { + image_ = QImage(); + } + emit imageChanged(); + update(); + } + + void setFill(const bool fill) { + if (fill != fill_) { + fill_ = fill; + emit fillChanged(); + update(); + } + } + + void paint(QPainter *painter) override; + + signals: + + void imageChanged(); + void fillChanged(); + + private: + QVariant image_property_; + QImage image_; + bool fill_ = {false}; + }; + + + class HELPER_QML_EXPORT MarkerModel : public JSONTreeModel { + Q_OBJECT + + Q_PROPERTY(QVariant markerData READ markerData WRITE setMarkerData NOTIFY + markerDataChanged) + + public: + enum Roles { + commentRole = JSONTreeModel::Roles::LASTROLE, + durationRole, + flagRole, + layerRole, + nameRole, + rateRole, + startRole + }; + explicit MarkerModel(QObject *parent = nullptr); + + Q_INVOKABLE bool setMarkerData(const QVariant &data); + + Q_INVOKABLE QModelIndex addMarker( + const int frame, + const double rate, + const QString &name = "Marker", + const QString &flag = "#FFFF0000", + const QVariant &metadata = mapFromValue(R"({})"_json)); + + [[nodiscard]] QVariant markerData() const; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + bool setData( + const QModelIndex &index, + const QVariant &value, + int role = Qt::EditRole) override; + + signals: + void markerDataChanged(); + + + private: + }; + + /* Surprisingly, QML does not provide any facility to bind to a named + property, where you name the property value as a string. + This class will allow us to do this.*/ + class HELPER_QML_EXPORT PropertyFollower : public QObject { + Q_OBJECT + + Q_PROPERTY(QVariant propertyValue READ propertyValue NOTIFY propertyValueChanged) + Q_PROPERTY(QObject *target READ target WRITE setTarget NOTIFY targetChanged) + Q_PROPERTY(QString propertyName READ propertyName WRITE setPropertyName NOTIFY + propertyNameChanged) + + public: + PropertyFollower(QObject *parent = nullptr) : QObject(parent) {} + + Q_INVOKABLE void setTarget(QObject *target); + + Q_INVOKABLE void setPropertyName(const QString propertyName); + + QVariant propertyValue() { return the_property_.read(); } + + QString propertyName() { return property_name_; } + + QObject *target() { return target_; } + + signals: + + void propertyValueChanged(); + void targetChanged(); + void propertyNameChanged(); + + private: + QQmlProperty the_property_; + QString property_name_; + QObject *target_ = {nullptr}; + }; + + } // namespace qml } // namespace ui } // namespace xstudio diff --git a/include/xstudio/ui/qml/hotkey_ui.hpp b/include/xstudio/ui/qml/hotkey_ui.hpp index 3c309f5c5..e37d02dde 100644 --- a/include/xstudio/ui/qml/hotkey_ui.hpp +++ b/include/xstudio/ui/qml/hotkey_ui.hpp @@ -29,16 +29,42 @@ namespace ui { Q_OBJECT + enum HotkeyRoles { + keyboardKey = Qt::UserRole + 1, + keyModifiers, + hotkeyName, + hotkeyCategory, + hotkeyDescription, + hotkeySequence, + hotkeyErrorMessage + }; + + static inline const QMap hotkeyRoleNames = { + {keyboardKey, "keyboardKey"}, + {keyModifiers, "keyModifiers"}, + {hotkeyName, "hotkeyName"}, + {hotkeyCategory, "hotkeyCategory"}, + {hotkeyDescription, "hotkeyDescription"}, + {hotkeySequence, "hotkeySequence"}, + {hotkeyErrorMessage, "hotkeyErrorMessage"}}; + public: using super = caf::mixin::actor_object; Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged) + Q_PROPERTY(QStringList categories READ categories NOTIFY categoriesChanged) + Q_PROPERTY(QString currentCategory READ currentCategory WRITE setCurrentCategory + NOTIFY currentCategoryChanged) HotkeysUI(QObject *parent = nullptr); ~HotkeysUI() override = default; [[nodiscard]] int rowCount() { return rowCount(QModelIndex()); } + [[nodiscard]] QStringList categories() { return categories_; } + + [[nodiscard]] QString currentCategory() { return current_category_; } + [[nodiscard]] int rowCount(const QModelIndex &parent) const override; [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; @@ -51,22 +77,41 @@ namespace ui { return Qt::ItemIsEnabled | Qt::ItemIsEditable; } + void setCurrentCategory(const QString &category) { + if (category != current_category_) { + current_category_ = category; + emit currentCategoryChanged(); + beginResetModel(); + endResetModel(); + emit rowCountChanged(); + } + } + + Q_INVOKABLE QString hotkey_sequence(const QVariant &hotkey_uuid); + + Q_INVOKABLE QString hotkey_sequence_from_hotkey_name(const QString &hotkey_name); + + signals: void rowCountChanged(); + void categoriesChanged(); + void currentCategoryChanged(); public slots: private: void update_hotkeys_model_data(const std::vector &new_hotkeys_data); + void checkCategories(); caf::actor_system &system() { return self()->home_system(); } virtual void init(caf::actor_system &system) { super::init(system); } - std::vector> hotkeys_data_; + std::vector hotkeys_data_; + QStringList categories_; + QString current_category_; }; - class VIEWPORT_QML_EXPORT HotkeyUI : public QMLActor { Q_OBJECT @@ -81,6 +126,7 @@ namespace ui { Q_PROPERTY(QString context READ context WRITE setContext NOTIFY contextChanged) Q_PROPERTY( bool autoRepeat READ autoRepeat WRITE setAutoRepeat NOTIFY autoRepeatChanged) + Q_PROPERTY(QUuid uuid READ uuid NOTIFY uuidChanged) public: explicit HotkeyUI(QObject *parent = nullptr); @@ -125,6 +171,7 @@ namespace ui { [[nodiscard]] const QString &errorMessage() const { return error_message_; } [[nodiscard]] const QString &context() const { return context_; } [[nodiscard]] bool autoRepeat() const { return autorepeat_; } + [[nodiscard]] QUuid uuid() const { return hotkey_uuid_; } public slots: @@ -140,6 +187,8 @@ namespace ui { void contextChanged(); void activated(); void autoRepeatChanged(); + void uuidChanged(); + void released(); private: QString sequence_; @@ -148,40 +197,65 @@ namespace ui { QString description_; QString error_message_; QString context_ = QString("any"); + QString window_name_; bool autorepeat_ = {false}; - utility::Uuid hotkey_uuid_; + QUuid hotkey_uuid_; }; + /*XsHotkeyReference item. This lets us 'watch' an already existing hotkey. + We use the name of the hotkey to identify it.*/ class VIEWPORT_QML_EXPORT HotkeyReferenceUI : public QMLActor { Q_OBJECT Q_PROPERTY(QString sequence READ sequence NOTIFY sequenceChanged) Q_PROPERTY( - QUuid hotkeyUuid READ hotkeyUuid WRITE setHotkeyUuid NOTIFY hotkeyUuidChanged) + QString hotkeyName READ hotkeyName WRITE setHotkeyName NOTIFY hotkeyNameChanged) + Q_PROPERTY(QUuid uuid READ uuid NOTIFY uuidChanged) + Q_PROPERTY(QString context READ context WRITE setContext NOTIFY contextChanged) + Q_PROPERTY(bool exclusive READ exclusive WRITE setExclusive NOTIFY exclusiveChanged) public: explicit HotkeyReferenceUI(QObject *parent = nullptr); - ~HotkeyReferenceUI() override = default; + ~HotkeyReferenceUI() override; void init(caf::actor_system &system) override; - void setHotkeyUuid(const QUuid &uuid); + void setHotkeyName(const QString &name); - [[nodiscard]] const QUuid &hotkeyUuid() const { return uuid_; } + [[nodiscard]] const QString &hotkeyName() const { return hotkey_name_; } [[nodiscard]] const QString &sequence() const { return sequence_; } + [[nodiscard]] QUuid uuid() const { return hotkey_uuid_; } + [[nodiscard]] const QString context() const { return QStringFromStd(context_); } + [[nodiscard]] bool exclusive() const { return exclusive_; } + + void setContext(const QString &context) { + context_ = StdFromQString(context); + emit contextChanged(); + } + + void setExclusive(const bool exclusive); signals: void sequenceChanged(); - void hotkeyUuidChanged(); + void hotkeyNameChanged(); + void activated(const QString context); + void uuidChanged(); + void contextChanged(); + void exclusiveChanged(); private: + void notifyExclusiveChanged(); + QString sequence_; - QUuid uuid_; + QString hotkey_name_; + QUuid hotkey_uuid_; + std::string context_; + bool exclusive_ = {false}; }; } // namespace qml } // namespace ui -} // namespace xstudio +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/qml/json_store_ui.hpp b/include/xstudio/ui/qml/json_store_ui.hpp index 9df1245b0..55fcce853 100644 --- a/include/xstudio/ui/qml/json_store_ui.hpp +++ b/include/xstudio/ui/qml/json_store_ui.hpp @@ -42,6 +42,7 @@ namespace ui { caf::actor store; caf::actor store_events; QString json_string_; + caf::disposable monitor_; }; } // namespace qml } // namespace ui diff --git a/include/xstudio/ui/qml/json_tree_model_ui.hpp b/include/xstudio/ui/qml/json_tree_model_ui.hpp index 435758edf..12910672a 100644 --- a/include/xstudio/ui/qml/json_tree_model_ui.hpp +++ b/include/xstudio/ui/qml/json_tree_model_ui.hpp @@ -1,6 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once - #include #include #include @@ -13,11 +12,15 @@ CAF_POP_WARNINGS #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/tree.hpp" +#include "xstudio/utility/uuid.hpp" +#include "xstudio/ui/qml/actor_object.hpp" #include "helper_qml_export.h" namespace xstudio::ui::qml { +typedef std::function JSONTreeSendEventFunc; + class HELPER_QML_EXPORT JSONTreeModel : public QAbstractItemModel { Q_OBJECT @@ -32,15 +35,32 @@ class HELPER_QML_EXPORT JSONTreeModel : public QAbstractItemModel { void lengthChanged(); public: - enum Roles { JSONRole = Qt::UserRole + 1, JSONTextRole, LASTROLE }; + enum Roles { + idRole = Qt::UserRole + 1, + childCountRole, + childrenRole, + JSONRole, + JSONTextRole, + JSONPathRole, + LASTROLE + }; inline static const std::map role_names = { - {Qt::DisplayRole, "display"}, {JSONRole, "jsonRole"}, {JSONTextRole, "jsonTextRole"}}; + {Qt::DisplayRole, "display"}, + {idRole, "idRole"}, + {childCountRole, "childCountRole"}, + {childrenRole, "childrenRole"}, + {JSONRole, "jsonRole"}, + {JSONTextRole, "jsonTextRole"}, + {JSONPathRole, "jsonPathRole"}}; JSONTreeModel(QObject *parent = nullptr); [[nodiscard]] bool canFetchMore(const QModelIndex &parent) const override; + Q_INVOKABLE void fetchMoreWait(const QModelIndex &parent); + + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; [[nodiscard]] int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 1; @@ -56,7 +76,7 @@ class HELPER_QML_EXPORT JSONTreeModel : public QAbstractItemModel { bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - Q_INVOKABLE [[nodiscard]] bool + Q_INVOKABLE bool set(const QModelIndex &item, const QVariant &value, const QString &role = "display"); Q_INVOKABLE [[nodiscard]] QVariant @@ -70,6 +90,7 @@ class HELPER_QML_EXPORT JSONTreeModel : public QAbstractItemModel { Q_INVOKABLE bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + Q_INVOKABLE bool moveRows( const QModelIndex &sourceParent, int sourceRow, @@ -79,6 +100,9 @@ class HELPER_QML_EXPORT JSONTreeModel : public QAbstractItemModel { Q_INVOKABLE bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + Q_INVOKABLE bool + insertRowsData(int row, int count, const QModelIndex &parent, const QVariant &data); + bool insertRows(int row, int count, const QModelIndex &parent, const nlohmann::json &data); Q_INVOKABLE QModelIndex invalidIndex() const { return QModelIndex(); } @@ -98,35 +122,35 @@ class HELPER_QML_EXPORT JSONTreeModel : public QAbstractItemModel { const QModelIndex &parent = QModelIndex(), const int start = 0); - Q_INVOKABLE QModelIndexList search_list( + Q_INVOKABLE QModelIndexList searchList( const QVariant &value, const int role, const QModelIndex &parent, const int start, const int hits); - Q_INVOKABLE QModelIndexList search_list( + Q_INVOKABLE QModelIndexList searchList( const QVariant &value, const QString &role, const QModelIndex &parent, const int start, const int hits); - Q_INVOKABLE QModelIndex search_recursive( + Q_INVOKABLE QModelIndex searchRecursive( const QVariant &value, const QString &role = "display", const QModelIndex &parent = QModelIndex(), const int start = 0, const int depth = -1); - Q_INVOKABLE QModelIndex search_recursive( + Q_INVOKABLE QModelIndex searchRecursive( const QVariant &value, const int role, const QModelIndex &parent = QModelIndex(), const int start = 0, const int depth = -1); - Q_INVOKABLE QModelIndexList search_recursive_list( + Q_INVOKABLE QModelIndexList searchRecursiveList( const QVariant &value, const int role, const QModelIndex &parent, @@ -134,7 +158,7 @@ class HELPER_QML_EXPORT JSONTreeModel : public QAbstractItemModel { const int hits, const int depth = -1); - Q_INVOKABLE QModelIndexList search_recursive_list( + Q_INVOKABLE QModelIndexList searchRecursiveList( const QVariant &value, const QString &role, const QModelIndex &parent, @@ -143,8 +167,8 @@ class HELPER_QML_EXPORT JSONTreeModel : public QAbstractItemModel { const int depth = -1); [[nodiscard]] nlohmann::json modelData() const; - void setModelData(const nlohmann::json &data); + virtual void setModelData(const nlohmann::json &data); const std::string &children() const { return children_; } void setChildren(const std::string &value) { children_ = value; } @@ -160,10 +184,66 @@ class HELPER_QML_EXPORT JSONTreeModel : public QAbstractItemModel { utility::JsonTree *indexToTree(const QModelIndex &index) const; nlohmann::json::json_pointer getIndexPath(const QModelIndex &index = QModelIndex()) const; - QModelIndex getPathIndex(const nlohmann::json::json_pointer &path); + virtual QModelIndex getPathIndex(const nlohmann::json::json_pointer &path); + + void bindEventFunc(JSONTreeSendEventFunc fs); + virtual bool receiveEvent(const utility::JsonStore &event); + + /* For row reordering at a given parent index only. The full list of + re-ordered indeces must be provbided. + + e.g. if we have rows in the model A,B,C,D,E and we are re-ordering to + E,A,D,C,B then new_row_indeces should be [4,0,3,2,1] */ + bool reorderRows(const QModelIndex &parent, const std::vector &new_row_indeces); + protected: - virtual QModelIndexList search_recursive_list_base( + void setModelDataBase(const nlohmann::json &data, const bool local = true); + + bool insertNodes( + const int row, + const int count, + utility::JsonTree *node, + const nlohmann::json &data = R"({})"_json); + + bool baseInsertRows( + int row, + int count, + const QModelIndex &parent = QModelIndex(), + const nlohmann::json &data = R"({})"_json, + const bool local = true); + + bool moveNodes( + utility::JsonTree *src, + int first_row, + int last_row, + utility::JsonTree *dst, + int dst_row); + + bool baseMoveRows( + const QModelIndex &sourceParent, + int sourceRow, + int count, + const QModelIndex &destinationParent, + int destinationChild, + const bool local = true); + + bool removeNodes(const int row, const int count, utility::JsonTree *node); + + bool baseRemoveRows( + int row, int count, const QModelIndex &parent = QModelIndex(), const bool local = true); + + bool baseSetData( + const QModelIndex &index, + const QVariant &value, + const std::string &key, + QVector roles, + const bool local = true); + + bool + baseSetDataAll(const QModelIndex &index, const QVariant &value, const bool local = true); + + virtual QModelIndexList searchRecursiveListBase( const QVariant &value, const int role, const QModelIndex &parent, @@ -175,13 +255,29 @@ class HELPER_QML_EXPORT JSONTreeModel : public QAbstractItemModel { std::string display_role_; std::vector role_names_; utility::JsonTree data_; + utility::Uuid model_id_; + + private: + JSONTreeSendEventFunc event_send_callback_{nullptr}; }; +// class JSONTreeListModel : public QAbstractProxyModel { +// Q_OBJECT + +// public: +// JSONTreeListModel(QObject *parent = nullptr) : QAbstractProxyModel(parent) { +// } + +// QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; +// QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; +// }; + class HELPER_QML_EXPORT JSONTreeFilterModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(int length READ length NOTIFY lengthChanged) Q_PROPERTY(int count READ length NOTIFY lengthChanged) + Q_PROPERTY(bool invert READ invert WRITE setInvert NOTIFY invertChanged) Q_PROPERTY(bool sortAscending READ sortAscending WRITE setSortAscending NOTIFY sortAscendingChanged) @@ -205,6 +301,13 @@ class HELPER_QML_EXPORT JSONTreeFilterModel : public QSortFilterProxyModel { Q_INVOKABLE [[nodiscard]] QVariant getRoleFilter(const QString &role = "display") const; Q_INVOKABLE void setRoleFilter(const QVariant &filter, const QString &role = "display"); + Q_INVOKABLE void setInvert(const bool invert) { + if (invert != invert_) { + invert_ = invert; + emit invertChanged(); + invalidateFilter(); + } + } Q_INVOKABLE [[nodiscard]] QVariant get(const QModelIndex &item, const QString &role = "display") const; @@ -222,10 +325,13 @@ class HELPER_QML_EXPORT JSONTreeFilterModel : public QSortFilterProxyModel { return QString(); } + [[nodiscard]] bool invert() const { return invert_; } + void setSortAscending(const bool ascending = true) { if (ascending != (sortOrder() == Qt::AscendingOrder ? true : false)) { sort(0, ascending ? Qt::AscendingOrder : Qt::DescendingOrder); emit sortAscendingChanged(); + invalidate(); } } @@ -242,8 +348,9 @@ class HELPER_QML_EXPORT JSONTreeFilterModel : public QSortFilterProxyModel { void setSortRoleName(const QString &role) { auto role_id = roleId(role); if (role_id != sortRole()) { - setSortRole(role_id); emit sortRoleNameChanged(); + setSortRole(role_id); + invalidate(); } } @@ -251,6 +358,7 @@ class HELPER_QML_EXPORT JSONTreeFilterModel : public QSortFilterProxyModel { void lengthChanged(); void sortAscendingChanged(); void sortRoleNameChanged(); + void invertChanged(); protected: [[nodiscard]] bool @@ -258,7 +366,6 @@ class HELPER_QML_EXPORT JSONTreeFilterModel : public QSortFilterProxyModel { private: std::map roleFilterMap_; + bool invert_ = {false}; }; - - -} // namespace xstudio::ui::qml +} // namespace xstudio::ui::qml \ No newline at end of file diff --git a/include/xstudio/ui/qml/model_data_ui.hpp b/include/xstudio/ui/qml/model_data_ui.hpp index 8b1bad14c..beed00bd3 100644 --- a/include/xstudio/ui/qml/model_data_ui.hpp +++ b/include/xstudio/ui/qml/model_data_ui.hpp @@ -5,13 +5,13 @@ #include "xstudio/ui/qml/helper_ui.hpp" #include "xstudio/ui/qml/json_tree_model_ui.hpp" -// #include "xstudio/ui/qml/tag_ui.hpp" CAF_PUSH_WARNINGS #include #include #include +#include CAF_POP_WARNINGS namespace xstudio::ui::qml { @@ -30,7 +30,8 @@ class HELPER_QML_EXPORT UIModelData : public caf::mixin::actor_object &role_names = {}); explicit UIModelData(QObject *parent); @@ -39,17 +40,22 @@ class HELPER_QML_EXPORT UIModelData : public caf::mixin::actor_object context_object_lookup_map_; }; class HELPER_QML_EXPORT MenusModelData : public UIModelData { @@ -103,23 +115,58 @@ class HELPER_QML_EXPORT ViewsModelData : public UIModelData { // call this function to register a widget (or view) that can be used to // fill an xSTUDIO panel in the interface. See main.qml for examples. - void register_view(QString qml_source, QString view_name); + void register_view(QString qml_source, QString view_name, float position); // call this function to retrieve the QML source (or the path to the // source .qml file) for the given view QVariant view_qml_source(QString view_name); }; -class HELPER_QML_EXPORT ReskinPanelsModel : public UIModelData { +class HELPER_QML_EXPORT PopoutWindowsData : public UIModelData { + + Q_OBJECT + + public: + explicit PopoutWindowsData(QObject *parent = nullptr); + + public slots: + + // call this function to register a widget (or view) that can be shown in + // a pop-out window via a button in the tool shelf at the top-left of the + // viewport window. + void register_popout_window( + QString name, + QString qml_source, + QString icon_path, + float button_position, + const QUuid hotkey = QUuid()); +}; + +class HELPER_QML_EXPORT SingletonsModelData : public UIModelData { + + Q_OBJECT + + public: + explicit SingletonsModelData(QObject *parent = nullptr); + + public slots: + + void register_singleton_qml(const QString &qml_code); +}; + +class HELPER_QML_EXPORT PanelsModel : public UIModelData { Q_OBJECT public: - explicit ReskinPanelsModel(QObject *parent = nullptr); + explicit PanelsModel(QObject *parent = nullptr); Q_INVOKABLE void close_panel(QModelIndex panel_index); Q_INVOKABLE void split_panel(QModelIndex panel_index, bool horizontal_split); - Q_INVOKABLE void duplicate_layout(QModelIndex panel_index); + Q_INVOKABLE int add_layout(QString layout_name, QModelIndex root, QString layoutType); + Q_INVOKABLE QModelIndex duplicate_layout(QModelIndex panel_index); + Q_INVOKABLE void storeFloatingWindowData(QString window_name, QVariant data); + Q_INVOKABLE QVariant retrieveFloatingWindowData(QString window_name); }; class HELPER_QML_EXPORT MediaListColumnsModel : public UIModelData { @@ -139,7 +186,7 @@ class HELPER_QML_EXPORT MenuModelItem : public caf::mixin::actor_object explicit MenuModelItem(QObject *parent = nullptr); - ~MenuModelItem() override; + virtual ~MenuModelItem() override; virtual void init(caf::actor_system &system); @@ -153,9 +200,17 @@ class HELPER_QML_EXPORT MenuModelItem : public caf::mixin::actor_object Q_PROPERTY(QString currentChoice READ currentChoice WRITE setCurrentChoice NOTIFY currentChoiceChanged) Q_PROPERTY(bool isChecked READ isChecked WRITE setIsChecked NOTIFY isCheckedChanged) - Q_PROPERTY(QString hotkey READ hotkey WRITE setHotkey NOTIFY hotkeyChanged) + Q_PROPERTY(QUuid hotkeyUuid READ hotkeyUuid WRITE setHotkeyUuid NOTIFY hotkeyUuidChanged) Q_PROPERTY( QString menuItemType READ menuItemType WRITE setMenuItemType NOTIFY menuItemTypeChanged) + Q_PROPERTY(QString menuCustomIcon READ menuCustomIcon WRITE setMenuCustomIcon NOTIFY + menuCustomIconChanged) + Q_PROPERTY(QString customMenuQml READ customMenuQml WRITE setCustomMenuQml NOTIFY + customMenuQmlChanged) + Q_PROPERTY(QVariant userData READ userData WRITE setUserData NOTIFY userDataChanged) + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(QObject *panelContext READ panelContext WRITE setPanelContext NOTIFY + panelContextChanged) const QString &menuPath() const { return menu_path_; } const QString &text() const { return text_; } @@ -166,6 +221,12 @@ class HELPER_QML_EXPORT MenuModelItem : public caf::mixin::actor_object bool isChecked() const { return is_checked_; } const QString &hotkey() const { return hotkey_; } const QString &menuItemType() const { return menu_item_type_; } + const QUuid &hotkeyUuid() const { return hotkey_uuid_; } + const QString &menuCustomIcon() const { return menu_custom_icon_; } + const QString &customMenuQml() const { return custom_menu_qml_; } + const QVariant &userData() const { return user_data_; } + bool enabled() const { return enabled_; } + QObject *panelContext() const { return panel_context_; } public slots: @@ -200,10 +261,10 @@ class HELPER_QML_EXPORT MenuModelItem : public caf::mixin::actor_object insertIntoMenuModel(); } } - void setHotkey(const QString &hotkey) { - if (hotkey != hotkey_) { - hotkey_ = hotkey; - emit hotkeyChanged(); + void setHotkeyUuid(const QUuid &hotkey_uuid) { + if (hotkey_uuid != hotkey_uuid_) { + hotkey_uuid_ = hotkey_uuid; + emit hotkeyUuidChanged(); insertIntoMenuModel(); } } @@ -214,22 +275,68 @@ class HELPER_QML_EXPORT MenuModelItem : public caf::mixin::actor_object insertIntoMenuModel(); } } + void setMenuCustomIcon(const QString &menu_custom_icon) { + if (menu_custom_icon != menu_custom_icon_) { + menu_custom_icon_ = menu_custom_icon; + emit menuCustomIconChanged(); + insertIntoMenuModel(); + } + } + void setCustomMenuQml(const QString &custom_menu_qml) { + if (custom_menu_qml != custom_menu_qml_) { + custom_menu_qml_ = custom_menu_qml; + emit customMenuQmlChanged(); + insertIntoMenuModel(); + } + } + void setUserData(const QVariant &user_data) { + if (user_data != user_data_) { + user_data_ = user_data; + emit userDataChanged(); + insertIntoMenuModel(); + } + } + + void setEnabled(const bool e) { + if (enabled_ != e) { + enabled_ = e; + emit enabledChanged(); + insertIntoMenuModel(); + } + } + + void setPanelContext(QObject *obj) { + if (panel_context_ != obj) { + panel_context_ = obj; + emit panelContextChanged(); + insertIntoMenuModel(); + } + } + + void setMenuPathPosition(const QString &menu_path, const QVariant position); signals: void menuPathChanged(); void menuNameChanged(); void textChanged(); - void activated(); + void activated(QObject *menuContext); + void hotkeyActivated(); void menuItemPositionChanged(); void choicesChanged(); void currentChoiceChanged(); void isCheckedChanged(); - void hotkeyChanged(); void menuItemTypeChanged(); + void hotkeyUuidChanged(); + void menuCustomIconChanged(); + void customMenuQmlChanged(); + void userDataChanged(); + void enabledChanged(); + void panelContextChanged(); private: void insertIntoMenuModel(); + QObject *contextPanel(); QString menu_name_; QString menu_path_ = QString("Undefined"); @@ -237,12 +344,84 @@ class HELPER_QML_EXPORT MenuModelItem : public caf::mixin::actor_object QString hotkey_; QString current_choice_; QString menu_item_type_ = QString("button"); + QString menu_custom_icon_; + QString custom_menu_qml_; QStringList choices_; + QUuid hotkey_uuid_; + QVariant user_data_; + QObject *panel_context_ = {nullptr}; bool is_checked_ = {false}; + bool enabled_ = {true}; float menu_item_position_ = -1.0f; utility::Uuid model_entry_id_ = utility::Uuid::generate(); bool dont_update_model_ = {false}; }; +/* In the xSTUDIO interface, we can have multiple instances of the same type +of panel. For example, the media list view might be instanced several times. +Each instance will declare its own 'MenuModelItems' to insert items into the +right click menu model. However, there is only one 'model' that describes what's +in the right click menu... we do it this way because there are other plugins +that might want to insert items into the media list menu and they only want to +deal with one menu model, not separate menu models for each media list view. + +Our solution is that a MenuModelItem can declare which panel instance it was +created in. Then, on the creation of the menu itself, we use this filter +to only include menu items that originated from the panel that has created the +menu. The MenuModelItem has a string property panelContext - we set this to +the stringified address of the parent panel. When the corresponding entry is +made in the central menu model this is recorded in the role 'menu_item_context'. +Then when the pop-up menu is built from the central menu model, we check if the +menu item has 'menu_item_context' and only add an entry on the menu *IF* the +menu_item_context matches the stringified address of the parent panel that is +creating the pop-up. simples ;-)*/ +class HELPER_QML_EXPORT PanelMenuModelFilter : public QSortFilterProxyModel { + + Q_OBJECT + + Q_PROPERTY( + QString panelAddress READ panelAddress WRITE setPanelAddress NOTIFY panelAddressChanged) + + public: + PanelMenuModelFilter(QObject *parent = nullptr); + + const QString &panelAddress() const { return panel_address_; } + + Q_INVOKABLE [[nodiscard]] QVariant + get(const QModelIndex &item, const QString &role = "display") const { + if (source_model_) { + return source_model_->get(mapToSource(item), role); + } + return QVariant(); + } + + void setSourceModel(QAbstractItemModel *sourceModel) override; + + public slots: + + void setPanelAddress(const QString &panelAddress) { + if (panelAddress != panel_address_) { + panel_address_ = panelAddress; + emit panelAddressChanged(); + invalidateFilter(); + } + } + + void nodeActivated(const QModelIndex &idx, const QString &data, QObject *panel) { + source_model_->nodeActivated(mapToSource(idx), data, panel); + } + + protected: + [[nodiscard]] bool + filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + + signals: + void panelAddressChanged(); + + private: + QString panel_address_; + int menu_item_context_role_id_ = {0}; + UIModelData *source_model_ = nullptr; +}; } // namespace xstudio::ui::qml \ No newline at end of file diff --git a/include/xstudio/ui/qml/module_data_ui.hpp b/include/xstudio/ui/qml/module_data_ui.hpp index 4986a59e1..37c691f56 100644 --- a/include/xstudio/ui/qml/module_data_ui.hpp +++ b/include/xstudio/ui/qml/module_data_ui.hpp @@ -20,7 +20,26 @@ class HELPER_QML_EXPORT ModulesModelData : public UIModelData { Q_OBJECT public: + Q_PROPERTY(QString singleAttributeName READ singleAttributeName WRITE setSingleAttributeName + NOTIFY singleAttributeNameChanged) + explicit ModulesModelData(QObject *parent = nullptr); + + QString singleAttributeName() { return single_attr_name_; } + + Q_INVOKABLE void setSingleAttributeName(const QString single_attr_name); + + Q_INVOKABLE QVariant + attributeRoleData(const QString attr_name, const QString role_name = QString("value")); + + void setModelData(const nlohmann::json &data) override; + + signals: + + void singleAttributeNameChanged(); + + private: + QString single_attr_name_; }; } // namespace xstudio::ui::qml \ No newline at end of file diff --git a/include/xstudio/ui/qml/module_menu_ui.hpp b/include/xstudio/ui/qml/module_menu_ui.hpp deleted file mode 100644 index 60bf56e2f..000000000 --- a/include/xstudio/ui/qml/module_menu_ui.hpp +++ /dev/null @@ -1,135 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -// include CMake auto-generated export hpp -#include "xstudio/ui/qml/module_qml_export.h" - -#include -#include - -CAF_PUSH_WARNINGS -#include -#include -#include -#include -CAF_POP_WARNINGS - -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/module/module.hpp" - - -namespace xstudio { -namespace ui { - namespace qml { - - class ModuleAttrsToQMLShim; - - class MODULE_QML_EXPORT ModuleMenusModel : public QAbstractListModel { - - Q_OBJECT - - public: - enum XsMenuRoles { - MenuText = Qt::UserRole + 1024, - IsCheckable, - IsChecked, - IsMultiChoice, - Value, - Enabled, - IsDivider, - Uuid, - AttrType, - HotkeyUuid - }; - - inline static const std::map role_names = { - {MenuText, "xs_module_menu_item_text"}, - {IsCheckable, "xs_module_menu_item_checkable"}, - {IsChecked, "xs_module_menu_item_checked"}, - {IsMultiChoice, "xs_module_menu_item_is_multichoice"}, - {Value, "xs_module_menu_item_value"}, - {Enabled, "xs_module_menu_item_enabled"}, - {IsDivider, "xs_module_menu_item_is_divider"}, - {Uuid, "xs_module_menu_item_uuid"}, - {AttrType, "xs_module_menu_item_attr_type"}, - {HotkeyUuid, "xs_module_menu_hotkey_uuid"}}; - - Q_PROPERTY(int num_submenus READ num_submenus NOTIFY num_submenusChanged) - Q_PROPERTY(QString root_menu_name READ rootMenuName WRITE setRootMenuName NOTIFY - rootMenuNameChanged) - Q_PROPERTY(QString title READ title NOTIFY titleChanged) - Q_PROPERTY(QStringList submenu_names READ submenu_names NOTIFY submenu_namesChanged) - Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged) - - ModuleMenusModel(QObject *parent = nullptr); - - ~ModuleMenusModel() override; - - [[nodiscard]] int rowCount(const QModelIndex &parent) const override; - - [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; - - [[nodiscard]] QHash roleNames() const override; - - bool setData(const QModelIndex &index, const QVariant &value, int role) override; - - void add_attributes_from_backend(const module::AttributeSet &attrs); - - [[nodiscard]] Qt::ItemFlags flags(const QModelIndex &) const override { - return Qt::ItemIsEnabled | Qt::ItemIsEditable; - } - - void update_attribute_from_backend( - const utility::Uuid &attr_uuid, - const int role, - const utility::JsonStore &role_value); - - void update_full_attribute_from_backend(const module::ConstAttributePtr &attr); - - void remove_attribute(const utility::Uuid &attr_uuid); - - [[nodiscard]] int num_submenus() const { return submenu_names_.size(); } - - [[nodiscard]] bool empty() const { return attributes_data_.empty(); } - - signals: - - void setAttributeFromFrontEnd(const QUuid, const int, const QVariant); - void rootMenuNameChanged(QString); - void num_submenusChanged(); - void titleChanged(); - void submenu_namesChanged(); - void emptyChanged(); - - public slots: - - [[nodiscard]] QString rootMenuName() const { return menu_path_; } - [[nodiscard]] QString title() const { return title_; } - void setRootMenuName(QString p); - // QObject * submenu(int index); - [[nodiscard]] QStringList submenu_names() const { return submenu_names_; } - - private: - bool already_have_attr_in_this_menu(const QUuid &uuid); - bool is_attr_in_this_menu(const module::ConstAttributePtr &attr); - void add_multi_choice_menu_item(const module::ConstAttributePtr &attr); - void add_checkable_menu_item(const module::ConstAttributePtr &attr); - void add_menu_action_item(const module::ConstAttributePtr &attr); - void update_multi_choice_menu_item( - const utility::Uuid &attr_uuid, const utility::JsonStore &string_choice_data); - - std::vector> attributes_data_; - - QStringList submenu_names_; - QMap> attrs_per_submenus_; - - // QList submenus_; - QString menu_path_; - QString title_; - int menu_nesting_depth_ = {0}; - ModuleAttrsToQMLShim *shim_ = {nullptr}; - }; - - } // namespace qml -} // namespace ui -} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/qml/module_ui.hpp b/include/xstudio/ui/qml/module_ui.hpp deleted file mode 100644 index 793cea5fb..000000000 --- a/include/xstudio/ui/qml/module_ui.hpp +++ /dev/null @@ -1,194 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -// include CMake auto-generated export hpp -#include "xstudio/ui/qml/module_qml_export.h" - -#include -#include - -CAF_PUSH_WARNINGS -#include -#include -#include -#include -#include -CAF_POP_WARNINGS - -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/ui/qml/module_menu_ui.hpp" -#include "xstudio/ui/qml/json_tree_model_ui.hpp" -#include "xstudio/module/module.hpp" - -namespace xstudio { -namespace ui { - namespace qml { - - class MODULE_QML_EXPORT ModuleAttrsDirect : public QQmlPropertyMap { - - Q_OBJECT - - public: - Q_PROPERTY(QStringList attributesGroupNames READ attributesGroupNames WRITE - setattributesGroupNames NOTIFY attributesGroupNamesChanged) - Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged) - - ModuleAttrsDirect(QObject *parent = nullptr); - virtual ~ModuleAttrsDirect(); - - void add_attributes_from_backend( - const module::AttributeSet &attrs, const bool check_group = false); - - void update_attribute_from_backend( - const utility::Uuid &attr_uuid, - const int role, - const utility::JsonStore &role_value); - - void remove_attribute(const utility::Uuid &attr_uuid); - - public slots: - - [[nodiscard]] QStringList attributesGroupNames() const { return attrs_group_name_; } - [[nodiscard]] QString roleName() const { return role_name_; } - - void setattributesGroupNames(QStringList group_name); - void setRoleName(QString group_name); - - signals: - - void setAttributeFromFrontEnd(const QUuid, const int, const QVariant); - void attributesGroupNamesChanged(QStringList group_name); - void attrAdded(QString attr_name); - void roleNameChanged(); - - protected: - QVariant updateValue(const QString &key, const QVariant &input) override; - - private: - QMap attr_uuids_by_name_; - QMap attr_names_by_uuid_; - QMap attr_values_by_uuid_; - QStringList attrs_group_name_; - QString role_name_; - }; - - - class MODULE_QML_EXPORT OrderedModuleAttrsModel : public QSortFilterProxyModel { - Q_OBJECT - - Q_PROPERTY(QStringList attributesGroupNames READ attributesGroupNames WRITE - setattributesGroupNames NOTIFY attributesGroupNamesChanged) - - public: - OrderedModuleAttrsModel(QObject *parent = nullptr); - // ~OrderedModuleAttrsModel() override = default; - - signals: - void attributesGroupNamesChanged(QStringList group_name); - - public slots: - [[nodiscard]] QStringList attributesGroupNames() const; - void setattributesGroupNames(const QStringList &group_name); - }; - - - class MODULE_QML_EXPORT ModuleAttrsModel : public QAbstractListModel { - - Q_OBJECT - - public: - Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged) - - Q_PROPERTY(QStringList attributesGroupNames READ attributesGroupNames WRITE - setattributesGroupNames NOTIFY attributesGroupNamesChanged) - - ModuleAttrsModel(QObject *parent = nullptr); - virtual ~ModuleAttrsModel(); - - [[nodiscard]] int rowCount() { return rowCount(QModelIndex()); } - - [[nodiscard]] int rowCount(const QModelIndex &parent) const override; - - [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; - - [[nodiscard]] QHash roleNames() const override; - - bool setData(const QModelIndex &index, const QVariant &value, int role) override; - - void add_attributes_from_backend( - const module::AttributeSet &attrs, const bool check_group = false); - - [[nodiscard]] Qt::ItemFlags flags(const QModelIndex &) const override { - return Qt::ItemIsEnabled | Qt::ItemIsEditable; - } - - void update_attribute_from_backend( - const utility::Uuid &attr_uuid, - const int role, - const utility::JsonStore &role_value); - - void update_full_attribute_from_backend(const module::ConstAttributePtr &attr); - - void remove_attribute(const utility::Uuid &attr_uuid); - - signals: - - void setAttributeFromFrontEnd(const QUuid, const int, const QVariant); - void rowCountChanged(); - void doFullupdateFromBackend(QStringList group_name); - void attributesGroupNamesChanged(QStringList group_name); - - public slots: - - [[nodiscard]] QStringList attributesGroupNames() const { return attrs_group_name_; } - - void setattributesGroupNames(QStringList group_name); - - private: - bool have_attr(const QUuid &uuid); - - std::vector> attributes_data_; - QStringList attrs_group_name_; - }; - - class ModuleAttrsToQMLShim : public QMLActor { - - Q_OBJECT - - public: - ModuleAttrsToQMLShim(ModuleAttrsModel *model); - ModuleAttrsToQMLShim(ModuleAttrsDirect *model); - ModuleAttrsToQMLShim(ModuleMenusModel *model); - ~ModuleAttrsToQMLShim() override; - - void init(caf::actor_system &system) override; - - void clear() { - model_ = nullptr; - qml_attrs_map_ = nullptr; - menu_model_ = nullptr; - } - - signals: - - public slots: - - void setAttributeFromFrontEnd( - const QUuid property_uuid, const int role, const QVariant role_value); - void setattributesGroupNames(QStringList group_name); - void setRootMenuName(QString root_menu_name); - void doFullupdateFromBackend(QStringList group_name); - - private: - ModuleAttrsModel *model_ = {nullptr}; - ModuleAttrsDirect *qml_attrs_map_ = {nullptr}; - ModuleMenusModel *menu_model_ = {nullptr}; - utility::Uuid uuid_; - caf::actor attrs_events_actor_; - caf::actor attrs_events_actor_group_; - QStringList attrs_group_name_; - }; - - } // namespace qml -} // namespace ui -} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/qml/playhead_ui.hpp b/include/xstudio/ui/qml/playhead_ui.hpp deleted file mode 100644 index 82119c4c0..000000000 --- a/include/xstudio/ui/qml/playhead_ui.hpp +++ /dev/null @@ -1,304 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include - -CAF_PUSH_WARNINGS -#include -#include -#include -CAF_POP_WARNINGS - -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/utility/frame_time.hpp" - -namespace xstudio { -namespace utility { - class MediaReference; -} -namespace ui { - namespace qml { - - class TimesliderMarker : public QObject { - Q_OBJECT - Q_PROPERTY(int x READ x NOTIFY xChanged) - Q_PROPERTY(int y READ y NOTIFY yChanged) - Q_PROPERTY(QString c READ c NOTIFY cChanged) - QML_ELEMENT - - public: - explicit TimesliderMarker( - const int x, const int y, const QString c = "", QObject *parent = nullptr) - : QObject(parent), x_(x), y_(y), c_(std::move(c)) {} - ~TimesliderMarker() override = default; - [[nodiscard]] int x() const { return x_; } - [[nodiscard]] int y() const { return y_; } - [[nodiscard]] QString c() const { return c_; } - - signals: - void xChanged(); - void yChanged(); - void cChanged(); - - private: - int x_; - int y_; - QString c_; - }; - - class PlayheadUI : public QMLActor { - - Q_OBJECT - Q_PROPERTY(bool playing READ playing WRITE setPlaying NOTIFY playingChanged) - Q_PROPERTY(int loopMode READ loopMode WRITE setLoopMode NOTIFY loopModeChanged) - Q_PROPERTY( - QVariant loopModeOptions READ loopModeOptions NOTIFY loopModeOptionsChanged) - Q_PROPERTY(bool native READ native WRITE setNative NOTIFY nativeChanged) - Q_PROPERTY(int playRateMode READ playRateMode WRITE setPlayRateMode NOTIFY - playRateModeChanged) - Q_PROPERTY(bool forward READ forward WRITE setForward NOTIFY forwardChanged) - Q_PROPERTY(double playheadRate READ playheadRate WRITE setPlayheadRate NOTIFY - playheadRateChanged) - Q_PROPERTY(double rate READ rate NOTIFY rateChanged) - Q_PROPERTY(float velocityMultiplier READ velocityMultiplier WRITE - setVelocityMultiplier NOTIFY velocityMultiplierChanged) - - Q_PROPERTY(int frame READ frame WRITE setFrame NOTIFY frameChanged) - Q_PROPERTY(int frames READ frames NOTIFY framesChanged) - - Q_PROPERTY(double second READ second NOTIFY secondChanged) - Q_PROPERTY(double seconds READ seconds NOTIFY secondsChanged) - - Q_PROPERTY(int mediaFrame READ mediaFrame NOTIFY mediaFrameChanged) - Q_PROPERTY(double mediaSecond READ mediaSecond NOTIFY mediaSecondChanged) - - // logical frame - absolute - Q_PROPERTY( - int mediaLogicalFrame READ mediaLogicalFrame NOTIFY mediaLogicalFrameChanged) - Q_PROPERTY(double mediaLogicalSecond READ mediaLogicalSecond NOTIFY - mediaLogicalSecondChanged) - - Q_PROPERTY(QString timecodeStart READ timecodeStart NOTIFY timecodeStartChanged) - Q_PROPERTY(QString timecode READ timecode NOTIFY timecodeChanged) - Q_PROPERTY(int timecodeFrames READ timecodeFrames NOTIFY timecodeFramesChanged) - Q_PROPERTY(QString timecodeEnd READ timecodeEnd NOTIFY timecodeEndChanged) - Q_PROPERTY( - QString timecodeDuration READ timecodeDuration NOTIFY timecodeDurationChanged) - - Q_PROPERTY(QUuid mediaUuid READ mediaUuid NOTIFY mediaUuidChanged) - - Q_PROPERTY(QString uuid READ uuid NOTIFY uuidChanged) - Q_PROPERTY(QString name READ name NOTIFY nameChanged) - Q_PROPERTY(int loopStart READ loopStart WRITE setLoopStart NOTIFY loopStartChanged) - Q_PROPERTY(int loopEnd READ loopEnd WRITE setLoopEnd NOTIFY loopEndChanged) - Q_PROPERTY(bool useLoopRange READ useLoopRange WRITE setUseLoopRange NOTIFY - useLoopRangeChanged) - Q_PROPERTY(int keyPlayheadIndex READ keyPlayheadIndex WRITE setKeyPlayheadIndex - NOTIFY keyPlayheadIndexChanged) - Q_PROPERTY( - QString compareLayerName READ compareLayerName NOTIFY compareLayerNameChanged) - Q_PROPERTY(int sourceOffsetFrames READ sourceOffsetFrames WRITE - setSourceOffsetFrames NOTIFY sourceOffsetFramesChanged) - Q_PROPERTY(QList cachedFrames READ cachedFrames NOTIFY cachedFramesChanged) - Q_PROPERTY(QList bookmarkedFrames READ bookmarkedFrames NOTIFY - bookmarkedFramesChanged) - - Q_PROPERTY(bool isNull READ isNull NOTIFY isNullChanged) - - public: - explicit PlayheadUI(QObject *parent = nullptr); - ~PlayheadUI() override = default; - - void init(caf::actor_system &system) override; - void set_backend(caf::actor backend); - - caf::actor backend() { return backend_; } - - [[nodiscard]] QUuid mediaUuid() const { return media_uuid_; } - - [[nodiscard]] bool playing() const { return playing_; } - void setPlaying(const bool playing); - - [[nodiscard]] int loopMode() const { return looping_; } - void setLoopMode(const int looping); - - [[nodiscard]] QVariant loopModeOptions() const { return loop_mode_options_; } - - [[nodiscard]] bool forward() const { return forward_; } - void setForward(const bool forward); - - [[nodiscard]] bool native() const { - return play_rate_mode_ == utility::TimeSourceMode::REMAPPED; - } - void setNative(const bool native); - - [[nodiscard]] int playRateMode() const { return static_cast(play_rate_mode_); } - void setPlayRateMode(const int tsm); - - [[nodiscard]] double playheadRate() const { return playhead_rate_; } - void setPlayheadRate(const double playhead_rate); - - [[nodiscard]] double rate() const { return rate_; } - - [[nodiscard]] bool isNull() const { return !bool(backend_); } - // void setRate(const double rate); - - [[nodiscard]] QList cachedFrames() const { return cache_detail_; } - [[nodiscard]] QList bookmarkedFrames() const { - return bookmark_detail_ui_; - } - - [[nodiscard]] float velocityMultiplier() const { return velocity_multiplier_; } - void setVelocityMultiplier(const float velocity_multiplier); - - [[nodiscard]] double seconds() const { return seconds_; } - [[nodiscard]] double second() const { return second_; } - [[nodiscard]] double mediaSecond() const { return media_second_; } - [[nodiscard]] double mediaLogicalSecond() const { return media_logical_second_; } - - void setFrame(const int frame); - [[nodiscard]] int frames() const { return frames_; } - [[nodiscard]] int frame() const { return frame_; } - [[nodiscard]] int mediaFrame() const { return media_frame_; } - [[nodiscard]] int mediaLogicalFrame() const { return media_logical_frame_; } - - [[nodiscard]] int loopStart() const { return loop_start_; } - [[nodiscard]] int loopEnd() const { return loop_end_; } - [[nodiscard]] int useLoopRange() const { return use_loop_range_; } - - [[nodiscard]] int keyPlayheadIndex() const { return key_playhead_index_; } - void setKeyPlayheadIndex(int i); - - [[nodiscard]] int sourceOffsetFrames() const { return source_offset_frames_; } - void setSourceOffsetFrames(int i); - - [[nodiscard]] QString uuid() const { - return QString::fromStdString(to_string(uuid_)); - } - - [[nodiscard]] QString timecodeStart() const { return timecode_start_; } - [[nodiscard]] QString timecode() const { return timecode_; } - [[nodiscard]] int timecodeFrames() const { return timecode_frames_; } - [[nodiscard]] QString timecodeEnd() const { return timecode_end_; } - [[nodiscard]] QString timecodeDuration() const { return timecode_duration_; } - QString compareLayerName(); - [[nodiscard]] QString name() const { return name_; } - - signals: - void uuidChanged(); - void frameChanged(); - void mediaFrameChanged(); - void mediaLogicalFrameChanged(); - void framesChanged(); - void secondChanged(); - void mediaSecondChanged(); - void mediaLogicalSecondChanged(); - void secondsChanged(); - void velocityMultiplierChanged(); - void playingChanged(); - void forwardChanged(); - void loopModeChanged(); - void loopModeOptionsChanged(); - void nativeChanged(); - void playheadRateChanged(); - void rateChanged(); - void playRateModeChanged(); - void playlistSelectionThingChanged(); - void backendChanged(); - void loopStartChanged(); - void loopEndChanged(); - void useLoopRangeChanged(); - void nameChanged(); - void cachedFramesChanged(); - void bookmarkedFramesChanged(); - - void timecodeStartChanged(); - void timecodeChanged(); - void timecodeFramesChanged(); - void timecodeEndChanged(); - void timecodeDurationChanged(); - - void keyPlayheadIndexChanged(); - void compareLayerNameChanged(); - void sourceOffsetFramesChanged(); - - void isNullChanged(); - - void mediaUuidChanged(QUuid); - - - public slots: - - void initSystem(QObject *system_qobject); - void step(int step_frames); - void skip(const bool forwards); - void setLoopStart(const int loop_start); - void setLoopEnd(const int loop_end); - void setUseLoopRange(const bool use_loop_range); - bool jumpToNextSource(); - bool jumpToPreviousSource(); - void jumpToSource(const QUuid media_uuid); - void setFitMode(const QString mode); - - void connectToUI(); - void disconnectFromUI(); - void resetReadaheadRequests(); - - [[nodiscard]] int nextBookmark(const int search_from_frame = -1) const; - [[nodiscard]] int previousBookmark(int search_from_frame = -1) const; - [[nodiscard]] QVariantList getNearestBookmark(const int search_from_frame) const; - - private: - void media_changed(); - void rebuild_cache(); - void rebuild_bookmarks(); - - private: - caf::actor backend_; - caf::actor backend_events_; - utility::Uuid uuid_; - bool playing_; - int looping_; - QVariant loop_mode_options_; - - utility::TimeSourceMode play_rate_mode_; - bool forward_; - - double rate_; - utility::FrameRate fr_rate_; - - double playhead_rate_; - utility::FrameRate fr_playhead_rate_; - - float velocity_multiplier_; - int frames_{0}; - double seconds_{0.0}; - int frame_{0}; - int media_frame_{0}; - int media_logical_frame_{0}; - int loop_start_; - int loop_end_; - bool use_loop_range_; - double second_{0.0}; - double media_second_{0.0}; - double media_logical_second_{0.0}; - int key_playhead_index_; - QString timecode_start_; - QString timecode_; - int timecode_frames_{0}; - QString timecode_end_; - QString timecode_duration_; - QString name_; - int compare_mode_; - int source_offset_frames_; - QVariant compare_mode_options_; - - QList cache_detail_; - QList bookmark_detail_ui_; - std::vector> bookmark_detail_; - QUuid media_uuid_; - }; - } // namespace qml -} // namespace ui -} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/qml/qml_viewport.hpp b/include/xstudio/ui/qml/qml_viewport.hpp index e0a8c2a90..048c6814f 100644 --- a/include/xstudio/ui/qml/qml_viewport.hpp +++ b/include/xstudio/ui/qml/qml_viewport.hpp @@ -1,17 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#include + // include CMake auto-generated export hpp #include "xstudio/ui/qml/viewport_qml_export.h" -#include - CAF_PUSH_WARNINGS #include #include #include -#include +#include #include #include #include @@ -30,52 +30,37 @@ namespace ui { namespace qml { class QMLViewportRenderer; - class PlayheadUI; class VIEWPORT_QML_EXPORT QMLViewport : public QQuickItem { Q_OBJECT - Q_PROPERTY(float zoom READ zoom WRITE setZoom NOTIFY zoomChanged) - Q_PROPERTY(bool frameOutOfRange READ frameOutOfRange NOTIFY frameOutOfRangeChanged) - Q_PROPERTY(bool noAlphaChannel READ noAlphaChannel NOTIFY noAlphaChannelChanged) - Q_PROPERTY(QString fpsExpression READ fpsExpression NOTIFY fpsExpressionChanged) - Q_PROPERTY(float scale READ scale WRITE setScale NOTIFY scaleChanged) - Q_PROPERTY( - QVector2D translate READ translate WRITE setTranslate NOTIFY translateChanged) - Q_PROPERTY(QObject *playhead READ playhead NOTIFY playheadChanged) - Q_PROPERTY(int mouseButtons READ mouseButtons NOTIFY mouseButtonsChanged) - Q_PROPERTY(QPoint mouse READ mouse NOTIFY mouseChanged) - Q_PROPERTY(int onScreenImageLogicalFrame READ onScreenImageLogicalFrame NOTIFY - onScreenImageLogicalFrameChanged) - Q_PROPERTY(QRectF imageBoundaryInViewport READ imageBoundaryInViewport NOTIFY - imageBoundaryInViewportChanged) - Q_PROPERTY(QSize imageResolution READ imageResolution NOTIFY imageResolutionChanged) - Q_PROPERTY(bool enableShortcuts READ enableShortcuts NOTIFY enableShortcutsChanged) + Q_PROPERTY(QPointF mousePosition READ mousePosition NOTIFY mousePositionChanged) + Q_PROPERTY(QVariantList imageBoundariesInViewport READ imageBoundariesInViewport + NOTIFY imageBoundariesInViewportChanged) + Q_PROPERTY(QVariantList imageResolutions READ imageResolutions NOTIFY + imageResolutionsChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged) - Q_PROPERTY(bool isQuickViewer READ isQuickViewer WRITE setIsQuickViewer NOTIFY - isQuickViewerChanged) + Q_PROPERTY(QUuid playheadUuid READ playheadUuid NOTIFY playheadUuidChanged) + Q_PROPERTY(bool hasPlayhead READ hasPlayhead NOTIFY playheadUuidChanged) public: QMLViewport(QQuickItem *parent = nullptr); virtual ~QMLViewport(); - float zoom(); - QString fpsExpression(); - float scale(); - QVector2D translate(); - QObject *playhead(); + + [[nodiscard]] QUuid playheadUuid() const { return playhead_uuid_; } [[nodiscard]] QString name() const; - [[nodiscard]] int mouseButtons() const { return mouse_buttons; } - [[nodiscard]] QPoint mouse() const { return mouse_position; } - [[nodiscard]] int onScreenImageLogicalFrame() const { - return on_screen_logical_frame_; - } - [[nodiscard]] bool frameOutOfRange() const { return frame_out_of_range_; } - [[nodiscard]] bool noAlphaChannel() const { return no_alpha_channel_; } - [[nodiscard]] bool enableShortcuts() const { return enable_shortcuts_; } - void setPlayhead(caf::actor playhead); + [[nodiscard]] QPointF mousePosition() const { return mouse_position; } + [[nodiscard]] bool hasPlayhead() const { return !playhead_uuid_.isNull(); } + QMLViewportRenderer *viewportActor() { return renderer_actor; } void deleteRendererActor(); - bool isQuickViewer() const { return is_quick_viewer_; } + + void setPlayheadUuid(const QUuid &uuid) { + if (uuid != playhead_uuid_) { + playhead_uuid_ = uuid; + emit playheadUuidChanged(); + } + } protected: void hoverEnterEvent(QHoverEvent *event) override; @@ -94,33 +79,19 @@ namespace ui { void sync(); void cleanup(); - void setZoom(const float z); - void revertFitZoomToPrevious(const bool ignoreOtherViewport = false); - void linkToViewport(QObject *other_viewport); void handleScreenChanged(QScreen *screen); void hideCursor(); void showCursor(); - QSize imageResolution(); - QVector2D bboxCornerInViewport(const int min_x, const int min_y); - void setScale(const float s); - void setTranslate(const QVector2D &tr); - void setOnScreenImageLogicalFrame(const int frame_num); - QRectF imageBoundaryInViewport(); - void setFrameOutOfRange(bool frame_out_of_range); - void setNoAlphaChannel(bool no_alpha_channel); - void renderImageToFile( - const QUrl filePath, - const int format, - const int compression, - const int width, - const int height, - const bool bakeColor); + QVariantList imageResolutions(); + QVariantList imageBoundariesInViewport(); void sendResetShortcut(); void setOverrideCursor(const QString &name, const bool centerOffset); void setOverrideCursor(const Qt::CursorShape cname); - void setRegularCursor(const Qt::CursorShape cname); - void setIsQuickViewer(const bool is_quick_viewer); + void setPlayhead(const QString actorAddress); + void reset(); + QString playheadActorAddress(); + void onVisibleChanged(); private slots: @@ -128,55 +99,50 @@ namespace ui { signals: - void zoomChanged(float); - void fpsExpressionChanged(QString); - void scaleChanged(float); - void playheadChanged(QObject *); - void translateChanged(QVector2D); - void mouseButtonsChanged(); - void mouseChanged(); - void onScreenImageLogicalFrameChanged(); - void imageBoundaryInViewportChanged(); - void imageResolutionChanged(); - void frameOutOfRangeChanged(); - void noAlphaChannelChanged(); - void enableShortcutsChanged(); + void mouseRelease(Qt::MouseButtons buttons); + void mouseDoubleClick( + QPointF position, Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers); + void mousePress( + QPointF position, Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers); + void mousePositionChanged( + QPointF position, Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers); + void mousePressScreenPixels( + QPointF position, Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers); + + void imageBoundariesInViewportChanged(); + void imageResolutionsChanged(); void doSnapshot(QString, QString, int, int, bool); void nameChanged(); - void quickViewSource(QStringList mediaActors, QString compareMode); + void quickViewSource( + QStringList mediaActors, QString compareMode, int in_pt, int out_pt); void quickViewBackendRequest(QStringList mediaActors, QString compareMode); void quickViewBackendRequestWithSize( QStringList mediaActors, QString compareMode, QPoint position, QSize size); void snapshotRequestResult(QString resultMessage); void pointerEntered(); void pointerExited(); - void isQuickViewerChanged(); + void playheadUuidChanged(); private: void releaseResources() override; - PointerEvent makePointerEvent( - Signature::EventType t, QMouseEvent *event, int force_modifiers = 0); - PointerEvent makePointerEvent( - Signature::EventType t, int buttons, int x, int y, int w, int h, int modifiers); + + void sendPointerEvent(EventType t, QMouseEvent *event, int force_modifiers = 0); + void sendPointerEvent(QHoverEvent *event); + QPointF toViewportCoords(const QPointF &in) const; QQuickWindow *m_window = {nullptr}; QMLViewportRenderer *renderer_actor{nullptr}; - PlayheadUI *playhead_{nullptr}; - static qt::OffscreenViewport *offscreen_viewport_; bool connected_{false}; QCursor cursor_; bool cursor_hidden{false}; - int mouse_buttons = {0}; - QPoint mouse_position; - int on_screen_logical_frame_ = {0}; - bool frame_out_of_range_ = {false}; - bool no_alpha_channel_ = {false}; - bool enable_shortcuts_ = {true}; - int viewport_index_ = {0}; - bool is_quick_viewer_ = {false}; + QPointF mouse_position; + bool is_quick_viewer_ = {false}; + QUuid playhead_uuid_; + + caf::actor keypress_monitor_; }; } // namespace qml } // namespace ui -} // namespace xstudio +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/qml/qml_viewport_renderer.hpp b/include/xstudio/ui/qml/qml_viewport_renderer.hpp index b59d6c210..c36bc3945 100644 --- a/include/xstudio/ui/qml/qml_viewport_renderer.hpp +++ b/include/xstudio/ui/qml/qml_viewport_renderer.hpp @@ -13,18 +13,20 @@ CAF_PUSH_WARNINGS #include CAF_POP_WARNINGS +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/viewport_qml_export.h" + namespace xstudio { namespace ui { namespace qml { class QMLViewport; - class PlayheadUI; - class QMLViewportRenderer : public QMLActor { + class VIEWPORT_QML_EXPORT QMLViewportRenderer : public QMLActor { Q_OBJECT public: - QMLViewportRenderer(QObject *owner, const int viewport_index); + QMLViewportRenderer(QObject *owner); virtual ~QMLViewportRenderer(); void setWindow(QQuickWindow *window); @@ -37,6 +39,8 @@ namespace ui { const QSize sceneSize, const float devicePixelRatio); + void prepareRenderData(); + void init_system(); void join_playhead(caf::actor group) { scoped_actor sys{system()}; @@ -56,19 +60,14 @@ namespace ui { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } } - void set_playhead(PlayheadUI *playhead); - - float zoom(); - [[nodiscard]] QString fpsExpression() const { return fps_expression_; } - void rawKeyDown(const int key, const bool autorepeat); - void keyboardTextEntry(const QString text); - void rawKeyUp(const int key); - void allKeysUp(); - Imath::V2i imageResolutionCoords(); - Imath::V2f imageCoordsToViewport(const int x, const int y); - [[nodiscard]] QRectF imageBoundsInViewportPixels() const; - void setScale(const float s); - void setTranslate(const QVector2D &t); + + void set_playhead(caf::actor playhead); + + [[nodiscard]] QVariantList imageResolutions() const; + [[nodiscard]] QVariantList imageBoundariesInViewport() const; + [[nodiscard]] caf::actor playhead() { + return viewport_renderer_ ? viewport_renderer_->playhead() : caf::actor(); + } bool pointerEvent(const PointerEvent &e); void setScreenInfos( QString name, @@ -76,60 +75,50 @@ namespace ui { QString manufacturer, QString serialNumber, double refresh_rate); + [[nodiscard]] QString name() const { - return QStringFromStd(viewport_renderer_->name()); + return viewport_renderer_ ? QStringFromStd(viewport_renderer_->name()) + : QString("Not Yet"); } - void linkToViewport(QMLViewportRenderer *other_viewport); + [[nodiscard]] std::string std_name() const { + return viewport_renderer_ ? viewport_renderer_->name() : "not yet"; + } - void renderImageToFile( - const QUrl filePath, - caf::actor playhead, - const int format, - const int compression, - const int width, - const int height, - const bool bakeColor); void setIsQuickViewer(const bool is_quick_viewer); + void visibleChanged(const bool is_visible); + QPointF toViewportCoords(const QPointF &in) const; public slots: void init_renderer(); void paint(); - void setZoom(const float f); - void revertFitZoomToPrevious(); void frameSwapped(); - float scale(); - QVector2D translate(); - void quickViewSource(QStringList mediaActors, QString compareMode); + void quickViewSource( + QStringList mediaActors, QString compareMode, int in_pt, int out_pt); + void reset(); + signals: - void zoomChanged(float); void fpsChanged(QString); - void scaleChanged(float); void exposureChanged(float); - void translateChanged(QVector2D); - void onScreenFrameChanged(int); - void outOfRange(bool); - void noAlphaChannelChanged(bool); + void translationChanged(); + void resolutionsChanged(); void doRedraw(); void doSnapshot(QString, QString, int, int, bool); void quickViewBackendRequest(QStringList mediaActors, QString compareMode); void quickViewBackendRequestWithSize( QStringList mediaActors, QString compareMode, QPoint position, QSize size); void snapshotRequestResult(QString resultMessage); - void isQuickviewerChanged(bool); private: void receive_change_notification(viewport::Viewport::ChangeCallbackId id); + void make_xstudio_viewport(); QQuickWindow *m_window; ui::viewport::Viewport *viewport_renderer_ = nullptr; bool init_done{false}; QString fps_expression_; - bool frame_out_of_range_ = {false}; - QRectF imageBounds_; - int viewport_index_; class QMLViewport *viewport_qml_item_; caf::actor viewport_update_group; diff --git a/include/xstudio/ui/qml/session_model_ui.hpp b/include/xstudio/ui/qml/session_model_ui.hpp index 108a849c9..9ba101b4c 100644 --- a/include/xstudio/ui/qml/session_model_ui.hpp +++ b/include/xstudio/ui/qml/session_model_ui.hpp @@ -1,13 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once + // include CMake auto-generated export hpp #include "xstudio/ui/qml/session_qml_export.h" #include +#include #include "xstudio/ui/qml/helper_ui.hpp" #include "xstudio/ui/qml/json_tree_model_ui.hpp" -#include "xstudio/ui/qml/tag_ui.hpp" #include "xstudio/timeline/item.hpp" @@ -30,16 +31,31 @@ class SESSION_QML_EXPORT SessionModel : public caf::mixin::actor_object; explicit SessionModel(QObject *parent = nullptr); virtual void init(caf::actor_system &system); - QQmlPropertyMap *tags() { return tag_manager_->attrs_map_; } - [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; @@ -103,6 +115,12 @@ class SESSION_QML_EXPORT SessionModel : public caf::mixin::actor_object importTimelineFuture( + const QModelIndex &playlist_index, const QUrl &path, const QUuid &before = QUuid()); + Q_INVOKABLE QModelIndex importTimeline( + const QModelIndex &playlist_index, const QUrl &path, const QUuid &before = QUuid()) { + return importTimelineFuture(playlist_index, path, before).result().toModelIndex(); + } + + + Q_INVOKABLE bool + removeTimelineItems(const QModelIndexList &indexes, const bool isRipple = false); + Q_INVOKABLE bool + removeTimelineItems(const QModelIndex &track_index, const int frame, const int duration); + + Q_INVOKABLE QModelIndex + bakeTimelineItems(const QModelIndexList &indexes, const QString &trackName = ""); + + Q_INVOKABLE QModelIndexList duplicateTimelineClips( + const QModelIndexList &indexes, + const QString &trackName = "", + const QString &trackSuffix = "Duplicate", + const bool append = true); + + Q_INVOKABLE QModelIndexList + duplicateTimelineClipsTo(const QModelIndexList &indexes, const QModelIndex &trackIndex); + + Q_INVOKABLE bool replaceTimelineTrack(const QModelIndex &src, const QModelIndex &dst); + + Q_INVOKABLE QRect timelineRect(const QModelIndexList &indexes) const; + Q_INVOKABLE QModelIndex getTimelineIndex(const QModelIndex &index) const; + Q_INVOKABLE QModelIndex getTimelineTrackIndex(const QModelIndex &index) const; + + Q_INVOKABLE QModelIndex + getTimelineClipIndex(const QModelIndex &timelineIndex, const int frame); + + Q_INVOKABLE int getNextTimelineClipFrame(const QModelIndex &timelineIndex, const int frame); + + Q_INVOKABLE int + getPreviousTimelineClipFrame(const QModelIndex &timelineIndex, const int frame); + + Q_INVOKABLE void resetTimelineItemDragFlag(const QModelIndexList &items); + Q_INVOKABLE void updateTimelineItemDragFlag( + const QModelIndexList &items, + const bool isRolling, + const bool isRipple, + const bool isOverwrite); + + Q_INVOKABLE void beginTimelineItemDrag( + const QModelIndexList &items, + const QString &mode, + const bool isRipple = false, + const bool isOverwrite = false); + Q_INVOKABLE void updateTimelineItemDrag( + const QModelIndexList &items, + const QString &mode, + int frameChange, + int trackChange, + const bool isRipple = false, + const bool isOverwrite = false); + Q_INVOKABLE void endTimelineItemDrag( + const QModelIndexList &items, const QString &mode, const bool isOverwrite = false); + + Q_INVOKABLE void draggingAdjust(const QModelIndex &item, const int frameChange); + Q_INVOKABLE int checkAdjust( + const QModelIndex &item, + const int frameChange, + const bool lockDuration = false, + const bool lockEnd = false); + + Q_INVOKABLE QModelIndexList + getTimelineClipIndexes(const QModelIndex &timelineIndex, const QModelIndex &mediaIndex); + + Q_INVOKABLE int startFrameInParent(const QModelIndex &timelineItemIndex); + + Q_INVOKABLE QVariantList snapTo( + const QModelIndex &ignore, + const int cursor, + const int clipStart, + const int clipDuration, + const int currentOffset, + const int window, + const QUuid &key = QUuid()); + + // return all gap/clip items boundaries in timeline frames. + Q_INVOKABLE QVariantList boundaryFramesInTimeline(const QModelIndexList &indexes); + + + Q_INVOKABLE QModelIndexList getIndexesByName( + const QModelIndex &idx, const QString &name, const QString &type = "") const; + + Q_INVOKABLE QModelIndexList modifyItemSelectionHorizontal( + const QModelIndexList &clips, const int left, const int right); + Q_INVOKABLE QModelIndexList + modifyItemSelectionVertical(const QModelIndexList &clips, const int up, const int down); + + Q_INVOKABLE QVariantList mediaFrameToTimelineFrames( + const QModelIndex &timelineIndex, + const QModelIndex &mediaIndex, + const int logicalMediaFrame, + const bool skipDisabled = false); + + Q_INVOKABLE QModelIndexList getTimelineVideoClipIndexesFromRect( + const QModelIndex &timelineIndex, + const int left, + const int top, + const int right, + const int bottom, + const double frameScale, + const double trackScale, + const bool skipLocked = false); + + Q_INVOKABLE QModelIndexList getTimelineAudioClipIndexesFromRect( + const QModelIndex &timelineIndex, + const int left, + const int top, + const int right, + const int bottom, + const double frameScale, + const double trackScale, + const bool skipLocked = false); + + Q_INVOKABLE int + getTimelineFrameFromClip(const QModelIndex &clipIndex, const int logicalMediaFrame); + Q_INVOKABLE QModelIndex insertTimelineGap( const int row, const QModelIndex &parent, @@ -151,9 +298,6 @@ class SESSION_QML_EXPORT SessionModel : public caf::mixin::actor_object exportOTIO( + const QModelIndex &timeline, + const QUrl &path, + const QString &type = "otio_json", + const QString &schema = ""); + Q_INVOKABLE QVariantList getTimelineExportTypes() const; + + // end timeline operations + + // notification methods + Q_INVOKABLE void removeNotification(const QModelIndex &index, const QUuid &uuid); + Q_INVOKABLE QUuid infoNotification( + const QModelIndex &index, + const QString &text, + const int seconds = 10, + const QUuid &replaceUuid = QUuid()); + Q_INVOKABLE QUuid warnNotification( + const QModelIndex &index, + const QString &text, + const int seconds = 10, + const QUuid &replaceUuid = QUuid()); + Q_INVOKABLE QUuid processingNotification(const QModelIndex &index, const QString &text); + Q_INVOKABLE QUuid + progressPercentageNotification(const QModelIndex &index, const QString &text); + Q_INVOKABLE QUuid progressRangeNotification( + const QModelIndex &index, const QString &text, const float min, const float max); + Q_INVOKABLE void + updateProgressNotification(const QModelIndex &index, const QUuid &uuid, const float value); + + [[nodiscard]] QString sessionActorAddr() const { return session_actor_addr_; }; void setSessionActorAddr(const QString &addr); @@ -219,15 +393,6 @@ class SESSION_QML_EXPORT SessionModel : public caf::mixin::actor_object getThumbnailURLFuture(const QModelIndex &index, const int frame); Q_INVOKABLE QFuture getThumbnailURLFuture(const QModelIndex &index, const float frame); @@ -248,17 +413,43 @@ class SESSION_QML_EXPORT SessionModel : public caf::mixin::actor_object getJSONObjectFuture( + const QModelIndex &index, const QString &path, const bool includeSource = false); + Q_INVOKABLE QString getJSON(const QModelIndex &index, const QString &path) { return getJSONFuture(index, path).result(); } - Q_INVOKABLE QFuture getJSONFuture(const QModelIndex &index, const QString &path); + Q_INVOKABLE QFuture getJSONFuture( + const QModelIndex &index, const QString &path, const bool includeSource = false); + + Q_INVOKABLE QStringList + getMediaSourceNames(const QModelIndex &media_index, bool image_sources); + Q_INVOKABLE QStringList setMediaSource( + const QModelIndex &media_index, const QString &mediaSourceName, bool image_source); + + Q_INVOKABLE bool + setJSONObject(const QModelIndex &index, const QVariant &json, const QString &path = "") { + return setJSONObjectFuture(index, json, path).result(); + } + Q_INVOKABLE QFuture setJSONObjectFuture( + const QModelIndex &index, const QVariant &json, const QString &path = ""); + Q_INVOKABLE bool setJSON(const QModelIndex &index, const QString &json, const QString &path = "") { @@ -267,69 +458,81 @@ class SESSION_QML_EXPORT SessionModel : public caf::mixin::actor_object setJSONFuture(const QModelIndex &index, const QString &json, const QString &path = ""); - Q_INVOKABLE void sortAlphabetically(const QModelIndex &index); + Q_INVOKABLE void sortByMediaDisplayInfo( + const QModelIndex &index, const int sort_column_idx, const bool ascending); - Q_INVOKABLE void setPlayheadTo(const QModelIndex &index); - Q_INVOKABLE void setCurrentPlaylist(const QModelIndex &index); + Q_INVOKABLE void setViewportCurrentMediaContainerIndex(const QModelIndex &index); + Q_INVOKABLE void setCurrentMediaContainer(const QModelIndex &index); Q_INVOKABLE void relinkMedia(const QModelIndexList &indexes, const QUrl &path); Q_INVOKABLE void decomposeMedia(const QModelIndexList &indexes); Q_INVOKABLE void rescanMedia(const QModelIndexList &indexes); Q_INVOKABLE QModelIndex getPlaylistIndex(const QModelIndex &index) const; + Q_INVOKABLE QModelIndex getContainerIndex(const QModelIndex &index) const; Q_INVOKABLE QFuture undoFuture(const QModelIndex &index); Q_INVOKABLE QFuture redoFuture(const QModelIndex &index); + Q_INVOKABLE void purgePlaylist(const QModelIndex &index); + + Q_INVOKABLE QPersistentModelIndex currentMediaContainerIndex() { + return current_playlist_index_; + } + + Q_INVOKABLE QPersistentModelIndex viewportCurrentMediaContainerIndex() { + return current_playhead_owner_index_; + } + Q_INVOKABLE void setTimelineFocus(const QModelIndex &timeline, const QModelIndexList &indexes) const; + Q_INVOKABLE void + setTimelineSelection(const QModelIndex &timeline, const QModelIndexList &indexes) const; Q_INVOKABLE bool undo(const QModelIndex &index) { return undoFuture(index).result(); } Q_INVOKABLE bool redo(const QModelIndex &index) { return redoFuture(index).result(); } - Q_INVOKABLE QFuture - conformInsertFuture(const QString &task, const QModelIndexList &indexes); - Q_INVOKABLE QModelIndexList - conformInsert(const QString &task, const QModelIndexList &indexes) { - return conformInsertFuture(task, indexes).result(); - } - - Q_INVOKABLE void updateMetadataSelection(const int slot, QStringList metadata_paths); + Q_INVOKABLE void updateCurrentMediaContainerIndexFromBackend(); + Q_INVOKABLE void updateViewportCurrentMediaContainerIndexFromBackend(); public slots: void updateMedia(); + void setSelectedMedia(const QModelIndexList &indexes); signals: + void bookmarkActorAddrChanged(); + void onScreenPlayheadUuidChanged(); void sessionActorAddrChanged(); void mediaAdded(const QModelIndex &index); void mediaStatusChanged(const QModelIndex &playlist_index); - void tagsChanged(); void modifiedChanged(); void playlistsChanged(); - void conformTasksChanged(); + void currentMediaContainerChanged(); + void viewportCurrentMediaContainerIndexChanged(); void mediaSourceChanged(const QModelIndex &media, const QModelIndex &source, const int mode); + void makeTimelineSelection(QModelIndex timeline, QModelIndexList items); public: caf::actor_system &system() const { return self()->home_system(); } static nlohmann::json createEntry(const nlohmann::json &update = R"({})"_json); protected: - QModelIndexList search_recursive_list_base( + QModelIndexList searchRecursiveListBase( const QVariant &value, const int role, const QModelIndex &parent, const int start, const int hits, const int depth = -1) override; - // QModelIndexList search_recursive_fast( + // QModelIndexList searchRecursive_fast( // const nlohmann::json &searchValue, // const std::string &searchKey, // const nlohmann::json::json_pointer &path, // const nlohmann::json &root, // const int hits) const; - // QModelIndexList search_recursive_media( + // QModelIndexList searchRecursive_media( // const nlohmann::json &searchValue, // const std::string &searchKey, // const nlohmann::json::json_pointer &path, @@ -342,10 +545,20 @@ class SESSION_QML_EXPORT SessionModel : public caf::mixin::actor_object &tasks); - void updateErroredCount(const QModelIndex &media_index); + QModelIndexList getTimelineClipIndexesFromRect( + const QModelIndex &timelineIndex, + const int left, + const int top, + const int right, + const int bottom, + const double frameScale, + const double trackScale, + const timeline::ItemType type, + const bool skipLocked); + + QModelIndexList insertRows( int row, int count, @@ -362,6 +575,7 @@ class SESSION_QML_EXPORT SessionModel : public caf::mixin::actor_object &metadata_paths = std::map()) const; + const int role) const; void requestData( const QVariant &search_value, const int search_role, const QPersistentModelIndex &search_hint, const nlohmann::json &data, - const int role, - const std::map &metadata_paths = std::map()) const; + const int role) const; caf::actor actorFromIndex(const QModelIndex &index, const bool try_parent = false) const; utility::Uuid actorUuidFromIndex(const QModelIndex &index, const bool try_parent = false); + utility::Uuid + containerUuidFromIndex(const QModelIndex &index, const bool try_parent = false); void processChildren(const nlohmann::json &result_json, const QModelIndex &index); @@ -412,16 +626,17 @@ class SESSION_QML_EXPORT SessionModel : public caf::mixin::actor_object media_thumbnails_; // key is actor string + utility::UuidSet processed_events_; + std::queue processed_events_queue_; +}; + +class SESSION_QML_EXPORT MediaListFilterModel : public QSortFilterProxyModel { + + Q_OBJECT + + Q_PROPERTY( + QString searchString READ searchString WRITE setSearchString NOTIFY searchStringChanged) + + public: + using super = QSortFilterProxyModel; + + MediaListFilterModel(QObject *parent = nullptr); + + const QString &searchString() const { return search_string_; } - std::map> metadata_sets_; + void setSearchString(const QString &ss) { + if (ss != search_string_) { + search_string_ = ss; + emit searchStringChanged(); + invalidateFilter(); + } + } + + [[nodiscard]] QHash roleNames() const override { + if (!sourceModel()) + return QHash(); + return sourceModel()->roleNames(); + } + + Q_INVOKABLE [[nodiscard]] QModelIndex rowToSourceIndex(const int row) const; + + Q_INVOKABLE [[nodiscard]] int sourceIndexToRow(const QModelIndex &) const; + + Q_INVOKABLE [[nodiscard]] int + getRowWithMatchingRoleData(const QVariant &searchValue, const QString &searchRole) const; + + protected: + [[nodiscard]] bool + filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + + signals: + void searchStringChanged(); + + private: + QString search_string_; }; } // namespace xstudio::ui::qml diff --git a/include/xstudio/ui/qml/shotgun_provider_ui.hpp b/include/xstudio/ui/qml/shotgun_provider_ui.hpp index 2bacedabe..8f81aa589 100644 --- a/include/xstudio/ui/qml/shotgun_provider_ui.hpp +++ b/include/xstudio/ui/qml/shotgun_provider_ui.hpp @@ -92,12 +92,12 @@ class ShotgunThumbnailReader : public ControllableJob (mode == "thumbnail" ? true : false), true); if (thumbnail_cache) - sys->anon_send( - thumbnail_cache, + anon_mail( media_cache::store_atom_v, key, static_cast(std::max(requestedSize_.width(), 128)), - tbp); + tbp) + .send(thumbnail_cache); } if (not cjc.shouldRun()) diff --git a/include/xstudio/ui/qml/snapshot_model_ui.hpp b/include/xstudio/ui/qml/snapshot_model_ui.hpp deleted file mode 100644 index 3a171d52f..000000000 --- a/include/xstudio/ui/qml/snapshot_model_ui.hpp +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/ui/qml/json_tree_model_ui.hpp" -#include "xstudio/utility/file_system_item.hpp" - - -CAF_PUSH_WARNINGS -#include -#include -#include -#include -CAF_POP_WARNINGS - -namespace xstudio::ui::qml { -using namespace caf; - -class HELPER_QML_EXPORT SnapshotModel : public JSONTreeModel { - Q_OBJECT - - Q_PROPERTY(QVariant paths READ paths WRITE setPaths NOTIFY pathsChanged) - - public: - enum Roles { - childrenRole = JSONTreeModel::Roles::LASTROLE, - mtimeRole, - nameRole, - pathRole, - typeRole, - }; - - - explicit SnapshotModel(QObject *parent = nullptr); - - QVariant paths() const { return paths_; } - void setPaths(const QVariant &value); - - [[nodiscard]] QVariant - data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - Q_INVOKABLE bool createFolder(const QModelIndex &index, const QString &name); - - Q_INVOKABLE void rescan(const QModelIndex &index = QModelIndex(), const int depth = 0); - Q_INVOKABLE QUrl buildSavePath(const QModelIndex &index, const QString &name) const; - - signals: - void pathsChanged(); - - protected: - void sortByName(nlohmann::json &jsn); - nlohmann::json sortByNameType(const nlohmann::json &jsn) const; - - - private: - utility::FileSystemItem items_; - - QVariant paths_; -}; - -} // namespace xstudio::ui::qml diff --git a/include/xstudio/ui/qml/studio_ui.hpp b/include/xstudio/ui/qml/studio_ui.hpp index 48e71eb66..22c3d0717 100644 --- a/include/xstudio/ui/qml/studio_ui.hpp +++ b/include/xstudio/ui/qml/studio_ui.hpp @@ -10,6 +10,9 @@ CAF_PUSH_WARNINGS #include CAF_POP_WARNINGS +// include CMake auto-generated export hpp +#include "xstudio/ui/qml/studio_qml_export.h" + #include "xstudio/ui/qml/helper_ui.hpp" #include "xstudio/ui/qt/offscreen_viewport.hpp" #include "xstudio/utility/uuid.hpp" @@ -18,8 +21,13 @@ namespace xstudio { namespace ui { namespace qml { + void setup_xstudio_qml_emgine( + QQmlEngine *engine, + caf::actor_system &system + ); + // top level utility actor, for stuff that lives out side the session. - class StudioUI : public QMLActor { + class STUDIO_QML_EXPORT StudioUI : public QMLActor { Q_OBJECT @@ -29,14 +37,13 @@ namespace ui { public: explicit StudioUI(caf::actor_system &system, QObject *parent = nullptr); - ~StudioUI(); + virtual ~StudioUI(); Q_INVOKABLE bool clearImageCache(); Q_INVOKABLE [[nodiscard]] QUrl userDocsUrl() const; Q_INVOKABLE [[nodiscard]] QUrl apiDocsUrl() const; Q_INVOKABLE [[nodiscard]] QUrl releaseDocsUrl() const; - Q_INVOKABLE [[nodiscard]] QUrl hotKeyDocsUrl() const; Q_INVOKABLE void newSession(const QString &name); @@ -57,6 +64,15 @@ namespace ui { void setSessionActorAddr(const QString &addr); + Q_INVOKABLE QString renderScreenShotToDisk( + const QUrl &path, const int compression, const int width, const int height); + + Q_INVOKABLE QString renderScreenShotToClipboard(const int width, const int height); + + Q_INVOKABLE void setupSnapshotViewport(const QString &playhead_addr); + + Q_INVOKABLE void loadVideoOutputPlugins(); + signals: void newSessionCreated(const QString &session_addr); @@ -64,23 +80,23 @@ namespace ui { void dataSourcesChanged(); void sessionRequest(const QUrl path, const QByteArray jsn); void sessionActorAddrChanged(); - void openQuickViewers(QStringList mediaActors, QString compareMode); + void + openQuickViewers(QStringList mediaActors, QString compareMode, int inpt, int outPt); void showMessageBox( QString messageTile, QString messageBody, bool closeButton, int timeoutSeconds); - public slots: private: void init(caf::actor_system &system) override; void updateDataSources(); - void loadVideoOutputPlugins(); + xstudio::ui::qt::OffscreenViewport *offscreen_snapshot_viewport(); QList data_sources_; - QString session_actor_addr_; + xstudio::ui::qt::OffscreenViewport *snapshot_offscreen_viewport_ = nullptr; std::vector offscreen_viewports_; std::vector video_output_plugins_; - xstudio::ui::qt::OffscreenViewport *snapshot_offscreen_viewport_ = nullptr; + QString session_actor_addr_; }; } // namespace qml } // namespace ui diff --git a/include/xstudio/ui/qml/tag_ui.hpp b/include/xstudio/ui/qml/tag_ui.hpp deleted file mode 100644 index f8a159f54..000000000 --- a/include/xstudio/ui/qml/tag_ui.hpp +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once -#pragma once - -#pragma once - -// include CMake auto-generated export hpp -#include "xstudio/ui/qml/tag_qml_export.h" - -#include -#include - -CAF_PUSH_WARNINGS -#include -#include -#include -#include -CAF_POP_WARNINGS - -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/tag/tag.hpp" - -namespace xstudio { -namespace ui { - namespace qml { - - - class TagUI : public QObject { - Q_OBJECT - - Q_PROPERTY(QUuid id READ id NOTIFY idChanged) - Q_PROPERTY(QUuid link READ link NOTIFY linkChanged) - Q_PROPERTY(QString type READ type NOTIFY typeChanged) - Q_PROPERTY(QString data READ data NOTIFY dataChanged) - Q_PROPERTY(bool persistent READ persistent NOTIFY persistentChanged) - Q_PROPERTY(int unique READ unique NOTIFY uniqueChanged) - - public: - TagUI(const tag::Tag tag, QObject *parent = nullptr); - ~TagUI() override = default; - - [[nodiscard]] QUuid id() const { return QUuidFromUuid(tag_.id()); } - [[nodiscard]] QUuid link() const { return QUuidFromUuid(tag_.link()); } - [[nodiscard]] QString data() const { return QStringFromStd(tag_.data()); } - [[nodiscard]] QString type() const { return QStringFromStd(tag_.type()); } - [[nodiscard]] int unique() const { return static_cast(tag_.unique()); } - [[nodiscard]] bool persistent() const { return tag_.persistent(); } - - signals: - void idChanged(); - void linkChanged(); - void typeChanged(); - void dataChanged(); - void uniqueChanged(); - void persistentChanged(); - - private: - tag::Tag tag_; - }; - - class TagListUI : public QObject { - Q_OBJECT - - Q_PROPERTY(QList tags READ tags NOTIFY tagsChanged) - - public: - TagListUI(QObject *parent = nullptr) : QObject(parent) {} - ~TagListUI() override = default; - - [[nodiscard]] QList tags() const { return tags_; } - - void addTag(TagUI *tag); - bool removeTag(const QUuid &id); - - signals: - void tagsChanged(); - - public: - QList tags_; - }; - - class TagAttrs : public QQmlPropertyMap { - - Q_OBJECT - - public: - TagAttrs(QObject *parent = nullptr); - ~TagAttrs() override = default; - - void reset(); - }; - - class TAG_QML_EXPORT TagManagerUI : public QMLActor { - - Q_OBJECT - - public: - TagManagerUI(QObject *parent = nullptr); - ~TagManagerUI() override = default; - - void init(caf::actor_system &system) override; - void set_backend(caf::actor backend); - - void add_tag(const tag::Tag &tag); - void remove_tag(const utility::Uuid &id); - - TagAttrs *attrs_map_ = {nullptr}; - caf::actor backend_; - caf::actor backend_events_; - }; - - } // namespace qml -} // namespace ui -} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/ui/qml/thumbnail_provider_ui.hpp b/include/xstudio/ui/qml/thumbnail_provider_ui.hpp index f3215a049..bceaff894 100644 --- a/include/xstudio/ui/qml/thumbnail_provider_ui.hpp +++ b/include/xstudio/ui/qml/thumbnail_provider_ui.hpp @@ -107,7 +107,8 @@ class ThumbnailReader : public ControllableJob> { mp, static_cast(std::max(requestedSize_.width(), 128)), hash, - cache_to_disk ? true : false); + cache_to_disk ? true : false, + utility::clock::now()); break; } catch (const std::exception &err) { // give up diff --git a/include/xstudio/ui/qt/offscreen_viewport.hpp b/include/xstudio/ui/qt/offscreen_viewport.hpp index 7cb1ed5d8..bc624991a 100644 --- a/include/xstudio/ui/qt/offscreen_viewport.hpp +++ b/include/xstudio/ui/qt/offscreen_viewport.hpp @@ -16,8 +16,19 @@ namespace opengl { class OpenGLViewportRenderer; } +class QQuickWindow; +class QQuickItem; +class QQmlComponent; +class QQuickRenderControl; +class QQmlEngine; + namespace xstudio { namespace ui { + + namespace qml { + class Helpers; + } + namespace qt { class OffscreenViewport : public caf::mixin::actor_object { @@ -26,7 +37,7 @@ namespace ui { using super = caf::mixin::actor_object; public: - OffscreenViewport(const std::string name); + OffscreenViewport(const std::string name, bool include_qml_overlays = true); ~OffscreenViewport() override; // Direct rendering to an output file @@ -41,9 +52,13 @@ namespace ui { public slots: - void autoDelete(); + void cleanup(); + void sceneChanged(); + void renderViewportUnderQML(); private: + caf::actor_system &system() { return self()->home_system(); } + void receive_change_notification(viewport::Viewport::ChangeCallbackId id); thumbnail::ThumbnailBufferPtr renderOffscreen( @@ -51,6 +66,8 @@ namespace ui { const int h, const media_reader::ImageBufPtr &image = media_reader::ImageBufPtr()); + media_reader::ImageBufPtr renderToImageBuf(const int width, const int height); + thumbnail::ThumbnailBufferPtr renderToThumbnail( const thumbnail::THUMBNAIL_FORMAT format, const int width, @@ -63,13 +80,22 @@ namespace ui { void renderToImageBuffer( const int w, const int h, - media_reader::ImageBufPtr &image, - const viewport::ImageFormat format); + media_reader::ImageBufPtr &dest_image, + const viewport::ImageFormat format, + const bool force_sync, + const utility::time_point &tp = utility::time_point(), + const media_reader::ImageBufPtr &image_to_use = media_reader::ImageBufPtr()); void initGL(); void exportToEXR(const media_reader::ImageBufPtr &image, const caf::uri path); + media_reader::ImageBufPtr renderMediaFrameToImage( + caf::actor media_actor, + const int media_frame, + const int width, + const int height); + thumbnail::ThumbnailBufferPtr renderMediaFrameToThumbnail( caf::actor media_actor, const int media_frame, @@ -78,16 +104,26 @@ namespace ui { const bool auto_scale, const bool show_annotations); + thumbnail::ThumbnailBufferPtr renderMediaFrameToThumbnail( + caf::actor media_actor, + const timebase::flicks media_timepoint, + const thumbnail::THUMBNAIL_FORMAT format, + const int width, + const bool auto_scale, + const bool show_annotations); + void exportToCompressedFormat( const media_reader::ImageBufPtr &buf, const caf::uri path, const std::string &ext); - void setupTextureAndFrameBuffer( + bool setupTextureAndFrameBuffer( const int width, const int height, const viewport::ImageFormat format); void make_conversion_lut(); + bool loadQMLOverlays(); + thumbnail::ThumbnailBufferPtr rgb96thumbFromHalfFloatImage(const media_reader::ImageBufPtr &image); @@ -112,9 +148,21 @@ namespace ui { viewport::ImageFormat vid_out_format_ = viewport::ImageFormat::RGBA_16; caf::actor video_output_actor_; std::vector output_buffers_; + media_reader::ImageBufPtr last_rendered_frame_; + media_reader::ImageBufPtr image_to_render_; std::vector half_to_int_32_lut_; caf::actor local_playhead_; + QString session_actor_addr_; + + QQuickWindow *quick_win_ = nullptr; + QQuickItem *root_qml_overlays_item_ = nullptr; + QQmlComponent *qml_component_ = nullptr; + QQuickRenderControl *render_control_ = nullptr; + QQmlEngine *qml_engine_ = nullptr; + ui::qml::Helpers *helper_ = nullptr; + bool overlays_loaded_ = false; + bool include_qml_overlays_ = true; }; } // namespace qt } // namespace ui diff --git a/include/xstudio/ui/qt/viewport_opengl_ui.hpp b/include/xstudio/ui/qt/viewport_opengl_ui.hpp index 67667bd5a..451c0e0c9 100644 --- a/include/xstudio/ui/qt/viewport_opengl_ui.hpp +++ b/include/xstudio/ui/qt/viewport_opengl_ui.hpp @@ -37,9 +37,9 @@ namespace ui { void leave_playhead(caf::actor group) { leave_broadcast(self(), group); } private: - void emitPointerEvent(viewport::Signature::EventType t, QMouseEvent *event); + void emitPointerEvent(viewport::EventType t, QMouseEvent *event); - opengl::ViewportRenderer viewport_; + opengl::viewport_; caf::actor player_; }; } // namespace qt diff --git a/include/xstudio/ui/qt/viewport_widget.hpp b/include/xstudio/ui/qt/viewport_widget.hpp index b0d400bb2..2d897afd9 100644 --- a/include/xstudio/ui/qt/viewport_widget.hpp +++ b/include/xstudio/ui/qt/viewport_widget.hpp @@ -14,25 +14,54 @@ namespace ui { class ViewportGLWidget : public caf::mixin::actor_object { + Q_OBJECT + public: using super = caf::mixin::actor_object; - ViewportGLWidget(QWidget *parent); + ViewportGLWidget( + QWidget *parent, + const bool live_viewport = false, + const QString window_name = "OffscreenViewport", + const QString viewport_name = ""); + + ~ViewportGLWidget(); virtual void init(caf::actor_system &system); void set_playhead(caf::actor playhead); - protected: - void initializeGL() override; + QString name(); void resizeGL(int w, int h) override; + public slots: + void frameBufferSwapped(); + + protected: + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *event) override; + bool event(QEvent *event) override; + + void initializeGL() override; + void paintGL() override; void receive_change_notification(viewport::Viewport::ChangeCallbackId id); + void sendPointerEvent(EventType t, QMouseEvent *event, int force_modifiers = 0); + void sendPointerEvent(QHoverEvent *event); + std::shared_ptr the_viewport_; + const bool live_viewport_; + caf::actor keypress_monitor_; + std::string window_name_; + std::string viewport_name_; }; } // namespace qt diff --git a/include/xstudio/ui/viewport/hud_plugin.hpp b/include/xstudio/ui/viewport/hud_plugin.hpp deleted file mode 100644 index 206cd15c9..000000000 --- a/include/xstudio/ui/viewport/hud_plugin.hpp +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - - -// #include "xstudio/colour_pipeline/colour_pipeline.hpp" -#include "xstudio/plugin_manager/plugin_base.hpp" - -#define NO_FRAME INT_MIN - - -namespace xstudio { -namespace ui { - namespace viewport { - - /** - * @brief HUDPluginBase class. - * - * @details - * Subclass to create custom HUDs in xStudio - which can be activated and configured - * from the 'HUD' toolbar button. The HUD graphics/text overlay can be implemented - * either as QML or an OpenGL renderer. Refer to the PixelProbe plugin example for - * a reference implementation. - */ - - class HUDPluginBase : public plugin::StandardPlugin { - public: - enum HUDElementPosition { - BottomLeft, - BottomCenter, - BottomRight, - TopLeft, - TopCenter, - TopRight - }; - - HUDPluginBase( - caf::actor_config &cfg, - std::string name, - const utility::JsonStore &init_settings); - ~HUDPluginBase() override = default; - - /** - * @brief Declare the name of the HUD element - * - * @details This string value is used to populate the list of hud elements shown on - * the HUD pop-up menu in the UI. Can be overridden - */ - virtual const std::string &hud_name() const { return Module::name(); } - - protected: - /** - * @brief Make a given attribute visible in the settings panel for the HUD plugin - * - * @details Calling this with an attribte will ensure that the attribute will - * appear in the settings pop-up dialog for the plugin - */ - void add_hud_settings_attribute(module::Attribute *attr); - - /** - * @brief Declare qml code that adds to the HUD. Plugins are responsible for - * providing attribute data and linking up with the qml code etc. See pixel_probe - * plugin for a reference example. - */ - void hud_element_qml(const std::string qml_code, const HUDElementPosition position); - - caf::message_handler message_handler_extensions() override { - return message_handler_; - } - - /** - * @brief Determines if HUD should be drawn to viewport at given moment - */ - bool visible() const { return globally_enabled_ && enabled_->value(); } - - caf::message_handler message_handler_; - - module::BooleanAttribute *enabled_; - bool globally_enabled_ = {true}; - }; - } // namespace viewport -} // namespace ui -} // namespace xstudio diff --git a/include/xstudio/ui/viewport/keypress_monitor.hpp b/include/xstudio/ui/viewport/keypress_monitor.hpp index 6702f62f9..8044f661d 100644 --- a/include/xstudio/ui/viewport/keypress_monitor.hpp +++ b/include/xstudio/ui/viewport/keypress_monitor.hpp @@ -5,6 +5,10 @@ #include "xstudio/ui/keyboard.hpp" +#ifdef __apple__ +#undef nil +#endif + #include #include #include @@ -24,14 +28,17 @@ namespace ui { private: caf::behavior make_behavior() override { return behavior_; } - void held_keys_changed(const std::string &context, const bool auto_repeat = false); + void held_keys_changed( + const std::string &context, + const bool auto_repeat = false, + const std::string &window = std::string()); protected: caf::actor keyboard_events_group_, hotkey_config_events_group_; caf::behavior behavior_; std::set held_keys_; std::map active_hotkeys_; - std::set actor_grabbing_all_mouse_input_; + std::vector actor_grabbing_all_mouse_input_; caf::actor actor_grabbing_all_keyboard_input_; }; } // namespace keypress_monitor diff --git a/include/xstudio/ui/viewport/video_output_plugin.hpp b/include/xstudio/ui/viewport/video_output_plugin.hpp new file mode 100644 index 000000000..5942630cf --- /dev/null +++ b/include/xstudio/ui/viewport/video_output_plugin.hpp @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/audio/audio_output_actor.hpp" +#include "xstudio/plugin_manager/plugin_base.hpp" + +namespace xstudio { +namespace ui { + namespace viewport { + + class VideoOutputPlugin : public plugin::StandardPlugin { + + public: + // Note - when deriving from VideoOutputPlugin your constructor must have + // a signature of this form: + // + // MyVidOutputPlugin(caf::actor_config &cfg, const utility::JsonStore + // &init_settings) + // + // cgf and init_settings must be supplied to base class with a unique plugin name + + VideoOutputPlugin( + caf::actor_config &cfg, + const utility::JsonStore &init_settings, + const std::string &plugin_name); + ~VideoOutputPlugin() override = default; + + // This method should be called on successful construction of your + // class - i.e. at the end of the constructor assuming all other set-up + // and initialisation (e.g. finding hardware devices) was completed. It + // will create the offscreen viewport ready for generating output video + // frames. + void finalise(); + + // This method should be implemented to allow cleanup of any/all resources + // relating to the video output + virtual void exit_cleanup() = 0; + + // This method is called when a new image buffer is ready to be displayed + virtual void incoming_video_frame_callback(media_reader::ImageBufPtr incoming) = 0; + + // Re-implement this method to receive data sent from your own thread(s) + // via 'send_status' - this is provided as a thread safe way to + // communicate between threads your plugin manages and the xstudio + // thread(s) that VideoOutputPlugin would normally execute within. + virtual void receive_status_callback(const utility::JsonStore &status_data) {} + + // Allocate your resources needed for video output, initialise hardware etc. in + // this function + virtual void initialise() = 0; + + void on_exit() override; + + // Call this method to intiate rendering of the xstudio viewport to an offscreen + // surface. The resulting pixel buffers are captured and returned via the + // 'incoming_video_frame_callback' method + // + // This method can be safely called from any thread + void start(int frame_width, int frame_height); + + // Call this method to stop rendering and cease calls to the + // incoming_video_frame_callback method + // + // This method can be safely called from any thread + void stop(); + + // This method MUST be called on every refresh of the video output to tell the + // offscreen viewport to send a new video frame. The 'beat' of frame_display_time + // is important to tell the offscreen viewport the natural refresh rate of the + // video output device and to sync the playhead to your video output. + // + // frame_display_time should inform as accurately as possible when the last frame + // delivered via incoming_video_frame_callback is actually shown on the display. + // + // This method can be safely called from any thread + void video_frame_consumed( + const utility::time_point &frame_display_time = utility::clock::now()); + + // Request a new video frame to be rendered and delivered via + // incoming_video_frame_callback. If possible, inform accurately when this video + // frame should be displayed in the video output. + void request_video_frame( + const utility::time_point &frame_display_time = utility::clock::now()); + + // Set a value for the video pipeline delay in milliseconds. This value is used to + // adjust the position of the playhead (forwards or backwards) when requesting + // frames for display. It allows you to compensate for video frames being buffered + // for display, for example, which would otherwise cause your video output to 'lag' + // the video output in xSTUDIO's main interface + void video_delay_milliseconds(const int millisceconds_delay) { + video_delay_millisecs_ = millisceconds_delay; + } + + // Override this method to instance a class derived from AudioOutputDevice with new + // that can receive an audio sample stream from xSTUDIO and deliver to your + // physical/virtual output device. Ownership of this object (and it's destruction) + // is with xSTUDIO. + virtual audio::AudioOutputDevice * + make_audio_output_device(const utility::JsonStore &prefs) { + return nullptr; + } + + // Use this method to communicate arbitrary data from your own threads (for example + // a worker loop delivering video frames to SDI video hardware) to xstudio. The data + // is received in the 'status_callback' virtual method + // + // This method can be safely called from any thread + void send_status(const utility::JsonStore &status_data); + + // Set whether the offscreen viewport tracks the mirror mode, zoom and + // pan andr the FitMode of the 'main' viewport in the xstudio interface + // // + // This method can be safely called from any thread + void sync_geometry_to_main_viewport(const bool sync); + + // Call this method to inform the viewport about what sort of display it is driving. + // This information is passed onto the colour management system so that it can + // try and pick an appropriate display (colour) transform. + void display_info( + const std::string &name, + const std::string &model, + const std::string &manufacturer, + const std::string &serialNumber); + + private: + void __init(const utility::JsonStore init_settings); + + void spawn_audio_output_actor(const utility::JsonStore &prefs) { + auto audio_dev = make_audio_output_device(prefs); + if (audio_dev) { + if (audio_output_) { + unlink_from(audio_output_); + send_exit(audio_output_, caf::exit_reason::user_shutdown); + } + audio_output_ = spawn( + std::shared_ptr(audio_dev), true); + link_to(audio_output_); + } + } + + caf::message_handler message_handler_extensions() override { + return message_handler_extensions_; + } + + caf::actor audio_output_; + caf::actor offscreen_viewport_; + caf::actor main_viewport_; + caf::message_handler message_handler_extensions_; + FitMode previous_fit_mode_ = {FitMode::Best}; + int video_delay_millisecs_ = {0}; + const utility::JsonStore init_settings_store_; + }; + } // namespace viewport +} // namespace ui +} // namespace xstudio diff --git a/include/xstudio/ui/viewport/viewport.hpp b/include/xstudio/ui/viewport/viewport.hpp index 384a78843..69e60d6e8 100644 --- a/include/xstudio/ui/viewport/viewport.hpp +++ b/include/xstudio/ui/viewport/viewport.hpp @@ -3,7 +3,7 @@ // #include "xstudio/colour_pipeline/colour_pipeline.hpp" -#include "xstudio/media_reader/image_buffer.hpp" +#include "xstudio/media_reader/image_buffer_set.hpp" #include "xstudio/ui/viewport/enums.hpp" #include "xstudio/ui/mouse.hpp" #include "xstudio/utility/chrono.hpp" @@ -36,8 +36,6 @@ namespace ui { Viewport( const utility::JsonStore &state_data, caf::actor parent_actor, - const int viewport_index, - ViewportRendererPtr the_renderer, const std::string &name = std::string()); virtual ~Viewport(); @@ -46,11 +44,15 @@ namespace ui { void set_pointer_event_viewport_coords(PointerEvent &pointer_event); void set_scale(const float scale); - void set_size(const float w, const float h, const float devicePixelRatio); + void set_size( + const float w, + const float h, + const float window_width, + const float window_height, + const float pixelRatio); void set_pan(const float x_pan, const float y_pan); - void set_fit_mode(const FitMode md); + void set_fit_mode(const FitMode md, const bool sync = true); void set_mirror_mode(const MirrorMode md); - void set_pixel_zoom(const float zoom); void set_screen_infos( const std::string &name, const std::string &model, @@ -58,15 +60,6 @@ namespace ui { const std::string &serialNumber, const double refresh_rate); - /** - * @brief Link to another viewport so the zoom, scale and colour - * management settings are shared between the two viewports - * - * @details This allows the pop-out viewer to track the primary - * viewer in the main interface, for example - */ - void link_to_viewport(caf::actor other_viewport); - /** * @brief Switch the fit mode and zoom to it's previous state (usually before * some user interaction) @@ -88,32 +81,49 @@ namespace ui { * @brief Render the viewport. * * @details Render the image data into some initialised graphics resource (e.g. - * an active OpenGL context) + * an active OpenGL context). Thread safe, as required by QML integration where + * render thread is separate to main GUI thread. + * + * prepare_render_data should be called from GUI thread before calling this + * method. */ - void render() { - std::vector next_images; - get_frames_for_display(next_images); - if (!next_images.empty()) - update_onscreen_frame_info(next_images[0]); - the_renderer_->render( - next_images, to_scene_matrix(), projection_matrix(), fit_mode_matrix()); - } + void render() const; + + /** + * @brief Render the viewport, for a specific display time measured as a system + * clock timepoint. + * + * @details Renders the image data into some initialised graphics resource (e.g. + * an active OpenGL context), allowing an adjustment to the playhead position to + * account for a display pipeline delay. For example, if this viewport is an + * offscreen viewport that delivers an image to be displayed on an SDI device + * like a projector, and the buffering in the SDI card results in a 250ms delay + * then 'when_going_on_screen' should be clock::now() + milliseconds(250) + * + * Calling prepare_render_data is not required before using this method. + */ + void render(const utility::time_point &when_going_on_screen); /** * @brief Render the viewport with a given image * * @details Render the image data into some initialised graphics resource (e.g. * an active OpenGL context) + * + * Calling prepare_render_data is not required before using this method. */ - void render(const media_reader::ImageBufPtr &image_buf) { - update_onscreen_frame_info(image_buf); - the_renderer_->render( - std::vector({image_buf}), - to_scene_matrix(), - projection_matrix(), - fit_mode_matrix()); - } + void render(const media_reader::ImageBufPtr &image_buf); + /** + * @brief Any pre-render interaction with Viewport state data etc. must be done + * in this function. All necessary data for rendering the viewport must be + * gatherered and finalised within this function. The data should be made thread + * safe, as the rendering can/will be executed in a separate thread to the main GUI + * thread that the Viewport lives in. + */ + void prepare_render_data( + const utility::time_point &when_going_on_screen = utility::time_point(), + const bool sync_to_playhead = false); /** * @brief Initialise the viewport. @@ -121,7 +131,10 @@ namespace ui { * @details Carry out first time render one-shot initialisation of any member * data or other state/data initialisation of graphics resources */ - void init() { the_renderer_->init(); } + void init() { + if (active_renderer_) + active_renderer_->init(); + } /** * @brief Inform the viewport of how its coordinate system maps to the @@ -133,28 +146,15 @@ namespace ui { * as in order to draw the viewport correctly into the QQuickItem bounds we * need to know how the QQuickItem is transformed into the parent QQuickWindow */ - bool set_scene_coordinates( - const Imath::V2f topleft, - const Imath::V2f topright, - const Imath::V2f bottomright, - const Imath::V2f bottomleft, - const Imath::V2i scene_size, + bool set_geometry( + const float x, + const float y, + const float width, + const float height, + const float window_width, + const float window_height, const float devicePixelRatio); - /** - * @brief Inform the viewport of the size of the image currently on screen to - * update the fit mode matrix. - * - * @details The matrix translating & scaling the image so that the active 'fit - * mode' works as required is dependent on the resolution of the image being - * displaye. This function receives the resolution of the current image being - * displayed and updates the fit mode matrix as required. - */ - void update_fit_mode_matrix( - const int image_width = 0, - const int image_height = 0, - const float pixel_aspect = 1.0f); - [[nodiscard]] float scale() const { return state_.scale_; } [[nodiscard]] float pixel_zoom() const; [[nodiscard]] Imath::V2f size() const { return state_.size_; } @@ -169,18 +169,11 @@ namespace ui { [[nodiscard]] const Imath::M44f &inv_projection_matrix() const { return inv_projection_matrix_; } - [[nodiscard]] const Imath::M44f &to_scene_matrix() const { + [[nodiscard]] const Imath::M44f &window_to_viewport_matrix() const { return viewport_to_canvas_; } - [[nodiscard]] const Imath::M44f &fit_mode_matrix() const { - return fit_mode_matrix_; - } - [[nodiscard]] const std::string &frame_rate_expression() const { - return frame_rate_expr_; - } - [[nodiscard]] bool frame_out_of_range() const { return frame_out_of_range_; } - [[nodiscard]] bool no_alpha_channel() const { return no_alpha_channel_; } - [[nodiscard]] int on_screen_frame() const { return on_screen_frame_; } + void apply_fit_mode(); + [[nodiscard]] bool playing() const { return playing_; } [[nodiscard]] const std::string &pixel_info_string() const { return pixel_info_string_; @@ -188,10 +181,12 @@ namespace ui { [[nodiscard]] caf::actor playhead() { return caf::actor_cast(playhead_addr_); } - [[nodiscard]] const std::string &toolbar_name() const { return toolbar_name_; } + [[nodiscard]] utility::Uuid playhead_uuid() { return playhead_uuid_; } [[nodiscard]] caf::actor colour_pipeline() { return colour_pipeline_; } + [[nodiscard]] const std::string &toolbar_name() const { return toolbar_name_; } + utility::JsonStore serialise() const override; /** @@ -201,16 +196,15 @@ namespace ui { */ bool use_bilinear_filtering() const; - - void deserialise(const nlohmann::json &json) override; - /** - * @brief Given image coordinate in pixels, returns the position in the - viewport coordinate system. + * @brief We only call this method if we want the viewport to always automatically + * connect to the playhead of the currently selected playlist/timeline/subset etc. */ - Imath::V2f image_coordinate_to_viewport_coordinate(const int x, const int y) const; + void auto_connect_to_global_selected_playhead(); + void deserialise(const nlohmann::json &json) override; + /** * @brief Return current pointer position transformed into the viewport * coordinate system. @@ -227,13 +221,17 @@ namespace ui { * @brief Get the bounding box, in the viewport area, of the current image * */ - Imath::Box2f image_bounds_in_viewport_pixels() const; + const std::vector &image_bounds_in_viewport_pixels() const { + return image_bounds_in_viewport_pixels_; + } /** - * @brief Get the resolution of the current image + * @brief Get the resolution of the current images * */ - Imath::V2i image_resolution() const; + const std::vector &image_resolutions() const { + return image_resolutions_; + } /** * @brief Get an ordered list of the FitMode enums and the corresponding name @@ -245,58 +243,41 @@ namespace ui { enum ChangeCallbackId { Redraw, - ZoomChanged, - ScaleChanged, - FitModeChanged, - MirrorModeChanged, - FrameRateChanged, - OutOfRangeChanged, - NoAlphaChannelChanged, - OnScreenFrameChanged, - ExposureChanged, TranslationChanged, + ImageResolutionsChanged, + ImageBoundsChanged, PlayheadChanged }; typedef std::function ChangeCallback; - /** - * @brief Set whether a viewport will automatically show the - * 'active' session playlist/subset/timeline - * - * @details When a viewport is set to auto-connect to the playhead, - * this means that when the 'active' playlist/subset/timeline at - * the session level changes (e.g. if the user double cliks on a - * playlist in the playlist panel interface) then the viewport - * will automatically connect to the playhead for that playlist/ - * subset/timeline such that it shows the select media therein. - * - * Then auto-connect is not set, the viewport remains connected - * to the playhead that was set by calling the 'set_playhead - * function. - */ - void auto_connect_to_playhead(bool auto_connect); - void set_change_callback(ChangeCallback f) { event_callback_ = f; } void set_playhead(caf::actor playhead, const bool wait_for_refresh = false); + void set_compare_mode(const std::string &compare_mode); + + void pointer_select_media(const PointerEvent &pointer_event); + caf::actor fps_monitor() { return fps_monitor_; } void framebuffer_swapped(const utility::time_point swap_time); - media_reader::ImageBufPtr get_image_from_playhead(caf::actor playhead); + void reset() override; media_reader::ImageBufPtr get_onscreen_image(); - void set_aux_shader_uniforms( - const utility::JsonStore &j, const bool clear_and_overwrite = false); + void set_visibility(bool is_visible); protected: void register_hotkeys() override; void attribute_changed(const utility::Uuid &attr_uuid, const int role) override; + void menu_item_activated( + const utility::JsonStore &menu_item_data, + const std::string &user_data) override; + /** * @brief Update viewport properties like frame number, error message or * other metadata at the moment when the on-screen frame is updated @@ -304,21 +285,24 @@ namespace ui { * @details Info like error message, whether the frame is out-of-range and * so-on is propagated up to the UI by this class and/or its actor wrapper */ - void update_onscreen_frame_info(const media_reader::ImageBufPtr &frame); + void update_onscreen_frame_info(const media_reader::ImageBufDisplaySetPtr &images); /** - * @brief Get a pointer to the framebuffer and colour pipe data that should be - * displayed in the next redraw. We also get the next framebuffer that - * we'll want to draw, if there is one in the queue. Calling this removes any - * images in the queue for display that have a display timestamp older than the - * timestamp for the returned data + * @brief Get the set of images that we need to draw to the viewport. + * Note that the set also includes not just images to be drawn now but + * the images that are due on screen next (during playback) to allow + * for asynchronous pixel uploads etc. for playback optimisation * * @details This function should be used by classes subclassing the Viewport in * their main draw function to receive the image(s) to be draw to screen. * Returns an empty pointer if the image does not need to be refreshed since the * last draw. */ - void get_frames_for_display(std::vector &next_images); + media_reader::ImageBufDisplaySetPtr get_frames_for_display( + const bool force_playhead_sync = false, + const utility::time_point &when_being_displayed = utility::time_point()); + + void instance_renderer_plugins(); void instance_overlay_plugins(); @@ -328,13 +312,14 @@ namespace ui { Imath::V3f translate_ = {0.0f, 0.0f, 0.0f}; float scale_ = {1.0f}; Imath::V2i image_size_ = {1920, 1080}; + Imath::V2f window_size_ = {}; Imath::V2f size_ = {}; Imath::V2i raw_pointer_position_ = {}; Imath::V4f pointer_position_; FitMode fit_mode_ = {Height}; MirrorMode mirror_mode_ = {Off}; - float image_aspect_ = {16.0f / 9.0f}; - float fit_mode_zoom_ = {1.0}; + float layout_aspect_ = {16.0f / 9.0f}; + float devicePixelRatio_ = 1.0f; } state_, interact_start_state_; struct FitModeStat { @@ -343,13 +328,24 @@ namespace ui { float scale_ = {0.0f}; } previous_fit_zoom_state_; + struct RenderData { + media_reader::ImageBufDisplaySetPtr images; + Imath::M44f projection_matrix; + Imath::M44f window_to_viewport_matrix; + Imath::V2i window_size; + ViewportRendererPtr renderer; + std::map overlay_renderers; + }; + std::shared_ptr render_data_; + Imath::M44f projection_matrix_; Imath::M44f inv_projection_matrix_; Imath::M44f interact_start_projection_matrix_; Imath::M44f interact_start_inv_projection_matrix_; Imath::M44f viewport_to_canvas_; - Imath::M44f fit_mode_matrix_; - float devicePixelRatio_ = {1.0}; + + float devicePixelRatio_ = {1.0}; + bool broadcast_fit_details_ = {true}; Imath::V4f normalised_pointer_position() const; @@ -357,23 +353,38 @@ namespace ui { void get_colour_pipeline(); - void - quickview_media(std::vector &media_items, std::string compare_mode); + void setup_menus(); + + void event_callback(const ChangeCallbackId ev); + + void quickview_media( + std::vector &media_items, + std::string compare_mode, + const int in_pt = -1, + const int out_pt = -1); + + media_reader::ImageBufDisplaySetPtr + prepare_image_for_display(const media_reader::ImageBufPtr &image_buf) const; + + void calc_image_bounds_in_viewport_pixels(); + + void update_image_resolutions(); utility::JsonStore settings_; + std::string window_id_; + typedef std::function PointerInteractFunc; std::map pointer_event_handlers_; - bool frame_out_of_range_ = {false}; - bool no_alpha_channel_ = {false}; - int on_screen_frame_; - std::string frame_rate_expr_ = {"--/--"}; std::string pixel_info_string_ = {"--"}; - media_reader::ImageBufPtr on_screen_frame_buffer_; - media_reader::ImageBufPtr about_to_go_on_screen_frame_buffer_; + media_reader::ImageBufPtr on_screen_hero_frame_; + media_reader::ImageBufPtr next_on_screen_hero_frame_; + media_reader::ImageBufDisplaySetPtr on_screen_frames_; + timebase::flicks screen_refresh_period_ = timebase::k_flicks_zero_seconds; std::string toolbar_name_; + std::string compare_mode_; caf::actor display_frames_queue_actor_; caf::actor parent_actor_; @@ -381,56 +392,76 @@ namespace ui { caf::actor fps_monitor_; caf::actor keypress_monitor_; caf::actor viewport_events_actor_; - std::vector other_viewports_; caf::actor colour_pipeline_; caf::actor keyboard_events_actor_; caf::actor quickview_playhead_; + caf::actor global_playhead_events_group_; caf::actor_addr playhead_addr_; + utility::Uuid playhead_uuid_; void dummy_evt_callback(ChangeCallbackId) {} ChangeCallback event_callback_; protected: - utility::Uuid current_playhead_, new_playhead_; - bool done_init_ = {false}; - int viewport_index_ = {0}; - bool playing_ = {false}; - bool playhead_pinned_ = {false}; + bool done_init_ = {false}; + bool playing_ = {false}; + bool is_visible_ = {false}; + size_t last_images_hash_ = {0}; + bool needs_redraw_ = {true}; + bool hover_image_select_ = {false}; std::set held_keys_; - - utility::JsonStore aux_shader_uniforms_; - + std::vector image_bounds_in_viewport_pixels_; + std::vector image_resolutions_; + size_t image_bounds_hash_ = {0}; std::map overlay_plugin_instances_; std::map hud_plugin_instances_; + std::map + viewport_overlay_renderers_; + std::map overlay_pre_render_hooks_; module::BooleanAttribute *zoom_mode_toggle_; module::BooleanAttribute *pan_mode_toggle_; module::StringChoiceAttribute *fit_mode_; module::StringChoiceAttribute *mirror_mode_; - module::StringAttribute *frame_error_message_; module::StringChoiceAttribute *filter_mode_preference_; module::StringChoiceAttribute *texture_mode_preference_; module::StringChoiceAttribute *mouse_wheel_behaviour_; module::BooleanAttribute *hud_toggle_; module::StringChoiceAttribute *hud_elements_; + module::StringAttribute *frame_rate_expr_; + module::StringAttribute *custom_cursor_name_; + module::BooleanAttribute *sync_to_main_viewport_; utility::Uuid zoom_hotkey_; utility::Uuid pan_hotkey_; utility::Uuid reset_hotkey_; utility::Uuid fit_mode_hotkey_; utility::Uuid mirror_mode_hotkey_; + utility::Uuid hover_select_; + utility::Uuid toggle_hug_; + + utility::Uuid reset_menu_item_; utility::time_point t1_; void hotkey_pressed( - const utility::Uuid &hotkey_uuid, const std::string &context) override; + const utility::Uuid &hotkey_uuid, + const std::string &context, + const std::string &window) override; void hotkey_released( const utility::Uuid &hotkey_uuid, const std::string &context) override; void update_attrs_from_preferences(const utility::JsonStore &) override; + struct ViewportLayout { + + caf::actor viewport_layout_controller_; + ViewportRendererPtr viewport_layout_renderer_; + std::vector> compare_modes_; + }; + std::vector viewport_layouts_; - ViewportRendererPtr the_renderer_; + ViewportRendererPtr active_renderer_; }; } // namespace viewport } // namespace ui diff --git a/include/xstudio/ui/viewport/viewport_frame_queue_actor.hpp b/include/xstudio/ui/viewport/viewport_frame_queue_actor.hpp index 0136f351e..2e8f8dcfb 100644 --- a/include/xstudio/ui/viewport/viewport_frame_queue_actor.hpp +++ b/include/xstudio/ui/viewport/viewport_frame_queue_actor.hpp @@ -2,6 +2,11 @@ #include "xstudio/ui/viewport/viewport.hpp" namespace xstudio { + +namespace media_reader { + class ImageBufferSet; +} + namespace ui { namespace viewport { @@ -25,18 +30,24 @@ namespace ui { public: ViewportFrameQueueActor( caf::actor_config &cfg, + caf::actor viewport, std::map overlay_actors, - const int viewport_index); + const std::string &viewport_name, + caf::actor colour_pipeline); + + ~ViewportFrameQueueActor() override; + + void on_exit() override; private: caf::behavior make_behavior() override { return behavior_; } - caf::behavior behavior_; + caf::disposable monitor_; - caf::actor playhead_broadcast_group_; - caf::actor playhead_; - utility::Uuid current_playhead_; + caf::behavior behavior_; + utility::UuidActor playhead_; + utility::Uuid current_key_sub_playhead_id_, previous_key_sub_playhead_id_; /** * @brief Receive frame buffer and colour pipeline data for drawing to screen. * @@ -47,51 +58,33 @@ namespace ui { virtual void queue_image_buffer_for_drawing( media_reader::ImageBufPtr &buf, const utility::Uuid &playhead_id); - /** - * @brief Get a pointer to the framebuffer and colour pipe data that should be - * displayed in the next redraw. We also get the next framebuffer that - * we'll want to draw, if there is one in the queue. Calling this removes any - * images in the queue for display that have a display timestamp older than the - * timestamp for the returned data - * - * @details This function should be used by classes subclassing the Viewport in - * their main draw function to receive the image(s) to be draw to screen. - * Returns an empty pointer if the image does not need to be refreshed since the - * last draw. - */ - void get_frames_for_display( - const utility::Uuid &playhead_id, - std::vector &next_images); - /** - * @brief Tell the viewport that certain playheads are defunct. - * - * @details The viewport can remove any images that came from these - * playheads that are in the display queue - */ - void - child_playheads_deleted(const std::vector &child_playhead_uuids); + void get_frames_for_display_sync( + caf::typed_response_promise rp); + + void get_frames_for_display( + caf::typed_response_promise rp, + const utility::time_point &when_going_on_screen = utility::time_point()); + void clear_images_from_old_playheads(); - void add_blind_data_to_image_in_queue( - const media_reader::ImageBufPtr &image, - const utility::BlindDataObjectPtr &bdata, - const utility::Uuid &overlay_actor_uuid); + void append_overlays_data( + caf::typed_response_promise rp, + media_reader::ImageBufDisplaySet *result); - void update_blind_data( - const std::vector bufs, const bool wait = true); + void append_overlays_data( + caf::typed_response_promise rp, + const int img_idx, + media_reader::ImageBufDisplaySet *result, + std::shared_ptr response_count); - typedef std::vector OrderedImagesToDraw; + typedef std::map OrderedImagesToDraw; media_reader::ImageBufPtr get_least_old_image_in_set(const OrderedImagesToDraw &image_set); void drop_old_frames(const utility::time_point out_of_date_threshold); - void update_image_blind_data_and_deliver( - const media_reader::ImageBufPtr &buf, - caf::typed_response_promise> rp); - timebase::flicks compute_video_refresh() const; utility::time_point @@ -99,13 +92,19 @@ namespace ui { timebase::flicks predicted_playhead_position_at_next_video_refresh(); + timebase::flicks predicted_playhead_position(const utility::time_point &when); + double average_video_refresh_period() const; + caf::typed_response_promise + set_playhead(utility::UuidActor playhead, const bool prefetch_inital_image); + bool playing_ = {false}; bool playing_forwards_ = {true}; - std::map overlay_actors_; + std::map viewport_overlay_plugins_; + const std::string viewport_name_; caf::actor colour_pipeline_; std::map frames_to_draw_per_playhead_; @@ -117,8 +116,17 @@ namespace ui { } video_refresh_data_; timebase::flicks playhead_vid_sync_phase_adjust_ = timebase::k_flicks_zero_seconds; + utility::time_point last_playhead_switch_tp_; + utility::time_point last_playhead_set_tp_; + std::set deleted_playheads_; + utility::UuidVector sub_playhead_ids_; + + playhead::AssemblyMode playhead_mode_; + caf::actor viewport_layout_manager_; + caf::actor viewport_; + std::string viewport_layout_mode_name_; - const int viewport_index_; + double playhead_velocity_ = {1.0}; }; } // namespace viewport diff --git a/include/xstudio/ui/viewport/viewport_layout_plugin.hpp b/include/xstudio/ui/viewport/viewport_layout_plugin.hpp new file mode 100644 index 000000000..6ba7bfe66 --- /dev/null +++ b/include/xstudio/ui/viewport/viewport_layout_plugin.hpp @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + + +#include "xstudio/media_reader/image_buffer_set.hpp" +#include "xstudio/plugin_manager/plugin_base.hpp" +#include "xstudio/plugin_manager/plugin_manager.hpp" +#include "xstudio/media/media.hpp" + +#define NO_FRAME INT_MIN + + +namespace xstudio { +namespace ui { + namespace viewport { + + /** + * @brief ViewportLayoutPlugin class. + * + * @details + * Subclass from this to create custom layouts for mulitple images in the + * xSTUDIO viewport. The plugin is told about the number of images that + * are available for drawing into the viewport. The plugin is then + * resposible for providing a transform matrix for each image to position + * each image in the viewport coordinate space. + * Plugins can also provide their own custom GPU shader code for both the + * vertex and fragment shaders at draw time. + * A complete draw routine can also be overidden for full control of how + * image pixels are rendered to the viewport. + */ + + class ViewportLayoutPlugin : public plugin::StandardPlugin { + public: + ViewportLayoutPlugin( + caf::actor_config &cfg, const utility::JsonStore &init_settings); + + protected: + /** + * @brief Calculate a transform matrix for each on-screen video item + * for displaying into the viewport coordinate space. Also provide an + * aspect ratio hint for the overall layout geometry. + * + * @details To use the regular viewport renderer, which does a + * straightforward draw of each image, set the member data in the + * layout_data. See image_buffer_set.hpp for more details. + * + * This method is called once only for a given image_set characteristics. + * The characteristics depends on the number of images and the size in + * pixels of each image in the set. If you want to re-compute the layout + * for a given image set characteritics call 'clear_cache()' to force + * a re-evaluation of this method. + */ + virtual void do_layout( + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set, + media_reader::ImageSetLayoutData &layout_data); + + /** + * @brief Override this method to provide your own, custom renderer. If + * you don't override the default renderer will be provided which may + * or may not be able to enable your desired viewport graphics layout. + * + * @details See the default renderer implementations for a reference + * for creating a custom viewport renderer + */ + virtual ViewportRenderer * + make_renderer(const std::string &window_id, const utility::JsonStore &prefs) = 0; + + /** + * @brief Call this method to add a layout mode that your plugin + * provides. The string sets the name, this will appear in the list + * of compare modes. If the name is already taken a warning is + * generated. + * The AssemblyMode tells the playhead how many images are needed to + * draw the viewport layout. + * + */ + void add_layout_mode( + const std::string &name, + const float menu_position, + const playhead::AssemblyMode mode, + const playhead::AutoAlignMode default_auto_align = + playhead::AutoAlignMode::AAM_ALIGN_OFF); + + /** + * @brief Expose an attribute in the Settings panel for your layout + * plugin. The button for the settings panel will show under the + * 'Compare' viewport toolbar button. + * + */ + void add_layout_settings_attribute( + module::Attribute *attr, const std::string &layout_name); + + /** + * @brief Call this method with necessary QML snippet to instance + * custom overlay graphics for the viewport. + * See plugins/viewport_layout/wipe_viewport_layout for example useage + * + */ + void add_viewport_layout_qml_overlay( + const std::string &layout_name, const std::string &qml_code); + + void on_exit() override; + + private: + void init(); + + using LayoutDataRP = + caf::typed_response_promise; + + void attribute_changed( + const utility::Uuid &attribute_uuid, const int /*role*/ + ) override; + + void __do_layout( + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set, + LayoutDataRP rp); + + media_reader::ImageSetLayoutDataPtr + python_layout_data_to_ours(const utility::JsonStore &python_data) const; + + caf::message_handler message_handler_extensions() override { return handler_; } + + caf::message_handler handler_; + + std::map layout_names_; + std::set layouts_with_settings_; + + std::map> + layouts_cache_; + + std::map> pending_responses_; + + const bool is_python_plugin_; + + + // Python ViewportLayout plugins work by setting this 'remote_' + // member. This C++ base class talks to the remote_ (which is the + // Python class instance) to effectively provide the virtual methods. + caf::actor event_group_; + caf::actor layouts_manager_; + caf::actor gobal_playhead_events_; + }; + + /** + * @brief ViewportLayoutManager class. + * + * @details + * A singleton instance of this class is created by the GlobalActor. The + * ViewportLayoutManager instances ViewportLayoutPlugins, provides model + * data about them to the UI layer so that the Compare button/menu can + * be populated, and activates the layout plugin when the user or api + * switches the active layout. + */ + + class ViewportLayoutManager : public caf::event_based_actor { + public: + ViewportLayoutManager(caf::actor_config &cfg); + + ~ViewportLayoutManager() override; + + void on_exit() override; + + caf::behavior make_behavior() override { return behavior_; } + + private: + void spawn_plugins(); + + caf::behavior behavior_; + + std::map viewport_layout_plugins_; + std::map< + std::string, + std::pair< + caf::actor, + std::pair>> + viewport_layouts_; + caf::actor event_group_; + }; + + } // namespace viewport +} // namespace ui +} // namespace xstudio diff --git a/include/xstudio/ui/viewport/viewport_renderer_base.hpp b/include/xstudio/ui/viewport/viewport_renderer_base.hpp index 5d3c58dbf..5999c13b5 100644 --- a/include/xstudio/ui/viewport/viewport_renderer_base.hpp +++ b/include/xstudio/ui/viewport/viewport_renderer_base.hpp @@ -2,7 +2,7 @@ #pragma once #include -#include "xstudio/media_reader/image_buffer.hpp" +#include "xstudio/media_reader/image_buffer_set.hpp" #include "xstudio/plugin_manager/plugin_base.hpp" namespace xstudio { @@ -38,44 +38,18 @@ namespace ui { * an active OpenGL context) */ virtual void render( - const std::vector &next_images, - const Imath::M44f &to_scene_matrix, - const Imath::M44f &projection_matrix, - const Imath::M44f &fit_mode_matrix) = 0; - - /** - * @brief Provide default preference dictionary for the viewport renderer - * - * @details ViewportRenderer implementations can declare a json dict for - * preferences specific to the given implementation - for example, texture mode or - * async upload settings that may be necessary for tuning performance on different - * systems. The xSTUDIO UI layer will expose these preferences in the prefs panel. - */ - virtual utility::JsonStore default_prefs() = 0; - - /** - * @brief Set rendering preferences - * - * @details A JSON dict passed in from UI layer - store the preference values as - * requires - */ - virtual void set_prefs(const utility::JsonStore &prefs) = 0; - - void set_aux_shader_uniforms(const utility::JsonStore &uniforms) { - shader_uniforms_ = uniforms; - } - - void add_overlay_renderer( - const utility::Uuid &uuid, plugin::ViewportOverlayRendererPtr renderer) { - viewport_overlay_renderers_[uuid] = renderer; + const media_reader::ImageBufDisplaySetPtr &images, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_matrix, + const Imath::V2i &window_size, + const std::map + &overlay_renderers) = 0; + + void set_pre_renderer_hooks( + const std::map &hooks) { + pre_render_gpu_hooks_ = hooks; } - void - add_pre_renderer_hook(const utility::Uuid &uuid, plugin::GPUPreDrawHookPtr hook) { - pre_render_gpu_hooks_[uuid] = hook; - } - - void set_render_hints(RenderHints hint) { render_hints_ = hint; } inline static const std::vector< @@ -96,6 +70,28 @@ namespace ui { } protected: + /** + * @brief Create a json dict with essential shader parameters for drawing + * the image to the viewport. + * + * @details The json dictionary returned from this function is as follows: + * + * mat4 to_coord_system (matrix to project from viewport to viewport coordinate + * system mat4 to_canvas (matrix to transform from window area to viewport area) + * bool use_bilinear_filtering (tells us whether to use bilinear pixel filtering) + * mat4 image_transform_matrix (transform image into viewport coordinate system) + * + */ + virtual utility::JsonStore core_shader_params( + const media_reader::ImageBufPtr &image_to_be_drawn, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx, + const utility::JsonStore &layout_data, + const int image_index) const; + + Imath::M44f mat; + /** * @brief Initialise the viewport. * @@ -104,14 +100,10 @@ namespace ui { */ virtual void pre_init() = 0; - std::map - viewport_overlay_renderers_; - std::map pre_render_gpu_hooks_; RenderHints render_hints_ = {BilinearWhenZoomedOut}; bool done_init_ = false; - utility::JsonStore shader_uniforms_; }; typedef std::shared_ptr ViewportRendererPtr; diff --git a/include/xstudio/utility/authentication.hpp b/include/xstudio/utility/authentication.hpp new file mode 100644 index 000000000..79e6b7bec --- /dev/null +++ b/include/xstudio/utility/authentication.hpp @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +//#include + +namespace xstudio::utility {} diff --git a/include/xstudio/utility/caf_helpers.hpp b/include/xstudio/utility/caf_helpers.hpp index 447ff961c..b8ed7ed67 100644 --- a/include/xstudio/utility/caf_helpers.hpp +++ b/include/xstudio/utility/caf_helpers.hpp @@ -29,14 +29,9 @@ namespace utility { struct absolute_receive_timeout { public: - using ms = std::chrono::milliseconds; -#ifdef _WIN32 + using ms = std::chrono::milliseconds; using clock_type = std::chrono::high_resolution_clock; - ; -#else - using clock_type = std::chrono::system_clock; - // using clock_type = std::chrono::high_resolution_clock; -#endif + absolute_receive_timeout(int msec) { x_ = clock_type::now() + ms(msec); } absolute_receive_timeout() = default; diff --git a/include/xstudio/utility/chrono.hpp b/include/xstudio/utility/chrono.hpp index 68b4f3d45..a66a4b99e 100644 --- a/include/xstudio/utility/chrono.hpp +++ b/include/xstudio/utility/chrono.hpp @@ -18,7 +18,7 @@ namespace utility { using milliseconds = std::chrono::milliseconds; #ifdef _WIN32 - using sysclock = std::chrono::high_resolution_clock; + using sysclock = std::chrono::system_clock; #else using sysclock = std::chrono::system_clock; #endif diff --git a/include/xstudio/utility/container.hpp b/include/xstudio/utility/container.hpp index 09447be25..feebb28cd 100644 --- a/include/xstudio/utility/container.hpp +++ b/include/xstudio/utility/container.hpp @@ -9,17 +9,16 @@ #include #include -#include - #include #include -#include - #include "xstudio/atoms.hpp" #include "xstudio/utility/chrono.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/uuid.hpp" +#include +#include + namespace xstudio { namespace utility { @@ -62,9 +61,15 @@ namespace utility { : name_(std::move(name)), type_(std::move(type)), uuid_(std::move(uuid)), - version_(PROJECT_VERSION) {} + version_(PROJECT_VERSION) { + // register_container(*this); + } explicit Container(const utility::JsonStore &jsn); - virtual ~Container() = default; + virtual ~Container() { + // unregister_container(*this); + } + + static caf::message_handler default_event_handler(); [[nodiscard]] utility::Uuid uuid() const { return uuid_; } [[nodiscard]] std::string name() const { return name_; } @@ -89,13 +94,25 @@ namespace utility { [[nodiscard]] Container duplicate() const; + caf::message_handler container_message_handler(caf::event_based_actor *act); + + caf::actor event_group() const { return event_group_; } + + void send_changed(const time_point last_changed = utility::clock::now()) { + if (last_changed > last_changed_) { + last_changed_ = std::move(last_changed); + actor_->mail(event_atom_v, last_changed_atom_v, last_changed_) + .send(event_group_); + } + } + void send_changed( caf::actor grp, caf::event_based_actor *act, const time_point last_changed = utility::clock::now()) { if (last_changed > last_changed_) { last_changed_ = std::move(last_changed); - act->send(grp, event_atom_v, last_changed_atom_v, last_changed_); + act->mail(event_atom_v, last_changed_atom_v, last_changed_).send(grp); } } @@ -105,7 +122,7 @@ namespace utility { const time_point last_changed = utility::clock::now()) { if (last_changed > last_changed_) { last_changed_ = std::move(last_changed); - act->send(grp, event_atom_v, last_changed_atom_v, last_changed_); + act->mail(event_atom_v, last_changed_atom_v, last_changed_).send(grp); } } @@ -116,17 +133,17 @@ namespace utility { return [=](name_atom, const std::string &name) { name_ = name; }; } - auto make_set_name_handler(caf::actor grp, caf::event_based_actor *act) { - return [=](name_atom, const std::string &name) { - name_ = name; - act->send(grp, event_atom_v, name_atom_v, name); - send_changed(grp, act); - }; - } + // auto make_set_name_handler(caf::actor grp, caf::event_based_actor *act) { + // return [=](name_atom, const std::string &name) { + // name_ = name; + // act->send(grp, event_atom_v, name_atom_v, name); + // send_changed(grp, act); + // }; + // } auto make_set_name_handler(caf::actor grp, caf::blocking_actor *act) { return [=](name_atom, const std::string &name) { name_ = name; - act->send(grp, event_atom_v, name_atom_v, name); + act->mail(event_atom_v, name_atom_v, name).send(grp); send_changed(grp, act); }; } @@ -134,28 +151,32 @@ namespace utility { auto make_last_changed_getter() { return [=](last_changed_atom) -> time_point { return last_changed_; }; } - auto make_last_changed_setter(caf::actor grp, caf::event_based_actor *act) { - return [=](last_changed_atom, const time_point &last_changed) { - if (last_changed > last_changed_ or last_changed == time_point()) { - last_changed_ = last_changed; - act->send(grp, utility::event_atom_v, last_changed_atom_v, last_changed_); - } - }; - } - auto make_last_changed_event_handler(caf::actor grp, caf::event_based_actor *act) { - return [=](utility::event_atom, last_changed_atom, const time_point &last_changed) { - if (last_changed > last_changed_) { - last_changed_ = last_changed; - act->send(grp, utility::event_atom_v, last_changed_atom_v, last_changed_); - } - }; - } + // auto make_last_changed_setter(caf::actor grp, caf::event_based_actor *act) { + // return [=](last_changed_atom, const time_point &last_changed) { + // if (last_changed > last_changed_ or last_changed == time_point()) { + // last_changed_ = last_changed; + // act->send(grp, utility::event_atom_v, last_changed_atom_v, + // last_changed_); + // } + // }; + // } + // auto make_last_changed_event_handler(caf::actor grp, caf::event_based_actor *act) { + // return [=](utility::event_atom, last_changed_atom, const time_point + // &last_changed) { + // if (last_changed > last_changed_) { + // last_changed_ = last_changed; + // act->send(grp, utility::event_atom_v, last_changed_atom_v, + // last_changed_); + // } + // }; + // } auto make_last_changed_setter(caf::actor grp, caf::blocking_actor *act) { return [=](last_changed_atom, const time_point &last_changed) { if (last_changed > last_changed_ or last_changed == time_point()) { last_changed_ = last_changed; - act->send(grp, utility::event_atom_v, last_changed_atom_v, last_changed_); + act->mail(utility::event_atom_v, last_changed_atom_v, last_changed_) + .send(grp); } }; } @@ -163,15 +184,12 @@ namespace utility { return [=](utility::event_atom, last_changed_atom, const time_point &last_changed) { if (last_changed > last_changed_) { last_changed_ = last_changed; - act->send(grp, utility::event_atom_v, last_changed_atom_v, last_changed_); + act->mail(utility::event_atom_v, last_changed_atom_v, last_changed_) + .send(grp); } }; } - auto make_ignore_error_handler() { - return [=](const caf::error) {}; - } - auto make_get_uuid_handler() { return [=](uuid_atom) -> Uuid { return uuid_; }; } @@ -179,10 +197,11 @@ namespace utility { return [=](type_atom) -> std::string { return type_; }; } - auto - make_get_detail_handler(caf::event_based_actor *act, caf::actor group = caf::actor()) { - return [=](detail_atom) -> ContainerDetail { return detail(act, group); }; - } + // auto + // make_get_detail_handler(caf::event_based_actor *act, caf::actor group = caf::actor()) + // { + // return [=](detail_atom) -> ContainerDetail { return detail(act, group); }; + // } auto make_get_detail_handler(caf::blocking_actor *act, caf::actor group = caf::actor()) { return [=](detail_atom) -> ContainerDetail { return detail(act, group); }; @@ -204,6 +223,11 @@ namespace utility { } private: + void register_container(const Container &cnt); + void unregister_container(const Container &cnt); + + caf::event_based_actor *actor_{nullptr}; + caf::actor event_group_; std::string name_; std::string type_; utility::Uuid uuid_; diff --git a/include/xstudio/utility/edit_list.hpp b/include/xstudio/utility/edit_list.hpp deleted file mode 100644 index 6b0f3c484..000000000 --- a/include/xstudio/utility/edit_list.hpp +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "xstudio/utility/edit_section.hpp" - -namespace xstudio { -namespace utility { - - // container for handling clip time/frames positions - class EditList { - public: - EditList(ClipList sl = ClipList()) : sl_(std::move(sl)) {} - EditList(const EditList &) = default; - EditList(EditList &&) = default; - virtual ~EditList() = default; - - EditList &operator=(const EditList &) = default; - EditList &operator=(EditList &&) = default; - - void extend(const EditList &o); - - void append(const EditListSection &s) { sl_.push_back(s); } - - [[nodiscard]] bool empty() const { return sl_.empty(); } - [[nodiscard]] size_t size() const { return sl_.size(); } - void clear() { sl_.clear(); } - [[nodiscard]] const ClipList §ion_list() const { return sl_; } - - [[nodiscard]] size_t duration_frames( - const TimeSourceMode tsm = TimeSourceMode::FIXED, - const FrameRate &fr = FrameRate()) const; - [[nodiscard]] double duration_seconds( - const TimeSourceMode tsm = TimeSourceMode::FIXED, - const FrameRate &fr = FrameRate()) const; - [[nodiscard]] timebase::flicks duration_flicks( - const TimeSourceMode tsm = TimeSourceMode::FIXED, - const FrameRate &fr = FrameRate()) const; - - [[nodiscard]] std::vector> - frame_durations(const TimeSourceMode tsm, const FrameRate &fr = FrameRate()) const; - [[nodiscard]] std::vector> - second_durations(const TimeSourceMode tsm, const FrameRate &fr = FrameRate()) const; - [[nodiscard]] std::vector> - flick_durations(const TimeSourceMode tsm, const FrameRate &fr = FrameRate()) const; - - [[nodiscard]] timebase::flicks flicks_from_logical( - const int logical_frame, - const TimeSourceMode tsm, - const FrameRate &fr = FrameRate()) const; - - EditListSection media_frame(const int logical_frame, int &media_frame) const; - - [[nodiscard]] timebase::flicks media_frame_to_flicks( - const utility::Uuid &media_source, - const int logical_frame, - const TimeSourceMode tsm, - const FrameRate &rate) const; - - [[nodiscard]] timebase::flicks media_flick_to_flicks( - const utility::Uuid &media_source, - const timebase::flicks media_flick, - const TimeSourceMode tsm, - const FrameRate &rate) const; - - [[nodiscard]] int logical_frame( - const TimeSourceMode tsm, - const FrameRate &rate, - const FrameRateDuration &frame) const; - - [[nodiscard]] int logical_frame( - const TimeSourceMode tsm, - const timebase::flicks target_flicks, - const utility::FrameRate &rate = utility::FrameRate(1.0 / 24.0)) const; - - [[nodiscard]] EditListSection skip_sections(int ref_frame, int skip_by) const; - - [[nodiscard]] timebase::flicks flicks_from_frame( - const TimeSourceMode tsm, - const int frame, - const utility::FrameRate &rate = utility::FrameRate(1.0 / 24.0)) const; - - [[nodiscard]] FrameRate frame_rate_at_frame(const int logical_frame) const; - - int step( - const TimeSourceMode tsm, - const FrameRate &rate, - const bool forward, - const float velocity, - const FrameRateDuration &frame, - FrameRateDuration &new_frame, - timebase::flicks &new_seconds) const; - - int step( - const TimeSourceMode tsm, - const FrameRate &override_rate, - const bool forward, - const float velocity, - const int logical_frame, - const int in_frame, - const int out_frame, - timebase::flicks &step_period) const; - - [[nodiscard]] std::pair flicks_range_to_logical_frame_range( - const timebase::flicks &in, - const timebase::flicks &out, - const TimeSourceMode tsm, - const FrameRate &rate) const; - - [[nodiscard]] EditListSection next_section( - const timebase::flicks &ref_timepoint, - const int skip_by, - const TimeSourceMode tsm, - const FrameRate &rate) const; - - timebase::flicks section_start_time( - const utility::Uuid &media_uuid, - const TimeSourceMode tsm, - const FrameRate &override_rate, - FrameRate &out_rate) const; - - void set_uuid(const Uuid &uuid); - - void push_back(const EditListSection &s) { sl_.push_back(s); } - - bool operator==(const EditList &other) const { - if (sl_.size() != other.sl_.size()) - return false; - for (size_t i = 0; i < sl_.size(); i++) { - if (not(sl_[i] == other.sl_[i])) - return false; - } - - return true; - } - - template friend bool inspect(Inspector &f, EditList &x) { - return f.object(x).fields(f.field("sl", x.sl_)); - } - - private: - ClipList sl_; - }; -} // namespace utility -} // namespace xstudio diff --git a/include/xstudio/utility/edit_section.hpp b/include/xstudio/utility/edit_section.hpp deleted file mode 100644 index 6815e6a0e..000000000 --- a/include/xstudio/utility/edit_section.hpp +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "xstudio/utility/frame_rate_and_duration.hpp" - -namespace xstudio { -namespace utility { - - struct EditListSection { - public: - EditListSection() = default; - EditListSection(const EditListSection &o) = default; - EditListSection(Uuid uuid, FrameRateDuration dur, Timecode tc) - : media_uuid_(std::move(uuid)), - frame_rate_and_duration_(std::move(dur)), - timecode_(std::move(tc)) {} - - EditListSection &operator=(const EditListSection &o) = default; - - bool operator==(const EditListSection &o) const { - return media_uuid_ == o.media_uuid_ && - frame_rate_and_duration_ == o.frame_rate_and_duration_ && - timecode_ == o.timecode_; - } - - template friend bool inspect(Inspector &f, EditListSection &x) { - return f.object(x).fields( - f.field("source_uuid", x.media_uuid_), - f.field("frame_rate_and_duration", x.frame_rate_and_duration_), - f.field("timecode", x.timecode_)); - } - - Uuid media_uuid_; - FrameRateDuration frame_rate_and_duration_; - Timecode timecode_; - }; - - using ClipList = std::vector; -} // namespace utility -} // namespace xstudio diff --git a/include/xstudio/utility/enums.hpp b/include/xstudio/utility/enums.hpp index 3eda9e89a..1d96112e6 100644 --- a/include/xstudio/utility/enums.hpp +++ b/include/xstudio/utility/enums.hpp @@ -4,5 +4,14 @@ namespace xstudio { namespace utility { enum class TimeSourceMode { FIXED = 1, REMAPPED = 2, DYNAMIC = 3 }; -} + enum NotificationType { + NT_UNKNOWN = 0, + NT_INFO, + NT_WARN, + NT_PROCESSING, + NT_PROGRESS_RANGE, + NT_PROGRESS_PERCENTAGE + }; + +} // namespace utility } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/utility/frame_list.hpp b/include/xstudio/utility/frame_list.hpp index 07d2516b3..96d6d0b8a 100644 --- a/include/xstudio/utility/frame_list.hpp +++ b/include/xstudio/utility/frame_list.hpp @@ -7,8 +7,7 @@ #include #include - -#include "xstudio/utility/edit_list.hpp" +#include namespace xstudio { namespace utility { @@ -71,6 +70,9 @@ namespace utility { [[nodiscard]] const std::vector &frame_groups() const { return frame_groups_; } + + std::vector &frame_groups() { return frame_groups_; } + void set_frame_groups(const std::vector &frame_groups) { frame_groups_ = frame_groups; } diff --git a/include/xstudio/utility/frame_range.hpp b/include/xstudio/utility/frame_range.hpp index 25ca7cc78..7541247d2 100644 --- a/include/xstudio/utility/frame_range.hpp +++ b/include/xstudio/utility/frame_range.hpp @@ -19,6 +19,11 @@ namespace utility { FrameRange(const FrameRateDuration &duration) : rate_(duration.rate()), duration_(duration.duration()) {} + FrameRange(const FrameRate start, const FrameRate duration, const FrameRate rate) + : start_(std::move(start)), + duration_(std::move(duration)), + rate_(std::move(rate)) {} + FrameRange(const FrameRateDuration &start, const FrameRateDuration &duration) : rate_(duration.rate()), start_(start.duration()), @@ -36,6 +41,30 @@ namespace utility { [[nodiscard]] FrameRate start() const { return start_; } [[nodiscard]] FrameRate duration() const { return duration_; } + // ignoring rate... + [[nodiscard]] FrameRange intersect(const FrameRange &fr) const { + auto result = fr; + + // adjust start + if (result.start_ < start_) { + result.duration_ -= + std::max((start_ - result.start_), timebase::k_flicks_zero_seconds); + result.start_ = start_; + } + + if (result.start_ >= start_ + duration_) { + result.duration_ = timebase::k_flicks_zero_seconds; + result.start_ = start_ + duration_; + } + + // adjust end + if (result.start_ + result.duration_ > start_ + duration_) { + result.duration_ -= (result.start_ + result.duration_) - (start_ + duration_); + } + + return result; + } + void set_rate(const FrameRate &rate) { rate_ = rate; } void set_start(const FrameRate &start) { start_ = start; } void set_duration(const FrameRate &duration) { duration_ = duration; } diff --git a/include/xstudio/utility/frame_rate.hpp b/include/xstudio/utility/frame_rate.hpp index 98781359f..7f85cab68 100644 --- a/include/xstudio/utility/frame_rate.hpp +++ b/include/xstudio/utility/frame_rate.hpp @@ -13,21 +13,96 @@ namespace xstudio { namespace utility { + static timebase::flicks fps_to_flicks(const double s) { + auto result = std::chrono::duration_cast( + std::chrono::duration{1.0 / s}); + + // if result is close to a standard rate tweak it. + std::vector std_rates = { + timebase::flicks(3675), // 192000 fps + timebase::flicks(7350), // 96000 fps + timebase::flicks(8000), // 88200 fps + timebase::flicks(14700), // 48000 fps + timebase::flicks(16000), // 44100 fps + timebase::flicks(22050), // 32000 fps + timebase::flicks(29400), // 24000 fps + timebase::flicks(32000), // 22050 fps + timebase::flicks(44100), // 16000 fps + timebase::flicks(88200), // 8000 fps + timebase::flicks(5880000), // 120 fps + timebase::flicks(5885880), // 120 * 1000/1001 (~119.88) fps + timebase::flicks(7056000), // 100 fps + timebase::flicks(7840000), // 90 fps + timebase::flicks(11760000), // 60 fps + timebase::flicks(11771760), // 60 * 1000/1001 (~59.94) fps + timebase::flicks(14112000), // 50 fps + timebase::flicks(14700000), // 48 fps + timebase::flicks(23520000), // 30 fps + timebase::flicks(23543520), // 30 * 1000/1001 (~29.97) fps + timebase::flicks(28224000), // 25 fps + timebase::flicks(29400000), // 24 fps + timebase::flicks(29429400) // 24 * 1000/1001 (~23.976) fps fps + }; + + for (const auto &i : std_rates) { + if (result.count() > i.count() - 3 and result.count() < i.count() + 3) { + result = i; + break; + } + if (result < i) + break; + } + + return result; + } + + class FrameRate : public timebase::flicks { + public: + inline static const std::map rate_string_to_flicks = { + {"23.976", timebase::flicks(29429400)}, + {"24.0", timebase::flicks(29400000)}, + {"25.0", timebase::flicks(28224000)}, + {"29.97", timebase::flicks(23543520)}, + {"30.0", timebase::flicks(23520000)}, + {"48.0", timebase::flicks(14700000)}, + {"50.0", timebase::flicks(14112000)}, + {"59.94", timebase::flicks(11771760)}, + {"60.0", timebase::flicks(11760000)}, + {"90.0", timebase::flicks(7840000)}, + {"100.0", timebase::flicks(7056000)}, + {"119.88", timebase::flicks(5885880)}, + {"120.0", timebase::flicks(5880000)}}; + FrameRate(const timebase::flicks &flicks = timebase::k_flicks_zero_seconds) : timebase::flicks(flicks) {} FrameRate(const double seconds) : FrameRate(timebase::to_flicks(seconds)) {} + FrameRate(const std::string &rate_string) { + const auto p = rate_string_to_flicks.find(rate_string); + if (p != rate_string_to_flicks.end()) { + *this = p->second; + } else { + try { + double d = std::stod(rate_string); + *this = timebase::to_flicks(1.0 / d); + } catch (...) { + *this = timebase::k_flicks_zero_seconds; + } + } + } FrameRate(const FrameRate &) = default; FrameRate(FrameRate &&) = default; ~FrameRate() = default; - // double to_double() const; [[nodiscard]] std::chrono::microseconds to_microseconds() const { return std::chrono::duration_cast(*this); } + // operator timebase::flicks() const { return this->to_flicks(); } + // operator const timebase::flicks & () const { return *this; } + // [[nodiscard]] std::chrono::nanoseconds::rep count() const { return count(); } [[nodiscard]] double to_seconds() const { return timebase::to_seconds(*this); } [[nodiscard]] double to_fps() const { @@ -36,43 +111,22 @@ namespace utility { fps = 1.0 / timebase::to_seconds(*this); return fps; } - [[nodiscard]] timebase::flicks to_flicks() const { return *this; } + [[nodiscard]] timebase::flicks to_flicks() const { + return timebase::flicks(this->count()); + } FrameRate &operator=(const FrameRate &) = default; FrameRate &operator=(FrameRate &&) = default; - // Rational& operator+= (const Rational& other); - // Rational& operator-= (const Rational& other); - // Rational& operator*= (const Rational& other); - // FrameRate &operator/=(const FrameRate &other) { - // *this /= other; - // return *this; - // } - // bool operator==(const FrameRate &other) const { return *this == other; } - - // bool operator!=(const FrameRate &other) const { return data_ != other.data_; } - - operator bool() const { return count() != 0; } + // operator bool() const { return count() != 0; } template friend bool inspect(Inspector &f, FrameRate &x) { return f.object(x).fields(f.field("flick", static_cast(x))); } - - // template friend bool inspect(Inspector &f, FrameRate &x) { - // return f.object(x).fields(f.field("flicks", x)); - // } }; inline std::string to_string(const FrameRate &v) { return std::to_string(v.to_fps()); } - - - // [[gnu::pure]] inline FrameRate operator/(FrameRate a, const FrameRate &b) { - // a /= b; - // return a; - // } - inline void from_json(const nlohmann::json &j, FrameRate &r) { r = timebase::flicks(j); } - inline void to_json(nlohmann::json &j, const FrameRate &r) { j = r.count(); } } // namespace utility } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/utility/frame_rate_and_duration.hpp b/include/xstudio/utility/frame_rate_and_duration.hpp index 25c3986df..61eb13513 100644 --- a/include/xstudio/utility/frame_rate_and_duration.hpp +++ b/include/xstudio/utility/frame_rate_and_duration.hpp @@ -14,13 +14,15 @@ namespace utility { duration_(timebase::k_flicks_zero_seconds) {} FrameRateDuration(const FrameRateDuration &) = default; FrameRateDuration(FrameRateDuration &&) = default; - FrameRateDuration(const int frames, const double fps) + explicit FrameRateDuration(const int frames, const double fps) : rate_(timebase::to_flicks(1.0 / fps)), duration_(rate_.count() * frames) {} - FrameRateDuration(const double seconds, FrameRate rat) + explicit FrameRateDuration(const double seconds, FrameRate rat) : rate_(std::move(rat)), duration_(timebase::to_flicks(seconds)) {} - FrameRateDuration(const timebase::flicks flicks, FrameRate rat) + explicit FrameRateDuration(const FrameRate dur, FrameRate rat) + : rate_(std::move(rat)), duration_(dur.to_flicks()) {} + explicit FrameRateDuration(const timebase::flicks flicks, FrameRate rat) : rate_(std::move(rat)), duration_(flicks) {} - FrameRateDuration(const int frames, FrameRate rat) + explicit FrameRateDuration(const int frames, FrameRate rat) : rate_(std::move(rat)), duration_(rate_.count() * frames) {} // FrameRateDuration (const int frames, const int fps) : rate_(1, den), // count_(timebase_ * frames) {} FrameRateDuration (const int frames, const int num, @@ -29,7 +31,11 @@ namespace utility { FrameRateDuration &operator=(const FrameRateDuration &) = default; FrameRateDuration &operator=(FrameRateDuration &&) = default; + FrameRateDuration &operator+=(const FrameRateDuration &); + FrameRateDuration &operator-=(const FrameRateDuration &); + FrameRateDuration operator-(const FrameRateDuration &); + FrameRateDuration operator+(const FrameRateDuration &); [[nodiscard]] FrameRateDuration subtract_frames(const FrameRateDuration &, const bool remapped = true) const; @@ -51,7 +57,7 @@ namespace utility { [[nodiscard]] int frame(const FrameRateDuration &rt, const bool remapped = true) const; [[nodiscard]] int frames(const FrameRate &override = FrameRate()) const; - void set_frames(const int frames) { duration_ = rate_ * frames; } + void set_frames(const int frames) { duration_ = rate_.to_flicks() * frames; } [[nodiscard]] double seconds(const FrameRate &override = FrameRate()) const; void set_seconds(double seconds) { duration_ = timebase::to_flicks(seconds); } @@ -72,13 +78,12 @@ namespace utility { // FrameRateDuration& operator*= (const FrameRateDuration& other); // FrameRateDuration& operator/= (const FrameRateDuration& other); bool operator==(const FrameRateDuration &other) const { - return rate_ == other.rate_ and duration_ == other.duration_; + return rate_.count() == other.rate_.count() and duration_ == other.duration_; } template friend bool inspect(Inspector &f, FrameRateDuration &x) { return f.object(x).fields(f.field("rate", x.rate_), f.field("data", x.duration_)); } - friend class EditList; public: FrameRate rate_; diff --git a/include/xstudio/utility/frame_time.hpp b/include/xstudio/utility/frame_time.hpp index cd40f3263..505f80fe4 100644 --- a/include/xstudio/utility/frame_time.hpp +++ b/include/xstudio/utility/frame_time.hpp @@ -1,4 +1,2 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once - -#include "xstudio/utility/edit_list.hpp" diff --git a/include/xstudio/utility/helpers.hpp b/include/xstudio/utility/helpers.hpp index ea4535d0c..02eb3cf13 100644 --- a/include/xstudio/utility/helpers.hpp +++ b/include/xstudio/utility/helpers.hpp @@ -13,6 +13,10 @@ #include #include +#ifdef __apple__ +#undef nil +#endif + #include #include @@ -22,6 +26,7 @@ #include "xstudio/utility/string_helpers.hpp" #include "xstudio/caf_error.hpp" #include "xstudio/caf_utility/caf_setup.hpp" + #ifdef _WIN32 #include #include @@ -50,8 +55,8 @@ namespace utility { class ActorSystemSingleton { public: - static caf::actor_system &actor_system_ref(); static caf::actor_system &actor_system_ref(caf::actor_system &sys); + static caf::actor_system &actor_system_ref(); private: ActorSystemSingleton(caf::actor_system &provided_sys) : system_ref_(provided_sys) {} @@ -84,6 +89,10 @@ namespace utility { std::vector read_file(const std::string &path); + inline auto make_ignore_error_handler() { + return [=](const caf::error) {}; + } + inline auto make_get_event_group_handler(caf::actor grp) { return [=](get_event_group_atom) -> caf::actor { return grp; }; } @@ -101,7 +110,6 @@ namespace utility { #endif } - inline bool check_create_path(const std::string &path) { bool create_path = true; bool success = true; @@ -126,12 +134,12 @@ namespace utility { } - inline std::vector hex_to_bytes(const std::string &hex) { - std::vector bytes; + inline std::vector hex_to_bytes(const std::string &hex) { + std::vector bytes; for (unsigned int i = 0; i < hex.length(); i += 2) { bytes.push_back( - static_cast(strtol(hex.substr(i, 2).c_str(), nullptr, 16))); + static_cast(strtol(hex.substr(i, 2).c_str(), nullptr, 16))); } return bytes; @@ -144,7 +152,8 @@ namespace utility { const caf::timespan &wait_for, Ts const &...args) { R result{}; - src.request(dest, wait_for, args...) + src.mail(args...) + .request(dest, wait_for) .receive( [&result](const R &res) mutable { result = std::move(res); }, [=](const caf::error &e) { throw XStudioError(e); }); @@ -156,7 +165,8 @@ namespace utility { R request_receive(caf::blocking_actor &src, const caf::actor &dest, Ts const &...args) { // spdlog::warn("REQUEST"); R result{}; - src.request(dest, caf::infinite, args...) + src.mail(args...) + .request(dest, caf::infinite) .receive( [&result](const R &res) mutable { // spdlog::error("RECEIVE"); @@ -174,7 +184,9 @@ namespace utility { R request_receive_high_priority( caf::blocking_actor &src, const caf::actor &dest, Ts const &...args) { R result{}; - src.request(dest, caf::infinite, args...) + src.mail(args...) + .urgent() + .request(dest, caf::infinite) .receive( [&result](const R &res) mutable { result = std::move(res); }, [=](const caf::error &e) { throw XStudioError(e); }); @@ -200,9 +212,9 @@ namespace utility { void print_on_exit( const caf::actor &hdl, const std::string &name, const Uuid &uuid = utility::Uuid()); - std::string exec(const std::vector &cmd, int &exit_code); + // std::string exec(const std::vector &cmd, int &exit_code); - std::string filemanager_show_uris(const std::vector &uris); + // std::string filemanager_show_uris(const std::vector &uris); caf::uri posix_path_to_uri(const std::string &path, const bool abspath = false); @@ -248,7 +260,7 @@ namespace utility { inline std::array get_signature(const caf::uri &uri) { std::array sig{}; // read header.. caller myse use try block to catch errors - if (to_string(uri.scheme()) != "file") + if (uri.scheme() != "file") return sig; std::ifstream myfile; @@ -267,42 +279,18 @@ namespace utility { return sig; } - inline std::string xstudio_root(const std::string &append_path) { - auto root = get_env("XSTUDIO_ROOT"); - - std::string fallback_root; -#ifdef _WIN32 - char filename[MAX_PATH]; - DWORD nSize = _countof(filename); - DWORD result = GetModuleFileNameA(NULL, filename, nSize); - if (result == 0) { - spdlog::debug("Unable to determine executable path from Windows API, falling back " - "to standard methods"); - } else { - auto exePath = fs::path(filename); - - // The first parent path gets us to the bin directory, the second gets us to the - // level above bin. - auto xstudio_root = exePath.parent_path().parent_path(); - fallback_root = xstudio_root.string(); - } -#else - // TODO: This could inspect the current running process and look one directory up. - fallback_root = std::string(BINARY_DIR); -#endif - + std::string xstudio_root(const std::string &append_path = ""); - std::string path = (root ? (*root) + append_path : fallback_root + append_path); + std::string xstudio_plugin_dir(const std::string &append_path = ""); - return path; - } + std::string xstudio_resources_dir(const std::string &append_path = ""); inline std::string remote_session_path() { const char *root; #ifdef _WIN32 root = std::getenv("USERPROFILE"); #else - root = std::getenv("HOME"); + root = std::getenv("HOME"); #endif std::filesystem::path path; if (root) { @@ -317,7 +305,7 @@ namespace utility { #ifdef _WIN32 root = std::getenv("USERPROFILE"); #else - root = std::getenv("HOME"); + root = std::getenv("HOME"); #endif std::filesystem::path path; if (root) { @@ -335,7 +323,7 @@ namespace utility { #ifdef _WIN32 root = std::getenv("USERPROFILE"); #else - root = std::getenv("HOME"); + root = std::getenv("HOME"); #endif std::filesystem::path path; if (root) { @@ -349,7 +337,13 @@ namespace utility { } inline std::string preference_path_context(const std::string &context) { - return preference_path(to_lower(context) + ".json"); + auto major = std::string(XSTUDIO_GLOBAL_VERSION); + // spdlog::warn("{}", major); + major = major.substr(0, major.find_first_of('.')); + if (major == "0" or major == "1") + return preference_path(to_lower(context) + ".json"); + + return preference_path(to_lower(context) + "-v" + major + ".json"); } inline bool are_same(float a, float b, int decimals) { @@ -359,6 +353,8 @@ namespace utility { std::vector> scan_posix_path(const std::string &path, const int depth = -1); + std::vector uri_subfolders(const caf::uri parent_uri); + std::string get_host_name(); std::string get_user_name(); std::string get_login_name(); @@ -397,7 +393,6 @@ namespace utility { #endif } - inline bool is_file_supported(const caf::uri &uri) { const std::string sp = uri_to_posix_path(uri); std::string ext = to_upper(get_path_extension(fs::path(sp))); @@ -421,7 +416,7 @@ namespace utility { inline bool is_timeline_supported(const caf::uri &uri) { fs::path p(uri_to_posix_path(uri)); - spdlog::error(p.string()); + // spdlog::error(p.string()); std::string ext = to_upper(get_path_extension(p)); for (const auto &i : supported_timeline_extensions) @@ -503,6 +498,15 @@ namespace utility { return result; } + template + std::vector concatenate_vector(const V &a, const V &b) { + std::vector result; + result.reserve(a.size() + b.size()); + result.insert(result.end(), a.begin(), a.end()); + result.insert(result.end(), b.begin(), b.end()); + return result; + } + // this is annoying.. we now have to create all these silly UuidActor functions.. @@ -543,5 +547,27 @@ namespace utility { return result; } + std::string forward_remap_file_path(const std::string path); + + std::string reverse_remap_file_path(const std::string path); + + // The json store here must be an array. Each element in the array must be + // another array of 2 strings and a boolean. + // + // For each entry, the first string is a regex_replace search/match expression. + // The second string is the regex_replace format string. + // The (third) boolean indicates if the remapping is forwards or reverse. + // Currently for this system to work correctly, the reverse is necessary + // because of the way we go back and forth via uri_to_posix_path and + // posix_path_to_uri to do other types of path manipulation. + // + // Example json to replace /jobs/ with J so that: + // + // [ + // ["\\/jobs\\/", "J\\:\\", true], + // ["J\\:\\", "\\/jobs\\/", false] + // ] + void setup_filepath_remap_regex(const utility::JsonStore &); + } // namespace utility -} // namespace xstudio +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/utility/json_store.hpp b/include/xstudio/utility/json_store.hpp index cda5ddfe3..5445e111e 100644 --- a/include/xstudio/utility/json_store.hpp +++ b/include/xstudio/utility/json_store.hpp @@ -33,6 +33,21 @@ template <> struct adl_serializer { } }; +template <> struct adl_serializer { + static void to_json(json &j, const Imath::V4f &c) { + j = json{"vec4", 1, c[0], c[1], c[2], c[3]}; + } + static void from_json(const json &j, Imath::V4f &c) { + auto vv = j.begin(); + vv++; // skip param type + vv++; // skip count + c[0] = (vv++).value().get(); + c[1] = (vv++).value().get(); + c[2] = (vv++).value().get(); + c[3] = (vv++).value().get(); + } +}; + template struct adl_serializer> { static void to_json(json &j, const Imath::Matrix44 &p) { j = json{ @@ -233,6 +248,8 @@ namespace utility { nlohmann::json &ref() { return *this; } + void parse_string(const std::string &data); + // nlohmann::json &operator[](const std::string &key) { return json_[key]; } // const nlohmann::json &operator[](const std::string &key) const { return @@ -306,7 +323,7 @@ namespace utility { merged.merge(j); } } catch ([[maybe_unused]] const std::exception &e) { - spdlog::warn("Preference path does not exist {}. ({})", path); + spdlog::warn("Preference path does not exist {}.", path); } return merged; } diff --git a/include/xstudio/utility/json_store_sync.hpp b/include/xstudio/utility/json_store_sync.hpp new file mode 100644 index 000000000..75b47c768 --- /dev/null +++ b/include/xstudio/utility/json_store_sync.hpp @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/utility/json_store.hpp" +#include "xstudio/utility/uuid.hpp" + +// special class for handing synced json stores, we restrict access so we don't miss changes. + +namespace xstudio::utility { +const auto SyncEvent = + R"({ + "undo": {}, + "redo": {} + })"_json; + +const auto RemoveRowsEvent = + R"({ + "type": "remove_rows", + "parent": "", + "row": -1, + "count": 0, + "id": null + })"_json; + +const auto InsertRowsEvent = + R"({ + "type": "insert_rows", + "parent": "", + "row": -1, + "count": 0, + "id": null, + "data": [] + })"_json; + +const auto InsertEvent = + R"({ + "type": "insert", + "parent": "", + "key": "", + "data": null + })"_json; + +const auto RemoveEvent = + R"({ + "type": "remove", + "parent": "", + "key": "" + })"_json; + +const auto SetEvent = + R"({ + "type": "set", + "parent": "", + "row": -1, + "id": null, + "data": {} + })"_json; + +const auto MoveEvent = + R"({ + "type": "move", + "src_parent": "", + "src_row": -1, + "count": 0, + "id": null, + "dst_parent": "", + "dst_row": -1 + })"_json; + +const auto ResetEvent = + R"({ + "type": "reset", + "id": null, + "data": null + })"_json; + +class JsonStoreSync : protected JsonStore { + public: + typedef std::function + SendEventFunc; + + JsonStoreSync(nlohmann::json json = R"({"children":[]})"_json); + virtual ~JsonStoreSync() = default; + + void bind_send_event_func(SendEventFunc fs); + + void apply_event(const nlohmann::json &event) { process_event(event, true, true, true); } + void unapply_event(const nlohmann::json &event) { process_event(event, false, true, true); } + void process_event( + const nlohmann::json &event, + const bool redo = true, + const bool undo_redo = false, + const bool local = false); + + void insert( + const std::string &key, + const nlohmann::json &data = nlohmann::json(), + const std::string &parent = ""); + + void insert_rows( + const int row, + const int count, + const nlohmann::json &data = nlohmann::json::array(), + const std::string &parent = ""); + + void remove(const std::string &key, const std::string &parent = ""); + + void remove_rows(const int row, const int count, const std::string &parent = ""); + + void + set(const int row, + const std::string &key, + const nlohmann::json &data = nlohmann::json(), + const std::string &parent = ""); + void + set(const int row, + const nlohmann::json &data = nlohmann::json::object(), + const std::string &parent = ""); + + void move_rows( + const std::string &src_parent, + const int src_row, + const int count, + const std::string &dst_parent, + const int dst_row); + + void reset_data(const nlohmann::json &data); + + std::vector find( + const std::string &key, + const std::optional value = {}, + const int max_find_count = -1, + const int prune_depth = -1, + const nlohmann::json::json_pointer &parent = nlohmann::json::json_pointer(), + const int row = 0) const; + + std::optional find_first( + const std::string &key, + const std::optional value = {}, + const int prune_depth = -1, + const nlohmann::json::json_pointer &parent = nlohmann::json::json_pointer(), + const int row = 0) const; + + using JsonStore::dump; + + nlohmann::json::const_reference at(size_type idx) const; + nlohmann::json::const_reference at(const nlohmann::json::json_pointer &ptr) const; + nlohmann::json::const_reference + at(const typename nlohmann::json::object_t::key_type &key) const; + + // template + // const_reference at(KeyType&& key) const; + const nlohmann::json &as_json() const; + + [[nodiscard]] bool origin() const { return origin_; } + void set_origin(const bool origin) { origin_ = origin; } + + [[nodiscard]] utility::Uuid id() const { return id_; } + void set_id(const utility::Uuid &id) { id_ = id; } + + [[nodiscard]] nlohmann::json last_event() const { return last_event_; } + + private: + void reset_data_base( + const nlohmann::json &data, + const bool local, + const utility::Uuid &id, + const bool undo_redo); + + void insert_base( + const std::string &key, + const nlohmann::json &data, + const std::string &parent, + const bool local, + const utility::Uuid &id, + const bool undo_redo); + + void insert_rows_base( + const int row, + const int count, + const nlohmann::json &data, + const std::string &parent, + const bool local, + const utility::Uuid &id, + const bool undo_redo); + + void remove_base( + const std::string &key, + const std::string &parent, + const bool local, + const utility::Uuid &id, + const bool undo_redo); + + void remove_rows_base( + const int row, + const int count, + const std::string &parent, + const bool local, + const utility::Uuid &id, + const bool undo_redo); + + void set_base( + const int row, + const nlohmann::json &data, + const std::string &parent, + const bool local, + const utility::Uuid &id, + const bool undo_redo); + + void move_rows_base( + const std::string &src_parent, + const int src_row, + const int count, + const std::string &dst_parent, + const int dst_row, + const bool local, + const utility::Uuid &id, + const bool undo_redo); + + bool origin_{false}; + SendEventFunc event_send_func_{nullptr}; + utility::Uuid id_ = Uuid::generate(); + nlohmann::json last_event_; + std::string children_{"children"}; +}; +} // namespace xstudio::utility diff --git a/include/xstudio/utility/lock_file.hpp b/include/xstudio/utility/lock_file.hpp index 115138718..6ced4aab5 100644 --- a/include/xstudio/utility/lock_file.hpp +++ b/include/xstudio/utility/lock_file.hpp @@ -217,4 +217,4 @@ namespace utility { }; } // namespace utility -} // namespace xstudio +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/utility/logging.hpp b/include/xstudio/utility/logging.hpp index 7e26aefec..0bf3476be 100644 --- a/include/xstudio/utility/logging.hpp +++ b/include/xstudio/utility/logging.hpp @@ -73,8 +73,8 @@ namespace utility { //! Stop logger void stop_logger(); - /*! @} End of Doxygen Groups*/ + /*! @} End of Doxygen UIDataModels*/ } // namespace utility -/*! @} End of Doxygen Groups*/ +/*! @} End of Doxygen UIDataModels*/ } // namespace xstudio diff --git a/include/xstudio/utility/media_reference.hpp b/include/xstudio/utility/media_reference.hpp index 6984ed712..5d3162391 100644 --- a/include/xstudio/utility/media_reference.hpp +++ b/include/xstudio/utility/media_reference.hpp @@ -5,8 +5,8 @@ #include #include -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/frame_list.hpp" +#include "xstudio/utility/frame_rate_and_duration.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/timecode.hpp" @@ -14,6 +14,8 @@ namespace xstudio { namespace utility { class MediaReference { public: + enum FramePadFormat { FPF_XSTUDIO = 0, FPF_SHAKE, FPF_NUKE }; + MediaReference( caf::uri uri = caf::uri(), const bool container = true, @@ -40,10 +42,11 @@ namespace utility { [[nodiscard]] virtual JsonStore serialise() const; virtual ~MediaReference() = default; - [[nodiscard]] caf::uri uri() const; + [[nodiscard]] caf::uri uri(const FramePadFormat fpf = FPF_XSTUDIO) const; [[nodiscard]] bool container() const { return container_; } [[nodiscard]] int frame_count() const { return duration_.frames(); } [[nodiscard]] int offset() const { return offset_; } + [[nodiscard]] int start_frame_offset() const { return start_frame_offset_; } [[nodiscard]] double seconds(const FrameRate &override = FrameRate()) const { return duration_.seconds(override); } @@ -54,11 +57,17 @@ namespace utility { [[nodiscard]] std::optional uri(const int logical_frame, int &file_frame) const; + // extend and/or fill-out the frame list for numbered frames that are + // not on disk but that are between the first and last numbered frames + // that ARE on disk + void fill_partial_sequences(); + void set_timecode_from_frames(); void set_uri(const caf::uri &uri); void set_frame_list(const FrameList &fl); void set_rate(const FrameRate &rate); void set_offset(const int offset); + void set_start_frame_offset(const int offset) { start_frame_offset_ = offset; } void set_duration(const FrameRateDuration &frd) { duration_ = frd; } [[nodiscard]] FrameRate rate() const { return duration_.rate(); } @@ -77,6 +86,7 @@ namespace utility { f.field("dur", x.duration_), f.field("fl", x.frame_list_), f.field("tc", x.timecode_), + f.field("sfo", x.start_frame_offset_), f.field("off", x.offset_)); } @@ -100,6 +110,9 @@ namespace utility { FrameList frame_list_; Timecode timecode_; + // if we've modified the apparent start, so we can get back to the real range/start + int start_frame_offset_{0}; + int offset_{0}; }; inline std::string to_string(const MediaReference &v) { diff --git a/include/xstudio/utility/mru_cache.hpp b/include/xstudio/utility/mru_cache.hpp index cdaae74b0..220b2655e 100644 --- a/include/xstudio/utility/mru_cache.hpp +++ b/include/xstudio/utility/mru_cache.hpp @@ -40,6 +40,7 @@ namespace utility { typename cache_type::iterator erase(const typename cache_type::iterator &it); + [[nodiscard]] bool empty() const { return count_ == 0; } [[nodiscard]] size_t size() const { return size_; } [[nodiscard]] size_t count() const { return count_; } diff --git a/include/xstudio/utility/notification_handler.hpp b/include/xstudio/utility/notification_handler.hpp new file mode 100644 index 000000000..0e15e3996 --- /dev/null +++ b/include/xstudio/utility/notification_handler.hpp @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include "xstudio/utility/uuid.hpp" +#include "xstudio/utility/chrono.hpp" +#include "xstudio/utility/json_store.hpp" +#include "xstudio/utility/enums.hpp" + +namespace xstudio::utility { + +class NotificationHandler; + +class Notification { + public: + Notification() = default; + Notification(const JsonStore &jsn); + virtual ~Notification() = default; + + Notification( + const std::string text, + const NotificationType type = NT_INFO, + const float progress = 0.0, + const float progress_minimum = 0.0, + const float progress_maximum = 100.0, + const std::chrono::milliseconds expires_in = std::chrono::minutes(10), + caf::event_based_actor *host = nullptr, + const caf::actor &destination = caf::actor()) + : text_(std::move(text)), + type_(type), + progress_(progress), + progress_minimum_(progress_minimum), + progress_maximum_(progress_maximum), + expires_in_(std::move(expires_in)), + host_(host), + destination_(destination) { + send(); + } + + static Notification InfoNotification( + caf::event_based_actor *host, + const caf::actor &destination, + const std::string text, + const std::chrono::milliseconds expires_in = std::chrono::seconds(10)) { + return Notification(text, NT_INFO, 0.0, 0.0, 100.0, expires_in, host, destination); + } + + static Notification InfoNotification( + const std::string text, + const std::chrono::milliseconds expires_in = std::chrono::seconds(10)) { + return Notification(text, NT_INFO, 0.0, 0.0, 100.0, expires_in); + } + + static Notification WarnNotification( + caf::event_based_actor *host, + const caf::actor &destination, + const std::string text, + const std::chrono::milliseconds expires_in = std::chrono::seconds(10)) { + return Notification(text, NT_WARN, 0.0, 0.0, 100.0, expires_in, host, destination); + } + + static Notification WarnNotification( + const std::string text, + const std::chrono::milliseconds expires_in = std::chrono::seconds(10)) { + return Notification(text, NT_WARN, 0.0, 0.0, 100.0, expires_in); + } + + static Notification ProcessingNotification( + caf::event_based_actor *host, const caf::actor &destination, const std::string text) { + return Notification( + text, NT_PROCESSING, 0.0, 0.0, 100.0, std::chrono::minutes(10), host, destination); + } + + static Notification ProcessingNotification(const std::string text) { + return Notification(text, NT_PROCESSING); + } + + static Notification ProgressRangeNotification( + caf::event_based_actor *host, + const caf::actor &destination, + const std::string text, + const float progress = 0.0, + const float progress_minimum = 0.0, + const float progress_maximum = 100.0, + const std::chrono::milliseconds expires_in = std::chrono::minutes(10)) { + return Notification( + text, + NT_PROGRESS_RANGE, + progress, + progress_minimum, + progress_maximum, + expires_in, + host, + destination); + } + + static Notification ProgressRangeNotification( + const std::string text, + const float progress = 0.0, + const float progress_minimum = 0.0, + const float progress_maximum = 100.0, + const std::chrono::milliseconds expires_in = std::chrono::minutes(10)) { + return Notification( + text, NT_PROGRESS_RANGE, progress, progress_minimum, progress_maximum, expires_in); + } + + static Notification ProgressPercentageNotification( + const std::string text, + const float progress = 0.0, + const std::chrono::milliseconds expires_in = std::chrono::minutes(10)) { + return Notification(text, NT_PROGRESS_PERCENTAGE, progress, 0.0, 100.0, expires_in); + } + + static Notification ProgressPercentageNotification( + caf::event_based_actor *host, + const caf::actor &destination, + const std::string text, + const float progress = 0.0, + const std::chrono::milliseconds expires_in = std::chrono::minutes(10)) { + return Notification( + text, NT_PROGRESS_PERCENTAGE, progress, 0.0, 100.0, expires_in, host, destination); + } + + template friend bool inspect(Inspector &f, Notification &x) { + return f.object(x).fields( + f.field("uid", x.uuid_), + f.field("exp", x.expires_), + f.field("exi", x.expires_in_), + f.field("min", x.progress_minimum_), + f.field("max", x.progress_maximum_), + f.field("pro", x.progress_), + f.field("typ", x.type_), + f.field("txt", x.text_)); + } + + friend void to_json(nlohmann::json &j, const Notification &n); + friend class NotificationHandler; + + [[nodiscard]] Uuid uuid() const { return uuid_; } + [[nodiscard]] std::string text() const { return text_; } + [[nodiscard]] float progress() const { return progress_; } + [[nodiscard]] float progress_maximum() const { return progress_maximum_; } + [[nodiscard]] float progress_minimum() const { return progress_minimum_; } + [[nodiscard]] utility::sys_time_point expires() const { return expires_; } + [[nodiscard]] std::chrono::milliseconds expires_in() const { return expires_in_; } + [[nodiscard]] NotificationType type() const { return type_; } + [[nodiscard]] float progress_percentage() const { + float percentage = 100.0f; + auto range = progress_maximum_ - progress_minimum_; + if (range > 0) { + auto div = 100.0f / static_cast(range); + percentage = div * static_cast(progress_ - progress_minimum_); + } + + return percentage; + } + + void uuid(const Uuid &value) { uuid_ = value; } + + void text(const std::string &value) { + text_ = value; + send(); + } + + void send() { + if (host_ and destination_) + anon_mail(notification_atom_v, *this).send(destination_); + } + + void progress(const float value) { + progress_ = std::min(progress_maximum_, std::max(value, progress_minimum_)); + if (host_ and destination_) + anon_mail(notification_atom_v, uuid_, progress_).send(destination_); + } + + void progress_maximum(const float value) { + progress_maximum_ = value; + send(); + } + + void progress_minimum(const float value) { + progress_minimum_ = value; + send(); + } + + void type(const NotificationType value) { + type_ = value; + send(); + } + + void expires_in(const std::chrono::milliseconds value) { + expires_in_ = value; + send(); + } + + void remove() { + if (host_ and destination_) + anon_mail(notification_atom_v, uuid_).send(destination_); + } + + void update_expires() { expires_ = utility::sysclock::now() + expires_in_; } + + [[nodiscard]] std::string progress_text_percentage() const { + return std::string(fmt::format( + fmt::runtime(text_), std::string(fmt::format("{:>5.1f}%", progress_percentage())))); + } + + [[nodiscard]] std::string progress_text_range() const { + return std::string(fmt::format( + fmt::runtime(text_), + std::string(fmt::format( + "{}/{}", + progress_ - progress_minimum_, + progress_maximum_ - progress_minimum_)))); + } + + + private: + Uuid uuid_{Uuid::generate()}; + std::chrono::milliseconds expires_in_; + utility::sys_time_point expires_; + float progress_minimum_{0.0}; + float progress_maximum_{100.0}; + float progress_{0.0}; + std::string text_; + NotificationType type_{NT_INFO}; + caf::event_based_actor *host_{nullptr}; + caf::actor destination_; +}; + +class NotificationHandler { + public: + NotificationHandler() = default; + virtual ~NotificationHandler() = default; + + virtual caf::message_handler + message_handler(caf::event_based_actor *act, caf::actor event_group); + static caf::message_handler default_event_handler(); + + private: + [[nodiscard]] JsonStore digest() const; + bool check_expired(); + bool remove_notification(const Uuid &uuid); + bool update_progress(const Uuid &uuid, const float value); + bool add_update_notification(const Notification ¬ification); + utility::sys_time_point next_expire() const; + caf::event_based_actor *actor_{nullptr}; + caf::actor event_group_; + + std::list notifications_; + + // utility::sys_time_point next_expire_ {utility::sysclock::now()}; +}; + +void to_json(nlohmann::json &j, const Notification &n); + +} // namespace xstudio::utility \ No newline at end of file diff --git a/include/xstudio/utility/remote_session_file.hpp b/include/xstudio/utility/remote_session_file.hpp index 632b7ce85..787a3e2f2 100644 --- a/include/xstudio/utility/remote_session_file.hpp +++ b/include/xstudio/utility/remote_session_file.hpp @@ -24,11 +24,10 @@ namespace utility { class RemoteSessionFile { public: RemoteSessionFile(const std::string &file_path); - RemoteSessionFile(const std::string path, const int port, const bool sync = false); + RemoteSessionFile(const std::string path, const int port); RemoteSessionFile( const std::string path, const int port, - const bool sync, const std::string session_name, const std::string host = "localhost", const bool force_cleanup = false); @@ -37,12 +36,10 @@ namespace utility { [[nodiscard]] std::string path() const { return path_; } [[nodiscard]] std::string session_name() const { return session_name_; } [[nodiscard]] std::string filename() const { - return session_name_ + "_" + (sync_ ? "sync" : "api") + "_" + host_ + "_" + - std::to_string(port_); + return session_name_ + "_api_" + host_ + "_" + std::to_string(port_); } [[nodiscard]] fs::path filepath() const; [[nodiscard]] std::string host() const { return host_; } - [[nodiscard]] bool sync() const { return sync_; } [[nodiscard]] int port() const { return port_; } [[nodiscard]] bool remove_on_delete() const { return remove_on_delete_; } void set_remove_on_delete(const bool remove = true) { remove_on_delete_ = remove; } @@ -62,7 +59,6 @@ namespace utility { int port_ = {0}; bool remove_on_delete_ = {false}; pid_t pid_ = {0}; - bool sync_ = {false}; fs::file_time_type last_write_; }; @@ -73,10 +69,9 @@ namespace utility { void scan(); - std::string create_session_file(const int port, const bool sync = false); + std::string create_session_file(const int port); void create_session_file( const int port, - const bool sync, const std::string session_name, const std::string host = "localhost", const bool force_cleanup = false); @@ -89,7 +84,6 @@ namespace utility { [[nodiscard]] std::optional first_api() const; - [[nodiscard]] std::optional first_sync() const; [[nodiscard]] std::optional find(const std::string &session_name) const; diff --git a/include/xstudio/utility/sequence.hpp b/include/xstudio/utility/sequence.hpp index fcbc48a8b..041c51e16 100644 --- a/include/xstudio/utility/sequence.hpp +++ b/include/xstudio/utility/sequence.hpp @@ -7,8 +7,8 @@ using uid_t = DWORD; // Use DWORD type for user ID using gid_t = DWORD; // Use DWORD type for group ID #else // For Linux or non-Windows platforms -using uid_t = uid_t; -using gid_t = gid_t; +// using uid_t = uid_t; +// using gid_t = gid_t; #endif // #include @@ -82,4 +82,4 @@ namespace utility { IgnoreSequenceFunc ignore_sequence = default_ignore_sequence); } // namespace utility -} // namespace xstudio +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/utility/serialise_headers.hpp b/include/xstudio/utility/serialise_headers.hpp index 327835cf0..3e2717453 100644 --- a/include/xstudio/utility/serialise_headers.hpp +++ b/include/xstudio/utility/serialise_headers.hpp @@ -2,15 +2,14 @@ #pragma once #include "xstudio/bookmark/bookmark.hpp" -#include "xstudio/conform/conformer.hpp" #include "xstudio/colour_pipeline/colour_pipeline.hpp" -#include "xstudio/event/event.hpp" +#include "xstudio/conform/conformer.hpp" #include "xstudio/media/media.hpp" #include "xstudio/plugin_manager/plugin_manager.hpp" #include "xstudio/shotgun_client/shotgun_client.hpp" -#include "xstudio/tag/tag.hpp" #include "xstudio/thumbnail/thumbnail.hpp" #include "xstudio/timeline/item.hpp" #include "xstudio/ui/viewport/viewport.hpp" #include "xstudio/utility/caf_helpers.hpp" #include "xstudio/utility/frame_range.hpp" +#include "xstudio/utility/notification_handler.hpp" diff --git a/include/xstudio/utility/string_helpers.hpp b/include/xstudio/utility/string_helpers.hpp index ef21c8cbc..fed1569aa 100644 --- a/include/xstudio/utility/string_helpers.hpp +++ b/include/xstudio/utility/string_helpers.hpp @@ -4,7 +4,6 @@ #ifdef _WIN32 #include #endif - #include #include #include @@ -59,6 +58,23 @@ namespace utility { return str; } + static inline std::string title_case(std::string str) { + static std::regex whitespace(R"([_\s]+)"); + static std::regex specials(R"([^\sa-zA-Z0-9])"); + str = std::regex_replace(str, whitespace, " "); + str = std::regex_replace(str, specials, ""); + + char last = ' '; + for (auto it = str.begin(); it != str.end(); ++it) { + if (last == ' ') + *it = toupper(*it); + else + *it = tolower(*it); + last = *it; + } + return str; + } + static inline std::string replace_all(std::string str, const std::string &from, const std::string &to) { size_t start_pos = 0; @@ -205,12 +221,12 @@ namespace utility { return result; } - // TODO: Ahead to refactor + // This is used on WIN build only. There was a note to refactor + // here. TODO: check what it's for and why it might need refactor. inline std::string to_upper_path(const std::filesystem::path &path) { static std::locale loc; std::string result; result.reserve(path.string().size()); - for (auto elem : path.string()) result += std::toupper(elem, loc); @@ -224,6 +240,13 @@ namespace utility { return {}; } + inline std::string get_env(const std::string &key, const std::string &default_value) { + const char *val = std::getenv(key.c_str()); + if (val) + return std::string(val); + return default_value; + } + inline std::string get_hostname() { std::array buffer; if (not gethostname(buffer.data(), (int)buffer.size())) { @@ -338,4 +361,4 @@ namespace utility { } // namespace utility -} // namespace xstudio +} // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/utility/time_cache.hpp b/include/xstudio/utility/time_cache.hpp index e3c0b8a91..707e4ba44 100644 --- a/include/xstudio/utility/time_cache.hpp +++ b/include/xstudio/utility/time_cache.hpp @@ -1,24 +1,45 @@ // SPDX-License-Identifier: Apache-2.0 // NOTES -// properties of the cache -// we store mixed frames as shared_ptrs (we only give out const to clients) -// we should limit on memory not the number of frames cached ? -// we either return a frame or request a frame from the media reader. -// we don't want to release frames that have been eager loaded (we'd end up reading the twice) -// allow cached frames to be manually expired. (watchout for shared frames). -// reuse buffers if they are a close match on size. NONONONONONO they are SHARED!! -// check ref COUNT ? IS THAT SAFE ? == 1 then no one is referencing it so we can reuse it.. -// who knows how big the buffer should be ? the reader ? -// who handles eager loading ? the reader ? -// sort cache on expected presentation timestamp ? -// if the expired buffer is still in use remove ref and deduct from our budget. -// make a distinction from request and readahead hint. -// can we generalise the cache ? -// make the media cache a global actor -// but allow local cache actors. -// the same way we use the json_store/global jsonstore -// not sure the request to read a frame should go to the cache.. -// seems like the reader should be the client... + +// TimeCache class is used to manage storage of ImageBuffers, AudioBuffers and +// ColourPipelineData - the MediaCacheActor owns the Class and provides access +// to the cache. +// When the cache is full (the size of the buffers exceeds the size set on +// the cache), buffers (values) are discarded to make space for new buffers. +// We do this by checking timestamps that store when a buffer will be needed +// in the near future. If the timestamps are in the past the buffer can be +// discarded. +// The playheads will continually notify the cache which buffers that are going +// to need in the coming few seconds for playback, and the cache updates the +// timepoints accordingly. +// +// Note that it's possible for a buffer to be needed 0.1s and again 2.0s in the +// future, say. Once we have advanced > 0.1s, then the other timepoint at 2.0s +// becomes relevant in terms of retaining the buffer when we need to make more +// space. +// +// Sometimes, if we have a buffer stored that's needed in 10.0s and we need to +// store a new buffer needed in 1.0s, then we might need to delete the buffer +// stored for 10s in favour of the buffer needed more imminently. This is the +// reason for storing a set of timepoints telling us when buffers are needed +// rather than a single timepoitn. +// +// We also store Uuids against the cache entries. These uuids tell us which +// object is 'insterested' in that cache entry. For example, a SubPlayhead X154 +// might be requesting reads for a particular media item and then retrieveing the +// buffers from the cache during display. When the user switches to view another +// piece of media it means the SubPlayhead X154 will be destroyed. It tells the +// MediaReader, which then checks the cache - if the only Uuid stored with the +// buffers is X154 then the buffers are considered safe for removal as they will +// no longer be needed for display. +// +// Final note: We use std::map, not std::unsorted_map. Although advice is that +// std::map should not be used for high performance applications, we found that +// lookups and insertions are much quicker for std::map. It might be becuase +// our cache is usually in the order of a few hundred or at most a few thousand +// entries and binary lookup of std::map is superiour at these small sizes. + +// The goal of the cache is to keep values that we need and #pragma once #include @@ -29,19 +50,29 @@ #include #include #include +#include #include "xstudio/utility/chrono.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/uuid.hpp" +#include "xstudio/media/media.hpp" namespace xstudio { namespace utility { template class TimeCache { public: - using cache_type = std::map; - using uuid_type = std::map>; - using timepoint_type = std::map>; + struct CacheEntry { + CacheEntry() = default; + CacheEntry(const V _v) : value(_v) {} + V value; + std::unordered_set uuids; + std::set timepoints; + }; + typedef std::shared_ptr CacheEntryPtr; + + using cache_type = std::map; + cache_type cache_; TimeCache( const size_t max_size = std::numeric_limits::max(), @@ -113,6 +144,8 @@ namespace utility { [[nodiscard]] size_t size() const { return size_; } [[nodiscard]] size_t count() const { return count_; } + [[nodiscard]] bool empty() const { return count_ == 0; } + [[nodiscard]] size_t max_size() const { return max_size_; } [[nodiscard]] size_t max_count() const { return max_count_; } @@ -150,6 +183,11 @@ namespace utility { change_callback_(store, erase); } + /*bool noisy = {false}; + std::vector retrieve_times; + size_t avg_sum{0}; + size_t avg_ct{0};*/ + private: std::function &store, const std::vector &erase)> change_callback_; @@ -164,9 +202,6 @@ namespace utility { const utility::Uuid &uuid, const size_t size); - cache_type cache_; - uuid_type uuid_cache_; - timepoint_type timepoint_cache_; size_t max_size_; size_t max_count_; size_t size_{0}; @@ -182,11 +217,9 @@ namespace utility { template void TimeCache::add_timepoint_reference( const K &key, const time_point &time, const utility::Uuid &uuid) { - uuid_cache_[key].insert(uuid); - if (timepoint_cache_.count(key)) - timepoint_cache_[key].insert(time); - else - timepoint_cache_[key] = std::set{time}; + auto it = cache_.find(key); + it->second->uuids.insert(uuid); + it->second->timepoints.insert(time); } template @@ -198,10 +231,11 @@ namespace utility { const size_t size) { count_++; size_ += size; - cache_[key] = value; + + cache_[key] = std::make_shared(value); + call_change_callback({key}, {}); - uuid_cache_[key] = std::set{}; add_timepoint_reference(key, time, uuid); } @@ -346,8 +380,6 @@ namespace utility { call_change_callback({}, keys()); cache_.clear(); - uuid_cache_.clear(); - timepoint_cache_.clear(); count_ = 0; size_ = 0; } @@ -364,6 +396,7 @@ namespace utility { template std::vector TimeCache::erase(const std::vector &keys) { std::vector result; + result.reserve(keys.size()); for (const auto &i : keys) { if (erase(i)) result.push_back(i); @@ -375,10 +408,10 @@ namespace utility { auto it = std::begin(cache_); bool result = false; while (it != std::end(cache_)) { - if (uuid_cache_[it->first].count(uuid)) { - uuid_cache_[it->first].erase(uuid); + if (it->second->uuids.count(uuid)) { + it->second->uuids.erase(uuid); result = true; - if (uuid_cache_[it->first].empty()) { + if (it->second->uuids.empty()) { it = erase(it); continue; } @@ -393,10 +426,8 @@ namespace utility { TimeCache::erase(const typename cache_type::iterator &it) { typename cache_type::iterator nit; if (it->second) - size_ -= it->second->size(); + size_ -= it->second->value->size(); count_--; - uuid_cache_.erase(uuid_cache_.find(it->first)); - timepoint_cache_.erase(timepoint_cache_.find(it->first)); call_change_callback({}, {it->first}); nit = cache_.erase(it); return nit; @@ -404,20 +435,27 @@ namespace utility { template bool TimeCache::erase(const K &key, const utility::Uuid &uuid) { - const auto it = uuid_cache_.find(key); + const auto it = cache_.find(key); bool result = false; - if (it != std::end(uuid_cache_)) { + if (it != std::end(cache_)) { // remove from set.. - it->second.erase(uuid); - result = true; - if (it->second.empty()) - erase(cache_.find(key)); + if (it->second->uuids.count(uuid)) { + it->second->uuids.erase(uuid); + result = true; + if (it->second->uuids.empty()) { + erase(it); + } + } } return result; } - // we must always release.. - // unless the size is set.. + // The goal here is to find the cache entry that has the oldest timepoints + // in the whole set, and remove it. Remember, timepoint is the estimated + // time when the given data entry will be needed. If the timepoints tell us + // it was needed in the past, then we can safely remove. If it's in the + // future, and inside 'newtp' then we can't get rid of it as we will need + // it soon so we have a release failure. // return empty buffer on fail / empty cache. template V TimeCache::release( @@ -428,41 +466,40 @@ namespace utility { return ptr; // release in special time order.. - long int max_offset(0); - K key; + long long max_offset(0); + + typename cache_type::iterator it = cache_.end(); + typename cache_type::iterator i = cache_.begin(); // set to our proposed time, if we're bigger than every chache entry we fail. if (not force_eviction) max_offset = std::abs( std::chrono::duration_cast(ntp - newtp).count()); - for (const auto &i : timepoint_cache_) { - long int min_offset(std::numeric_limits::max()); - - // calculate offset in time from now, - for (const auto &tp : i.second) { - long int offset = std::abs( - std::chrono::duration_cast(ntp - tp).count()); - if (min_offset > offset) - min_offset = offset; - } - - if (min_offset >= max_offset) { - max_offset = min_offset; - key = i.first; + // loop over every cache entry + while (i != cache_.end()) { + + const std::set &timepoints = i->second->timepoints; + + // timepoints is an ordered set, so the last entry is the timepoint + // furthest forward in time. How does this timepoint (which represents + // the furthest point in the future that we would need this Value) + // compare to 'max_offset' ? + if (timepoints.size()) { + long long offset = + std::abs(std::chrono::duration_cast( + ntp - *(timepoints.rbegin())) + .count()); + if (offset >= max_offset) { + max_offset = offset; + it = i; + } } + i++; } - // valid key ? - auto it = cache_.find(key); if (it != cache_.end()) { - ptr = it->second; - const auto &old_tps = timepoint_cache_[key]; - auto ms = std::chrono::duration_cast( - utility::clock::now() - *(old_tps.begin())) - .count(); + ptr = it->second->value; erase(it); - // spdlog::debug("Release buffer {} which has timestamp {} milliseconds from - // now", key, ms); } return ptr; } @@ -475,35 +512,37 @@ namespace utility { if (cache_.empty()) return ptr; - K key; - - long int maxx = 0; - std::vector bb; - for (const auto &i : timepoint_cache_) { - - // calculate offset in time from now, - long int ctt = std::numeric_limits::max(); - std::vector cc; - for (const auto &tp : i.second) { - ctt = std::min( - ctt, - static_cast(std::chrono::duration_cast( - out_of_date_time - tp) - .count())); - cc.push_back(tp); - } - if (ctt > maxx) { - maxx = ctt; - key = i.first; - bb = cc; + typename cache_type::iterator it = cache_.end(); + typename cache_type::iterator i = cache_.begin(); + + long long maxx = 0; + + // loop over every cache entry + while (i != cache_.end()) { + + const std::set &timepoints = i->second->timepoints; + + // timepoints is an ordered set, so the last entry is the timepoint + // furthest forward in time. How does this timepoint (which represents + // the furthest point in the future that we would need this Value) + // compare to 'max_offset' ? + if (timepoints.size()) { + long long offset = std::chrono::duration_cast( + out_of_date_time - *(timepoints.rbegin())) + .count(); + if (offset >= maxx) { + maxx = offset; + it = i; + } } + ++i; } // valid key ? - auto it = cache_.find(key); if (it != cache_.end()) { - ptr = it->second; + ptr = it->second->value; erase(it); } + return ptr; } @@ -527,19 +566,47 @@ namespace utility { template V TimeCache::retrieve( const K &key, const time_point &time, const utility::Uuid &uuid) { + auto tp = utility::clock::now(); auto it = cache_.find(key); if (it == std::end(cache_)) return V(); // found entry, add timestamp (bit like lru ?) - timepoint_cache_[key].insert(time); + it->second->timepoints.insert(time); if (not uuid.is_null()) - uuid_cache_[key].insert(uuid); + it->second->uuids.insert(uuid); clean_timepoints(key); - return it->second; + + /*if (noisy){ + retrieve_times.push_back(std::chrono::duration_cast(utility::clock::now()-tp).count()); + if (retrieve_times.size() == 1024) { + int _max = 0; + int _min = 100000000; + int sum = 0; + for (auto &t: retrieve_times) { + sum += t; + _max=std::max(t, _max); + _min=std::min(t, _min); + } + std::sort(retrieve_times.begin(), retrieve_times.end()); + int last_ten = 0; + for (int i = 924; i < 1024; ++i) { + last_ten += retrieve_times[i]; + } + avg_sum += sum; + avg_ct += 1024; + std::cerr << this << " Cache Size: " << cache_.size() << " Min retrieve: " << + _min << ", Max retrieve: " << _max << ", Average: " << sum/1024 << " (mean), " << + retrieve_times[512] << " (median), " << " worst 100: " << last_ten/100 << " Running + average " << avg_sum/avg_ct << "\n"; retrieve_times.clear(); + } + }*/ + + return it->second->value; } template std::vector TimeCache::keys() const { std::vector _keys; + _keys.reserve(cache_.size()); for (const auto &i : cache_) _keys.push_back(i.first); return _keys; @@ -571,7 +638,7 @@ namespace utility { for (const auto &key : keys_and_timepoints) { auto it = cache_.find(key.first); if (it != std::end(cache_)) { - timepoint_cache_[key.first] = std::set({key.second + delta}); + it->second->timepoints = std::set({key.second + delta}); } } } @@ -581,14 +648,15 @@ namespace utility { // set the 'required by' time point on all cache entries that mathch uuid // backwards by one hour so that it can be dropped from the cache in // favour of new incoming data, - auto it = std::begin(cache_); - while (it != std::end(cache_)) { - if (uuid_cache_[it->first].count(uuid) && timepoint_cache_.count(it->first)) { - std::set ntp; - for (const auto &tp : timepoint_cache_[it->first]) { - ntp.insert(tp - std::chrono::hours(1)); + typename cache_type::iterator it = cache_.begin(); + while (it != cache_.end()) { + if (it->second->uuids.count(uuid)) { + std::set new_timepoints; + std::set &timepoints = it->second->timepoints; + for (const auto &tp : timepoints) { + new_timepoints.emplace(tp - std::chrono::hours(1)); } - timepoint_cache_[it->first] = ntp; + timepoints = new_timepoints; } it++; } @@ -596,20 +664,30 @@ namespace utility { // if they never expire the timepoint list can grow indefinitely.. // remove timepoints older than now, but keep the most recent of these. template void TimeCache::clean_timepoints(const K &key) { + + auto it = cache_.find(key); + if (it == cache_.end()) + return; + + std::set &timepoints = it->second->timepoints; + + if (timepoints.empty()) + return; + const time_point now = utility::clock::now(); - time_point newest; - - for (auto i = std::begin(timepoint_cache_[key]); - i != std::end(timepoint_cache_[key]);) { - if (*i < now) { - if (*i > newest) - newest = *i; - i = timepoint_cache_[key].erase(i); - } else - ++i; + + // get the most recent timepoint + time_point newest = *(timepoints.rbegin()); + + // erasing all entries older than 'now' + while ((*timepoints.begin()) < now) { + timepoints.erase(timepoints.begin()); + if (timepoints.empty()) + break; } - if (newest != time_point()) - timepoint_cache_[key].insert(newest); + + if (timepoints.empty()) + timepoints.emplace(newest); } } // namespace utility } // namespace xstudio diff --git a/include/xstudio/utility/timecode.hpp b/include/xstudio/utility/timecode.hpp index 12bf59c97..2aa5f647f 100644 --- a/include/xstudio/utility/timecode.hpp +++ b/include/xstudio/utility/timecode.hpp @@ -43,6 +43,7 @@ namespace utility { void frames(const unsigned int f) { frames_ = f; } void framerate(const double fr) { frame_rate_ = fr; } void dropframe(const bool df) { drop_frame_ = df; } + void total_frames(const unsigned int f); [[nodiscard]] std::string to_string() const; operator int() const { return total_frames(); } diff --git a/include/xstudio/utility/tree.hpp b/include/xstudio/utility/tree.hpp index 5cd4771ed..97b2c6061 100644 --- a/include/xstudio/utility/tree.hpp +++ b/include/xstudio/utility/tree.hpp @@ -71,7 +71,6 @@ namespace utility { return it; } - void splice(typename TreeList::const_iterator pos, TreeList &other) { TreeList::splice(pos, other); reparent(); @@ -135,6 +134,7 @@ namespace utility { return result; } + template void do_sort(C compare) { TreeList::sort(compare); } protected: void reparent() { @@ -219,8 +219,8 @@ namespace utility { return result; } - inline nlohmann::json - tree_to_json(const JsonTree &node, const std::string &childname, const int depth = -1) { + inline nlohmann::json tree_to_json( + const JsonTree &node, const std::string &childname = "children", const int depth = -1) { // unroll.. auto jsn = node.data(); diff --git a/include/xstudio/utility/types.hpp b/include/xstudio/utility/types.hpp index 3a3ede48f..11672bfdf 100644 --- a/include/xstudio/utility/types.hpp +++ b/include/xstudio/utility/types.hpp @@ -1,6 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#include +#include + namespace xstudio { namespace utility { @@ -22,7 +25,26 @@ namespace utility { float r = {0.0f}; // NOLINT float g = {0.0f}; // NOLINT float b = {0.0f}; // NOLINT + + float red() const { return r; } + float green() const { return g; } + float blue() const { return b; } + + void setRed(const float _r) { r = _r; } + void setGreen(const float _g) { g = _g; } + void setBlue(const float _b) { b = _b; } + + friend std::string to_string(const ColourTriplet &value); + + template friend bool inspect(Inspector &f, ColourTriplet &x) { + return f.object(x).fields(f.field("r", x.r), f.field("g", x.g), f.field("b", x.b)); + } }; + inline std::string to_string(const ColourTriplet &c) { + return fmt::format("ColourTriplet({}, {}, {})", c.r, c.g, c.b); + } + + } // namespace utility } // namespace xstudio \ No newline at end of file diff --git a/include/xstudio/utility/uuid.hpp b/include/xstudio/utility/uuid.hpp index 2d25b7e14..cb20f902b 100644 --- a/include/xstudio/utility/uuid.hpp +++ b/include/xstudio/utility/uuid.hpp @@ -13,6 +13,7 @@ #include #include +#include #include #include "stduuid/uuid.h" @@ -154,12 +155,17 @@ namespace utility { UuidActor(const utility::Uuid uuid, const caf::actor actor) : uuid_(std::move(uuid)), actor_(std::move(actor)) {} + UuidActor(const utility::Uuid uuid, const caf::actor_addr &actor) + : uuid_(std::move(uuid)), actor_(std::move(caf::actor_cast(actor))) {} + UuidActor &operator=(const UuidActor &o) = default; - bool operator==(const UuidActor &o) const { - return (uuid_ == o.uuid_ && actor_ == o.actor_); + friend bool operator==(const UuidActor &a, const UuidActor &b) { + return (a.uuid_ == b.uuid_ && a.actor_ == b.actor_); }; + friend bool operator!=(const UuidActor &a, const UuidActor &b) { return !(a == b); }; + operator caf::actor &() { return actor_; } operator const caf::actor &() const { return actor_; } operator utility::Uuid &() { return uuid_; } @@ -171,6 +177,10 @@ namespace utility { caf::actor &actor() { return actor_; } [[nodiscard]] const caf::actor &actor() const { return actor_; } + [[nodiscard]] caf::actor_addr actor_addr() const { + return caf::actor_cast(actor_); + } + template friend bool inspect(Inspector &f, UuidActor &x) { return f.object(x).fields(f.field("uuid", x.uuid_), f.field("actor", x.actor_)); } diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 8227751e7..d5ae02eba 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -7,56 +7,61 @@ set(VERSION_PY_IN "${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/version.py.in") set(VERSION_PY_OUT "${CMAKE_CURRENT_BINARY_DIR}/src/xstudio/version.py") set(VERSION_PY "${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/version.py") -file(GLOB DEPS - ${CMAKE_CURRENT_SOURCE_DIR}/*.in - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/api/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/api/auxiliary/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/api/auxiliary/*/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/api/intrinsic/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/api/session/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/api/*/*/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/api/*/*/*/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/cli/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/demo/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/common_api/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/connection/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/gui/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/plugin/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/sync_api/*.py - ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio/core/*.py -) - -set(OUTPUT "${CMAKE_BINARY_DIR}/bin/python") +file(GLOB_RECURSE DEPS ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio *.py) set(PYTHONVP "python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}") -configure_file(${SETUP_PY_IN} ${SETUP_PY}) -configure_file(${VERSION_PY_IN} ${VERSION_PY_OUT} @ONLY) -configure_file(${VERSION_PY_IN} ${VERSION_PY} @ONLY) +if(APPLE) -add_custom_command(OUTPUT ${OUTPUT}/lib/${PYTHONVP}/site-packages/xstudio/api - COMMAND ${Python_EXECUTABLE} - ARGS setup.py install --old-and-unmanageable --prefix=${OUTPUT} - DEPENDS ${DEPS}) + set(VCPKG_LOCATION "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}") -add_custom_target(python_module ALL DEPENDS __pybind_xstudio ${OUTPUT}/lib/${PYTHONVP}/site-packages/xstudio/api) + configure_file(${SETUP_PY_IN} ${SETUP_PY}) + configure_file(${VERSION_PY_IN} ${VERSION_PY_OUT} @ONLY) + configure_file(${VERSION_PY_IN} ${VERSION_PY} @ONLY) + add_custom_command(OUTPUT ${VCPKG_LOCATION}/xstudio/api + COMMAND ${Python_EXECUTABLE} + ARGS setup.py install --old-and-unmanageable --prefix=${VCPKG_LOCATION} + DEPENDS ${DEPS}) -if(INSTALL_PYTHON_MODULE) - install(DIRECTORY ${OUTPUT}/lib/${PYTHONVP}/site-packages/xstudio - DESTINATION lib/python/) + add_custom_target(python_module ALL DEPENDS __pybind_xstudio ${VCPKG_LOCATION}/xstudio/api) - install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio-control - DESTINATION bin) - install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio-inject - DESTINATION bin) - install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudiopy - DESTINATION bin) + # Here we copy the entire python installation into Frameworks + install(DIRECTORY ${VCPKG_LOCATION}/lib/${PYTHONVP} DESTINATION ${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Frameworks/lib/) + +else() + + set(OUTPUT "${CMAKE_BINARY_DIR}/bin/python") + + configure_file(${SETUP_PY_IN} ${SETUP_PY}) + configure_file(${VERSION_PY_IN} ${VERSION_PY_OUT} @ONLY) + configure_file(${VERSION_PY_IN} ${VERSION_PY} @ONLY) + + add_custom_command(OUTPUT ${OUTPUT}/lib/${PYTHONVP}/site-packages/xstudio/api + COMMAND ${Python_EXECUTABLE} + ARGS setup.py install --old-and-unmanageable --prefix=${OUTPUT} + DEPENDS ${DEPS}) + + add_custom_target(python_module ALL DEPENDS __pybind_xstudio ${OUTPUT}/lib/${PYTHONVP}/site-packages/xstudio/api) + +endif() + + +if(NOT APPLE) + if(INSTALL_PYTHON_MODULE) + install(DIRECTORY ${OUTPUT}/lib/${PYTHONVP}/site-packages/xstudio + DESTINATION lib/python/) + install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio-control + DESTINATION bin) + install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio-inject + DESTINATION bin) + install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudiopy + DESTINATION bin) + endif() endif() if(WIN32) -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio DESTINATION ${CMAKE_INSTALL_PREFIX}/python/ FILES_MATCHING PATTERN "*.py") +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio DESTINATION python FILES_MATCHING PATTERN "*.py") endif() # install(CODE "execute_process(COMMAND ${PYTHON} ${SETUP_PY} install)") diff --git a/python/src/xstudio-inject b/python/src/xstudio-inject old mode 100755 new mode 100644 diff --git a/python/src/xstudio/api/__init__.py b/python/src/xstudio/api/__init__.py index aa4ff3c62..b5bd9f43d 100644 --- a/python/src/xstudio/api/__init__.py +++ b/python/src/xstudio/api/__init__.py @@ -1,6 +1,8 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.core import get_studio_atom, get_global_image_cache_atom, get_global_audio_cache_atom, get_global_thumbnail_atom -from xstudio.core import get_global_store_atom, get_plugin_manager_atom, get_scanner_atom, exit_atom +from xstudio.core import get_global_store_atom, get_plugin_manager_atom, get_scanner_atom, exit_atom, get_python_atom +from xstudio.core import get_actor_from_registry_atom, log_atom +from xstudio.core import LogLevel from xstudio.common_api import CommonAPI from xstudio.api.studio import Studio from xstudio.api.intrinsic import GlobalStore @@ -9,6 +11,7 @@ from xstudio.api.intrinsic import PluginManager from xstudio.api.intrinsic import Scanner from xstudio.api.auxiliary.helpers import Filesize +from xstudio.api.auxiliary import ActorConnection class API(CommonAPI): """API connection. @@ -95,6 +98,69 @@ def session(self): """ return self.app.session + def get_actor_from_registry(self, name): + """Get actor fro registry name + + Args: + name(str): Registry name + Returns: + remote actor(ActorConnection): Requested actor.""" + return ActorConnection( + self.connection, + self.connection.request_receive(self.connection.remote(), get_actor_from_registry_atom(), name)[0] + ) + + + def log(self, level, messge): + """Log msg at level + Args: + level(str): Log message level + messge(str): Log message + """ + self.connection.request_receive(self.connection.remote(), log_atom(), level, messge) + + def log_trace(self, messge): + """Log msg at level + Args: + messge(str): Log message + """ + self.log(LogLevel.SPDLOG_LEVEL_TRACE, messge) + + def log_debug(self, messge): + """Log msg at level + Args: + messge(str): Log message + """ + self.log(LogLevel.SPDLOG_LEVEL_DEBUG, messge) + + def log_info(self, messge): + """Log msg at level + Args: + messge(str): Log message + """ + self.log(LogLevel.SPDLOG_LEVEL_INFO, messge) + + def log_warn(self, messge): + """Log msg at level + Args: + messge(str): Log message + """ + self.log(LogLevel.SPDLOG_LEVEL_WARN, messge) + + def log_err(self, messge): + """Log msg at level + Args: + messge(str): Log message + """ + self.log(LogLevel.SPDLOG_LEVEL_ERROR, messge) + + def log_critical(self, messge): + """Log msg at level + Args: + messge(str): Log message + """ + self.log(LogLevel.SPDLOG_LEVEL_CRITICAL, messge) + @property def global_store(self): """Global JSON store object. @@ -222,7 +288,6 @@ def status(self): else: # connection stats stat["connection"]["connected"] = True - stat["connection"]["type"] = self.connection.api_type stat["connection"]["version"] = self.connection.app_version stat["connection"]["application"] = self.connection.app_type stat["connection"]["host"] = self.connection.host diff --git a/python/src/xstudio/api/app.py b/python/src/xstudio/api/app.py index 038216dcc..0b7fcbfb7 100644 --- a/python/src/xstudio/api/app.py +++ b/python/src/xstudio/api/app.py @@ -2,34 +2,58 @@ from xstudio.core import session_atom, join_broadcast_atom from xstudio.core import colour_pipeline_atom, get_actor_from_registry_atom from xstudio.core import viewport_playhead_atom, quickview_media_atom -from xstudio.core import UuidActorVec, UuidActor +from xstudio.core import UuidActorVec, UuidActor, viewport_atom +from xstudio.core import get_global_playhead_events_atom from xstudio.api.session import Session, Container +from xstudio.api.session.playhead import Playhead from xstudio.api.module import ModuleBase from xstudio.api.auxiliary import ActorConnection class Viewport(ModuleBase): """Viewport object, represents a viewport in the UI or offscreen.""" - def __init__(self, connection): + def __init__(self, connection, viewport_name): """Create Viewport object. Args: connection(Connection): Connection object - remote(actor): Remote actor object + viewport_name(str): Name of viewport Kwargs: uuid(Uuid): Uuid of remote actor. """ + gphev = connection.request_receive( + connection.remote(), + get_global_playhead_events_atom())[0] + + remote = connection.request_receive( + gphev, + viewport_atom(), + viewport_name + )[0] + ModuleBase.__init__( self, connection, - connection.request_receive( - connection.remote(), - get_actor_from_registry_atom(), - "MAIN_VIEWPORT" - )[0] + remote ) + @property + def playhead(self): + """Access the Playhead object supplying images to the viewport + """ + return Playhead(self.connection, self.connection.request_receive(self.remote, viewport_playhead_atom())[0]) + + @playhead.setter + def set_playhead(self, playhead): + """Set the playhead that is delivering frames to the viewport, i.e. + the active playhead. + + Args: + playhead(Playhead): The playhead.""" + + self.connection.request_receive(self.remote, viewport_playhead_atom(), playhead.remote) + def quickview(self, media_items, compare_mode="Off", position=(100,100), size=(1280,720)): """Connect this playhead to the viewport. @@ -85,13 +109,41 @@ def colour_pipeline(self): colour_pipeline(ModuleBase): Colour Pipeline object.""" return ModuleBase(self.connection, self.connection.request_receive(self.connection.remote(), colour_pipeline_atom())[0]) - @property - def viewport(self): - """The main Viewport module. + def viewport(self, name): + """Access a named Viewport. + Args: + name(str): The viewport name Returns: viewport(ModuleBase): Viewport module.""" - return Viewport(self.connection) + return Viewport(self.connection, name) + + def set_viewport_playhead(self, viewport_name, playhead): + """Set's the named viewport to be driven by the given playhead + Args: + name(str): The viewport name + playhead(Playhead): The playhead to drive the viewport""" + + viewport_remote = self.connection.request_receive( + self.global_playhead_events.remote, + viewport_atom(), + viewport_name + )[0] + + self.connection.send(viewport_remote, viewport_playhead_atom(), playhead.remote) + + def set_global_playhead(self, playhead): + """Set's the global playhead to the given playhead. This will result + in viewports showing frames coming from the given playhead + Args: + playhead(Playhead): The playhead to drive all viewports auto + connecting to the global playhead""" + + self.connection.send( + self.global_playhead_events.remote, + viewport_playhead_atom(), + playhead.remote + ) @property def global_playhead_events(self): @@ -106,12 +158,3 @@ def global_playhead_events(self): self.connection, self.connection.request_receive(self.connection.remote(), get_actor_from_registry_atom(), "GLOBALPLAYHEADEVENTS")[0] ) - - def set_viewport_playhead(self, playhead): - """Set the playhead that is delivering frames to the viewport, i.e. - the active playhead. - - Args: - playhead(Playhead): The playhead.""" - - self.connection.request_receive(self.viewport.remote, viewport_playhead_atom(), playhead.remote) \ No newline at end of file diff --git a/python/src/xstudio/api/auxiliary/__init__.py b/python/src/xstudio/api/auxiliary/__init__.py index e47f878ab..940e052b5 100644 --- a/python/src/xstudio/api/auxiliary/__init__.py +++ b/python/src/xstudio/api/auxiliary/__init__.py @@ -1,2 +1,3 @@ # SPDX-License-Identifier: Apache-2.0 -from xstudio.api.auxiliary.helpers import ActorConnection \ No newline at end of file +from xstudio.api.auxiliary.helpers import ActorConnection +from xstudio.api.auxiliary.notification import Notification, NotificationHandler \ No newline at end of file diff --git a/python/src/xstudio/api/auxiliary/helpers.py b/python/src/xstudio/api/auxiliary/helpers.py index c3975992d..23a8f94ee 100644 --- a/python/src/xstudio/api/auxiliary/helpers.py +++ b/python/src/xstudio/api/auxiliary/helpers.py @@ -33,6 +33,25 @@ def remove_handler(self, handler_id): """ return self.connection.remove_handler(handler_id) + def add_event_callback(self, callback): + """Add event message callback + Args: + callback(fun(sender, req_id, event)): Callback function. + + Returns: + handler_id(str): Key for removing handler. + """ + return self.connection.add_event_callback(self.remote, callback) + + def remove_event_callback(self, handler_id): + """Add event handler + Args: + handler_id(str): Handler id. + + Returns: + success(bool): Success. + """ + return self.connection.remove_event_callback(handler_id) def get_name(connection, remote): """Get actor name diff --git a/python/src/xstudio/api/auxiliary/json_store.py b/python/src/xstudio/api/auxiliary/json_store.py index 1e5e1c61b..637fd7309 100644 --- a/python/src/xstudio/api/auxiliary/json_store.py +++ b/python/src/xstudio/api/auxiliary/json_store.py @@ -1,20 +1,18 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.core import get_json_atom, set_json_atom, JsonStore from xstudio.api.session import Container +import json -class JsonStorePy(Container): - """Access JSON stores""" - def __init__(self, connection, remote, uuid=None): - """Create JsonStorePy object. +class JsonStoreHandler(): + def __init__(self, actorconnection): + """Create JsonStoreHandler object. Args: - connection(Connection): Connection object - remote(actor): Remote actor object + actorconnection(ActorConnection): Connection object - Kwargs: - uuid(Uuid): Uuid of remote actor. """ - Container.__init__(self, connection, remote, uuid) + + self._actor_connection = actorconnection def get(self, path=None): """Get JSON. @@ -27,9 +25,9 @@ def get(self, path=None): """ if path is None: - return self.connection.request_receive(self.remote, get_json_atom())[0] + return self._actor_connection.connection.request_receive(self._actor_connection.remote, get_json_atom())[0] - return self.connection.request_receive(self.remote, get_json_atom(), path)[0] + return self._actor_connection.connection.request_receive(self._actor_connection.remote, get_json_atom(), path)[0] def set(self, obj, path=None): """Set JSON. @@ -44,6 +42,55 @@ def set(self, obj, path=None): success(bool): Did it work. """ if path is None: - return self.connection.request_receive(self.remote, set_json_atom(), JsonStore(obj))[0] + return self._actor_connection.connection.request_receive(self._actor_connection.remote, set_json_atom(), JsonStore(obj))[0] + + return self._actor_connection.connection.request_receive(self._actor_connection.remote, set_json_atom(), JsonStore(obj), path)[0] + + + @property + def metadata(self): + """Get media metadata. + + Returns: + metadata(json): Media metadata. + """ + return json.loads(self._actor_connection.connection.request_receive(self._actor_connection.remote, get_json_atom())[0].dump()) + + + @metadata.setter + def metadata(self, new_metadata): + """Set media reference rate. + + Args: + new_metadata(json): Json dict to set as media source metadata + + Returns: + bool: success + + """ + return self._actor_connection.connection.request_receive(self._actor_connection.remote, set_json_atom(), JsonStore(new_metadata)) + + def get_metadata(self, path): + """Get metdata at JSON path + + Args: + path(str): JSON Pointer + + Returns: + metadata(json) Json at pointer location + """ + + return json.loads(self._actor_connection.connection.request_receive(self._actor_connection.remote, get_json_atom(), path)[0].dump()) + + def set_metadata(self, data, path): + """Get metdata at JSON path + + Args: + data(json): JSON Data + path(str): JSON Pointer + + Returns: + bool: success + """ - return self.connection.request_receive(self.remote, set_json_atom(), JsonStore(obj), path)[0] + return self._actor_connection.connection.request_receive(self._actor_connection.remote, set_json_atom(), JsonStore(data), path)[0] diff --git a/python/src/xstudio/api/auxiliary/notification.py b/python/src/xstudio/api/auxiliary/notification.py new file mode 100644 index 000000000..79639a008 --- /dev/null +++ b/python/src/xstudio/api/auxiliary/notification.py @@ -0,0 +1,208 @@ +# SPDX-License-Identifier: Apache-2.0 +from xstudio.core import notification_atom, Notification +import json + +class NotificationPy(): + """Wrapper for Notifaction""" + def __init__(self, actorconnection, notification): + """Create NotificationHandler object. + + Args: + actorconnection(ActorConnection): Connection object + + """ + + self._actor_connection = actorconnection + self._notification = notification + + @property + def type(self): + return self._notification.type() + + @type.setter + def type(self, value): + self._notification.set_type(value) + + @property + def text(self): + return self._notification.text() + + @text.setter + def text(self, value): + self._notification.set_text(value) + + @property + def uuid(self): + return self._notification.uuid() + + @uuid.setter + def uuid(self, value): + self._notification.set_uuid(value) + + @property + def progress(self): + return self._notification.progress() + + @progress.setter + def progress(self, value): + self._notification.set_progress(value) + + @property + def progress_maximum(self): + return self._notification.progress_maximum() + + @progress_maximum.setter + def progress_maximum(self, value): + self._notification.set_progress_maximum(value) + + @property + def progress_minimum(self): + return self._notification.progress_minimum() + + @progress_minimum.setter + def progress_minimum(self, value): + self._notification.set_progress_minimum(value) + + @property + def progress_percentage(self): + return self._notification.progress_percentage() + + @property + def progress_text_percentage(self): + return self._notification.progress_text_percentage() + + @property + def progress_text_range(self): + return self._notification.progress_text_range() + + def set_expires_in(self, seconds): + self._notification.set_expires_in(seconds) + + def remove(self): + """Remove Notification""" + return self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), self.uuid)[0] + + def push_update(self): + """Push changes""" + return self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), self._notification)[0] + + def push_progress(self, progress): + """Push changes""" + return self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), self.uuid, progress)[0] + + +class NotificationHandler(): + """Access and create notification events""" + def __init__(self, actorconnection): + """Create NotificationHandler object. + + Args: + actorconnection(ActorConnection): Connection object + + """ + + self._actor_connection = actorconnection + + @property + def notification_digest(self): + return json.loads(self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom())[0].dump()) + + @property + def notifications(self): + result = [] + # turn into array of NotificationPy + + tmp = self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), notification_atom())[0] + for i in tmp: + result.append(NotificationPy(self._actor_connection, i)) + + return result + + def createInfoNotification(self, text, expiresSeconds=10): + """Create Info Notification. + + Args: + text(str): Text of notification + expiresSeconds(int): Expiration in seconds. + + Returns: + notification(NotificationPy): Notification created + """ + + result = Notification.InfoNotification(text, expiresSeconds) + self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), result) + return NotificationPy(self._actor_connection, result) + + def createWarnNotification(self, text, expiresSeconds=10): + """Create Info Notification. + + Args: + text(str): Text of notification + expiresSeconds(int): Expiration in seconds. + + Returns: + notification(NotificationPy): Notification created + """ + + result = Notification.WarnNotification(text, expiresSeconds) + self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), result) + return NotificationPy(self._actor_connection, result) + + def createProcessingNotification(self, text): + """Create Info Notification. + + Args: + text(str): Text of notification + expiresSeconds(int): Expiration in seconds. + + Returns: + notification(NotificationPy): Notification created + """ + + result = Notification.ProcessingNotification(text) + self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), result) + return NotificationPy(self._actor_connection, result) + + def createProgressPercentageNotification(self, text, progress=0.0, expiresSeconds=600): + """Create Info Notification. + + Args: + text(str): Text of notification + expiresSeconds(int): Expiration in seconds. + + Returns: + notification(NotificationPy): Notification created + """ + + result = Notification.ProgressPercentageNotification(text, progress, expiresSeconds) + self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), result) + return NotificationPy(self._actor_connection, result) + + def createProgressRangeNotification(self, text, progress=0.0, progress_min=0.0, progress_max=0.0, expiresSeconds=600): + """Create Info Notification. + + Args: + text(str): Text of notification + expiresSeconds(int): Expiration in seconds. + + Returns: + notification(NotificationPy): Notification created + """ + + result = Notification.ProgressRangeNotification(text, progress, progress_min, progress_max, expiresSeconds) + self._actor_connection.connection.request_receive(self._actor_connection.remote, notification_atom(), result) + return NotificationPy(self._actor_connection, result) + + + + + + + + + + + + + + diff --git a/python/src/xstudio/api/auxiliary/otio/__init__.py b/python/src/xstudio/api/auxiliary/otio/__init__.py index b2fabe169..43e6fbee2 100644 --- a/python/src/xstudio/api/auxiliary/otio/__init__.py +++ b/python/src/xstudio/api/auxiliary/otio/__init__.py @@ -1,3 +1,4 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.api.auxiliary.otio.reader import import_timeline_from_file -from xstudio.api.auxiliary.otio.writer import export_timeline_to_file \ No newline at end of file +from xstudio.api.auxiliary.otio.writer import export_timeline_to_file +from xstudio.api.auxiliary.otio.writer import timeline_to_otio_string diff --git a/python/src/xstudio/api/auxiliary/otio/reader.py b/python/src/xstudio/api/auxiliary/otio/reader.py index 2d77ab4dc..eab65f4e1 100644 --- a/python/src/xstudio/api/auxiliary/otio/reader.py +++ b/python/src/xstudio/api/auxiliary/otio/reader.py @@ -13,7 +13,7 @@ def __process_obj(playlist, timeline, parent, obj, media_lookup): if i.source_range is not None: trk.active_range = FrameRange(FrameRateDuration(int(i.source_range.duration.value), FrameRate(i.source_range.duration.rate))) parent.insert_child(trk) - __process_obj(playlist, timeline, trk, i.each_child(), media_lookup) + __process_obj(playlist, timeline, trk, i.find_children(shallow_search=True), media_lookup) elif i.kind == Track.Kind.Audio: # print("Audio Track") @@ -21,7 +21,7 @@ def __process_obj(playlist, timeline, parent, obj, media_lookup): if i.source_range is not None: trk.active_range = FrameRange(FrameRateDuration(int(i.source_range.duration.value), FrameRate(i.source_range.duration.rate))) parent.insert_child(trk) - __process_obj(playlist, timeline, trk, i.each_child(), media_lookup) + __process_obj(playlist, timeline, trk, i.find_children(shallow_search=True), media_lookup) elif isinstance(i, Gap): # print("Gap") @@ -65,7 +65,7 @@ def __process_obj(playlist, timeline, parent, obj, media_lookup): if i.source_range is not None: stk.active_range = FrameRange(FrameRateDuration(int(i.source_range.duration.value), FrameRate(i.source_range.duration.rate))) parent.insert_child(stk) - __process_obj(playlist, timeline, trk, i.each_child(), media_lookup) + __process_obj(playlist, timeline, trk, i.find_children(shallow_search=True), media_lookup) def import_timeline(playlist, otio, name=None, before=Uuid()): """Create Timeline from otio. diff --git a/python/src/xstudio/api/auxiliary/otio/writer.py b/python/src/xstudio/api/auxiliary/otio/writer.py index 4403e3b94..b03571dae 100644 --- a/python/src/xstudio/api/auxiliary/otio/writer.py +++ b/python/src/xstudio/api/auxiliary/otio/writer.py @@ -51,6 +51,36 @@ def export_timeline_to_file(timeline, path, adapter_name=None): write_otio(otio, path, adapter_name) +def timeline_to_otio_string(timeline, adapter_name=None): + """Create otio from timeline and reutrn as a string + + Args: + timeline(Timeline): Timeline object + + Kwargs: + adapter_name(str): Override adaptor. + + Returns: + otio(str): Otio serialised timeline as string + """ + + if not isinstance(timeline, Timeline): + raise RuntimeError("Not a timeline") + + # build otio + otio = oTimeline() + + # our timeline has one stack, so does otio. + # we just need to start porcessing it. + for i in reversed(timeline.video_tracks): + __process_obj(i, otio.tracks, oTrack.Kind.Video) + for i in timeline.audio_tracks: + __process_obj(i, otio.tracks, oTrack.Kind.Audio) + + return opentimelineio.adapters.write_to_string( + otio, adapter_name=adapter_name + ) + def __process_obj(obj, otio, context=oTrack.Kind.Video): ar = obj.available_range if ar is None: @@ -95,11 +125,20 @@ def __process_obj(obj, otio, context=oTrack.Kind.Video): # if has media.. add external reference. media = obj.media if media: - ext = ExternalReference(str(media.media_source().media_reference.uri()), TimeRange(RationalTime(ar.frame_start().frames(), ar.rate().fps()), RationalTime(ar.frame_duration().frames(), ar.rate().fps()))) + ext = ExternalReference( + str(media.media_source().media_reference.uri()), + TimeRange( + RationalTime(ar.frame_start().frames() + media.media_source().media_reference.start_frame_offset(), ar.rate().fps()), + RationalTime(ar.frame_duration().frames() - media.media_source().media_reference.start_frame_offset(), ar.rate().fps()) + ) + ) ext.name = media.name c.media_reference = ext # c.availiable_range = TimeRange(RationalTime(ar.frame_start().frames(), ar.rate().fps()), RationalTime(ar.frame_duration().frames(), ar.rate().fps())) - c.source_range = TimeRange(RationalTime(sr.frame_start().frames(), sr.rate().fps()), RationalTime(sr.frame_duration().frames(), sr.rate().fps())) + c.source_range = TimeRange( + RationalTime(sr.frame_start().frames(), sr.rate().fps()), + RationalTime(sr.frame_duration().frames(), sr.rate().fps()) + ) otio.append(c) elif isinstance(obj, Gap): diff --git a/python/src/xstudio/api/intrinsic/global_store.py b/python/src/xstudio/api/intrinsic/global_store.py index fb6fecd8e..06cfd9666 100644 --- a/python/src/xstudio/api/intrinsic/global_store.py +++ b/python/src/xstudio/api/intrinsic/global_store.py @@ -1,8 +1,9 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.core import autosave_atom -from xstudio.api.auxiliary.json_store import JsonStorePy +from xstudio.api.session import Container +from xstudio.api.auxiliary.json_store import JsonStoreHandler -class GlobalStore(JsonStorePy): +class GlobalStore(Container, JsonStoreHandler): """Access global state dictionary""" def __init__(self, connection, remote, uuid=None): @@ -15,7 +16,8 @@ def __init__(self, connection, remote, uuid=None): Kwargs: uuid(Uuid): Uuid of remote actor. """ - JsonStorePy.__init__(self, connection, remote, uuid) + Container.__init__(self, connection, remote, uuid) + JsonStoreHandler.__init__(self, self) @property def autosave(self): diff --git a/python/src/xstudio/api/intrinsic/history.py b/python/src/xstudio/api/intrinsic/history.py index 89b751c61..29f938b56 100644 --- a/python/src/xstudio/api/intrinsic/history.py +++ b/python/src/xstudio/api/intrinsic/history.py @@ -5,7 +5,7 @@ class History(Container): """Access JSON stores""" def __init__(self, connection, remote, uuid=None): - """Create JsonStorePy object. + """Create object. Args: connection(Connection): Connection object diff --git a/python/src/xstudio/api/module.py b/python/src/xstudio/api/module.py index f0b89b91e..8ca4f6e56 100644 --- a/python/src/xstudio/api/module.py +++ b/python/src/xstudio/api/module.py @@ -1,25 +1,25 @@ # SPDX-License-Identifier: Apache-2.0 -from xstudio.core import add_attribute_atom, connect_to_ui_atom +from xstudio.core import add_attribute_atom, connect_to_ui_atom, remove_attribute_atom from xstudio.core import disconnect_from_ui_atom from xstudio.core import attribute_role_data_atom, change_attribute_event_atom from xstudio.core import attribute_value_atom, register_hotkey_atom from xstudio.core import get_global_playhead_events_atom, join_broadcast_atom from xstudio.core import viewport_playhead_atom, hotkey_event_atom -from xstudio.core import attribute_uuids_atom, request_full_attributes_description_atom -from xstudio.core import AttributeRole, remove_attribute_atom +from xstudio.core import attribute_uuids_atom +from xstudio.core import AttributeRole, get_event_group_atom +from xstudio.core import event_atom, show_atom, module_add_menu_item_atom +from xstudio.core import module_remove_menu_item_atom, uuid_atom +from xstudio.core import menu_node_activated_atom, set_node_data_atom +from xstudio.core import ColourTriplet from xstudio.api.auxiliary import ActorConnection from xstudio.core import JsonStore, Uuid from xstudio.api.auxiliary.helpers import get_event_group -import sys +from xstudio.api.session.media import Media, MediaSource +import json import os +import sys import traceback -try: - import XStudioExtensions -except: - XStudioExtensions = None - - class ModuleAttribute: """Simple wrapper providing attribute object interface""" @@ -45,22 +45,56 @@ def __init__( if uuid: self.uuid = uuid else: + # as yet, I haven't worked out how to allow type conversion from + # custom python type (like ColourTriplet) and JsonStore !! + if isinstance(attribute_value, ColourTriplet): + # backend knows to interpret this back into a ColourTriplet + attribute_value = ["colour", 1, attribute_value.red, attribute_value.green, attribute_value.blue] + + converted_val = JsonStore() + if type(attribute_value) == dict or type(attribute_value) == list: + converted_val.parse_string(json.dumps(attribute_value)) + elif type(attribute_value) == type(JsonStore()): + converted_val = attribute_value + else: + converted_val = JsonStore(attribute_value) + self.uuid = connection.request_receive( parent_remote, add_attribute_atom(), attribute_name, - attribute_value if type(attribute_value) == type(JsonStore()) else JsonStore(attribute_value), + converted_val, JsonStore(attribute_role_data) )[0] + def role_data(self, role_name): + + return json.loads(self.connection.request_receive( + self.parent_remote, + attribute_role_data_atom(), + self.uuid, + role_name)[0].dump()) + def expose_in_ui_attrs_group(self, group_name): - self.connection.send( + try: + group_list = self.role_data("groups") + if type(group_name) == str: + group_list.append(group_name) + elif type(group_name) == list: + group_list = group_list+group_name + except: + if type(group_name) == str: + group_list = [group_name] + elif type(group_name) == list: + group_list = group_name + + self.connection.request_receive( self.parent_remote, attribute_role_data_atom(), self.uuid, "groups", - JsonStore(group_name)) + JsonStore(group_list)) def set_tool_tip(self, tool_tip): @@ -71,12 +105,25 @@ def set_tool_tip(self, tool_tip): "tooltip", JsonStore(tool_tip)) + def set_role_data(self, role_name, data): + + r = self.connection.request_receive( + self.parent_remote, + attribute_role_data_atom(), + self.uuid, + role_name, + JsonStore(data))[0] + if r != True: + raise Exception("set_role_data with rolename: {0}, data: {1} failed with error {2}:", + role_name, + data, + r) + def set_redraw_viewport_on_change(self, *args): pass def value(self): - return self.connection.request_receive( self.parent_remote, attribute_value_atom(), @@ -94,28 +141,42 @@ def name(self): def set_value(self, value): + if isinstance(value, list): + v = JsonStore() + v.parse_string(json.dumps(value)) + value = v + elif not isinstance(value, JsonStore): + value = JsonStore(value) + self.connection.send( self.parent_remote, attribute_value_atom(), self.uuid, - JsonStore(value)) + value) - def set_role_data(self, role_name, data): + def add_to_preferences(self): - r = self.connection.request_receive( + self.connection.send( self.parent_remote, - attribute_role_data_atom(), + attribute_value_atom(), + self.uuid) + + def as_json(self): + + return self.connection.request_receive( + self.parent_remote, + attribute_value_atom(), self.uuid, - role_name, - JsonStore(data))[0] - if r != True: - raise Exception("set_role_data with rolename: {0}, data: {1} failed with error {2}:", - role_name, - data, - r) + True + )[0].get() +class ModuleMeta(type): + def __call__(cls, *args, **kwargs): + instance = super().__call__(*args, **kwargs) + instance.setup_message_handler() + return instance -class ModuleBase(ActorConnection): +class ModuleBase(ActorConnection, metaclass=ModuleMeta): """Wrapper for Module class instances""" @@ -138,8 +199,11 @@ def __init__( attribute_uuids_atom())[0] self.attrs_by_name_ = {} + self.attrs_by_uuid_ = {} self.menu_trigger_callbacks = {} + self.menu_item_ids = [] self.hotkey_callbacks = {} + self._uuid = None for attr_uuid in attr_uuids: attr_wrapper = ModuleAttribute( @@ -147,17 +211,18 @@ def __init__( remote, uuid=attr_uuid) self.attrs_by_name_[attr_wrapper.name] = attr_wrapper - - remote_event_group = get_event_group(self.connection, remote) - if XStudioExtensions: - XStudioExtensions.add_message_callback( - (remote_event_group, self.incoming_msg) - ) - else: - connection.add_handler(remote, self.message_handler) + self.attrs_by_uuid_[attr_uuid] = attr_wrapper self.__attribute_changed = None - self.__playhead_event_callback = None + + def setup_message_handler(self): + + # this call gets the event group for Module attribute change events + remote_event_group = self.connection.request_receive(self.remote, get_event_group_atom(), True)[0] + + self.connection.link.add_message_callback( + remote_event_group, self.message_handler + ) def connect_to_ui(self): """Call this method to 'activate' the plugin and forward live data about @@ -177,6 +242,22 @@ def disconnect_from_ui(self): self.remote, disconnect_from_ui_atom()) + def get_attribute( + self, + attribute_name + ): + """Get an attribute with the given name. Throws and exception if the + attribute does not exist + + Args: + attribute_name(str): Name of attribute + Returns: + + """ + if attribute_name in self.attrs_by_name_: + return self.attrs_by_name_[attribute_name] + raise Exception("No such attribute {}".format(attribute_name)) + def add_attribute( self, attribute_name, @@ -195,7 +276,7 @@ def add_attribute( attribute attribute_role_data(dict): Other role data of the attribute preference_path(str): If provided the attribute value will be - recorded in the users preferences data when xstudio closes and + recorded in the users preferences data when xstudio closes and the value restored next time xstudio starts up. """ @@ -207,6 +288,7 @@ def add_attribute( attribute_role_data) self.attrs_by_name_[attribute_name] = new_attr + self.attrs_by_uuid_[new_attr.uuid] = new_attr if preference_path: new_attr.set_role_data("preference_path", preference_path) @@ -240,61 +322,59 @@ def set_attribute(self, attr_name, value): else: raise Exception("No attribute named {0}".format(attr_name)) - def attributes_digest(self, verbose=False): - """Get a summary of the attributes that are part of this Module. Use - verbose=True to get a full description as a JsonStore object. Otherwise - a list of attribute names is returned. + def attribute_changed(self, attr, role): + """Override this method to get callbacks when an attribute's role + data has changed Args: - verbose(bool): Set True to return a full JsonStore dictionary of - attributes and their role data. - - Returns: - attrs_description(list(str) or JsonStore) + attr(ModuleAttribute): The attribute that has changed + role(int): The index for the role data that has changed """ - if verbose: - return self.connection.request_receive(self.remote, request_full_attributes_description_atom())[0] - else: - return self.attrs_by_name_.keys() + pass - def set_attribute_changed_event_handler(self, handler): - """Set the callback function that receives events from - the xSTUDIO messaging system + def subscribe_to_playhead_events(self, playhead_event_callback): + """Set the callback function for receiving events specific to + the playhead and subscrive to the playheads events broadcast + group. Args: - handler(Callable): Call back function + playhead_event_callback(Callable): Call back function """ - self.__attribute_changed = handler - def subscribe_to_playhead_events(self, playhead_event_callback): + gphev = self.connection.request_receive( + self.connection.remote(), + get_global_playhead_events_atom())[0] + + self.connection.link.add_message_callback( + gphev, playhead_event_callback + ) + + def subscribe_to_event_group(self, event_source, callback_method): """Set the callback function for receiving events specific to the playhead and subscrive to the playheads events broadcast group. Args: - playhead_event_callback(Callable): Call back function + event_source(ActorConnection): The actor whose event group + we want to join. + callback_method(Callable): The function which will be called + with event. Must take a single argument (a tuple of the event + data) """ - if XStudioExtensions: - gphev = self.connection.request_receive( - self.connection.remote(), - get_global_playhead_events_atom())[0] + event_group = self.connection.request_receive( + event_source.remote, + get_event_group_atom())[0] - playhead_event_group = get_event_group(self.connection, gphev) + if not event_group: + raise Exception("Actor has no event group.") - XStudioExtensions.add_message_callback( - (gphev, self.__playhead_events) + self.connection.link.add_message_callback( + event_group, callback_method ) - self.__playhead_event_callback = playhead_event_callback - - def __playhead_events(self, *args): - - try: - message_content = self.connection.caf_message_to_tuple(args[0][0]) - self.__playhead_event_callback(message_content) - except Exception as e: - pass + def menu_item_activated(self, menu_item_data, user_data): + pass def register_hotkey(self, hotkey_callback, @@ -336,8 +416,7 @@ def register_hotkey(self, hotkey_name, description, auto_repeat, - component, - context)[0] + component)[0] self.hotkey_callbacks[str(hotkey_uuid)] = hotkey_callback return hotkey_uuid @@ -365,46 +444,177 @@ def add_menu_item( JsonStore(role_data), )[0] + self.menu_item_ids.append(menu_item_uuid) + self.menu_trigger_callbacks[ - str(menu_item_uuid)] = menu_trigger_callback + menu_item_uuid] = menu_trigger_callback + + def insert_menu_item( + self, + menu_model_name, + menu_text, + menu_path, + menu_item_position, + attr_id=Uuid(), + divider=False, + hotkey_uuid=Uuid(), + callback=None, + user_data="" + ): + + menu_item_uuid = self.connection.request_receive( + self.remote, + module_add_menu_item_atom(), + menu_model_name, + menu_text, + menu_path, + menu_item_position, + attr_id, + divider, + hotkey_uuid, + user_data)[0] + + self.menu_item_ids.append(menu_item_uuid) + + if callback: + self.menu_trigger_callbacks[ + menu_item_uuid] = callback + + return menu_item_uuid + + def insert_hotkey_into_menu( + self, + menu_model_name, + menu_path, + menu_item_position, + hotkey_uuid + ): + + menu_item_uuid = self.connection.request_receive( + self.remote, + module_add_menu_item_atom(), + menu_model_name, + menu_path, + menu_item_position, + hotkey_uuid)[0] + + self.menu_item_ids.append(menu_item_uuid) + + return menu_item_uuid + + def set_submenu_position( + self, + menu_model_name, + submenu_path, + menu_item_position + ): + """When adding menu items that are placed under a submenu, the position + of the submenu in the parent menu is not set by default. Use this method + to ensure the submenu appears in the parent menu at the position you + prefer + + Args: + menu_model_name(str): The name of the menu model + submenu_path(str): The submenu name (preceded by parent submenus + separated by a pipe symbol e.g. 'Publish|Notes' if you wish to + set the position of the Notes submenu within the Publish submenu. + menu_item_position(float): The relative position in the parent menu. + """ + + self.connection.request_receive( + self.remote, + set_node_data_atom(), + menu_model_name, + submenu_path, + menu_item_position) + + def remove_menu_item( + self, + menu_uuid + ): + """Remove a menu item from the xSTUDIO menu models + + Args: + menu_uuid(Uuid): Id of menu item - def message_handler(self, sender, req_id, message_content): + Returns: + None + """ + + self.connection.request_receive( + self.remote, + module_remove_menu_item_atom(), + menu_uuid) + + def remove_menu_items( + self, + menu_uuids + ): + """Remove menu items from the xSTUDIO menu models + + Args: + menu_uuid(list(Uuid)): list of ids of menu items + + Returns: + None + """ + + for menu_uuid in menu_uuids: + self.connection.request_receive( + self.remote, + module_remove_menu_item_atom(), + menu_uuid) + + def message_handler(self, message_content): try: atom = message_content[0] if len(message_content) else None + + # message handler for attribute change if isinstance(atom, type(change_attribute_event_atom())): role = message_content[3] - if role == AttributeRole.Value: - attr_uuid = str(message_content[2]) if len( - message_content) > 2 else "" - if self.__attribute_changed: - for attr in self.attrs_by_name_.values(): - if attr.uuid == Uuid(attr_uuid): - self.__attribute_changed(attr) - if attr_uuid in self.menu_trigger_callbacks: - self.menu_trigger_callbacks[attr_uuid]() + attr_uuid = message_content[2] if len( + message_content) > 2 else Uuid() + if attr_uuid in self.attrs_by_uuid_: + self.attribute_changed(self.attrs_by_uuid_[attr_uuid], role) + if attr_uuid in self.menu_trigger_callbacks: + self.menu_trigger_callbacks[attr_uuid]() elif isinstance(atom, type(hotkey_event_atom())): hotkey_uuid = str(message_content[1]) if len( - message_content) > 1 else "" + message_content) > 1 else "" activated = bool(message_content[2]) if len( message_content) > 2 else False context = str(message_content[3]) if len( - message_content) > 3 else "" + message_content) > 3 else "" if hotkey_uuid in self.hotkey_callbacks: self.hotkey_callbacks[hotkey_uuid](activated, context) - + + elif isinstance(atom, type(menu_node_activated_atom())): + menu_item_data = message_content[1].get() if len( + message_content) > 1 else None + user_data = str(message_content[2]) if len( + message_content) > 2 else "" + if "uuid" in menu_item_data: + uuid = Uuid(menu_item_data["uuid"]) + if uuid in self.menu_item_ids: + self.menu_item_activated(menu_item_data, user_data) + if uuid in self.menu_trigger_callbacks: + self.menu_trigger_callbacks[uuid]() + except Exception as err: print (err) print (traceback.format_exc()) - def incoming_msg(self, *args): + @property + def uuid(self): + """Get Module uuid - try: - self.message_handler( - None, - None, - self.connection.caf_message_to_tuple(args[0][0]) - ) - except Exception as err: - pass + Returns: + uuid(Uuid): Uuid of actor container. + """ + if not self._uuid: + self._uuid = self.connection.request_receive( + self.remote, + uuid_atom())[0] + return self._uuid diff --git a/python/src/xstudio/api/session/bookmark/bookmark.py b/python/src/xstudio/api/session/bookmark/bookmark.py index 760bf12f5..bbe4c9b0f 100644 --- a/python/src/xstudio/api/session/bookmark/bookmark.py +++ b/python/src/xstudio/api/session/bookmark/bookmark.py @@ -106,6 +106,23 @@ def duration(self, x): detail.duration = x self.connection.request_receive(self.remote, bookmark_detail_atom(), detail) + + @property + def user_data(self): + """Is enabled. + + Returns: + result(bool): Enabled ? + """ + + return self.connection.request_receive(self.remote, bookmark_detail_atom())[0].user_data + + @user_data.setter + def user_data(self, x): + detail = BookmarkDetail() + detail.user_data = x + self.connection.request_receive(self.remote, bookmark_detail_atom(), detail) + @property def author(self): """Is enabled. diff --git a/python/src/xstudio/api/session/container.py b/python/src/xstudio/api/session/container.py index 5233aa850..be0f57f17 100644 --- a/python/src/xstudio/api/session/container.py +++ b/python/src/xstudio/api/session/container.py @@ -116,7 +116,6 @@ def children(self): return self._children - class Container(ActorConnection): def __init__(self, connection, remote, uuid=None): """Create Container object. @@ -137,6 +136,30 @@ def uuid_actor(self): """Return as a UuidActor.""" return UuidActor(self.uuid, self.remote) + def add_event_callback_function(self, callback_function): + """Set a callback that will be called whenever the Container sends + a message to its event group + Args: + callback_function(function): Callback function + + Returns: + uuid(Uuid): A unique ID for the cinternal allback handler. Use to + remove the callback + """ + return self.connection.link.add_message_callback( + self.group, callback_function + ) + + def remove_event_callback_function(self, callback_id): + """Remove a callback that was set-up to get event messages from the + container + Args: + callback_id(Uuid): Callback ID + """ + self.connection.link.remove_message_callback( + self.group, callback_id + ) + @property def name(self): """Get container name diff --git a/python/src/xstudio/api/session/media/media.py b/python/src/xstudio/api/session/media/media.py index b64ff7d7c..70a8faaa6 100644 --- a/python/src/xstudio/api/session/media/media.py +++ b/python/src/xstudio/api/session/media/media.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.core import get_media_source_atom, current_media_source_atom, get_json_atom, get_metadata_atom, reflag_container_atom, rescan_atom -from xstudio.core import invalidate_cache_atom, get_media_pointer_atom, MediaType, Uuid -from xstudio.core import add_media_source_atom, FrameRate, FrameList, parse_posix_path, URI +from xstudio.core import invalidate_cache_atom, get_media_pointer_atom, MediaType, Uuid, media_status_atom +from xstudio.core import add_media_source_atom, FrameRate, FrameList, parse_posix_path, URI, MediaStatus from xstudio.core import set_json_atom, JsonStore, quickview_media_atom from xstudio.api.session.container import Container @@ -41,6 +41,26 @@ def media_sources(self): return _media_sources + @property + def status(self): + """Get media status + + Returns: + status(MediaStatus): Status of current media source. + """ + + return self.connection.request_receive(self.remote, media_status_atom())[0] + + @property + def is_online(self): + """Get media status + + Returns: + status(MediaStatus): Status of current media source. + """ + + return self.status == MediaStatus.MS_ONLINE + @property def flag_colour(self): """Get media flag colour. @@ -61,6 +81,32 @@ def flag_text(self): return self.connection.request_receive(self.remote, reflag_container_atom())[0][1] + @flag_colour.setter + def flag_colour(self, colour): + """Set media flag colour. + + Args: + colour(string): colour string + + Returns: + bool: success + + """ + return self.reflag(colour, self.flag_text) + + @flag_text.setter + def flag_text(self, text): + """Set media flag text. + + Args: + text(string): text string + + Returns: + bool: success + + """ + return self.reflag(self.flag_colour, text) + def media_source(self, media_type=MediaType.MT_IMAGE): """Get current media source. Args: diff --git a/python/src/xstudio/api/session/media/media_source.py b/python/src/xstudio/api/session/media/media_source.py index 21f0ce4d7..c00990d79 100644 --- a/python/src/xstudio/api/session/media/media_source.py +++ b/python/src/xstudio/api/session/media/media_source.py @@ -1,13 +1,14 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.core import get_media_stream_atom, current_media_stream_atom, MediaType, media_reference_atom, rescan_atom, invalidate_cache_atom -from xstudio.core import media_status_atom, get_json_atom, set_json_atom, JsonStore +from xstudio.core import media_status_atom from xstudio.api.session.container import Container from xstudio.api.session.media.media_stream import MediaStream +from xstudio.api.auxiliary.json_store import JsonStoreHandler import json -class MediaSource(Container): +class MediaSource(Container, JsonStoreHandler): """MediaSource object.""" def __init__(self, connection, remote, uuid=None): @@ -25,6 +26,7 @@ def __init__(self, connection, remote, uuid=None): str(MediaType.MT_IMAGE): None, str(MediaType.MT_AUDIO): None } + JsonStoreHandler.__init__(self, self) @property def image_streams(self): @@ -132,54 +134,6 @@ def invalidate_cache(self): """ return self.connection.request_receive(self.remote, invalidate_cache_atom())[0] - @property - def metadata(self): - """Get media metadata. - - Returns: - metadata(json): Media metadata. - """ - return json.loads(self.connection.request_receive(self.remote, get_json_atom(), "")[0].dump()) - - - @metadata.setter - def metadata(self, new_metadata): - """Set media reference rate. - - Args: - new_metadata(json): Json dict to set as media source metadata - - Returns: - bool: success - - """ - return self.connection.request_receive(self.remote, set_json_atom(), JsonStore(new_metadata)) - - def get_metadata(self, path): - """Get metdata at JSON path - - Args: - path(str): JSON Pointer - - Returns: - metadata(json) Json at pointer location - """ - - return json.loads(self.connection.request_receive(self.remote, get_json_atom(), path)[0].dump()) - - def set_metadata(self, data, path): - """Get metdata at JSON path - - Args: - data(json): JSON Data - path(str): JSON Pointer - - Returns: - bool: success - """ - - return self.connection.request_receive(self.remote, set_json_atom(), JsonStore(data), path)[0] - @property def image_stream(self): """Get current image stream. diff --git a/python/src/xstudio/api/session/media/media_stream.py b/python/src/xstudio/api/session/media/media_stream.py index af66ac719..e9c02245e 100644 --- a/python/src/xstudio/api/session/media/media_stream.py +++ b/python/src/xstudio/api/session/media/media_stream.py @@ -1,9 +1,9 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.core import get_media_type_atom, get_stream_detail_atom - from xstudio.api.session.container import Container +from xstudio.api.auxiliary.json_store import JsonStoreHandler -class MediaStream(Container): +class MediaStream(Container, JsonStoreHandler): """MediaStream object, contains media track.""" def __init__(self, connection, remote, uuid=None): @@ -17,6 +17,7 @@ def __init__(self, connection, remote, uuid=None): uuid(Uuid): Uuid of remote actor. """ Container.__init__(self, connection, remote, uuid) + JsonStoreHandler.__init__(self, self) @property def media_type(self): @@ -34,4 +35,5 @@ def media_stream_detail(self): Returns: stream_detail(StreamDetail): Detail of media stream. """ - return self.connection.request_receive(self.remote, get_stream_detail_atom())[0] \ No newline at end of file + return self.connection.request_receive(self.remote, get_stream_detail_atom())[0] + diff --git a/python/src/xstudio/api/session/playhead/__init__.py b/python/src/xstudio/api/session/playhead/__init__.py index c02013f37..96b0b7119 100644 --- a/python/src/xstudio/api/session/playhead/__init__.py +++ b/python/src/xstudio/api/session/playhead/__init__.py @@ -1,2 +1,3 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.api.session.playhead.playhead import Playhead +from xstudio.api.session.playhead.playhead import PlayheadSelection diff --git a/python/src/xstudio/api/session/playhead/playhead.py b/python/src/xstudio/api/session/playhead/playhead.py index 68cfd337f..09658f0ac 100644 --- a/python/src/xstudio/api/session/playhead/playhead.py +++ b/python/src/xstudio/api/session/playhead/playhead.py @@ -2,7 +2,7 @@ from xstudio.core import play_atom, loop_atom, compare_mode_atom, play_forward_atom from xstudio.core import logical_frame_atom, play_rate_mode_atom, source_atom, media_atom from xstudio.core import simple_loop_start_atom, simple_loop_end_atom, use_loop_range_atom -from xstudio.core import viewport_playhead_atom, media_logical_frame_atom +from xstudio.core import viewport_playhead_atom, media_logical_frame_atom, playhead_rate_atom from xstudio.core import JsonStore, change_attribute_value_atom, jump_atom from xstudio.api.module import ModuleBase @@ -120,31 +120,6 @@ def looping(self, x): """ self.connection.send(self.remote, loop_atom(), x) - @property - def compare_mode(self): - """Playhead compare mode. - - Returns: - compare_mode(str): Current compare mode. - """ - self.connection.send(self.remote, attribute_value_atom(), "Compare") - result = self.connection.dequeue_message() - return result[0] - - @compare_mode.setter - def compare_mode(self, x): - """Set compare mode. - - Args: - mode(str): Set compare mode. - """ - self.connection.send( - self.remote, - change_attribute_value_atom(), - "Compare", - JsonStore(x) - ) - @property def play_forward(self): """Is playing forwards. @@ -216,3 +191,12 @@ def source(self, new_source): new_source(actor): Set playable source to this. """ self.connection.send(self.remote, source_atom(), new_source) + + @property + def frame_rate(self): + """Get playhead rate. + + Returns: + playhead_rate(core.FrameRate): Rate for new playheads. + """ + return self.connection.request_receive(self.remote, playhead_rate_atom())[0] diff --git a/python/src/xstudio/api/session/playhead/playhead_selection.py b/python/src/xstudio/api/session/playhead/playhead_selection.py index 38760e199..46e08cc70 100644 --- a/python/src/xstudio/api/session/playhead/playhead_selection.py +++ b/python/src/xstudio/api/session/playhead/playhead_selection.py @@ -1,9 +1,10 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.core import monitored_atom, Uuid, MT_IMAGE, MT_AUDIO from xstudio.core import move_atom, remove_atom, actor, select_all_media_atom -from xstudio.core import select_media_atom +from xstudio.core import select_media_atom, get_selected_sources_atom from xstudio.api.session import Container +from xstudio.api.session.media.media import Media class PlayheadSelection(Container): """Playhead selection object, what the playhead plays.""" @@ -53,7 +54,7 @@ def selected_sources(self): result = self.connection.request_receive(self.remote, get_selected_sources_atom())[0] for i in result: - clips.append(Media(self.connection, i)) + clips.append(Media(self.connection, i.actor, i.uuid)) return clips diff --git a/python/src/xstudio/api/session/playlist/contact_sheet.py b/python/src/xstudio/api/session/playlist/contact_sheet.py index 685263c5d..8687b2cad 100644 --- a/python/src/xstudio/api/session/playlist/contact_sheet.py +++ b/python/src/xstudio/api/session/playlist/contact_sheet.py @@ -1,5 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.api.session.playlist.subset import Subset +from xstudio.core import URI, selection_actor_atom +from xstudio.api.session.playhead import Playhead, PlayheadSelection class ContactSheet(Subset): """ContactSheet object.""" @@ -17,3 +19,14 @@ def __init__(self, connection, remote, uuid=None): uuid(Uuid): Uuid of remote actor. """ Subset.__init__(self, connection, remote, uuid) + + @property + def playhead_selection(self): + """The actor that filters a selection of media from a playhead + and passes to a playhead for playback. + + Returns: + source(PlayheadSelection): Currently playing this. + """ + result = self.connection.request_receive(self.remote, selection_actor_atom())[0] + return PlayheadSelection(self.connection, result) diff --git a/python/src/xstudio/api/session/playlist/playlist.py b/python/src/xstudio/api/session/playlist/playlist.py index 98823d5e9..e78a524b8 100644 --- a/python/src/xstudio/api/session/playlist/playlist.py +++ b/python/src/xstudio/api/session/playlist/playlist.py @@ -4,11 +4,10 @@ from xstudio.core import rename_container_atom, create_subset_atom, create_timeline_atom from xstudio.core import move_container_atom, remove_container_atom, type_atom, parse_posix_path from xstudio.core import create_divider_atom, media_rate_atom, playhead_rate_atom, URI, FrameRate -from xstudio.core import remove_media_atom, UuidVec, move_media_atom, create_playhead_atom, selection_actor_atom +from xstudio.core import remove_media_atom, VectorUuid, move_media_atom, create_playhead_atom, selection_actor_atom from xstudio.core import convert_to_timeline_atom, convert_to_subset_atom, convert_to_contact_sheet_atom -from xstudio.core import reflag_container_atom +from xstudio.core import reflag_container_atom, expanded_atom, session_atom, copy_media_atom from xstudio.core import FrameList, FrameRate, MediaType -from xstudio.core import get_json_atom, set_json_atom, JsonStore from xstudio.api.session.container import Container, PlaylistTree from xstudio.api.session.playhead.playhead import Playhead @@ -17,10 +16,12 @@ from xstudio.api.session.playlist.subset import Subset from xstudio.api.session.playlist.contact_sheet import ContactSheet from xstudio.api.session.playlist.timeline import Timeline +from xstudio.api.auxiliary import NotificationHandler +from xstudio.api.auxiliary.json_store import JsonStoreHandler import json -class Playlist(Container): +class Playlist(Container, NotificationHandler, JsonStoreHandler): """Playlist object.""" def __init__(self, connection, remote, uuid=None): @@ -36,6 +37,8 @@ def __init__(self, connection, remote, uuid=None): uuid(Uuid): Uuid of remote actor. """ Container.__init__(self, connection, remote, uuid) + NotificationHandler.__init__(self, self) + JsonStoreHandler.__init__(self, self) @property def playhead(self): @@ -70,7 +73,7 @@ def add_media_list(self, path, recurse=False, media_rate=None): result = self.connection.request_receive(self.remote, add_media_atom(), path, recurse, Uuid())[0] else: result = self.connection.request_receive(self.remote, add_media_atom(), path, recurse, media_rate, Uuid())[0] - + return [Media(self.connection, i.actor, i.uuid) for i in result] def add_media_with_audio(self, image_path, audio_path, audio_offset=0): @@ -426,20 +429,19 @@ def remove_media(self, media): Returns: success(bool): Returns result. """ + media_list = [] + if isinstance(media, Media): - media = media.uuid + media_list = [media.uuid] - if isinstance(media, list): - media_list = UuidVec() + elif isinstance(media, list): for m in media: if isinstance(m, Media): - media_list.push_back(m.uuid) + media_list.append(m.uuid) else: media_list.push_back(m) - media = media_list - - return self.connection.request_receive(self.remote, remove_media_atom(), media)[0] + return self.connection.request_receive(self.remote, remove_media_atom(), VectorUuid(media_list))[0] def move_media(self, media, before=Uuid()): """Move media in tree. @@ -461,6 +463,55 @@ def move_media(self, media, before=Uuid()): return self.connection.request_receive(self.remote, move_media_atom(), media, before)[0] + def copy_media(self, media, before=Uuid()): + """Copy media into playlist. + + Args: + media(Media/Uuid): Media to copy. + + Kwargs: + before(Uuid): Insert before this item. + + Returns: + success(UuidList): Returns result. + """ + + # get session actor.. + # + if isinstance(before, Media): + before = before.uuid + + media_list = [] + + if isinstance(media, Media): + media_list.append(media.uuid) + + if isinstance(media, Uuid): + media_list.append(media) + + if isinstance(media, list): + for m in media: + if isinstance(m, Media): + media_list.append(m.uuid) + else: + media_list.append(m) + + session = self.connection.request_receive(self.remote, session_atom())[0] + + uuids = self.connection.request_receive(session, copy_media_atom(), self.uuid, VectorUuid(media_list), True, before, False)[0] + + current_media = self.media + new_media = [] + + for u in uuids: + for m in current_media: + if m.uuid == u: + new_media.append(m) + break + + + return new_media + def convert_to_subset(self, src, name="Converted", before=Uuid()): """Convert src to Subset. @@ -529,51 +580,28 @@ def convert_to_timeline(self, src, name="Converted", before=Uuid()): return (result[0], Timeline(self.connection, result[1].actor, result[1].uuid)) @property - def metadata(self): - """Get metadata. + def expanded(self): + """Expanded. Returns: - metadata(json): Metadata attached to playlist. + expanded(bool): Playlist expanded state (whether it's subsets, timelines are visible + in the playlists panel) """ - return json.loads(self.connection.request_receive(self.remote, get_json_atom(), "")[0].dump()) - - @metadata.setter - def metadata(self, new_metadata): - """Set media reference rate. - - Args: - new_metadata(json): Json dict to set as media source metadata - - Returns: - bool: success + return self.connection.request_receive(self.remote, expanded_atom())[0] - """ - return self.connection.request_receive(self.remote, set_json_atom(), JsonStore(new_metadata)) - - def get_metadata(self, path): - """Get metdata at JSON path + @expanded.setter + def expanded(self, is_expanded): + """Set playlist expanded state Args: - path(str): JSON Pointer + is_expanded(bool): Expanded state Returns: - metadata(json) Json at pointer location - """ + None - return json.loads(self.connection.request_receive(self.remote, get_json_atom(), path)[0].dump()) - - def set_metadata(self, data, path): - """Get metdata at JSON path - - Args: - data(json): JSON Data - path(str): JSON Pointer - - Returns: - bool: success """ + self.connection.request_receive(self.remote, expanded_atom(), is_expanded) - return self.connection.request_receive(self.remote, set_json_atom(), JsonStore(data), path)[0] from xstudio.api.auxiliary.otio import import_timeline_from_file diff --git a/python/src/xstudio/api/session/playlist/subset.py b/python/src/xstudio/api/session/playlist/subset.py index 44b9c362c..a3945ccd4 100644 --- a/python/src/xstudio/api/session/playlist/subset.py +++ b/python/src/xstudio/api/session/playlist/subset.py @@ -1,9 +1,11 @@ # SPDX-License-Identifier: Apache-2.0 -from xstudio.core import Uuid, add_media_atom, actor +from xstudio.core import Uuid, add_media_atom, actor, selection_actor_atom, get_media_atom from xstudio.api.session.container import Container from xstudio.api.session.media.media import Media +from xstudio.api.session.playhead import Playhead, PlayheadSelection +from xstudio.api.auxiliary.json_store import JsonStoreHandler -class Subset(Container): +class Subset(Container, JsonStoreHandler): """Subset object.""" def __init__(self, connection, remote, uuid=None): @@ -19,6 +21,7 @@ def __init__(self, connection, remote, uuid=None): uuid(Uuid): Uuid of remote actor. """ Container.__init__(self, connection, remote, uuid) + JsonStoreHandler.__init__(self, self) def add_media(self, media, before_uuid=Uuid()): """Add media to subset. @@ -34,7 +37,7 @@ def add_media(self, media, before_uuid=Uuid()): """ if not isinstance(media, actor) and not isinstance(media, Uuid): - media = media.remote + media = media.uuid_actor() if not isinstance(before_uuid, Uuid): before_uuid = before_uuid.uuid @@ -42,3 +45,26 @@ def add_media(self, media, before_uuid=Uuid()): result = self.connection.request_receive(self.remote, add_media_atom(), media, before_uuid)[0] return Media(self.connection, result.actor, result.uuid) + + @property + def media(self): + """Get media in subset + + Returns: + media(list[media]): Media + """ + result = self.connection.request_receive(self.remote, get_media_atom())[0] + return [Media(self.connection, i.actor, i.uuid) for i in result] + + + @property + def playhead_selection(self): + """The actor that filters a selection of media from a playhead + and passes to a playhead for playback. + + Returns: + source(PlayheadSelection): Currently playing this. + """ + result = self.connection.request_receive(self.remote, selection_actor_atom())[0] + return PlayheadSelection(self.connection, result) + diff --git a/python/src/xstudio/api/session/playlist/timeline/__init__.py b/python/src/xstudio/api/session/playlist/timeline/__init__.py index 0f5a418f4..4c93450c1 100644 --- a/python/src/xstudio/api/session/playlist/timeline/__init__.py +++ b/python/src/xstudio/api/session/playlist/timeline/__init__.py @@ -1,9 +1,15 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.core import UuidActor, Uuid, actor, item_atom, MediaType, ItemType, enable_atom, item_flag_atom from xstudio.core import active_range_atom, available_range_atom, undo_atom, redo_atom, history_atom, add_media_atom, item_name_atom +from xstudio.core import URI, selection_actor_atom, item_selection_atom, item_type_atom, get_media_atom, save_atom, export_atom +from xstudio.core import import_atom, erase_item_atom, get_playhead_atom, FrameRate, FrameRateDuration from xstudio.api.session.container import Container from xstudio.api.intrinsic import History from xstudio.api.session.media.media import Media +from xstudio.api.session.playlist.timeline.item import Item +from xstudio.api.session.playhead import Playhead, PlayheadSelection +from xstudio.api.auxiliary import NotificationHandler +from xstudio.api.auxiliary.json_store import JsonStoreHandler def create_gap(connection, name="Gap"): """Create Gap object. @@ -18,7 +24,7 @@ def create_gap(connection, name="Gap"): gap(Gap): Gap object. """ - return Gap(connection, connection.remote_spawn("Gap",name)) + return Gap(connection, connection.remote_spawn("Gap", name, FrameRateDuration())) def create_clip(connection, media, name="Clip"): """Create Clip object. @@ -64,7 +70,7 @@ def create_video_track(connection, name="Video Track"): track(Track): Track object. """ - return Track(ItemType.IT_VIDEO_TRACK, connection, connection.remote_spawn("Track", name, MediaType.MT_IMAGE)) + return Track(ItemType.IT_VIDEO_TRACK, connection, connection.remote_spawn("Track", name, FrameRate(), MediaType.MT_IMAGE)) def create_audio_track(connection, name="Audio Track"): """Create Track object. @@ -79,29 +85,32 @@ def create_audio_track(connection, name="Audio Track"): track(Track): Track object. """ - return Track(ItemType.IT_AUDIO_TRACK, connection, connection.remote_spawn("Track", name, MediaType.MT_AUDIO)) + return Track(ItemType.IT_AUDIO_TRACK, connection, connection.remote_spawn("Track", name, FrameRate(), MediaType.MT_AUDIO)) def create_item_container(connection, item): + return create_item_container_from_type(connection, item.item_type(), item.uuid(), item.actor()) + +def create_item_container_from_type(connection, item_type, uuid, actor): result = None - if item.item_type() == ItemType.IT_GAP: - result = Gap(connection, item.actor(), item.uuid()) - elif item.item_type() == ItemType.IT_CLIP: - result = Clip(connection, item.actor(), item.uuid()) - elif item.item_type() == ItemType.IT_STACK: - result = Stack(connection, item.actor(), item.uuid()) - elif item.item_type() == ItemType.IT_VIDEO_TRACK: - result = Track(ItemType.IT_VIDEO_TRACK, connection, item.actor(), item.uuid()) - elif item.item_type() == ItemType.IT_AUDIO_TRACK: - result = Track(ItemType.IT_AUDIO_TRACK, connection, item.actor(), item.uuid()) - elif item.item_type() == ItemType.IT_TIMELINE: - result = Timeline(connection, item.actor(), item.uuid()) + if item_type == ItemType.IT_GAP: + result = Gap(connection, actor, uuid) + elif item_type == ItemType.IT_CLIP: + result = Clip(connection, actor, uuid) + elif item_type == ItemType.IT_STACK: + result = Stack(connection, actor, uuid) + elif item_type == ItemType.IT_VIDEO_TRACK: + result = Track(ItemType.IT_VIDEO_TRACK, connection, actor, uuid) + elif item_type == ItemType.IT_AUDIO_TRACK: + result = Track(ItemType.IT_AUDIO_TRACK, connection, actor, uuid) + elif item_type == ItemType.IT_TIMELINE: + result = Timeline(connection, actor, uuid) else: - raise RuntimeError("Invalid type " + i.item_type()) + raise RuntimeError("Invalid type " + item_type) return result -class Timeline(Container): +class Timeline(Item, NotificationHandler, JsonStoreHandler): """Timeline object.""" def __init__(self, connection, remote, uuid=None): @@ -116,7 +125,9 @@ def __init__(self, connection, remote, uuid=None): Kwargs: uuid(Uuid): Uuid of remote actor. """ - Container.__init__(self, connection, remote, uuid) + Item.__init__(self, connection, remote, uuid) + NotificationHandler.__init__(self, self) + JsonStoreHandler.__init__(self, self) def __len__(self): """Get size. @@ -138,67 +149,23 @@ def stack(self): return create_item_container(self.connection, item) @property - def item_name(self): - """Get name. - - Returns: - name(str): Name. - """ - return self.item.name() - - @item_name.setter - def item_name(self, x): - """Set name. - - Args: - name(str): Set name. - """ - self.connection.request_receive(self.remote, item_name_atom(), x) - - @property - def item_flag(self): - """Get flag. - - Returns: - name(str): flag. - """ - return self.item.flag() - - @item_flag.setter - def item_flag(self, x): - """Set flag. - - Args: - name(str): Set flag. - """ - self.connection.request_receive(self.remote, item_flag_atom(), x) - - @property - def enabled(self): - """Get enabled state. + def selection(self): + """Get currently selected items Returns: - enabled(bool): State. + list([item]): Selected Items """ - return self.item.enabled() - @enabled.setter - def enabled(self, x): - """Set enabled state. + items = self.connection.request_receive(self.remote, item_selection_atom())[0] + result = [] - Args: - state(bool): Set enabled state. - """ - self.connection.request_receive(self.remote, enable_atom(), x) - - @property - def item(self): - """Get item. + for i in items: + item_type = self.connection.request_receive(i.actor, item_type_atom())[0] + result.append( + create_item_container_from_type(self.connection, item_type, i.uuid, i.actor) + ) - Returns: - item(Item): Item for timeline. - """ - return self.connection.request_receive(self.remote, item_atom())[0] + return result @property def children(self): @@ -220,36 +187,6 @@ def children_of_type(self, types): """ return [create_item_container(self.connection, i) for i in self.item.children() if i.item_type() in types] - @property - def trimmed_range(self): - return self.item.trimmed_range() - - @property - def available_range(self): - return self.item.available_range() - - @available_range.setter - def available_range(self, x): - """Set available_range. - - Args: - x(FrameRange): Set available_range. - """ - self.connection.request_receive(self.remote, available_range_atom(), x) - - @property - def active_range(self): - return self.item.active_range() - - @active_range.setter - def active_range(self, x): - """Set active_range. - - Args: - x(FrameRange): Set active_range. - """ - self.connection.request_receive(self.remote, active_range_atom(), x) - @property def tracks(self): """Get children. @@ -293,6 +230,12 @@ def undo(self): def redo(self): return self.connection.request_receive(self.remote, redo_atom())[0] + def clear(self): + """Clear all video and audio tracks in the stack""" + while len(self.tracks): + print (self.tracks) + self.stack.erase_child(0) + def create_gap(self, name="Gap"): """Create Gap object. @@ -354,6 +297,37 @@ def create_video_track(self, name="Video Track"): """ return create_video_track(self.connection, name) + def insert_video_track(self, name="Video Track", index=0): + """Insert video track + + Args: + name(str): Name of new track. + index(int): Position to insert + + Returns: + success(Item): New item + """ + + result = Track(ItemType.IT_VIDEO_TRACK, self.connection, self.connection.remote_spawn("Track", name, self.rate, MediaType.MT_IMAGE)) + self.stack.insert_child(result, index) + return result + + def insert_audio_track(self, name="Audio Track", index=-1): + """Insert audio track + + Args: + name(str): Name of new track. + index(int): Position to insert + + Returns: + success(Item): New item + """ + + result = Track(ItemType.IT_AUDIO_TRACK, self.connection, self.connection.remote_spawn("Track", name, self.rate, MediaType.MT_AUDIO)) + self.stack.insert_child(result, index) + return result + + def export_timeline_to_file(self, path, adapter_name=None): """Create otio from timeline. @@ -379,7 +353,7 @@ def add_media(self, media, before_uuid=Uuid()): """ if not isinstance(media, actor) and not isinstance(media, Uuid): - media = media.remote + media = media.uuid_actor() if not isinstance(before_uuid, Uuid): before_uuid = before_uuid.uuid @@ -388,6 +362,97 @@ def add_media(self, media, before_uuid=Uuid()): return result + def load_otio(self, otio_body, path="", clear=False): + """Load otio json data into the timeline. Replaces entire timeline + with the incoming otio + + Args: + otio_body(json dict): OTIO data + + Kwargs: + path(str): Path that otio_body was loaded from + clear(bool): Clear timeline completely before load + + Returns: + seccess(bool): True on successful load from OTIO. + """ + return self.connection.request_receive( + self.remote, + import_atom(), + URI(path) if path else URI(), + otio_body, + clear)[0] + + def export_otio(self, path, schema=""): + """Export timeline via OpenTimelineIO. File path extension infers the + format of the exported file. + + Args: + path(str/uri): Path to export to. + + Returns: + bool: True on success, False on failure + """ + otio_string = self.connection.request_receive(self.remote, export_atom())[0] + + from opentimelineio.adapters import read_from_string, write_to_file + from opentimelineio import versioning + + otio_object = read_from_string(otio_string) + + result = False + + if schema: + # versioning.full_map() contains list.. + downgrade_manifest = versioning.fetch_map("OTIO_CORE", schema) + result = write_to_file(otio_object, path, target_schema_versions=downgrade_manifest) + else: + result = write_to_file(otio_object, path) + + return result + + @property + def audio_tracks(self): + return self.stack.audio_tracks + + @property + def video_tracks(self): + return self.stack.video_tracks + + @property + def media(self): + """Get media in timeline + + Returns: + media(list[media]): Media + """ + result = self.connection.request_receive(self.remote, get_media_atom())[0] + return [Media(self.connection, i.actor, i.uuid) for i in result] + + + @property + def playhead(self): + """Get playhead. + + Returns: + playhead(Playhead): Playhead attached to timeline. + """ + result = self.connection.request_receive(self.remote, get_playhead_atom())[0] + + return Playhead(self.connection, result.actor, result.uuid) + + + @property + def playhead_selection(self): + """The actor that filters a selection of media from a playhead + and passes to a playhead for playback. + + Returns: + source(PlayheadSelection): Currently playing this. + """ + result = self.connection.request_receive(self.remote, selection_actor_atom())[0] + return PlayheadSelection(self.connection, result) + # @property # def audio_tracks(self): # """Get children. diff --git a/python/src/xstudio/api/session/playlist/timeline/clip.py b/python/src/xstudio/api/session/playlist/timeline/clip.py index 038ee4566..8160f596a 100644 --- a/python/src/xstudio/api/session/playlist/timeline/clip.py +++ b/python/src/xstudio/api/session/playlist/timeline/clip.py @@ -1,15 +1,15 @@ # SPDX-License-Identifier: Apache-2.0 -from xstudio.core import enable_atom, item_atom, active_range_atom, available_range_atom, get_media_atom, item_name_atom, item_flag_atom -from xstudio.api.session.container import Container +from xstudio.api.session.playlist.timeline.item import Item from xstudio.api.session.media.media import Media +from xstudio.core import get_media_atom -class Clip(Container): +class Clip(Item): """Timeline object.""" def __init__(self, connection, remote, uuid=None): """Create Clip object. - Derived from Container. + Derived from Item. Args: connection(Connection): Connection object @@ -18,16 +18,7 @@ def __init__(self, connection, remote, uuid=None): Kwargs: uuid(Uuid): Uuid of remote actor. """ - Container.__init__(self, connection, remote, uuid) - - @property - def item(self): - """Get item. - - Returns: - item(Item): Item for timeline. - """ - return self.connection.request_receive(self.remote, item_atom())[0] + Item.__init__(self, connection, remote, uuid) @property def media(self): @@ -41,91 +32,3 @@ def media(self): return None return Media(self.connection, m.actor, m.uuid) - - @property - def item_name(self): - """Get name. - - Returns: - name(str): Name. - """ - return self.item.name() - - @item_name.setter - def item_name(self, x): - """Set name. - - Args: - name(str): Set name. - """ - self.connection.request_receive(self.remote, item_name_atom(), x) - - @property - def item_flag(self): - """Get flag. - - Returns: - name(str): flag. - """ - return self.item.flag() - - @item_flag.setter - def item_flag(self, x): - """Set flag. - - Args: - name(str): Set flag. - """ - self.connection.request_receive(self.remote, item_flag_atom(), x) - - @property - def enabled(self): - """Get enabled state. - - Returns: - enabled(bool): State. - """ - return self.item.enabled() - - @enabled.setter - def enabled(self, x): - """Set enabled state. - - Args: - state(bool): Set enabled state. - """ - self.connection.request_receive(self.remote, enable_atom(), x) - - @property - def children(self): - return [] - - @property - def trimmed_range(self): - return self.item.trimmed_range() - - @property - def available_range(self): - return self.item.available_range() - - @available_range.setter - def available_range(self, x): - """Set available_range. - - Args: - x(FrameRange): Set available_range. - """ - self.connection.request_receive(self.remote, available_range_atom(), x) - - @property - def active_range(self): - return self.item.active_range() - - @active_range.setter - def active_range(self, x): - """Set active_range. - - Args: - x(FrameRange): Set active_range. - """ - self.connection.request_receive(self.remote, active_range_atom(), x) diff --git a/python/src/xstudio/api/session/playlist/timeline/gap.py b/python/src/xstudio/api/session/playlist/timeline/gap.py index 4d2975390..61d14a2c8 100644 --- a/python/src/xstudio/api/session/playlist/timeline/gap.py +++ b/python/src/xstudio/api/session/playlist/timeline/gap.py @@ -1,14 +1,13 @@ # SPDX-License-Identifier: Apache-2.0 -from xstudio.core import Uuid, actor, item_atom, enable_atom, active_range_atom, available_range_atom, item_name_atom, item_flag_atom -from xstudio.api.session.container import Container +from xstudio.api.session.playlist.timeline.item import Item -class Gap(Container): +class Gap(Item): """Timeline object.""" def __init__(self, connection, remote, uuid=None): """Create Gap object. - Derived from Container. + Derived from Item. Args: connection(Connection): Connection object @@ -17,101 +16,5 @@ def __init__(self, connection, remote, uuid=None): Kwargs: uuid(Uuid): Uuid of remote actor. """ - Container.__init__(self, connection, remote, uuid) + Item.__init__(self, connection, remote, uuid) - @property - def item(self): - """Get item. - - Returns: - item(Item): Item for timeline. - """ - return self.connection.request_receive(self.remote, item_atom())[0] - - @property - def enabled(self): - """Get enabled state. - - Returns: - enabled(bool): State. - """ - return self.item.enabled() - - @enabled.setter - def enabled(self, x): - """Set enabled state. - - Args: - state(bool): Set enabled state. - """ - self.connection.request_receive(self.remote, enable_atom(), x) - - @property - def item_name(self): - """Get name. - - Returns: - name(str): Name. - """ - return self.item.name() - - @item_name.setter - def item_name(self, x): - """Set name. - - Args: - name(str): Set name. - """ - self.connection.request_receive(self.remote, item_name_atom(), x) - - @property - def item_flag(self): - """Get flag. - - Returns: - name(str): flag. - """ - return self.item.flag() - - @item_flag.setter - def item_flag(self, x): - """Set flag. - - Args: - name(str): Set flag. - """ - self.connection.request_receive(self.remote, item_flag_atom(), x) - - @property - def children(self): - return [] - - @property - def trimmed_range(self): - return self.item.trimmed_range() - - @property - def available_range(self): - return self.item.available_range() - - @available_range.setter - def available_range(self, x): - """Set available_range. - - Args: - x(FrameRange): Set available_range. - """ - self.connection.request_receive(self.remote, available_range_atom(), x) - - @property - def active_range(self): - return self.item.active_range() - - @active_range.setter - def active_range(self, x): - """Set active_range. - - Args: - x(FrameRange): Set active_range. - """ - self.connection.request_receive(self.remote, active_range_atom(), x) diff --git a/python/src/xstudio/api/session/playlist/timeline/item.py b/python/src/xstudio/api/session/playlist/timeline/item.py index 988131360..755e61b2d 100644 --- a/python/src/xstudio/api/session/playlist/timeline/item.py +++ b/python/src/xstudio/api/session/playlist/timeline/item.py @@ -1 +1,233 @@ # SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: Apache-2.0 +from xstudio.core import Uuid, actor, item_atom, enable_atom, item_prop_atom, item_lock_atom, JsonStore +from xstudio.core import active_range_atom, available_range_atom, item_name_atom, item_flag_atom, trimmed_range_atom +from xstudio.api.session.container import Container + +import json + +class Item(Container): + """Timeline object.""" + + def __init__(self, connection, remote, uuid=None): + """Create Gap object. + + Derived from Container. + + Args: + connection(Connection): Connection object + remote(object): Remote actor object + + Kwargs: + uuid(Uuid): Uuid of remote actor. + """ + Container.__init__(self, connection, remote, uuid) + + @property + def item(self): + """Get item. + + Returns: + item(Item): Item for timeline. + """ + return self.connection.request_receive(self.remote, item_atom())[0] + + @property + def enabled(self): + """Get enabled state. + + Returns: + enabled(bool): State. + """ + return self.item.enabled() + + @enabled.setter + def enabled(self, x): + """Set enabled state. + + Args: + state(bool): Set enabled state. + """ + self.connection.request_receive(self.remote, enable_atom(), x) + + @property + def locked(self): + """Get enabled state. + + Returns: + enabled(bool): State. + """ + return self.item.locked() + + @locked.setter + def locked(self, x): + """Set enabled state. + + Args: + state(bool): Set enabled state. + """ + self.connection.request_receive(self.remote, item_lock_atom(), x) + + @property + def item_prop(self): + """Get item. + + Returns: + item(Item): Item for timeline. + """ + return json.loads(self.connection.request_receive(self.remote, item_prop_atom())[0].dump()) + + @item_prop.setter + def item_prop(self, x): + """Set enabled state. + + Args: + state(bool): Set enabled state. + """ + if not isinstance(x, JsonStore): + x = JsonStore(x) + + self.connection.request_receive(self.remote, item_prop_atom(), x) + + @property + def item_name(self): + """Get name. + + Returns: + name(str): Name. + """ + return self.item.name() + + @item_name.setter + def item_name(self, x): + """Set name. + + Args: + name(str): Set name. + """ + self.connection.request_receive(self.remote, item_name_atom(), x) + + @property + def item_flag(self): + """Get flag. + + Returns: + name(str): flag. + """ + return self.item.flag() + + @property + def item_flag_colour(self): + """Get flag. + + Returns: + name(str): flag. + """ + colour = self.item.flag() + + colours = { + "#FFFF0000": "Red", + "#FF00FF00": "Green", + "#FF0000FF": "Blue", + "#FFFFFF00": "Yellow", + "#FFFFA500": "Orange", + "#FF800080": "Purple", + "#FF000000": "Black", + "#FFFFFFFF": "White", + } + + return colours[colour] if colour in colours else colour + + @item_flag.setter + def item_flag(self, x): + """Set flag. + + Args: + name(str): Set flag. + """ + self.connection.request_receive(self.remote, item_flag_atom(), x) + + @property + def children(self): + """Get children. + + Returns: + children([Gap/Track/Clip/Stack]): Children. + """ + from xstudio.api.session.playlist.timeline import create_item_container + children = [] + + for i in self.item.children(): + children.append(create_item_container(self.connection, i)) + + return children + + @property + def trimmed_range(self): + return self.item.trimmed_range() + + @trimmed_range.setter + def trimmed_range(self, x): + """Set available_range. + + Args: + x(FrameRange): Set available_range. + """ + self.connection.request_receive(self.remote, trimmed_range_atom(), x, x) + + + @property + def rate(self): + return self.item.trimmed_range().rate() + + @property + def available_range(self): + return self.item.available_range() + + def range_at_index(self, index): + return self.item.range_at_index(index) + + def item_at_frame(self, frame): + return self.item.item_at_frame(frame) + + def frame_at_index(self, index): + return self.item.frame_at_index(index) + + @available_range.setter + def available_range(self, x): + """Set available_range. + + Args: + x(FrameRange): Set available_range. + """ + self.connection.request_receive(self.remote, available_range_atom(), x) + + @property + def active_range(self): + return self.item.active_range() + + @active_range.setter + def active_range(self, x): + """Set active_range. + + Args: + x(FrameRange): Set active_range. + """ + self.connection.request_receive(self.remote, active_range_atom(), x) + + def index_of_child(self, child): + """Get index of child."" + + Args child([Gap/Track/Clip/Stack]): Child. + + Returns: + index(int): Index. + """ + + index = -1 + children = self.children + for i in range(len(children)): + if children[i].uuid == child.uuid: + index = i + break + return index diff --git a/python/src/xstudio/api/session/playlist/timeline/stack.py b/python/src/xstudio/api/session/playlist/timeline/stack.py index 20c87d5ad..faaeb7069 100644 --- a/python/src/xstudio/api/session/playlist/timeline/stack.py +++ b/python/src/xstudio/api/session/playlist/timeline/stack.py @@ -1,11 +1,12 @@ # SPDX-License-Identifier: Apache-2.0 -from xstudio.core import Uuid, actor, UuidActor, ItemType, item_flag_atom -from xstudio.core import item_atom, insert_item_atom, enable_atom, remove_item_atom, erase_item_atom, item_name_atom, move_item_atom -from xstudio.core import active_range_atom, available_range_atom -from xstudio.api.session.container import Container + +from xstudio.core import insert_item_atom, remove_item_atom, erase_item_atom, move_item_atom +from xstudio.core import Uuid, actor, UuidActor, ItemType, UuidActorVec, MediaType +from xstudio.api.session.playlist.timeline.item import Item from xstudio.api.session.playlist.timeline import create_item_container +from xstudio.api.session.playlist.timeline.track import Track -class Stack(Container): +class Stack(Item): """Timeline object.""" def __init__(self, connection, remote, uuid=None): @@ -20,78 +21,57 @@ def __init__(self, connection, remote, uuid=None): Kwargs: uuid(Uuid): Uuid of remote actor. """ - Container.__init__(self, connection, remote, uuid) + Item.__init__(self, connection, remote, uuid) - @property - def item(self): - """Get item. + def __len__(self): + """Get size. Returns: - item(Item): Item for timeline. + size(int): Size. """ - return self.connection.request_receive(self.remote, item_atom())[0] + return len(self.item) @property - def enabled(self): - """Get enabled state. - - Returns: - enabled(bool): State. - """ - return self.item.enabled() - - @enabled.setter - def enabled(self, x): - """Set enabled state. - - Args: - state(bool): Set enabled state. - """ - self.connection.request_receive(self.remote, enable_atom(), x) + def tracks(self): + return self.children @property - def item_flag(self): - """Get flag. + def audio_tracks(self): + return self.children_of_type([ItemType.IT_AUDIO_TRACK]) - Returns: - name(str): flag. - """ - return self.item.flag() + @property + def video_tracks(self): + return self.children_of_type([ItemType.IT_VIDEO_TRACK]) - @item_flag.setter - def item_flag(self, x): - """Set flag. + def insert_video_track(self, name="Video Track", index=0): + """Insert video track Args: - name(str): Set flag. - """ - self.connection.request_receive(self.remote, item_flag_atom(), x) - - @property - def item_name(self): - """Get name. + name(str): Name of new track. + index(int): Position to insert Returns: - name(str): Name. + success(Item): New item """ - return self.item.name() - @item_name.setter - def item_name(self, x): - """Set name. + result = Track(ItemType.IT_VIDEO_TRACK, self.connection, self.connection.remote_spawn("Track", name, self.rate, MediaType.MT_IMAGE)) + self.insert_child(result, index) + return result - Args: - name(str): Set name. - """ - self.connection.request_receive(self.remote, item_name_atom(), x) + def insert_audio_track(self, name="Audio Track", index=-1): + """Insert audio track - def __len__(self): - """Get size. + Args: + name(str): Name of new track. + index(int): Position to insert Returns: - size(int): Size. + success(Item): New item """ - return len(self.item) + + result = Track(ItemType.IT_AUDIO_TRACK, self.connection, self.connection.remote_spawn("Track", name, self.rate, MediaType.MT_AUDIO)) + self.insert_child(result, index) + return result def insert_child(self, obj, index=-1): """Insert child obj @@ -107,10 +87,13 @@ def insert_child(self, obj, index=-1): if not isinstance(obj, UuidActor): obj = obj.uuid_actor() - return self.connection.request_receive(self.remote, insert_item_atom(), index, obj)[0] + uav = UuidActorVec() + uav.push_back(obj) + + return self.connection.request_receive(self.remote, insert_item_atom(), index, uav)[0] - def remove_child(self, index=-1): - """Remove child obj + def remove_child_at_index(self, index=-1): + """Remove child obj, removed item must be released or reparented. Args: index(int): Index to remove @@ -120,7 +103,7 @@ def remove_child(self, index=-1): """ return self.connection.request_receive(self.remote, remove_item_atom(), index)[0] - def erase_child(self, index=-1): + def erase_child_at_index(self, index=-1): """Remove and destroy child obj Args: @@ -129,35 +112,42 @@ def erase_child(self, index=-1): Returns: success(bool): Item erased """ - return self.connection.request_receive(self.remote, erase_item_atom(), index)[0] + return self.connection.request_receive(self.remote, erase_item_atom(), index, True)[0] - def move_children(self, start, count, dest): - """Move child items + def remove_child(self, child): + """Remove child obj, removed item must be released or reparented. Args: - start(int): Index of first item - count(int): Count items - dest(int): Destination index (-1 for append) + child(Item): Child to remove Returns: - success(bool): Items moved + item(Item): Item removed """ - return self.connection.request_receive(self.remote, move_item_atom(), start, count, dest)[0] + return self.connection.request_receive(self.remote, remove_item_atom(), child.uuid)[0] + def erase_child(self, child): + """Remove and destroy child obj - @property - def children(self): - """Get children. + Args: + child(Item): Child to erase Returns: - children([Gap/Track/Clip/Stack]): Children. + success(bool): Item erased """ - children = [] + return self.connection.request_receive(self.remote, erase_item_atom(), child.uuid, True)[0] - for i in self.item.children(): - children.append(create_item_container(self.connection, i)) + def move_children(self, start, count, dest): + """Move child items - return children + Args: + start(int): Index of first item + count(int): Count items + dest(int): Destination index (-1 for append) + + Returns: + success(bool): Items moved + """ + return self.connection.request_receive(self.remote, move_item_atom(), start, count, dest)[0] def children_of_type(self, types): """Get children matching types. @@ -169,33 +159,3 @@ def children_of_type(self, types): children([item]): Children. """ return [create_item_container(self.connection, i) for i in self.item.children() if i.item_type() in types] - - @property - def trimmed_range(self): - return self.item.trimmed_range() - - @property - def available_range(self): - return self.item.available_range() - - @available_range.setter - def available_range(self, x): - """Set available_range. - - Args: - x(FrameRange): Set available_range. - """ - self.connection.request_receive(self.remote, available_range_atom(), x) - - @property - def active_range(self): - return self.item.active_range() - - @active_range.setter - def active_range(self, x): - """Set active_range. - - Args: - x(FrameRange): Set active_range. - """ - self.connection.request_receive(self.remote, active_range_atom(), x) diff --git a/python/src/xstudio/api/session/playlist/timeline/track.py b/python/src/xstudio/api/session/playlist/timeline/track.py index 069b11d35..edce87648 100644 --- a/python/src/xstudio/api/session/playlist/timeline/track.py +++ b/python/src/xstudio/api/session/playlist/timeline/track.py @@ -1,12 +1,15 @@ # SPDX-License-Identifier: Apache-2.0 -from xstudio.core import UuidActor, ItemType -from xstudio.core import item_atom, insert_item_atom, enable_atom, remove_item_atom, erase_item_atom, item_name_atom, move_item_atom -from xstudio.core import move_item_atom, item_flag_atom -from xstudio.core import active_range_atom, available_range_atom -from xstudio.api.session.container import Container +from xstudio.core import insert_item_atom, remove_item_atom, erase_item_atom, move_item_atom +from xstudio.core import split_item_at_frame_atom, split_item_atom, erase_item_at_frame_atom +from xstudio.core import move_item_at_frame_atom +from xstudio.core import Uuid, actor, UuidActor, ItemType, UuidActorVec, FrameRateDuration +from xstudio.api.session.playlist.timeline.item import Item from xstudio.api.session.playlist.timeline import create_item_container +from xstudio.api.session.playlist.timeline.gap import Gap +from xstudio.api.session.playlist.timeline.clip import Clip +from xstudio.api.auxiliary import NotificationHandler -class Track(Container): +class Track(Item, NotificationHandler): """Timeline object.""" def __init__(self, item_type, connection, remote, uuid=None): @@ -21,7 +24,8 @@ def __init__(self, item_type, connection, remote, uuid=None): Kwargs: uuid(Uuid): Uuid of remote actor. """ - Container.__init__(self, connection, remote, uuid) + Item.__init__(self, connection, remote, uuid) + NotificationHandler.__init__(self, self) self.item_type = item_type @property @@ -43,75 +47,52 @@ def is_video(self): return self.item_type == ItemType.IT_VIDEO_TRACK @property - def item_flag(self): - """Get flag. - - Returns: - name(str): flag. - """ - return self.item.flag() - - @item_flag.setter - def item_flag(self, x): - """Set flag. - - Args: - name(str): Set flag. - """ - self.connection.request_receive(self.remote, item_flag_atom(), x) + def clips(self): + return self.children_of_type([ItemType.IT_CLIP]) @property - def item_name(self): - """Get name. + def gaps(self): + return self.children_of_type([ItemType.IT_GAP]) + + def __len__(self): + """Get size. Returns: - name(str): Name. + size(int): Size. """ - return self.item.name() - - @item_name.setter - def item_name(self, x): - """Set name. + return len(self.item) + def insert_gap(self, frames, index=-1): + """Insert gap Args: - name(str): Set name. - """ - self.connection.request_receive(self.remote, item_name_atom(), x) - - @property - def enabled(self): - """Get enabled state. + frames(int): Duration in frames. + index(int): Position to insert - Returns: - enabled(bool): State. + returns success(Item): New item """ - return self.item.enabled() + result = Gap(self.connection, + self.connection.remote_spawn("Gap", "Gap", FrameRateDuration(frames, self.rate)) + ) - @enabled.setter - def enabled(self, value): - """Set enabled state. + self.insert_child(result, index) - Args: - value(bool): Set enabled state. - """ - self.connection.request_receive(self.remote, enable_atom(), value) + return result - def __len__(self): - """Get size. + def insert_clip(self, media, index=-1): + """Insert gap + Args: + media(Media): Media to insert, MUST already exist in timeline. + index(int): Position to insert - Returns: - size(int): Size. + returns success(Item): New item """ - return len(self.item) + result = Clip(self.connection, + self.connection.remote_spawn("Clip", media.uuid_actor(), "") + ) - @property - def item(self): - """Get item. + self.insert_child(result, index) - Returns: - item(Item): Item for timeline. - """ - return self.connection.request_receive(self.remote, item_atom())[0] + return result def insert_child(self, obj, index=-1): """Insert child obj @@ -127,10 +108,13 @@ def insert_child(self, obj, index=-1): if not isinstance(obj, UuidActor): obj = obj.uuid_actor() - return self.connection.request_receive(self.remote, insert_item_atom(), index, obj)[0] + uav = UuidActorVec() + uav.push_back(obj) - def remove_child(self, index=-1): - """Remove child obj + return self.connection.request_receive(self.remote, insert_item_atom(), index, uav)[0] + + def remove_child_at_index(self, index=-1, count=1, add_gap=True): + """Remove child obj, removed item must be released or reparented. Args: index(int): Index to remove @@ -138,9 +122,22 @@ def remove_child(self, index=-1): Returns: item(Item): Item removed """ - return self.connection.request_receive(self.remote, remove_item_atom(), index)[0] + return self.connection.request_receive(self.remote, remove_item_atom(), index, count, add_gap)[0] + - def erase_child(self, index=-1): + def erase_frames(self, frame=0, duration=1, add_gap=True): + """Remove and destroy frames + Args: + frame(int): start frame + duration(int): Duration frames + add_gap(bool): Replace with gap + + Returns: + Transaction(JsonStore): Transaction. + """ + return self.connection.request_receive(self.remote, erase_item_at_frame_atom(), frame, duration, add_gap)[0] + + def erase_child_at_index(self, index=-1, count=1, add_gap=True): """Remove and destroy child obj Args: @@ -149,9 +146,31 @@ def erase_child(self, index=-1): Returns: success(bool): Item erased """ - return self.connection.request_receive(self.remote, erase_item_atom(), index)[0] + return self.connection.request_receive(self.remote, erase_item_atom(), index, count, add_gap)[0] + + def remove_child(self, child, add_gap=True): + """Remove child obj, removed item must be released or reparented. + + Args: + child(Item): Child to remove + + Returns: + item(Item): Child removed + """ + return self.connection.request_receive(self.remote, remove_item_atom(), child.uuid, add_gap)[0] + + def erase_child(self, child, add_gap=True): + """Remove and destroy child obj + + Args: + child(Item): Child to erase + + Returns: + success(bool): Item erased + """ + return self.connection.request_receive(self.remote, erase_item_atom(), child.uuid, add_gap)[0] - def move_children(self, start, count, dest): + def move_children(self, start, count, dest, add_gap=False): """Move child items Args: @@ -162,59 +181,66 @@ def move_children(self, start, count, dest): Returns: success(bool): Items moved """ - return self.connection.request_receive(self.remote, move_item_atom(), start, count, dest)[0] + return self.connection.request_receive(self.remote, move_item_atom(), start, count, dest, add_gap)[0] - @property - def children(self): - """Get children. + def move_frames(self, frame, duration, destination, insert=True, add_gap=False): + """Move frames + + Args: + frame(int): First frame + duration(int): Duration in frames + destination(int): Destination frame + insert(bool): Insert at destination frame, overwrite if False + add_gap(bool): Add gap at source Returns: - children([Gap/Track/Clip/Stack]): Children. + success(bool): Items moved """ - children = [] + return self.connection.request_receive(self.remote, move_item_at_frame_atom(), frame, duration, destination, insert, add_gap)[0] - for i in self.item.children(): - children.append(create_item_container(self.connection, i)) - return children - - def children_of_type(self, types): - """Get children matching types. + def split_child_at_index(self, index, frame): + """Split child obj Args: - types([ItemType]): List of item types. + index(int): Index to split + frame(int): Frame of child to split Returns: - children([item]): Children. + success(bool): Item split """ - return [create_item_container(self.connection,i) for i in self.item.children() if i.item_type() in types] + return self.connection.request_receive(self.remote, split_item_atom(), index, frame)[0] - @property - def trimmed_range(self): - return self.item.trimmed_range() + def split_child(self, child, frame): + """Split child obj - @property - def available_range(self): - return self.item.available_range() + Args: + child(Item): Child to split + frame(int): Frame of child to split - @available_range.setter - def available_range(self, x): - """Set available_range. + Returns: + success(bool): Item split + """ + return self.connection.request_receive(self.remote, split_item_atom(), child.uuid, frame)[0] + + def split(self, frame): + """Split track item at frame Args: - x(FrameRange): Set available_range. - """ - self.connection.request_receive(self.remote, available_range_atom(), x) + frame(int): Frame of track to split - @property - def active_range(self): - return self.item.active_range() + Returns: + success(bool): Item split + """ + return self.connection.request_receive(self.remote, split_item_at_frame_atom(), frame)[0] - @active_range.setter - def active_range(self, x): - """Set active_range. + def children_of_type(self, types): + """Get children matching types. Args: - x(FrameRange): Set active_range. + types([ItemType]): List of item types. + + Returns: + children([item]): Children. """ - self.connection.request_receive(self.remote, active_range_atom(), x) + return [create_item_container(self.connection,i) for i in self.item.children() if i.item_type() in types] diff --git a/python/src/xstudio/api/session/session.py b/python/src/xstudio/api/session/session.py index 3698ab8a8..1082dcfce 100644 --- a/python/src/xstudio/api/session/session.py +++ b/python/src/xstudio/api/session/session.py @@ -3,15 +3,21 @@ from xstudio.core import get_container_atom, create_group_atom, remove_container_atom, path_atom, get_media_atom from xstudio.core import rename_container_atom, create_divider_atom, media_rate_atom, playhead_rate_atom from xstudio.core import reflag_container_atom, merge_playlist_atom, copy_container_to_atom -from xstudio.core import get_bookmark_atom, save_atom, current_playlist_atom -from xstudio.core import URI, Uuid, UuidVec +from xstudio.core import get_bookmark_atom, save_atom, active_media_container_atom, current_media_atom, name_atom +from xstudio.core import viewport_active_media_container_atom +from xstudio.core import URI, Uuid, VectorUuid, item_selection_atom, type_atom from xstudio.api.session.container import Container, PlaylistTree, PlaylistItem from xstudio.api.session.playlist import Playlist from xstudio.api.session.media.media import Media from xstudio.api.session.bookmark import Bookmarks +from xstudio.api.session.playlist.subset import Subset +from xstudio.api.session.playlist.contact_sheet import ContactSheet +from xstudio.api.session.playlist.timeline import Timeline +from xstudio.api.auxiliary import NotificationHandler -class Session(Container): + +class Session(Container, NotificationHandler): """Session object.""" def __init__(self, connection, remote, uuid=None): @@ -27,6 +33,132 @@ def __init__(self, connection, remote, uuid=None): uuid(Uuid): Uuid of remote actor. """ Container.__init__(self, connection, remote, uuid) + NotificationHandler.__init__(self, self) + + @property + def selected_media(self): + """Get currently selected media. + + Returns: + Media(): Media. + """ + + result = self.connection.request_receive(self.remote, current_media_atom())[0] + media = [] + for i in result: + media.append(Media(self.connection, i.actor, i.uuid)) + + return media + + @property + def selected_containers(self): + """Get currently selected containers. + + Returns: + container(Playlist,Subset,Timelime,ContactSheet): Container. + """ + + items = self.connection.request_receive(self.remote, item_selection_atom())[0] + result = [] + + for i in items: + item_type = self.connection.request_receive(i.actor, type_atom())[0] + if item_type == "Timeline": + result.append( + Timeline(self.connection, i.actor, i.uuid) + ) + elif item_type == "Subset": + result.append( + Subset(self.connection, i.actor, i.uuid) + ) + elif item_type == "ContactSheet": + result.append( + ContactSheet(self.connection, i.actor, i.uuid) + ) + elif item_type == "Playlist": + result.append( + Playlist(self.connection, i.actor, i.uuid) + ) + + + return result + + @property + def viewed_playlist(self): + """Get currently viewed (Playlist,Subset,Timelime,ContactSheet). + + Returns: + Playlist(): Playlist. + """ + return self.viewed_container + + @property + def viewed_container(self): + """Get currently viewed container. + + Returns: + container(Playlist,Subset,Timelime,ContactSheet): Container. + """ + + result = self.connection.request_receive(self.remote, viewport_active_media_container_atom())[0] + c = Container(self.connection, result.actor) + + if c.type == "Timeline": + return Timeline(self.connection, result.actor, result.uuid) + elif c.type == "Subset": + return Subset(self.connection, result.actor, result.uuid) + elif c.type == "ContactSheet": + return ContactSheet(self.connection, result.actor, result.uuid) + + return Playlist(self.connection, result.actor, result.uuid) + + @viewed_container.setter + def viewed_container(self, container): + """Set the current, on-screen playlist + + Args: + container(Playlist,Subset,Timelime,ContactSheet): playlist, subset or timeline(sequence) + """ + self.connection.send(self.remote, viewport_active_media_container_atom(), container.uuid) + + @property + def inspected_playlist(self): + """Get currently inspected playlist/subset/timeline etc. + + Returns: + container(Playlist,Subset,Timelime,ContactSheet): Container. + """ + return self.inspected_container + + + @property + def inspected_container(self): + """Get currently inspected container. + + Returns: + container(Playlist,Subset,Timelime,ContactSheet): Container. + """ + + result = self.connection.request_receive(self.remote, active_media_container_atom())[0] + c = Container(self.connection, result.actor) + + if c.type == "Timeline": + return Timeline(self.connection, result.actor, result.uuid) + elif c.type == "Subset": + return Subset(self.connection, result.actor, result.uuid) + elif c.type == "ContactSheet": + return ContactSheet(self.connection, result.actor, result.uuid) + + return Playlist(self.connection, result.actor, result.uuid) + + @inspected_container.setter + def inspected_container(self, container): + """Set the current, *inspected* playlist + + Args: + container(Playlist,Subset,Timelime,ContactSheet): playlist, subset or timeline(sequence) + """ + self.connection.send(self.remote, active_media_container_atom(), container.uuid) @property def path(self): @@ -128,16 +260,16 @@ def merge_playlist(self, playlists, name="Combined Playlist", before=Uuid(), int Returns: playlist(Uuid,Playlist): Returns container Uuid and Playlist """ - uuids = UuidVec() + uuids = [] for i in playlists: if not isinstance(i, Uuid): i = i.uuid - uuids.push_back(i) + uuids.append(i) if not isinstance(before, Uuid): before = before.uuid - result = self.connection.request_receive(self.remote, merge_playlist_atom(), name, before, uuids)[0] + result = self.connection.request_receive(self.remote, merge_playlist_atom(), name, before, VectorUuid(uuids))[0] return (result[0], Playlist(self.connection, result[1].actor, result[1].uuid)) # return (result[0], Playlist(self.connection, result[1][1], result[1][0])) @@ -218,7 +350,7 @@ def set_on_screen_source(self, src): """ self.connection.send( self.remote, - current_playlist_atom(), + active_media_container_atom(), src.uuid_actor().actor ) @@ -290,18 +422,18 @@ def copy_containers_to(self, playlist, containers, before=Uuid(), into=False): Returns: uuids(list[Uuid]): Returns list of new container uuids. """ - uuids = UuidVec() + uuids = [] for i in containers: if not isinstance(i, Uuid): i = i.uuid - uuids.push_back(i) + uuids.append(i) if not isinstance(before, Uuid): before = before.uuid if not isinstance(playlist, Uuid): playlist = playlist.uuid - return self.connection.request_receive(self.remote, copy_container_to_atom(), playlist, uuids, before, into)[0] + return self.connection.request_receive(self.remote, copy_container_to_atom(), playlist, VectorUuid(uuids), before, into)[0] def get_media(self): """Return all media over all playlists @@ -332,4 +464,4 @@ def save_as(self, path): if not isinstance(path,URI): path = URI(path) - return self.connection.request_receive(self.remote, save_atom(), path)[0] + return self.connection.request_receive(self.remote, save_atom(), path)[0] \ No newline at end of file diff --git a/python/src/xstudio/cli/control.py b/python/src/xstudio/cli/control.py index c93767e51..f3ee81fbb 100644 --- a/python/src/xstudio/cli/control.py +++ b/python/src/xstudio/cli/control.py @@ -38,12 +38,6 @@ def control_main(): default=45500 ) - parser.add_argument( - "-r", "--require-sync", action="store_true", - help="Downgrade FULL to SYNC.", - default=False - ) - parser.add_argument( "-i", "--info", action="store_true", help="Print connection info.", @@ -67,19 +61,19 @@ def control_main(): m = RemoteSessionManager(remote_session_path()) s = m.find(args.session) conn = Connection(debug=args.debug) - conn.connect_remote(s.host(), s.port.port(), args.require_sync) + conn.connect_remote(s.host(), s.port.port()) elif args.host: conn = Connection(debug=args.debug) - conn.connect_remote(args.host, args.port, args.require_sync) + conn.connect_remote(args.host, args.port) else: conn = Connection(debug=args.debug) - conn.connect_remote("localhost", args.port, args.require_sync) + conn.connect_remote("localhost", args.port) if args.info: print( - " App: {}\n API: {}\nVersion: {}\nSession: {}\n".format( - conn.app_type, conn.api_type, + " App: {}\n Version: {}\nSession: {}\n".format( + conn.app_type, conn.app_version, conn.api.app.session.name ) ) diff --git a/python/src/xstudio/cli/inject.py b/python/src/xstudio/cli/inject.py index 149f3fa35..c2d4870f5 100644 --- a/python/src/xstudio/cli/inject.py +++ b/python/src/xstudio/cli/inject.py @@ -50,14 +50,14 @@ def inject_main(): m = RemoteSessionManager(remote_session_path()) s = m.find(args.session) conn = Connection() - conn.connect_remote(s.host(), s.port.port(), False) + conn.connect_remote(s.host(), s.port.port()) elif args.host: conn = Connection() - conn.connect_remote(args.host, args.port, False) + conn.connect_remote(args.host, args.port) else: conn = Connection() - conn.connect_remote("localhost", args.port, False) + conn.connect_remote("localhost", args.port) pl = conn.api.app.session.create_playlist("Injected Media")[1] diff --git a/python/src/xstudio/cli/xstudiopy_startup.py b/python/src/xstudio/cli/xstudiopy_startup.py index 14abef9d0..311288301 100644 --- a/python/src/xstudio/cli/xstudiopy_startup.py +++ b/python/src/xstudio/cli/xstudiopy_startup.py @@ -8,10 +8,6 @@ __DEBUG = False -__SYNC = os.environ.get("XSTUDIOPY_SYNC","0").lower() in [ - 'true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly', 'uh-huh' - ] - if os.environ.get("XSTUDIOPY_DEBUG", "False") == "True": __DEBUG = True @@ -21,8 +17,7 @@ ) XSTUDIO.connect_remote( os.environ.get("XSTUDIOPY_HOST", "localhost"), - int(os.environ.get("XSTUDIOPY_PORT", "45500")), - __SYNC + int(os.environ.get("XSTUDIOPY_PORT", "45500")) ) elif os.environ.get("XSTUDIOPY_SESSION", None): m = RemoteSessionManager(remote_session_path()) @@ -34,17 +29,13 @@ if s is not None: XSTUDIO.connect_remote( s.host(), - s.port(), - __SYNC + s.port() ) else: print("Remote session not found") else: m = RemoteSessionManager(remote_session_path()) - if __SYNC: - s = m.first_sync() - else: - s = m.first_api() + s = m.first_api() XSTUDIO = Connection( debug=__DEBUG @@ -52,8 +43,7 @@ if s is not None: XSTUDIO.connect_remote( s.host(), - s.port(), - __SYNC + s.port() ) else: print("No remote session found") diff --git a/python/src/xstudio/connection/__init__.py b/python/src/xstudio/connection/__init__.py index eb8d80368..e00246bd5 100644 --- a/python/src/xstudio/connection/__init__.py +++ b/python/src/xstudio/connection/__init__.py @@ -4,26 +4,22 @@ Controls xStudio connections. """ -from xstudio.core import get_api_mode_atom, absolute_receive_timeout -from xstudio.core import request_connection_atom, get_sync_atom, version_atom -from xstudio.core import get_application_mode_atom, authorise_connection_atom, broadcast_down_atom +from xstudio.core import absolute_receive_timeout +from xstudio.core import version_atom, authenticate_atom +from xstudio.core import get_application_mode_atom, broadcast_down_atom from xstudio.core import join_broadcast_atom, exit_atom, api_exit_atom, leave_broadcast_atom, get_event_group_atom from xstudio.core import error, Link +from xstudio.core import XSTUDIO_LOCAL_PLUGIN_PATH from xstudio.api import API -from xstudio.sync_api import SyncAPI from xstudio.core import RemoteSessionManager, remote_session_path import uuid import time -import traceback import os from pathlib import Path from threading import Thread class Connection(object): """Handle connection to xstudio.""" - API_TYPE_FULL = "FULL" - API_TYPE_SYNC = "SYNC" - API_TYPE_GATEWAY = "GATEWAY" APP_TYPE_XSTUDIO = "XSTUDIO" APP_TYPE_XSTUDIO_GUI = "XSTUDIO_GUI" @@ -37,7 +33,6 @@ def __init__(self, auto_connect=False, background_processing=False, debug=False, """ self.link = Link() self.connected = False - self.api_type = None self.app_type = None self.app_version = None self._api = None @@ -88,6 +83,20 @@ def api_handler(self, sender, req_id, event): (type(event[0]) == type(exit_atom()))): self.disconnect() + def add_event_callback(self, remote, handler): + """Add a callback function to recieve event broadcasts from some actor + + Args: + remote (Actor): Owner of broadcast actor. + handler (func(sender, req_id, event)): Handler to install. + + Returns: + handler_id(str): Handler Id. + """ + agrp = self.get_broadcast(remote) + return self.link.add_message_callback( + agrp, handler + ) def add_handler(self, remote, handler): """Add handler to remote broadcasts. @@ -244,7 +253,7 @@ def handle_broadcast(self, event): pass # print("ignored broadcast", sender, req_id, event[:len(event)-2]) - def get_key_from_stdin(self, lock): + def get_authentication_from_stdin(self): """Request lock key from user. Args: @@ -253,7 +262,7 @@ def get_key_from_stdin(self, lock): Returns: user_input(str): String entered by user. """ - return input("Enter key for lock {}: ".format(lock)) + return input("Enter script key or userpassword for remote session {}:{}: ".format(self.link.host, self.link.port)) def connect_local(self, actor): """Connect to in-process actor. @@ -268,25 +277,21 @@ def connect_local(self, actor): else: raise RuntimeError("Failed to connect") - def connect_remote_auto(self, session=None, sync_mode=False, sync_key_callback=None): + def connect_remote_auto(self, session=None, authentication_callback=None): """Connect to xStudio using session file. Kwargs: session (str): Session name. - sync_mode (bool): Sync API. - sync_key_callback (func): Callback for sync key. + authentication_callback (func): Callback for authentication. """ r = RemoteSessionManager(remote_session_path()) if session is None: - if sync_mode: - s = r.first_sync() - else: - s = r.first_api() + s = r.first_api() else: s = m.find(session) - self.connect_remote(s.host(), s.port(), sync_mode, sync_key_callback) + self.connect_remote(s.host(), s.port(), authentication_callback) - def connect_remote(self, host, port, sync_mode=False, sync_key_callback=None): + def connect_remote(self, host, port, authentication_callback=None): """Connect to xStudio using host/port. Args: @@ -294,52 +299,43 @@ def connect_remote(self, host, port, sync_mode=False, sync_key_callback=None): port (int): Host port number. Kwargs: - sync_mode (bool): Sync API. - sync_key_callback (func): Callback for sync key. + authentication_callback (func): Callback for authentication. """ - if sync_key_callback is None: - sync_key_callback = self.get_key_from_stdin + if authentication_callback is None: + authentication_callback = self.get_authentication_from_stdin self.disconnect() connected = self.link.connect_remote(host, port) if connected: - self.negotiate(sync_mode, sync_key_callback) + self.negotiate(authentication_callback) else: raise RuntimeError("Failed to connect") - def negotiate(self, sync_mode=False, sync_key_callback=None): - """Negotiate connection, also handles sync lock/key. + def negotiate(self, authentication_callback=None): + """Negotiate connection, also handles authentication. Kwargs: - sync_mode (bool): Sync API. - sync_key_callback (func): Callback for sync key. + authentication_callback (func): Callback for authentication. """ + self.app_version = self.request_receive(self.link.remote(), version_atom())[0] + self.app_type = self.request_receive(self.link.remote(), get_application_mode_atom())[0] - self.api_type = self.request_receive(self.link.remote(), get_api_mode_atom())[0] - - if self.api_type == self.API_TYPE_GATEWAY: - if not sync_mode: - raise RuntimeError("Connection failed, this is the wrong port for FULL API") - - # need to authorise.. - lock = self.request_receive(self.link.remote(), request_connection_atom())[0] - remote = self.request_receive(self.link.remote(), - authorise_connection_atom(), - lock, - sync_key_callback(lock) - )[0] - - self.link.set_remote(remote) - self.api_type = self.request_receive(self.link.remote(), get_api_mode_atom())[0] - elif self.api_type == self.API_TYPE_FULL and sync_mode: - self.link.set_remote(self.request_receive(self.link.remote(), get_sync_atom())[0]) - self.api_type = self.request_receive(self.link.remote(), get_api_mode_atom())[0] + try: + self.link.set_remote(self.request_receive(self.link.remote(), authenticate_atom())[0]) + except: + if authentication_callback: + auth = authentication_callback().split() + if len(auth) == 2: + self.link.set_remote(self.request_receive(self.link.remote(), authenticate_atom(), auth[0], auth[1])[0]) + elif len(auth) == 1: + self.link.set_remote(self.request_receive(self.link.remote(), authenticate_atom(), auth[0])[0]) + else: + raise + else: + raise - self.app_version = self.request_receive(self.link.remote(), version_atom())[0] - self.app_type = self.request_receive(self.link.remote(), get_application_mode_atom())[0] - if self.api_type: - self.connected = True + self.connected = True # clear old handlers.. self.broadcast_channel = {} @@ -451,24 +447,6 @@ def send(self, *args): if self.connected: self.link.send(*args) - # def join(self, group): - # """Join message group - - # Args: - # group (group): Group to join. - # """ - # if self.api_type: - # self.link.join(group) - - # def leave(self, group): - # """Leave message group - - # Args: - # group (group): Group to leave. - # """ - # if self.api_type: - # self.link.leave(group) - def dequeue_messages(self, timeout=10): """Pop waiting messages""" while True: @@ -479,14 +457,6 @@ def dequeue_messages(self, timeout=10): except TimeoutError as e: break - def caf_message_to_tuple(self, caf_message): - """Decompose a CAF message object into a tuple of message params. - - Args: - caf_message (CafMessage): A message originating in xstudio backend. - """ - return self.link.tuple_from_message(caf_message) - def _dequeue_messages(self, timeout_milli, watch_for = None): """Pop response messages off stack. @@ -531,10 +501,6 @@ def _dequeue_messages(self, timeout_milli, watch_for = None): def disconnect(self): """Disconnect from xstudio""" - if self.api_type == "SYNC": - self.link.send_exit(self.link.remote()) - - self.api_type = None self.app_type = None self.app_version = None self._api = None @@ -547,13 +513,10 @@ def api(self): """Access API object. Returns: - API(API or SyncAPI): API object or None + API(API): API object or None """ if self._api is None: - if self.api_type == "SYNC": - self._api = SyncAPI(self) - elif self.api_type == "FULL": - self._api = API(self) + self._api = API(self) return self._api @@ -584,6 +547,33 @@ def load_python_plugins(self): for search_path in search_paths: self.load_plugins_in_path(search_path) + # XSTUDIO_LOCAL_PLUGIN_PATH points us at the folder where python plugins + # installed/deployed as part of xstudio app bundle + self.load_plugins_in_path(XSTUDIO_LOCAL_PLUGIN_PATH) + + try: + + # use python_plugin_search_folders and + # user_python_plugin_search_folders prefs to scan for python plugins + + import json + extra_paths = json.loads( + self.api.global_store.value( + "/core/python/python_plugin_search_folders").dump() + ) + extra_paths += json.loads( + self.api.global_store.value( + "/core/python/user_python_plugin_search_folders").dump() + ) + + for extra_path in extra_paths: + if os.path.isdir(extra_path): + self.load_plugins_in_path(extra_path) + + except Exception as e: + print("Error in load_python_plugins: {}".format(e)) + + def load_plugins_in_path(self, path): """Load plugins found under the given directoy path @@ -595,6 +585,8 @@ def load_plugins_in_path(self, path): # search path mechanisms return + print("Scanning folder {} for python plugins.".format(path)) + for p in Path(path).iterdir(): if p.is_dir(): self.load_plugin_from_path(path, p.name) @@ -605,9 +597,11 @@ def load_plugin_from_path(self, path, plugin_name): Args: path (Path): Path to a directory on filesystem """ - import importlib.util - import sys + try: + import importlib.util + import sys + sys.path.insert(0, path) spec = importlib.util.find_spec(plugin_name) if spec is not None: @@ -616,11 +610,7 @@ def load_plugin_from_path(self, path, plugin_name): spec.loader.exec_module(module) self.plugins[path + plugin_name] = module.create_plugin_instance(self) else: - print ("Error loading plugin \"{1}\" from \"{0}\" - not python importable.".format( - path, plugin_name)) + print ("Error loading plugin \"{0}\" from \"{0}\" - not python importable.".format( + path)) except Exception as e: - print ("Error loading plugin \"{0}\" from \"{1}\" - : {2}".format( - plugin_name, - path, - e)) - print (traceback.format_exc()) + print (str(e)) \ No newline at end of file diff --git a/python/src/xstudio/gui/__init__.py b/python/src/xstudio/gui/__init__.py index cfe6282e9..be1edcc13 100644 --- a/python/src/xstudio/gui/__init__.py +++ b/python/src/xstudio/gui/__init__.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 -"""PySide2 shim. +"""PySide6 shim. -Import pyside2 shim. +Import pyside6 shim. """ -from .__pyside2_xstudio import * \ No newline at end of file +from .__pyside6_xstudio import * \ No newline at end of file diff --git a/python/src/xstudio/plugin/__init__.py b/python/src/xstudio/plugin/__init__.py index cd1436d56..bc02f0218 100644 --- a/python/src/xstudio/plugin/__init__.py +++ b/python/src/xstudio/plugin/__init__.py @@ -1,7 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -"""PySide2 shim. - -Import pyside2 shim. -""" from xstudio.plugin.plugin_base import PluginBase +from xstudio.plugin.hud_plugin import HUDPlugin +from xstudio.plugin.viewport_layout_plugin import ViewportLayoutPlugin diff --git a/python/src/xstudio/plugin/hud_plugin.py b/python/src/xstudio/plugin/hud_plugin.py new file mode 100644 index 000000000..bbae393a5 --- /dev/null +++ b/python/src/xstudio/plugin/hud_plugin.py @@ -0,0 +1,115 @@ +from xstudio.plugin.plugin_base import PluginBase +from xstudio.core import hud_settings_atom, HUDElementPosition +import pathlib + +class HUDPlugin(PluginBase): + + """Base class for HUD (Viewport Overlay) plugins""" + + def __init__( + self, + connection, + name, + qml_folder="", + position_in_hud_list=0.0 + ): + """Create a plugin + + Args: + name(str): Name of HUD plugin - this will appear in the HUD toolbar menu + """ + PluginBase.__init__(self, connection, name, "HUDPlugin", qml_folder=qml_folder) + + # base class has added an attribute with the same name as the plugin + # name. This is how toggles for hud plugins are added to the HUD button + # in the viewport toolbar. The 'toolbar_position' role data is used + # to sort the list of HUD items + self.toggle_attr = self.get_attribute(name) + self.toggle_attr.set_role_data("toolbar_position", position_in_hud_list) + + @property + def enabled(self): + return self.toggle_attr.value() + + def add_hud_settings_attribute( + self, + attr + ): + """Add an attribute to the 'settings' for the HUD plugin. This means + the attribute will appear in the settings interface for the plugin + via the HUD toolbar button. + + Args: + attr_uuid(ModuleAttribute): The attribute to be exposed in the + settings dialog. + """ + attr.expose_in_ui_attrs_group(self.name + " Settings") + + def hud_element_qml( + self, + qml_code, + hud_position=HUDElementPosition.FullScreen + ): + """Set the QML code for the visual element of the HUD that appears in + the xSTUDIO viewport. + + Args: + qml_code(str): The qml code required to instance a visual element + that will be displayed over the viewport + hud_position(HUDElementPosition): The position in the viewport where the visual + element will be. Must be one of (HUDElementPosition.FullScreen, + HUDElementPosition.BottomLeft, HUDElementPosition.BottomCentre, + HUDElementPosition.TopLeft etc.). The FullScreen option requires that + the visual element manages its own transformation to draw + itself over the viewport as required. + """ + hud_attr = self.get_attribute(self.name) + + # if qml code is being added as attribute data and we also have qml_folder + # set we add an import directive to the qml code with the path to the + # plugin location. This allows plugins with qml to be relocatable. + if self.qml_folder: + + uri = pathlib.Path( + "{0}/{1}".format( + self.plugin_path, + self.qml_folder)).as_uri() + + qml_code =\ + "import \"{0}\"\n{1}".format( + uri, + qml_code + ) + + self.connection.send( + self.remote, + hud_settings_atom(), + qml_code, + hud_position + ) + + + def set_custom_settings_qml(self, qml_code): + """Add qml for a custom dialog to control the settings for a HUD item + + Args: + qml_code(str): The qml code required to instance a dialog that will + allow the user to adjust the settings for your HUD item.""" + + # Note - HUD Plugins always have an attribute with the same name as + # the plugin itself. It's this attribute that toggles the HUD item on + # and off in the HUD toolbar button. We can also add custom qml to this + # attribute if the plugin author wants to provide a custom settings + # dialog. + if self.qml_folder: + uri = pathlib.Path( + "{0}/{1}".format( + self.plugin_path, + self.qml_folder)).as_uri() + + qml_code =\ + "import \"{0}\"\n{1}".format( + uri, + qml_code + ) + self.get_attribute(self.name).set_role_data("user_data", qml_code) \ No newline at end of file diff --git a/python/src/xstudio/plugin/plugin_base.py b/python/src/xstudio/plugin/plugin_base.py index 621a8d770..d874a63f8 100644 --- a/python/src/xstudio/plugin/plugin_base.py +++ b/python/src/xstudio/plugin/plugin_base.py @@ -1,18 +1,14 @@ # SPDX-License-Identifier: Apache-2.0 from xstudio.api.module import ModuleBase from xstudio.api.session.playhead import Playhead -from xstudio.core import JsonStore, Uuid +from xstudio.core import JsonStore, Uuid, AttributeRole from xstudio.api.auxiliary.helpers import get_event_group from xstudio.core import spawn_plugin_base_atom, viewport_playhead_atom from xstudio.core import get_global_playhead_events_atom, show_message_box_atom import sys import os import traceback - -try: - import XStudioExtensions -except: - XStudioExtensions = None +import pathlib def make_simple_string(string_in): import re @@ -39,8 +35,9 @@ def __init__( connection.api.plugin_manager.remote, spawn_plugin_base_atom(), name, - JsonStore()) - self.uuid = a[1] + JsonStore(), + base_class_name) + remote = a[0] ModuleBase.__init__(self, connection, remote) @@ -52,6 +49,9 @@ def __init__( sys.modules[self.__module__].__file__ ) ) + self.qml_item_attrs = {} + + self.user_attr_handler_ = None def add_attribute( self, @@ -76,11 +76,17 @@ def add_attribute( """ + # if qml code is being added as attribute data and we also have qml_folder + # set we add an import directive to the qml code with the path to the + # plugin location. This allows plugins with qml to be relocatable. if "qml_code" in attribute_role_data and self.qml_folder: - attribute_role_data["qml_code"] =\ - "import \"file://{0}/{1}\"\n{2}".format( + uri = pathlib.Path( + "{0}/{1}".format( self.plugin_path, - self.qml_folder, + self.qml_folder)).as_uri() + attribute_role_data["qml_code"] =\ + "import \"{0}\"\n{1}".format( + uri, attribute_role_data["qml_code"] ) @@ -120,7 +126,7 @@ def popup_message_box( close_button(bool): Add a close button to the box autohide_timeout_secs(int): Optional timeout to auto-hide the message box """ - app = self.connection.api._app + app = self.connection.api.app cp = self.connection.send( app.remote, show_message_box_atom(), @@ -128,4 +134,56 @@ def popup_message_box( message_body, close_button, int(autohide_timeout_secs) - ) \ No newline at end of file + ) + + def create_qml_item( + self, + qml_item, + callback_fn=None + ): + """Create and show a qml item (typically a pop-up window, dialog etc.). + Args: + qml_item(str): The class name for the item to be created + callback_fn(method): Callback function that receives data from the + qml item (e.g. text entered by the user). On the qml side the + item can trigger the callback by calling xstudio_callback(JSON) + function somewhere in the qml signal handlers etc. + """ + + attr_name = "QML item {}".format(hash(qml_item)) + + try: + attr = self.get_attribute(attr_name) + except: + attr = None + + if not attr: + attr = self.add_attribute( + attr_name, + attribute_value = False, + attribute_role_data={ + "qml_code": qml_item, + "groups": ["dynamic qml items"], + "enabled": False + }) + + if callback_fn: + self.qml_item_attrs[attr.uuid] = (attr, callback_fn) + # 'attr_enabled' controls the visibility of the widget + attr.set_role_data("attr_enabled", True) + + def attribute_changed( + self, + attribute, + role + ): + + # check if 'CallbackData' has been set - if the attribute is one of our + # qml_item_attrs then we execute the associated callback + if role == AttributeRole.CallbackData and \ + attribute.uuid in self.qml_item_attrs and \ + attribute.role_data("callback_data") != {}: + self.qml_item_attrs[attribute.uuid][1](attribute.role_data("callback_data")) + # now reset the callback data so if the callback is called again with + # the same data as before we still get an 'attribute_changed' signal + attribute.set_role_data("callback_data", {}) \ No newline at end of file diff --git a/python/src/xstudio/plugin/viewport_layout_plugin.py b/python/src/xstudio/plugin/viewport_layout_plugin.py new file mode 100644 index 000000000..f1ced1010 --- /dev/null +++ b/python/src/xstudio/plugin/viewport_layout_plugin.py @@ -0,0 +1,113 @@ +from xstudio.plugin.plugin_base import PluginBase +from xstudio.core import viewport_layout_atom +from xstudio.core import AssemblyMode, AutoAlignMode +from xstudio.core import JsonStore +import json + +class ViewportLayoutPlugin(PluginBase): + + """Base class for Viewport Layout plugins""" + + def __init__( + self, + connection, + name, + qml_folder="", + ): + """Create a plugin + + Args: + name(str): Name of Viewport Layout plugin + """ + PluginBase.__init__( + self, + connection, + name, + "ViewportLayoutPlugin", + qml_folder=qml_folder + ) + + # subscribe to the event group of the remote actor (which is a + # ViewportLayoutPlugin). + self.subscribe_to_event_group( + self, + self.layout_callback) + + def add_layout_mode(self, name, menu_position, playhead_assembly_mode, auto_align_mode=AutoAlignMode.AAM_ALIGN_OFF): + """Add a viewport layout mode + + Args: + name(str): Name of the layout. This must be unique and not one that + has already been added by another plugin + menu_position(float): Position in the 'Compare' toobar menu. + playhead_assembly_mode (xstudio.core.AssemblyMode): The playhead + assembly mode. Can be AM_STRING for stringing together the selected + media to play in sequence. AM_ONE means only one image is needed for + the layout. AM_ALL means all selected media is decoded at the + same time and delivered for rendering to the viewport. + """ + self.connection.send( + self.remote, + viewport_layout_atom(), + name, + menu_position, + playhead_assembly_mode, + auto_align_mode) + + def add_layout_settings_attribute( + self, + attr, + layout_name + ): + """Add an attribute to the 'settings' for the lahyout plugin. This means + the attribute will appear in the settings interface for the plugin + via the Compare toolbar button. + + Args: + attr(Attribute): The attribute to be exposed in the + settings dialog. + """ + self.connection.send( + self.remote, + viewport_layout_atom(), + layout_name, + attr.uuid) + + def do_layout(self, layout_name, image_set_data): + """Re-implement this function to do your custom layout. You must return + a tuple of 3 elements: + 0: list of tuples of 3 floats (xtranslate, ytranslate, scale), one for + each image in image_set_data, whether or not it will be drawn + 1: list if ints (indexes of images that will be drawn) + 2: float (the aspect ratio of the layout) + + Args: + layout_name(str): Name of the layout. + image_set_data(json): Information about the image set that needs + to be laid out. The json is an array, one entry for each image + to be displayed. Each entry provides the pixels size of the image + and the pixel aspect ratio. + + Return: + tuple(list(tuple(float, float, float)), list(int), float): + """ + return ([(0.0, 0.5, 0.5) for a in image_set_data["image_info"]], [0], 16.0/9.0) + + def layout_callback(self, args): + + if len(args) == 4 and type(args[0]) == viewport_layout_atom and\ + type(args[1]) == str and type(args[2]) == JsonStore: + layout = self.do_layout(args[1], json.loads(str(args[2]))) + j = { + "transforms": layout[0], + "image_draw_order": layout[1], + "layout_aspect_ratio": layout[2], + "hash": args[3] + } + # horrible. The has is an int64 ... JsonStore won't convert. + j = json.loads(json.dumps(j)) + self.connection.send( + self.remote, + viewport_layout_atom(), + args[1], + JsonStore(j)) \ No newline at end of file diff --git a/python/src/xstudio/sync_api/__init__.py b/python/src/xstudio/sync_api/__init__.py deleted file mode 100644 index 9d2dc7ff5..000000000 --- a/python/src/xstudio/sync_api/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -from xstudio.common_api import CommonAPI - -class SyncAPI(CommonAPI): - """We use this as a base for the SYNC API connection.""" - - def __init__(self, connection): - """Hold connection handle. - - Args: - connection (object): Hold connection object. - - """ - super(SyncAPI, self).__init__(connection) - diff --git a/python/src/xstudiopy b/python/src/xstudiopy old mode 100755 new mode 100644 index 7099cd8a6..a9804c798 --- a/python/src/xstudiopy +++ b/python/src/xstudiopy @@ -8,7 +8,6 @@ function help echo " -s SESSION, --session SESSION Session name (DEFAULT: '')" echo " -h HOST, --host HOST Host xstudio is running on (DEFAULT: '')" echo " -p, --port Port to connect to (DEFAULT: 45500)" - echo " --sync Force SYNC mode (DEFAULT: False)" echo " -e, --execute Execute command and exit" echo " --help show this help message and exit" echo " -d, --debug Print debug output" @@ -20,7 +19,6 @@ DEBUG=False SESSION= HOST= PORT=45500 -SYNC=False EXECUTE= SOURCE="${BASH_SOURCE[0]}" @@ -61,9 +59,6 @@ case $key in PORT="$2" shift ;; - --sync) - SYNC=True - ;; --help) help ;; @@ -89,7 +84,6 @@ export XSTUDIOPY_DEBUG=$DEBUG export XSTUDIOPY_SESSION=$SESSION export XSTUDIOPY_HOST=$HOST export XSTUDIOPY_PORT=$PORT -export XSTUDIOPY_SYNC=$SYNC export XSTUDIOPY_EXECUTE=$EXECUTE if [[ -z "$EXECUTE" ]]; then diff --git a/python/test/test_api.py b/python/test/test_api.py index 73d811f8c..f5bed076b 100644 --- a/python/test/test_api.py +++ b/python/test/test_api.py @@ -4,9 +4,6 @@ def test_type(spawn): assert spawn.app_type == spawn.APP_TYPE_XSTUDIO -def test_api_type(spawn): - assert spawn.api_type == spawn.API_TYPE_FULL - def test_api_session(spawn): assert isinstance(spawn.api.session, xstudio.api.session.Session) diff --git a/retired/XsAddMediaDialog.qml b/retired/XsAddMediaDialog.qml deleted file mode 100644 index ce75424d4..000000000 --- a/retired/XsAddMediaDialog.qml +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Dialogs 1.0 -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudio 1.0 - -FileDialog { - id: media_dialog - title: "Select media files" - folder: session.pathNative ? XsUtils.stem(session.path.toString()).replace("localhost","") : shortcuts.home - - nameFilters: [ "Media files ("+helpers.validMediaExtensions()+")", "All files (*)" ] - selectExisting: true - selectMultiple: true - - function newItem(parent, obj, value) { - if(obj.selected && obj.loadMedia) { - Future.promise(obj.loadMediaFuture(value) - ).then(function(quuids){}) - return true - } - return false - } - - onAccepted: { - // check for empty session.. - if(app_window.session.itemModel.empty()) { - app_window.session.createPlaylist() - } - for (var i = 0; i < media_dialog.fileUrls.length; i++) { - XsUtils.forFirstItem(app_window.session, null, newItem, media_dialog.fileUrls[i]) - } - } - onRejected: { - } -} diff --git a/retired/XsCloseableTab.qml b/retired/XsCloseableTab.qml deleted file mode 100644 index e81a6dd5a..000000000 --- a/retired/XsCloseableTab.qml +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.5 - -import xStudio 1.0 - -Rectangle { - - id: tabButton - - anchors.top: parent ? parent.top : undefined - anchors.bottom: parent ? parent.bottom : undefined - color: { - if (isCurrentIndex) { - return XsStyle.mainBackground - } else { - return Qt.darker(XsStyle.mainBackground,1.2) - } - } - - Rectangle { - anchors.fill: parent - color: "white" - opacity: 0.2 - visible: mouseArea.containsMouse || mouseCloseArea.containsMouse - } - - Rectangle { - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - color: XsStyle.tabSeparatorColor - width: XsStyle.tabSeparatorWidth - visible: player_tabs.get(0).tab_id != tab_id - } - - property bool isCurrentIndex: tab_id == playerWidget.currentTabId - property bool canClose: player_tabs.count > 1 - property var tabBar - - Timer { - // remove the item after the width animation. - id: destroyTimer - running: false - repeat: false - interval: 200 - onTriggered: { - player_tabs.removeTab(tab_id) - } - } - - Label { - id: tabButtonText - anchors.fill: parent - text: source ? source.fullName : "" - font.pixelSize: XsStyle.sessionBarFontSize - font.family: XsStyle.controlTitleFontFamily - font.hintingPreference: Font.PreferNoHinting - font.weight: Font.DemiBold - color: isCurrentIndex?XsStyle.hoverColor:XsStyle.mainColor - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - XsMenu { - - title: { "Playlist"} - // place this menu so it shows just next to the '+' - // button - - id: playlistsMenu - Instantiator { - model: session.playlistNames - delegate: XsMenuItem { - mytext: modelData["text"] - onTriggered: { - session.switchOnScreenSource(modelData["uuid"]) - session.setSelection([modelData["uuid"]]) - } - } - onObjectAdded: playlistsMenu.insertItem(index, object) - onObjectRemoved: playlistsMenu.removeItem(object) - } - } - - - MouseArea { - id: mouseArea - cursorShape: Qt.PointingHandCursor - anchors.fill: tabButtonText - hoverEnabled: true - - onHoveredChanged: { - if (mouseArea.containsMouse) { - if (isCurrentIndex) { - status_bar.normalMessage("Current tab " + tabButtonText.text) - } else { - status_bar.normalMessage("Switch to tab " + tabButtonText.text) - } - } else { - status_bar.clearMessage() - } - } - onClicked: { - if (playerWidget.currentTabId == tab_id) { - playlistsMenu.x = mouse.x - playlistsMenu.y = mouse.y - playlistsMenu.toggleShow() - } else { - playerWidget.currentTabId = tab_id - } - } - } - - Label { - id: closeX - text: 'X' - width: tabButton.height * .75 - height: width - font.pixelSize: 8 - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - color: mouseCloseArea.containsMouse?XsStyle.hoverColor:isCurrentIndex?XsStyle.hoverColor:XsStyle.mainColor - anchors.right: parent.right - anchors.rightMargin: 5 - anchors.verticalCenter: parent.verticalCenter - visible: canClose && (mouseArea.containsMouse || mouseCloseArea.containsMouse) - background: Rectangle { - anchors.fill: parent - radius: closeX.height - color: mouseCloseArea.containsMouse ? "#933" : "transparent" - } - } - - MouseArea { - id: mouseCloseArea - cursorShape: Qt.PointingHandCursor - anchors.fill: closeX - hoverEnabled: canClose - onClicked: { - if (canClose) { - tabButton.width = 0 - playerWidget.switchToAlternativeTab(tab_id) - destroyTimer.start() - } - } - onHoveredChanged: { - if (mouseCloseArea.containsMouse) { - status_bar.normalMessage("Close tab " + tabButton.text) - } else { - status_bar.clearMessage() - } - } - } -} \ No newline at end of file diff --git a/retired/XsMediaPaneListView.qml b/retired/XsMediaPaneListView.qml deleted file mode 100644 index eee5bda35..000000000 --- a/retired/XsMediaPaneListView.qml +++ /dev/null @@ -1,772 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.12 -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudio 1.0 - -Flickable { - - id: media_list - contentHeight: mediaList.length*item_height - flickableDirection: Flickable.VerticalFlick - //clip: true - - property var source: session.selectedSource - // property var source: session.selectedSource ? session.selectedSource : session.onScreenSource - property var mediaList: source ? source.mediaList ? source.mediaList : [] : [] - property var mediaModel: source ? source.mediaModel ? source.mediaModel : [] : [] - property var header - property var dragDropAnimationDuration: 100 - property var item_height: header.item_height - property var selection_uuids: selectionFilter ? selectionFilter.selectedMediaUuids : [] - property var previousCompareMode: -1 - property var playhead: sessionWidget.viewport.playhead - property var selectionFilter: source ? source.selectionFilter : undefined - property var firstMediaItemInSelection - - property var currentOnScreenMediaUuid: playhead.media ? playhead.media.uuid : undefined - property alias mediaMenu: media_menu - - onItem_heightChanged: { - for (var idx = 0; idx < mediaList.length; idx++) { - if(repeater.itemAt(idx)) - repeater.itemAt(idx).y = item_height * idx - } - autoscroll(true, currentOnScreenMediaUuid) - } - - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AlwaysOn - } - - Rectangle { - color: XsStyle.mainBackground - width: media_list.width - height: media_list.height - x: 0 - y: contentY - } - - // function jumpToPreviousSource() { - // console.log("jumpToPreviousSource") - // if (!playhead.jumpToPreviousSource()) { - // selectionFilter.moveSelectionByIndex(-1) - // } - // } - - // function jumpToNextSource() { - // console.log("jumpToNextSource") - - // if (!playhead.jumpToNextSource()) { - // selectionFilter.moveSelectionByIndex(1) - // } - // } - - function selectSingleItem(item_idx) { - if (selection_uuids.length == 1 && mediaList[item_idx].uuid == selection_uuids[0] ) { - // The user has clicked on a single, already selected item. We do not allow - // nothing to be selected so pass - return - } - - var the_item = repeater.itemAt(item_idx) - if (the_item.selected) { - playhead.jumpToSource(mediaList[item_idx].uuid) - return - } - - // user has clicked outside of any existing selection - selectionFilter.newSelection([mediaList[item_idx].uuid]) - } - - function selectAll() { - var uuids = [] - for (var idx = 0; idx < mediaList.length; idx++) { - uuids.push(mediaList[idx].uuid) - } - selectionFilter.newSelection(uuids) - } - - function deselectAll() { - selectionFilter.newSelection([currentOnScreenMediaUuid]) - } - - function sortAlphabetically() { - if (source) source.sortAlphabetically() - if (playhead) { - // this refreshes the whole list, or it can get in a muddle due - // to previous drag-drop ordering not matching following the - // sortAlphabetically - var all_uuids = [] - for (var i = 0; i < mediaList.length; ++i) { - all_uuids.push(mediaList[i].uuid) - } - source.dragDropReorder( - all_uuids, - "" - ) - mouse_area.dragging_group = [] - mouse_area.not_dragging_group = [] - autoscroll(true, currentOnScreenMediaUuid) - } - } - - onSelection_uuidsChanged: { - - if (selection_uuids.length) { - //playhead.jumpToSource(selection_uuids[selection_uuids.length-1]) - } - set_first_in_selection() - - } - - function delete_selected() { - selectionFilter.deleteSelected() - } - - function gather_media_for_selected() { - selectionFilter.gatherSourcesForSelected() - } - - function evict_selected() { - selectionFilter.evictSelected() - } - - function appendSingleItem(item_idx) { - - var the_item = repeater.itemAt(item_idx) - // add to the selection like this so we get a signal - // that selection_uuids has changed. This signal is - // essential so that the selection order indicator on - // each item updates. Direct push does not trigger the - // signal - var uuids = selection_uuids - uuids.push(mediaList[item_idx].uuid) - selectionFilter.newSelection(uuids) - - } - - function indexFromUuid(uuid) { - var item_index = -1 - for (var idx = 0; idx < mediaList.length; idx++) { - if (repeater.itemAt(idx) != undefined && repeater.itemAt(idx).uuid == uuid) { - item_index = idx - break - } - } - return item_index - } - - function multiSelect(item_idx) { - - if (selection_uuids.length == 0) { - selectSingleItem(item_idx) - return - } - var uuids = selection_uuids - var last_item_index = indexFromUuid(uuids[uuids.length-1]) - - if (last_item_index > item_idx) { - for (var idx = last_item_index-1; idx >= item_idx; idx--) { - if (!repeater.itemAt(idx).selected) { - uuids.push(mediaList[idx].uuid) - } - } - } else if (last_item_index != -1) { - for (var idx = last_item_index+1; idx <= item_idx; idx++) { - if (!repeater.itemAt(idx).selected) { - uuids.push(mediaList[idx].uuid) - } - } - } - selectionFilter.newSelection(uuids) - - } - - function deSelectItem(item_idx) { - - var the_item = repeater.itemAt(item_idx) - var uuid = mediaList[item_idx].uuid - var uuids = selection_uuids.filter(function(ele){ - return ele != uuid; - }); - selectionFilter.newSelection(uuids) - } - - Item { - - id: clayout - anchors.fill: parent - - Repeater { - - id: repeater - model: mediaModel - - XsMediaViewItemContainer { - reapeaterGroup: repeater - y: index*item_height - currentIndex: index - x: 0 - width: parent.width - uuid: media_ui_object.uuid - - // switching visibility according to whether they are in the visible - // portion of the scroll box makes the UI much faster when you have - // a very long playlist - visible: (y+height) > media_list.contentY && y < (media_list.contentY+media_list.height) - - } - - } - } - - PropertyAnimation { - id: scroll_animator - target: media_list - property: "contentY" - running: false - duration: XsStyle.mediaListMaxScrollSpeed - - function autoScroll(scroll_to) { - running = false - to = Math.min(Math.max(0, scroll_to), mediaList.length*item_height-Math.min(contentHeight,height)) - current_target = to - running = true - } - - property var current_target: undefined - - function mouseWheel(delta) { - if (running) { - running = false - to = Math.min(Math.max(0, current_target + delta*item_height/120), mediaList.length*item_height-height) - current_target = to - running = true - } else { - current_target = Math.min(Math.max(0, contentY + delta*item_height/120), mediaList.length*item_height-height) - to = current_target - running = true - } - } - - } - - function autoscroll(force, target_uuid) { - if (mediaList == undefined) return - var matched = false - for (var i = 0; i < mediaList.length; ++i) { - var item = repeater.itemAt(i) - if (item && item.uuid == target_uuid) { - var curr_onscreen_y = i*item_height - contentY - if (force || curr_onscreen_y < item_height*0.25 || curr_onscreen_y > (height-item_height*0.75)) { - scroll_animator.autoScroll((i+0.5)*item_height - height/2) - } - matched = true - break - } - } - if (!matched) scroll_animator.autoScroll(0) - } - - onMediaListChanged: { - - for (var i = 0; i < mediaList.length; ++i) { - var item = repeater.itemAt(i) - if(item) { - item.y = i*item_height - } - } - set_first_in_selection() - //autoscroll(true, currentOnScreenMediaUuid) - } - - function set_first_in_selection() { - - var result = undefined - if (selection_uuids.length) { - var first_selected = indexFromUuid(selection_uuids[0]) - if (first_selected != -1) { - result = mediaList[first_selected] - } else if (mediaList.length) { - result = mediaList[0] - } - } else if (mediaList.length) { - result = mediaList[0] - } - firstMediaItemInSelection = result; - - } - - function switchSelectedToNamedStream(stream_name) { - for (var i = 0; i < mediaList.length; ++i) { - var item = repeater.itemAt(i) - if (item.selected) { - mediaList[i].mediaSource.switchToStream(stream_name) - } - } - } - - onCurrentOnScreenMediaUuidChanged: { - autoscroll(false, currentOnScreenMediaUuid) - } - - onSourceChanged: { - contentY = 0 - autoscroll(true, currentOnScreenMediaUuid) - } - - XsMediaMenu { - id: media_menu - } - - // media dropped from desktop - DropArea { - anchors.fill: parent - - onEntered: { - if(drag.hasUrls || drag.hasText) { - drag.acceptProposedAction() - } - } - onDropped: { - var count = mediaList.length - - if(drop.hasUrls) { - for(var i=0; i < drop.urls.length; i++) { - if(drop.urls[i].toLowerCase().endsWith('.xst')) { - Future.promise(studio.loadSessionRequestFuture(drop.urls[i])).then(function(result){}) - app_window.sessionFunction.newRecentPath(drop.urls[i]) - return; - } - } - } - - // prepare drop data - let data = {} - for(let i=0; i< drop.keys.length; i++){ - data[drop.keys[i]] = drop.getDataAsString(drop.keys[i]) - } - - if(session.selectedSource) { - Future.promise(source.handleDropFuture(data)).then( - function(quuids){ - if(mediaList.length >= count+1) { - selectSingleItem(count) - } - } - ) - } else { - Future.promise(session.handleDropFuture(data)).then(function(quuids){}) - } - } - } - - // This mouse event handler implements the drag/drop re-ordering - // in the media list and also selection behaviour - MouseArea { - - id: mouse_area - anchors.fill: parent - hoverEnabled: true - preventStealing: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - - property var under_mouse - property var last_selected_idx: undefined - - property bool doing_drag: false - property var y_drag_start - property var x_drag_start - - property var dragging_group: [] - property var not_dragging_group: [] - - property var dragged_item_intial_pos_x - property var dragged_item_intial_pos_y - property var under_mouse_idx: 0 - - property var drag_drop_uuids: [] - property var drag_drop_before_uuid - property var freshly_selected: undefined - property bool dragged_outside: doing_drag ? (mouseX < 0.0 || mouseY < 0.0 || mouseX > width || mouseY > height) : 0 - property var drag_outside_items: [] - - onPressed: { - - if (mouse.button != Qt.LeftButton) return; - - y_drag_start = mouse.y - x_drag_start = mouse.x - doing_drag = false - freshly_selected = undefined - - if (under_mouse) { - - // if item is already selected, deselect it if CTRL is held or do nothing - if (under_mouse.selected && mouse.modifiers == Qt.ControlModifier) { - deSelectItem(under_mouse.currentIndex) - } else { - if (mouse.modifiers == Qt.ControlModifier) { - appendSingleItem(under_mouse.currentIndex) - } else if (mouse.modifiers == Qt.ShiftModifier) { - multiSelect(under_mouse.currentIndex) - } else { - // if the user is clicking for a second time on the only selected item, don't - // do anytjhing incase they are doing a drag-drop playlist reorder. - if (!(selection_uuids.length == 1 && under_mouse.uuid == selection_uuids[0])) - { - selectSingleItem(under_mouse.currentIndex) - freshly_selected = under_mouse - } - - } - } - - } - - } - - Timer { - - id: dragDropFinishTimer - interval: dragDropAnimationDuration - running: false - repeat: false - - onTriggered: { - - source.dragDropReorder( - mouse_area.drag_drop_uuids, - mouse_area.drag_drop_before_uuid - ) - - } - } - - onReleased: { - - if (mouse.button == Qt.RightButton) { - // mouse is relative to control, if we're inside a scrollable area - // we need to translate.. - media_menu.x = mouse.x - media_menu.y = mouse.y-contentY - media_menu.visible = true - return; - } - - if (doing_drag && !dragged_outside) { - - // execute the drag/drop action to update the playlist ordering - - drag_drop_uuids = [] - - var top_of_first_drag_item = dragged_item_intial_pos_y - under_mouse_idx*item_height + (mouseY - y_drag_start) - var target_idx = Math.min(Math.max(Math.round(top_of_first_drag_item/item_height),0), not_dragging_group.length) - - for (var i = 0; i < dragging_group.length; ++i) { - // This starts animation to make the items slide into the gap - // in the list - dragging_group[i].drag_animation(0.0, (i + target_idx)*item_height, 100.0) - drag_drop_uuids.push(mediaList[dragging_group[i].currentIndex].uuid) - } - doing_drag = false - - if (target_idx < not_dragging_group.length) { - drag_drop_before_uuid = "" + mediaList[not_dragging_group[target_idx].currentIndex].uuid - } else { - drag_drop_before_uuid = "" // no 'before' uuid ensures the items are put at the end of the list - } - - // use a timer to execute the actual re-ordering when the - // animation has completed - dragDropFinishTimer.start() - - } else if (doing_drag && dragged_outside) { - - // force a rebuild of the items in the media list view - var all_uuids = [] - for (var i = 0; i < mediaList.length; ++i) { - all_uuids.push(mediaList[i].uuid) - } - - source.dragDropReorder( - all_uuids, - "" - ) - - sessionWidget.dropping_items_from_media_list( - source.uuid, - source.parent_playlist ? source.parent_playlist : source, - dragging_group, - mapToGlobal(mouseX, mouseY) - ) - - dragging_group = [] - not_dragging_group = [] - - } else if (under_mouse && under_mouse != freshly_selected) { - - // if the user clicks (and releases without starting a drag and drop) on a selected - // item, the selection is reset to include only the item clicked on, i.e. deselects - // any other items. - if (selection_uuids.length == 1 && under_mouse.uuid == selection_uuids[0]) - { - selectSingleItem(under_mouse.currentIndex) - } - - } - } - - onContainsMouseChanged: { - if (!containsMouse && !pressed && under_mouse) { - under_mouse.underMouse = false - under_mouse = undefined - } - } - - onWheel: { - // TODO: also use wheel.pixelDelta for trackpad/wacom ? - scroll_animator.mouseWheel(-wheel.angleDelta.y) - - } - - onDoubleClicked: { - if (mouse.button == Qt.RightButton) return; - if (under_mouse && source != session.onScreenSource) { - session.switchOnScreenSource(source.uuid) - selectSingleItem(under_mouse.currentIndex) - } - // show some viewer if none present. - if((!app_window.popout_window || !app_window.popout_window.visible) && !playerWidget.visible) { - app_window.togglePopoutViewer() - } - } - - onMouseXChanged: { - if (!pressed && under_mouse) { - if(mouseX > 0 && mouseX < under_mouse.media_item.thumb_width) { - var new_frame = Math.round(mouseX / under_mouse.media_item.thumb_width * 20.0) / 20.0 - if(new_frame != under_mouse.preview_frame) - under_mouse.preview_frame = new_frame - } else { - if(under_mouse.preview_frame != -1){ - under_mouse.preview_frame = -1 - } - } - } - } - - onDragged_outsideChanged: { - - if (doing_drag && dragged_outside) { - reverseDragDropAnimation() - var drag_items = [] - for (var idx in dragging_group) { - drag_items.push(dragging_group[idx].media_item.media_item) - } - drag_outside_items = drag_items - } else if (doing_drag && !dragged_outside) { - - var dx = (mouseX - x_drag_start) - var dy = (mouseY - y_drag_start) - updateDragDropAnimation(dx, dy, true) - drag_outside_items = [] - sessionWidget.dragging_items_from_media_list( - source.uuid, - source.parent_playlist ? source.parent_playlist.uuid : null, - drag_outside_items, - mapToGlobal(0.0, 0.0) - ) - - } - } - - onMouseYChanged: { - - if (!pressed) { - - var idx = Math.floor(mouseY/item_height) - if (idx < mediaList.length && idx >=0) { - if (under_mouse != repeater.itemAt(idx)) { - if (under_mouse) { - under_mouse.underMouse = false - } - under_mouse = repeater.itemAt(idx) - under_mouse.underMouse = true - } - } - - } else if (under_mouse && mouse.buttons == Qt.LeftButton && mouse.modifiers == Qt.NoModifier) { - - // user clicked on a media item and has - // started dragging... work out how far - // they have dragged - var dx = (mouseX - x_drag_start) - var dy = (mouseY - y_drag_start) - var delta = Math.sqrt(dx*dx + dy*dy) - - if ((delta > 30.0 || doing_drag) && !dragged_outside) { - - if (!doing_drag) { - - makeDragDropGroups() - - dragged_item_intial_pos_x = under_mouse.x; - dragged_item_intial_pos_y = under_mouse.y; - - updateDragDropAnimation(dx, dy, true) - - doing_drag = true - - } else { - updateDragDropAnimation(dx, dy, false) - } - } else if (doing_drag && dragged_outside) { - - sessionWidget.dragging_items_from_media_list( - source.uuid, - source.parent_playlist ? source.parent_playlist.uuid : null, - drag_outside_items, - mapToGlobal(mouseX, mouseY) - ) - } - - } else if (under_mouse && mouse.buttons == Qt.LeftButton && mouse.modifiers == Qt.AltModifier) { - - // user clicked on a media item and has - // started dragging... work out how far - // they have dragged - var dx = (mouseX - x_drag_start) - var dy = (mouseY - y_drag_start) - var delta = Math.sqrt(dx*dx + dy*dy) - - if (delta > 30.0 || doing_drag) { - if (!doing_drag) { - - makeDragDropGroups() - - dragged_item_intial_pos_x = under_mouse.x; - dragged_item_intial_pos_y = under_mouse.y; - - startDragOutside(dx, dy) - - doing_drag = true - - } else { - - updateDragOutside(dx, dy) - - } - } - } - } - - // make ordered lists of media items that are being - // drag/dropped and those that aren't being drag/dropped - function makeDragDropGroups() { - var new_dragging_group = [] - not_dragging_group = [] - for (var i = 0; i < mediaList.length; ++i) { - var item = repeater.itemAt(i) - if (item.selected) { - if (item == under_mouse) { - under_mouse_idx = new_dragging_group.length - } - new_dragging_group.push(item) - } else { - not_dragging_group.push(item) - } - item.orig_x = item.x - item.orig_y = item.y - } - dragging_group = new_dragging_group - } - - function startDragOutside(dx, dy) { - for (var i = 0; i < dragging_group.length; ++i) { - dragging_group[i].being_dragged_outside = true; - } - } - - function updateDragOutside(dx, dy) { - - } - - function reverseDragDropAnimation() { - - for (var i = 0; i < not_dragging_group.length; ++i) { - not_dragging_group[i].reverse_animation() - } - for (var i = 0; i < dragging_group.length; ++i) { - dragging_group[i].reverse_animation() - } - - } - - // As the user starts the drag drop, the selected items slide - // together to form a continuous group stuck to the item that - // is being dragged (the 'under_mouse' item). The items *not* - // being dragged have to re-arrange themselves to make a gap - // into which the drag-drop selection will be dropped. - function updateDragDropAnimation(dx, dy, start_animation) { - - // this is the index in the list where the drag/drop set - // would be dropped - var top_of_first_drag_item = dragged_item_intial_pos_y - under_mouse_idx*item_height + dy - var target_idx = Math.min( - Math.max( - Math.round(top_of_first_drag_item/item_height), - 0), - not_dragging_group.length - ) - - // The size of the gap needed for them to fall into - var gap_size = dragging_group.length*item_height - - // arrange the items not being dragged so they are continuous - // up to the drag-drop index, and then a gap, and then continuous - // again - for (var i = 0; i < not_dragging_group.length; ++i) { - if (i < target_idx) { - not_dragging_group[i].drag_animation(0, i*item_height, 0.0) - } else { - not_dragging_group[i].drag_animation(0, i*item_height + gap_size, 0.0) - } - } - - if(not_dragging_group[target_idx]){ - autoscroll(false, not_dragging_group[target_idx].uuid) - } - - // The items being dragged track the position of the 'under_mouse' item that - // it the anchor for the drag. When the drag drop animation starts, they slide - // into their position, otherwise they immediately track the position of the - // 'under_mouse' key item - if (start_animation) { - for (var i = 0; i < dragging_group.length; ++i) { - - dragging_group[i].drag_animation( - dragged_item_intial_pos_x + dx, - dragged_item_intial_pos_y + (i - under_mouse_idx)*item_height + dy, - 100.0 - ) - - } - } else { - for (var i = 0; i < dragging_group.length; ++i) { - - dragging_group[i].immediate_drag( - dragged_item_intial_pos_x + dx, - dragged_item_intial_pos_y + (i - under_mouse_idx)*item_height + dy - ) - - } - } - } - } -} diff --git a/retired/XsMediaViewItemContainer.qml b/retired/XsMediaViewItemContainer.qml deleted file mode 100644 index 9d4090d99..000000000 --- a/retired/XsMediaViewItemContainer.qml +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.12 - -import xStudio 1.0 - -Rectangle { - - id: listViewContainer - height: header.item_height - //z: (dragArea.drag.active || x_animator.running) ? 100 : 0 - property var uuid - property var currentIndex - property int targetIndex: currentIndex - property var reapeaterGroup - property bool underMouse: false - property bool selected: selection_index != 0 - property int selection_index: selection_uuids.indexOf(uuid) + 1 - property bool being_dragged_outside: false - - property alias media_item: media_item - property real preview_frame: -1 - color: "transparent" - property var orig_x - property var orig_y - - onUnderMouseChanged: { - if (!underMouse) { - preview_frame = -1 - } - } - - XsMediaViewListItem { - id: media_item - preview_frame: listViewContainer.preview_frame - } - - PropertyAnimation { - id: x_animator - target: listViewContainer - property: "x" - running: false - duration: dragDropAnimationDuration - } - - PropertyAnimation { - id: y_animator - target: listViewContainer - property: "y" - running: false - duration: dragDropAnimationDuration - } - - function reverse_animation() { - - z = 0 - x_animator.running = false - y_animator.running = false - x_animator.to = orig_x - y_animator.to = orig_y - x_animator.running = true - y_animator.running = true - - } - - function drag_animation(target_x, target_y, zz) { - z = zz - x_animator.running = false - y_animator.running = false - x_animator.to = target_x - y_animator.to = target_y - x_animator.running = true - y_animator.running = true - } - - function drag_stop() { - z = 0 - } - - function immediate_drag(target_x, target_y) { - if (y_animator.running) { - y_animator.to = target_y - } else { - y = target_y - } - if (x_animator.running) { - x_animator.to = target_x - } else { - x = target_x - } - } - - function setNewTargetIndex (newIdx) { - y_animator.to = newIdx*header.item_height - y_animator.running = true - targetIndex = newIdx - } - - Rectangle { - anchors.fill: parent - visible: underMouse - color: "transparent" - opacity: 0.5 - border.color: "white" - } - - Rectangle { - anchors.fill: parent - visible: being_dragged_outside - color: "orange" - opacity: 0.5 - //border.color: "white" - } -} diff --git a/retired/XsMediaViewListItem.qml b/retired/XsMediaViewListItem.qml deleted file mode 100644 index 2446feeba..000000000 --- a/retired/XsMediaViewListItem.qml +++ /dev/null @@ -1,436 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.0 -import xstudio.qml.module 1.0 -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudio 1.0 - -Rectangle { - id: control - - XsModuleAttributes { - id: colour_settings - attributesGroupNames: "colour_pipe_attributes" - onValueChanged: { - if(key == "display" || key == "view") { - if(media_source && media_source.durationFrames != ""){ - imageChanged() - } - else{ - updateImage.running = true - } - } - } - } - - anchors.fill: parent - color: ((index & 1 == 1) ? XsStyle.mediaListItemBG2 : XsStyle.mediaListItemBG1) - - property var playhead: viewport ? viewport.playhead : undefined - property var media_item: media_ui_object - property var media_item_uuid: media_item ? media_item.uuid : null - // property bool invalid: (media_item && media_source && media_source.streams ? false : true) - // property bool missing_media: (!invalid && ! media_source.streams.length ? true : false) - property bool online: media_item ? media_item.mediaOnline : false - property bool corrupt: media_item ? media_item.mediaStatus == "Corrupt" : false - - property var media_source: media_item ? media_item.mediaSource : null - - property var thumb_width: thumb_image1.width - property real preview_frame: -1 - property int imageVisible: 1 - - z: -100000 - - Timer { - id: updateImage - interval: 0; running: true; repeat: false - onTriggered: imageChanged() - } - - // delay event if object isn't ready.. - onOnlineChanged: {if(! updateImage.running){ imageChanged() }} - - function imageChanged() { - if(online && media_source && media_source.durationFrames != "") { - // Update invisible first.. - if(imageVisible === 1) { - Future.promise(media_source.getThumbnailURLFuture(-1) - ).then(function(url){ - thumb_image2.source_url = url - function finishImage(){ - if(thumb_image2.status !== Image.Loading) { - thumb_image2.statusChanged.disconnect(finishImage); - imageVisible = 2 - Future.promise(media_source.getThumbnailURLFuture(-1) - ).then(function(url){ - thumb_image1.source_url = url - } - ) - } - } - if (thumb_image2.status === Image.Loading){ - thumb_image2.statusChanged.connect(finishImage); - } - else { - finishImage(); - } - } - ) - } else { - Future.promise(media_source.getThumbnailURLFuture(-1) - ).then(function(url){ - thumb_image1.source_url = url - function finishImage(){ - if(thumb_image1.status !== Image.Loading) { - thumb_image1.statusChanged.disconnect(finishImage); - imageVisible = 1 - Future.promise(media_source.getThumbnailURLFuture(-1) - ).then(function(url){ - thumb_image2.source_url = url - } - ) - } - } - if (thumb_image1.status === Image.Loading){ - thumb_image1.statusChanged.connect(finishImage); - } - else { - finishImage(); - } - } - ) - } - } else { - if(!media_source || media_source.durationFrames == "") { - // try again.. - updateImage.interval = 500 - updateImage.running = true - } else { - thumb_image1.source_url = "qrc:///feather_icons/film.svg" - thumb_image2.source_url = "qrc:///feather_icons/film.svg" - } - } - } - - function setSource(source){ - var imageNew = imageVisible === 1 ? thumb_image2 : thumb_image1; - var imageOld = imageVisible === 2 ? thumb_image2 : thumb_image1; - - imageNew.source_url = source; - - function finishImage(){ - if(imageNew.status === Component.Ready) { - imageNew.statusChanged.disconnect(finishImage); - imageVisible = imageVisible === 1 ? 2 : 1; - } - } - - if (imageNew.status === Component.Loading){ - imageNew.statusChanged.connect(finishImage); - } - else { - finishImage(); - } - } - - onPreview_frameChanged: { - if(!online || preview_frame == -1) { - // restore bindings - control.imageChanged() - } else if (visible) { - if(media_source && media_source.durationFrames != "") { - Future.promise(media_source.getThumbnailURLFuture(preview_frame * (media_source.durationFramesNumeric-1)) - ).then(function(url){ - setSource(url) - } - ) - } - } - } - - onVisibleChanged: { - - if (visible) { - if(media_source && media_source.durationFrames != "") { - var frame = preview_frame == -1 ? -1: preview_frame * (media_source.durationFramesNumeric-1); - Future.promise(media_source.getThumbnailURLFuture(frame) - ).then(function(url){ - setSource(url) - } - ) - } - thumb_image1.source = thumb_image1.source_url - thumb_image2.source = thumb_image2.source_url - } - - } - - Rectangle { - color: media_item ? media_item.flag : "#00000000" - width:3 - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: 1 - } - - XsBusyIndicator { - id: loading_image1 - property bool loading: (imageVisible === 1 && (thumb_image1.status == Image.Loading || thumb_image1.status == Image.Null)) || (imageVisible === 2 && (thumb_image2.status == Image.Loading || thumb_image2.status == Image.Null)) - x: thumb_image1.x + thumb_image1.width/2 - width/2 - y: 3 - width: height - height: parent.height-6 - z: 100 - visible: loading - running: loading - } - - function nearestPowerOf2(n) { - return 1 << 31 - Math.clz32(n); - } - - Image { - id: thumb_image1 - z:1 - - property var source_url: "qrc:///feather_icons/film.svg" - source: "qrc:///feather_icons/film.svg" - - width: header.thumb_size-6 - sourceSize.width: Math.max(128, nearestPowerOf2(width)) - - anchors.left: selection_order.right - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: 3 - asynchronous: true - fillMode: Image.PreserveAspectFit - cache: true - smooth: true - transformOrigin: Item.Center - layer { - enabled: !online - effect: ColorOverlay { - color: XsStyle.controlTitleColor - } - } - visible: imageVisible === 1 - - onSource_urlChanged: { - if (control.visible) { - source = source_url - } - } - } - - Image { - id: thumb_image2 - - property var source_url: "qrc:///feather_icons/film.svg" - source: "qrc:///feather_icons/film.svg" - - width: header.thumb_size-6 - sourceSize.width: Math.max(128, nearestPowerOf2(width)) - - z:1 - anchors.left: selection_order.right - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: 3 - asynchronous: true - fillMode: Image.PreserveAspectFit - cache: true - smooth: true - transformOrigin: Item.Center - layer { - enabled: !online - effect: ColorOverlay { - color: XsStyle.controlTitleColor - } - } - visible: imageVisible === 2 - - onSource_urlChanged: { - if (control.visible) { - source = source_url - } - } - - } - - Rectangle { - z:1 - width: 12 - height: 12 - radius: 6 - color: XsStyle.highlightColor - gradient: styleGradient.accent_gradient - anchors.bottom: thumb_image1.bottom - anchors.right: thumb_image1.right - anchors.margins: 1 - visible: playhead ? playhead.media.uuid == uuid : false - id: playing_indicator - } - - XsLabel { - id: bookmark_indicator - z:1 - // font.bold: true - font.weight: Font.Black - // style: Text.Outline - // styleColor: "black" - // sourceSize: 12 - text: "N" - width: 12 - height: 12 - // opacity: 0.5 - font.pixelSize: 14 - anchors.margins: 1 - color: XsStyle.highlightColor - anchors.top: thumb_image1.top - anchors.right: thumb_image1.right - // anchors.topMargin: 1 - // anchors.bottomMargin: 1 - // anchors.leftMargin: 1 - // anchors.rightMargin: 4 - visible: app_window.bookmarkModel.search(uuid, "ownerRole").valid - - Connections { - target: app_window.bookmarkModel - function onLengthChanged() { - callback_delay_timer.setTimeout(function(){ bookmark_indicator.visible = app_window.bookmarkModel.search(uuid, "ownerRole").valid }, 500); - } - } - - Timer { - id: callback_delay_timer - function setTimeout(cb, delayTime) { - callback_delay_timer.interval = delayTime; - callback_delay_timer.repeat = false; - callback_delay_timer.triggered.connect(cb); - callback_delay_timer.triggered.connect(function release () { - callback_delay_timer.triggered.disconnect(cb); // This is important - callback_delay_timer.triggered.disconnect(release); // This is important as well - }); - callback_delay_timer.start(); - } - } - } - // XsColoredImage{ - // id: bookmark_indicator - // // sourceSize: 12 - // source: "qrc:/icons/notes.png" - // width: 12 - // height: 12 - // // opacity: 0.5 - // iconColor: XsStyle.highlightColor - // anchors.top: thumb_image1.top - // anchors.right: thumb_image1.right - // anchors.margins: 1 - // visible: session.bookmarks.ownedBookmarks.hasOwnProperty(uuid.toString().substr(1,36)) - // } - - - // Rectangle { - // id: bookmark_indicator - - // width: 12 - // height: 12 - // radius: 6 - // opacity: 0.5 - // color: XsStyle.highlightColor - // gradient: styleGradient.accent_gradient - // anchors.top: thumb_image1.top - // anchors.right: thumb_image1.right - // anchors.margins: 1 - // visible: session.bookmarks.ownedBookmarks.hasOwnProperty(uuid.toString().substr(1,36)) - // } - - DropShadow { - anchors.fill: playing_indicator - horizontalOffset: 3 - verticalOffset: 3 - radius: 6.0 - samples: 12 - color: "#ff000000" - source: playing_indicator - visible: playing_indicator.visible - } - - DropShadow { - anchors.fill: bookmark_indicator - horizontalOffset: 3 - verticalOffset: 3 - radius: 6.0 - samples: 12 - color: "#ff000000" - source: bookmark_indicator - visible: bookmark_indicator.visible - } - - Rectangle { - - color: "transparent" - x: 2 - width: header.selection_indicator_width - anchors.top: parent.top - anchors.bottom: parent.bottom - id: selection_order - - Text { - anchors.fill: parent - anchors.margins: 4 - text: selection_index - visible: selection_index != 0 && selection_uuids.length > 1 - color: "white" - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - - } - - Rectangle { - color: "transparent" - x: header.filename_left - width: parent.width-header.filename_left - anchors.top: parent.top - anchors.bottom: parent.bottom - - Text { - anchors.fill: parent - anchors.margins: 4 - text: media_item ? media_source ? media_source["fileName"] : "-" : "-" - color: "white" - verticalAlignment: Text.AlignVCenter - elide: Text.ElideLeft - } - } - - Rectangle { - color: "white" - anchors.fill: parent - visible: online && selection_index != 0 - opacity: 0.2 - } - - Rectangle { - color: corrupt ? "orange" : "red" - anchors.fill: parent - visible: !online - opacity: 0.2 - } - - /*Rectangle { - color: "grey" - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: 1 - }*/ - -} diff --git a/retired/XsNewContactSheetDialog.qml b/retired/XsNewContactSheetDialog.qml deleted file mode 100644 index b7bf0f7b1..000000000 --- a/retired/XsNewContactSheetDialog.qml +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import xstudio.qml.uuid 1.0 - -import xStudio 1.0 - -XsStringRequestDialog { - property var uuid: null_uuid.asQuuid - property bool insert_new: false - property var playlist: null - signal created(QMLUuid uuid) - - okay_text: "Add Contact Sheet" - text: "Untitled Contact Sheet" - - y: playlist_panel.mapToGlobal(0, 25).y - centerOn: playlist_panel - - function newItem(parent, obj, value) { - if(obj.selected) { - if(parent.createContactSheet) { - if(!insert_new) { - result_uuid.setFromQuuid(parent.createContactSheet(value, null_uuid.asQuuid)) - } else { - obj = parent.itemModel.next_object(obj) - if(obj) { - result_uuid.setFromQuuid(parent.createContactSheet(value, obj.cuuid)) - } else { - result_uuid.setFromQuuid(parent.createContactSheet(value, null_uuid.asQuuid)) - } - } - created(result_uuid) - return true - } else if(obj.createContactSheet) { - result_uuid.setFromQuuid(obj.createContactSheet(value, null_uuid.asQuuid)) - created(result_uuid) - return true - } - } - return false - } - - onOkayed: { - var pobj = playlist ? playlist : app_window.session - if(uuid != null_uuid.asQuuid || !XsUtils.forAllItems(pobj, null, newItem, text)) { - result_uuid.setFromQuuid(pobj.createContactSheet(text, uuid)) - created(result_uuid) - } - pobj.expanded = true; - } - QMLUuid { - id: null_uuid - } - QMLUuid { - id: result_uuid - } -} diff --git a/retired/XsNewPlaylistDialog.qml b/retired/XsNewPlaylistDialog.qml deleted file mode 100644 index 122d22044..000000000 --- a/retired/XsNewPlaylistDialog.qml +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import xstudio.qml.uuid 1.0 - -import xStudio 1.0 - -XsStringRequestDialog { - property var uuid: null_uuid.asQuuid - property bool insert_new: false - signal created(QMLUuid uuid) - signal created_secondary(QMLUuid uuid) - okay_text: "Add Playlist" - text: "Untitled Playlist" - - y: playlist_panel.mapToGlobal(0, 25).y - centerOn: playlist_panel - - function newItem(parent, obj, value) { - if(!insert_new) { - result_uuid.setFromQuuid(parent.createPlaylist(value, null_uuid.asQuuid)) - created(result_uuid) - return true - } else if(obj.selected && parent.createPlaylist) { - obj = parent.itemModel.next_object(obj) - if(obj) { - result_uuid.setFromQuuid(parent.createPlaylist(value, obj.cuuid)) - } else { - result_uuid.setFromQuuid(parent.createPlaylist(value, null_uuid.asQuuid)) - } - created(result_uuid) - return true - } - return false - } - - function newItem_secondary(parent, obj, value) { - if(!insert_new) { - result_uuid.setFromQuuid(parent.createPlaylist(value, null_uuid.asQuuid)) - created_secondary(result_uuid) - return true - } else if(obj.selected && parent.createPlaylist) { - obj = parent.itemModel.next_object(obj) - if(obj) { - result_uuid.setFromQuuid(parent.createPlaylist(value, obj.cuuid)) - } else { - result_uuid.setFromQuuid(parent.createPlaylist(value, null_uuid.asQuuid)) - } - created_secondary(result_uuid) - return true - } - return false - } - - onOkayed: { - if(uuid != null_uuid.asQuuid || !XsUtils.forFirstItem(app_window.session, null, newItem, text)) { - result_uuid.setFromQuuid(session.createPlaylist(text, uuid)) - created(result_uuid) - } - session.expanded = true; - } - - onSecondary_okayed:{ - if(uuid != null_uuid.asQuuid || !XsUtils.forFirstItem(app_window.session, null, newItem_secondary, text)) { - result_uuid.setFromQuuid(session.createPlaylist(text, uuid)) - created_secondary(result_uuid) - } - session.expanded = true; - } - - QMLUuid { - id: null_uuid - } - QMLUuid { - id: result_uuid - } -} diff --git a/retired/XsNewPlaylistDividerDialog.qml b/retired/XsNewPlaylistDividerDialog.qml deleted file mode 100644 index 05753bfd0..000000000 --- a/retired/XsNewPlaylistDividerDialog.qml +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import xstudio.qml.uuid 1.0 - -import xStudio 1.0 - -XsStringRequestDialog { - property var uuid: null_uuid.asQuuid - property var playlist: null - property bool insert_new: false - - - okay_text: "Add Divider" - text: "Untitled Divider" - - y: playlist_panel.mapToGlobal(0, 25).y - centerOn: playlist_panel - - function newItem(parent, obj, value) { - if(obj.selected) { - if(parent.createDivider) { - if(!insert_new) { - parent.createDivider(value, null_uuid.asQuuid); - } else { - obj = parent.itemModel.next_object(obj) - if(obj) { - parent.createDivider(value, obj.cuuid); - } else { - parent.createDivider(value, null_uuid.asQuuid); - } - } - return true - } else if(obj.createDivider) { - obj.createDivider(value, null_uuid.asQuuid); - return true - } - } - return false - } - - onOkayed: { - var pobj = playlist ? playlist : app_window.session - if(uuid != null_uuid.asQuuid || !XsUtils.forAllItems(pobj, null, newItem, text)) { - pobj.createDivider(text, uuid); - } - pobj.expanded = true; - } - QMLUuid { - id: null_uuid - } -} diff --git a/retired/XsNewSubsetDialog.qml b/retired/XsNewSubsetDialog.qml deleted file mode 100644 index 69425291e..000000000 --- a/retired/XsNewSubsetDialog.qml +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import xstudio.qml.uuid 1.0 - -import xStudio 1.0 - -XsStringRequestDialog { - property var uuid: null_uuid.asQuuid - property bool insert_new: false - property var playlist: null - signal created(QMLUuid uuid) - - okay_text: "Add Subset" - text: "Untitled Subset" - - y: playlist_panel.mapToGlobal(0, 25).y - centerOn: playlist_panel - - function newItem(parent, obj, value) { - if(obj.selected) { - if(parent.createSubset) { - if(!insert_new) { - result_uuid.setFromQuuid(parent.createSubset(value, null_uuid.asQuuid)) - } else { - obj = parent.itemModel.next_object(obj) - if(obj) { - result_uuid.setFromQuuid(parent.createSubset(value, obj.cuuid)) - } else { - result_uuid.setFromQuuid(parent.createSubset(value, null_uuid.asQuuid)) - } - } - created(result_uuid) - return true - } else if(obj.createSubset) { - result_uuid.setFromQuuid(obj.createSubset(value, null_uuid.asQuuid)) - created(result_uuid) - return true - } - } - return false - } - - onOkayed: { - var pobj = playlist ? playlist : app_window.session - if(uuid != null_uuid.asQuuid || !XsUtils.forAllItems(pobj, null, newItem, text)) { - result_uuid.setFromQuuid(pobj.createSubset(text, uuid)) - created(result_uuid) - } - pobj.expanded = true; - } - QMLUuid { - id: null_uuid - } - QMLUuid { - id: result_uuid - } -} diff --git a/retired/XsNewTimelineDialog.qml b/retired/XsNewTimelineDialog.qml deleted file mode 100644 index 4c7256eb5..000000000 --- a/retired/XsNewTimelineDialog.qml +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import xstudio.qml.uuid 1.0 - -import xStudio 1.0 - -XsStringRequestDialog { - property var uuid: null_uuid.asQuuid - property var playlist: null - - y: playlist_panel.mapToGlobal(0, 25).y - centerOn: playlist_panel.mapToGlobal(0, 0) - - okay_text: "Add Timeline" - text: "Untitled Timeline" - - function newItem(parent, obj, value) { - if(obj.selected) { - if(parent.createTimeline) { - obj = parent.itemModel.next_object(obj) - if(obj) { - parent.createTimeline(value, obj.cuuid); - } else { - parent.createTimeline(value, null_uuid.asQuuid); - } - return true - } else if(obj.createTimeline) { - obj.createTimeline(value, null_uuid.asQuuid); - return true - } - } - return false - } - - onOkayed: { - var pobj = playlist ? playlist : app_window.session - if(uuid != null_uuid.asQuuid || !XsUtils.forAllItems(pobj, null, newItem, text)) { - pobj.createTimeline(text, uuid); - } - pobj.expanded = true; - } - QMLUuid { - id: null_uuid - } -} diff --git a/retired/XsPlaylistContactSheetWidget.qml b/retired/XsPlaylistContactSheetWidget.qml deleted file mode 100644 index 00a9a0015..000000000 --- a/retired/XsPlaylistContactSheetWidget.qml +++ /dev/null @@ -1,235 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 1.4 -import QtQuick 2.4 -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.12 -import QtQml.Models 2.12 -import Qt.labs.qmlmodels 1.0 -import QtQuick.Dialogs 1.0 -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudio 1.0 - -Rectangle { - id: playlist - color: XsStyle.mainBackground - implicitHeight: playlist_control.implicitHeight - property string text - property var backend - property string type - property var parent_backend - property ListView listView - property int listViewIndex - - property bool highlighted: backend.selected - - function not_drag_drop_target() { - is_drag_drop_target = false - } - function dragging_items_from_media_list(source_uuid, mousePos) { - is_drag_drop_target = true - } - - function labelButtonReleased(mouse) { - if (mouse.button == Qt.LeftButton) { - if(mouse.modifiers & Qt.ControlModifier) { - backend.selected = !backend.selected - } else if(mouse.modifiers == Qt.NoModifier) { - parent_backend.setSelection([backend.cuuid]) - } - } - } - Timer { - id: busy - interval: 500; - running: false; - repeat: false - onTriggered: playlist_control.busy.running = false - } - - Connections { - target: backend - function onMediaAdded(uuid) { - busy.stop() - playlist_control.busy.running = true - busy.start() - } - } - XsSessionBarWidget { - id: playlist_control - property bool has_drag: false - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - tint: backend.flag - - text: playlist.text - type_icon_source: "qrc:///feather_icons/grid.svg" - type_icon_color: XsStyle.highlightColor - search_visible: false - expand_visible: false - expand_button_holder: true - color: { - has_drag ? XsStyle.highlightColor : ( - highlighted ? XsStyle.menuBorderColor : (hovered ? XsStyle.controlBackground : XsStyle.mainBackground) - ) - } - gradient: has_drag ? styleGradient.accent_gradient : null - icon_border_color: XsStyle.menuBorderColor - highlighted: playlist.highlighted - child_count: playlist.backend.mediaList.length - - onLabelPressed: labelButtonReleased(mouse) - onPlusPressed: {parent_backend.setSelection([backend.cuuid]); plusMenu.toggleShow()} - onMorePressed: {parent_backend.setSelection([backend.cuuid]); moreMenu.toggleShow()} - - onLabelDoubleClicked: { - playerWidget.switchSource(backend) - } - - DropArea { - anchors.fill: playlist_control.drop_area - - onEntered: { - if(drag.hasUrls || drag.hasText) { - drag.acceptProposedAction() - playlist_control.has_drag = true - } - } - onExited: { - playlist_control.has_drag = false - } - onDropped: { - playlist_control.has_drag = false - - // if session then ignore everying else.. - if(drop.hasUrls) { - for(var i=0; i < drop.urls.length; i++) { - if(drop.urls[i].toLowerCase().endsWith('.xst')) { - session.loadUrl(drop.urls[i]) - app_window.sessionFunction.newRecentPath(drop.urls[i]) - return; - } - } - } - - // prepare drop data - let data = {} - for(let i=0; i< drop.keys.length; i++){ - data[drop.keys[i]] = drop.getDataAsString(drop.keys[i]) - } - Future.promise(backend.handleDropFuture(data)).then(function(quuids){}) - } - } - } - - property bool is_drag_drop_target: false - Rectangle { - id: drag_target_highlight - anchors.fill: parent - color: "transparent" - border.color: "white" - border.width: 2 - visible: playlist.is_drag_drop_target - z: 100 - } - - XsStringRequestDialog { - id: request_name - okay_text: "Rename" - text: "noname" - onOkayed: backend.setName(text) - x: XsUtils.centerXInParent(panel, parent, width) - y: XsUtils.centerYInParent(panel, parent, height) - } - - XsButtonDialog { - id: removeContainer - text: "Remove" - width: 300 - buttonModel: ["Cancel", "ContactSheet And Media", "ContactSheet"] - onSelected: { - if(button_index == 2) { - parent_backend.removeContainer(backend.cuuid) - } else if(button_index == 1) { - parent_backend.removeContainer(backend.cuuid, true) - } - } - } - - XsMenu { - id: moreMenu - x: playlist_control.more_button.x - y: playlist_control.more_button.y - - fakeDisabled: true - - XsFlagMenu { - flag: backend.flag - onFlagHexChanged: { - if(backend.flag !== flagHex) - parent_backend.reflagContainer(flagHex, backend.cuuid) - } - } - - XsMenuSeparator {} - - XsMenuItem { - mytext: qsTr("Rename...") - onTriggered: { - request_name.text = backend.name - request_name.open() - } - } - XsMenuItem { - mytext: qsTr("Duplicate") - onTriggered: { - var nextItem = listView.itemAtIndex(listViewIndex+1) - if(nextItem) - parent_backend.duplicateContainer(backend.cuuid, nextItem.contentItem.backend.cuuid) - else - parent_backend.duplicateContainer(backend.cuuid) - } - } - - XsMenu { - title: "Convert To" - XsMenuItem { - mytext: qsTr("Subset") - onTriggered: { - parent_backend.convertToSubset(backend.cuuid) - } - } - XsMenuItem { - mytext: qsTr("Timeline") - enabled: false - } - } - - XsMenuSeparator {} - XsMenuItem { - mytext: qsTr("Remove") - onTriggered: { - removeContainer.open() - } - } - } - XsMenu { - id: plusMenu - x: playlist_control.plus_button.x - y: playlist_control.plus_button.y - - fakeDisabled: true - - XsMenuItem { - mytext: qsTr("Add Media...") - onTriggered: XsUtils.openDialogPlaylist("qrc:/dialogs/XsAddMediaDialog.qml").open() - } - XsMenuItem { - mytext: qsTr("Add Media From Clipboard") - onTriggered: app_window.session.add_media_from_clipboard(backend) - } - } -} diff --git a/retired/XsPlaylistDividerWidget.qml b/retired/XsPlaylistDividerWidget.qml deleted file mode 100644 index 61371fd1c..000000000 --- a/retired/XsPlaylistDividerWidget.qml +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 1.4 -import QtQuick 2.4 -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.12 -import QtQml.Models 2.12 -import Qt.labs.qmlmodels 1.0 - -import xStudio 1.0 - -XsSessionBarDivider { - id: control - property var backend - property string type - property var parent_backend - property ListView listView - property int listViewIndex - - color: highlighted ? XsStyle.menuBorderColor : (hovered ? XsStyle.controlBackground : XsStyle.mainBackground) - tint: backend.flag - - expand_button_holder: true - - property bool highlighted: backend.selected - - function labelButtonReleased(mouse) { - if (mouse.button == Qt.LeftButton) { - if(mouse.modifiers & Qt.ControlModifier) { - backend.selected = !backend.selected - } else if(mouse.modifiers == Qt.NoModifier) { - parent_backend.setSelection([backend.cuuid]) - } - } - } - - onLabelPressed: labelButtonReleased(mouse) - onMorePressed: {parent_backend.setSelection([backend.cuuid]); moreMenu.toggleShow()} - - XsStringRequestDialog { - id: request_name - okay_text: "Rename" - text: "noname" - onOkayed: parent_backend.renameContainer(text, backend.cuuid) - x: XsUtils.centerXInParent(panel, parent, width) - y: XsUtils.centerYInParent(panel, parent, height) - } - - XsMenu { - id: moreMenu - x: more_button.x - y: more_button.y - - fakeDisabled: true - - XsFlagMenu { - flag: backend.flag - onFlagHexChanged: { - if(backend.flag !== flagHex) - parent_backend.reflagContainer(flagHex, backend.cuuid) - } - } - - XsMenuItem { - mytext: qsTr("Rename") - onTriggered: { - request_name.text = backend.name - request_name.open() - } - } - XsMenuItem { - mytext: qsTr("Duplicate") - onTriggered: { - var nextItem = listView.itemAtIndex(listViewIndex+1) - if(nextItem) - parent_backend.duplicateContainer(backend.cuuid, nextItem.contentItem.backend.cuuid) - else - parent_backend.duplicateContainer(backend.cuuid) - } - } - XsMenuItem { - mytext: qsTr("Remove") - onTriggered: parent_backend.removeContainer(backend.cuuid) - } - } -} \ No newline at end of file diff --git a/retired/XsPlaylistGroupWidget.qml b/retired/XsPlaylistGroupWidget.qml deleted file mode 100644 index da5e05124..000000000 --- a/retired/XsPlaylistGroupWidget.qml +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 1.4 -import QtQuick 2.4 -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.12 -import QtQml.Models 2.12 -import Qt.labs.qmlmodels 1.0 - -import xStudio 1.0 - -Rectangle { - id: playlist_group - color: XsStyle.mainBackground - implicitHeight: playlist_control.implicitHeight + (playlist_control.expanded ? layout.implicitHeight : 0) - property string text - property var backend - property var parent_backend - - property string type - property var session - property ListView listView - property int listViewIndex - - property bool highlighted: backend.selected - - function labelButtonReleased(mouse) { - if (mouse.button == Qt.LeftButton) { - if(mouse.modifiers & Qt.ControlModifier) { - backend.selected = !backend.selected - } else if(mouse.modifiers == Qt.NoModifier) { - parent_backend.setSelection([backend.cuuid]) - } - } - } - - DelegateChooser { - id: chooser - role: "type" - DelegateChoice { - roleValue: "Playlist"; - XsPlaylistWidget { - anchors.left: parent.left - anchors.right: parent.right - text: object.name - type: object.type - backend: object - } - } - DelegateChoice { - roleValue: "ContainerDivider"; - XsPlaylistDividerWidget { - anchors.left: parent.left - anchors.right: parent.right - text: object.name - type: object.type - backend: object - parent_backend: playlist_group.parent_backend - } - } - } - - XsSessionBarWidget { - id: playlist_control - text: playlist_group.text - type_icon_source: "qrc:///feather_icons/server.svg" - type_icon_color: XsStyle.highlightColor - search_visible: false - color: highlighted ? Qt.tint(XsStyle.menuBorderColor, Qt.rgba(tint.r,tint.g,tint.b, (tint.a ? 0.1 : 0.0))) : Qt.tint(XsStyle.mainBackground,Qt.rgba(tint.r,tint.g,tint.b, (tint.a ? 0.1 : 0.0))) - tint: backend.flag - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - icon_border_color: XsStyle.menuBorderColor - onPlusPressed: plusMenu.toggleShow() - onLabelPressed: labelButtonReleased(mouse) - onMorePressed: moreMenu.toggleShow() - - highlighted: playlist_group.highlighted - expanded: backend.expanded - onExpandClicked: backend.expanded = !backend.expanded - expand_visible: backend.itemModel.rowCount() - } - - ListView { - id: layout - implicitHeight: contentItem.childrenRect.height - anchors.top: playlist_control.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.leftMargin: playlist_control.icon_width * 2 - clip: true - visible: playlist_control.expanded - model: backend.itemModel - delegate: chooser - focus: true - } - - XsStringRequestDialog { - id: request_new_playlist - property var uuid - property bool into - okay_text: "New Playlist" - text: "Untitled Playlist" - onOkayed: {parent_backend.createPlaylist(text, uuid, into); backend.expanded = true;} - y: playlist_control.plus_button.y -// x: playlist_control.plus_button.x - } - - XsStringRequestDialog { - id: request_new_divider - property var uuid - property bool into - okay_text: "New Divider" - text: "Untitled Divider" - onOkayed: {parent_backend.createDivider(text, uuid, into); backend.expanded = true;} - y: playlist_control.plus_button.y -// x: playlist_control.plus_button.x - } - - XsStringRequestDialog { - id: request_name - okay_text: "Rename" - text: "noname" - onOkayed: parent_backend.renameContainer(text, backend.cuuid) - y: playlist_control.plus_button.y - } - - XsMenu { - id: moreMenu - x: playlist_control.more_button.x - y: playlist_control.more_button.y - - fakeDisabled: true - - XsFlagMenu { - flag: backend.flag - onFlagHexChanged: { - if(backend.flag !== flagHex) - parent_backend.reflagContainer(flagHex, backend.cuuid) - } - } - - XsMenuItem { - mytext: qsTr("Rename") - onTriggered: { - request_name.text = backend.name - request_name.open() - } - } - XsMenuItem { - mytext: qsTr("Duplicate") - onTriggered: { - var nextItem = ListView.view.itemAtIndex(index+1) - if(nextItem) - parent_backend.duplicateContainer(backend.cuuid, nextItem.backend.cuuid) - else - parent_backend.duplicateContainer(backend.cuuid) - } - } - } - - XsMenu { - id: plusMenu - x: playlist_control.plus_button.x - y: playlist_control.plus_button.y - - fakeDisabled: true - XsMenuItem { - mytext: qsTr("New Playlist") - onTriggered: { - backend.expanded = true - // request_new_playlist.uuid = layout.currentItem ? layout.currentItem.backend.cuuid : backend.cuuid - request_new_playlist.uuid = backend.cuuid - request_new_playlist.into = layout.currentItem ? false : true - request_new_playlist.open() - } - } - - XsMenuItem { - mytext: qsTr("New Divider") - onTriggered: { - backend.expanded = true - request_new_divider.uuid = backend.cuuid - // request_new_divider.uuid = layout.currentItem ? layout.currentItem.backend.cuuid : backend.cuuid - request_new_divider.into = layout.currentItem ? false : true - request_new_divider.open() - } - } - } -} diff --git a/retired/XsPlaylistSubGroupWidget.qml b/retired/XsPlaylistSubGroupWidget.qml deleted file mode 100644 index ff19c8795..000000000 --- a/retired/XsPlaylistSubGroupWidget.qml +++ /dev/null @@ -1,266 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 1.4 -import QtQuick 2.4 -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.12 -import QtQml.Models 2.12 -import Qt.labs.qmlmodels 1.0 - -import xStudio 1.0 - -Rectangle { - id: playlist_group - color: XsStyle.mainBackground - implicitHeight: playlist_control.implicitHeight + (playlist_control.expanded ? layout.implicitHeight : 0) - property string text - property var backend - property string type - property var parent_backend - property ListView listView - property int listViewIndex - - property bool highlighted: backend.selected - - function labelButtonReleased(mouse) { - if (mouse.button == Qt.LeftButton) { - if(mouse.modifiers & Qt.ControlModifier) { - backend.selected = !backend.selected - } else if(mouse.modifiers == Qt.NoModifier) { - parent_backend.setSelection([backend.cuuid]) - } - } - } - - DelegateChooser { - id: chooser - role: "type" - DelegateChoice { - roleValue: "ContainerDivider"; - XsPlaylistDividerWidget { - anchors.left: parent.left - anchors.right: parent.right - text: object.name - type: object.type - parent_backend: playlist_group.parent_backend - backend: object - } - } - DelegateChoice { - roleValue: "Subset"; - XsPlaylistSubsetWidget { - anchors.left: parent.left - anchors.right: parent.right - text: object.name - type: object.type - backend: object - } - } - DelegateChoice { - roleValue: "Timeline"; - XsPlaylistTimelineWidget { - anchors.left: parent.left - anchors.right: parent.right - text: object.name - type: object.type - backend: object - } - } - DelegateChoice { - roleValue: "ContactSheet"; - XsPlaylistContactSheetWidget { - anchors.left: parent.left - anchors.right: parent.right - text: object.name - type: object.type - backend: object - } - } } - - XsSessionBarWidget { - id: playlist_control - text: playlist_group.text - type_icon_source: "qrc:///feather_icons/server.svg" - type_icon_color: XsStyle.highlightColor - search_visible: false - color: highlighted ? Qt.tint(XsStyle.menuBorderColor, Qt.rgba(tint.r,tint.g,tint.b, (tint.a ? 0.1 : 0.0))) : Qt.tint(XsStyle.mainBackground,Qt.rgba(tint.r,tint.g,tint.b, (tint.a ? 0.1 : 0.0))) - tint: backend.flag - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - icon_border_color: XsStyle.menuBorderColor - onPlusPressed: plusMenu.toggleShow() - onMorePressed: moreMenu.toggleShow() - onLabelPressed: labelButtonReleased(mouse) - highlighted: playlist_group.highlighted - expanded: backend.expanded - onExpandClicked: backend.expanded = !backend.expanded - expand_visible: backend.itemModel.rowCount() - } - - ListView { - id: layout - implicitHeight: contentItem.childrenRect.height - anchors.top: playlist_control.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.leftMargin: playlist_control.icon_width * 2 - clip: true - visible: playlist_control.expanded - model: backend.itemModel - delegate: chooser - focus: true - } - - XsStringRequestDialog { - id: request_name - okay_text: "Rename" - text: "noname" - onOkayed: parent_backend.renameContainer(text, backend.cuuid) - y: playlist_control.more_button.y - } - - function rename() { - request_name.text = backend.name - request_name.open() - } - - XsMenu { - id: moreMenu - x: playlist_control.more_button.x - y: playlist_control.more_button.y - - fakeDisabled: true - - XsFlagMenu { - flag: backend.flag - onFlagHexChanged: { - if(backend.flag !== flagHex) - parent_backend.reflagContainer(flagHex, backend.cuuid) - } - } - - XsMenuItem { - mytext: qsTr("Rename") - onTriggered: { - rename() - } - } - XsMenuItem { - mytext: qsTr("Duplicate") - onTriggered: { - var nextItem = ListView.view.itemAtIndex(index+1) - if(nextItem) - parent_backend.duplicateContainer(backend.cuuid, nextItem.backend.cuuid) - else - parent_backend.duplicateContainer(backend.cuuid) - } - } - XsMenuItem { - mytext: qsTr("Remove") - onTriggered: { - parent_backend.removeContainer(backend.cuuid) - } - } - } - - XsStringRequestDialog { - id: request_new_divider - property var uuid - property bool into - okay_text: "New Divider" - text: "Untitled Divider" - onOkayed: {parent_backend.createDivider(text, uuid, into); backend.expanded = true;} - y: playlist_control.plus_button.y -// x: playlist_control.plus_button.x - } - - XsStringRequestDialog { - id: request_new_timeline - property var uuid - property bool into - okay_text: "New Timeline" - text: "Untitled Timeline" - onOkayed: {parent_backend.createTimeline(text, uuid, into); backend.expanded = true;} - y: playlist_control.plus_button.y -// x: playlist_control.plus_button.x - } - - XsStringRequestDialog { - id: request_new_subset - property var uuid - property bool into - okay_text: "New Subset" - text: "Untitled Subset" - onOkayed: {parent_backend.createSubset(text, uuid, into); backend.expanded = true;} - y: playlist_control.plus_button.y -// x: playlist_control.plus_button.x - } - - XsStringRequestDialog { - id: request_new_contact_sheet - property var uuid - property bool into - okay_text: "New Contact Sheet" - text: "Untitled Contact Sheet" - onOkayed: {parent_backend.createContactSheet(text, uuid, into); backend.expanded = true;} - y: playlist_control.plus_button.y -// x: playlist_control.plus_button.x - } - - XsMenu { - id: plusMenu - x: playlist_control.plus_button.x - y: playlist_control.plus_button.y - - fakeDisabled: true - XsMenuItem { - mytext: qsTr("New Divider") - onTriggered: { - backend.expanded = true - request_new_divider.uuid = layout.currentItem ? layout.currentItem.backend.cuuid : backend.cuuid - request_new_divider.into = layout.currentItem ? false : true - request_new_divider.open() - } - } - - XsMenuItem { - mytext: qsTr("New Timeline") - // enabled: layout.currentItem && layout.currentItem.type === "Playlist" - onTriggered: { - backend.expanded = true - request_new_timeline.uuid = layout.currentItem ? layout.currentItem.backend.cuuid : backend.cuuid - request_new_timeline.into = layout.currentItem ? false : true - request_new_timeline.open() - } - } - - XsMenuItem { - mytext: qsTr("New Subset") - // enabled: layout.currentItem && layout.currentItem.type === "Playlist" - onTriggered: { - backend.expanded = true - request_new_subset.uuid = layout.currentItem ? layout.currentItem.backend.cuuid : backend.cuuid - request_new_subset.into = layout.currentItem ? false : true - request_new_subset.open() - } - } - - XsMenuItem { - mytext: qsTr("New Contact Sheet") - enabled: false - - // enabled: layout.currentItem && layout.currentItem.type === "Playlist" - onTriggered: { - backend.expanded = true - request_new_contact_sheet.uuid = layout.currentItem ? layout.currentItem.backend.cuuid : backend.cuuid - request_new_contact_sheet.into = layout.currentItem ? false : true - request_new_contact_sheet.open() - } - } - } -} - - - diff --git a/retired/XsPlaylistSubsetWidget.qml b/retired/XsPlaylistSubsetWidget.qml deleted file mode 100644 index dbbc735e9..000000000 --- a/retired/XsPlaylistSubsetWidget.qml +++ /dev/null @@ -1,239 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 1.4 -import QtQuick 2.4 -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.12 -import QtQml.Models 2.12 -import Qt.labs.qmlmodels 1.0 -import QtQuick.Dialogs 1.0 -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudio 1.0 - -Rectangle { - id: playlist - color: XsStyle.mainBackground - implicitHeight: playlist_control.implicitHeight - property string text - property var backend - property string type - property var parent_backend - property ListView listView - property int listViewIndex - - property bool highlighted: backend.selected - - property bool is_drag_drop_target: false - function not_drag_drop_target() { - is_drag_drop_target = false - } - function dragging_items_from_media_list(source_uuid, mousePos) { - - is_drag_drop_target = true - } - - function labelButtonReleased(mouse) { - if (mouse.button == Qt.LeftButton) { - if(mouse.modifiers & Qt.ControlModifier) { - backend.selected = !backend.selected - } else if(mouse.modifiers == Qt.NoModifier) { - parent_backend.setSelection([backend.cuuid]) - } - } - } - Timer { - id: busy - interval: 500; - running: false; - repeat: false - onTriggered: playlist_control.busy.running = false - } - - Connections { - target: backend - function onMediaAdded(uuid) { - busy.stop() - playlist_control.busy.running = true - busy.start() - } - } - XsSessionBarWidget { - id: playlist_control - property bool has_drag: false - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - tint: backend.flag - - text: playlist.text - type_icon_source: "qrc:///feather_icons/trello.svg" - type_icon_color: XsStyle.highlightColor - search_visible: false - expand_visible: false - expand_button_holder: true - color: { - has_drag ? XsStyle.highlightColor : ( - highlighted ? XsStyle.menuBorderColor : (hovered ? XsStyle.controlBackground : XsStyle.mainBackground) - ) - } - gradient: has_drag ? styleGradient.accent_gradient : null - icon_border_color: XsStyle.menuBorderColor - highlighted: playlist.highlighted - child_count: playlist.backend.mediaList.length - - onLabelPressed: labelButtonReleased(mouse) - onPlusPressed: {parent_backend.setSelection([backend.cuuid]); plusMenu.toggleShow()} - onMorePressed: {parent_backend.setSelection([backend.cuuid]); moreMenu.toggleShow()} - - onLabelDoubleClicked: { - playerWidget.switchSource(backend) - } - - DropArea { - anchors.fill: playlist_control.drop_area - - onEntered: { - if(drag.hasUrls || drag.hasText) { - drag.acceptProposedAction() - playlist_control.has_drag = true - } - } - onExited: { - playlist_control.has_drag = false - } - onDropped: { - playlist_control.has_drag = false - - // if session then ignore everying else.. - if(drop.hasUrls) { - for(var i=0; i < drop.urls.length; i++) { - if(drop.urls[i].toLowerCase().endsWith('.xst')) { - session.loadUrl(drop.urls[i]) - app_window.sessionFunction.newRecentPath(drop.urls[i]) - return; - } - } - } - - // prepare drop data - let data = {} - for(let i=0; i< drop.keys.length; i++){ - data[drop.keys[i]] = drop.getDataAsString(drop.keys[i]) - } - Future.promise(backend.handleDropFuture(data)).then(function(quuids){}) - } - } - } - - Rectangle { - id: drag_target_highlight - anchors.fill: parent - color: "transparent" - border.color: "white" - border.width: 2 - visible: playlist.is_drag_drop_target - z: 100 - } - - XsStringRequestDialog { - id: request_name - okay_text: "Rename" - text: "noname" - onOkayed: backend.setName(text) - x: XsUtils.centerXInParent(panel, parent, width) - y: XsUtils.centerYInParent(panel, parent, height) - } - - XsButtonDialog { - id: removeContainer - text: "Remove" - width: 300 - buttonModel: ["Cancel", "Subset And Media", "Subset"] - onSelected: { - if(button_index == 2) { - parent_backend.removeContainer(backend.cuuid) - } else if(button_index == 1) { - parent_backend.removeContainer(backend.cuuid, true) - } - } - } - - XsMenu { - id: moreMenu - x: playlist_control.more_button.x - y: playlist_control.more_button.y - - fakeDisabled: true - - XsFlagMenu { - flag: backend.flag - onFlagHexChanged: { - if(backend.flag !== flagHex) - parent_backend.reflagContainer(flagHex, backend.cuuid) - } - } - - XsMenuSeparator {} - - XsMenuItem { - mytext: qsTr("Rename...") - onTriggered: { - request_name.text = backend.name - request_name.open() - } - } - XsMenuItem { - mytext: qsTr("Duplicate") - onTriggered: { - var nextItem = listView.itemAtIndex(listViewIndex+1) - if(nextItem) - parent_backend.duplicateContainer(backend.cuuid, nextItem.contentItem.backend.cuuid) - else - parent_backend.duplicateContainer(backend.cuuid) - } - } - - XsMenu { - title: "Convert To" - XsMenuItem { - enabled: false - mytext: qsTr("Contact Sheet") - onTriggered: { - parent_backend.convertToContactSheet(backend.cuuid) - } - } - XsMenuItem { - mytext: qsTr("Timeline") - enabled: false - } - } - - XsMenuSeparator {} - - XsMenuItem { - mytext: qsTr("Remove") - onTriggered: { - removeContainer.open() - } - } - } - - XsMenu { - id: plusMenu - x: playlist_control.plus_button.x - y: playlist_control.plus_button.y - - fakeDisabled: true - - XsMenuItem { - mytext: qsTr("Add Media...") - onTriggered: XsUtils.openDialogPlaylist("qrc:/dialogs/XsAddMediaDialog.qml").open() - } - XsMenuItem { - mytext: qsTr("Add Media From Clipboard") - onTriggered: app_window.session.add_media_from_clipboard(backend) - } - } -} diff --git a/retired/XsPlaylistTimelineWidget.qml b/retired/XsPlaylistTimelineWidget.qml deleted file mode 100644 index c8f564a09..000000000 --- a/retired/XsPlaylistTimelineWidget.qml +++ /dev/null @@ -1,239 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 1.4 -import QtQuick 2.4 -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.12 -import QtQml.Models 2.12 -import Qt.labs.qmlmodels 1.0 -import QtQuick.Dialogs 1.0 -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudio 1.0 - -Rectangle { - id: playlist - color: "green" - implicitHeight: playlist_control.implicitHeight - property string text - property var backend - property string type - property var parent_backend - property ListView listView - property int listViewIndex - - property bool highlighted: backend.selected - - property bool is_drag_drop_target: false - - function not_drag_drop_target() { - is_drag_drop_target = false - } - function dragging_items_from_media_list(source_uuid, mousePos) { - is_drag_drop_target = true - } - - function labelButtonReleased(mouse) { - if (mouse.button == Qt.LeftButton) { - if(mouse.modifiers & Qt.ControlModifier) { - backend.selected = !backend.selected - } else if(mouse.modifiers == Qt.NoModifier) { - parent_backend.setSelection([backend.cuuid]) - } - } - } - - Timer { - id: busy - interval: 500; - running: false; - repeat: false - onTriggered: playlist_control.busy.running = false - } - - Connections { - target: backend - function onMediaAdded(uuid) { - busy.stop() - playlist_control.busy.running = true - busy.start() - } - } - - XsSessionBarWidget { - id: playlist_control - property bool has_drag: false - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - tint: backend.flag - - text: playlist.text - type_icon_source: "qrc:///feather_icons/align-left.svg" - type_icon_color: XsStyle.highlightColor - search_visible: false - expand_visible: false - expand_button_holder: true - color: { - has_drag ? XsStyle.highlightColor : ( - highlighted ? XsStyle.menuBorderColor : (hovered ? XsStyle.controlBackground : XsStyle.mainBackground) - ) - } - gradient: has_drag ? styleGradient.accent_gradient : null - icon_border_color: XsStyle.menuBorderColor - highlighted: playlist.highlighted - child_count: playlist.backend.mediaList.length - onLabelPressed: labelButtonReleased(mouse) - onPlusPressed: {parent_backend.setSelection([backend.cuuid]); plusMenu.toggleShow()} - onMorePressed: {parent_backend.setSelection([backend.cuuid]); moreMenu.toggleShow()} - - onLabelDoubleClicked: { - playerWidget.switchSource(backend) - } - - DropArea { - anchors.fill: playlist_control.drop_area - - onEntered: { - if(drag.hasUrls || drag.hasText) { - drag.acceptProposedAction() - playlist_control.has_drag = true - } - } - onExited: { - playlist_control.has_drag = false - } - onDropped: { - // playlist_control.has_drag = false - - // // if session then ignore everying else.. - // if(drop.hasUrls) { - // for(var i=0; i < drop.urls.length; i++) { - // if(drop.urls[i].toLowerCase().endsWith('.xst')) { - // session.loadUrl(drop.urls[i]) - // return; - // } - // } - // } - - // // prepare drop data - // let data = {} - // for(let i=0; i< drop.keys.length; i++){ - // data[drop.keys[i]] = drop.getDataAsString(drop.keys[i]) - // } - // Future.promise(backend.handleDropFuture(data)).then(function(quuids){}) - } - } - } - - Rectangle { - id: drag_target_highlight - anchors.fill: parent - color: "transparent" - border.color: "white" - border.width: 2 - visible: playlist.is_drag_drop_target - z: 100 - } - XsStringRequestDialog { - id: request_name - okay_text: "Rename" - text: "noname" - onOkayed: backend.setName(text) - x: XsUtils.centerXInParent(panel, parent, width) - y: XsUtils.centerYInParent(panel, parent, height) - } - - XsButtonDialog { - id: removeContainer - text: "Remove" - width: 300 - buttonModel: ["Cancel", "Timeline And Media", "Timeline"] - onSelected: { - if(button_index == 2) { - parent_backend.removeContainer(backend.cuuid) - } else if(button_index == 1) { - parent_backend.removeContainer(backend.cuuid, true) - } - } - } - - XsMenu { - id: moreMenu - x: playlist_control.more_button.x - y: playlist_control.more_button.y - - fakeDisabled: true - - XsFlagMenu { - flag: backend.flag - onFlagHexChanged: { - if(backend.flag !== flagHex) - parent_backend.reflagContainer(flagHex, backend.cuuid) - } - } - - XsMenuSeparator {} - - XsMenuItem { - mytext: qsTr("Rename...") - onTriggered: { - request_name.text = backend.name - request_name.open() - } - } - XsMenuItem { - mytext: qsTr("Duplicate") - onTriggered: { - var nextItem = listView.itemAtIndex(listViewIndex+1) - if(nextItem) - parent_backend.duplicateContainer(backend.cuuid, nextItem.contentItem.backend.cuuid) - else - parent_backend.duplicateContainer(backend.cuuid) - } - } - - XsMenu { - title: "Convert To" - XsMenuItem { - enabled: false - mytext: qsTr("Contact Sheet") - onTriggered: { - parent_backend.convertToContactSheet(backend.cuuid) - } - } - XsMenuItem { - enabled: false - mytext: qsTr("Subset") - onTriggered: { - parent_backend.convertToSubset(backend.cuuid) - } - } - } - - XsMenuSeparator {} - XsMenuItem { - mytext: qsTr("Remove") - onTriggered: { - removeContainer.open() - } - } - } - - XsMenu { - id: plusMenu - x: playlist_control.plus_button.x - y: playlist_control.plus_button.y - - fakeDisabled: true - - XsMenuItem { - mytext: qsTr("Add Media...") - onTriggered: XsUtils.openDialogPlaylist("qrc:/dialogs/XsAddMediaDialog.qml").open() - } - XsMenuItem { - mytext: qsTr("Add Media From Clipboard") - onTriggered: app_window.session.add_media_from_clipboard(backend) - } - } -} diff --git a/retired/XsPlaylistWidget.qml b/retired/XsPlaylistWidget.qml deleted file mode 100644 index 9fa8375c6..000000000 --- a/retired/XsPlaylistWidget.qml +++ /dev/null @@ -1,542 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 1.4 -import QtQuick 2.12 -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.12 -import QtQml.Models 2.12 -import Qt.labs.qmlmodels 1.0 -import QtQuick.Dialogs 1.0 -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudio 1.0 -import xstudio.qml.uuid 1.0 - -Rectangle { - id: playlist - color: XsStyle.mainBackground - height: playlist_control.implicitHeight + (playlist_control.expanded ? layout.contentItem.childrenRect.height + 5 : 0) - property string text - property var backend - property string type - property var parent_backend - - property bool highlighted: backend.selected - property ListView listView - property int listViewIndex - - function labelButtonReleased(mouse) { - if (mouse.button == Qt.LeftButton) { - if(mouse.modifiers & Qt.ControlModifier) { - backend.selected = !backend.selected - } else if(mouse.modifiers == Qt.NoModifier) { - parent_backend.setSelection([backend.cuuid]) - } - } - } - - Connections { - target: backend - function onSelectedSubitemChanged() { - if(!backend.selectedSubitem) { - // select playlist as subgroup no longer valid. - parent_backend.setSelection([backend.cuuid]) - - // does current backend exist.. - if(!session.onScreenSource || !session.onScreenSource.hasBackend) { - // selects a valid backend - session.switchOnScreenSource(null_uuid.asQuuid) - } - } - } - } - // Timer { - // id: busy - // interval: 500; - // running: false; - // repeat: false - // onTriggered: playlist_control.busy.running = false - // } - - property var internal_drag_drop_cursor: sessionWidget.internal_drag_drop_cursor_position - property var internal_drag_drop_source_playlist_uuid: sessionWidget.internal_drag_drop_source_uuid - property bool is_drag_drop_target: false - - XsSessionBarWidget { - busy.running: playlist_control.isLoadingMedia || playlist_control.isBusy - id: playlist_control - property bool has_drag: false - text: playlist.text - type_icon_source: "qrc:///feather_icons/list.svg" - type_icon_color: XsStyle.highlightColor - color: { - has_drag ? XsStyle.highlightColor : ( - highlighted ? XsStyle.menuBorderColor : (hovered ? XsStyle.controlBackground : XsStyle.mainBackground) - ) - } - gradient: has_drag ? styleGradient.accent_gradient : null - - progress: app_window.events[backend.uuid.toString()] ? app_window.events[backend.uuid.toString()].progressPercentage : 0 - property bool is_drag_drop_target: false - Rectangle { - id: drag_target_highlight - anchors.fill: parent - color: "transparent" - border.color: "white" - border.width: 2 - visible: playlist_control.is_drag_drop_target - x: 10000 - } - decoratorModel: ( - app_window.sessionModel.tags[backend.uuid] ? - ( - app_window.sessionModel.tags[backend.uuid]["Decorator"] ? app_window.sessionModel.tags[backend.uuid]["Decorator"].tags : null - ) - : null - ) - // decoratorModel: {backend ? backend.decorators : null} - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - tint: backend.flag - child_count: playlist.backend.mediaModel.length - issue_count: playlist.backend.offlineMediaCount - - property var isLoadingMedia: backend.loadingMedia - property var isBusy: backend.isBusy - - icon_border_color: XsStyle.menuBorderColor - onLabelPressed: labelButtonReleased(mouse) - onPlusPressed: {parent_backend.setSelection([backend.cuuid]); plusMenu.toggleShow()} - onMorePressed: {parent_backend.setSelection([backend.cuuid],false);moreMenu.toggleShow()} - highlighted: playlist.highlighted - expand_button_holder: true - search_visible: false - expanded: backend.expanded - expand_visible: backend.itemModel.rowCount() - onExpandClicked: backend.expanded = !backend.expanded - - onLabelDoubleClicked: { - playerWidget.switchSource(backend) - } - - DropArea { - anchors.fill: playlist_control.drop_area - - onEntered: { - if(drag.hasUrls || drag.hasText) { - drag.acceptProposedAction() - playlist_control.has_drag = true - } - } - onExited: { - playlist_control.has_drag = false - } - onDropped: { - playlist_control.has_drag = false - // if session then ignore everying else.. - if(drop.hasUrls) { - for(var i=0; i < drop.urls.length; i++) { - if(drop.urls[i].toLowerCase().endsWith('.xst')) { - Future.promise(studio.loadSessionRequestFuture(drop.urls[i])).then(function(result){}) - app_window.sessionFunction.newRecentPath(drop.urls[i]) - return; - } - } - } - - // prepare drop data - let data = {} - for(let i=0; i< drop.keys.length; i++){ - data[drop.keys[i]] = drop.getDataAsString(drop.keys[i]) - } - Future.promise(backend.handleDropFuture(data)).then(function(quuids){}) - } - } - } - - property var internal_drag_drop_target - - function not_drag_drop_target() { - if (internal_drag_drop_target) { - internal_drag_drop_target.not_drag_drop_target() - internal_drag_drop_target = null - } else if (playlist_control.is_drag_drop_target) { - playlist_control.is_drag_drop_target = false - } - } - - XsButtonDialog { - id: move_or_copy - text: "Add Media" - width: 200 - buttonModel: ["Cancel", "Move", "Copy"] - property var dropped_media_uuids: null - property var dest_uuid: null - property var src_uuid: null - onSelected: { - if(button_index == 2) { - session.copyMedia(dest_uuid, dropped_media_uuids); - } else if(button_index == 1) { - session.moveMedia(dest_uuid, src_uuid, dropped_media_uuids); - } - } - } - - function dropping_items_from_media_list(source_uuid, source_playlist, drag_drop_items, mousePos) { - - // clear drag-drop highlight status - if (internal_drag_drop_target) { - internal_drag_drop_target.not_drag_drop_target() - } - playlist_control.is_drag_drop_target = false - - var dropped_media_uuids = [] - for (var i in drag_drop_items) { - dropped_media_uuids.push(drag_drop_items[i].uuid) - } - - var local_pos = playlist.mapFromGlobal(mousePos) - - // drop target is this playlist - if (local_pos.y > 0.0 && local_pos.y < playlist_control.height) { - - // n.b. source_uuid can come from a subset (of this playlist) - can't drag drop - // from a subset of this playlist into this playlist - if (source_uuid != backend.uuid && source_playlist.uuid != backend.uuid) { - move_or_copy.dropped_media_uuids = dropped_media_uuids - move_or_copy.dest_uuid = backend.uuid - move_or_copy.src_uuid = source_uuid - move_or_copy.open() - } - - } else { - - // drop target is a child of this playlist - var item = playlistMainContent.the_view.layout.itemAt(local_pos.x, local_pos.y-playlistMainContent.y) - if (item && item.content) { - if (item.content.backend.uuid != source_uuid) { - // check if media is from parent.. - if(backend.uuid != source_uuid) { - move_or_copy.dropped_media_uuids = dropped_media_uuids - move_or_copy.dest_uuid = item.content.backend.uuid - move_or_copy.src_uuid = source_uuid - move_or_copy.open() - } else { - session.copyMedia(item.content.backend.uuid, dropped_media_uuids); - } - } - } - } - - } - - function dragging_items_from_media_list(source_uuid, parent_playlist_uuid, mousePos) { - - var local_pos = playlist.mapFromGlobal(mousePos) - - if (local_pos.y > 0.0 && local_pos.y < playlist_control.height) { - if (internal_drag_drop_target) { - internal_drag_drop_target.not_drag_drop_target() - internal_drag_drop_target = null - } - if (source_uuid != backend.uuid && backend.uuid != parent_playlist_uuid) { - playlist_control.is_drag_drop_target = true - } - return - } else if (playlist_control.is_drag_drop_target) { - playlist_control.is_drag_drop_target = false - } - - var item = playlistMainContent.the_view.layout.itemAt(local_pos.x, local_pos.y-playlistMainContent.y) - if (item && item.content) { - if (internal_drag_drop_target != item.content) { - if (internal_drag_drop_target) { - internal_drag_drop_target.not_drag_drop_target() - } - if (item.content.backend.uuid != source_uuid) - { - internal_drag_drop_target = item.content - internal_drag_drop_target.dragging_items_from_media_list(source_uuid, mousePos) - } else { - internal_drag_drop_target = null - } - } - } else if (internal_drag_drop_target) { - internal_drag_drop_target.not_drag_drop_target() - internal_drag_drop_target = null - } - } - - - property alias playlistMainContent: playlistMainContent - - Item { - id: playlistMainContent - property alias the_view: cl.the_view - - anchors { - left: parent.left - right: parent.right - top: playlist_control.bottom - bottom: parent.bottom - } - - height: the_view.layout.contentItem.childrenRect.height - - - ColumnLayout { - anchors.fill: parent - spacing: 0 - id: cl - property alias the_view: the_view - - ScrollView { - Layout.fillWidth: true - Layout.fillHeight: true - id: the_view - property alias layout: layout - - ListView { - id: layout - // implicitHeight: contentItem.childrenRect.height - // anchors.top: playlist_control.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.bottom: parent.bottom - anchors.leftMargin: (playlist_control.icon_width + 5) - visible: playlist_control.expanded - model: backend.itemModel - delegate: XsSessionChooser {} - focus: true - clip: true - } - } - } - } - - // XsBusyIndicator { - // Layout.fillHeight: true - // Layout.preferredWidth: height - // running: playlist_control.isLoadingMedia || playlist_control.isBusy - // } - - // BusyIndicator { - // anchors.right: parent.right - // anchors.top: parent.top - // anchors.bottom: parent.bottom - // anchors.margins: 2 - // running: playlist_control.isLoadingMedia || playlist_control.isBusy - // } - - // ListView { - // id: layout - // implicitHeight: contentItem.childrenRect.height - // anchors.top: playlist_control.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.bottom: parent.bottom - // anchors.leftMargin: (playlist_control.icon_width + 5)*2 - // clip: true - // visible: playlist_control.expanded - // model: backend.itemModel - // delegate: XsSessionChooser {} - // focus: true - // } - - - XsStringRequestDialog { - id: request_name - okay_text: "Rename" - text: "noname" - onOkayed: backend.setName(text) - centerOn: playlist_panel - y: playlist_panel.mapToGlobal(0, 25).y - } - - function onlySubTypes(obj) { - if(obj.type == "Subset" || obj.type == "Timeline" || obj.type == "ContactSheet" || obj.type == "ContainerDivider" ) { - return true - } - return false - } - - XsMenu { - id: moreMenu - x: playlist_control.more_button.x - y: playlist_control.more_button.y - - fakeDisabled: true - - XsFlagMenu { - showChecked: false - onFlagSet: app_window.sessionFunction.flagSelected(hex) - } - - XsMenuItem { - mytext: qsTr("Combine Selected") - onTriggered: { - app_window.session.mergePlaylists(XsUtils.getSelectedCuuids(app_window.session)) - } - } - XsMenuItem { - mytext: qsTr("Export As Session...") - onTriggered: app_window.sessionFunction.saveSelectedSessionDialog() - } - - XsMenuSeparator {} - - XsMenuItem { - mytext: qsTr("Rename...") - onTriggered: { - request_name.text = backend.name - request_name.open() - } - } - XsMenuItem { - mytext: qsTr("Duplicate") - onTriggered: { - var nextItem = listView.itemAtIndex(listViewIndex+1) - if(nextItem) - parent_backend.duplicateContainer(backend.cuuid, nextItem.contentItem.backend.cuuid) - else - parent_backend.duplicateContainer(backend.cuuid) - } - } - - // XsMenuItem { - // mytext: qsTr("Copy Selected To") - // enabled: false - // onTriggered: { - // var selected_cuuids = XsUtils.getSelectedCuuids(app_window.session, true, onlySubTypes) - // parent_backend.copyContainers(selected_cuuids, backend.cuuid) - // } - // } - // XsMenuItem { - // mytext: qsTr("Move Selected To") - // enabled: false - // onTriggered: { - // var selected_cuuids = XsUtils.getSelectedCuuids(app_window.session, true, onlySubTypes) - // parent_backend.moveContainers(selected_cuuids, backend.cuuid, false) - // } - // } - // XsMenuItem { - // mytext: qsTr("Move (With Media) Selected To") - // enabled: false - // onTriggered: { - // var selected_cuuids = XsUtils.getSelectedCuuids(app_window.session, true, onlySubTypes) - // parent_backend.moveContainers(selected_cuuids, backend.cuuid, true) - // } - // } - - XsMenuSeparator { - visible: app_window.sessionModel.tags[backend.uuid] ? (app_window.sessionModel.tags[backend.uuid]["Menu"] ? app_window.sessionModel.tags[backend.uuid]["Menu"].tags.length : 0 ) : 0 - } - - // decorators - Repeater { - id: additionalMenus - model: ( - app_window.sessionModel.tags[backend.uuid] ? - ( - app_window.sessionModel.tags[backend.uuid]["Menu"] ? app_window.sessionModel.tags[backend.uuid]["Menu"].tags : null - ) - : null - ) - XsDecoratorWidget { - widgetString: modelData.data - } - onItemAdded: moreMenu.insertMenu(moreMenu.count-2,item.widget_ui) - onItemRemoved: moreMenu.removeMenu(item.widget_ui) - } - - XsMenuSeparator {} - - XsMenuItem { - mytext: qsTr("Remove Selected") - onTriggered: app_window.sessionFunction.removeSelected() - } - } - - QMLUuid { - id: null_uuid - } - - XsStringRequestDialog { - id: request_new_group - property var uuid - okay_text: "New Group" - text: "Untitled Group" - onOkayed: {backend.createGroup(text, uuid); backend.expanded = true;} - centerOn: playlist_panel - y: playlist_panel.mapToGlobal(0, 25).y - } - - XsNewPlaylistDividerDialog { - id: request_new_divider - playlist: backend - } - - XsNewTimelineDialog { - id: request_new_timeline - playlist: backend - } - XsNewSubsetDialog { - id: request_new_subset - playlist: backend - } - XsNewContactSheetDialog { - id: request_new_contact_sheet - playlist: backend - } - - XsMenu { - id: plusMenu - x: playlist_control.plus_button.x - y: playlist_control.plus_button.y - - fakeDisabled: true - - XsMenuItem { - mytext: qsTr("New Subset") - onTriggered: request_new_subset.open() - } - - XsMenuItem { - enabled: false - mytext: qsTr("New Contact Sheet") - onTriggered: request_new_contact_sheet.open() - } - - XsMenuItem { - mytext: qsTr("New Timeline") - onTriggered: request_new_timeline.open() - enabled: false - } - - XsMenuItem { - mytext: qsTr("New Divider") - onTriggered: request_new_divider.open() - } - - XsMenuSeparator {} - - XsMenuItem { - mytext: qsTr("Add Media...") - onTriggered: XsUtils.openDialogPlaylist("qrc:/dialogs/XsAddMediaDialog.qml").open() - } - XsMenuItem { - mytext: qsTr("Add Media From Clipboard") - onTriggered: app_window.session.add_media_from_clipboard(backend) - } - - // XsMenuItem { - // mytext: qsTr("New Group") - // onTriggered: { - // backend.expanded = true - // request_new_group.uuid = layout.currentItem ? layout.currentItem.backend.cuuid : null_uuid.asQuuid - // request_new_group.open() - // } - // } - } -} diff --git a/retired/XsPlaylistsPanel.qml b/retired/XsPlaylistsPanel.qml deleted file mode 100644 index 2c6e6efde..000000000 --- a/retired/XsPlaylistsPanel.qml +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 2.3 -import QtQuick 2.14 -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.12 -import QtQml.Models 2.12 -import QtQml 2.12 -import Qt.labs.qmlmodels 1.0 -import xstudio.qml.uuid 1.0 -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudio 1.0 - -Rectangle { - id: panel - color: XsStyle.mainBackground - property var internalDragDropCursor - property var dragdropSourcePlaylistUuid - - Keys.onPressed: { - console.log("move left 2", event); - } - focus: true - - // function dragCursorFromMediaList(playlist_uuid, pos) { - // var localPos = mapFromGlobal(pos.x, pos.y) - // if (localPos.x > 0.0 && localPos.y > 0.0 && localPos.x < width && localPos.y < height) { - // internalDragDropCursor = pos - // dragdropSourcePlaylistUuid = playlist_uuid - // } - // } - - DropArea { - anchors.fill: parent - - onEntered: { - if(drag.hasUrls || drag.hasText) { - drag.acceptProposedAction() - } - } - onDropped: { - if(drop.hasUrls) { - for(var i=0; i < drop.urls.length; i++) { - if(drop.urls[i].toLowerCase().endsWith(".xst")) { - Future.promise(studio.loadSessionRequestFuture(drop.urls[i])).then(function(result){}) - app_window.sessionFunction.newRecentPath(drop.urls[i]) - return - } - } - } - - let data = {} - for(let i=0; i< drop.keys.length; i++){ - data[drop.keys[i]] = drop.getDataAsString(drop.keys[i]) - } - Future.promise(session.handleDropFuture(data)).then(function(quuids){}) - } - } - - Label { - text: 'Drop media or folders here\nto create new playlists' - color: XsStyle.controlTitleColor - opacity: 0.3 - visible: { - return session ? session.itemModel.rowCount() == 0 : 1 - } - anchors.centerIn: parent - verticalAlignment: Qt.AlignVCenter - horizontalAlignment: Qt.AlignHCenter - font.pixelSize: 14 - - } - - DelegateChooser { - id: chooser - role: "type" - } - - - property var internal_drag_drop_target - - function dropping_items_from_media_list(source_uuid, source_playlist, drag_drop_items, mousePos) { - - var local_pos = mapFromGlobal(mousePos) - var item = mainContent.the_view.layout.itemAt(local_pos.x + layout.contentX, local_pos.y + layout.contentY) - - if (item && item.content) { - item.content.dropping_items_from_media_list(source_uuid, source_playlist, drag_drop_items, mousePos) - } - - } - - function dragging_items_from_media_list(source_uuid, parent_playlist_uuid, drag_drop_items, mousePos) { - - var local_pos = mapFromGlobal(mousePos) - var item = mainContent.the_view.layout.itemAt(local_pos.x + layout.contentX, local_pos.y + layout.contentY) - - if (item && item.content) { - if (internal_drag_drop_target != item.content) { - if (internal_drag_drop_target) { - internal_drag_drop_target.not_drag_drop_target() - } - internal_drag_drop_target = item.content - } - internal_drag_drop_target.dragging_items_from_media_list(source_uuid, parent_playlist_uuid, mousePos) - } else if (internal_drag_drop_target) { - internal_drag_drop_target.not_drag_drop_target() - internal_drag_drop_target = null - } - - } - - - property alias mainContent: mainContent - - Item { - - id: mainContent - property alias the_view: cl.the_view - - z: 10 - focus: true - - anchors { - left: parent.left - right: parent.right - top: parent.top - bottom: parent.bottom - } - ColumnLayout { - - anchors.fill: parent - spacing: 0 - id: cl - property alias the_view: the_view - - ScrollView { - Layout.fillWidth: true - Layout.fillHeight: true - id: the_view - property alias layout: layout - // ScrollBar.vertical.policy: ScrollBar.AlwaysOn - ListView { - id: layout - implicitHeight: contentItem.childrenRect.height - model: session ? session.itemModel : null - delegate: chooser - focus: true - clip: true - } - } - } - } - -} \ No newline at end of file diff --git a/retired/XsSessionChooser.qml b/retired/XsSessionChooser.qml deleted file mode 100644 index aa5289b9b..000000000 --- a/retired/XsSessionChooser.qml +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import Qt.labs.qmlmodels 1.0 -import QtQuick.Controls 2 -import QtQuick 2.14 - -import xStudio 1.0 - -DelegateChooser { - role: "type" - DelegateChoice { - roleValue: "ContainerGroup"; - XsDraggableItem { - XsPlaylistSubGroupWidget { - id: content - anchors.centerIn: parent - width: layout.width - text: object.name - type: object.type - backend: object - parent_backend: playlist.backend - listView: layout - listViewIndex: index - } - draggedItemParent: playlistMainContent - - onMoveItemRequested: { - // console.log("move", from,to) - ListView.view.model.move(from, 1, to); - } - } - } - DelegateChoice { - roleValue: "ContainerDivider"; - XsDraggableItem { - XsPlaylistDividerWidget { - id: content - anchors.centerIn: parent - width: layout.width - text: object.name - type: object.type - backend: object - parent_backend: playlist.backend - listView: layout - listViewIndex: index - } - draggedItemParent: playlistMainContent - - onMoveItemRequested: { - // console.log("move", from,to) - ListView.view.model.move(from, 1, to); - } - } - } - DelegateChoice { - roleValue: "Subset"; - XsDraggableItem { - property alias content: content - XsPlaylistSubsetWidget { - id: content - anchors.centerIn: parent - width: layout.width - text: object.name - type: object.type - backend: object - parent_backend: playlist.backend - listView: layout - listViewIndex: index - } - draggedItemParent: playlistMainContent - - onMoveItemRequested: { - // console.log("move", from,to) - ListView.view.model.move(from, 1, to); - } - } - } - DelegateChoice { - roleValue: "Timeline"; - XsDraggableItem { - property alias content: content - XsPlaylistTimelineWidget { - id: content - anchors.centerIn: parent - width: layout.width - text: object.name - type: object.type - backend: object - parent_backend: playlist.backend - listView: layout - listViewIndex: index - } - draggedItemParent: playlistMainContent - - onMoveItemRequested: { - // console.log("move", from,to) - ListView.view.model.move(from, 1, to); - } - } - } - DelegateChoice { - roleValue: "ContactSheet"; - XsDraggableItem { - property alias content: content - XsPlaylistContactSheetWidget { - id: content - anchors.centerIn: parent - width: layout.width - text: object.name - type: object.type - backend: object - parent_backend: playlist.backend - listView: layout - listViewIndex: index - } - draggedItemParent: playlistMainContent - - onMoveItemRequested: { - // console.log("move", from,to) - ListView.view.model.move(from, 1, to); - } - } - } -} diff --git a/retired/contact_sheet/src/CMakeLists.txt b/retired/contact_sheet/src/CMakeLists.txt deleted file mode 100644 index 2006c25f2..000000000 --- a/retired/contact_sheet/src/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core - xstudio::ui::qml::helper - xstudio::utility -) - -SET(EXTRAMOC -) - -create_qml_component(contact_sheet 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/retired/contact_sheet/src/contact_sheet_ui.cpp b/retired/contact_sheet/src/contact_sheet_ui.cpp deleted file mode 100644 index 281265221..000000000 --- a/retired/contact_sheet/src/contact_sheet_ui.cpp +++ /dev/null @@ -1,373 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/ui/qml/playlist_ui.hpp" -#include "xstudio/ui/qml/playlist_selection_ui.hpp" -#include "xstudio/ui/qml/contact_sheet_ui.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/logging.hpp" - -using namespace caf; -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::ui::qml; - -ContactSheetUI::ContactSheetUI(const utility::Uuid cuuid, QObject *parent) - : QMLActor(parent), - cuuid_(std::move(cuuid)), - backend_(), - backend_events_(), - name_("unknown"), - flag_("#00000000") { - - auto *p = dynamic_cast(parent); - if (p) { - QObject::connect(p, SIGNAL(nameChanged()), this, SIGNAL(nameChanged())); - } - emit parent_playlistChanged(); - emit mediaModelChanged(); -} - -void ContactSheetUI::set_backend(caf::actor backend) { - scoped_actor sys{system()}; - - if (backend_events_) { - try { - request_receive( - *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - backend_events_ = caf::actor(); - } - - backend_ = backend; - - if (backend_) { - - - try { - auto detail = request_receive(*sys, backend_, detail_atom_v); - name_ = detail.name_; - uuid_ = detail.uuid_; - backend_events_ = detail.group_; - request_receive( - *sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - auto selection_actor = - request_receive(*sys, backend_, playlist::selection_actor_atom_v); - playlist_selection_ = new PlaylistSelectionUI(this); - playlist_selection_->initSystem(this); - playlist_selection_->set_backend(selection_actor); - emit playlistSelectionThingChanged(); - - update_media(); - emit nameChanged(); - emit uuidChanged(); - emit compareModeChanged(); - } - emit backendChanged(); - spdlog::debug("ContactSheetUI set_backend {}", to_string(uuid_)); -} - -void ContactSheetUI::setName(const QString &name) { - std::string _name = StdFromQString(name); - if (_name != name_) { - if (backend_) { - scoped_actor sys{system()}; - sys->anon_send(backend_, utility::name_atom_v, _name); - } - } -} - -QFuture> ContactSheetUI::loadMediaFuture(const QUrl &path) { - // expect URI's - return QtConcurrent::run([=]() { - QList result; - scoped_actor sys{system()}; - try { - auto actor = request_receive(*sys, backend_, playhead::source_atom_v); - - auto items = utility::scan_posix_path(StdFromQString(path.path())); - std::sort( - std::begin(items), - std::end(items), - [](std::pair a, std::pair b) { - return a.first < b.first; - }); - - for (const auto &i : items) { - try { - if (is_file_supported(i.first)) { - auto ua = request_receive( - *sys, - actor, - playlist::add_media_atom_v, - "New media", - i.first, - i.second); - result.push_back(QUuidFromUuid(ua.uuid())); - anon_send(backend_, playlist::add_media_atom_v, ua.uuid(), Uuid()); - } else { - spdlog::warn("Unsupported file type {}.", to_string(i.first)); - } - } catch (const std::exception &e) { - spdlog::error("Failed to create media {}", e.what()); - } - } - - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; - }); -} - - -// needs to add to playlist first.. -QFuture> ContactSheetUI::handleDropFuture(const QVariantMap &drop) { - // handle drag drop.. - return QtConcurrent::run([=]() { - scoped_actor sys{system()}; - QList results; - - auto jsn = dropToJsonStore(drop); - - auto playlist = request_receive(*sys, backend_, playhead::source_atom_v); - - - // conver to json.. - if (jsn.count("text/uri-list")) { - for (const auto &path : jsn["text/uri-list"]) { - auto uri = caf::make_uri(path); - if (uri) { - auto new_media = request_receive( - *sys, playlist, playlist::add_media_atom_v, *uri, true); - for (const auto &i : new_media) { - anon_send(backend_, playlist::add_media_atom_v, i.uuid(), Uuid()); - results.push_back(QUuidFromUuid(i.uuid())); - } - } - } - } else { - // forward to datasources for non file paths - auto pm = system().registry().template get(plugin_manager_registry); - try { - auto result = request_receive( - *sys, pm, data_source::use_data_atom_v, JsonStore(jsn), true); - if (not result.empty()) { - // we've got a collection of actors.. - // lets assume they are media... (WARNING this may not be the case...) - // create new playlist and add them... - for (const auto &i : result) { - anon_send(playlist, playlist::add_media_atom_v, i, utility::Uuid()); - anon_send(backend_, playlist::add_media_atom_v, i.uuid(), Uuid()); - results.push_back(QUuidFromUuid(i.uuid())); - } - } else { - // try file load.. - for (const auto &i : jsn["text/plain"]) { - auto uri = caf::make_uri("file:" + i.get()); - if (uri) { - auto new_media = request_receive( - *sys, playlist, playlist::add_media_atom_v, *uri, true); - - - for (const auto &j : new_media) { - anon_send( - backend_, playlist::add_media_atom_v, j.uuid(), Uuid()); - results.push_back(QUuidFromUuid(j.uuid())); - } - } - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - return results; - }); -} - - -// bool ContactSheetUI::addMedia(const QUuid &uuid, const QUuid &before_uuid) { -// bool result = false; -// scoped_actor sys{system()}; - -// try { -// result = request_receive( -// *sys, -// backend_, -// playlist::add_media_atom_v, -// UuidFromQUuid(uuid), -// UuidFromQUuid(before_uuid)); -// } catch (const std::exception &e) { -// spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); -// } - -// return result; -// } - -void ContactSheetUI::update_media() { - scoped_actor sys{system()}; - - sys->request(backend_, infinite, playlist::get_media_atom_v) - .receive( - [&](const std::vector &actors) { - QList old_media = media_; - - for (auto i : actors) { - if (not uuid_media_.count(i.uuid())) { - uuid_media_[i.uuid()] = new MediaUI(this); - uuid_media_[i.uuid()]->initSystem(this); - uuid_media_[i.uuid()]->set_backend(i.actor()); - } - } - - bool done = false; - while (not done) { - done = true; - for (const auto &i : uuid_media_) { - bool found = false; - for (const auto &ii : actors) { - if (i.first == ii.uuid()) { - found = true; - break; - } - } - if (not found) { - delete i.second; - uuid_media_.erase(i.first); - done = false; - break; - } - } - } - - // rebuild list. - media_.clear(); - media_order_.clear(); - auto ind = 0; - for (auto i : actors) { - media_.append(uuid_media_[i.uuid()]); - media_order_[QUuidFromUuid(i.uuid()).toString()] = QVariant::fromValue(ind); - ind++; - } - - if (old_media != media_) { - media_model_.populate(media_); - emit mediaOrderChanged(); - emit mediaListChanged(); - } - }, - [=](const caf::error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); -} - - -void ContactSheetUI::init(actor_system &system_) { - QMLActor::init(system_); - emit systemInit(this); - - spdlog::debug("ContactSheetUI init"); - - set_message_handler([=](actor_companion * /*self_*/) -> message_handler { - return { - [=](utility::event_atom, utility::last_changed_atom, const time_point &) {}, - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg &) {}, - - [=](utility::event_atom, playlist::add_media_atom, const UuidActor &ua) { - emit mediaAdded(QUuidFromUuid(ua.uuid())); - }, - - [=](utility::event_atom, - playlist::media_content_changed_atom, - const std::vector &) {}, - - [=](utility::event_atom, utility::change_atom) { update_media(); }, - - [=](utility::event_atom, utility::name_atom, const std::string &name) { - if (name_ != name) { - name_ = name; - emit nameChanged(); - } - }}; - }); -} - -void ContactSheetUI::dragDropReorder( - const QVariantList dropped_uuids, const QString before_uuid) { - try { - - utility::UuidList drop_uuids; - for (auto v : dropped_uuids) { - drop_uuids.push_back(UuidFromQUuid(v.toUuid())); - } - utility::Uuid before = before_uuid == "" ? Uuid() : Uuid(StdFromQString(before_uuid)); - anon_send(backend_, playlist::move_media_atom_v, drop_uuids, before); - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } -} - -void ContactSheetUI::sortAlphabetically() { - anon_send(backend_, playlist::sort_alphabetically_atom_v); -} - -QObject *ContactSheetUI::selectionFilter() { - return static_cast(playlist_selection_); -} - -QString ContactSheetUI::fullName() { - if (auto *p = dynamic_cast(parent())) { - return p->name() + QString(" - ") + name(); - } else { - return name(); - } -} - -// find item based off container uuid or actor uuid -MediaUI *ContactSheetUI::getNextItem(const utility::Uuid &uuid) { - // might be media uuid ? - if (uuid_media_.count(uuid)) { - // find nextmedia - MediaUI *mojb = uuid_media_[uuid]; - int ind = 0; - for (auto &i : media_) { - if (i == mojb) { - ind++; - if (media_.size() > ind) { - return dynamic_cast(media_[ind]); - } - break; - } - ind++; - } - } - - return nullptr; -} - -QUuid ContactSheetUI::getNextItemUuid(const QUuid &quuid) { - QUuid result; - - auto obj = getNextItem(UuidFromQUuid(quuid)); - if (obj) - result = obj->uuid(); - - return result; -} - -bool ContactSheetUI::contains_media(const QUuid &key) const { - return uuid_media_.count(UuidFromQUuid(key)); -} diff --git a/retired/contact_sheet_ui.hpp b/retired/contact_sheet_ui.hpp deleted file mode 100644 index c20fa48da..000000000 --- a/retired/contact_sheet_ui.hpp +++ /dev/null @@ -1,153 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include - -CAF_PUSH_WARNINGS -#include -#include -#include -CAF_POP_WARNINGS - -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/ui/qml/media_ui.hpp" -#include "xstudio/ui/qml/playlist_ui.hpp" -#include "xstudio/utility/uuid.hpp" - -namespace xstudio { -namespace ui { - namespace qml { - - class PlaylistSelectionUI; - - class ContactSheetUI : public QMLActor { - - Q_OBJECT - Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) - Q_PROPERTY(QUuid uuid READ quuid NOTIFY uuidChanged) - Q_PROPERTY(QString type READ type NOTIFY typeChanged) - Q_PROPERTY(bool expanded READ expanded WRITE setExpanded NOTIFY expandedChanged) - Q_PROPERTY(QUuid cuuid READ qcuuid NOTIFY cuuidChanged) - Q_PROPERTY(QString flag READ flag WRITE setFlag NOTIFY flagChanged) - - Q_PROPERTY(QVariant mediaModel READ mediaModel NOTIFY mediaModelChanged) - Q_PROPERTY(QList mediaList READ mediaList NOTIFY mediaListChanged) - Q_PROPERTY(QVariantMap mediaOrder READ mediaOrder NOTIFY mediaOrderChanged) - - Q_PROPERTY(bool selected READ selected WRITE setSelected NOTIFY selectedChanged) - Q_PROPERTY(QObject *selectionFilter READ selectionFilter NOTIFY - playlistSelectionThingChanged) - Q_PROPERTY(QString fullName READ fullName NOTIFY nameChanged) - Q_PROPERTY( - QObject *parent_playlist READ parent_playlist NOTIFY parent_playlistChanged) - - Q_PROPERTY(bool hasBackend READ hasBackend NOTIFY backendChanged) - - public: - explicit ContactSheetUI( - const utility::Uuid cuuid = utility::Uuid(), QObject *parent = nullptr); - ~ContactSheetUI() override = default; - - void init(caf::actor_system &system) override; - void set_backend(caf::actor backend); - [[nodiscard]] caf::actor backend() const { return backend_; } - - [[nodiscard]] bool selected() const { return selected_; } - [[nodiscard]] QString name() const { return QStringFromStd(name_); } - [[nodiscard]] QUuid quuid() const { return QUuidFromUuid(uuid_); } - [[nodiscard]] utility::Uuid uuid() const { return uuid_; } - [[nodiscard]] QString type() const { return "ContactSheet"; } - [[nodiscard]] bool expanded() const { return expanded_; } - [[nodiscard]] QUuid qcuuid() const { return QUuidFromUuid(cuuid_); } - [[nodiscard]] utility::Uuid cuuid() const { return cuuid_; } - [[nodiscard]] bool hasBackend() const { - return backend_ ? true : false; - ; - } - [[nodiscard]] QString flag() const { return QStringFromStd(flag_); } - - QVariant mediaModel() { return QVariant::fromValue(&media_model_); } - QList mediaList() { return media_; } - [[nodiscard]] QVariantMap mediaOrder() const { return media_order_; } - - QObject *selectionFilter(); - QObject *parent_playlist() { - return static_cast(dynamic_cast(parent())); - } - - QString fullName(); - - signals: - void uuidChanged(); - void nameChanged(); - void typeChanged(); - void backendChanged(); - void systemInit(QObject *object); - void cuuidChanged(); - void expandedChanged(); - void flagChanged(); - void selectedChanged(); - void compareModeChanged(); - void playlistSelectionThingChanged(); - void mediaAdded(const QUuid &uuid); - void parent_playlistChanged(); - void mediaModelChanged(); - void mediaListChanged(); - void mediaOrderChanged(); - - public slots: - void setName(const QString &name); - void setExpanded(const bool value = true) { - expanded_ = value; - emit expandedChanged(); - } - void setFlag(const QString &_flag) { - if (_flag != flag()) { - flag_ = StdFromQString(_flag); - emit flagChanged(); - } - } - QList loadMedia(const QUrl &path) { return loadMediaFuture(path).result(); } - QFuture> loadMediaFuture(const QUrl &path); - - QList handleDrop(const QVariantMap &drop) { - return handleDropFuture(drop).result(); - } - QFuture> handleDropFuture(const QVariantMap &drop); - - // bool addMedia(const QUuid &uuid, const QUuid &before_uuid = QUuid()); - void setSelected(const bool value = true) { - if (selected_ != value) { - selected_ = value; - emit selectedChanged(); - } - } - - void dragDropReorder(const QVariantList drop_uuids, const QString before_uuid); - - void sortAlphabetically(); - QUuid getNextItemUuid(const QUuid &quuid); - bool contains_media(const QUuid &key) const; - - private: - MediaUI *getNextItem(const utility::Uuid &uuid); - void update_media(); - - utility::Uuid cuuid_; - caf::actor backend_; - caf::actor backend_events_; - std::string name_; - std::string flag_; - utility::Uuid uuid_; - bool expanded_{true}; - bool selected_{false}; - QList media_; - std::map uuid_media_; - QVariantMap media_order_; - PlaylistSelectionUI *playlist_selection_ = {nullptr}; - MediaModel media_model_; - }; - } // namespace qml -} // namespace ui -} // namespace xstudio diff --git a/retired/media/src/CMakeLists.txt b/retired/media/src/CMakeLists.txt deleted file mode 100644 index 12a778657..000000000 --- a/retired/media/src/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core - xstudio::media - xstudio::ui::qml::helper - xstudio::utility -) - -SET(EXTRAMOC - "${ROOT_DIR}/include/xstudio/ui/qml/thumbnail_ui.hpp" -) - -create_qml_component(media 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/retired/media/src/media_source_ui.cpp b/retired/media/src/media_source_ui.cpp deleted file mode 100644 index 10396cee6..000000000 --- a/retired/media/src/media_source_ui.cpp +++ /dev/null @@ -1,503 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include - -#include -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/ui/qml/media_ui.hpp" -#include "xstudio/media/media.hpp" -#include "xstudio/thumbnail/thumbnail.hpp" -#include "xstudio/utility/edit_list.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" - -namespace fs = std::filesystem; - - -using namespace caf; -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::ui::qml; - -MediaSourceUI::MediaSourceUI(QObject *parent) - : QMLActor(parent), backend_(), backend_events_() {} - -void MediaSourceUI::set_backend(caf::actor backend) { - spdlog::debug("MediaSourceUI set_backend"); - - const auto tt = utility::clock::now(); - - backend_ = backend; - scoped_actor sys{system()}; - - if (backend_events_) { - try { - request_receive( - *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - backend_events_ = caf::actor(); - } - - // we can't do this.. we rely on the value.. - // we're getting in a big mess with how this stuff currently works. - try { - const auto tmp = request_receive_wait( - *sys, backend_, std::chrono::seconds(1), media::media_status_atom_v); - if (media_status_ != tmp) { - media_status_ = tmp; - emit mediaStatusChanged(); - } - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - try { - auto detail = request_receive_wait( - *sys, backend_, std::chrono::seconds(1), utility::detail_atom_v); - update_from_detail(detail); - } catch (std::exception &e) { - spdlog::warn("MediaSourceUI set_backend {}", e.what()); - } - - if (backend_) { - anon_send(backend_, media::get_media_details_atom_v, as_actor()); - } - - spdlog::debug( - "MediaSourceUI set_backend took {} milliseconds to complete", - std::chrono::duration_cast(utility::clock::now() - tt) - .count()); -} - -QFuture MediaSourceUI::getThumbnailURLFuture(int frame) { - return QtConcurrent::run([=]() { - QString thumburl("qrc:///feather_icons/film.svg"); - try { - auto the_frame = frame; - if (backend_ && ((frame == -1 && duration_frames_ != "") || frame != -1)) { - auto middle_frame = (duration_frames_int_ - 1) / 2; - if (frame == -1) - the_frame = middle_frame; - - thumburl = qml::getThumbnailURL( - system(), backend_, the_frame, the_frame == middle_frame); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - return thumburl; - }); -} - -void MediaSourceUI::requestThumbnail(float position_in_source_duration) { - - if (backend_) { - latest_thumb_request_job_id_ = utility::Uuid::generate(); - anon_send( - backend_, - media_reader::get_thumbnail_atom_v, - position_in_source_duration, - latest_thumb_request_job_id_, - as_actor()); - } -} - -void MediaSourceUI::cancelThumbnailRequest() { - if (backend_ && !latest_thumb_request_job_id_.is_null()) { - anon_send( - backend_, - media_reader::cancel_thumbnail_request_atom_v, - latest_thumb_request_job_id_); - latest_thumb_request_job_id_ = utility::Uuid(); - } -} - -void MediaSourceUI::setFpsString(const QString &fps) { - try { - double value = std::stod(fps.toStdString()); - if (backend_ and value >= 1.0) { - // push change top backend.. - scoped_actor sys{system()}; - - auto mr = - request_receive(*sys, backend_, media::media_reference_atom_v); - mr.set_rate(FrameRate(1.0 / value)); - anon_send(backend_, media::media_reference_atom_v, mr); - fps_string_ = fps; - emit fpsChanged(); - } - } catch (...) { - } -} - - -// ??? NEVER CALLED ? - -void MediaSourceUI::update_from_backend( - const utility::JsonStore &meta_data, - const utility::JsonStore &colour_params, - const utility::ContainerDetail &detail) { - scoped_actor sys{system()}; - - try { - inspect_metadata(meta_data); - } catch (const std::exception &e) { - format_ = "TBD"; - resolution_ = "TBD"; - bit_depth_ = "TBD"; - // suppress warning as metadata may not yet be read, watch for event instead - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - if (colour_params.contains("colourspace")) - colourManaged_ = colour_params["colourspace"].get() != ""; - - name_ = QStringFromStd(detail.name_); - uuid_ = detail.uuid_; - - if (backend_events_) { - try { - request_receive( - *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - backend_events_ = caf::actor(); - } - - backend_events_ = detail.group_; - try { - request_receive( - *sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - - update_on_change(); - emit backendChanged(); - emit nameChanged(); - emit uuidChanged(); - emit metadataChanged(); - emit bitDepthChanged(); - emit formatChanged(); - emit resolutionChanged(); - - spdlog::debug("MediaSourceUI set_backend {}", to_string(uuid_)); -} - -void MediaSourceUI::set_sources_count(const size_t count) { - while (streams_.size() > count) { - delete streams_.back(); - streams_.pop_back(); - } - - while (streams_.size() < count) { - streams_.push_back(new MediaStreamUI(this)); - streams_.back()->initSystem(this); - } -} - -QString MediaSourceUI::metadata() { - QString result; - try { - if (backend_) { - scoped_actor sys{system()}; - result = QStringFromStd(to_string(request_receive( - *sys, backend_, json_store::get_json_atom_v, ""))); - } - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - return result; -} - - -void MediaSourceUI::initSystem(QObject *system_qobject) { - init(dynamic_cast(system_qobject)->system()); -} - -void MediaSourceUI::init(actor_system &system_) { - QMLActor::init(system_); - - spdlog::debug("MediaSourceUI init"); - - // self()->set_down_handler([=](down_msg& msg) { - // if(msg.source == store) - // unsubscribe(); - // }); - - set_message_handler([=](actor_companion * /*self_*/) -> message_handler { - return { - [=](backend_atom, caf::actor actor) { set_backend(actor); }, - - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - - [=](const group_down_msg & /*msg*/) { - // if(msg.source == store_events) - // unsubscribe(); - }, - - [=](const std::tuple< - utility::Uuid, - std::string, - std::string, - double, - media::StreamDetail, - std::vector, - Uuid> &stream_details) { - // it's possible we get all these details from a media source - // that has been switched out for another one - so we verify - // that these are the details we want using the uuid. - utility::Uuid media_source_uuid = std::get<0>(stream_details); - if (media_source_uuid == uuid_) { - std::string filename = std::get<1>(stream_details); - std::string fps_string = std::get<2>(stream_details); - double fps = std::get<3>(stream_details); - media::StreamDetail stream_detail = std::get<4>(stream_details); - std::vector streams = std::get<5>(stream_details); - Uuid current_stream = std::get<6>(stream_details); - - update_streams_from_backend( - filename, fps_string, fps, stream_detail, streams, current_stream); - } - }, - - [=](utility::detail_atom, const ContainerDetail &detail) { - update_from_detail(detail); - }, - - [=](utility::event_atom, media::add_media_stream_atom, const UuidActor &) { - anon_send(backend_, media::get_media_details_atom_v, as_actor()); - }, - - [=](utility::event_atom, - media_metadata::get_metadata_atom, - const utility::JsonStore &meta) { - try { - inspect_metadata(meta); - } catch (std::exception &e) { - format_ = "TBD"; - resolution_ = "TBD"; - bit_depth_ = "TBD"; - } - emit currentChanged(); - emit metadataChanged(); - emit bitDepthChanged(); - emit pixelAspectChanged(); - emit formatChanged(); - emit resolutionChanged(); - }, - - [=](utility::event_atom, utility::change_atom) { update_on_change(); }, - - [=](utility::event_atom, utility::last_changed_atom, const time_point &) { - update_on_change(); - }, - - [=](utility::event_atom, utility::name_atom, const std::string &name) { - QString tmp = QStringFromStd(name); - if (tmp != name_) { - name_ = tmp; - emit nameChanged(); - } - }, - [=](const thumbnail::ThumbnailBufferPtr &tnail, - const float position_in_source_duration, - const utility::Uuid job_uuid, - const std::string &err_msg) { - if (tnail && job_uuid == latest_thumb_request_job_id_) { - thumbnail_ = QImage( - (uchar *)&(tnail->data()[0]), - tnail->width(), - tnail->height(), - 3 * tnail->width(), - QImage::Format_RGB888); - thumbnail_position_in_clip_duration_ = position_in_source_duration; - emit thumbnailChanged(thumbnail_, thumbnail_position_in_clip_duration_); - } else if (!tnail) { - spdlog::warn("Thumbanil load failuer: {}", err_msg); - } - }}; - }); -} - -void MediaSourceUI::update_from_detail(const ContainerDetail &detail) { - - name_ = QStringFromStd(detail.name_); - uuid_ = detail.uuid_; - backend_events_ = detail.group_; - scoped_actor sys{system()}; - request_receive(*sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); - update_on_change(); - emit backendChanged(); - emit nameChanged(); - emit uuidChanged(); -} - - -void MediaSourceUI::update_streams_from_backend( - const std::string filename, - const std::string fps_string, - const double fps, - const media::StreamDetail stream_detail, - std::vector streams, - Uuid current_stream) { - - file_name_ = QStringFromStd(fs::path(filename).filename()); - file_path_ = QStringFromStd(filename); - set_sources_count(streams.size()); - size_t ind = 0; - for (auto i : streams) { - anon_send(streams_[ind]->as_actor(), backend_atom_v, i.actor()); - if (i.uuid() == current_stream) { - current_ = streams_[ind]; - } - ind++; - } - - fps_string_ = QStringFromStd(fps_string); - fps_ = fps; - - duration_frames_ = (stream_detail.duration_.rate().to_seconds() == 0.0) - ? QString("error") - : QString("%1").arg(stream_detail.duration_.frames()); - duration_frames_int_ = (stream_detail.duration_.rate().to_seconds() == 0.0) - ? 0 - : stream_detail.duration_.frames(); - - if (stream_detail.duration_.rate().to_seconds() == 0.0) { - - duration_seconds_ = QString("error"); - - } else { - const double dur = stream_detail.duration_.seconds(); - const int hours = (int)dur / (60 * 60); - const int minutes = ((int)dur / 60) % 60; - const int seconds = int(dur - minutes * 60 - hours * 60 * 60); - const int fsec = int(floor((dur - floor(dur)) * 10.0)); - - if (hours) { - duration_seconds_ = QString("%1:%2:%3.%4") - .arg(hours, 2, 10, QChar('0')) - .arg(minutes, 2, 10, QChar('0')) - .arg(seconds, 2, 10, QChar('0')) - .arg(fsec); - } else { - duration_seconds_ = QString("%1:%2.%3") - .arg(minutes, 2, 10, QChar('0')) - .arg(seconds, 2, 10, QChar('0')) - .arg(fsec); - } - } - - if (stream_detail.duration_.rate().to_seconds() == 0.0) { - duration_timecode_ = QString("error"); - } else { - - const Timecode tc( - stream_detail.duration_.frames(), - stream_detail.duration_.rate().to_fps(), - false // TODO: find out if the TC is drop frame - ); - - duration_timecode_ = QStringFromStd(tc.to_string()); - } - - emit durationChanged(); - emit streamsChanged(); - emit currentChanged(); - emit fpsChanged(); - emit pathChanged(); - emit fileNameChanged(); - emit thumbnailURLChanged(); -} - -void MediaSourceUI::update_on_change() { - scoped_actor sys{system()}; - try { - const auto tmp = request_receive_wait( - *sys, backend_, std::chrono::seconds(1), media::media_status_atom_v); - if (media_status_ != tmp) { - media_status_ = tmp; - emit mediaStatusChanged(); - } - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - // force rebuild of thumbnail on media change - // captures colourspace change, this is really needs it's own event to trigger. - emit thumbnailURLChanged(); -} - -void MediaSourceUI::inspect_metadata(const nlohmann::json &data) { - if (data.size() and data.begin().value().contains("standard_fields")) { - const nlohmann::json &standard_fields = data.begin().value()["standard_fields"]; - if (standard_fields.contains("bit_depth")) - bit_depth_ = QStringFromStd(standard_fields["bit_depth"].get()); - if (standard_fields.contains("format")) - format_ = QStringFromStd(standard_fields["format"].get()); - if (standard_fields.contains("resolution")) - resolution_ = QStringFromStd(standard_fields["resolution"].get()); - if (standard_fields.contains("pixel_aspect")) - pixel_aspect_ = standard_fields["pixel_aspect"].get(); - } else if (data.contains("standard_fields")) { - const nlohmann::json &standard_fields = data["standard_fields"]; - if (standard_fields.contains("bit_depth")) - bit_depth_ = QStringFromStd(standard_fields["bit_depth"].get()); - if (standard_fields.contains("format")) - format_ = QStringFromStd(standard_fields["format"].get()); - if (standard_fields.contains("resolution")) - resolution_ = QStringFromStd(standard_fields["resolution"].get()); - if (standard_fields.contains("pixel_aspect")) - pixel_aspect_ = standard_fields["pixel_aspect"].get(); - } else { - throw std::runtime_error("No standard metadata fields."); - } -} - -void MediaSourceUI::switchToStream(const QUuid stream_uuid) { - - if (backend_) { - scoped_actor sys{system()}; - try { - const bool changed = request_receive( - *sys, - backend_, - media::current_media_stream_atom_v, - media::MT_IMAGE, - UuidFromQUuid(stream_uuid)); - if (changed) { - anon_send(backend_, media::get_media_details_atom_v, as_actor()); - } - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - } -} - -void MediaSourceUI::switchToStream(const QString stream_id) { - - if (backend_) { - scoped_actor sys{system()}; - try { - const bool changed = request_receive( - *sys, - backend_, - media::current_media_stream_atom_v, - media::MT_IMAGE, - StdFromQString(stream_id)); - if (changed) { - anon_send(backend_, media::get_media_details_atom_v, as_actor()); - } - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - } -} \ No newline at end of file diff --git a/retired/media/src/media_stream_ui.cpp b/retired/media/src/media_stream_ui.cpp deleted file mode 100644 index 3a4803864..000000000 --- a/retired/media/src/media_stream_ui.cpp +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/ui/qml/media_ui.hpp" -#include "xstudio/utility/edit_list.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" - -using namespace caf; -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::ui::qml; - -MediaStreamUI::MediaStreamUI(QObject *parent) - : QMLActor(parent), backend_(), backend_events_() {} - -void MediaStreamUI::set_backend(caf::actor backend) { - spdlog::debug("MediaStreamUI set_backend"); - scoped_actor sys{system()}; - - backend_ = backend; - // get backend state.. - if (backend_events_) { - try { - request_receive( - *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - backend_events_ = caf::actor(); - } - - - try { - auto detail = request_receive(*sys, backend_, detail_atom_v); - name_ = QStringFromStd(detail.name_); - uuid_ = detail.uuid_; - backend_events_ = detail.group_; - request_receive( - *sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); - - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - update_on_change(); - - emit backendChanged(); - emit nameChanged(); - emit uuidChanged(); - spdlog::debug("MediaStreamUI set_backend {}", to_string(uuid_)); -} - -void MediaStreamUI::initSystem(QObject *system_qobject) { - init(dynamic_cast(system_qobject)->system()); -} - -void MediaStreamUI::init(actor_system &system_) { - QMLActor::init(system_); - - spdlog::debug("MediaStreamUI init"); - - // self()->set_down_handler([=](down_msg& msg) { - // if(msg.source == store) - // unsubscribe(); - // }); - - set_message_handler([=](actor_companion * /*self_*/) -> message_handler { - return { - [=](backend_atom, caf::actor actor) { set_backend(actor); }, - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) { - // if(msg.source == store_events) - // unsubscribe(); - }, - - [=](utility::event_atom, utility::change_atom) { update_on_change(); }, - - [=](utility::event_atom, utility::last_changed_atom, const time_point &) {}, - - [=](utility::event_atom, utility::name_atom, const std::string &name) { - QString tmp = QStringFromStd(name); - if (tmp != name_) { - name_ = tmp; - emit nameChanged(); - } - }}; - }); -} - -void MediaStreamUI::update_on_change() {} diff --git a/retired/media/src/media_ui.cpp b/retired/media/src/media_ui.cpp deleted file mode 100644 index f2bdca49e..000000000 --- a/retired/media/src/media_ui.cpp +++ /dev/null @@ -1,379 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include - - -#include "xstudio/atoms.hpp" -#include "xstudio/ui/qml/media_ui.hpp" -#include "xstudio/utility/edit_list.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" - - -CAF_PUSH_WARNINGS -#include -#include -CAF_POP_WARNINGS - -using namespace caf; -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::ui::qml; - -MediaModel::MediaModel(QObject *parent) : QAbstractListModel(parent) {} - -int MediaModel::get_role(const QString &rolename) const { - auto role = -1; - auto rolenames = roleNames(); - auto i = rolenames.constBegin(); - while (i != rolenames.constEnd()) { - if (QString(i.value()) == rolename) { - role = i.key(); - break; - } - ++i; - } - return role; -} - - -QVariant MediaModel::get(const int row, const QString &rolename) const { - return data(index(row, 0), get_role(rolename)); -} - -// QModelIndexList QAbstractItemModel::match(const QModelIndex &start, int role, const QVariant -// &value, int hits = 1, Qt::MatchFlags flags = -// Qt::MatchFlags(Qt::MatchStartsWith|Qt::MatchWrap)) - -// Q_INVOKABLE int MediaModel::match(const QVariant &value, const QString &rolename,const int -// start_row) const { -// auto row = -1; - -// auto indexs = QAbstractListModel::match(index(start_row, 0), get_role(rolename), value, -// 1, Qt::MatchExactly); if(not indexs.isEmpty()) -// row = indexs[0].row(); - -// return row; -// } - -int MediaModel::rowCount(const QModelIndex &parent) const { - Q_UNUSED(parent); - return data_.count(); -} - -QVariant MediaModel::data(const QModelIndex &index, int role) const { - auto row = index.row(); - if (row >= 0 and row < data_.count()) { - switch (role) { - case ObjectRole: - return QVariant::fromValue(data_[row]); - break; - case UuidRole: - if (auto i = dynamic_cast(data_[row])) - return QVariant::fromValue(i->uuid()); - break; - default: - break; - } - } - return QVariant(); -} - -QHash MediaModel::roleNames() const { - QHash roles; - roles[ObjectRole] = "media_ui_object"; - roles[UuidRole] = "uuid"; - return roles; -} - -bool MediaModel::populate(QList &items) { - // build change list - bool different = true; - bool changed = false; - const int previousSize = data_.size(); - // move/delete/insert.. - - // delete - // items only appear once in list.. - while (different) { - different = false; - for (auto i = 0; i < data_.size(); i++) { - if (not items.contains(data_[i])) { - beginRemoveRows(QModelIndex(), i, i); - data_.removeAt(i); - endRemoveRows(); - different = true; - changed = true; - break; - } - } - } - - // move // insert. - different = true; - while (different) { - different = false; - for (auto i = 0; i < items.size(); i++) { - // must be new item.. - if (data_.size() <= i) { - changed = true; - different = true; - beginInsertRows(QModelIndex(), i, i); - data_.push_back(items[i]); - endInsertRows(); - } else { - // check for difference. - if (items[i] != data_[i]) { - changed = true; - different = true; - // could be move ? - if (data_.contains(items[i])) { - // it's a move.. - auto src = data_.indexOf(items[i]); - beginMoveRows(QModelIndex(), src, src, QModelIndex(), i); - data_.move(src, i); - endMoveRows(); - } else { - beginInsertRows(QModelIndex(), i, i); - data_.insert(i, items[i]); - endInsertRows(); - } - } - } - } - } - - if (data_.size() != previousSize) - emit lengthChanged(); - - return changed; -} - - -MediaUI::MediaUI(QObject *parent) : QMLActor(parent), backend_(), backend_events_() {} - -bool MediaUI::mediaOnline() const { - if (not media_source_) - return false; - - return media_source_->mediaOnline(); -} - -QString MediaUI::mediaStatus() const { - if (not media_source_) - return QStringFromStd(to_string(media::MediaStatus::MS_MISSING)); - - return media_source_->mediaStatus(); -} - - -void MediaUI::set_backend(caf::actor backend) { - spdlog::debug("MediaUI set_backend"); - - const auto tt = utility::clock::now(); - scoped_actor sys{system()}; - - backend_ = backend; - // get backend state.. - if (backend_events_) { - try { - request_receive( - *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - backend_events_ = caf::actor(); - } - - if (!backend_) { - name_ = ""; - uuid_ = Uuid(); - media_source_ = nullptr; - emit mediaSourceChanged(); - emit backendChanged(); - emit uuidChanged(); - emit nameChanged(); - emit mediaStatusChanged(); - return; - } - - - try { - auto detail = request_receive(*sys, backend_, detail_atom_v); - name_ = QStringFromStd(detail.name_); - uuid_ = detail.uuid_; - backend_events_ = detail.group_; - request_receive( - *sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - // find current.. - utility::Uuid current_ms; - try { - auto source = - request_receive(*sys, backend_, media::current_media_source_atom_v); - media_source_ = new MediaSourceUI(this); - media_source_->initSystem(this); - media_source_->set_backend(source.actor()); - QObject::connect( - media_source_, - &MediaSourceUI::mediaStatusChanged, - this, - &MediaUI::mediaStatusChanged); - emit mediaSourceChanged(); - emit mediaStatusChanged(); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - try { - auto [flag, flag_text] = request_receive>( - *sys, backend_, playlist::reflag_container_atom_v); - flag_ = flag; - flag_text_ = flag_text; - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - update_on_change(); - emit backendChanged(); - emit mediaSourceChanged(); - emit uuidChanged(); - emit nameChanged(); - emit flagChanged(); - emit flagTextChanged(); - - spdlog::debug( - "MediaUI set_backend {} took {} milliseconds to complete", - to_string(uuid_), - std::chrono::duration_cast(utility::clock::now() - tt) - .count()); -} -void MediaUI::setFlag(const QString &_flag) { - if (_flag != flag()) { - anon_send( - backend_, - playlist::reflag_container_atom_v, - std::make_tuple( - std::optional(StdFromQString(_flag)), - std::optional{})); - } -} - -void MediaUI::setFlagText(const QString &_flag) { - if (_flag != flagText()) - anon_send( - backend_, - playlist::reflag_container_atom_v, - std::make_tuple( - std::optional{}, - std::optional(StdFromQString(_flag)))); -} - -void MediaUI::setCurrentSource(const QUuid &quuid) { - auto uuid = UuidFromQUuid(quuid); - anon_send(backend_, media::current_media_source_atom_v, uuid); -} - -void MediaUI::initSystem(QObject *system_qobject) { - init(dynamic_cast(system_qobject)->system()); -} - -void MediaUI::init(actor_system &system_) { - QMLActor::init(system_); - - spdlog::debug("MediaUI init"); - - // self()->set_down_handler([=](down_msg& msg) { - // if(msg.source == store) - // unsubscribe(); - // }); - - set_message_handler([=](actor_companion * /*self_*/) -> message_handler { - return { - [=](backend_atom, caf::actor actor) { set_backend(actor); }, - - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) { - // if(msg.source == store_events) - // unsubscribe(); - }, - - [=](utility::event_atom, - media::current_media_source_atom, - UuidActor &media_source, - const media::MediaType media_type) { - // for now, skipping audio media type - if (media_type != media::MT_IMAGE) - return; - - // N.B. QML Garbage collection should delete the - // old media source object, C++ side does not own it!! - if (!media_source_) { - media_source_ = new MediaSourceUI(this); - media_source_->initSystem(this); - QObject::connect( - media_source_, - &MediaSourceUI::mediaStatusChanged, - this, - &MediaUI::mediaStatusChanged); - } - - media_source_->set_backend(media_source.actor()); - - emit mediaSourceChanged(); - emit mediaStatusChanged(); - }, - - [=](utility::event_atom, utility::change_atom) { update_on_change(); }, - - [=](utility::event_atom, - playlist::reflag_container_atom, - const utility::Uuid &, - const std::tuple &_flag) { - if (std::get<0>(_flag) != flag_) { - flag_ = std::get<0>(_flag); - emit flagChanged(); - } - if (std::get<1>(_flag) != flag_text_) { - flag_text_ = std::get<1>(_flag); - emit flagTextChanged(); - } - }, - - [=](utility::event_atom, bookmark::bookmark_change_atom, const utility::Uuid &) {}, - - [=](utility::event_atom, utility::last_changed_atom, const time_point &) {}, - - [=](utility::event_atom, utility::name_atom, const std::string &name) { - QString tmp = QStringFromStd(name); - if (tmp != name_) { - name_ = tmp; - emit nameChanged(); - } - }}; - }); -} - -void MediaUI::update_on_change() {} - - -QFuture MediaUI::getMetadataFuture() { - return QtConcurrent::run([=]() { - if (backend_) { - try { - scoped_actor sys{system()}; - return QStringFromStd( - request_receive( - *sys, backend_, json_store::get_json_atom_v, utility::Uuid(), "") - .dump()); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - return QStringFromStd(err.what()); - } - } - return QString(); - }); -} \ No newline at end of file diff --git a/retired/media/src/thumbnail_ui.cpp b/retired/media/src/thumbnail_ui.cpp deleted file mode 100644 index 9f00b633b..000000000 --- a/retired/media/src/thumbnail_ui.cpp +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include - -#include "xstudio/ui/qml/media_ui.hpp" -#include "xstudio/ui/qml/thumbnail_ui.hpp" - -CAF_PUSH_WARNINGS -#include -#include -#include -#include -CAF_POP_WARNINGS - -using namespace caf; -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::ui::qml; - -static QImage film_image; - -static QTimer *s_anim_timer = nullptr; - -ThumbNail::ThumbNail(QQuickItem *parent) : QQuickPaintedItem(parent), thumb_image_(film_image) { - - if (!s_anim_timer) { - s_anim_timer = new QTimer(nullptr); - s_anim_timer->setInterval(100); - s_anim_timer->start(); - } - - /*if (film_image.isNull()) { - QPixmap p(256, 256); - QPainter ptr(&p); - film_image = QImage(QString(":/feather_icons/film.svg")); - ptr.drawImage(QRectF(0.0f,0.0f,256.f,256.f), film_image); - ptr.setCompositionMode(QPainter::CompositionMode_SourceIn); - ptr.fillRect( p.rect(), Qt::white ); - ptr.end(); - film_image = p.toImage(); - thumb_image_ = film_image; - }*/ -} - -void ThumbNail::paint(QPainter *painter) { - - if (!thumb_loaded_) { - // when no thumbnail, fill with mid grey - painter->fillRect(0, 0, width(), height(), color_); - - QTransform t; - t.translate(width() / 2, height() / 2); - t.rotate(busy_anim_); - t.translate(-width() / 2, -height() / 2); - painter->setTransform(t); - - painter->fillRect(0, height() / 2 - 0.5f, width(), 10.0f, QColor(192, 192, 192)); - - return; - } - - const float image_aspect = float(thumb_image_.width()) / float(thumb_image_.height()); - const float canvas_aspect = float(width()) / float(height()); - - if (image_aspect > canvas_aspect) { - float bottom = float(height()) * 0.5f * (image_aspect - canvas_aspect) / image_aspect; - painter->drawImage( - QRectF(0.0f, bottom, width(), height() - (bottom * 2.0f)), thumb_image_); - } else { - float left = float(width()) * 0.5f * (canvas_aspect - image_aspect) / canvas_aspect; - painter->drawImage(QRectF(left, 0.0f, width() - (left * 2.0f), height()), thumb_image_); - } -} - -QObject *ThumbNail::mediaSource() const { return static_cast(media_source_); } - -void ThumbNail::setMediaSource(QObject *media_item) { - - auto src = dynamic_cast(media_item); - if (media_source_ != src) { - - // TODO: don't hold pointer to media_itme ... what if it is destroyed? - // use signal slot exclusively here instead - if (src) { - if (media_source_) { - QObject::disconnect( - media_source_, - &MediaSourceUI::thumbnailChanged, - this, - &ThumbNail::newThumbnail); - } - media_source_ = src; - QObject::connect( - media_source_, - &MediaSourceUI::thumbnailChanged, - this, - &ThumbNail::newThumbnail); - if (width()) { - // ThumbNail is hidden if width is zero - media_source_->requestThumbnail(position_in_clip_duration_); - } - } else { - media_source_ = src; - } - connect(s_anim_timer, &QTimer::timeout, this, &ThumbNail::busyAnimAdvance); - thumb_loaded_ = false; - position_in_clip_duration_ = -1.0f; - emit thumbLoadedChanged(); - emit mediaSourceChanged(); - update(); - } -} - -void ThumbNail::setBusyAnim(float anim) { - busy_anim_ = anim; - emit busyAnimChanged(); - update(); -} - -void ThumbNail::busyAnimAdvance() { - busy_anim_ += 30; - emit busyAnimChanged(); - update(); -} - -void ThumbNail::setColor(QColor c) { - if (c != color_) { - color_ = c; - emit colorChanged(); - /*if (position_in_clip_duration_ == -1.0f) { - QPixmap p(256, 256); - QPainter ptr(&p); - QImage film_image(QString(":/feather_icons/film.svg")); - ptr.drawImage(QRectF(0.0f,0.0f,256.f,256.f), film_image); - ptr.setCompositionMode(QPainter::CompositionMode_SourceIn); - ptr.fillRect( p.rect(), color_ ); - ptr.end(); - film_image = p.toImage(); - thumb_image_ = film_image; - update(); - }*/ - } -} - - -void ThumbNail::loadThumbnail(float position_in_clip_duration) { - - // position_in_clip_duration should be in range 0.0 (first frame) and 1.0 (last frame of - // source). We round it to first decimal place so we can only load up to 10 thumbnail frames - // through the range of the source - we're doing this to limit IO load - position_in_clip_duration = - std::min(std::max(0.0f, round(position_in_clip_duration * 10.0f) / 10.0f), 1.0f); - - if (position_in_clip_duration != position_in_clip_duration_ && media_source_) { - media_source_->requestThumbnail(position_in_clip_duration); - } -} - -void ThumbNail::cancelLoadThumbnail() { - if (media_source_) - media_source_->cancelThumbnailRequest(); -} - -void ThumbNail::newThumbnail(const QImage &tnail, const float position_in_clip_duration) { - if (!thumb_loaded_) { - thumb_loaded_ = true; - emit thumbLoadedChanged(); - disconnect(s_anim_timer, &QTimer::timeout, this, &ThumbNail::busyAnimAdvance); - } - thumb_image_ = tnail; - position_in_clip_duration_ = position_in_clip_duration; - update(); -} diff --git a/retired/media/test/CMakeLists.txt b/retired/media/test/CMakeLists.txt deleted file mode 100644 index ca5580c50..000000000 --- a/retired/media/test/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -include(CTest) diff --git a/retired/media_ui.hpp b/retired/media_ui.hpp deleted file mode 100644 index 2a3690018..000000000 --- a/retired/media_ui.hpp +++ /dev/null @@ -1,322 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include - -CAF_PUSH_WARNINGS -#include -#include -#include -#include -#include -#include -#include -#include -CAF_POP_WARNINGS - -#include "xstudio/media/media.hpp" -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/utility/media_reference.hpp" - -namespace xstudio { -namespace ui { - namespace qml { - - /* This class allows us to expose a dynamic list of MediaUI objects to QML, it's - used by PlaylistUI, SubsetUI, ContactSheetUI etc so the UI can expose their - media item contents */ - - // QModelIndexList QAbstractItemModel::match(const QModelIndex &start, int role, - // const QVariant &value, int hits = 1, Qt::MatchFlags flags = - // Qt::MatchFlags(Qt::MatchStartsWith|Qt::MatchWrap)) const - class MediaModel : public QAbstractListModel { - Q_OBJECT - - Q_PROPERTY(int length READ length NOTIFY lengthChanged) - - public: - enum MediaRoles { UuidRole, ObjectRole }; - - MediaModel(QObject *parent = nullptr); - [[nodiscard]] int - rowCount(const QModelIndex &parent = QModelIndex()) const override; - [[nodiscard]] QVariant - data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - [[nodiscard]] int length() const { return rowCount(); } - - Q_INVOKABLE [[nodiscard]] QVariant - get(const int row, const QString &rolename) const; - // Q_INVOKABLE [[nodiscard]] int match(const QVariant &value, const QString - // &rolename,const int start_row=0) const; - - bool populate(QList &items); - - signals: - - void lengthChanged(); - - protected: - [[nodiscard]] QHash roleNames() const override; - - private: - [[nodiscard]] int get_role(const QString &rolename) const; - - private: - QList data_; - QObject *controller_{nullptr}; - }; - - class MediaStreamUI : public QMLActor { - - Q_OBJECT - Q_PROPERTY(QString name READ name NOTIFY nameChanged) - Q_PROPERTY(QUuid uuid READ uuid NOTIFY uuidChanged) - - public: - explicit MediaStreamUI(QObject *parent = nullptr); - ~MediaStreamUI() override = default; - - void init(caf::actor_system &system) override; - void set_backend(caf::actor backend); - - caf::actor backend() { return backend_; } - - [[nodiscard]] QString name() const { return name_; } - [[nodiscard]] QUuid uuid() const { return QUuidFromUuid(uuid_); } - - signals: - void nameChanged(); - void uuidChanged(); - void backendChanged(); - - public slots: - void initSystem(QObject *system_qobject); - - private: - void update_on_change(); - - private: - caf::actor backend_; - caf::actor backend_events_; - QString name_; - utility::Uuid uuid_; - }; - - class MediaSourceUI : public QMLActor { - - Q_OBJECT - - Q_PROPERTY(QString name READ name NOTIFY nameChanged) - Q_PROPERTY(QString fpsString READ fpsString WRITE setFpsString NOTIFY fpsChanged) - Q_PROPERTY(QString fileName READ fileName NOTIFY fileNameChanged) - Q_PROPERTY(QString path READ path NOTIFY pathChanged) - Q_PROPERTY(QUuid uuid READ uuid NOTIFY uuidChanged) - Q_PROPERTY(QObject *current READ current NOTIFY currentChanged) - Q_PROPERTY(QList streams READ streams NOTIFY streamsChanged) - Q_PROPERTY(QString metadata READ metadata NOTIFY metadataChanged) - Q_PROPERTY(QString bitDepth READ bitDepth NOTIFY bitDepthChanged) - Q_PROPERTY(QString format READ format NOTIFY formatChanged) - Q_PROPERTY(QString resolution READ resolution NOTIFY resolutionChanged) - Q_PROPERTY( - int durationFramesNumeric READ durationFramesNumeric NOTIFY durationChanged) - Q_PROPERTY(QString durationFrames READ durationFrames NOTIFY durationChanged) - Q_PROPERTY(QString durationSeconds READ durationSeconds NOTIFY durationChanged) - Q_PROPERTY(QString durationTimecode READ durationTimecode NOTIFY durationChanged) - Q_PROPERTY(double fps READ fps NOTIFY fpsChanged) - Q_PROPERTY(QString thumbnailURL READ getThumbnailURL NOTIFY thumbnailURLChanged) - Q_PROPERTY(QString mediaStatus READ mediaStatus NOTIFY mediaStatusChanged) - Q_PROPERTY(bool mediaOnline READ mediaOnline NOTIFY mediaStatusChanged) - Q_PROPERTY(QImage thumbnail READ thumbnail NOTIFY thumbnailChanged) - Q_PROPERTY(float pixelAspect READ pixelAspect NOTIFY pixelAspectChanged) - - public: - explicit MediaSourceUI(QObject *parent = nullptr); - ~MediaSourceUI() override = default; - - void init(caf::actor_system &system) override; - void set_backend(caf::actor backend); - - caf::actor backend() { return backend_; } - - QString name() { return name_; } - [[nodiscard]] QString fpsString() const { return fps_string_; } - QString fileName() { return file_name_; } - QString path() { return file_path_; } - QUuid uuid() { return QUuidFromUuid(uuid_); } - QObject *current() { return current_; } - QList streams() { - QList streams; - for (const auto &i : streams_) - streams.append(i); - return streams; - } - QString metadata(); - - QString bitDepth() { return bit_depth_; } - QString format() { return format_; } - QString resolution() { return resolution_; } - [[nodiscard]] bool colourManaged() const { return colourManaged_; } - - [[nodiscard]] QString durationFrames() const { return duration_frames_; } - [[nodiscard]] int durationFramesNumeric() const { return duration_frames_int_; } - [[nodiscard]] QString durationSeconds() const { return duration_seconds_; } - [[nodiscard]] QString durationTimecode() const { return duration_timecode_; } - [[nodiscard]] double fps() const { return fps_; } - - [[nodiscard]] bool mediaOnline() const { - return media_status_ == media::MediaStatus::MS_ONLINE; - } - [[nodiscard]] QString mediaStatus() const { - return QStringFromStd(to_string(media_status_)); - } - [[nodiscard]] const QImage &thumbnail() const { return thumbnail_; } - [[nodiscard]] float thumbnail_position_in_clip_duration() const { - return thumbnail_position_in_clip_duration_; - } - [[nodiscard]] float pixelAspect() const { return pixel_aspect_; } - - signals: - void fileNameChanged(); - void pathChanged(); - void fpsChanged(); - void nameChanged(); - void uuidChanged(); - void backendChanged(); - void streamsChanged(); - void currentChanged(); - void metadataChanged(); - void bitDepthChanged(); - void formatChanged(); - void resolutionChanged(); - void durationChanged(); - void thumbnailURLChanged(); - void mediaStatusChanged(); - void thumbnailChanged(const QImage &, const float); - void pixelAspectChanged(); - - public slots: - void initSystem(QObject *system_qobject); - QString getThumbnailURL(int frame = -1) { - return getThumbnailURLFuture(frame).result(); - } - QFuture getThumbnailURLFuture(int frame = -1); - void requestThumbnail(float position_in_clip_duration); - void cancelThumbnailRequest(); - void setFpsString(const QString &fps); - void switchToStream(const QUuid stream_uuid); - void switchToStream(const QString stream_id); - - private: - void update_from_detail(const utility::ContainerDetail &detail); - void update_on_change(); - void set_sources_count(const size_t count); - void inspect_metadata(const nlohmann::json &); - void update_from_backend( - const utility::JsonStore &meta_data, - const utility::JsonStore &colour_params, - const utility::ContainerDetail &detail); - void update_streams_from_backend( - const std::string filename, - const std::string fps_string, - const double fps, - const media::StreamDetail stream_detail, - std::vector streams, - utility::Uuid current_stream); - - private: - caf::actor backend_; - caf::actor backend_events_; - QString name_; - QString file_name_; - QString file_path_; - utility::Uuid uuid_; - MediaStreamUI *current_{nullptr}; - std::vector streams_; - QString bit_depth_; - QString format_; - QString resolution_; - float pixel_aspect_; - QString fps_string_; - double fps_; - QString duration_seconds_; - QString duration_frames_; - QString duration_timecode_; - int duration_frames_int_{0}; - bool colourManaged_ = {false}; - media::MediaStatus media_status_{media::MediaStatus::MS_ONLINE}; - QImage thumbnail_; - float thumbnail_position_in_clip_duration_; - utility::Uuid latest_thumb_request_job_id_; - }; - - class MediaUI : public QMLActor { - Q_OBJECT - - Q_PROPERTY(QString name READ name NOTIFY nameChanged) - Q_PROPERTY(QObject *mediaSource READ mediaSource NOTIFY mediaSourceChanged) - Q_PROPERTY(QUuid uuid READ uuid NOTIFY uuidChanged) - Q_PROPERTY(QString flag READ flag WRITE setFlag NOTIFY flagChanged) - Q_PROPERTY(QString flagText READ flagText WRITE setFlagText NOTIFY flagTextChanged) - Q_PROPERTY(QString mediaStatus READ mediaStatus NOTIFY mediaStatusChanged) - Q_PROPERTY(bool mediaOnline READ mediaOnline NOTIFY mediaStatusChanged) - - public: - explicit MediaUI(QObject *parent = nullptr); - ~MediaUI() override = default; - - void init(caf::actor_system &system) override; - void set_backend(caf::actor backend); - - caf::actor backend() { return backend_; } - QUuid uuid() const { return QUuidFromUuid(uuid_); } - - QString name() { return name_; } - QObject *mediaSource() { return media_source_; } - - utility::UuidActor backend_ua() const { - return utility::UuidActor(uuid_, backend_); - } - [[nodiscard]] QString flag() const { return QStringFromStd(flag_); } - [[nodiscard]] QString flagText() const { return QStringFromStd(flag_text_); } - - [[nodiscard]] bool mediaOnline() const; - [[nodiscard]] QString mediaStatus() const; - - Q_INVOKABLE QString getMetadata() { return getMetadataFuture().result(); } - Q_INVOKABLE QFuture getMetadataFuture(); - - signals: - - void mediaSourceChanged(); - void nameChanged(); - void uuidChanged(); - void backendChanged(); - void flagChanged(); - void flagTextChanged(); - void mediaStatusChanged(); - - public slots: - - void initSystem(QObject *system_qobject); - void setCurrentSource(const QUuid &uuid); - void setFlag(const QString &_flag); - void setFlagText(const QString &_flag_text); - - private: - void update_on_change(); - - private: - caf::actor backend_; - caf::actor backend_events_; - QString name_; - utility::Uuid uuid_; - MediaSourceUI *media_source_{nullptr}; - std::vector sources_; - std::string flag_; - std::string flag_text_; - }; - - } // namespace qml -} // namespace ui -} // namespace xstudio \ No newline at end of file diff --git a/retired/playlist/src/CMakeLists.txt b/retired/playlist/src/CMakeLists.txt deleted file mode 100644 index 7cc3d7d03..000000000 --- a/retired/playlist/src/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core - xstudio::ui::qml::helper - xstudio::ui::qml::playhead - xstudio::utility -) - -SET(EXTRAMOC -) - -create_qml_component(playlist 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/retired/playlist/src/playlist_ui.cpp b/retired/playlist/src/playlist_ui.cpp deleted file mode 100644 index 20bf07c55..000000000 --- a/retired/playlist/src/playlist_ui.cpp +++ /dev/null @@ -1,1235 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/ui/qml/contact_sheet_ui.hpp" -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/ui/qml/timeline_ui.hpp" -#include "xstudio/ui/qml/playlist_selection_ui.hpp" -#include "xstudio/ui/qml/playlist_ui.hpp" -#include "xstudio/ui/qml/session_ui.hpp" -#include "xstudio/ui/qml/subset_ui.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" -#include "xstudio/global_store/global_store.hpp" - -using namespace caf; -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::ui::qml; -using namespace xstudio::global_store; - -PlaylistUI::PlaylistUI(const utility::Uuid cuuid, const utility::Uuid uuid, QObject *parent) - : QMLActor(parent), - cuuid_(std::move(cuuid)), - uuid_(std::move(uuid)), - backend_(), - backend_events_(), - name_("unknown"), - flag_("#00000000") { - emit mediaModelChanged(); -} - -void PlaylistUI::setName(const QString &name) { - std::string _name = StdFromQString(name); - if (_name != name_) { - if (backend_) { - scoped_actor sys{system()}; - sys->anon_send(backend_, utility::name_atom_v, _name); - } - } -} - -void PlaylistUI::setSelected(const bool value, const bool recurse) { - - if (selected_ != value) { - selected_ = value; - emit selectedChanged(); - } - if (recurse) { - for (auto i : items_) { - setSelection(i, false); - } - } -} - - -void PlaylistUI::set_backend(caf::actor backend, const bool partial) { - - backend_ = caf::actor(); - selected_subitem_ = nullptr; - scoped_actor sys{system()}; - - if (backend_events_) { - try { - request_receive( - *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - backend_events_ = caf::actor(); - } - - if (partial) { - backend_ = backend; - } else { - - try { - auto detail = request_receive(*sys, backend, detail_atom_v); - name_ = detail.name_; - uuid_ = detail.uuid_; - backend_events_ = detail.group_; - backend_ = backend; - - request_receive( - *sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); - - emit backendChanged(); - emit nameChanged(); - emit uuidChanged(); - emit selectedSubitemChanged(); - update_on_change(false); - - auto selection_actor = - request_receive(*sys, backend_, playlist::selection_actor_atom_v); - playlist_selection_ = new PlaylistSelectionUI(this); - playlist_selection_->initSystem(this); - playlist_selection_->set_backend(selection_actor); - emit playlistSelectionThingChanged(); - - // this will force the backend playlist to re-broadcast it's list of media - // back to us (via the event_group) - anon_send(backend_, playlist::media_content_changed_atom_v); - - } catch (const std::exception &e) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, e.what()); - } - } -} - -void PlaylistUI::initSystem(QObject *system_qobject) { - init(dynamic_cast(system_qobject)->system()); -} - - -QFuture PlaylistUI::setJSONFuture(const QString &json, const QString &path) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - scoped_actor sys{system()}; - - auto result = request_receive( - *sys, - backend_, - json_store::set_json_atom_v, - JsonStore(nlohmann::json::parse(StdFromQString(json))), - StdFromQString(path)); - - return result; - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - return false; - } - } - return false; - }); -} - -QFuture PlaylistUI::getJSONFuture(const QString &path) { - return QtConcurrent::run([=]() { - if (backend_) { - try { - scoped_actor sys{system()}; - - auto result = request_receive( - *sys, backend_, json_store::get_json_atom_v, StdFromQString(path)); - - return QStringFromStd(result.dump()); - } catch (const std::exception &) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - return QString(); // QStringFromStd(err.what()); - } - } - return QString(); - }); -} - - -bool PlaylistUI::setJSON(const QString &json, const QString &path) { - return setJSONFuture(json, path).result(); -} - -QString PlaylistUI::getJSON(const QString &path) { return getJSONFuture(path).result(); } - - -QObject *PlaylistUI::selectionFilter() { return static_cast(playlist_selection_); } - -// QUrl to Uri ? -// we need trun a URL into media objects. -// url maybe a dir.. -// Turn URL into list.. -// do we handle http:// ? Or just file:// -QFuture> PlaylistUI::loadMediaFuture(const QUrl &path) { - return QtConcurrent::run([=]() { - // Code in this block will run in another thread - QList result; - try { - scoped_actor sys{system()}; - auto new_media = request_receive( - *sys, backend_, playlist::add_media_atom_v, UriFromQUrl(path), true); - for (const auto &i : new_media) - result.push_back(QUuidFromUuid(i.uuid())); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; - }); -} - -QFuture> PlaylistUI::handleDropFuture(const QVariantMap &drop) { - // handle drag drop.. - return QtConcurrent::run([=]() { - scoped_actor sys{system()}; - QList results; - - auto jsn = dropToJsonStore(drop); - - // conver to json.. - if (jsn.count("text/uri-list")) { - for (const auto &path : jsn["text/uri-list"]) { - auto uri = caf::make_uri(path); - if (uri) { - auto new_media = request_receive( - *sys, backend_, playlist::add_media_atom_v, *uri, true); - for (const auto &i : new_media) - results.push_back(QUuidFromUuid(i.uuid())); - } - } - } else { - // forward to datasources for non file paths - auto pm = system().registry().template get(plugin_manager_registry); - - try { - auto result = request_receive( - *sys, pm, data_source::use_data_atom_v, JsonStore(jsn), true); - if (not result.empty()) { - // we've got a collection of actors.. - // lets assume they are media... (WARNING this may not be the case...) - // create new playlist and add them... - for (const auto &i : result) { - anon_send(backend_, playlist::add_media_atom_v, i, utility::Uuid()); - results.push_back(QUuidFromUuid(i.uuid())); - } - } else { - // try file load.. - for (const auto &i : jsn["text/plain"]) { - auto uri = caf::make_uri("file:" + i.get()); - if (uri) { - auto new_media = request_receive( - *sys, backend_, playlist::add_media_atom_v, *uri, true); - for (const auto &j : new_media) - results.push_back(QUuidFromUuid(j.uuid())); - } - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - return results; - }); -} - - -// QFuture PlaylistUI::addMediaFuture(const QUrl &path, const QString &name, const QUuid -// &) { -// return QtConcurrent::run([=]() { -// // Code in this block will run in another thread -// QUuid result; -// try { -// scoped_actor sys{system()}; -// auto new_media = request_receive( -// *sys, -// backend_, -// playlist::add_media_atom_v, -// StdFromQString(name), -// UriFromQUrl(path) -// ); -// result = QUuidFromUuid(new_media.first); -// } catch (const std::exception &e) { -// spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); -// } -// return result; -// }); -// } - -void PlaylistUI::setSelection(const QVariantList &cuuids, const bool clear) { - emit newSelection(cuuids, clear); - - std::set uuids; - for (auto &i : cuuids) { - uuids.insert(i.toUuid()); - } - - bool selected_subitem_changed = false; - - // clear unselected - for (QMap::iterator i = items_.begin(); i != items_.end(); i++) { - if (not uuids.count(i.key()) and clear) { - setSelection(i.value(), false); - if (i.value() == selected_subitem_) { - selected_subitem_ = nullptr; - selected_subitem_changed = true; - } - } - } - - // selected - for (QMap::iterator i = items_.begin(); i != items_.end(); i++) { - if (uuids.count(i.key())) { - setSelection(i.value(), true); - } - } - - if (selected_subitem_changed) - emit selectedSubitemChanged(); -} - -void PlaylistUI::setSelection(QObject *obj, const bool selected) { - if (auto item = dynamic_cast(obj)) { - item->setSelected(selected); - item->setSelection(QVariantList()); - } else if (auto item = dynamic_cast(obj)) { - item->setSelected(selected); - if (!selected_subitem_ and selected) { - selected_subitem_ = obj; - emit selectedSubitemChanged(); - } - } else if (auto item = dynamic_cast(obj)) { - item->setSelected(selected); - if (not selected_subitem_ and selected) { - selected_subitem_ = obj; - emit selectedSubitemChanged(); - } - } else if (auto item = dynamic_cast(obj)) { - item->setSelected(selected); - if (!selected_subitem_ and selected) { - selected_subitem_ = obj; - emit selectedSubitemChanged(); - } - } else if (auto item = dynamic_cast(obj)) { - item->setSelected(selected); - } else if (auto item = dynamic_cast(obj)) { - item->setSelected(selected); - } -} - -void PlaylistUI::dragDropReorder(const QVariantList dropped_uuids, const QString before_uuid) { - try { - - utility::UuidList drop_uuids; - for (auto v : dropped_uuids) { - drop_uuids.push_back(UuidFromQUuid(v.toUuid())); - } - utility::Uuid before = before_uuid == "" ? Uuid() : Uuid(StdFromQString(before_uuid)); - anon_send(backend_, playlist::move_media_atom_v, drop_uuids, before); - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } -} - -void PlaylistUI::sortAlphabetically() { - anon_send(backend_, playlist::sort_alphabetically_atom_v); -} - - -void PlaylistUI::update_on_change(const bool select_new_items) { - - updateItemModel(select_new_items); - emit itemModelChanged(); -} - - -void PlaylistUI::init(actor_system &system_) { - QMLActor::init(system_); - emit systemInit(this); - - spdlog::debug("PlaylistUI init"); - - // self()->set_down_handler([=](down_msg& msg) { - // if(msg.source == store) - // unsubscribe(); - // }); - - set_message_handler([=](actor_companion * /*self_*/) -> message_handler { - return { - [=](backend_atom, caf::actor actor) { - set_backend(actor); - - // If this PlaylistUI is the one showing in the viewer, tell the backend - // so it can open readers on the media in the playlist ready for quick - // viewing - if (auto session = dynamic_cast(parent())) { - if (session->playlist() == static_cast(this)) { - anon_send(actor, playlist::set_playlist_in_viewer_atom_v, true); - } - } - }, - - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg &) { - // if (msg.source == backend_events_) - // self()->leave(backend_events_); - }, - - [=](utility::event_atom, playlist::add_media_atom, const UuidActor &ua) { - spdlog::warn("media added, emit signal"); - emit mediaAdded(QUuidFromUuid(ua.uuid())); - }, - - [=](utility::event_atom, playlist::create_contact_sheet_atom, const UuidActor &) { - setExpanded(true); - updateItemModel(); - emit itemModelChanged(); - // emit newItem(QVariant::fromValue(dynamic_cast(findItem(ua.first))->qcuuid())); - }, - - [=](utility::event_atom, playlist::create_divider_atom, const Uuid &uuid) { - setExpanded(true); - updateItemModel(); - emit itemModelChanged(); - emit newItem(QVariant::fromValue(QUuidFromUuid(uuid))); - }, - - [=](utility::event_atom, playlist::create_group_atom, const Uuid &uuid) { - setExpanded(true); - updateItemModel(); - emit itemModelChanged(); - emit newItem(QVariant::fromValue(QUuidFromUuid(uuid))); - }, - - [=](utility::event_atom, playlist::create_timeline_atom, const UuidActor &) { - setExpanded(true); - updateItemModel(); - emit itemModelChanged(); - // emit newItem(QVariant::fromValue(dynamic_cast(findItem(ua.first))->qcuuid())); - }, - - [=](utility::event_atom, playlist::create_subset_atom, const UuidActor &) { - setExpanded(true); - updateItemModel(); - emit itemModelChanged(); - // emit newItem(QVariant::fromValue(dynamic_cast(findItem(ua.first))->qcuuid())); - }, - - [=](utility::event_atom, playlist::loading_media_atom, const bool is_loading) { - if (loadingMedia_ != is_loading) { - loadingMedia_ = is_loading; - emit loadingMediaChanged(); - } - }, - - [=](utility::event_atom, - playlist::media_content_changed_atom, - const std::vector &actors) { - const auto tt = utility::clock::now(); - for (auto i : actors) { - if (not uuid_media_.count(i.uuid())) { - // subscribe to status event.. - uuid_media_[i.uuid()] = new MediaUI(this); - QQmlEngine::setObjectOwnership( - uuid_media_[i.uuid()], QQmlEngine::CppOwnership); - QObject::connect( - uuid_media_[i.uuid()], - &MediaUI::mediaStatusChanged, - this, - &PlaylistUI::offlineMediaCountChanged); - - uuid_media_[i.uuid()]->initSystem(this); - anon_send(uuid_media_[i.uuid()]->as_actor(), backend_atom_v, i.actor()); - } - } - - // remove old media.. - for (auto it = uuid_media_.begin(); it != uuid_media_.end(); ++it) { - bool found = false; - for (auto i : actors) { - if (i.uuid() == it->first) { - found = true; - break; - } - } - if (not found) { - delete (it->second); - it = uuid_media_.erase(it); - - // hitting a weird bug in this for loop - // if uuid_media_ gets emptied it hangs - // unless we break here - if (!uuid_media_.size()) - break; - } - } - - // rebuild list. - media_.clear(); - media_order_.clear(); - auto ind = 0; - for (auto i : actors) { - media_.append(static_cast(uuid_media_[i.uuid()])); - media_order_[QUuidFromUuid(i.uuid()).toString()] = QVariant::fromValue(ind); - ind++; - } - - - media_model_.populate(media_); - - emit offlineMediaCountChanged(); - emit mediaOrderChanged(); - emit mediaListChanged(); - - spdlog::debug( - "PlaylistUI event_atom, media_content_changed_atom message handler took {} " - "milliseconds to complete", - std::chrono::duration_cast( - utility::clock::now() - tt) - .count()); - }, - - [=](utility::event_atom, - playlist::move_container_atom, - const Uuid &, - const Uuid &, - const bool) { - updateItemModel(); - emit itemModelChanged(); - }, - - [=](utility::event_atom, - playlist::reflag_container_atom, - const Uuid &, - const std::string &) { - updateItemModel(); - emit itemModelChanged(); - }, - - [=](utility::event_atom, - playlist::remove_container_atom, - const std::vector &) {}, - - [=](utility::event_atom, playlist::remove_container_atom, const Uuid &) { - updateItemModel(); - emit itemModelChanged(); - }, - - [=](utility::event_atom, - playlist::rename_container_atom, - const Uuid &, - const std::string &) { - updateItemModel(); - emit itemModelChanged(); - }, - - [=](utility::event_atom, utility::change_atom) { update_on_change(); }, - - [=](utility::event_atom, utility::last_changed_atom, const time_point &) { - update_on_change(false); - }, - - [=](utility::event_atom, utility::name_atom, const std::string &name) { - if (name != name_) { - name_ = name; - emit nameChanged(); - } - }, - - [=](utility::last_changed_atom, const time_point &) {}}; - }); -} - -QUuid PlaylistUI::findCUuidFromUuid(const utility::Uuid &uuuid) const { - QMap::const_iterator i = items_.constBegin(); - QUuid cuuid; - - while (i != items_.constEnd()) { - if (uuid(i.value()) == uuuid) - return i.key(); - // else if(auto item = dynamic_cast(i)){ - // cuuid = item->findCUuidFromUuid(uuid); - // if(cuuid != QUuid()) - // return cuuid; - // } - - ++i; - } - - return cuuid; -} - -QObject *PlaylistUI::findItem(const utility::Uuid &suuid) { - QUuid qu = QUuidFromUuid(suuid); - - auto f = items_.find(qu); - if (f != items_.end()) - return *f; - - for (auto &i : items_) { - if (uuid(i) == suuid) - return i; - if (auto item = dynamic_cast(i)) { - QObject *found = item->findItem(suuid); - if (found) - return found; - } - } - return nullptr; -} - -utility::Uuid PlaylistUI::uuid(const QObject *obj) const { - if (auto item = dynamic_cast(obj)) { - return UuidFromQUuid(item->uuid()); - } else if (auto item = dynamic_cast(obj)) { - return item->uuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->uuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->uuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->uuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->uuid(); - } - - return utility::Uuid(); -} - -utility::Uuid PlaylistUI::cuuid(const QObject *obj) const { - if (auto item = dynamic_cast(obj)) { - return item->cuuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->cuuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->cuuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->cuuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->cuuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->cuuid(); - } - - return utility::Uuid(); -} - -QUuid PlaylistUI::createTimeline(const QString &name, const QUuid &before, const bool into) { - QUuid result; - try { - scoped_actor sys{system()}; - auto new_item = request_receive( - *sys, - backend_, - playlist::create_timeline_atom_v, - StdFromQString(name), - UuidFromQUuid(before), - into); - result = QUuidFromUuid(new_item.second.uuid()); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - -QUuid PlaylistUI::createSubset(const QString &name, const QUuid &before, const bool into) { - QUuid result; - try { - scoped_actor sys{system()}; - auto new_item = request_receive( - *sys, - backend_, - playlist::create_subset_atom_v, - StdFromQString(name), - UuidFromQUuid(before), - into); - result = QUuidFromUuid(new_item.second.uuid()); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - -QUuid PlaylistUI::createContactSheet( - const QString &name, const QUuid &before, const bool into) { - QUuid result; - try { - scoped_actor sys{system()}; - auto new_item = request_receive( - *sys, - backend_, - playlist::create_contact_sheet_atom_v, - StdFromQString(name), - UuidFromQUuid(before), - into); - result = QUuidFromUuid(new_item.second.uuid()); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - -QUuid PlaylistUI::createDivider(const QString &name, const QUuid &before, const bool into) { - QUuid result; - try { - scoped_actor sys{system()}; - auto new_item = request_receive( - *sys, - backend_, - playlist::create_divider_atom_v, - StdFromQString(name), - UuidFromQUuid(before), - into); - result = QUuidFromUuid(new_item); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - -QUuid PlaylistUI::createGroup(const QString &name, const QUuid &before) { - QUuid result; - try { - scoped_actor sys{system()}; - auto new_item = request_receive( - *sys, - backend_, - playlist::create_group_atom_v, - StdFromQString(name), - UuidFromQUuid(before)); - result = QUuidFromUuid(new_item); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - -bool PlaylistUI::reflagContainer(const QString &flag, const QUuid &uuid) { - bool result = false; - try { - scoped_actor sys{system()}; - auto new_item = request_receive( - *sys, - backend_, - playlist::reflag_container_atom_v, - StdFromQString(flag), - UuidFromQUuid(uuid)); - result = new_item; - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - - -bool PlaylistUI::renameContainer(const QString &name, const QUuid &uuid) { - bool result = false; - try { - scoped_actor sys{system()}; - auto new_item = request_receive( - *sys, - backend_, - playlist::rename_container_atom_v, - StdFromQString(name), - UuidFromQUuid(uuid)); - result = new_item; - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - -bool PlaylistUI::removeContainer(const QUuid &uuid, const bool remove_media) { - bool result = false; - try { - scoped_actor sys{system()}; - auto new_item = request_receive( - *sys, - backend_, - playlist::remove_container_atom_v, - UuidFromQUuid(uuid), - remove_media); - result = new_item; - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - -bool PlaylistUI::removeMedia(const QUuid &uuid) { - bool result = false; - try { - scoped_actor sys{system()}; - result = request_receive( - *sys, backend_, playlist::remove_media_atom_v, UuidFromQUuid(uuid)); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - - -bool PlaylistUI::moveContainer(const QUuid &uuid, const QUuid &before_uuid, const bool into) { - bool result = false; - try { - scoped_actor sys{system()}; - auto new_item = request_receive( - *sys, - backend_, - playlist::move_container_atom_v, - UuidFromQUuid(uuid), - UuidFromQUuid(before_uuid), - into); - result = new_item; - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - - -QUuid PlaylistUI::duplicateContainer( - const QUuid &cuuid, const QUuid &before_cuuid, const bool into) { - QUuid result; - try { - scoped_actor sys{system()}; - auto new_playlist = request_receive( - *sys, - backend_, - playlist::duplicate_container_atom_v, - UuidFromQUuid(cuuid), - UuidFromQUuid(before_cuuid), - into); - result = QUuidFromUuid(new_playlist[0]); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - -void PlaylistUI::updateItemModel(const bool select_new_items) { - - auto tt = utility::clock::now(); - - try { - scoped_actor sys{system()}; - std::map uuid_actor; - std::map hold; - QList items_list_; - - auto rec_items_container = request_receive( - *sys, backend_, playlist::get_container_atom_v); - - if (last_changed_tree_ == rec_items_container) - return; - - last_changed_tree_ = rec_items_container; - - // build look up for playlist actors - for (const auto &i : request_receive>( - *sys, backend_, playlist::get_container_atom_v, true)) { - uuid_actor[i.uuid()] = i.actor(); - } - // decompose.. - // all objects now in hold. - - bool last_selected_still_valid = false; - - while (!items_.empty()) { - if (auto item = dynamic_cast(items_.begin().value())) { - hold[item->uuid()] = items_.begin().value(); - } else if (auto item = dynamic_cast(items_.begin().value())) { - hold[item->uuid()] = items_.begin().value(); - } else if (auto item = dynamic_cast(items_.begin().value())) { - hold[item->uuid()] = items_.begin().value(); - } else if (auto item = dynamic_cast(items_.begin().value())) { - hold[item->uuid()] = items_.begin().value(); - } else if (auto item = dynamic_cast(items_.begin().value())) { - hold[item->uuid()] = items_.begin().value(); - while (not item->items_.empty()) { - if (auto iitem = dynamic_cast(item->items_[0])) { - hold[iitem->uuid()] = item->items_[0]; - } else if (auto iitem = dynamic_cast(item->items_[0])) { - hold[iitem->uuid()] = item->items_[0]; - } else if (auto iitem = dynamic_cast(item->items_[0])) { - hold[iitem->uuid()] = item->items_[0]; - } else if (auto iitem = dynamic_cast(item->items_[0])) { - hold[iitem->uuid()] = item->items_[0]; - } - item->items_.removeAt(0); - } - } - items_.erase(items_.begin()); - } - - QVariantList new_objects; - // Our QList is a collection of (playlists and playlist groups) - // we use uuids as the index, as names may change.. - // playlist groups are only at the top level. - for (auto &i : rec_items_container.children_ref()) { - // auto i = dynamic_cast(i_); - - // spdlog::info("{} {} {} {}", to_string(i.uuid()), i.value().name(), - // i.value().type(), to_string(i.value().uuid())); is item in hold? - auto qcu = QUuidFromUuid(i.uuid()); - auto uuid = i.value().uuid(); - auto type = i.value().type(); - - if (hold.count(uuid)) { - if (type == "ContainerGroup") { - auto obj = dynamic_cast(hold[uuid]); - obj->setName(QStringFromStd(i.value().name())); - } else if (type == "ContainerDivider") { - auto obj = dynamic_cast(hold[uuid]); - obj->setName(QStringFromStd(i.value().name())); - obj->setFlag(QStringFromStd(i.value().flag())); - } else if (type == "Timeline") { - auto obj = dynamic_cast(hold[uuid]); - obj->setFlag(QStringFromStd(i.value().flag())); - } else if (type == "Subset") { - auto obj = dynamic_cast(hold[uuid]); - obj->setFlag(QStringFromStd(i.value().flag())); - } else if (type == "ContactSheet") { - auto obj = dynamic_cast(hold[uuid]); - obj->setFlag(QStringFromStd(i.value().flag())); - } - - if (hold[uuid] == selected_subitem_) { - last_selected_still_valid = true; - } - items_list_.append(hold[uuid]); - items_[qcu] = hold[uuid]; - hold.erase(uuid); - } else { - // new item - if (type == "ContainerGroup") { - auto obj = new ContainerGroupUI( - i.uuid(), - i.value().name(), - i.value().type(), - i.value().flag(), - i.value().uuid(), - this); - new_objects.append(qcu); - items_[qcu] = obj; - items_list_.append(obj); - } else if (type == "ContainerDivider") { - new_objects.append(qcu); - auto obj = new ContainerDividerUI( - i.uuid(), - i.value().name(), - i.value().type(), - i.value().flag(), - i.value().uuid(), - this); - new_objects.append(qcu); - items_[qcu] = obj; - items_list_.append(obj); - } else if (type == "Timeline") { - new_objects.append(qcu); - auto obj = new TimelineUI(i.uuid(), this); - items_[qcu] = obj; - items_list_.append(obj); - obj->init(system()); - obj->set_backend(uuid_actor[uuid]); - obj->setFlag(QStringFromStd(i.value().flag())); - } else if (type == "Subset") { - new_objects.append(qcu); - auto obj = new SubsetUI(i.uuid(), this); - items_[qcu] = obj; - items_list_.append(obj); - obj->init(system()); - obj->set_backend(uuid_actor[uuid]); - obj->setFlag(QStringFromStd(i.value().flag())); - } else if (type == "ContactSheet") { - new_objects.append(qcu); - auto obj = new ContactSheetUI(i.uuid(), this); - items_[qcu] = obj; - items_list_.append(obj); - obj->init(system()); - obj->set_backend(uuid_actor[uuid]); - obj->setFlag(QStringFromStd(i.value().flag())); - } - } - - if (type == "ContainerGroup") { - auto new_group = dynamic_cast(items_[qcu]); - // as it's a groups we need to scan it.. - for (const auto &ii : i.children_ref()) { - // auto ii = dynamic_cast(ii_); - spdlog::info( - " {} {} {}", - ii.value().name(), - ii.value().type(), - to_string(ii.value().uuid())); - auto chtype = ii.value().type(); - auto chuuid = ii.value().uuid(); - - if (hold.count(chuuid)) { - new_group->items_.append(hold[chuuid]); - if (chtype == "ContainerGroup") { - dynamic_cast(hold[chuuid]) - ->setName(QStringFromStd(ii.value().name())); - } else if (chtype == "ContainerDivider") { - dynamic_cast(hold[chuuid]) - ->setName(QStringFromStd(ii.value().name())); - dynamic_cast(hold[chuuid]) - ->setFlag(QStringFromStd(ii.value().flag())); - } else if (chtype == "Timeline") { - auto obj = dynamic_cast(hold[chuuid]); - obj->setFlag(QStringFromStd(ii.value().flag())); - } else if (chtype == "Subset") { - auto obj = dynamic_cast(hold[chuuid]); - obj->setFlag(QStringFromStd(ii.value().flag())); - } else if (chtype == "ContactSheet") { - auto obj = dynamic_cast(hold[chuuid]); - obj->setFlag(QStringFromStd(ii.value().flag())); - } - hold.erase(chuuid); - } else if (chtype == "ContainerDivider") { - new_objects.append(QUuidFromUuid(chuuid)); - auto obj = new ContainerDividerUI( - ii.uuid(), - ii.value().name(), - ii.value().type(), - ii.value().flag(), - ii.value().uuid(), - this); - new_group->items_.append(obj); - } else if (chtype == "Timeline") { - new_objects.append(QUuidFromUuid(chuuid)); - auto obj = new TimelineUI(i.uuid(), this); - new_group->items_.append(obj); - obj->init(system()); - obj->set_backend(uuid_actor[chuuid]); - obj->setFlag(QStringFromStd(ii.value().flag())); - } else if (chtype == "Subset") { - new_objects.append(QUuidFromUuid(chuuid)); - auto obj = new SubsetUI(i.uuid(), this); - new_group->items_.append(obj); - obj->init(system()); - obj->set_backend(uuid_actor[chuuid]); - obj->setFlag(QStringFromStd(ii.value().flag())); - } else if (chtype == "ContactSheet") { - new_objects.append(QUuidFromUuid(chuuid)); - auto obj = new ContactSheetUI(i.uuid(), this); - new_group->items_.append(obj); - obj->init(system()); - obj->set_backend(uuid_actor[chuuid]); - obj->setFlag(QStringFromStd(ii.value().flag())); - } - } - new_group->changedItems(); - } - } - - // these are gone. - for (auto &i : hold) { - // we should notify the item that it's backend is gone.. - if (auto item = dynamic_cast(i.second)) { - item->set_backend(caf::actor()); - } else if (auto item = dynamic_cast(i.second)) { - item->set_backend(caf::actor()); - } else if (auto item = dynamic_cast(i.second)) { - item->set_backend(caf::actor()); - } - } - - if (!last_selected_still_valid and selected_subitem_) { - selected_subitem_ = nullptr; - emit selectedSubitemChanged(); - } - - // select first new object. - if (new_objects.count() and select_new_items) { - auto i = new_objects[0]; - new_objects.clear(); - new_objects.append(i); - setSelection(new_objects, true); - } - - - model_.populate(items_list_, this); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - spdlog::debug( - "PlaylistUI::updateItemModeltook {} milliseconds to complete", - std::chrono::duration_cast(utility::clock::now() - tt) - .count()); -} - -// find item based off container uuid or actor uuid -QObject *PlaylistUI::getNextItem(const utility::Uuid &cuuid) { - // might be media uuid ? - if (uuid_media_.count(cuuid)) { - // find nextmedia - QObject *mojb = uuid_media_[cuuid]; - int ind = 0; - for (auto &i : media_) { - if (i == mojb) { - ind++; - if (media_.size() > ind) { - return media_[ind]; - } - break; - } - ind++; - } - } - - try { - scoped_actor sys{system()}; - auto rec_items_container = request_receive( - *sys, backend_, playlist::get_container_atom_v); - auto next_item = rec_items_container.find_next_at_same_depth(cuuid); - if (next_item) - return findItem(*next_item); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - return nullptr; -} - -QObject *PlaylistUI::findMediaObject(const QUuid &quuid) { - auto uuid = UuidFromQUuid(quuid); - - if (uuid_media_.count(uuid)) { - return uuid_media_[uuid]; - } - return nullptr; -} - - -QUuid PlaylistUI::getNextItemUuid(const QUuid &quuid) { - QUuid result; - - auto obj = getNextItem(UuidFromQUuid(quuid)); - if (obj) - return QUuidFromUuid(uuid(obj)); - - try { - scoped_actor sys{system()}; - auto rec_items_container = request_receive( - *sys, backend_, playlist::get_container_atom_v); - auto next_item = - rec_items_container.find_next_value_at_same_depth(UuidFromQUuid(quuid)); - if (next_item) - result = QUuidFromUuid(*next_item); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return result; -} - -bool PlaylistUI::contains(const QUuid &uuid_or_cuuid, QUuid &cuuid) const { - QMap::const_iterator i = items_.begin(); - - while (i != items_.end()) { - if (i.key() == uuid_or_cuuid) { - cuuid = uuid_or_cuuid; - return true; - } - QObject *oo = i.value(); - if (auto *ui = dynamic_cast(oo)) { - if (ui->quuid() == uuid_or_cuuid) { - cuuid = ui->qcuuid(); - return true; - } - } - if (auto *ui = dynamic_cast(oo)) { - if (ui->quuid() == uuid_or_cuuid) { - cuuid = ui->qcuuid(); - return true; - } - } - if (auto *ui = dynamic_cast(oo)) { - if (ui->quuid() == uuid_or_cuuid) { - cuuid = ui->qcuuid(); - return true; - } - } - ++i; - } - - return false; -} - -QUuid PlaylistUI::convertToContactSheet( - const QUuid &uuid_or_cuuid, const QString &name, const QUuid &before, const bool) { - QUuid result; - - scoped_actor sys{system()}; - try { - auto uua = request_receive>( - *sys, - backend_, - playlist::convert_to_contact_sheet_atom_v, - UuidFromQUuid(uuid_or_cuuid), - StdFromQString(name), - UuidFromQUuid(before)); - result = QUuidFromUuid(uua.first); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return result; -} - -QUuid PlaylistUI::convertToSubset( - const QUuid &uuid_or_cuuid, const QString &name, const QUuid &before, const bool) { - QUuid result; - scoped_actor sys{system()}; - try { - auto uua = request_receive>( - *sys, - backend_, - playlist::convert_to_subset_atom_v, - UuidFromQUuid(uuid_or_cuuid), - StdFromQString(name), - UuidFromQUuid(before)); - result = QUuidFromUuid(uua.first); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return result; -} - -bool PlaylistUI::contains_media(const QUuid &key) const { - return uuid_media_.count(UuidFromQUuid(key)); -} - -void PlaylistUI::build_media_order() { - media_order_.clear(); - for (auto i = 0; i < media_.size(); i++) { - media_order_[QUuidFromUuid(uuid(media_[i])).toString()] = QVariant::fromValue(i); - } -} - -[[nodiscard]] int PlaylistUI::offlineMediaCount() const { - auto offline = 0; - for (const auto &i : media_) { - auto m = qobject_cast(i); - if (m and not m->mediaOnline()) - offline++; - } - return offline; -} diff --git a/retired/playlist/test/CMakeLists.txt b/retired/playlist/test/CMakeLists.txt deleted file mode 100644 index ca5580c50..000000000 --- a/retired/playlist/test/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -include(CTest) diff --git a/retired/playlist_selection_ui.cpp b/retired/playlist_selection_ui.cpp deleted file mode 100644 index d1b80ec19..000000000 --- a/retired/playlist_selection_ui.cpp +++ /dev/null @@ -1,204 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/playhead/playhead.hpp" -#include "xstudio/playhead/playhead_actor.hpp" -#include "xstudio/playhead/playhead_selection_actor.hpp" -#include "xstudio/ui/qml/playlist_selection_ui.hpp" -#include "xstudio/utility/edit_list.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" -#include "xstudio/utility/media_reference.hpp" -#include "xstudio/utility/timecode.hpp" - -using namespace caf; -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::ui::qml; - -PlaylistSelectionUI::PlaylistSelectionUI(QObject *parent) - : QMLActor(parent), backend_(), backend_events_() {} - -// helper ? - -void PlaylistSelectionUI::set_backend(caf::actor backend) { - - scoped_actor sys{system()}; - - backend_ = backend; - // get backend state.. - if (backend_events_) { - try { - request_receive( - *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - backend_events_ = caf::actor(); - } - - try { - auto detail = request_receive(*sys, backend_, detail_atom_v); - backend_events_ = detail.group_; - request_receive( - *sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - // this call triggers the backend to send us back the current selection - anon_send(backend_, playhead::get_selection_atom_v, as_actor()); -} - -void PlaylistSelectionUI::initSystem(QObject *system_qobject) { - init(dynamic_cast(system_qobject)->system()); -} - -void PlaylistSelectionUI::init(actor_system &system_) { - - QMLActor::init(system_); - - spdlog::debug("PlaylistSelectionUI init"); - - scoped_actor sys{system()}; - - set_message_handler([=](actor_companion * /*self_*/) -> message_handler { - return { - - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) { - // if(msg.source == store_events) - // unsubscribe(); - }, - - [=](utility::event_atom, playhead::source_atom, const std::vector &) { - // This comes from the 'PlaylistSelectionActor' and tells us that - // the list of viewables has changed - we then request the selection - // as a Uuid list which is received just below - anon_send(backend_, playhead::get_selection_atom_v, as_actor()); - }, - - [=](utility::event_atom, utility::change_atom) { - // something changed in the backend... update the selection - // list for QML - anon_send(backend_, playhead::get_selection_atom_v, as_actor()); - }, - - [=](utility::event_atom, utility::last_changed_atom, const time_point &) {}, - - [=](utility::event_atom, - playhead::selection_changed_atom, - const UuidList &selection) { - // this message is returned by the backend when we send it - // get_selection_atom - selected_media_.clear(); - for (const auto &uuid : selection) { - selected_media_.append(QUuidFromUuid(uuid)); - } - emit selectionChanged(); - }}; - }); -} - -void PlaylistSelectionUI::newSelection(const QVariantList &selection_list) { - - utility::UuidList selection; - selected_media_.clear(); - for (auto v : selection_list) { - selection.push_back(UuidFromQUuid(v.toUuid())); - // update our local copy as there is implied latency in this code. - selected_media_.push_back(v.toUuid()); - } - - anon_send(backend_, playlist::select_media_atom_v, selection); -} - - -QStringList PlaylistSelectionUI::selectedMediaFilePaths() { - - QStringList result; - for (const auto &i : get_selected_media_refences()) - result.append(QStringFromStd(uri_to_posix_path(i.uri())).replace("{:04d}", "####")); - return result; -} - -QStringList PlaylistSelectionUI::selectedMediaFileNames() { - - QStringList results = selectedMediaFilePaths(); - for (auto &path : results) { - path = path.mid(path.lastIndexOf("/") + 1); - } - return results; -} - -QList PlaylistSelectionUI::selectedMediaURLs() { - QList result; - - int file_frame; - for (const auto &i : get_selected_media_refences()) { - auto _uri = i.uri(0, file_frame); - if (_uri) - result.append(QUrlFromUri(*_uri)); - } - - return result; -} - -std::vector PlaylistSelectionUI::get_selected_media_refences() { - std::vector result; - - try { - scoped_actor sys{system()}; - auto selection = request_receive_wait>( - *sys, - backend_, - std::chrono::milliseconds(2500), - playhead::get_selected_sources_atom_v); - - for (auto &actor : selection) { - auto source = request_receive_wait( - *sys, - actor, - std::chrono::milliseconds(2500), - media::current_media_source_atom_v); - - result.push_back(request_receive_wait( - *sys, - source.actor(), - std::chrono::milliseconds(2500), - media::media_reference_atom_v)); - } - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - return result; -} - -QVariantList PlaylistSelectionUI::currentSelection() const { - QVariantList result; - - for (const auto &i : selected_media_) - result.push_back(QVariant::fromValue(i)); - - return result; -} - - -void PlaylistSelectionUI::deleteSelected() { - anon_send(backend_, playhead::delete_selection_from_playlist_atom_v); -} - -void PlaylistSelectionUI::gatherSourcesForSelected() { - anon_send(backend_, media_hook::gather_media_sources_atom_v); -} - -void PlaylistSelectionUI::evictSelected() { - anon_send(backend_, playhead::evict_selection_from_cache_atom_v); -} - -void PlaylistSelectionUI::moveSelectionByIndex(const int index) { - anon_send(backend_, playhead::select_next_media_atom_v, index); -} diff --git a/retired/playlist_selection_ui.hpp b/retired/playlist_selection_ui.hpp deleted file mode 100644 index 66a577834..000000000 --- a/retired/playlist_selection_ui.hpp +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include - -CAF_PUSH_WARNINGS -#include -#include -#include -CAF_POP_WARNINGS - -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/ui/qml/media_ui.hpp" -#include "xstudio/utility/frame_time.hpp" - -namespace xstudio { -namespace utility { - class MediaReference; -} -namespace ui { - namespace qml { - - class PlaylistSelectionUI : public QMLActor { - - Q_OBJECT - - Q_PROPERTY( - QList selectedMediaUuids READ selectedMedia NOTIFY selectionChanged) - - public: - explicit PlaylistSelectionUI(QObject *parent = nullptr); - ~PlaylistSelectionUI() override = default; - - void init(caf::actor_system &system) override; - void set_backend(caf::actor backend); - - caf::actor backend() { return backend_; } - - [[nodiscard]] const QList &selectedMedia() const { return selected_media_; } - - signals: - - void selectionChanged(); - - public slots: - - void initSystem(QObject *system_qobject); - void newSelection(const QVariantList &selection); - [[nodiscard]] QVariantList currentSelection() const; - void deleteSelected(); - void gatherSourcesForSelected(); - void evictSelected(); - void moveSelectionByIndex(const int index); - - QStringList selectedMediaFilePaths(); - QStringList selectedMediaFileNames(); - QList selectedMediaURLs(); - - private: - std::vector get_selected_media_refences(); - - private: - caf::actor backend_; - caf::actor backend_events_; - - QList selected_media_; - }; - } // namespace qml -} // namespace ui -} // namespace xstudio \ No newline at end of file diff --git a/retired/playlist_ui.hpp b/retired/playlist_ui.hpp deleted file mode 100644 index 9371cd7d7..000000000 --- a/retired/playlist_ui.hpp +++ /dev/null @@ -1,259 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include - -CAF_PUSH_WARNINGS -#include -#include -#include -CAF_POP_WARNINGS - -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/ui/qml/media_ui.hpp" -#include "xstudio/ui/qml/session_ui.hpp" - -namespace xstudio { -namespace ui { - namespace qml { - - class PlaylistSelectionUI; - - class PlaylistUI : public QMLActor { - - Q_OBJECT - Q_PROPERTY(QString name READ name NOTIFY nameChanged) - Q_PROPERTY(QUuid uuid READ quuid NOTIFY uuidChanged) - Q_PROPERTY(QObject *selectionFilter READ selectionFilter NOTIFY - playlistSelectionThingChanged) - Q_PROPERTY(QString type READ type NOTIFY typeChanged) - Q_PROPERTY(bool expanded READ expanded WRITE setExpanded NOTIFY expandedChanged) - Q_PROPERTY(QVariant itemModel READ itemModel NOTIFY itemModelChanged) - Q_PROPERTY(QVariant mediaModel READ mediaModel NOTIFY mediaModelChanged) - Q_PROPERTY(QList mediaList READ mediaList NOTIFY mediaListChanged) - Q_PROPERTY(QVariantMap mediaOrder READ mediaOrder NOTIFY mediaOrderChanged) - Q_PROPERTY(QUuid cuuid READ qcuuid NOTIFY cuuidChanged) - Q_PROPERTY(QString flag READ flag WRITE setFlag NOTIFY flagChanged) - Q_PROPERTY(bool selected READ selected WRITE setSelected NOTIFY selectedChanged) - Q_PROPERTY(bool loadingMedia READ loadingMedia NOTIFY loadingMediaChanged) - Q_PROPERTY(bool isBusy READ isBusy WRITE setIsBusy NOTIFY isBusyChanged) - Q_PROPERTY( - QObject *selectedSubitem READ selectedSubitem NOTIFY selectedSubitemChanged) - Q_PROPERTY(QString fullName READ name NOTIFY nameChanged) - Q_PROPERTY(bool hasBackend READ hasBackend NOTIFY backendChanged) - Q_PROPERTY( - int offlineMediaCount READ offlineMediaCount NOTIFY offlineMediaCountChanged) - - public: - explicit PlaylistUI( - const utility::Uuid cuuid = utility::Uuid(), - const utility::Uuid uuid = utility::Uuid(), - QObject *parent = nullptr); - ~PlaylistUI() override = default; - - void init(caf::actor_system &system) override; - void set_backend(caf::actor backend, const bool partial = false); - - caf::actor backend() { return backend_; } - [[nodiscard]] bool hasBackend() const { - return backend_ ? true : false; - ; - } - - [[nodiscard]] QString name() const { return QStringFromStd(name_); } - - [[nodiscard]] QVariantMap mediaOrder() const { return media_order_; } - - [[nodiscard]] QUuid quuid() const { return QUuidFromUuid(uuid_); } - [[nodiscard]] QUuid qcuuid() const { return QUuidFromUuid(cuuid_); } - [[nodiscard]] utility::Uuid uuid() const { return uuid_; } - [[nodiscard]] QString type() const { return "Playlist"; } - - QVariant itemModel() { return QVariant::fromValue(&model_); } - QVariant mediaModel() { return QVariant::fromValue(&media_model_); } - QList mediaList() { return media_; } - - [[nodiscard]] bool expanded() const { return expanded_; } - [[nodiscard]] bool selected() const { return selected_; } - [[nodiscard]] bool loadingMedia() const { return loadingMedia_; } - [[nodiscard]] bool isBusy() const { return isBusy_; } - - [[nodiscard]] utility::Uuid cuuid() const { return cuuid_; } - [[nodiscard]] QString flag() const { return QStringFromStd(flag_); } - [[nodiscard]] int offlineMediaCount() const; - - void setIsBusy(const bool busy) { - isBusy_ = busy; - emit isBusyChanged(); - } - - QObject *selectionFilter(); - - utility::Uuid cuuid(const QObject *obj) const; - - QObject *selectedSubitem() { return selected_subitem_; } - - bool contains(const QUuid &uuid_or_cuuid, QUuid &cuuid) const; - - - signals: - - void uuidChanged(); - void nameChanged(); - void typeChanged(); - void systemInit(QObject *); - void playlistSelectionThingChanged(); - void backendChanged(); - void compareModeChanged(); - void expandedChanged(); - void selectedChanged(); - void itemModelChanged(); - void mediaModelChanged(); - void cuuidChanged(); - void mediaListChanged(); - void flagChanged(); - // QUuid is the container uuid - void newItem(const QVariant &cuuid); - void newSelection(const QVariantList cuuids, const bool clear); - void selectedSubitemChanged(); - void mediaAdded(const QUuid &uuid); - void loadingMediaChanged(); - void isBusyChanged(); - void mediaOrderChanged(); - void offlineMediaCountChanged(); - - public slots: - void setName(const QString &name); - void initSystem(QObject *system_qobject); - - QList loadMedia(const QUrl &path) { return loadMediaFuture(path).result(); } - QFuture> loadMediaFuture(const QUrl &path); - - QList handleDrop(const QVariantMap &drop) { - return handleDropFuture(drop).result(); - } - QFuture> handleDropFuture(const QVariantMap &drop); - - // QUuid addMedia( - // const QUrl &path, - // const QString &name = "New Media", - // const QUuid &before_uuid = QUuid()) {return addMediaFuture(path, name, - // before_uuid).result(); } - // QFuture addMediaFuture( - // const QUrl &path, - // const QString &name = "New Media", - // const QUuid &before_uuid = QUuid()); - - Q_INVOKABLE QObject *findMediaObject(const QUuid &uuid); - - void setExpanded(const bool value = true) { - expanded_ = value; - emit expandedChanged(); - } - void setSelected(const bool value = true, const bool recurse = true); - - QUuid createTimeline( - const QString &name = "Untitled Timeline", - const QUuid &before = QUuid(), - const bool into = false); - QUuid createSubset( - const QString &name = "Untitled Subset", - const QUuid &before = QUuid(), - const bool into = false); - QUuid createContactSheet( - const QString &name = "Untitled ContactSheet", - const QUuid &before = QUuid(), - const bool into = false); - QUuid createDivider( - const QString &name = "Untitled Divider", - const QUuid &before = QUuid(), - const bool into = false); - - QUuid convertToContactSheet( - const QUuid &uuid_or_cuuid, - const QString &name = "Converted", - const QUuid &before = QUuid(), - const bool into = false); - QUuid convertToSubset( - const QUuid &uuid_or_cuuid, - const QString &name = "Converted", - const QUuid &before = QUuid(), - const bool into = false); - QUuid - createGroup(const QString &name = "Untitled Group", const QUuid &before = QUuid()); - bool removeMedia(const QUuid &uuid); - - bool renameContainer(const QString &name, const QUuid &uuid); - bool reflagContainer(const QString &flag, const QUuid &uuid); - bool removeContainer(const QUuid &uuid, const bool remove_media = false); - bool - moveContainer(const QUuid &uuid, const QUuid &before_uuid, const bool into = false); - QUuid duplicateContainer( - const QUuid &cuuid, - const QUuid &before_cuuid = QUuid(), - const bool into = false); - void setFlag(const QString &_flag) { - if (_flag != flag()) { - flag_ = StdFromQString(_flag); - emit flagChanged(); - } - } - void setSelection(const QVariantList &cuuids, const bool clear = true); - void setSelection(QObject *obj, const bool selected = true); - void dragDropReorder(const QVariantList drop_uuids, const QString before_uuid); - void sortAlphabetically(); - - bool setJSON(const QString &json, const QString &path); - QString getJSON(const QString &path); - QFuture setJSONFuture(const QString &json, const QString &path); - QFuture getJSONFuture(const QString &path); - - QUuid getNextItemUuid(const QUuid &quuid); - - bool contains_media(const QUuid &key) const; - - private: - QObject *getNextItem(const utility::Uuid &cuuid); - - void build_media_order(); - void update_on_change(const bool select_new_items = false); - - void updateItemModel(const bool select_new_items = false); - QObject *findItem(const utility::Uuid &uuid); - utility::Uuid uuid(const QObject *obj) const; - [[nodiscard]] QUuid findCUuidFromUuid(const utility::Uuid &uuid) const; - - private: - utility::Uuid cuuid_; - utility::Uuid uuid_; - caf::actor backend_; - caf::actor backend_events_; - std::string name_; - std::string flag_; - PlaylistSelectionUI *playlist_selection_ = {nullptr}; - - std::map uuid_media_; - QList media_; - QVariantMap media_order_; - // holds map to all out children - QMap items_; - - // children mapped to model - GroupModel model_; - MediaModel media_model_; - - bool expanded_{false}; - bool selected_{false}; - bool loadingMedia_{false}; - bool isBusy_{false}; - - QObject *selected_subitem_{nullptr}; - - // dirty hack, we get spurious updates so we need to make sure something has - // really changed. - utility::PlaylistTree last_changed_tree_; - }; - } // namespace qml -} // namespace ui -} // namespace xstudio \ No newline at end of file diff --git a/retired/session_ui.cpp b/retired/session_ui.cpp deleted file mode 100644 index f0640841d..000000000 --- a/retired/session_ui.cpp +++ /dev/null @@ -1,1816 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include - -#include -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/global_store/global_store.hpp" -#include "xstudio/playlist/playlist_actor.hpp" -#include "xstudio/session/session_actor.hpp" -#include "xstudio/ui/qml/contact_sheet_ui.hpp" -#include "xstudio/ui/qml/timeline_ui.hpp" -#include "xstudio/ui/qml/playlist_ui.hpp" -#include "xstudio/ui/qml/session_ui.hpp" -#include "xstudio/ui/qml/subset_ui.hpp" -#include "xstudio/utility/container.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" -#include "xstudio/utility/uuid.hpp" - -using namespace caf; -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::ui::qml; -using namespace std::chrono_literals; -namespace fs = std::filesystem; - - -GroupModel::GroupModel(QObject *parent) : QAbstractListModel(parent) {} - -int GroupModel::rowCount(const QModelIndex &parent) const { - Q_UNUSED(parent); - return data_.count(); -} - -QVariant GroupModel::data(const QModelIndex &index, int role) const { - if (!index.isValid()) - return QVariant(); - - if (index.row() < 0 || index.row() >= data_.count()) - return QVariant(); - - if (role == ObjectRole) - return QVariant::fromValue(data_[index.row()]); - - if (auto item = dynamic_cast(data_[index.row()])) { - if (role == TypeRole) - return item->type(); - else if (role == NameRole) - return item->name(); - else if (role == ExpandedRole) - return item->expanded(); - else if (role == SelectedRole) - return item->selected(); - } else if (auto item = dynamic_cast(data_[index.row()])) { - if (role == TypeRole) - return item->type(); - else if (role == NameRole) - return item->name(); - else if (role == ExpandedRole) - return item->expanded(); - else if (role == SelectedRole) - return item->selected(); - } else if (auto item = dynamic_cast(data_[index.row()])) { - if (role == TypeRole) - return item->type(); - else if (role == NameRole) - return item->name(); - else if (role == SelectedRole) - return item->selected(); - } else if (auto item = dynamic_cast(data_[index.row()])) { - if (role == TypeRole) - return item->type(); - else if (role == NameRole) - return item->name(); - else if (role == SelectedRole) - return item->selected(); - } else if (auto item = dynamic_cast(data_[index.row()])) { - if (role == TypeRole) - return item->type(); - else if (role == NameRole) - return item->name(); - else if (role == SelectedRole) - return item->selected(); - } else if (auto item = dynamic_cast(data_[index.row()])) { - if (role == TypeRole) - return item->type(); - else if (role == NameRole) - return item->name(); - else if (role == SelectedRole) - return item->selected(); - } - - return QVariant(); -} - -QVariant GroupModel::get_object(const int sourceRow) { - if (sourceRow < 0 || sourceRow >= data_.count()) - return QVariant(); - - return QVariant::fromValue(data_[sourceRow]); -} - -QVariant GroupModel::next_object(const QVariant obj) { - for (auto i = 0; i < size(); i++) { - if (get_object(i).value() == obj.value()) - return get_object(i + 1); - } - return QVariant(); -} - -bool GroupModel::empty() const { return data_.count() == 0; } - -int GroupModel::size() const { return data_.count(); } - - -QHash GroupModel::roleNames() const { - QHash roles; - roles[TypeRole] = "type"; - roles[NameRole] = "name"; - roles[ObjectRole] = "object"; - roles[ExpandedRole] = "expanded"; - roles[SelectedRole] = "selected"; - return roles; -} - -// bool GroupModel::moveRows( -// const QModelIndex &, -// int sourceRow, -// int count, -// const QModelIndex &, -// int destinationChild) { -// spdlog::warn("{} {} {}", sourceRow, count, destinationChild); - -// beginMoveRows(QModelIndex(), sourceRow, sourceRow+(count-1), QModelIndex(), -// destinationChild); -// // / wrong.. -// for(auto i=0; i = data_.size() || dst > data_.size()) - return false; - - QUuid cdst = QUuid(); - - if (auto session = dynamic_cast(controller_)) { - if (dst < data_.size()) - cdst = QUuidFromUuid(session->cuuid(data_[dst])); - - return session->moveContainer(QUuidFromUuid(session->cuuid(data_[src])), cdst); - } else if (auto playlist = dynamic_cast(controller_)) { - if (dst < data_.size()) - cdst = QUuidFromUuid(playlist->cuuid(data_[dst])); - - return playlist->moveContainer(QUuidFromUuid(playlist->cuuid(data_[src])), cdst); - } - - return false; -} - -bool GroupModel::populate(QList &items, QObject *controller) { - // build change list - bool different = true; - bool changed = false; - if (controller) - controller_ = controller; - - // move/delete/insert.. - - // delete - // items only appear once in list.. - while (different) { - different = false; - for (auto i = 0; i < data_.size(); i++) { - if (not items.contains(data_[i])) { - beginRemoveRows(QModelIndex(), i, i); - data_.removeAt(i); - endRemoveRows(); - different = true; - changed = true; - break; - } - } - } - - // move // insert. - different = true; - while (different) { - different = false; - for (auto i = 0; i < items.size(); i++) { - // must be new item.. - if (data_.size() <= i) { - changed = true; - different = true; - beginInsertRows(QModelIndex(), i, i); - data_.push_back(items[i]); - endInsertRows(); - } else { - // check for difference. - if (items[i] != data_[i]) { - changed = true; - different = true; - // could be move ? - if (data_.contains(items[i])) { - // it's a move.. - auto src = data_.indexOf(items[i]); - beginMoveRows(QModelIndex(), src, src, QModelIndex(), i); - data_.move(src, i); - endMoveRows(); - } else { - beginInsertRows(QModelIndex(), i, i); - data_.insert(i, items[i]); - endInsertRows(); - } - } - } - } - } - return changed; -} - -ContainerGroupUI::ContainerGroupUI(QObject *parent) : QObject(parent) {} - -ContainerGroupUI::ContainerGroupUI( - const utility::Uuid cuuid, - const std::string name, - const std::string type, - const std::string flag, - const utility::Uuid uuid, - QObject *parent) - : QObject(parent), - cuuid_(std::move(cuuid)), - name_(std::move(name)), - type_(std::move(type)), - flag_(std::move(flag)), - uuid_(std::move(uuid)) {} - -ContainerDividerUI::ContainerDividerUI(QObject *parent) : QObject(parent) {} - -ContainerDividerUI::ContainerDividerUI( - const utility::Uuid cuuid, - const std::string name, - const std::string type, - const std::string flag, - const utility::Uuid uuid, - QObject *parent) - : QObject(parent), - cuuid_(std::move(cuuid)), - name_(std::move(name)), - type_(std::move(type)), - flag_(std::move(flag)), - uuid_(std::move(uuid)) {} - -QObject *ContainerGroupUI::findItem(const utility::Uuid &uuid) const { - QObject *found = nullptr; - - for (auto i = 0; i < items_.count() && not found; i++) { - if (auto obj = dynamic_cast(items_[i])) { - if (obj->uuid() == uuid or obj->cuuid() == uuid) - found = items_[i]; - } else if (auto obj = dynamic_cast(items_[i])) { - if (obj->uuid() == uuid or obj->cuuid() == uuid) - found = items_[i]; - } - } - return found; -} - -SessionUI::SessionUI(QObject *parent) - : QMLActor(parent), - backend_(), - name_("unknown") - -{ - init(CafSystemObject::get_actor_system()); -} - -SessionUI::~SessionUI() { - if (dummy_playlist_) { - anon_send_exit(dummy_playlist_, caf::exit_reason::user_shutdown); - dummy_playlist_ = caf::actor(); - } -} - -// helper ? - -void SessionUI::set_backend(caf::actor backend) { - backend_ = backend; - playlist_ = nullptr; - selected_source_ = nullptr; - emit playlistChanged(); - // get backend state.. - scoped_actor sys{system()}; - - if (backend_events_) { - try { - request_receive( - *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - backend_events_ = caf::actor(); - } - - - try { - backend_events_ = request_receive(*sys, backend_, get_event_group_atom_v); - request_receive( - *sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - try { - auto nm = request_receive(*sys, backend_, utility::name_atom_v); - name_ = QStringFromStd(nm); - media_rate_ = request_receive(*sys, backend_, session::media_rate_atom_v); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - try { - auto pt = request_receive>( - *sys, backend_, session::path_atom_v); - path_ = pt.first; - session_file_mtime_ = pt.second; - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - if (backend_) { - session_actor_addr_ = actorToQString(QMLActor::system(), backend_); - emit sessionActorAddrChanged(); - } - - emit mediaRateChanged(media_rate_); - emit nameChanged(); - emit pathChanged(); - updateItemModel(false, true); - emit itemModelChanged(); - - if (items_.empty() && !dummy_playlist_) { - - // Because the toolbar is built dynamically based on attributes belonging - // to the playhead, viewport and colour pipeline, we need a playhead - // to exist so that the toolbar fills out even when there is no playlist - scoped_actor sys{system()}; - dummy_playlist_ = sys->spawn( - std::string("Dummy2"), utility::Uuid(), backend_); - auto playhead = request_receive_wait( - *sys, dummy_playlist_, infinite, playlist::create_playhead_atom_v); - - anon_send(playhead.actor(), module::connect_to_ui_atom_v); - } - emit backendChanged(); -} - -void SessionUI::initSystem(QObject *system_qobject) { - init(dynamic_cast(system_qobject)->system()); -} - -void SessionUI::init(actor_system &system_) { - QMLActor::init(system_); - - // It's alive! - - spdlog::debug("SessionUI init"); - - { - scoped_actor sys{system()}; - try { - auto grp = request_receive( - *sys, - system().registry().template get(studio_registry), - utility::get_event_group_atom_v); - request_receive(*sys, grp, broadcast::join_broadcast_atom_v, as_actor()); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - // make sure the audio output is connected to the UI (i.e. it is broacasting and receiveing - // events to do with its Module attributes) - auto audio_output_actor = - system().registry().template get(audio_output_registry); - if (audio_output_actor) - anon_send(audio_output_actor, module::connect_to_ui_atom_v); - - - { - scoped_actor sys{system()}; - auto prefs = global_store::GlobalStoreHelper(system()); - utility::JsonStore js; - request_receive( - *sys, prefs.get_group(js), broadcast::join_broadcast_atom_v, as_actor()); - } - - - set_message_handler([=](actor_companion * /*self_*/) -> message_handler { - return { - - [=](json_store::update_atom, - const utility::JsonStore & /*change*/, - const std::string & /*path*/, - const utility::JsonStore &full) {}, - - [=](json_store::update_atom, const utility::JsonStore &js) {}, - - [=](utility::event_atom, utility::name_atom, const std::string &name) { - if (name_ != QString::fromStdString(name)) { - name_ = QString::fromStdString(name); - emit nameChanged(); - } - }, - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg &) {}, - - [=](utility::event_atom, playlist::add_media_atom, const UuidActor &ua) { - // not sure we recieve these.. as we listen to session events not playlist - // ones. - emit mediaAdded(QUuidFromUuid(ua.uuid())); - }, - - [=](utility::event_atom, playlist::create_divider_atom, const Uuid & /*uuid*/) { - updateItemModel(); - emit itemModelChanged(); - // emit newItem(QVariant::fromValue(QUuidFromUuid(uuid))); - }, - - [=](utility::event_atom, playlist::create_group_atom, const Uuid & /*uuid*/) { - updateItemModel(); - emit itemModelChanged(); - // emit newItem(QVariant::fromValue(QUuidFromUuid(uuid))); - }, - - [=](utility::event_atom, - session::path_atom, - const std::pair &pt) { - path_ = pt.first; - session_file_mtime_ = pt.second; - emit pathChanged(); - emit sessionFileMTimechanged(); - }, - - [=](utility::event_atom, playlist::loading_media_atom, const bool) {}, - - [=](utility::event_atom, playlist::loading_media_atom, const bool) {}, - - [=](utility::event_atom, session::current_playlist_atom, caf::actor playlist) { - if (!playlist_ || playlist_->backend() != playlist) { - - if (playlist) { - - scoped_actor sys{system()}; - auto uuid = request_receive_wait( - *sys, playlist, std::chrono::seconds(1), utility::uuid_atom_v); - switchOnScreenSource(QUuidFromUuid(uuid), false); - - } else { - switchOnScreenSource(); - } - - updateItemModel(false, true); - } - }, - - [=](utility::event_atom, - playlist::move_container_atom, - const Uuid & /*src*/, - const Uuid & /*before*/) { - updateItemModel(); - emit itemModelChanged(); - }, - - [=](utility::event_atom, - playlist::reflag_container_atom, - const Uuid & /*uuid*/, - const std::string &) { - updateItemModel(); - emit itemModelChanged(); - }, - - [=](utility::event_atom, - playlist::remove_container_atom, - const std::vector &) {}, - - [=](utility::event_atom, playlist::remove_container_atom, const Uuid & /*uuid*/) { - updateItemModel(); - emit itemModelChanged(); - }, - - [=](utility::event_atom, playlist::remove_container_atom, const Uuid &uuid) { - emit playlistRemoved(QVariant::fromValue(QUuidFromUuid(uuid))); - updateItemModel(); - emit itemModelChanged(); - }, - - [=](utility::event_atom, - playlist::rename_container_atom, - const Uuid & /*uuid*/, - const std::string &) { - updateItemModel(); - emit itemModelChanged(); - }, - - [=](utility::event_atom, session::add_playlist_atom, const UuidActor & /*ua*/) { - updateItemModel(); - emit itemModelChanged(); - // ua.first must exist in our tree.. - // or not - // auto plu = dynamic_cast(findItem(ua.first)); - // if (plu) { - // emit newItem(QVariant::fromValue(plu->qcuuid())); - // } - }, - - [=](utility::event_atom, session::media_rate_atom, const FrameRate &rate) { - if (rate != media_rate_) { - media_rate_ = rate; - emit mediaRateChanged(media_rate_.to_fps()); - } - }, - - [=](utility::event_atom, session::session_atom, caf::actor session) { - if (backend_ != session) - set_backend(session); - }, - - [=](utility::event_atom, - session::session_request_atom, - const std::string path, - const JsonStore &js) { - emit sessionRequest( - QUrlFromUri(posix_path_to_uri(path)), QByteArray(js.dump().c_str())); - - // scoped_actor sys{system()}; - // auto session = sys->spawn(js); - // auto global = system().registry().template get(global_registry); - - // sys->anon_send( - // global, - // session::session_atom_v, - // session - // ); - - // setModified(false, true); - // setPath(QStringFromStd(path)); - }, - - [=](utility::event_atom, utility::change_atom) { - // something changed in the playhead... - // use this for media changes, which impact timeline - // update_on_change(); - }, - - [=](utility::event_atom, - utility::last_changed_atom, - const time_point &last_changed) { - last_changed_ = last_changed; - emit modifiedChanged(); - }}; - }); - - - // is this ever reached ? - try { - scoped_actor sys{system()}; - // Get the session ... - auto global = system().registry().template get(global_registry); - - auto session = request_receive_wait( - *sys, global, std::chrono::seconds(1), session::session_atom_v); - if (backend_ != session) - set_backend(session); - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } -} - -QList SessionUI::getPlaylists() { - QList result; - - for (auto i : items_) { - if (dynamic_cast(i)) - result.append(i); - } - - return result; -} - - -void SessionUI::setSelection(const QVariantList &cuuids, const bool clear) { - - std::set uuids; - - for (auto &i : cuuids) { - uuids.insert(i.toUuid()); - } - QMap::iterator i = items_.begin(); - - auto last_selected = selected_source_; - selected_source_ = nullptr; - - while (i != items_.end()) { - if (uuids.count(i.key())) { - setSelection(i.value(), true, clear); - } else if (clear) { - setSelection(i.value(), false, clear); - } - if (auto playlist = dynamic_cast(i.value())) { - for (auto uuid : uuids) { - QUuid unused; - if (playlist->quuid() == uuid) { - - setSelection(i.value(), true, clear); - bool oldState = playlist->blockSignals(true); - selected_source_ = playlist; - playlist->setSelection(QVariantList()); - playlist->blockSignals(oldState); - - } else if (playlist->contains(uuid, unused)) { - bool oldState = playlist->blockSignals(true); - playlist->setSelection(QVariantList({unused})); - playlist->blockSignals(oldState); - selected_source_ = playlist->selectedSubitem(); - } - } - } - ++i; - } - - if (last_selected != selected_source_) { - emit selectedSourceChanged(); - } -} - - -bool SessionUI::isSelected(QObject *obj) const { - - if (auto item = dynamic_cast(obj)) { - return item->selected(); - } else if (auto item = dynamic_cast(obj)) { - return item->selected(); - } else if (auto item = dynamic_cast(obj)) { - return item->selected(); - } else if (auto item = dynamic_cast(obj)) { - return item->selected(); - } else if (auto item = dynamic_cast(obj)) { - return item->selected(); - } else if (auto item = dynamic_cast(obj)) { - return item->selected(); - } - - return false; -} - -void SessionUI::setSelection(QObject *obj, const bool selected, const bool clear) { - - if (auto item = dynamic_cast(obj)) { - item->setSelected(selected); - if (clear) { - // stop signal loop - bool oldState = item->blockSignals(true); - item->setSelection(QVariantList()); - item->blockSignals(oldState); - } - if (selected) { - selected_source_ = item; - } - } else if (auto item = dynamic_cast(obj)) { - item->setSelected(selected); - } else if (auto item = dynamic_cast(obj)) { - item->setSelected(selected); - } else if (auto item = dynamic_cast(obj)) { - item->setSelected(selected); - } else if (auto item = dynamic_cast(obj)) { - item->setSelected(selected); - } else if (auto item = dynamic_cast(obj)) { - item->setSelected(selected); - } -} - -QObject *SessionUI::getPlaylist(const QUuid &uuid) { - QObject *rt = nullptr; - for (auto i : items_) { - if (auto item = dynamic_cast(i)) { - - if (item->quuid() == uuid || item->qcuuid() == uuid) { - - rt = static_cast(item); - break; - } - } - } - return rt; -} - -void SessionUI::setMediaRate(const double rate) { - if ((1.0 / rate) != media_rate_.to_fps()) - anon_send(backend_, session::media_rate_atom_v, FrameRate(1.0 / rate)); -} - - -// find item based off container uuid or actor uuid -QObject *SessionUI::findItem(const utility::Uuid &cuuid) const { - QUuid qu = QUuidFromUuid(cuuid); - - auto f = items_.find(qu); - if (f != items_.end()) - return *f; - - for (auto &i : items_) { - if (uuid(i) == cuuid) { - return i; - } else if (auto item = dynamic_cast(i)) { - QObject *found = item->findItem(cuuid); - if (found) - return found; - } - } - return nullptr; -} - -// find item based off container uuid or actor uuid -QObject *SessionUI::getNextItem(const utility::Uuid &cuuid) { - scoped_actor sys{system()}; - auto rec_items_container = - request_receive(*sys, backend_, playlist::get_container_atom_v); - auto next_item = rec_items_container.find_next_at_same_depth(cuuid); - if (next_item) - return findItem(*next_item); - - return nullptr; -} - -utility::Uuid SessionUI::uuid(const QObject *obj) const { - if (auto item = dynamic_cast(obj)) { - return item->uuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->uuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->uuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->uuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->uuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->uuid(); - } - - return utility::Uuid(); -} - -utility::Uuid SessionUI::cuuid(const QObject *obj) const { - if (auto item = dynamic_cast(obj)) { - return item->cuuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->cuuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->cuuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->cuuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->cuuid(); - } else if (auto item = dynamic_cast(obj)) { - return item->cuuid(); - } - - return utility::Uuid(); -} - -bool SessionUI::moveContainer(const QUuid &uuid, const QUuid &before_uuid, const bool into) { - bool result = false; - try { - scoped_actor sys{system()}; - auto new_item = request_receive( - *sys, - backend_, - playlist::move_container_atom_v, - UuidFromQUuid(uuid), - UuidFromQUuid(before_uuid), - into); - result = new_item; - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - -bool SessionUI::renameContainer(const QString &name, const QUuid &uuid) { - bool result = false; - try { - scoped_actor sys{system()}; - auto new_item = request_receive( - *sys, - backend_, - playlist::rename_container_atom_v, - StdFromQString(name), - UuidFromQUuid(uuid)); - result = new_item; - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - -bool SessionUI::reflagContainer(const QString &flag, const QUuid &uuid) { - bool result = false; - try { - scoped_actor sys{system()}; - auto new_item = request_receive( - *sys, - backend_, - playlist::reflag_container_atom_v, - StdFromQString(flag), - UuidFromQUuid(uuid)); - result = new_item; - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - -QUuid SessionUI::createDivider(const QString &name, const QUuid before, const bool into) { - QUuid result; - try { - scoped_actor sys{system()}; - auto new_playlist = request_receive( - *sys, - backend_, - playlist::create_divider_atom_v, - StdFromQString(name), - UuidFromQUuid(before), - into); - result = QUuidFromUuid(new_playlist); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - - -QUuid SessionUI::createPlaylist(const QString &name, const QUuid before, const bool into) { - QUuid result; - try { - scoped_actor sys{system()}; - auto new_playlist = request_receive( - *sys, - backend_, - session::add_playlist_atom_v, - StdFromQString(name), - UuidFromQUuid(before), - into); - result = QUuidFromUuid(new_playlist.second.uuid()); - updateItemModel(); - emit itemModelChanged(); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - -QUuid SessionUI::createGroup(const QString &name, const QUuid before) { - QUuid result; - try { - scoped_actor sys{system()}; - auto new_playlist = request_receive( - *sys, - backend_, - playlist::create_group_atom_v, - StdFromQString(name), - UuidFromQUuid(before)); - result = QUuidFromUuid(new_playlist); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - -QUuid SessionUI::getNextItemUuid(const QUuid &quuid) { - QUuid result; - auto obj = getNextItem(UuidFromQUuid(quuid)); - if (obj) - result = QUuidFromUuid(uuid(obj)); - - return result; -} - -bool SessionUI::removeContainer(const QUuid &uuid) { - - // are we trying to remove a playlist that is currently selected and (possibly) - // the on screen source? - if (selected_source_ && cuuid(selected_source_) == UuidFromQUuid(uuid)) { - - bool onscreen_is_selected = on_screen_source_ == selected_source_; - // we are about to remove the selected item - therefore we need to try and select - // the next object - QObject *next = getNextItem(UuidFromQUuid(uuid)); - if (next) { - setSelection(next); - emit selectedSourceChanged(); - if (onscreen_is_selected) { - switchOnScreenSource(QUuidFromUuid(cuuid(selected_source_))); - } - } - } - - bool result = false; - try { - scoped_actor sys{system()}; - auto new_item = request_receive( - *sys, backend_, playlist::remove_container_atom_v, UuidFromQUuid(uuid)); - result = new_item; - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - - -QUuid SessionUI::duplicateContainer( - const QUuid &cuuid, const QUuid &before_cuuid, const bool into) { - QUuid result; - try { - scoped_actor sys{system()}; - auto new_playlist = request_receive( - *sys, - backend_, - playlist::duplicate_container_atom_v, - UuidFromQUuid(cuuid), - UuidFromQUuid(before_cuuid), - into); - result = QUuidFromUuid(new_playlist[0]); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - -void SessionUI::updateItemModel(const bool select_new_items, const bool reset) { - // spdlog::stopwatch sw; - try { - scoped_actor sys{system()}; - std::map uuid_actor; - std::map hold; - items_list_.clear(); - - auto rec_items_container = request_receive( - *sys, backend_, playlist::get_container_atom_v); - - if (not reset && last_changed_tree_ == rec_items_container) - return; - - if (reset) { - items_.clear(); - items_list_.clear(); - on_screen_source_ = nullptr; - } - - last_changed_tree_ = rec_items_container; - - // build look up for actors - // based on object UUID - for (const auto &i : request_receive>( - *sys, backend_, session::get_playlists_atom_v)) { - uuid_actor[i.uuid()] = i.actor(); - } - - // decompose.. - // all QT objects now in hold. - // we store base on object uuid NOT container UUID - // we also clear dependencies. - - while (!items_.empty()) { - if (auto item = dynamic_cast(items_.begin().value())) { - hold[item->uuid()] = items_.begin().value(); - } else if (auto item = dynamic_cast(items_.begin().value())) { - hold[item->uuid()] = items_.begin().value(); - } else if (auto item = dynamic_cast(items_.begin().value())) { - hold[item->uuid()] = items_.begin().value(); - while (not item->items_.empty()) { - if (auto iitem = dynamic_cast(item->items_[0])) { - hold[iitem->uuid()] = item->items_[0]; - } else if (auto iitem = dynamic_cast(item->items_[0])) { - hold[iitem->uuid()] = item->items_[0]; - } - item->items_.removeAt(0); - } - } - items_.erase(items_.begin()); - } - - bool new_objects = false; - - bool last_selected_still_valid = false; - - // Our QList is a collection of (playlists and playlist groups) - // we use uuids as the index, as names may change.. - // playlist groups are only at the top level. - for (auto &i : rec_items_container.children_ref()) { - // is item in hold? - auto qcu = QUuidFromUuid(i.uuid()); - auto uuid = i.value().uuid(); - auto type = i.value().type(); - - if (hold.count(uuid)) { - if (type == "ContainerGroup") { - auto obj = dynamic_cast(hold[uuid]); - obj->setName(QStringFromStd(i.value().name())); - obj->setFlag(QStringFromStd(i.value().flag())); - } else if (type == "ContainerDivider") { - auto obj = dynamic_cast(hold[uuid]); - obj->setName(QStringFromStd(i.value().name())); - obj->setFlag(QStringFromStd(i.value().flag())); - } else if (type == "Playlist") { - auto obj = dynamic_cast(hold[uuid]); - obj->setFlag(QStringFromStd(i.value().flag())); - if (obj == selected_source_) { - last_selected_still_valid = true; - } - } - items_list_.append(hold[uuid]); - items_[qcu] = hold[uuid]; - - } else { - // new item - if (type == "ContainerGroup") { - new_objects = true; - auto obj = new ContainerGroupUI( - i.uuid(), i.value().name(), type, i.value().flag(), uuid, this); - if (select_new_items) - obj->setSelected(); - items_[qcu] = obj; - items_list_.append(obj); - emit newItem(QVariant::fromValue(QUuidFromUuid(uuid))); - - } else if (type == "ContainerDivider") { - auto obj = new ContainerDividerUI( - i.uuid(), i.value().name(), type, i.value().flag(), uuid, this); - if (select_new_items and not new_objects) - obj->setSelected(); - new_objects = true; - items_[qcu] = obj; - items_list_.append(obj); - emit newItem(QVariant::fromValue(QUuidFromUuid(uuid))); - } else if (type == "Playlist") { - auto obj = new PlaylistUI(i.uuid(), uuid, this); - QObject::connect( - obj, - SIGNAL(newSelection(const QVariantList, const bool)), - this, - SLOT(setSelection(const QVariantList &, const bool))); - QObject::connect( - obj, SIGNAL(nameChanged()), this, SLOT(rebuildPlaylistNamesList())); - if (select_new_items and not new_objects) - obj->setSelected(); - new_objects = true; - items_[qcu] = obj; - items_list_.append(obj); - obj->init(system()); - // quick set backend - obj->set_backend(uuid_actor[uuid], true); - - // we need this to complete.. - anon_send(obj->as_actor(), backend_atom_v, uuid_actor[uuid]); - - obj->setFlag(QStringFromStd(i.value().flag())); - emit newItem(QVariant::fromValue(QUuidFromUuid(uuid))); - } - } - - if (type == "ContainerGroup") { - auto new_group = dynamic_cast(items_[qcu]); - // as it's a groups we need to scan it.. - for (const auto &ii : i.children_ref()) { - auto chtype = ii.value().type(); - auto chuuid = ii.value().uuid(); - - if (hold.count(chuuid)) { - if (chtype == "ContainerGroup") { - auto obj = dynamic_cast(hold[chuuid]); - obj->setName(QStringFromStd(ii.value().name())); - obj->setFlag(QStringFromStd(ii.value().flag())); - } else if (chtype == "ContainerDivider") { - auto obj = dynamic_cast(hold[chuuid]); - obj->setName(QStringFromStd(ii.value().name())); - obj->setFlag(QStringFromStd(ii.value().flag())); - } else if (chtype == "Playlist") { - auto obj = dynamic_cast(hold[chuuid]); - obj->setFlag(QStringFromStd(ii.value().flag())); - } - new_group->items_.append(hold[chuuid]); - } else if (chtype == "ContainerDivider") { - auto obj = new ContainerDividerUI( - ii.uuid(), - ii.value().name(), - ii.value().type(), - ii.value().flag(), - ii.value().uuid(), - this); - if (select_new_items and not new_objects) - obj->setSelected(); - new_objects = true; - new_group->items_.append(obj); - emit newItem(QVariant::fromValue(QUuidFromUuid(ii.value().uuid()))); - } else if (chtype == "Playlist") { - // new playlist - auto obj = new PlaylistUI(ii.uuid(), chuuid, this); - QObject::connect( - obj, SIGNAL(nameChanged()), this, SLOT(rebuildPlaylistNamesList())); - if ((select_new_items or dummy_playlist_) and not new_objects) - obj->setSelected(); - new_objects = true; - new_group->items_.append(obj); - - obj->init(system()); - anon_send(obj->as_actor(), backend_atom_v, uuid_actor[chuuid]); - - obj->setFlag(QStringFromStd(ii.value().flag())); - emit newItem(QVariant::fromValue(QUuidFromUuid(ii.value().uuid()))); - } - } - new_group->changedItems(); - } - } - - if (!last_selected_still_valid) { - selected_source_ = nullptr; - emit selectedSourceChanged(); - } - - // deselect old objects. - if (new_objects and select_new_items) { - for (auto i : hold) { - if (auto item = dynamic_cast(i.second)) { - item->setSelected(false); - } else if (auto item = dynamic_cast(i.second)) { - item->setSelected(false); - } else if (auto item = dynamic_cast(i.second)) { - item->setSelected(false); - } else if (auto item = dynamic_cast(i.second)) { - item->setSelected(false); - } else if (auto item = dynamic_cast(i.second)) { - item->setSelected(false); - } else if (auto item = dynamic_cast(i.second)) { - item->setSelected(false); - } - } - } - - if (model_.populate(items_list_, this)) - emit itemModelChanged(); - - if ((on_screen_source_ && !items_list_.contains(on_screen_source_)) || - !on_screen_source_) { - // the current selected source is not in our list (it may have been removed) or - // there is no on screen source, so try and select - switchOnScreenSource(); - } - - rebuildPlaylistNamesList(); - - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - // spdlog::error("session update {:.3}", sw); -} - -bool SessionUI::loadUris(const std::vector &uris, const bool single_playlist) { - - scoped_actor sys{system()}; - // check for session files ? - // scan for single session file.. as we use difference behaviour. - if (uris.size() == 1 and ends_with(to_lower(uri_to_posix_path(uris[0])), ".xst")) { - try { - auto path = uri_to_posix_path(uris[0]); - auto global = system().registry().template get(global_registry); - JsonStore js; - std::ifstream i(path); - i >> js; - - if (not model_.empty()) { - return request_receive( - *sys, global, session::session_request_atom_v, path, js); - } else { - auto session = sys->spawn(js, uris[0]); - sys->anon_send(global, session::session_atom_v, session); - set_backend(session); - setModified(false, true); - setPath(QUrlFromUri(uris[0])); - return true; - } - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - return false; - } - } else - sys->anon_send(backend_, session::load_uris_atom_v, uris, single_playlist); - - return true; -} - -bool SessionUI::loadUrl(const QUrl &url, const bool single_playlist) { - // bool result=true; - std::vector paths; - paths.emplace_back(UriFromQUrl(url)); - return loadUris(paths, single_playlist); -} - -bool SessionUI::loadUrls(const QList &urls, const bool single_playlist) { - // bool result=true; - - std::vector paths; - - for (const auto &i : urls) { - paths.emplace_back(UriFromQUrl(i)); - } - - return loadUris(paths, single_playlist); -} - -void SessionUI::rebuildPlaylistNamesList() { - - scoped_actor sys{system()}; - - auto rec_items_container = - request_receive(*sys, backend_, playlist::get_container_atom_v); - - QVariantList rt; - - for (auto &i : rec_items_container.children_ref()) { - - auto qcu = QUuidFromUuid(i.uuid()); - auto type = i.value().type(); - - if (items_.count(qcu) && type == "Playlist") { - auto item = dynamic_cast(items_[qcu]); - if (item) { - QMap m; - m["text"] = QVariant(item->name()); - m["uuid"] = QVariant(item->quuid()); - rt.append(m); - } - } - } - - playlistNames_ = QVariant(rt); - emit(playlistNamesChanged()); -} - -void SessionUI::setName(const QString &name) { - if (name != name_) { - if (backend_) { - scoped_actor sys{system()}; - sys->anon_send(backend_, utility::name_atom_v, name.toStdString()); - } - } -} - -QString SessionUI::save() { - if (not path().path().isEmpty()) { - return save(path()); - } - return QString("Session has not been saved yet, use SaveAs instead."); -} - -void SessionUI::setSessionActorAddr(const QString &addr) { - auto actor = actorFromQString(system(), addr); - if (actor) { - set_backend(actor); - setModified(false); - setPath(QUrl()); - switchOnScreenSource(QUuid()); - setViewerPlayhead(); - } -} - - -void SessionUI::newSession(const QString &name) { - - scoped_actor sys{system()}; - - auto session = sys->spawn(StdFromQString(name)); - set_backend(session); - - auto global = system().registry().template get(global_registry); - sys->anon_send(global, session::session_atom_v, session); - - setModified(false); - setPath(QUrl()); - switchOnScreenSource(QUuid()); - setViewerPlayhead(); -} - -QString SessionUI::save(const QUrl &path) { - QString result; - // maybe done by backend? - try { - scoped_actor sys{system()}; - // Get the session ... - if (request_receive_wait( - *sys, - backend_, - std::chrono::seconds(60), - global_store::save_atom_v, - UriFromQUrl(path))) { - setModified(false); - setPath(path); - } - } catch (std::exception &err) { - std::string error = "Session save to " + StdFromQString(path.path()) + " failed!\n"; - - if (std::string(err.what()) == "caf::sec::request_timeout") { - error += "Backend did not return session data within timeout."; - } else { - auto errstr = std::string(err.what()); - errstr = std::regex_replace( - errstr, std::regex(R"foo(.*caf::sec::runtime_error\("([^"]+)"\).*)foo"), "$1"); - error += errstr; - } - - spdlog::warn("{} {}", __PRETTY_FUNCTION__, error); - result = QStringFromStd(error); - } - - return result; -} - -QString SessionUI::save_selected(const QUrl &path) { - QString result; - - try { - scoped_actor sys{system()}; - // Get the session ... - // build selected item list. - std::vector selected; - for (const auto &i : items_) { - if (isSelected(i)) - selected.push_back(cuuid(i)); - } - - request_receive_wait( - *sys, - backend_, - std::chrono::seconds(60), - global_store::save_atom_v, - UriFromQUrl(path), - selected); - - } catch (std::exception &err) { - std::string error = - "Selected session save to " + StdFromQString(path.path()) + " failed!\n"; - - if (std::string(err.what()) == "caf::sec::request_timeout") { - error += "Backend did not return session data within timeout."; - } else { - auto errstr = std::string(err.what()); - errstr = std::regex_replace( - errstr, std::regex(R"foo(.*caf::sec::runtime_error\("([^"]+)"\).*)foo"), "$1"); - error += errstr; - } - - spdlog::warn("{} {}", __PRETTY_FUNCTION__, error); - result = QStringFromStd(error); - } - - return result; -} - -void SessionUI::setPath(const QUrl &path, const bool inform_backend) { - auto newpath = UriFromQUrl(path); - if (newpath != path_) { - path_ = newpath; - if (backend_ and inform_backend) { - scoped_actor sys{system()}; - sys->anon_send(backend_, session::path_atom_v, path_); - } - - emit pathChanged(); - } -} - -bool SessionUI::load(const QUrl &path, const QVariant &json) { - bool result = false; - try { - scoped_actor sys{system()}; - - JsonStore js(qvariant_to_json(json)); - auto session = sys->spawn(js, UriFromQUrl(path)); - - auto global = system().registry().template get(global_registry); - sys->anon_send(global, session::session_atom_v, session); - - set_backend(session); - - - setModified(false, true); - setPath(path, false); - result = true; - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - return result; -} - -bool SessionUI::import(const QUrl &path, const QVariant &json) { - - bool result = false; - JsonStore js; - scoped_actor sys{system()}; - if (json.isNull()) { - try { - std::ifstream i(StdFromQString(path.path())); - i >> js; - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - return false; - } - } else { - try { - js = JsonStore(qvariant_to_json(json)); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - return false; - } - } - - try { - auto session = sys->spawn(js, UriFromQUrl(path)); - sys->anon_send(backend_, session::merge_session_atom_v, session); - - result = true; - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return result; -} - -bool SessionUI::load(const QUrl &path) { - - scoped_actor sys{system()}; - - bool result = false; - - try { - JsonStore js; - std::ifstream i(StdFromQString(path.path())); - i >> js; - - auto session = sys->spawn(js, UriFromQUrl(path)); - - // destroy old session and point to new session. - auto global = system().registry().template get(global_registry); - sys->anon_send(global, session::session_atom_v, session); - - set_backend(session); - - setModified(false, true); - setPath(path); - result = true; - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - return result; -} - -QObject *SessionUI::playlist() { return static_cast(playlist_); } - -QObject *SessionUI::selectedSource() { return selected_source_; } - - -void SessionUI::switchOnScreenSource(const QUuid &uuid, const bool broadcast) { - - // This can be called at start-up before a playlist uuid is set-up - spdlog::debug("switchOnScreenSource {}", to_string(UuidFromQUuid(uuid))); - // to_string(UuidFromQUuid(suuid))); - QObject *prev_source = on_screen_source_; - on_screen_source_ = nullptr; - PlaylistUI *prev_playlist = playlist_; - playlist_ = nullptr; - // null and no current playlist - pick the first playlist that we have - if (uuid.isNull()) { - if (not playlist_ and not items_.empty()) { - for (auto i : items_) { - if (auto item = dynamic_cast(i)) { - playlist_ = item; - anon_send( - backend_, - session::current_playlist_atom_v, - playlist_->backend(), - broadcast); - on_screen_source_ = playlist_; - emit playlistChanged(); - setSelection(QVariantList({QVariant(item->qcuuid())}), true); - break; - } - } - } else { - emit playlistChanged(); - emit onScreenSourceChanged(); - } - } else { - - QUuid cuuid; - for (auto i : items_) { - if (auto item = dynamic_cast(i)) { - if (item->quuid() == uuid || item->qcuuid() == uuid) { - playlist_ = item; - anon_send( - backend_, - session::current_playlist_atom_v, - playlist_->backend(), - broadcast); - on_screen_source_ = playlist_; - emit playlistChanged(); - break; - } else if (item->contains(uuid, cuuid)) { - playlist_ = item; - emit playlistChanged(); - playlist_->setSelection(QVariantList({QVariant(cuuid)}), true); - on_screen_source_ = playlist_->selectedSubitem(); - } - } - } - } - - if (on_screen_source_ != prev_source) { - emit onScreenSourceChanged(); - setViewerPlayhead(); - } - - if (playlist_ && playlist_ != prev_playlist) { - - // when user has changed playlist, we want to open readers for the media - // items ready for the user to jump around the playlist - if (playlist_->backend()) - anon_send(playlist_->backend(), playlist::set_playlist_in_viewer_atom_v, true); - if (prev_playlist && prev_playlist->backend()) - anon_send(prev_playlist->backend(), playlist::set_playlist_in_viewer_atom_v, false); - } else if (playlist_ != prev_playlist) { - emit playlistChanged(); - } -} - -void SessionUI::setViewerPlayhead() { - - - caf::actor on_screen_item_backend; - if (auto subset = dynamic_cast(on_screen_source_)) { - on_screen_item_backend = subset->backend(); - } else if (auto contact_sheet = dynamic_cast(on_screen_source_)) { - on_screen_item_backend = contact_sheet->backend(); - } else if (auto timeline = dynamic_cast(on_screen_source_)) { - on_screen_item_backend = timeline->backend(); - } else if (auto playlist = dynamic_cast(on_screen_source_)) { - on_screen_item_backend = playlist->backend(); - } - auto ph_events = system().registry().template get(global_playhead_events_actor); - if (!on_screen_item_backend) { - on_screen_item_backend = dummy_playlist_; - } - scoped_actor sys{system()}; - try { - auto playhead = request_receive( - *sys, on_screen_item_backend, playlist::create_playhead_atom_v) - .actor(); - anon_send(ph_events, viewport::viewport_playhead_atom_v, playhead); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } -} - - -QUuid SessionUI::mergePlaylists( - const QVariantList &cuuids, const QString &name, const QUuid &before_cuuid, const bool) { - // convert cuuids into playlist uuids. - QUuid new_playlist_cuuid; - UuidVector playlists; - - for (auto &i : cuuids) { - auto it = items_.find(i.toUuid()); - if (it != items_.end()) { - PlaylistUI *pl = nullptr; - if ((pl = dynamic_cast(*it))) { - playlists.push_back(pl->uuid()); - } - } - } - - scoped_actor sys{system()}; - try { - auto result = request_receive( - *sys, - backend_, - session::merge_playlist_atom_v, - StdFromQString(name), - UuidFromQUuid(before_cuuid), - playlists); - new_playlist_cuuid = QUuidFromUuid(result.first); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return new_playlist_cuuid; -} - -QVariantList SessionUI::copyContainers( - const QVariantList &cuuids, const QUuid &playlist_cuuid, const QUuid &before_cuuid) { - QVariantList containers; - - UuidVector copy_containers; - - for (auto &i : cuuids) - copy_containers.push_back(UuidFromQUuid(i.toUuid())); - - scoped_actor sys{system()}; - try { - auto result = request_receive( - *sys, - backend_, - playlist::copy_container_to_atom_v, - UuidFromQUuid(playlist_cuuid), - copy_containers, - UuidFromQUuid(before_cuuid), - false); - - for (auto const &i : result) - containers.push_back(QVariant(QUuidFromUuid(i))); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return containers; -} - -QVariantList SessionUI::moveContainers( - const QVariantList &cuuids, - const QUuid &playlist_cuuid, - const bool with_media, - const QUuid &before_cuuid) { - QVariantList containers; - - UuidVector copy_containers; - - for (auto &i : cuuids) - copy_containers.push_back(UuidFromQUuid(i.toUuid())); - - scoped_actor sys{system()}; - try { - auto result = request_receive( - *sys, - backend_, - playlist::move_container_to_atom_v, - UuidFromQUuid(playlist_cuuid), - copy_containers, - with_media, - UuidFromQUuid(before_cuuid), - false); - - for (auto const &i : result) - containers.push_back(QVariant(QUuidFromUuid(i))); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return containers; -} - -QVariantList SessionUI::copyMedia( - const QUuid &target_cuuid, - const QVariantList &media_quuids, - const bool force_duplicate, - const QUuid &before_cuuid) { - - QVariantList qresult; - UuidVector media_uuids; - - for (auto &i : media_quuids) - media_uuids.push_back(UuidFromQUuid(i.toUuid())); - - scoped_actor sys{system()}; - try { - auto result = request_receive( - *sys, - backend_, - playlist::copy_media_atom_v, - UuidFromQUuid(target_cuuid), - media_uuids, - force_duplicate, - UuidFromQUuid(before_cuuid), - false); - - for (auto const &i : result) - qresult.push_back(QVariant(QUuidFromUuid(i))); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return qresult; -} - -QVariantList SessionUI::moveMedia( - const QUuid &target_cuuid, - const QUuid &src_cuuid, - const QVariantList &media_quuids, - const QUuid &before_cuuid) { - - QVariantList qresult; - UuidVector media_uuids; - - for (auto &i : media_quuids) - media_uuids.push_back(UuidFromQUuid(i.toUuid())); - - scoped_actor sys{system()}; - try { - auto result = request_receive( - *sys, - backend_, - playlist::move_media_atom_v, - UuidFromQUuid(target_cuuid), - UuidFromQUuid(src_cuuid), - media_uuids, - UuidFromQUuid(before_cuuid), - false); - - for (auto const &i : result) - qresult.push_back(QVariant(QUuidFromUuid(i))); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return qresult; -} - -QFuture> SessionUI::handleDropFuture(const QVariantMap &drop) { - return QtConcurrent::run([=]() { - QList results; - try { - auto jsn = dropToJsonStore(drop); - - // spdlog::warn("{}", jsn.dump(2)); - - if (jsn.count("text/uri-list")) { - QList urls; - for (const auto &i : jsn["text/uri-list"]) - urls.append(QUrl(QStringFromStd(i))); - loadUrls(urls); - } else { - // something else... - // dispatch to datasource, if that doesn't work then assume file paths.. - scoped_actor sys{system()}; - auto pm = system().registry().template get(plugin_manager_registry); - try { - auto result = request_receive( - *sys, pm, data_source::use_data_atom_v, JsonStore(jsn), true); - if (result.empty()) { - for (const auto &i : jsn["text/plain"]) - loadUrl(QUrl(QStringFromStd("file:" + i.get()))); - } else { - // we've got a collection of actors.. - // lets assume they are media... (WARNING this may not be the case...) - // create new playlist and add them... - - auto new_playlist = request_receive( - *sys, - backend_, - session::add_playlist_atom_v, - "DragDrop Playlist", - utility::Uuid(), - false); - for (const auto &i : result) { - anon_send( - new_playlist.second.actor(), - playlist::add_media_atom_v, - i, - utility::Uuid()); - results.push_back(QUuidFromUuid(i.uuid())); - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return results; - }); -} diff --git a/retired/session_ui.hpp b/retired/session_ui.hpp deleted file mode 100644 index b80a03f64..000000000 --- a/retired/session_ui.hpp +++ /dev/null @@ -1,478 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include - -#include -#include - -CAF_PUSH_WARNINGS -#include -#include -#include -#include -#include -#include -CAF_POP_WARNINGS - -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/utility/container.hpp" -#include "xstudio/utility/logging.hpp" -#include "xstudio/utility/uuid.hpp" - -namespace xstudio { -namespace ui { - namespace qml { - - class PlaylistUI; - class BookmarksUI; - class PlaylistSelectionUI; - - class GroupModel : public QAbstractListModel { - Q_OBJECT - public: - enum PlaylistRoles { - TypeRole = Qt::UserRole + 1, - NameRole, - ObjectRole, - ExpandedRole, - SelectedRole - }; - - GroupModel(QObject *parent = nullptr); - [[nodiscard]] int - rowCount(const QModelIndex &parent = QModelIndex()) const override; - // int columnCount(const QModelIndex& parent = QModelIndex()) const; - [[nodiscard]] QVariant - data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - bool populate(QList &items, QObject *controller = nullptr); - // virtual bool insertRows(int row, int count, const QModelIndex &parent = - // QModelIndex()); - // bool moveRows( - // const QModelIndex &sourceParent, - // int sourceRow, - // int count, - // const QModelIndex &destinationParent, - // int destinationChild); - public slots: - bool move(int sourceRow, int count, int destinationChild); - QVariant get_object(const int sourceRow); - QVariant next_object(const QVariant obj); - [[nodiscard]] bool empty() const; - [[nodiscard]] int size() const; - - - protected: - [[nodiscard]] QHash roleNames() const override; - - private: - QList data_; - QObject *controller_{nullptr}; - }; - - class ContainerDividerUI : public QObject { - Q_OBJECT - - Q_PROPERTY(QUuid cuuid READ qcuuid NOTIFY cuuidChanged) - - Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) - Q_PROPERTY(QUuid uuid READ quuid NOTIFY uuidChanged) - Q_PROPERTY(QString type READ type NOTIFY typeChanged) - Q_PROPERTY(QString flag READ flag WRITE setFlag NOTIFY flagChanged) - Q_PROPERTY(bool selected READ selected WRITE setSelected NOTIFY selectedChanged) - - public: - explicit ContainerDividerUI(QObject *parent = nullptr); - explicit ContainerDividerUI( - const utility::Uuid cuuid, - const std::string name, - const std::string type, - const std::string flag, - const utility::Uuid uuid, - QObject *parent = nullptr); - ~ContainerDividerUI() override = default; - - [[nodiscard]] bool selected() const { return selected_; } - [[nodiscard]] QString name() const { return QStringFromStd(name_); } - - [[nodiscard]] QUuid quuid() const { return QUuidFromUuid(uuid_); } - [[nodiscard]] QUuid qcuuid() const { return QUuidFromUuid(cuuid_); } - - [[nodiscard]] utility::Uuid uuid() const { return uuid_; } - [[nodiscard]] utility::Uuid cuuid() const { return cuuid_; } - - [[nodiscard]] QString type() const { return QStringFromStd(type_); } - [[nodiscard]] QString flag() const { return QStringFromStd(flag_); } - - friend class SessionUI; - - signals: - void uuidChanged(); - void cuuidChanged(); - void nameChanged(); - void typeChanged(); - void flagChanged(); - void selectedChanged(); - - public slots: - void setName(const QString &_name) { - if (_name != name()) { - name_ = StdFromQString(_name); - emit nameChanged(); - } - } - void setFlag(const QString &_flag) { - if (_flag != flag()) { - flag_ = StdFromQString(_flag); - emit flagChanged(); - } - } - void setSelected(const bool value = true) { - if (selected_ != value) { - selected_ = value; - emit selectedChanged(); - } - } - - private: - utility::Uuid cuuid_; - std::string name_; - std::string type_; - std::string flag_; - utility::Uuid uuid_; - bool selected_{false}; - }; - - - class ContainerGroupUI : public QObject { - Q_OBJECT - - Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) - Q_PROPERTY(QUuid cuuid READ qcuuid NOTIFY cuuidChanged) - Q_PROPERTY(QUuid uuid READ quuid NOTIFY uuidChanged) - Q_PROPERTY(QList items READ items NOTIFY itemsChanged) - Q_PROPERTY(QVariant itemModel READ itemModel NOTIFY itemModelChanged) - Q_PROPERTY(QString type READ type NOTIFY typeChanged) - Q_PROPERTY(bool expanded READ expanded WRITE setExpanded NOTIFY expandedChanged) - Q_PROPERTY(QString flag READ flag WRITE setFlag NOTIFY flagChanged) - Q_PROPERTY(bool selected READ selected WRITE setSelected NOTIFY selectedChanged) - - public: - explicit ContainerGroupUI(QObject *parent = nullptr); - explicit ContainerGroupUI( - const utility::Uuid cuuid, - const std::string name, - const std::string type, - const std::string flag, - const utility::Uuid uuid, - QObject *parent = nullptr); - ~ContainerGroupUI() override = default; - - [[nodiscard]] bool selected() const { return selected_; } - [[nodiscard]] QString name() const { return QStringFromStd(name_); } - [[nodiscard]] QUuid quuid() const { return QUuidFromUuid(uuid_); } - [[nodiscard]] utility::Uuid uuid() const { return uuid_; } - - [[nodiscard]] QUuid qcuuid() const { return QUuidFromUuid(cuuid_); } - [[nodiscard]] utility::Uuid cuuid() const { return cuuid_; } - - QList items() { return items_; } - - [[nodiscard]] QObject *findItem(const utility::Uuid &uuid) const; - [[nodiscard]] QString type() const { return QStringFromStd(type_); } - QVariant itemModel() { return QVariant::fromValue(&model_); } - - QList items_; - [[nodiscard]] bool expanded() const { return expanded_; } - [[nodiscard]] QString flag() const { return QStringFromStd(flag_); } - - friend class SessionUI; - friend class PlaylistUI; - - public slots: - void setExpanded(const bool value = true) { - expanded_ = value; - emit expandedChanged(); - } - void setName(const QString &_name) { - if (_name != name()) { - name_ = StdFromQString(_name); - emit nameChanged(); - } - } - void setFlag(const QString &_flag) { - if (_flag != flag()) { - flag_ = StdFromQString(_flag); - emit flagChanged(); - } - } - void setSelected(const bool value = true) { - selected_ = value; - emit selectedChanged(); - } - - signals: - void cuuidChanged(); - void uuidChanged(); - void nameChanged(); - void itemsChanged(); - void typeChanged(); - void itemModelChanged(); - void expandedChanged(); - void flagChanged(); - void selectedChanged(); - - private: - // QUuid findCUuidFromUuid(const utility::Uuid &uuid) const; - - void changedItems() { - spdlog::info("changed group {}", items_.count()); - model_.populate(items_, this); - emit itemsChanged(); - } - - private: - utility::Uuid cuuid_; - std::string name_; - std::string type_; - std::string flag_; - utility::Uuid uuid_; - GroupModel model_; - bool expanded_{true}; - bool selected_{false}; - }; - - class SessionUI : public QMLActor { - - Q_OBJECT - // Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) - Q_PROPERTY(QVariant itemModel READ itemModel NOTIFY itemModelChanged) - Q_PROPERTY(bool expanded READ expanded WRITE setExpanded NOTIFY expandedChanged) - Q_PROPERTY(bool modified READ modified WRITE setModified NOTIFY modifiedChanged) - Q_PROPERTY(QUrl path READ path WRITE setPath NOTIFY pathChanged) - Q_PROPERTY(QString pathNative READ pathNative NOTIFY pathChanged) - - Q_PROPERTY(QString sessionActorAddr READ sessionActorAddr WRITE setSessionActorAddr - NOTIFY sessionActorAddrChanged) - - Q_PROPERTY(QVariant playlistNames READ playlistNames NOTIFY playlistNamesChanged) - Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) - Q_PROPERTY( - QDateTime sessionFileMTime READ sessionFileMTime NOTIFY sessionFileMTimechanged) - - Q_PROPERTY(QObject *playlist READ playlist NOTIFY playlistChanged) - Q_PROPERTY(QObject *selectedSource READ selectedSource NOTIFY selectedSourceChanged) - Q_PROPERTY(QObject *onScreenSource READ onScreenSource NOTIFY onScreenSourceChanged) - Q_PROPERTY( - double mediaRate READ mediaRate WRITE setMediaRate NOTIFY mediaRateChanged) - - public: - explicit SessionUI(QObject *parent = nullptr); - ~SessionUI() override; - - void init(caf::actor_system &system) override; - void set_backend(caf::actor backend); - - // QList playlists() {return playlists_list_;} - QVariant itemModel() { return QVariant::fromValue(&model_); } - [[nodiscard]] bool expanded() const { return expanded_; } - - [[nodiscard]] bool modified() const { - return xstudio::utility::time_point() != last_changed_ and - saved_time_ < last_changed_; - } - [[nodiscard]] QUrl path() const { return QUrlFromUri(path_); } - [[nodiscard]] QString pathNative() const { - return QStringFromStd(utility::uri_to_posix_path(path_)); - } - [[nodiscard]] QString name() const { return name_; } - [[nodiscard]] QDateTime sessionFileMTime() const { - return QDateTime::fromMSecsSinceEpoch( - std::chrono::duration_cast( - session_file_mtime_.time_since_epoch()) - .count()); - } - - void setSessionActorAddr(const QString &addr); - - QVariant playlistNames() { return playlistNames_; } - - QObject *playlist(); - - QObject *selectedSource(); - QObject *onScreenSource() { return on_screen_source_; } - - // QString name(); - utility::Uuid uuid(const QObject *obj) const; - utility::Uuid cuuid(const QObject *obj) const; - - double mediaRate() { return media_rate_.to_fps(); } - - [[nodiscard]] QString sessionActorAddr() const { return session_actor_addr_; }; - - signals: - - // void nameChanged(); - void backendChanged(); - - void sessionActorAddrChanged(); - - // void playlistsChanged(); - void modifiedChanged(); - void itemModelChanged(); - void expandedChanged(); - void pathChanged(); - void newItem(QVariant cuuid); - void playlistRemoved(const QVariant uuid); - void nameChanged(); - void playlistChanged(); - void playlistNamesChanged(); - void selectedSourceChanged(); - void onScreenSourceChanged(); - void sessionRequest(const QUrl path, const QByteArray jsn); - void mediaAdded(const QUuid &uuid); - void mediaRateChanged(const double rate); - void sessionFileMTimechanged(); - - public slots: - // bool save(const QUrl &path); - // bool load(const QUrl &path); - void setModified(const bool modified = true, bool clear = false) { - if (modified) - saved_time_ = utility::time_point(); - else if (clear) - saved_time_ = utility::clock::now() + std::chrono::seconds(1); - else - saved_time_ = last_changed_; - emit modifiedChanged(); - } - void setPath(const QUrl &path, const bool inform_backend = true); - QString posixPath() const { - return QStringFromStd(utility::uri_to_posix_path(path_)); - } - void initSystem(QObject *system_qobject); - - QUuid createPlaylist( - const QString &name = "Untitled Playlist", - const QUuid before = QUuid(), - const bool into = false); - - QUuid - createGroup(const QString &name = "Untitled Group", const QUuid before = QUuid()); - QUuid createDivider( - const QString &name = "Untitled Divider", - const QUuid before = QUuid(), - const bool into = false); - void setExpanded(const bool value) { - expanded_ = value; - emit expandedChanged(); - } - bool reflagContainer(const QString &flag, const QUuid &uuid); - bool renameContainer(const QString &name, const QUuid &cuuid); - bool - moveContainer(const QUuid &uuid, const QUuid &before_uuid, const bool into = false); - bool removeContainer(const QUuid &cuuid); - QUuid duplicateContainer( - const QUuid &cuuid, - const QUuid &before_cuuid = QUuid(), - const bool into = false); - QObject *findItem(const QUuid &uuid) const { return findItem(UuidFromQUuid(uuid)); } - - QList handleDrop(const QVariantMap &drop) { - return handleDropFuture(drop).result(); - } - QFuture> handleDropFuture(const QVariantMap &drop); - - bool loadUrls(const QList &urls, const bool single_playlist = false); - bool loadUrl(const QUrl &url, const bool single_playlist = false); - void - setSelection(const QVariantList &cuuids = QVariantList(), const bool clear = true); - void - setSelection(QObject *obj, const bool selected = true, const bool clear = true); - bool isSelected(QObject *obj) const; - QObject *getPlaylist(const QUuid &uuid); - QList getPlaylists(); - - void newSession(const QString &name); - QString save(); - QString save(const QUrl &path); - QString save_selected(const QUrl &path); - bool load(const QUrl &path); - bool load(const QUrl &path, const QVariant &json); - bool import(const QUrl &path, const QVariant &json = QVariant()); - void setName(const QString &name); - void switchOnScreenSource(const QUuid &uuid = QUuid(), const bool broadcast = true); - void rebuildPlaylistNamesList(); - void setMediaRate(const double rate); - void setViewerPlayhead(); - QUuid mergePlaylists( - const QVariantList &cuuids = QVariantList(), - const QString &name = "Combined Playlist", - const QUuid &before_cuuid = QUuid(), - const bool into = false); - - QVariantList copyContainers( - const QVariantList &cuuids, - const QUuid &playlist_cuuid, - const QUuid &before_cuuid = QUuid()); - - QVariantList moveContainers( - const QVariantList &cuuids, - const QUuid &playlist_cuuid, - const bool with_media = false, - const QUuid &before_cuuid = QUuid()); - - QVariantList copyMedia( - const QUuid &target_cuuid, - const QVariantList &media_quuids, - const bool force_duplicate = false, - const QUuid &before_cuuid = QUuid()); - - QVariantList moveMedia( - const QUuid &target_cuuid, - const QUuid &src_cuuid, - const QVariantList &media_quuids, - const QUuid &before_cuuid = QUuid()); - - QUuid getNextItemUuid(const QUuid &quuid); - - private: - bool - loadUris(const std::vector &uris, const bool single_playlist = false); - [[nodiscard]] QObject *findItem(const utility::Uuid &uuid) const; - void updateItemModel(const bool select_new_items = false, const bool reset = false); - QObject *getNextItem(const utility::Uuid &cuuid); - - - private: - caf::actor backend_; - caf::actor backend_events_; - caf::uri path_; - std::filesystem::file_time_type session_file_mtime_{ - std::filesystem::file_time_type::min()}; - - QMap items_; - QList items_list_; - GroupModel model_; - - QVariant playlistNames_; - QString name_; - - bool expanded_{true}; - utility::time_point saved_time_; - utility::time_point last_changed_; - - PlaylistUI *playlist_{nullptr}; - QObject *selected_source_{nullptr}; - QObject *on_screen_source_{nullptr}; - utility::PlaylistTree last_changed_tree_; - utility::FrameRate media_rate_; - - caf::actor dummy_playlist_; - - QString session_actor_addr_; - }; - } // namespace qml -} // namespace ui -} // namespace xstudio \ No newline at end of file diff --git a/retired/subset/src/CMakeLists.txt b/retired/subset/src/CMakeLists.txt deleted file mode 100644 index d77cf5b31..000000000 --- a/retired/subset/src/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core - xstudio::ui::qml::helper - xstudio::utility -) - -SET(EXTRAMOC -) - -create_qml_component(subset 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/retired/subset/src/subset_ui.cpp b/retired/subset/src/subset_ui.cpp deleted file mode 100644 index 0e5d817d2..000000000 --- a/retired/subset/src/subset_ui.cpp +++ /dev/null @@ -1,403 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/ui/qml/playlist_selection_ui.hpp" -#include "xstudio/ui/qml/playlist_ui.hpp" -#include "xstudio/ui/qml/subset_ui.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/logging.hpp" - -using namespace caf; -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::ui::qml; - -SubsetUI::SubsetUI(const utility::Uuid cuuid, QObject *parent) - : QMLActor(parent), - cuuid_(std::move(cuuid)), - backend_(), - backend_events_(), - name_("unknown"), - flag_("#00000000") { - - auto *p = dynamic_cast(parent); - if (p) { - QObject::connect(p, SIGNAL(nameChanged()), this, SIGNAL(nameChanged())); - } - emit parent_playlistChanged(); - emit mediaModelChanged(); -} - -void SubsetUI::set_backend(caf::actor backend) { - scoped_actor sys{system()}; - - if (backend_events_) { - try { - request_receive( - *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - backend_events_ = caf::actor(); - } - - backend_ = backend; - - if (backend_) { - try { - auto detail = request_receive(*sys, backend_, detail_atom_v); - name_ = detail.name_; - uuid_ = detail.uuid_; - backend_events_ = detail.group_; - request_receive( - *sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - update_media(); - emit nameChanged(); - emit uuidChanged(); - - auto selection_actor = - request_receive(*sys, backend_, playlist::selection_actor_atom_v); - playlist_selection_ = new PlaylistSelectionUI(this); - playlist_selection_->initSystem(this); - playlist_selection_->set_backend(selection_actor); - emit playlistSelectionThingChanged(); - } - emit backendChanged(); - spdlog::debug("SubsetUI set_backend {}", to_string(uuid_)); -} - -void SubsetUI::setName(const QString &name) { - std::string _name = StdFromQString(name); - if (_name != name_) { - if (backend_) { - scoped_actor sys{system()}; - sys->anon_send(backend_, utility::name_atom_v, _name); - } - } -} - -QObject *SubsetUI::findMediaObject(const QUuid &quuid) { - auto uuid = UuidFromQUuid(quuid); - - if (uuid_media_.count(uuid)) { - return uuid_media_[uuid]; - } - return nullptr; -} - -QFuture> SubsetUI::loadMediaFuture(const QUrl &path) { - // expect URI's - return QtConcurrent::run([=]() { - QList result; - scoped_actor sys{system()}; - try { - auto actor = request_receive(*sys, backend_, playhead::source_atom_v); - - auto items = utility::scan_posix_path(StdFromQString(path.path())); - std::sort( - std::begin(items), - std::end(items), - [](std::pair a, std::pair b) { - return a.first < b.first; - }); - - for (const auto &i : items) { - try { - if (is_file_supported(i.first)) { - auto ua = request_receive( - *sys, - actor, - playlist::add_media_atom_v, - "New media", - i.first, - i.second); - result.push_back(QUuidFromUuid(ua.uuid())); - anon_send(backend_, playlist::add_media_atom_v, ua.uuid(), Uuid()); - } else { - spdlog::warn("Unsupported file type {}.", to_string(i.first)); - } - } catch (const std::exception &e) { - spdlog::error("Failed to create media {}", e.what()); - } - } - - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; - }); -} - - -// needs to add to playlist first.. -QFuture> SubsetUI::handleDropFuture(const QVariantMap &drop) { - // handle drag drop.. - return QtConcurrent::run([=]() { - scoped_actor sys{system()}; - QList results; - - auto jsn = dropToJsonStore(drop); - - auto playlist = request_receive(*sys, backend_, playhead::source_atom_v); - - - // conver to json.. - if (jsn.count("text/uri-list")) { - for (const auto &path : jsn["text/uri-list"]) { - auto uri = caf::make_uri(path); - if (uri) { - auto new_media = request_receive( - *sys, playlist, playlist::add_media_atom_v, *uri, true); - for (const auto &i : new_media) { - anon_send(backend_, playlist::add_media_atom_v, i.uuid(), Uuid()); - results.push_back(QUuidFromUuid(i.uuid())); - } - } - } - } else { - // forward to datasources for non file paths - auto pm = system().registry().template get(plugin_manager_registry); - try { - auto result = request_receive( - *sys, pm, data_source::use_data_atom_v, JsonStore(jsn), true); - if (not result.empty()) { - // we've got a collection of actors.. - // lets assume they are media... (WARNING this may not be the case...) - // create new playlist and add them... - for (const auto &i : result) { - anon_send(playlist, playlist::add_media_atom_v, i, utility::Uuid()); - anon_send(backend_, playlist::add_media_atom_v, i.uuid(), Uuid()); - results.push_back(QUuidFromUuid(i.uuid())); - } - } else { - // try file load.. - for (const auto &i : jsn["text/plain"]) { - auto uri = caf::make_uri("file:" + i.get()); - if (uri) { - auto new_media = request_receive( - *sys, playlist, playlist::add_media_atom_v, *uri, true); - - - for (const auto &j : new_media) { - anon_send( - backend_, playlist::add_media_atom_v, j.uuid(), Uuid()); - results.push_back(QUuidFromUuid(j.uuid())); - } - } - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - return results; - }); -} - - -// bool SubsetUI::addMedia(const QUuid &uuid, const QUuid &before_uuid) { -// bool result = false; -// scoped_actor sys{system()}; - -// try { -// result = request_receive( -// *sys, -// backend_, -// playlist::add_media_atom_v, -// UuidFromQUuid(uuid), -// UuidFromQUuid(before_uuid)); -// } catch (const std::exception &e) { -// spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); -// } - -// return result; -// } - -void SubsetUI::update_media() { - scoped_actor sys{system()}; - QList to_delete; - - sys->request(backend_, infinite, playlist::get_media_atom_v) - .receive( - [&](const std::vector &actors) { - QList old_media = media_; - - for (auto i : actors) { - if (not uuid_media_.count(i.uuid())) { - uuid_media_[i.uuid()] = new MediaUI(this); - uuid_media_[i.uuid()]->initSystem(this); - uuid_media_[i.uuid()]->set_backend(i.actor()); - } - } - - // rebuild list. - media_.clear(); - media_order_.clear(); - - bool done = false; - while (not done) { - done = true; - for (const auto &i : uuid_media_) { - bool found = false; - for (const auto &ii : actors) { - if (i.first == ii.uuid()) { - found = true; - break; - } - } - if (not found) { - to_delete.append(i.second); - uuid_media_.erase(i.first); - done = false; - break; - } - } - } - - auto ind = 0; - for (auto i : actors) { - media_.append(uuid_media_[i.uuid()]); - media_order_[QUuidFromUuid(i.uuid()).toString()] = QVariant::fromValue(ind); - ind++; - } - - // dodgy are we still holding refs to dead media inside the model ..? - if (old_media != media_) { - media_model_.populate(media_); - emit mediaOrderChanged(); - emit mediaListChanged(); - } - - for (auto &i : to_delete) - i->deleteLater(); - }, - [=](const caf::error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); -} - - -void SubsetUI::init(actor_system &system_) { - QMLActor::init(system_); - emit systemInit(this); - - // self()->set_down_handler([=](down_msg &msg) { - // if (msg.source == backend_) { - // self()->leave(backend_events_); - // backend_events_ = caf::actor(); - // } - // }); - - spdlog::debug("SubsetUI init"); - - set_message_handler([=](actor_companion * /*self_*/) -> message_handler { - return { - [=](utility::event_atom, utility::last_changed_atom, const time_point &) {}, - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - - [=](const group_down_msg &) {}, - - [=](utility::event_atom, playlist::add_media_atom, const UuidActor &ua) { - emit mediaAdded(QUuidFromUuid(ua.uuid())); - }, - - [=](utility::event_atom, - playlist::media_content_changed_atom, - const std::vector &) {}, - - [=](utility::event_atom, utility::change_atom) { update_media(); }, - - [=](utility::event_atom, utility::name_atom, const std::string &name) { - if (name_ != name) { - name_ = name; - emit nameChanged(); - } - }}; - }); -} - -bool SubsetUI::removeMedia(const QUuid &uuid) { - bool result = false; - try { - scoped_actor sys{system()}; - result = request_receive( - *sys, backend_, playlist::remove_media_atom_v, UuidFromQUuid(uuid)); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return result; -} - - -void SubsetUI::dragDropReorder(const QVariantList dropped_uuids, const QString before_uuid) { - try { - - utility::UuidList drop_uuids; - for (auto v : dropped_uuids) { - drop_uuids.push_back(UuidFromQUuid(v.toUuid())); - } - utility::Uuid before = before_uuid == "" ? Uuid() : Uuid(StdFromQString(before_uuid)); - anon_send(backend_, playlist::move_media_atom_v, drop_uuids, before); - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } -} - -void SubsetUI::sortAlphabetically() { - anon_send(backend_, playlist::sort_alphabetically_atom_v); -} - -QObject *SubsetUI::selectionFilter() { return static_cast(playlist_selection_); } - -QString SubsetUI::fullName() { - if (auto *p = dynamic_cast(parent())) { - return p->name() + QString(" - ") + name(); - } else { - return name(); - } -} - -// find item based off container uuid or actor uuid -MediaUI *SubsetUI::getNextItem(const utility::Uuid &uuid) { - // might be media uuid ? - if (uuid_media_.count(uuid)) { - // find nextmedia - MediaUI *mojb = uuid_media_[uuid]; - int ind = 0; - for (auto &i : media_) { - if (i == mojb) { - ind++; - if (media_.size() > ind) { - return dynamic_cast(media_[ind]); - } - break; - } - ind++; - } - } - - return nullptr; -} - -QUuid SubsetUI::getNextItemUuid(const QUuid &quuid) { - QUuid result; - - auto obj = getNextItem(UuidFromQUuid(quuid)); - if (obj) - result = obj->uuid(); - - return result; -} - -bool SubsetUI::contains_media(const QUuid &key) const { - return uuid_media_.count(UuidFromQUuid(key)); -} diff --git a/retired/subset/test/CMakeLists.txt b/retired/subset/test/CMakeLists.txt deleted file mode 100644 index ca5580c50..000000000 --- a/retired/subset/test/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -include(CTest) diff --git a/retired/subset_ui.hpp b/retired/subset_ui.hpp deleted file mode 100644 index 25c949e7f..000000000 --- a/retired/subset_ui.hpp +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include - -CAF_PUSH_WARNINGS -#include -#include -#include -CAF_POP_WARNINGS - -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/ui/qml/media_ui.hpp" -#include "xstudio/ui/qml/playlist_ui.hpp" -#include "xstudio/utility/uuid.hpp" - -namespace xstudio { -namespace ui { - namespace qml { - - - class SubsetUI : public QMLActor { - - Q_OBJECT - Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) - Q_PROPERTY(QUuid uuid READ quuid NOTIFY uuidChanged) - Q_PROPERTY(QString type READ type NOTIFY typeChanged) - Q_PROPERTY(bool expanded READ expanded WRITE setExpanded NOTIFY expandedChanged) - Q_PROPERTY(QUuid cuuid READ qcuuid NOTIFY cuuidChanged) - Q_PROPERTY(QString flag READ flag WRITE setFlag NOTIFY flagChanged) - - Q_PROPERTY(QVariant mediaModel READ mediaModel NOTIFY mediaModelChanged) - Q_PROPERTY(QList mediaList READ mediaList NOTIFY mediaListChanged) - Q_PROPERTY(QVariantMap mediaOrder READ mediaOrder NOTIFY mediaOrderChanged) - - Q_PROPERTY(bool selected READ selected WRITE setSelected NOTIFY selectedChanged) - Q_PROPERTY(QObject *selectionFilter READ selectionFilter NOTIFY - playlistSelectionThingChanged) - Q_PROPERTY( - QObject *parent_playlist READ parent_playlist NOTIFY parent_playlistChanged) - Q_PROPERTY(QString fullName READ fullName NOTIFY nameChanged) - - Q_PROPERTY(bool hasBackend READ hasBackend NOTIFY backendChanged) - - public: - explicit SubsetUI( - const utility::Uuid cuuid = utility::Uuid(), QObject *parent = nullptr); - ~SubsetUI() override = default; - - void init(caf::actor_system &system) override; - void set_backend(caf::actor backend); - [[nodiscard]] caf::actor backend() const { return backend_; } - - [[nodiscard]] bool selected() const { return selected_; } - [[nodiscard]] QString name() const { return QStringFromStd(name_); } - [[nodiscard]] QUuid quuid() const { return QUuidFromUuid(uuid_); } - [[nodiscard]] utility::Uuid uuid() const { return uuid_; } - [[nodiscard]] QString type() const { return "Subset"; } - [[nodiscard]] bool expanded() const { return expanded_; } - [[nodiscard]] bool hasBackend() const { - return backend_ ? true : false; - ; - } - [[nodiscard]] QUuid qcuuid() const { return QUuidFromUuid(cuuid_); } - [[nodiscard]] utility::Uuid cuuid() const { return cuuid_; } - [[nodiscard]] QString flag() const { return QStringFromStd(flag_); } - - QVariant mediaModel() { return QVariant::fromValue(&media_model_); } - QList mediaList() { return media_; } - [[nodiscard]] QVariantMap mediaOrder() const { return media_order_; } - - QObject *selectionFilter(); - QObject *parent_playlist() { - return static_cast(dynamic_cast(parent())); - } - - QString fullName(); - - Q_INVOKABLE QObject *findMediaObject(const QUuid &uuid); - - signals: - void uuidChanged(); - void nameChanged(); - void typeChanged(); - void backendChanged(); - void systemInit(QObject *object); - void cuuidChanged(); - void expandedChanged(); - void flagChanged(); - void selectedChanged(); - void playlistSelectionThingChanged(); - void mediaAdded(const QUuid &uuid); - void parent_playlistChanged(); - void mediaModelChanged(); - void mediaListChanged(); - void mediaOrderChanged(); - - public slots: - void setName(const QString &name); - void setExpanded(const bool value = true) { - expanded_ = value; - emit expandedChanged(); - } - void setFlag(const QString &_flag) { - if (_flag != flag()) { - flag_ = StdFromQString(_flag); - emit flagChanged(); - } - } - - QList loadMedia(const QUrl &path) { return loadMediaFuture(path).result(); } - QFuture> loadMediaFuture(const QUrl &path); - - QList handleDrop(const QVariantMap &drop) { - return handleDropFuture(drop).result(); - } - QFuture> handleDropFuture(const QVariantMap &drop); - - // bool addMedia(const QUuid &uuid, const QUuid &before_uuid = QUuid()); - void setSelected(const bool value = true) { - if (selected_ != value) { - selected_ = value; - emit selectedChanged(); - } - } - - void dragDropReorder(const QVariantList drop_uuids, const QString before_uuid); - - void sortAlphabetically(); - QUuid getNextItemUuid(const QUuid &quuid); - bool contains_media(const QUuid &key) const; - bool removeMedia(const QUuid &uuid); - - private: - MediaUI *getNextItem(const utility::Uuid &uuid); - void update_media(); - - utility::Uuid cuuid_; - caf::actor backend_; - caf::actor backend_events_; - std::string name_; - std::string flag_; - utility::Uuid uuid_; - bool expanded_{true}; - bool selected_{false}; - QList media_; - std::map uuid_media_; - QVariantMap media_order_; - PlaylistSelectionUI *playlist_selection_ = {nullptr}; - - MediaModel media_model_; - }; - } // namespace qml -} // namespace ui -} // namespace xstudio \ No newline at end of file diff --git a/retired/thumbnail_ui.hpp b/retired/thumbnail_ui.hpp deleted file mode 100644 index a5eb67d1e..000000000 --- a/retired/thumbnail_ui.hpp +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include - -#include "xstudio/thumbnail/thumbnail.hpp" - -CAF_PUSH_WARNINGS -#include -#include -#include -CAF_POP_WARNINGS - -namespace xstudio { -namespace ui { - namespace qml { - - class MediaSourceUI; - - class ThumbNail : public QQuickPaintedItem { - - Q_OBJECT - - Q_PROPERTY(QObject *mediaSource READ mediaSource WRITE setMediaSource NOTIFY - mediaSourceChanged) - Q_PROPERTY(bool thumbLoaded READ thumbLoaded NOTIFY thumbLoadedChanged) - Q_PROPERTY(bool hasError READ hasError NOTIFY hasErrorChanged) - Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) - Q_PROPERTY(float busyAnim READ busyAnim WRITE setBusyAnim NOTIFY busyAnimChanged) - - public: - ThumbNail(QQuickItem *parent = nullptr); - ~ThumbNail() override = default; - - [[nodiscard]] QObject *mediaSource() const; - [[nodiscard]] bool thumbLoaded() const { return thumb_loaded_; } - [[nodiscard]] bool hasError() const { return has_error_; } - [[nodiscard]] QColor color() const { return color_; } - [[nodiscard]] float busyAnim() const { return busy_anim_; } - - void setMediaSource(QObject *media_item); - void setColor(QColor); - void setBusyAnim(float); - - void paint(QPainter *painter) override; - - public slots: - - void newThumbnail(const QImage &tnail, const float position_in_clip_duration); - void loadThumbnail(const float position_in_clip_duration); - void cancelLoadThumbnail(); - void busyAnimAdvance(); - - signals: - - void mediaSourceChanged(); - void thumbLoadedChanged(); - void colorChanged(); - void hasErrorChanged(); - void busyAnimChanged(); - - private: - QImage thumb_image_; - MediaSourceUI *media_source_ = {nullptr}; - float position_in_clip_duration_ = {-1.0f}; - bool thumb_loaded_ = {false}; - bool has_error_ = {false}; - QColor color_; - float busy_anim_ = {0.0f}; - }; - - - } // namespace qml -} // namespace ui -} // namespace xstudio \ No newline at end of file diff --git a/retired/timeline/src/CMakeLists.txt b/retired/timeline/src/CMakeLists.txt deleted file mode 100644 index 969e8c250..000000000 --- a/retired/timeline/src/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core - xstudio::ui::qml::helper - xstudio::utility -) - -SET(EXTRAMOC -) - -create_qml_component(timeline 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/retired/timeline/src/timeline_ui.cpp b/retired/timeline/src/timeline_ui.cpp deleted file mode 100644 index a43c30e8f..000000000 --- a/retired/timeline/src/timeline_ui.cpp +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/ui/qml/playlist_selection_ui.hpp" -#include "xstudio/ui/qml/timeline_ui.hpp" -#include "xstudio/utility/container.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/logging.hpp" - -using namespace caf; -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::ui::qml; - -TimelineUI::TimelineUI(const utility::Uuid cuuid, QObject *parent) - : QMLActor(parent), - cuuid_(std::move(cuuid)), - backend_(), - backend_events_(), - name_("unknown"), - flag_("#00000000") { - - auto *p = dynamic_cast(parent); - if (p) { - QObject::connect(p, SIGNAL(nameChanged()), this, SIGNAL(nameChanged())); - } - emit parent_playlistChanged(); - emit mediaModelChanged(); -} - -void TimelineUI::set_backend(caf::actor backend) { - - scoped_actor sys{system()}; - // get backend state.. - if (backend_events_) { - try { - request_receive( - *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - backend_events_ = caf::actor(); - } - - backend_ = backend; - - if (backend_) { - - try { - auto detail = request_receive(*sys, backend_, detail_atom_v); - name_ = detail.name_; - uuid_ = detail.uuid_; - backend_events_ = detail.group_; - request_receive( - *sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - update_media(); - emit nameChanged(); - emit uuidChanged(); - } - emit backendChanged(); - spdlog::debug("TimelineUI set_backend {}", to_string(uuid_)); -} - -void TimelineUI::setName(const QString &name) { - std::string _name = StdFromQString(name); - if (_name != name_) { - if (backend_) { - scoped_actor sys{system()}; - sys->anon_send(backend_, utility::name_atom_v, _name); - } - } -} - -void TimelineUI::update_media() { - scoped_actor sys{system()}; - - sys->request(backend_, infinite, playlist::get_media_atom_v, true) - .receive( - [&](const std::vector &actors) { - QList old_media = media_; - - for (auto i : actors) { - if (not uuid_media_.count(i.uuid())) { - uuid_media_[i.uuid()] = new MediaUI(this); - uuid_media_[i.uuid()]->initSystem(this); - uuid_media_[i.uuid()]->set_backend(i.actor()); - } - } - bool done = false; - while (not done) { - done = true; - for (const auto &i : uuid_media_) { - bool found = false; - for (const auto &ii : actors) { - if (i.first == ii.uuid()) { - found = true; - break; - } - } - if (not found) { - delete i.second; - uuid_media_.erase(i.first); - done = false; - break; - } - } - } - - // rebuild list. - media_.clear(); - media_order_.clear(); - auto ind = 0; - for (auto i : actors) { - media_.append(uuid_media_[i.uuid()]); - media_order_[QUuidFromUuid(i.uuid()).toString()] = QVariant::fromValue(ind); - ind++; - } - - if (old_media != media_) { - // media_model_.populate(media_); - emit mediaOrderChanged(); - emit mediaListChanged(); - } - }, - [=](const caf::error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); -} - - -void TimelineUI::init(actor_system &system_) { - QMLActor::init(system_); - emit systemInit(this); - - spdlog::debug("TimelineUI init"); - - set_message_handler([=](actor_companion * /*self_*/) -> message_handler { - return { - [=](utility::event_atom, utility::last_changed_atom, const time_point &) {}, - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg &) {}, - - [=](utility::event_atom, timeline::item_atom, const timeline::Item &) {}, - [=](utility::event_atom, timeline::item_atom, const JsonStore &, const bool) {}, - - [=](utility::event_atom, utility::change_atom) { update_media(); }, - - [=](utility::event_atom, utility::name_atom, const std::string &name) { - if (name_ != name) { - name_ = name; - emit nameChanged(); - } - }}; - }); -} - -QObject *TimelineUI::selectionFilter() { return static_cast(playlist_selection_); } - -QString TimelineUI::fullName() { - if (auto *p = dynamic_cast(parent())) { - return p->name() + QString(" - ") + name(); - } else { - return name(); - } -} \ No newline at end of file diff --git a/retired/timeline/test/CMakeLists.txt b/retired/timeline/test/CMakeLists.txt deleted file mode 100644 index ca5580c50..000000000 --- a/retired/timeline/test/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -include(CTest) diff --git a/retired/timeline_ui.hpp b/retired/timeline_ui.hpp deleted file mode 100644 index 9a78af992..000000000 --- a/retired/timeline_ui.hpp +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include - -CAF_PUSH_WARNINGS -#include -#include -#include -CAF_POP_WARNINGS - -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/ui/qml/media_ui.hpp" -#include "xstudio/ui/qml/playlist_ui.hpp" -#include "xstudio/utility/uuid.hpp" - -namespace xstudio { -namespace ui { - namespace qml { - - class PlaylistSelectionUI; - - class TimelineUI : public QMLActor { - - Q_OBJECT - Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) - Q_PROPERTY(QUuid uuid READ quuid NOTIFY uuidChanged) - Q_PROPERTY(QString type READ type NOTIFY typeChanged) - Q_PROPERTY(bool expanded READ expanded WRITE setExpanded NOTIFY expandedChanged) - Q_PROPERTY(QUuid cuuid READ qcuuid NOTIFY cuuidChanged) - Q_PROPERTY(QString flag READ flag WRITE setFlag NOTIFY flagChanged) - - Q_PROPERTY(QVariant mediaModel READ mediaModel NOTIFY mediaModelChanged) - Q_PROPERTY(QList mediaList READ mediaList NOTIFY mediaListChanged) - Q_PROPERTY(QVariantMap mediaOrder READ mediaOrder NOTIFY mediaOrderChanged) - - Q_PROPERTY(bool selected READ selected WRITE setSelected NOTIFY selectedChanged) - Q_PROPERTY(QObject *selectionFilter READ selectionFilter NOTIFY - playlistSelectionThingChanged) - Q_PROPERTY( - QObject *parent_playlist READ parent_playlist NOTIFY parent_playlistChanged) - Q_PROPERTY(QString fullName READ fullName NOTIFY nameChanged) - - Q_PROPERTY(bool hasBackend READ hasBackend NOTIFY backendChanged) - - public: - explicit TimelineUI( - const utility::Uuid cuuid = utility::Uuid(), QObject *parent = nullptr); - ~TimelineUI() override = default; - - void init(caf::actor_system &system) override; - void set_backend(caf::actor backend); - - [[nodiscard]] bool selected() const { return selected_; } - [[nodiscard]] QString name() const { return QStringFromStd(name_); } - [[nodiscard]] QUuid quuid() const { return QUuidFromUuid(uuid_); } - [[nodiscard]] utility::Uuid uuid() const { return uuid_; } - [[nodiscard]] QString type() const { return "Timeline"; } - [[nodiscard]] bool expanded() const { return expanded_; } - [[nodiscard]] QUuid qcuuid() const { return QUuidFromUuid(cuuid_); } - [[nodiscard]] utility::Uuid cuuid() const { return cuuid_; } - [[nodiscard]] QString flag() const { return QStringFromStd(flag_); } - [[nodiscard]] bool hasBackend() const { - return backend_ ? true : false; - ; - } - [[nodiscard]] caf::actor backend() const { return backend_; } - - QVariant mediaModel() { return QVariant::fromValue(&media_model_); } - QList mediaList() { return media_; } - [[nodiscard]] QVariantMap mediaOrder() const { return media_order_; } - - QObject *selectionFilter(); - QObject *parent_playlist() { - return static_cast(dynamic_cast(parent())); - } - - QString fullName(); - - signals: - void uuidChanged(); - void nameChanged(); - void typeChanged(); - void backendChanged(); - void systemInit(QObject *object); - void cuuidChanged(); - void expandedChanged(); - void flagChanged(); - void selectedChanged(); - void mediaAdded(const QUuid &uuid); - void playlistSelectionThingChanged(); - void parent_playlistChanged(); - void mediaModelChanged(); - void mediaListChanged(); - void mediaOrderChanged(); - - - public slots: - void setName(const QString &name); - void setExpanded(const bool value = true) { - expanded_ = value; - emit expandedChanged(); - } - void setFlag(const QString &_flag) { - if (_flag != flag()) { - flag_ = StdFromQString(_flag); - emit flagChanged(); - } - } - void setSelected(const bool value = true) { - if (selected_ != value) { - selected_ = value; - emit selectedChanged(); - } - } - void dragDropReorder(const QVariantList, const QString) {} - void sortAlphabetically() {} - - // QList loadMedia(const QUrl &path) { return - // loadMediaFuture(path).result(); } QFuture> loadMediaFuture(const - // QUrl &path); bool addMedia(const QUuid &uuid, const QUuid &before_uuid = - // QUuid()); QUuid getNextItemUuid(const QUuid &quuid); bool - // contains_media(const QUuid &key) const; - - - private: - // MediaUI *getNextItem(const utility::Uuid &uuid); - void update_media(); - - utility::Uuid cuuid_; - caf::actor backend_; - caf::actor backend_events_; - std::string name_; - std::string flag_; - utility::Uuid uuid_; - bool expanded_{true}; - bool selected_{false}; - QList media_; - std::map uuid_media_; - - QVariantMap media_order_; - PlaylistSelectionUI *playlist_selection_ = {nullptr}; - - MediaModel media_model_; - }; - } // namespace qml -} // namespace ui -} // namespace xstudio \ No newline at end of file diff --git a/scripts/debug/hung_actors.py b/scripts/debug/hung_actors.py new file mode 100755 index 000000000..aea878f4d --- /dev/null +++ b/scripts/debug/hung_actors.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: Apache-2.0 + +import sys +import argparse + +class LogLine(): + def __init__(self, line): + self.line = line + [self.something, self.module, self.level, self.actor, self.timestamp, self.action, self.detail] = self.line.split("|", 6) + + self.id = "" + self.make = False + self.unmake = False + if self.action == "on_cleanup": + self.id = self.detail.split(" ; ", 3)[1].strip() + self.unmake = True + elif self.action.startswith("make_actor"): + self.id = self.detail.split(" ; ", 3)[1].strip() + self.make = True + +# 9|caf_flow|DEBUG|actor0|139866115874624|make_actor|/tmp/bobscratch/build/libcaf_core/caf/make_actor.hpp:27|SPAWN ; ID = 4 ; NAME = caf.system.basp-broker ; TYPE = caf.io.basp_broker ; ARGS = [actor_config(hidden_flag)] ; NODE = 55844C7CC2C19DFBB296A94ED6107BF430CE3A3E#3678201 + + +def hung_main(): + """Do main function.""" + retval = 0 + parser = argparse.ArgumentParser( + # description=__doc__ + " Scans CAF logs.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "-l", "--log", action="store", + type=str, + help="Log file", + default=None + ) + + args = parser.parse_args() + +# 468 caf_flow DEBUG actor12 140372996126464 make_actor /tmp/bobscratch/build/libcaf_core/caf/make_actor.hpp:27 SPAWN ; ID = 13 ; NAME = scoped_actor ; TYPE = caf.ANON.impl ; ARGS = [actor_config(detached_flag)] ; NODE = 999146CCA9C0736D297B32D1725433D9877D838B#3673267 +# 468 caf_flow DEBUG actor13 140372996126464 on_cleanup /tmp/bobscratch/build/libcaf_core/caf/local_actor.cpp:172 TERMINATE ; ID = 13 ; REASON = none ; NODE = 999146CCA9C0736D297B32D1725433D9877D838B#3673267 + + + if args.log: + logs = [] + + with open(args.log) as logfile: + for line in logfile: + line = line.rstrip() + if line[0].isdigit(): + try: + logs.append(LogLine(line)) + except: + pass + + # prune logs we don't want. + + logs = [x for x in logs if x.module == "caf_flow"] + logs = [x for x in logs if x.action == "on_cleanup" or x.action.startswith("make_actor")] + + # for l in logs: + # print(l.line) + + # scan logs handling actors.. + + active = {} + created_by = {} + creation = {} + + for l in logs: + if l.make: + if l.id in active: + print(l.id) + else: + active[l.id] = l + creation[l.id] = l + if l.id not in created_by: + created_by[l.id] = "ID = "+l.actor[5:] + + elif l.unmake: + if l.id not in active: + print(l.line) + else: + del active[l.id] + + for a in active: + if created_by[active[a].id] in creation: + print(active[a].line) + print(creation[created_by[active[a].id]].detail+"\n") + else: + print(active[a].line) + +if __name__ == "__main__": + hung_main() diff --git a/scripts/linting/find_unused_atoms b/scripts/linting/find_unused_atoms old mode 100755 new mode 100644 diff --git a/scripts/linting/find_unused_qml b/scripts/linting/find_unused_qml old mode 100755 new mode 100644 diff --git a/scripts/linting/license_stub_check b/scripts/linting/license_stub_check deleted file mode 100755 index bee358fb4..000000000 --- a/scripts/linting/license_stub_check +++ /dev/null @@ -1,59 +0,0 @@ -#! /usr/bin/env python - -import re -import sys -import os -from pathlib import Path -import argparse - -class ColorPrint: - - @staticmethod - def print_fail(message, end='\n'): - sys.stderr.write('\x1b[1;31m' + message + '\x1b[0m' + end) - - @staticmethod - def print_pass(message, end='\n'): - sys.stdout.write('\x1b[1;32m' + message + '\x1b[0m' + end) - - @staticmethod - def print_warn(message, end='\n'): - sys.stderr.write('\x1b[1;33m' + message + '\x1b[0m' + end) - - @staticmethod - def print_info(message, end='\n'): - sys.stdout.write('\x1b[1;34m' + message + '\x1b[0m' + end) - - @staticmethod - def print_bold(message, end='\n'): - sys.stdout.write('\x1b[1;37m' + message + '\x1b[0m' + end) - -def check_for_license_stub(filepath): - - with open(filepath) as f: - - data = f.readline() - if 'SPDX-License-Identifier: Apache-2.0' not in data: - data = f.readline() - if 'SPDX-License-Identifier: Apache-2.0' not in data: - data = f.readline() - if 'SPDX-License-Identifier: Apache-2.0' not in data: - data = f.readline() - if 'SPDX-License-Identifier: Apache-2.0' not in data: - ColorPrint.print_warn("Filepath has no licence stub: {0}".format(filepath)) - -if __name__=="__main__": - - dirs = ['./src/', './include/', './python/', './ui/'] - ignore_exts = ['.cpp', '.hpp', '.qml', '.py'] - - for d in dirs: - for filepath in Path(d).rglob('*.*'): - if not True in [str(filepath).find(b) != -1 for b in ignore_exts]: - continue - if filepath.is_dir(): - continue - try: - check_for_license_stub(filepath) - except Exception as e: - ColorPrint.print_warn("{0} : {1}".format(filepath, e)) \ No newline at end of file diff --git a/scripts/linting/tidy_message_handlers b/scripts/linting/tidy_message_handlers old mode 100755 new mode 100644 index e1c8de6c5..7402b484f --- a/scripts/linting/tidy_message_handlers +++ b/scripts/linting/tidy_message_handlers @@ -152,7 +152,7 @@ def parse_behaviour_assign(original_code, position, reordered_code): if not lambda_returns_something and lambda_contents.find("response_promise") != -1: - print ("\n\nResponse promise used in lambda not returning a value:\n {0}\n\n".format(lambda_contents)); + print ("\n\ndodgy mothafucka {0}\n\n".format(lambda_contents)); if lambda_contents.count("\n") > 8: num_overlength_lambdas += 1 diff --git a/scripts/qt_install/CMakeLists.txt b/scripts/qt_install/CMakeLists.txt index 487937c1d..d97074dd6 100644 --- a/scripts/qt_install/CMakeLists.txt +++ b/scripts/qt_install/CMakeLists.txt @@ -1,2 +1,2 @@ #After everything else is installed, windeployqt will scan the contents and package up Qt dependencies. -install(CODE "execute_process(COMMAND ${Qt5_DIR}/../../../bin/windeployqt.exe ${CMAKE_INSTALL_PREFIX}/bin/xstudio.exe --qmldir ${CMAKE_SOURCE_DIR}/ui)" ) \ No newline at end of file +install(CODE "execute_process(COMMAND ${Qt6_DIR}/../../../bin/windeployqt.exe ${CMAKE_INSTALL_PREFIX}/bin/xstudio.exe --qmldir ${CMAKE_SOURCE_DIR}/ui)" ) \ No newline at end of file diff --git a/share/docs/_static/css/badge_only.css b/share/docs/_static/css/badge_only.css index 3c33cef54..a01ebc6ec 100644 --- a/share/docs/_static/css/badge_only.css +++ b/share/docs/_static/css/badge_only.css @@ -1 +1 @@ -.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../fonts/fontawesome-webfont.eot");src:url("../fonts/fontawesome-webfont.eot?#iefix") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff") format("woff"),url("../fonts/fontawesome-webfont.ttf") format("truetype"),url("../fonts/fontawesome-webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} +.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../fonts/fontawesome-webfont.eot");src:url("../fonts/fontawesome-webfont.eot?#iefix") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff") format("woff"),url("../fonts/fontawesome-webfont.ttf") format("truetype"),url("../fonts/fontawesome-webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} diff --git a/share/fonts/CMakeLists.txt b/share/fonts/CMakeLists.txt index 30de99c03..981493f08 100644 --- a/share/fonts/CMakeLists.txt +++ b/share/fonts/CMakeLists.txt @@ -1,32 +1,8 @@ -set(fonts) - -macro(add_font name) - add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/bin/fonts/${name} - COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/${name} - ${CMAKE_BINARY_DIR}/bin/fonts/${name} - DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${name}) - list(APPEND fonts ${CMAKE_BINARY_DIR}/bin/fonts/${name}) -endmacro() - file(GLOB FONTFILES ${CMAKE_CURRENT_SOURCE_DIR}/*.ttf) +add_custom_target(FONTS ALL) + foreach(FONTFile ${FONTFILES}) get_filename_component(FONTNAME ${FONTFile} NAME) - add_font(${FONTNAME}) -endforeach() - -add_custom_target( - fonts-copy ALL DEPENDS - ${fonts} -) - -if(WIN32) - install(FILES - ${fonts} - DESTINATION - ${CMAKE_INSTALL_PREFIX}/fonts) -else() - install(FILES - ${fonts} - DESTINATION share/xstudio/fonts) -endif() \ No newline at end of file + add_font(${FONTNAME} ${CMAKE_CURRENT_SOURCE_DIR} FONTS) +endforeach() \ No newline at end of file diff --git a/share/fonts/Overpass-Regular.ttf b/share/fonts/Overpass-Regular.ttf old mode 100755 new mode 100644 diff --git a/share/preference/CMakeLists.txt b/share/preference/CMakeLists.txt index c36ebee60..03c4c34f9 100644 --- a/share/preference/CMakeLists.txt +++ b/share/preference/CMakeLists.txt @@ -1,33 +1,11 @@ -set(prefs) - -macro(add_pref name) - add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/bin/preference/${name} - COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/${name} - ${CMAKE_BINARY_DIR}/bin/preference/${name} - DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${name}) - list(APPEND prefs ${CMAKE_BINARY_DIR}/bin/preference/${name}) -endmacro() file(GLOB PREFFILES ${CMAKE_CURRENT_SOURCE_DIR}/*.json) +add_custom_target(PREFERENCES ALL) + foreach(PREFFile ${PREFFILES}) get_filename_component(PREFNAME ${PREFFile} NAME) - add_pref(${PREFNAME}) + add_preference(${PREFNAME} ${CMAKE_CURRENT_SOURCE_DIR} PREFERENCES) endforeach() -add_custom_target( - preferences-copy ALL DEPENDS - ${prefs} -) - -if(WIN32) - install(FILES - ${prefs} - DESTINATION - ${CMAKE_INSTALL_PREFIX}/preference) -else() - install(FILES - ${prefs} - DESTINATION share/xstudio/preference) -endif() \ No newline at end of file diff --git a/share/preference/core_api.json b/share/preference/core_api.json index 3336f1305..0dbd99a3a 100644 --- a/share/preference/core_api.json +++ b/share/preference/core_api.json @@ -36,6 +36,32 @@ "value": "127.0.0.1", "datatype": "string", "context": ["APPLICATION"] + }, + "authentication": { + "allow_unauthenticated": { + "path": "/core/api/authentication/allow_unauthenticated", + "default_value": false, + "description": "Allow unauthenticated access.", + "value": true, + "datatype": "bool", + "context": ["APPLICATION"] + }, + "keys": { + "path": "/core/api/authentication/keys", + "default_value": [], + "description": "Authentication client/keys.", + "value": [], + "datatype": "json", + "context": ["APPLICATION"] + }, + "passwords": { + "path": "/core/api/authentication/passwords", + "default_value": [], + "description": "Authentication user/passwords.", + "value": [], + "datatype": "json", + "context": ["APPLICATION"] + } } } } diff --git a/share/preference/core_audio.json b/share/preference/core_audio.json index 44f121039..04db9104b 100644 --- a/share/preference/core_audio.json +++ b/share/preference/core_audio.json @@ -4,28 +4,34 @@ "audio_latency_millisecs": { "path": "/core/audio/audio_latency_millisecs", "default_value": 0, - "description": "Adjustable latency to overcome fixed latency in the audio playback pipeline", + "description": "Adjustable latency to overcome fixed latency in the audio playback pipeline. If you find the audio is playling ahead of the video, try reducing this number (and vice versa).", "value": 0, "minimum": -1000, "maximum": 1000, "datatype": "int", - "context": ["APPLICATION"] + "context": ["APPLICATION"], + "category": "Audio", + "display_name": "Audio Delay / ms" }, "audio_repitch": { "path": "/core/audio/audio_repitch", "default_value": false, - "description": "Adjustable latency to overcome fixed latency in the audio playback pipeline", + "description": "Audio will be re-pitched to match your playback 'velocity' if this is enabled.", "value": false, "datatype": "bool", - "context": ["APPLICATION"] + "context": ["APPLICATION"], + "category": "Audio", + "display_name": "Audio Re-pitch" }, "audio_scrubbing": { "path": "/core/audio/audio_scrubbing", "default_value": false, - "description": "Sound audio when not playing but the user is scrubbing frames", + "description": "Enable this to hear audio when the playhead/timeline is being scrubbed.", "value": false, "datatype": "bool", - "context": ["APPLICATION"] + "context": ["APPLICATION"], + "category": "Audio", + "display_name": "Audio Scrubbing" }, "volume": { "path": "/core/audio/volume", @@ -56,9 +62,9 @@ }, "buffer_size": { "path": "/core/audio/pulse_audio_prefs/buffer_size", - "default_value": 4096, + "default_value": 2048, "description": "Souncard audio samples buffer size", - "value": 4096, + "value": 2048, "minimum": 512, "maximum": 16384, "datatype": "int", diff --git a/share/preference/core_bookmark.json b/share/preference/core_bookmark.json index e2660d6be..7582fe90a 100644 --- a/share/preference/core_bookmark.json +++ b/share/preference/core_bookmark.json @@ -3,8 +3,8 @@ "bookmark":{ "note_category": { "path": "/core/bookmark/note_category", - "default_value": "default", - "value": "default", + "default_value": "", + "value": "", "description": "Default note category.", "datatype": "string", "context": ["APPLICATION"] @@ -31,7 +31,27 @@ { "value": "default", "colour": "", - "text": "Default" + "text": "Miscellaneous" + }, + { + "value": "director", + "colour": "#FFFF0000", + "text": "Director Note" + }, + { + "value": "studio", + "colour": "#FF00FF00", + "text": "Studio Note" + }, + { + "value": "vfx", + "colour": "#FF0000FF", + "text": "VFX Supervisor Note" + }, + { + "value": "editor", + "colour": "#FF00FFFF", + "text": "Editor Note" } ], "description": "Category defaults.", diff --git a/share/preference/core_cache.json b/share/preference/core_cache.json index 8d9725b94..4b63bf93d 100644 --- a/share/preference/core_cache.json +++ b/share/preference/core_cache.json @@ -4,7 +4,7 @@ "max_count": { "path": "/core/image_cache/max_count", "default_value": 1000000, - "description": "Maximum number of entries to store in cache.", + "description": "This value sets the maximum number of cached frames.", "value": 1000000, "minimum": 1, "maximum": 1000000, @@ -13,19 +13,31 @@ }, "max_size": { "path": "/core/image_cache/max_size", - "default_value": 1024, - "description": "Maximum total size of cache in megabytes.", - "value": 20480, + "default_value": 4096, + "description": "This setting controls the maximum amount of your computer's RAM that xSTUDIO will use to store video frames during playback. A higher number means more memory is used but this will allow xSTUDIO to keep de-coded frames ready for immediate display.", + "value": 4096, "datatype": "int", - "context": ["APPLICATION","SESSION"] + "context": ["APPLICATION","SESSION"], + "category": "General", + "display_name": "Video Cache Size / Mb" + }, + "release_on_idle": { + "path": "/core/image_cache/release_on_idle", + "default_value": 0, + "description": "This setting will release the memory used by the cache if the app has been idle for X minutes, 0 disables this.", + "value": 0, + "datatype": "int", + "context": ["APPLICATION","SESSION"], + "category": "General", + "display_name": "Video Cache Idle Clear" } }, "audio_cache":{ "max_count": { "path": "/core/audio_cache/max_count", - "default_value": 1000000, + "default_value": 4096, "description": "Maximum number of entries to store in cache.", - "value": 1000000, + "value": 4096, "minimum": 1, "maximum": 1000000, "datatype": "int", @@ -35,9 +47,17 @@ "path": "/core/audio_cache/max_size", "default_value": 512, "description": "Maximum total size of cache in megabytes.", - "value": 2048, + "value": 512, "datatype": "int", "context": ["APPLICATION"] + }, + "release_on_idle": { + "path": "/core/image_cache/release_on_idle", + "default_value": 0, + "description": "This setting will release the memory used by the cache if the app has been idle for X minutes, 0 disables this.", + "value": 0, + "datatype": "int", + "context": ["APPLICATION","SESSION"] } } } diff --git a/share/preference/core_conform.json b/share/preference/core_conform.json index d27609c9c..b60fb2692 100644 --- a/share/preference/core_conform.json +++ b/share/preference/core_conform.json @@ -10,6 +10,18 @@ "maximum": 100, "datatype": "int", "context": ["APPLICATION"] + }, + "conform_sibling_count": { + "path": "/core/conform/conform_sibling_count", + "default_value": 2, + "description": "Conform with N siblings", + "value": 2, + "minimum": -1, + "maximum": 10, + "datatype": "int", + "context": ["APPLICATION"], + "category": "Conform", + "display_name": "Conform Siblings Count" } } } diff --git a/share/preference/core_global_store.json b/share/preference/core_global_store.json index 06e11bf72..478023659 100644 --- a/share/preference/core_global_store.json +++ b/share/preference/core_global_store.json @@ -10,6 +10,14 @@ "maximum": 600, "datatype": "int", "context": ["APPLICATION"] + }, + "autosave_enable": { + "path": "/core/global_store/autosave_enable", + "default_value": true, + "description": "Enable autosave of preferences.", + "value": true, + "datatype": "bool", + "context": ["APPLICATION"] } } } diff --git a/share/preference/core_media_reader.json b/share/preference/core_media_reader.json index ca53741d5..3d02ae54e 100644 --- a/share/preference/core_media_reader.json +++ b/share/preference/core_media_reader.json @@ -1,6 +1,25 @@ { "core": { "media_reader":{ + "filepath_map_regex_replace": { + "path": "/core/media_reader/filepath_map_regex_replace", + "default_value": [], + "description": "This preference must be a json array. Each element of the array must be another array of 4 strings. The first two strings are a pair of regex search and replace to map an incoming (virtual) path to a local filepath. The second pair of strings are likewise regex searh/replace expressions to map the local filepath back to the 'virtual' filepath.", + "value": [], + "datatype": "json", + "context": ["APPLICATION"] + }, + "user_filepath_map_regex_replace": { + "path": "/core/media_reader/user_filepath_map_regex_replace", + "default_value": [], + "description": "This preference must be a json array. Each element of the array must be another array of 4 strings. The first two strings are a pair of regex search and replace to map an incoming (virtual) path to a local filepath. The second pair of strings are likewise regex searh/replace expressions to map the local filepath back to the 'virtual' filepath.", + "value": [], + "datatype": "json", + "display_name": "Filepath re-mapping REGEX", + "context": ["APPLICATION"], + "category": "General", + "options": "import xStudio 1.0; XsJsonPreference {}" + }, "max_source_count": { "path": "/core/media_reader/max_source_count", "default_value": 16, diff --git a/share/preference/core_playhead.json b/share/preference/core_playhead.json index a704f1cf1..256315134 100644 --- a/share/preference/core_playhead.json +++ b/share/preference/core_playhead.json @@ -1,6 +1,23 @@ { "core": { "playhead":{ + "remote_desktop_optimisation": { + "path": "/core/playhead/remote_desktop_optimisation", + "default_value": false, + "description": "Reduce UI updates whilst playing.", + "value": false, + "datatype": "bool", + "category": "Playback", + "context": ["APPLICATION"] + }, + "remote_desktop_optimisation_delay": { + "path": "/core/playhead/remote_desktop_optimisation_delay", + "default_value": 250, + "description": "UI refresh delay.", + "value": 250, + "datatype": "int", + "context": ["APPLICATION"] + }, "read_ahead": { "path": "/core/playhead/read_ahead", "default_value": 128, @@ -23,23 +40,58 @@ }, "max_compare_sources": { "path": "/core/playhead/max_compare_sources", - "default_value": 9, + "default_value": 48, "description": "Maximum number of media sources that can be compared via xstudio compare modes. Playback performance will suffer if you try and compare many sources.", - "value": 9, + "value": 48, "minimum": 2, "maximum": 64, "datatype": "int", "context": ["APPLICATION"] }, - "align_mode": { - "path": "/core/playhead/align_mode", - "default_value": "On (Trim)", - "description": "Compare mode behaviour for media with different but overlapping frame ranges. 'Off' means line up first logical frames, 'Frames' mean align on the first frame number, 'Frames & Trim' means trim to the overlapping frame range only.", - "value": "On (Trim)", - "datatype": "string", - "context": ["APPLICATION"] + "playlist_default_compare": { + "path": "/core/playhead/playlist_default_compare", + "default_value": ["A/B", "On"], + "description": "Set the default compare mode and auto align behaviour for new Playlists.", + "value": ["A/B", "On"], + "datatype": "json", + "options": "import xStudio 1.0; XsCompareModePref {}", + "context": ["APPLICATION"], + "category": "Compare", + "display_name": "Playlists" + }, + "subset_default_compare": { + "path": "/core/playhead/subset_default_compare", + "default_value": ["A/B", "On"], + "description": "Set the default compare mode and auto align behaviour for new Subsets.", + "value": ["A/B", "On"], + "datatype": "json", + "options": "import xStudio 1.0; XsCompareModePref {}", + "context": ["APPLICATION"], + "category": "Compare", + "display_name": "Subsets" + }, + "contact_sheet_default_compare": { + "path": "/core/playhead/contact_sheet_default_compare", + "default_value": ["Grid", "Off"], + "description": "Set the default compare mode and auto align behaviour for new Contact Sheets.", + "value": ["Grid", "Off"], + "datatype": "json", + "options": "import xStudio 1.0; XsCompareModePref {}", + "context": ["APPLICATION"], + "category": "Compare", + "display_name": "Contact Sheets" + }, + "timeline_default_compare": { + "path": "/core/playhead/timeline_default_compare", + "default_value": ["Off", "Off"], + "description": "Set the default compare mode and auto align behaviour for new Timelines.", + "value": ["Off", "Off"], + "datatype": "json", + "options": "import xStudio 1.0; XsCompareModePref {}", + "context": ["APPLICATION"], + "category": "Compare", + "display_name": "Timelines" } - } } } \ No newline at end of file diff --git a/share/preference/core_plugin_manager.json b/share/preference/core_plugin_manager.json index 84b1020f7..e244c5f38 100644 --- a/share/preference/core_plugin_manager.json +++ b/share/preference/core_plugin_manager.json @@ -6,14 +6,19 @@ "default_value": "{}", "description": "Enabled plugins.", "value": { - "e4e1d569-2338-4e6e-b127-5a9688df161a": false, + "e4e1d569-2338-4e6e-b127-5a9688df161a": true, "33201f8d-db32-4278-9c40-8c068372a304": false, "46f386a0-cb9a-4820-8e99-fb53f6c019eb": true, - "5598e01e-c6bc-4cf9-80ff-74bb560df12a": true, "9437e200-80da-4725-97d7-02d5a11b3af1": true, "95268f7c-88d1-48da-8543-c5275ef5b2c5": true, "f8a09960-606d-11ed-9b6a-0242ac120002": true, - "4006826a-6ff2-41ec-8ef2-d7a40bfd65e4": true + "4006826a-6ff2-41ec-8ef2-d7a40bfd65e4": true, + "f3e7c2db-2578-45d6-8ad5-743779057a63": true, + "5598e01e-c6bc-4cf9-80ff-74bb560df12a": true, + "b78e2aff-4709-46a1-9db2-61260997d401": true, + "5a095fde-296c-4d78-90e1-7da55ee44806": true, + "873c508b-276b-44e3-82d0-15db2f039aa7": true, + "8c0f06a8-cf43-44f3-94e2-0428bf7d150c": false }, "datatype": "json", "context": ["APPLICATION"] diff --git a/share/preference/core_python.json b/share/preference/core_python.json index a54bb5475..25c7cf2f5 100644 --- a/share/preference/core_python.json +++ b/share/preference/core_python.json @@ -9,16 +9,34 @@ "datatype": "bool", "context": ["APPLICATION"] }, - "snippets":{ "path": { "path": "/core/python/snippets/path", - "default_value": [], + "default_value": ["${XSTUDIO_ROOT}/snippets","${HOME}/xStudio/snippets"], "description": "Paths to snippet dirs", - "value": [], + "value": null, "datatype": "json", "context": ["APPLICATION"] } + }, + "python_plugin_search_folders": { + "path": "/core/python/python_plugin_search_folders", + "default_value": [], + "description": "This preference must be a json array. Each element of the array must be a path.", + "value": [], + "datatype": "json", + "context": ["APPLICATION"] + }, + "user_python_plugin_search_folders": { + "path": "/core/python/user_python_plugin_search_folders", + "default_value": [], + "description": "This preference must be a json array. Each element of the array must be a path to a filesystem folder. xSTUDIO will search these folders for xSTUDIO python plugins.", + "value": [], + "datatype": "json", + "display_name": "Python Plugins Search Paths", + "context": ["APPLICATION"], + "category": "Python", + "options": "import xStudio 1.0; XsJsonPreference {}" } } } diff --git a/share/preference/core_sequence.json b/share/preference/core_sequence.json new file mode 100644 index 000000000..6b4ba8952 --- /dev/null +++ b/share/preference/core_sequence.json @@ -0,0 +1,18 @@ +{ + "core": { + "sequence":{ + "create_tracks": { + "path": "/core/sequence/create_tracks", + "default_value": [ + {"name": "ABC", "video tracks": ["A","B","C"], "audio tracks": ["A", "B","C"]} + ], + "description": "Create default tracks quickly", + "value": [ + {"name": "ABC", "video tracks": [{"name":"A", "colour":"#ffff0000"},"B","C"], "audio tracks": ["A", "B","C"]} + ], + "datatype": "json", + "context": ["APPLICATION"] + } + } + } +} \ No newline at end of file diff --git a/share/preference/core_session.json b/share/preference/core_session.json index 43896d03b..9422f7bec 100644 --- a/share/preference/core_session.json +++ b/share/preference/core_session.json @@ -3,12 +3,12 @@ "session":{ "play_rate": { "path": "/core/session/play_rate", - "default_value": 24.0, + "default_value": "24.0", "description": "Default playback rate.", - "value": 24.0, - "minimum": 1.0, - "maximum": 120.0, - "datatype": "double", + "value": "24.0", + "minimum": "1.0", + "maximum": "120.0", + "datatype": "string", "context": ["NEW_SESSION"] }, "session_link_prefix": { @@ -21,14 +21,26 @@ }, "media_rate": { "path": "/core/session/media_rate", - "default_value": 24.0, - "description": "Default media rate.", - "value": 24.0, - "minimum": 1.0, - "maximum": 120.0, - "datatype": "double", - "context": ["NEW_SESSION"] + "default_value": "24.0", + "description": "Default media rate for new sessions - sets the timeline frame rate and play rate of frame based media.", + "value": "24.0", + "context": ["NEW_SESSION"], + "datatype": "multichoice string", + "options": ["23.976", "24.0", "25.0", "29.97", "30.0", "48.0", "50.0", "59.94", "60.0", "90.0", "100.0", "119.88", "120.0"], + "category": "General", + "display_name": "Default media rate for new sessions." }, + "partial_sequence_behaviour": { + "path": "/core/session/partial_sequence_behaviour", + "default_value": "Hold Frames", + "description": "When loading a sequence of images, if sequential numbered frames are missing, this setting decides if the sequence is compacted into only the frames on disk, or whether it fills missing numbered frames with blanks, or whether it uses 'hold frame' behaviour", + "value": "Hold Frames", + "datatype": "multichoice string", + "context": ["APPLICATION"], + "category": "General", + "display_name": "Partial Sequence Behaviour", + "options": ["Insert Blank Frames", "Hold Frames", "Skip Missing Frames"] + }, "compression": { "path": "/core/session/compression", "default_value": false, @@ -45,6 +57,27 @@ "datatype": "bool", "context": ["APPLICATION"] }, + "pushed_media_playlist_behaviour": { + "path": "/core/session/pushed_media_playlist_behaviour", + "default_value": "Push to Named Playlist", + "description": "When media is pushed to xstudio by something external (e.g. command line load when an xstudio session is running), this option let's you decide if you want the media to be added to the current playlist that you are viewing or to a named playlist.", + "value": "Push to Named Playlist", + "datatype": "multichoice string", + "context": ["APPLICATION"], + "category": "General", + "display_name": "Pushed Media Destination", + "options": ["Push to Named Playlist", "Push to Current Playlist"] + }, + "pushed_media_playlist_name": { + "path": "/core/session/pushed_media_playlist_name", + "default_value": "Pushed Media", + "description": "Set the name of the playlist to which media is added if it is pushed by an external process.", + "value": "Added Media", + "datatype": "string", + "context": ["APPLICATION"], + "category": "General", + "display_name": "Pushed Media Playlist Name" + }, "media_flags": { "path": "/core/session/media_flags", "description": "Media flag names.", @@ -80,6 +113,8 @@ "description": "Enable autosave of session.", "value": true, "datatype": "bool", + "display_name": "Enable Autosave", + "category": "General", "context": ["APPLICATION"] }, "interval": { diff --git a/share/preference/core_sync.json b/share/preference/core_sync.json deleted file mode 100644 index ec3d8c21d..000000000 --- a/share/preference/core_sync.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "core": { - "sync":{ - "enabled": { - "path": "/core/sync/enabled", - "default_value": false, - "description": "Enable external SYNC API port.", - "value": false, - "datatype": "bool", - "context": ["APPLICATION"] - }, - "port_minimum": { - "path": "/core/sync/port_minimum", - "default_value": 12346, - "description": "SYNC minimum port number.", - "value": 45130, - "minimum": 0, - "maximum": 65535, - "datatype": "int", - "context": ["APPLICATION"] - }, - "port_maximum": { - "path": "/core/sync/port_maximum", - "default_value": 12346, - "description": "SYNC maximum port number.", - "value": 45130, - "minimum": 0, - "maximum": 65535, - "datatype": "int", - "context": ["APPLICATION"] - }, - "bind_address": { - "path": "/core/sync/bind_address", - "default_value": "127.0.0.1", - "description": "IP to bind against.", - "value": "127.0.0.1", - "datatype": "string", - "context": ["APPLICATION"] - } - } - } -} \ No newline at end of file diff --git a/share/preference/plugin_basic_masking.json b/share/preference/plugin_basic_masking.json index 6b9a6471f..1a927a8ae 100644 --- a/share/preference/plugin_basic_masking.json +++ b/share/preference/plugin_basic_masking.json @@ -57,11 +57,19 @@ "datatype": "bool", "context": ["APPLICATION"] }, - "mask_selection": { - "path": "/plugin/basic_masking/mask_selection", - "default_value": "Off", + "mask_enabled": { + "path": "/plugin/basic_masking/mask_enabled", + "default_value": false, "description": "Sets the mask on / off", - "value": "Off", + "value": false, + "datatype": "bool", + "context": ["APPLICATION"] + }, + "mask_render_method": { + "path": "/plugin/basic_masking/mask_render_method", + "default_value": "OpenGL", + "description": "Sets whether QML or OpenGL is used to draw the mask", + "value": "OpenGL", "datatype": "string", "context": ["APPLICATION"] } diff --git a/share/preference/plugin_colour_pipeline_ocio.json b/share/preference/plugin_colour_pipeline_ocio.json index 7b970f331..e063eaecd 100644 --- a/share/preference/plugin_colour_pipeline_ocio.json +++ b/share/preference/plugin_colour_pipeline_ocio.json @@ -18,19 +18,19 @@ "datatype": "string", "context": ["APPLICATION"] }, - "user_preferred_view": { - "path": "/plugin/colour_pipeline/ocio/user_preferred_view", + "preferred_view": { + "path": "/plugin/colour_pipeline/ocio/preferred_view", "default_value": "Default", "description": "User preferred ocio view.", "value": "Default", "datatype": "string", "context": ["APPLICATION"] }, - "user_view_mode": { - "path": "/plugin/colour_pipeline/ocio/user_view_mode", - "default_value": true, + "global_view_mode": { + "path": "/plugin/colour_pipeline/ocio/global_view_mode", + "default_value": false, "description": "User view mode (global if true, per media otherwise).", - "value": true, + "value": false, "datatype": "bool", "context": ["APPLICATION"] }, @@ -41,22 +41,6 @@ "value": true, "datatype": "bool", "context": ["APPLICATION"] - }, - "enable_gamma": { - "path": "/plugin/colour_pipeline/ocio/enable_gamma", - "default_value": true, - "description": "Enable the gamma control in the viewport toolbar.", - "value": false, - "datatype": "bool", - "context": ["APPLICATION"] - }, - "enable_saturation": { - "path": "/plugin/colour_pipeline/ocio/enable_saturation", - "default_value": true, - "description": "Enable the saturation control in the viewport toolbar.", - "value": false, - "datatype": "bool", - "context": ["APPLICATION"] } } } diff --git a/share/preference/plugin_data_source_shotbrowser.json b/share/preference/plugin_data_source_shotbrowser.json deleted file mode 100644 index 2f9f841f2..000000000 --- a/share/preference/plugin_data_source_shotbrowser.json +++ /dev/null @@ -1,2449 +0,0 @@ -{ - "plugin": { - "data_source": { - "shotbrowser": { - "authentication": { - "client_id": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "", - "description": "Client Id.", - "path": "/plugin/data_source/shotbrowser/authentication/client_id", - "value": "" - }, - "client_secret": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "", - "description": "Client Secret.", - "path": "/plugin/data_source/shotbrowser/authentication/client_secret", - "value": "" - }, - "grant_type": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "password", - "description": "Authentication method.", - "path": "/plugin/data_source/shotbrowser/authentication/grant_type", - "value": "password" - }, - "password": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "", - "description": "Authentication password.", - "path": "/plugin/data_source/shotbrowser/authentication/password", - "value": "" - }, - "refresh_token": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "", - "description": "Authentication refresh token.", - "path": "/plugin/data_source/shotbrowser/authentication/refresh_token", - "value": "" - }, - "session_token": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "", - "description": "Authentication session_token.", - "path": "/plugin/data_source/shotbrowser/authentication/session_token", - "value": "" - }, - "username": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "${USER}", - "description": "Authentication Username.", - "path": "/plugin/data_source/shotbrowser/authentication/username", - "value": "${USER}" - } - }, - "download": { - "path": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "${HOME}/xStudio/shotbrowser_cache", - "description": "Path to shotbrowser download cache.", - "path": "/plugin/data_source/shotbrowser/download/path", - "value": "${TMPDIR}/${USER}/xStudio/shotbrowser_cache" - }, - "size": { - "context": [ - "APPLICATION" - ], - "datatype": "int", - "default_value": 5, - "description": "Cache size in GBytes.", - "path": "/plugin/data_source/shotbrowser/download/size", - "value": 5 - } - }, - - "note_history": { - "scope": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "", - "description": "Note history scope.", - "path": "/plugin/data_source/shotbrowser/note_history/scope", - "value": "" - }, - "type": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "", - "description": "Note history type.", - "path": "/plugin/data_source/shotbrowser/note_history/type", - "value": "" - } - - }, - "shot_history": { - "scope": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "", - "description": "Shot history scope.", - "path": "/plugin/data_source/shotbrowser/shot_history/scope", - "value": "" - } - - }, - "browser": { - "location": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "${DNSITEDATA_SHORT_NAME}", - "description": "Location. *NOT USED CURRENTLY*", - "path": "/plugin/data_source/shotbrowser/browser/location", - "value": "${DNSITEDATA_SHORT_NAME}" - }, - "show_hidden": { - "context": [ - "APPLICATION" - ], - "datatype": "bool", - "default_value": false, - "description": "Show hidden presets/groups", - "path": "/plugin/data_source/shotbrowser/browser/show_hidden", - "value": false - }, - "category": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "Tree", - "description": "Current category", - "path": "/plugin/data_source/shotbrowser/browser/category", - "value": "Tree" - }, - "maximum_result_count": { - "context": [ - "APPLICATION" - ], - "datatype": "int", - "default_value": 1000, - "description": "Maximum results returned.*NOT USED CURRENTLY*", - "maximum": 4999, - "minimum": 50, - "path": "/plugin/data_source/shotbrowser/browser/maximum_result_count", - "value": 1000 - }, - "project_id": { - "context": [ - "APPLICATION" - ], - "datatype": "int", - "default_value": -1, - "description": "Project id.", - "path": "/plugin/data_source/shotbrowser/browser/project_id", - "value": 329 - }, - "pipestep": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [], - "description": "Default pipesteps.", - "path": "/plugin/data_source/shotbrowser/browser/pipestep", - "value": [ - { - "name": "Anim" - }, - { - "name": "Body Track" - }, - { - "name": "Camera Track" - }, - { - "name": "Comp" - }, - { - "name": "Creature" - }, - { - "name": "Creature FX" - }, - { - "name": "Crowd" - }, - { - "name": "DMP" - }, - { - "name": "Editorial" - }, - { - "name": "Environ" - }, - { - "name": "Envsetup" - }, - { - "name": "FX" - }, - { - "name": "Groom" - }, - { - "name": "Layout" - }, - { - "name": "Lighting" - }, - { - "name": "Look Dev" - }, - { - "name": "Model" - }, - { - "name": "Muscle" - }, - { - "name": "Postvis" - }, - { - "name": "Prep" - }, - { - "name": "Previs" - }, - { - "name": "Retime Layout" - }, - { - "name": "Rig" - }, - { - "name": "Roto" - }, - { - "name": "Scan" - }, - { - "name": "Shot Sculpt" - }, - { - "name": "Skin" - }, - { - "name": "Sweatbox" - }, - { - "name": "TD" - }, - { - "name": "None" - } - ] - } - }, - "note_publishing": { - "note_publish_settings": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": { - "__ignore__": true, - "addFrame": false, - "addPlaylistName": true, - "addType": false, - "combine": false, - "defaultType": "", - "ignoreEmpty": false, - "notifyCreator": true, - "skipAlreadyPublished": false - }, - "description": "Prefs relating to note publishing.", - "path": "/plugin/data_source/shotbrowser/note_publishing/note_publish_settings", - "value": { - "__ignore__": true, - "addFrame": false, - "addPlaylistName": true, - "addType": false, - "combine": false, - "defaultType": "", - "ignoreEmpty": false, - "notifyCreator": true, - "skipAlreadyPublished": false - } - } - }, - "server": { - "host": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "shotgun.dneg.com", - "description": "Shotgun host.", - "path": "/plugin/data_source/shotbrowser/server/host", - "value": "shotgun.dneg.com" - }, - "port": { - "context": [ - "APPLICATION" - ], - "datatype": "int", - "default_value": 0, - "description": "Shotgun host port.", - "path": "/plugin/data_source/shotbrowser/server/port", - "value": 0 - }, - "protocol": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "https", - "description": "Connection protocol.", - "path": "/plugin/data_source/shotbrowser/server/protocol", - "value": "http" - }, - "timeout": { - "context": [ - "APPLICATION" - ], - "datatype": "int", - "default_value": 120, - "description": "Connection timeout.", - "path": "/plugin/data_source/shotbrowser/server/timeout", - "value": 120 - } - }, - "project_presets": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [], - "description": "Project presets.", - "path": "/plugin/data_source/shotbrowser/project_presets", - "value": null - }, - "site_presets": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "4c9c33b0-ba33-4108-b3e4-0bfcd270a523", - "livelink": null, - "negated": null, - "term": "Flag Media", - "type": "term", - "value": "Orange" - }, - { - "enabled": true, - "id": "52f31750-6595-4a5e-9647-bfb6585e84ad", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/2D" - }, - { - "enabled": true, - "id": "4ea3fca3-4d9e-4a0b-b9b4-7f8f2d715dc3", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/out" - }, - { - "enabled": true, - "id": "de3e5274-87f8-4029-9a99-6b1958236fb4", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/cg" - }, - { - "enabled": true, - "id": "46c178e0-578a-4fde-aec8-74fb838428c4", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/element" - }, - { - "enabled": true, - "id": "f4ec7b44-3428-43df-a48f-56f41f77dcd7", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/playblast" - }, - { - "enabled": true, - "id": "ced576a8-ccfb-4c2a-bd99-b886e701c4a5", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/playblast/working" - } - ], - "id": "119bc765-02e8-4da8-a6d8-db335e0461ef", - "name": "Override", - "type": "preset", - "update": false - }, - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "93c68a66-da6e-4849-8b34-1b13eb44cf2a", - "livelink": true, - "negated": null, - "term": "Twig Name", - "type": "term", - "value": "" - } - ], - "hidden": false, - "id": "b8d1c72e-c885-41fd-9187-6f3f5afaa01b", - "name": "Stream", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "79582aa7-9585-409e-b878-3e4b6515113f", - "livelink": true, - "negated": null, - "term": "Shot", - "type": "term", - "value": "" - }, - { - "enabled": true, - "id": "51e0b692-9434-4dea-8885-1f1e1be11df4", - "livelink": true, - "negated": null, - "term": "Pipeline Step", - "type": "term", - "value": "" - }, - { - "enabled": true, - "id": "2e28f743-0141-4b96-b79a-e217aa258f2e", - "livelink": null, - "negated": null, - "term": "Latest Version", - "type": "term", - "value": "True" - } - ], - "hidden": false, - "id": "8e140a38-08e5-49c2-b473-4c17ff318527", - "name": "Step", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "a5f6a949-f92b-4836-a44b-7fccafa68a87", - "livelink": true, - "negated": null, - "term": "Shot", - "type": "term", - "value": "" - }, - { - "enabled": true, - "id": "80bd21cd-1ef2-4452-95f0-87ed70ce2045", - "livelink": null, - "negated": null, - "term": "Latest Version", - "type": "term", - "value": "True" - } - ], - "hidden": false, - "id": "e33d0a5f-7040-49fb-93d9-97995f9ab3b1", - "name": "Shot", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "72a8be72-5237-415f-82dd-f833b299a2a4", - "livelink": true, - "negated": null, - "term": "Shot", - "type": "term", - "value": "" - }, - { - "enabled": true, - "id": "814e6340-ae7e-4976-ae96-0bdc2c689b9b", - "livelink": null, - "negated": null, - "term": "Sent To Dailies", - "type": "term", - "value": "True" - } - ], - "hidden": false, - "id": "b68e3a3b-7f3b-4add-a4cf-66113d0b6a0a", - "name": "Dailies", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "71127382-aee9-4a02-b79a-cba6b50fe6ff", - "livelink": true, - "negated": null, - "term": "Shot", - "type": "term", - "value": "" - }, - { - "enabled": true, - "id": "15e83a61-6fbf-41ab-8625-982f26eb70cd", - "livelink": null, - "negated": null, - "term": "Sent To Client", - "type": "term", - "value": "True" - } - ], - "hidden": false, - "id": "71527cdc-2cc5-405f-9d59-be587037528f", - "name": "Client", - "type": "preset", - "update": false - } - ], - "id": "c5ce1db6-dac0-4481-a42b-202e637ac819", - "type": "presets" - } - ], - "entity": "Versions", - "hidden": false, - "id": "4c512dae-e1e3-43b7-a02a-4fb7d93fde62", - "name": "Version Panel", - "type": "group", - "userdata": "menus" - }, - - { - "children": [ - { - "children": [], - "id": "d0b15051-275b-4601-bf6b-8d44ecd0fb4f", - "name": "Override", - "type": "preset", - "update": false - }, - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "dfd505c8-a394-42fc-8bed-138c24c141d9", - "livelink": true, - "negated": null, - "term": "Version Name", - "type": "term", - "value": "" - } - ], - "hidden": false, - "id": "5d3d87ad-ea87-4349-bb2e-f6baea908486", - "name": "Current", - "type": "preset", - "update": false, - "userdata": "scope" - }, - { - "children": [ - { - "enabled": true, - "id": "e47db10a-7d8f-4f9b-8e22-c79ec716602f", - "livelink": true, - "negated": null, - "term": "Twig Name", - "type": "term", - "value": "" - } - ], - "hidden": false, - "id": "12ca841f-b6e5-4ffa-8345-f8843588f84f", - "name": "Stream", - "type": "preset", - "update": false, - "userdata": "scope" - }, - { - "children": [ - { - "enabled": true, - "id": "a9933769-247b-4169-9604-66ab1c14f74f", - "livelink": true, - "negated": null, - "term": "Shot", - "type": "term", - "value": "" - }, - { - "enabled": true, - "id": "8edd718f-5fee-42b3-a889-bd0363efb33d", - "livelink": true, - "negated": null, - "term": "Pipeline Step", - "type": "term", - "value": "" - } - ], - "hidden": false, - "id": "636667ac-d691-4934-be2a-78cf169bb377", - "name": "Step", - "type": "preset", - "update": false, - "userdata": "scope" - }, - { - "children": [ - { - "enabled": true, - "id": "af3aef89-f132-48f3-afd9-308732b29e3b", - "livelink": true, - "negated": null, - "term": "Shot", - "type": "term", - "value": "" - } - ], - "hidden": false, - "id": "facaba43-3e3c-4f56-8c40-74d8f9c05987", - "name": "Shot", - "type": "preset", - "update": false, - "userdata": "scope" - }, - { - "children": [ - { - "enabled": true, - "id": "79fc54d2-8835-49a8-9430-3001c08b7f97", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "Artist" - }, - { - "enabled": true, - "id": "d4c76cd3-4d2f-4a97-ab23-087e925b9231", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "Submission" - }, - { - "enabled": true, - "id": "1b4b193a-0eba-4d5d-807d-4d02c7238b54", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "Wizard Dailies" - } - ], - "hidden": false, - "id": "0543a552-2cd5-41d6-87ee-0df879fdfb87", - "name": "Artist", - "type": "preset", - "update": false, - "userdata": "type" - }, - { - "children": [ - { - "enabled": true, - "id": "5aa514f8-fe7b-4e15-8bcf-d0b2a0e58d63", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "DeptSupe" - } - ], - "hidden": false, - "id": "4e9a9127-9591-4056-af99-c160c854812f", - "name": "Dept", - "type": "preset", - "update": false, - "userdata": "type" - }, - { - "children": [ - { - "enabled": true, - "id": "7dd3282d-703a-4738-b94b-9c9b37759cd7", - "livelink": true, - "negated": null, - "term": "Version Name", - "type": "term", - "value": "" - }, - { - "enabled": true, - "id": "e84965d2-e17d-47df-ada2-b328b904be7e", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "2DSupe" - }, - { - "enabled": true, - "id": "f39536f6-35ea-488c-b026-f6868311b025", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "Anim DIR" - }, - { - "enabled": true, - "id": "63711d12-0709-4421-936f-21249ee4e773", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "CgSupe" - }, - { - "enabled": true, - "id": "65d69f6f-2934-4dd3-a509-9fcc175000b7", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "DFXSupe" - }, - { - "enabled": true, - "id": "b7fe8e27-e6a5-44c0-bbb8-0f3f31046af0", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "DeptSupe" - }, - { - "enabled": true, - "id": "f3c8f2b8-b1eb-473d-9f32-66facdb469a7", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "VFXSupe" - } - ], - "hidden": false, - "id": "1c9de6ef-f9b0-48b6-a26f-30d4a66950db", - "name": "Supes", - "type": "preset", - "update": false, - "userdata": "type" - }, - { - "children": [ - { - "enabled": true, - "id": "540f9873-8f26-424d-ac6c-d9c297caa128", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "Client Editorial" - }, - { - "enabled": true, - "id": "bc2fd877-604b-498b-aa0d-1f6b7b0354d7", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "Editorial" - }, - { - "enabled": true, - "id": "22bb99f3-82ea-4112-92c8-b2860ed496cd", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "Editorial Query" - } - ], - "hidden": false, - "id": "31980c5d-1d88-4a9f-adbd-f6639b62faf1", - "name": "Editorial", - "type": "preset", - "update": false, - "userdata": "type" - }, - { - "children": [ - { - "enabled": true, - "id": "682ba215-44b2-4b9a-982f-3b10577af11e", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "Client" - }, - { - "enabled": true, - "id": "5e32fd1f-3eff-4cce-8fb2-339e62a5e69f", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "Client Stereo" - }, - { - "enabled": true, - "id": "596c4077-08f2-43c2-b547-8d9c649edf0d", - "livelink": null, - "negated": null, - "term": "Note Type", - "type": "term", - "value": "Director" - } - ], - "hidden": false, - "id": "5cc27243-80e7-4a9c-8166-cb0ebf970d29", - "name": "Client", - "type": "preset", - "update": false, - "userdata": "type" - } - ], - "id": "aac8207e-129d-4988-9e05-b59f75ae2f75", - "type": "presets" - } - ], - "entity": "Notes", - "hidden": false, - "id": "28612cf7-a814-4714-a4eb-443126cf0cd4", - "name": "Note History", - "type": "group", - "userdata": "menus" - }, - - - - - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "9372e556-c42e-499e-b1ae-5b99d69bc66a", - "livelink": null, - "negated": null, - "term": "Result Limit", - "type": "term", - "value": "1000" - }, - { - "enabled": true, - "id": "486d3f78-7694-4b14-b181-af1604ac05e1", - "livelink": null, - "negated": null, - "term": "Twig Type", - "type": "term", - "value": "render/out" - }, - { - "enabled": true, - "id": "83a894a6-7537-42d2-befd-e2fdc5fa5f4a", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/playblast" - }, - { - "enabled": true, - "id": "c25c821a-fb02-40b8-86ba-8b489c75349e", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/playblast/working" - }, - { - "enabled": true, - "id": "0f3cae7e-3728-433f-8f90-fa3e13d3a16e", - "livelink": null, - "negated": null, - "term": "Preferred Visual", - "type": "term", - "value": "movie_dneg" - } - ], - "hidden": false, - "id": "3446efbd-3cc5-4af4-8c99-bde94b495102", - "name": "OVERRIDE", - "type": "preset", - "update": false - }, - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "ee92cf04-8a4f-44a5-a1dc-979b4f488453", - "livelink": null, - "negated": null, - "term": "Lookback", - "type": "term", - "value": "20 Days" - }, - { - "enabled": true, - "id": "6d561adb-fbbc-4909-be32-145ae6c380c5", - "livelink": null, - "negated": null, - "term": "Latest Version", - "type": "term", - "value": "True" - } - ], - "hidden": false, - "id": "42a40bab-16d4-4490-8aab-22748f53f090", - "name": "Latest Dailies", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "bc45e233-909e-4f33-b12f-5b4efa0911cc", - "livelink": null, - "negated": null, - "term": "Lookback", - "type": "term", - "value": "3 Days" - }, - { - "enabled": true, - "id": "47b50922-4204-4fe5-8330-b03eb3cafc15", - "livelink": null, - "negated": null, - "term": "Latest Version", - "type": "term", - "value": "True" - }, - { - "enabled": true, - "id": "00121f88-b078-4645-b773-e98f050cbdb4", - "livelink": null, - "negated": null, - "term": "Sent To Client", - "type": "term", - "value": "True" - } - ], - "hidden": false, - "id": "6d077c45-27ae-4da0-b31b-1d028e46c6e0", - "name": "Latest Client Sends", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "46d06a5a-4c6e-4ec6-9a00-f7718e38a41a", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "turnover" - }, - { - "enabled": true, - "id": "dc45de84-3a4f-4469-ac8d-b0a37f43f5a9", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "data/clip/cut" - }, - { - "enabled": true, - "id": "80403019-742c-49c5-842f-8c8c16e2eec9", - "livelink": null, - "negated": null, - "term": "Latest Version", - "type": "term", - "value": "True" - } - ], - "hidden": false, - "id": "6d306c69-c3ba-4ea5-adce-a52e168254d9", - "name": "Latest Turnover Cuts", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "f3d7fa7a-0fe3-42f0-8c0d-610d81ca00db", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "data/clip/cut" - }, - { - "enabled": true, - "id": "514637eb-2759-4a74-a059-8cb64e575180", - "livelink": null, - "negated": null, - "term": "Latest Version", - "type": "term", - "value": "True" - }, - { - "enabled": true, - "id": "df9a05e6-5a16-4c0c-b93a-447af08f2975", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "minicut" - } - ], - "hidden": false, - "id": "6c57f8b9-28fc-4651-ba10-fb9990a29c89", - "name": "Latest Minicut Outputs", - "type": "preset", - "update": false - } - ], - "id": "7d85dd2d-753c-4063-a31d-99a5d7095608", - "type": "presets" - } - ], - "entity": "Versions", - "hidden": false, - "id": "4689c10f-eb27-4e16-8164-468cdd69142e", - "name": "Test", - "type": "group", - "update": false, - "userdata": "recent" - }, - - - - - - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "94b375f0-ebf4-4db5-bc97-07bccad3ac22", - "livelink": null, - "negated": null, - "term": "Preferred Visual", - "type": "term", - "value": "movie_dneg" - } - ], - "hidden": false, - "id": "b4e83342-71dd-45df-8ec2-3f5b74c6f03f", - "name": "Bob", - "type": "preset", - "update": false - }, - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "d3e87349-cd54-46d0-9239-d6c60c39f5b2", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/out" - }, - { - "enabled": true, - "id": "73c207cb-5b4d-487e-9056-ad78fa46563f", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/playblast" - }, - { - "enabled": true, - "id": "28fd9fb9-8abc-495e-b905-9e78298e44cd", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/playblast/working" - }, - { - "enabled": true, - "id": "3b5931bd-fd27-4251-bc5d-35003b4a026a", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/2D" - }, - { - "enabled": false, - "id": "336935bb-185a-4e7f-bdc6-580a4693fc7b", - "livelink": null, - "negated": null, - "term": "Latest Version", - "type": "term", - "value": "True" - } - ], - "hidden": false, - "id": "f7149213-e490-464e-9f42-7ddf42c81130", - "name": "Outputs", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "02467636-ea64-4b7d-97ce-427d00a3671a", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/cg" - }, - { - "enabled": true, - "id": "da081a3a-1376-42d2-aad9-7e88488d037d", - "livelink": null, - "negated": null, - "term": "Preferred Visual", - "type": "term", - "value": "main_proxy0" - }, - { - "enabled": true, - "id": "030e1758-b336-461a-8c39-329c8de3a635", - "livelink": null, - "negated": null, - "term": "Latest Version", - "type": "term", - "value": "True" - } - ], - "hidden": false, - "id": "fbaece6c-e91d-4aac-8a9a-56a99748d4bf", - "name": "CG Renders", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "276bdec5-7e52-4f04-b946-2e9666b67581", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/element" - }, - { - "enabled": false, - "id": "1e6fa2a9-37e1-46e9-b60f-7e715d19e365", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/2D" - }, - { - "enabled": false, - "id": "29ad89f0-354c-442d-9db2-1120a8d2fa86", - "livelink": null, - "negated": null, - "term": "Latest Version", - "type": "term", - "value": "True" - }, - { - "enabled": true, - "id": "2008703e-c1f6-49ee-8724-32a15335290b", - "livelink": null, - "negated": null, - "term": "Preferred Visual", - "type": "term", - "value": "main_proxy0" - } - ], - "hidden": false, - "id": "e2be6fbd-98ec-41c5-8e6a-e77ebcc2e4e3", - "name": "2D Elements", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "d5685b81-7ba7-427c-8160-b074fc421c11", - "livelink": false, - "negated": false, - "term": "Pipeline Step", - "type": "term", - "value": "Camera Track" - }, - { - "enabled": true, - "id": "22513f5e-fda3-4847-a98d-c31ebdd48c7a", - "livelink": false, - "negated": false, - "term": "Pipeline Step", - "type": "term", - "value": "Body Track" - }, - { - "enabled": true, - "id": "250c79b6-38d0-43d9-9256-4e32e962dd35", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/2D" - }, - { - "enabled": true, - "id": "3824f198-c584-4d05-99b4-8d894b886703", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/out" - }, - { - "enabled": true, - "id": "85c90ddf-4eee-4f02-86cd-2388387340a6", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/playblast" - }, - { - "enabled": true, - "id": "69ab5a89-f5f5-47e4-812c-177244331f76", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/playblast/working" - } - ], - "hidden": false, - "id": "46df7ee1-8ce2-490d-9f63-5b74b5856330", - "name": "Camera / Body Tracks", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "ad5c2fd4-0119-4c92-9044-fbee536c450c", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/2D" - }, - { - "enabled": true, - "id": "370599f0-0a32-4065-baae-44ec1ceb11bc", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/out" - }, - { - "enabled": true, - "id": "ec2f5f5b-77e4-432a-ba2e-4c260b2af6ec", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/playblast" - }, - { - "enabled": true, - "id": "f78aaacf-8dc7-4193-b6c3-11aae92e90ce", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/playblast/working" - }, - { - "enabled": true, - "id": "77f80cc2-ced1-4ac6-bd03-e8280bfc7fd1", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "retime" - }, - { - "enabled": true, - "id": "a736147a-0cd3-42cc-afb3-122a866a5344", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "repo" - } - ], - "hidden": false, - "id": "bba9beff-f7cf-45fc-a68e-f4a7e7f1d830", - "name": "Retimes / Repos", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "cd108259-37cb-4f73-8e47-632d0a8d8ee3", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "^S_" - }, - { - "enabled": true, - "id": "0a06da43-4cdd-41f6-a9d6-910cf991804c", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "scan" - }, - { - "enabled": false, - "id": "b6955257-a1bd-4adc-81df-c83027e9b21c", - "livelink": false, - "negated": false, - "term": "Pipeline Step", - "type": "term", - "value": "Scan" - }, - { - "enabled": false, - "id": "9b914963-9004-48f5-bc38-a7632ed35828", - "livelink": false, - "negated": false, - "term": "Pipeline Step", - "type": "term", - "value": "Comp" - }, - { - "enabled": false, - "id": "fecaefce-3417-4983-939e-7b88192d72a4", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "retime" - }, - { - "enabled": false, - "id": "ce97af87-058c-43c8-a9df-4b0023e055be", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "repo" - }, - { - "enabled": false, - "id": "dac0cd95-d2ea-4bcd-b0cd-6cde30345d3a", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "^REF_" - } - ], - "hidden": false, - "id": "ea16bd8c-ad5f-4322-8898-a05554418474", - "name": "Plates", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "f2e6db19-0445-4da7-ad67-615c39dc25e9", - "livelink": null, - "negated": null, - "term": "Sent To Client", - "type": "term", - "value": "True" - }, - { - "enabled": false, - "id": "96801bd7-b6fb-4aa9-ac46-95602d68ab3f", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "scan" - }, - { - "enabled": false, - "id": "0b1b484e-e531-4368-a802-113dbb537f2a", - "livelink": false, - "negated": false, - "term": "Pipeline Step", - "type": "term", - "value": "Scan" - }, - { - "enabled": false, - "id": "30b585f3-3741-4172-830d-8527ac1b4dcb", - "livelink": false, - "negated": false, - "term": "Pipeline Step", - "type": "term", - "value": "Comp" - }, - { - "enabled": false, - "id": "639eb663-6544-4e1a-983f-e8c88de9bb06", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "retime" - }, - { - "enabled": false, - "id": "326accca-56ac-4dfc-892c-b160bb5a5903", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "repo" - } - ], - "hidden": false, - "id": "4cc534f4-3626-44c5-a558-5c3e28c1cb49", - "name": "Sent To Client", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "dc74d80b-9270-4576-9232-e425bbc2848a", - "livelink": null, - "negated": null, - "term": "Sent To Dailies", - "type": "term", - "value": "True" - }, - { - "enabled": false, - "id": "eed67278-1224-41be-8088-b7e4654781bd", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "scan" - }, - { - "enabled": false, - "id": "db92fc9c-5fb0-434b-ba92-bab530b8f249", - "livelink": false, - "negated": false, - "term": "Pipeline Step", - "type": "term", - "value": "Scan" - }, - { - "enabled": false, - "id": "b5d86070-806e-496c-b35b-76866277203f", - "livelink": false, - "negated": false, - "term": "Pipeline Step", - "type": "term", - "value": "Comp" - }, - { - "enabled": false, - "id": "a9d94a83-5557-4f6f-b834-dc01caf38712", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "retime" - }, - { - "enabled": false, - "id": "823bc899-a8d5-40e3-bb35-9c2cf57f1597", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "repo" - } - ], - "hidden": false, - "id": "5693511f-56a4-435a-8e8f-d7e2c632f218", - "name": "Sent To Dailies", - "type": "preset", - "update": false - } - ], - "id": "a07eab89-7672-4b9d-8559-57b1b6b20577", - "type": "presets" - } - ], - "entity": "Versions", - "hidden": false, - "id": "c8c5c2de-23ca-44ec-95c1-ea44384f43aa", - "name": "Test 2", - "type": "group", - "update": false, - "userdata": "tree" - }, - - - - - - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "5040dcfb-336c-4ac5-a607-4a0b98cacecb", - "livelink": null, - "negated": null, - "term": "Preferred Visual", - "type": "term", - "value": "movie_dneg" - }, - { - "enabled": true, - "id": "72ff3917-828c-44c8-a276-489b9a270bec", - "livelink": null, - "negated": null, - "term": "Result Limit", - "type": "term", - "value": "500" - } - ], - "id": "55009e65-58ee-41e5-9503-bf6fdb012551", - "name": "dsfasdfa", - "type": "preset", - "update": false - }, - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "43be422c-f0de-4eaa-b86d-c55eea7a9fe0", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "art" - }, - { - "enabled": true, - "id": "a89a15bb-3802-42d2-828e-31db97c1208f", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "concept_art" - } - ], - "hidden": false, - "id": "ebcc8123-4670-4535-8c87-37d380af0fda", - "name": "Concept Art", - "type": "preset", - "update": false, - "userdata": "" - }, - { - "children": [ - { - "enabled": false, - "id": "49b6edc4-913b-4533-bc2a-2d68dcf0829d", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "^VIDREF" - }, - { - "enabled": false, - "id": "159cc1aa-a19b-436c-a344-838168fa93c8", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "texture_ref" - }, - { - "enabled": false, - "id": "b6537194-c9d9-4a38-ab80-76e9e7389969", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "video_ref" - }, - { - "enabled": false, - "id": "b6bdf07d-cf37-4ae5-b0b2-4641abcd0ed8", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "video_ref_witness" - }, - { - "enabled": true, - "id": "87745bd4-9a57-4185-a00b-9bf97f832e95", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "witcam" - } - ], - "hidden": false, - "id": "24a25b0b-6099-4f9f-9fce-7ba7f3f5fdd0", - "name": "Witness Cams", - "type": "preset", - "update": false, - "userdata": "" - }, - { - "children": [ - { - "enabled": true, - "id": "5e292ff5-4c22-43ec-b1d3-c2471720ca19", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "^OSREF" - }, - { - "enabled": false, - "id": "d7f86bc5-0f5c-4895-bc08-72452346acdd", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "texture_ref" - }, - { - "enabled": false, - "id": "d8cb70e3-36aa-4cef-bec2-049b59c8f080", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "video_ref" - }, - { - "enabled": false, - "id": "d322cd37-b6b1-4f8f-9286-732dfc7d3e04", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "video_ref_witness" - } - ], - "hidden": false, - "id": "c1411e93-b27c-461a-8fbe-e56039fdd700", - "name": "On Set Ref", - "type": "preset", - "update": false, - "userdata": "" - }, - { - "children": [ - { - "enabled": false, - "id": "2aad577c-6496-4c99-bfd7-d6aaec5a7ddd", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "" - }, - { - "enabled": false, - "id": "1cddb1d7-9b2f-4c44-8957-1b7dc6eeaa28", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "texture_ref" - }, - { - "enabled": false, - "id": "b0f9d4d9-5614-4664-a26f-21981618e5bd", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "video_ref" - }, - { - "enabled": false, - "id": "f1402913-da27-4ba1-a18c-d651ffd48251", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "video_ref_witness" - }, - { - "enabled": true, - "id": "880fc59c-45f2-49f9-aead-8c82a3aa1c72", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "tex" - } - ], - "hidden": false, - "id": "4efb3ec4-a722-4064-8699-ffd681cedda7", - "name": "Texture Ref", - "type": "preset", - "update": false, - "userdata": "" - }, - { - "children": [ - { - "enabled": false, - "id": "1620b2ab-6572-4d6e-80c5-076486b3ff48", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "hdr" - }, - { - "enabled": false, - "id": "9345e2bd-76e3-4fa3-91d3-60e6918055ce", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "video_ref" - }, - { - "enabled": false, - "id": "1cd70537-1017-4e6f-9459-8fd35d3db7f1", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "video_ref_witness" - }, - { - "enabled": false, - "id": "4a5cbce3-22d0-4328-b57f-bf05baa359ac", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "tex" - }, - { - "enabled": true, - "id": "6245523c-b795-4cb5-8d16-3b5712850991", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "hdri" - } - ], - "hidden": false, - "id": "c6c5f383-fb47-4274-acac-be0aef3d1d1b", - "name": "HDRIs", - "type": "preset", - "update": false, - "userdata": "" - } - ], - "id": "9a28ca83-4092-492a-9d19-1d1398619f06", - "type": "presets" - } - ], - "entity": "Versions", - "hidden": false, - "id": "1318d120-a016-448f-8537-14b654fa430d", - "name": "New Group", - "type": "group", - "userdata": "tree" - }, - - - - - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "aa1c1d1e-4a7d-4edd-a243-36add2ab21fa", - "livelink": null, - "negated": null, - "term": "Preferred Visual", - "type": "term", - "value": "movie_dneg" - }, - { - "enabled": true, - "id": "6a6694aa-399c-4a95-9c66-cf49c747316d", - "livelink": null, - "negated": null, - "term": "Result Limit", - "type": "term", - "value": "500" - } - ], - "id": "9f349b78-e4de-43fb-b46d-31ea296cd751", - "name": "dsfasdfa", - "type": "preset", - "update": false - }, - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "6a48f16e-3d4d-45dc-81a1-49fc5b628341", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "turnover" - }, - { - "enabled": true, - "id": "8575b272-6b41-4144-8083-597c3bd3106b", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "edit_ref" - }, - { - "enabled": false, - "id": "19e7b086-3223-422b-ab15-739b3db34943", - "livelink": null, - "negated": null, - "term": "Latest Version", - "type": "term", - "value": "True" - } - ], - "hidden": false, - "id": "adaf07c1-d62e-43d0-8be3-645b5917005f", - "name": "Turnover & Edit Ref", - "type": "preset", - "update": false, - "userdata": "" - }, - { - "children": [ - { - "enabled": true, - "id": "9db20e65-5faf-4b4a-bac7-e1793f5e930e", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "minicut" - } - ], - "hidden": false, - "id": "7238c33d-f858-4228-a719-b6b3ace1d569", - "name": "Minicut Outputs", - "type": "preset", - "update": false, - "userdata": "" - }, - { - "children": [ - { - "enabled": true, - "id": "85bb3fa4-59cb-40a6-8215-7ee85423289a", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "previs" - } - ], - "hidden": false, - "id": "e1f64b1f-16a4-4ba3-ba0d-937618c4d9cb", - "name": "Previs", - "type": "preset", - "update": false, - "userdata": "" - }, - { - "children": [ - { - "enabled": true, - "id": "7e803de9-75bf-4ce3-8500-8c1961c5bd14", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "postvis" - }, - { - "enabled": true, - "id": "847cb248-b631-4729-b4a3-17f36b00b42e", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "sketchvis" - } - ], - "hidden": false, - "id": "c5ad64bb-51aa-4351-b04c-0445f03c789b", - "name": "Postvis", - "type": "preset", - "update": false, - "userdata": "" - }, - { - "children": [ - { - "enabled": true, - "id": "80af1ce6-d61d-4d60-8c2f-3d6f0f4ecd20", - "livelink": null, - "negated": false, - "term": "Filter", - "type": "term", - "value": "bidding" - } - ], - "hidden": false, - "id": "f633473a-6c46-4173-a264-955b2882b3b0", - "name": "Bidding QTs", - "type": "preset", - "update": false, - "userdata": "" - }, - { - "children": [ - { - "enabled": true, - "id": "3788b53e-fc31-48b7-89d0-bfed2104c97d", - "livelink": false, - "negated": false, - "term": "Pipeline Step", - "type": "term", - "value": "Editorial" - } - ], - "hidden": false, - "id": "77abb8be-4147-476b-ac4b-b39f18b18b6e", - "name": "All Editorial Outputs", - "type": "preset", - "update": false, - "userdata": "" - } - ], - "id": "b0c1b7f1-2f46-4655-93cc-89a61822326d", - "type": "presets" - } - ], - "entity": "Versions", - "hidden": false, - "id": "9731813e-81e8-4e05-9e16-6e4a1dee5d5f", - "name": "New Group", - "type": "group", - "userdata": "tree" - }, - - - - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "298e82bd-6abf-4867-b918-58a0290191ef", - "livelink": null, - "negated": null, - "term": "Preferred Visual", - "type": "term", - "value": "movie_dneg" - } - ], - "id": "4e62e3eb-e210-4d9d-9594-11ae45dc32df", - "name": "Notes", - "type": "preset", - "update": false - }, - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "b2aef648-349e-4c86-9a91-2433b04739e3", - "livelink": null, - "negated": false, - "term": "Note Type", - "type": "term", - "value": "Director" - }, - { - "enabled": true, - "id": "81977e1d-b8c8-4526-bb02-16f856d49fef", - "livelink": null, - "negated": false, - "term": "Note Type", - "type": "term", - "value": "Client" - }, - { - "enabled": true, - "id": "61ab46fd-3e1c-4867-b374-c2f6417ac09d", - "livelink": null, - "negated": false, - "term": "Note Type", - "type": "term", - "value": "Editorial" - } - ], - "hidden": false, - "id": "0304d825-0454-44d9-977e-e555d76a9557", - "name": "Client Notes", - "type": "preset", - "update": false, - "userdata": "" - }, - { - "children": [ - { - "enabled": true, - "id": "77d7dc65-5356-4401-91e0-61fb4a4afe55", - "livelink": null, - "negated": false, - "term": "Note Type", - "type": "term", - "value": "VFXSupe" - }, - { - "enabled": true, - "id": "e618a745-f3ba-47bc-af72-c676a22ed6b7", - "livelink": null, - "negated": false, - "term": "Note Type", - "type": "term", - "value": "DFXSupe" - }, - { - "enabled": true, - "id": "1fa45b53-12f4-48f9-832c-8f79163cc791", - "livelink": null, - "negated": false, - "term": "Note Type", - "type": "term", - "value": "CgSupe" - }, - { - "enabled": true, - "id": "afad2537-78e0-418d-bbc2-05b3179fa3f1", - "livelink": null, - "negated": false, - "term": "Note Type", - "type": "term", - "value": "Anim DIR" - } - ], - "hidden": false, - "id": "199124f0-faa9-4cba-b375-eacda8e303ca", - "name": "Internal Supes", - "type": "preset", - "update": false, - "userdata": "" - }, - { - "children": [ - { - "enabled": true, - "id": "434ec257-0d4f-4e68-ba72-fc5ab59d26bb", - "livelink": null, - "negated": false, - "term": "Note Type", - "type": "term", - "value": "Facility" - }, - { - "enabled": true, - "id": "552cc89f-3c20-4367-a68a-49d9169f81b0", - "livelink": null, - "negated": false, - "term": "Note Type", - "type": "term", - "value": "Submission" - } - ], - "hidden": false, - "id": "4f60137e-e7e4-4298-8b0c-5107e5c84c09", - "name": "Facility Notes", - "type": "preset", - "update": false, - "userdata": "" - }, - { - "children": [ - { - "enabled": true, - "id": "2b3356e8-8861-409e-87b4-6953ab2c2585", - "livelink": null, - "negated": false, - "term": "Note Type", - "type": "term", - "value": "Wizard Dailies" - } - ], - "hidden": false, - "id": "f0322215-8d82-4870-8974-b89c53897ffb", - "name": "Artist's Notes", - "type": "preset", - "update": false, - "userdata": "" - }, - { - "children": [ - { - "enabled": true, - "id": "214ed442-71b8-4e8a-b874-55c343e0a05c", - "livelink": false, - "negated": null, - "term": "Recipient", - "type": "term", - "value": "${USERFULLNAME}" - } - ], - "hidden": false, - "id": "46540248-cc0b-41e7-9e02-9d6eba4bfef6", - "name": "To Me", - "type": "preset", - "update": false, - "userdata": "" - }, - { - "children": [], - "hidden": false, - "id": "3604e5cb-dac5-4233-b775-9653e537c785", - "name": "All Notes", - "type": "preset", - "update": false, - "userdata": "" - } - ], - "id": "394165e3-859f-4218-bccc-c64afae20fd9", - "type": "presets" - } - ], - "entity": "Notes", - "hidden": false, - "id": "4262d5ac-7732-4e5a-9092-6bb31369d119", - "name": "New Group", - "type": "group", - "userdata": "tree" - }, - - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "f626afb0-73fb-4f89-bc0f-254811bf4d55", - "livelink": null, - "negated": null, - "term": "Preferred Visual", - "type": "term", - "value": "movie_dneg" - }, - { - "enabled": true, - "id": "da54cdfd-7a1e-40b1-baa9-14fa6c108864", - "livelink": null, - "negated": null, - "term": "Result Limit", - "type": "term", - "value": "500" - } - ], - "id": "52a9bf1a-814a-4cd7-a718-1f543adb67b4", - "name": "OVERRIDE", - "type": "preset", - "update": false - }, - { - "children": [ - { - "children": [ - { - "enabled": true, - "id": "fd9cc981-70b5-4a23-aafe-1a2f58504417", - "livelink": false, - "negated": false, - "term": "Twig Type", - "type": "term", - "value": "render/out" - }, - { - "enabled": true, - "id": "123ef315-68ae-4ebf-82eb-55e1136be03c", - "livelink": null, - "negated": null, - "term": "Latest Version", - "type": "term", - "value": "True" - }, - { - "enabled": true, - "id": "db5072d6-9590-4fde-b04c-29d8541aecad", - "livelink": false, - "negated": false, - "term": "Pipeline Step", - "type": "term", - "value": "Comp" - }, - { - "enabled": true, - "id": "", - "livelink": false, - "negated": false, - "term": "Twig Name", - "type": "term", - "value": "comp$" - } - ], - "hidden": false, - "id": "b4972154-cec3-43ce-b3a8-b5d3d20fcf07", - "name": "Latest Comp", - "type": "preset", - "update": false - }, - { - "children": [ - { - "enabled": true, - "id": "ebb8c80e-d9c4-4b0e-9e99-7ddb30fdfb3f", - "livelink": null, - "negated": null, - "term": "Lookback", - "type": "term", - "value": "3 Days" - } - ], - "hidden": false, - "id": "07e521ae-29a7-41ff-9a61-798352b6fad8", - "name": "Latest Dailies", - "type": "preset", - "update": false - }, - { - "children": [ - ], - "hidden": false, - "id": "d04ecb05-509a-4d33-8876-5af78fb06455", - "name": "Client Reviewed", - "type": "preset", - "update": false - } - ], - "id": "137aa66a-87e2-4c53-b304-44bd7ff9f755", - "type": "presets" - } - ], - - "entity": "Versions", - "hidden": false, - "id": "ef787e88-1b8f-4d89-bbc7-3ecf85987792", - "name": "Quick Load", - "type": "group", - "update": false, - "userdata": "tree" - } - - - ], - "description": "Site presets.", - "path": "/plugin/data_source/shotbrowser/site_presets", - "value": null - }, - "user_presets": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [], - "description": "User presets.", - "path": "/plugin/data_source/shotbrowser/user_presets", - "value": null - } - } - } - }, - "ui": { - "qml": { - "shotbrowser_settings": { - "context": [ - "QML_UI" - ], - "datatype": "json", - "default_value": {}, - "description": "Prefs relating to window position.", - "path": "/ui/qml/shotbrowser_settings", - "value": { - "__ignore__": true, - "height": 400, - "visibility": 0, - "width": 700, - "x": 100, - "y": 100 - } - } - } - } -} \ No newline at end of file diff --git a/share/preference/plugin_data_source_shotgun.json b/share/preference/plugin_data_source_shotgun.json deleted file mode 100644 index b78caa2e5..000000000 --- a/share/preference/plugin_data_source_shotgun.json +++ /dev/null @@ -1,2051 +0,0 @@ -{ - "plugin": { - "data_source": { - "shotgun": { - "disable_integration": { - "context": [ - "APPLICATION" - ], - "datatype": "bool", - "default_value": false, - "description": "Disable integration.", - "path": "/plugin/data_source/shotgun/disable_integration", - "value": false - }, - - "authentication": { - "client_id": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "", - "description": "Client Id.", - "path": "/plugin/data_source/shotgun/authentication/client_id", - "value": "" - }, - "client_secret": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "", - "description": "Client Secret.", - "path": "/plugin/data_source/shotgun/authentication/client_secret", - "value": "" - }, - "grant_type": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "password", - "description": "Authentication method.", - "path": "/plugin/data_source/shotgun/authentication/grant_type", - "value": "password" - }, - "password": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "", - "description": "Authentication password.", - "path": "/plugin/data_source/shotgun/authentication/password", - "value": "" - }, - "refresh_token": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "", - "description": "Authentication refresh token.", - "path": "/plugin/data_source/shotgun/authentication/refresh_token", - "value": "" - }, - "session_token": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "", - "description": "Authentication session_token.", - "path": "/plugin/data_source/shotgun/authentication/session_token", - "value": "" - }, - "username": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "${USER}", - "description": "Authentication Username.", - "path": "/plugin/data_source/shotgun/authentication/username", - "value": "${USER}" - } - }, - "context": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "Playlists", - "description": "Default context.", - "path": "/plugin/data_source/shotgun/context", - "value": "Playlists" - }, - "download": { - "path": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "${HOME}/xStudio/shotgun_cache", - "description": "Path to shotgun download cache.", - "path": "/plugin/data_source/shotgun/download/path", - "value": "${TMPDIR}/${USER}/xStudio/shotgun_cache" - }, - "size": { - "context": [ - "APPLICATION" - ], - "datatype": "int", - "default_value": 5, - "description": "Cache size in GBytes.", - "path": "/plugin/data_source/shotgun/download/size", - "value": 5 - } - }, - "global_filters": { - "edit": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "expanded": false, - "name": "edit Filter", - "queries": [] - } - ], - "description": "edit presets.", - "path": "/plugin/data_source/shotgun/global_filters/edit", - "value": null - }, - "media_action": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "expanded": false, - "name": "Media Filter", - "queries": [ - { - "enabled": false, - "livelink": false, - "term": "On Disk", - "value": "${DNSITEDATA_SHORT_NAME}" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/out" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/playblast" - }, - { - "enabled": true, - "term": "Preferred Visual", - "value": "movie_dneg" - }, - { - "enabled": true, - "term": "Result Limit", - "value": "10" - }, - { - "enabled": true, - "term": "Flag Media", - "value": "Orange" - } - ] - } - ], - "description": "Media Action presets.", - "path": "/plugin/data_source/shotgun/global_filters/media_action", - "value": null - }, - "note": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "expanded": false, - "name": "note Filter", - "queries": [ - { - "enabled": true, - "term": "Preferred Visual", - "value": "movie_dneg" - }, - { - "enabled": false, - "term": "Result Limit", - "value": "2500" - } - ] - } - ], - "description": "note presets.", - "path": "/plugin/data_source/shotgun/global_filters/note", - "value": null - }, - "playlist": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "expanded": false, - "name": "playlist Filter", - "queries": [ - { - "enabled": true, - "term": "Has Contents", - "value": "True" - }, - { - "enabled": true, - "term": "Order By", - "value": "Created DESC" - }, - { - "enabled": true, - "term": "Result Limit", - "value": "1000" - }, - { - "enabled": true, - "term": "Preferred Visual", - "value": "movie_dneg" - } - ] - } - ], - "description": "playlist presets.", - "path": "/plugin/data_source/shotgun/global_filters/playlist", - "value": null - }, - "reference": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "expanded": false, - "name": "reference Filter", - "queries": [ - { - "enabled": true, - "term": "Result Limit", - "value": "2500" - }, - { - "enabled": false, - "term": "On Disk", - "value": "${DNSITEDATA_SHORT_NAME}" - } - ] - } - ], - "description": "reference presets.", - "path": "/plugin/data_source/shotgun/global_filters/reference", - "value": null - }, - "shot": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "expanded": false, - "name": "Shot Filter", - "queries": [ - { - "enabled": false, - "term": "On Disk", - "value": "${DNSITEDATA_SHORT_NAME}" - }, - { - "enabled": true, - "term": "Preferred Visual", - "value": "movie_dneg" - }, - { - "enabled": true, - "term": "Result Limit", - "value": "2500" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/out" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/playblast" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/playblast/working" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "scan" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "data/clip/cut" - }, - { - "enabled": false, - "livelink": false, - "term": "Twig Type", - "value": "render/cg" - }, - { - "enabled": false, - "livelink": false, - "term": "Twig Type", - "value": "render/element" - } - ] - } - ], - "description": "Shot presets.", - "path": "/plugin/data_source/shotgun/global_filters/shot", - "value": null - } - }, - "location": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "${DNSITEDATA_SHORT_NAME}", - "description": "Location.", - "path": "/plugin/data_source/shotgun/location", - "value": "${DNSITEDATA_SHORT_NAME}" - }, - "maximum_result_count": { - "context": [ - "APPLICATION" - ], - "datatype": "int", - "default_value": 1000, - "description": "Maximum results returned.", - "maximum": 4999, - "minimum": 50, - "path": "/plugin/data_source/shotgun/maximum_result_count", - "value": 1000 - }, - "note_publish_settings": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": { - "__ignore__": true, - "addFrame": false, - "addPlaylistName": true, - "addType": false, - "combine": false, - "defaultType": "", - "ignoreEmpty": false, - "notifyCreator": true, - "skipAlreadyPublished": false - }, - "description": "Prefs relating to note publishing.", - "path": "/plugin/data_source/shotgun/note_publish_settings", - "value": { - "__ignore__": true, - "addFrame": false, - "addPlaylistName": true, - "addType": false, - "combine": false, - "defaultType": "", - "ignoreEmpty": false, - "notifyCreator": true, - "skipAlreadyPublished": false - } - }, - "pipestep": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [], - "description": "Default pipesteps.", - "path": "/plugin/data_source/shotgun/pipestep", - "value": [ - { - "name": "Anim" - }, - { - "name": "Body Track" - }, - { - "name": "Camera Track" - }, - { - "name": "Comp" - }, - { - "name": "Creature" - }, - { - "name": "Creature FX" - }, - { - "name": "Crowd" - }, - { - "name": "DMP" - }, - { - "name": "Editorial" - }, - { - "name": "Environ" - }, - { - "name": "Envsetup" - }, - { - "name": "FX" - }, - { - "name": "Groom" - }, - { - "name": "Layout" - }, - { - "name": "Lighting" - }, - { - "name": "Look Dev" - }, - { - "name": "Model" - }, - { - "name": "Muscle" - }, - { - "name": "Postvis" - }, - { - "name": "Prep" - }, - { - "name": "Previs" - }, - { - "name": "Retime Layout" - }, - { - "name": "Rig" - }, - { - "name": "Roto" - }, - { - "name": "Scan" - }, - { - "name": "Shot Sculpt" - }, - { - "name": "Skin" - }, - { - "name": "Sweatbox" - }, - { - "name": "TD" - }, - { - "name": "None" - } - ] - }, - "presets": { - "edit": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "expanded": false, - "name": "Approved Cuts", - "queries": [ - { - "enabled": true, - "term": "Lookback", - "value": "1 Day" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Trailers", - "queries": [ - { - "enabled": true, - "term": "Lookback", - "value": "7 Days" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "All Cuts", - "queries": [ - { - "enabled": true, - "term": "Lookback", - "value": "30 Days" - } - ], - "type": "system" - } - ], - "description": "Edit presets.", - "path": "/plugin/data_source/shotgun/presets/edit", - "value": null - }, - "media_action": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "expanded": false, - "name": "Hero Scan", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "term": "Is Hero", - "value": "True" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "scan" - }, - { - "enabled": true, - "term": "Twig Type", - "value": "render/element" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Scan Retime/Repo", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "term": "Twig Type", - "value": "render/element" - }, - { - "enabled": true, - "term": "Filter", - "value": "retime" - }, - { - "enabled": true, - "term": "Filter", - "value": "repo" - }, - { - "enabled": false, - "term": "Filter", - "value": "despil" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Edit Ref", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "data/clip/cut" - }, - { - "enabled": false, - "livelink": false, - "term": "Twig Name", - "value": "edit_ref$" - }, - { - "enabled": true, - "term": "Result Limit", - "value": "1" - }, - { - "enabled": true, - "term": "Order By", - "value": "Created DESC" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Latest Client Version", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - }, - { - "enabled": true, - "term": "Sent To Client", - "value": "True" - }, - { - "enabled": true, - "term": "Order By", - "value": "Client Submit DESC" - }, - { - "enabled": true, - "term": "Result Limit", - "value": "1" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Latest Camera Track", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "livelink": false, - "term": "Pipeline Step", - "value": "Camera Track" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "autoslap" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Latest Body Track", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "livelink": false, - "term": "Pipeline Step", - "value": "Body Track" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "autoslap" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Latest Layout", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - }, - { - "enabled": true, - "livelink": false, - "term": "Pipeline Step", - "value": "Layout" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "layout$" - }, - { - "enabled": true, - "term": "Filter", - "value": "^P_" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Latest Animation", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/out" - }, - { - "enabled": true, - "livelink": false, - "term": "Pipeline Step", - "value": "Anim" - }, - { - "enabled": true, - "term": "Filter", - "value": "^P_" - }, - { - "enabled": false, - "term": "Filter", - "value": "^WP_" - }, - { - "enabled": false, - "term": "Filter", - "value": "^O_" - }, - { - "enabled": true, - "term": "Result Limit", - "value": "1" - }, - { - "enabled": true, - "term": "Order By", - "value": "Created DESC" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/playblast" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Latest Crowd", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/cg" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/out" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/playblast" - }, - { - "enabled": false, - "livelink": false, - "term": "Twig Name", - "value": "cloth_slap" - }, - { - "enabled": false, - "livelink": false, - "term": "Twig Name", - "value": "creaturesculpt$" - }, - { - "enabled": false, - "livelink": false, - "term": "Twig Name", - "value": "skin_slap" - }, - { - "enabled": false, - "livelink": false, - "term": "Twig Name", - "value": "hair_slap" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "crowd_slap$" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Latest FX", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/out" - }, - { - "enabled": true, - "livelink": false, - "term": "Pipeline Step", - "value": "FX" - }, - { - "enabled": true, - "livelink": false, - "term": "Filter", - "value": "fx_slap" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Latest CFX", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/cg" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/out" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/playblast" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "cloth_slap" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "creaturesculpt$" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "skin_slap" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "hair_slap" - }, - { - "enabled": false, - "livelink": false, - "term": "Twig Name", - "value": "crowd_slap$" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Latest Environment", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/out" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - }, - { - "enabled": true, - "livelink": false, - "term": "Pipeline Step", - "value": "Environ" - }, - { - "enabled": true, - "term": "Result Limit", - "value": "1" - }, - { - "enabled": true, - "term": "Order By", - "value": "Created DESC" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "slap$" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "comp$" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Latest Lighting", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/out" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - }, - { - "enabled": true, - "livelink": false, - "term": "Pipeline Step", - "value": "Lighting" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "comp$" - }, - { - "enabled": true, - "term": "Result Limit", - "value": "1" - }, - { - "enabled": true, - "term": "Order By", - "value": "Created DESC" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "slap$" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Latest Comp", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Type", - "value": "render/out" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - }, - { - "enabled": true, - "livelink": false, - "term": "Pipeline Step", - "value": "Comp" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "comp$" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Next Version", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "livelink": true, - "term": "Twig Name", - "value": "^O_00TS_0020_comp_repo$" - }, - { - "enabled": true, - "livelink": true, - "term": "Twig Type", - "value": "render/playblast" - }, - { - "enabled": true, - "term": "On Disk", - "value": "${DNSITEDATA_SHORT_NAME}" - }, - { - "enabled": true, - "livelink": true, - "term": "Newer Version", - "value": "1" - }, - { - "enabled": true, - "term": "Order By", - "value": "Version ASC" - }, - { - "enabled": true, - "term": "Result Limit", - "value": "1" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Previous Version", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "livelink": true, - "term": "Twig Name", - "value": "^O_00TS_0020_comp_repo$" - }, - { - "enabled": true, - "livelink": true, - "term": "Twig Type", - "value": "render/playblast" - }, - { - "enabled": true, - "term": "On Disk", - "value": "${DNSITEDATA_SHORT_NAME}" - }, - { - "enabled": true, - "livelink": true, - "term": "Older Version", - "value": "1" - }, - { - "enabled": true, - "term": "Order By", - "value": "Version DESC" - }, - { - "enabled": true, - "term": "Result Limit", - "value": "1" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Latest Version", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "13VJ_1075" - }, - { - "enabled": true, - "livelink": true, - "term": "Twig Name", - "value": "^O_00TS_0030_comp_skinny$" - }, - { - "enabled": true, - "livelink": true, - "term": "Twig Type", - "value": "render/playblast" - }, - { - "enabled": true, - "term": "On Disk", - "value": "${DNSITEDATA_SHORT_NAME}" - }, - { - "enabled": true, - "livelink": true, - "term": "Newer Version", - "value": "5" - }, - { - "enabled": true, - "term": "Order By", - "value": "Version DESC" - }, - { - "enabled": true, - "term": "Result Limit", - "value": "1" - } - ], - "type": "system" - } - ], - "description": "Shot presets.", - "path": "/plugin/data_source/shotgun/presets/media_action", - "value": null - }, - "note": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "expanded": false, - "name": "Latest Notes", - "queries": [ - { - "enabled": true, - "term": "Lookback", - "value": "7 Days" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Supervisor Notes", - "queries": [ - { - "enabled": true, - "term": "Lookback", - "value": "7 Days" - }, - { - "enabled": true, - "term": "Note Type", - "value": "VFXSupe" - }, - { - "enabled": false, - "livelink": false, - "term": "Recipient", - "value": "${USERFULLNAME}" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Client Notes", - "queries": [ - { - "enabled": true, - "term": "Lookback", - "value": "7 Days" - }, - { - "enabled": true, - "term": "Note Type", - "value": "Client" - }, - { - "enabled": true, - "term": "Note Type", - "value": "Director" - }, - { - "enabled": false, - "livelink": false, - "term": "Recipient", - "value": "${USERFULLNAME}" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "My Notes", - "queries": [ - { - "enabled": true, - "livelink": false, - "term": "Recipient", - "value": "${USERFULLNAME}" - }, - { - "enabled": false, - "term": "Note Type", - "value": "Director" - }, - { - "enabled": false, - "term": "Note Type", - "value": "Client" - }, - { - "enabled": false, - "term": "Note Type", - "value": "VFXSupe" - }, - { - "enabled": false, - "term": "Note Type", - "value": "Anim DIR" - } - ], - "type": "system" - } - ], - "description": "Note presets.", - "path": "/plugin/data_source/shotgun/presets/note", - "value": null - }, - "note_tree": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "expanded": false, - "name": "All", - "queries": [ - { - "dynamic": true, - "enabled": true, - "term": "Shot", - "value": "" - } - ], - "type": "system" - } - ], - "description": "Note Tree presets.", - "path": "/plugin/data_source/shotgun/presets/note_tree", - "type": "system", - "value": null - }, - "playlist": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "expanded": false, - "name": "Latest Playlists", - "queries": [ - { - "enabled": true, - "term": "Lookback", - "value": "7 Days" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Dailies & Desk Reviews", - "queries": [ - { - "enabled": true, - "term": "Playlist Type", - "value": "Dailies" - }, - { - "enabled": true, - "term": "Playlist Type", - "value": "Desk Review" - }, - { - "enabled": true, - "term": "Lookback", - "value": "7 Days" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Client Playlists", - "queries": [ - { - "enabled": true, - "term": "Playlist Type", - "value": "Client Review" - }, - { - "enabled": true, - "term": "Playlist Type", - "value": "Cinesync" - }, - { - "enabled": false, - "term": "Playlist Type", - "value": "Client Send" - }, - { - "enabled": true, - "term": "Lookback", - "value": "30 Days" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Future Playlists", - "queries": [ - { - "enabled": true, - "term": "Lookback", - "value": "Future Only" - }, - { - "enabled": true, - "term": "Disable Global", - "value": "Has Contents" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Reference Playlists", - "queries": [ - { - "enabled": true, - "term": "Playlist Type", - "value": "Reference Playlist" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "My Playlists", - "queries": [ - { - "enabled": true, - "livelink": false, - "term": "Author", - "value": "${USERFULLNAME}" - } - ], - "type": "system" - } - ], - "description": "Playlist presets.", - "path": "/plugin/data_source/shotgun/presets/playlist", - "value": null - }, - "reference": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "expanded": false, - "name": "Concept Art", - "queries": [ - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "^ARTCON" - }, - { - "enabled": false, - "term": "Filter", - "value": "environ" - }, - { - "enabled": false, - "term": "Filter", - "value": "prop" - }, - { - "enabled": false, - "term": "Filter", - "value": "character" - }, - { - "enabled": false, - "term": "Filter", - "value": "vehicle" - }, - { - "enabled": false, - "term": "Sent To Client", - "value": "True" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Video Reference", - "queries": [ - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "^VIDREF" - }, - { - "enabled": false, - "term": "Filter", - "value": "environ" - }, - { - "enabled": false, - "term": "Filter", - "value": "prop" - }, - { - "enabled": false, - "term": "Filter", - "value": "character" - }, - { - "enabled": false, - "term": "Filter", - "value": "vehicle" - }, - { - "enabled": false, - "term": "Sent To Client", - "value": "True" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Texture Reference", - "queries": [ - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "^TEXREF" - }, - { - "enabled": false, - "term": "Filter", - "value": "environ" - }, - { - "enabled": true, - "term": "Filter", - "value": "prop" - }, - { - "enabled": false, - "term": "Filter", - "value": "character" - }, - { - "enabled": false, - "term": "Filter", - "value": "vehicle" - }, - { - "enabled": false, - "term": "Sent To Client", - "value": "True" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "On Set Reference", - "queries": [ - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "^OSREF" - }, - { - "enabled": false, - "term": "Filter", - "value": "environ" - }, - { - "enabled": false, - "term": "Filter", - "value": "prop" - }, - { - "enabled": false, - "term": "Filter", - "value": "character" - }, - { - "enabled": false, - "term": "Filter", - "value": "vehicle" - }, - { - "enabled": false, - "term": "Sent To Client", - "value": "True" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Witness Cameras", - "queries": [ - { - "enabled": false, - "livelink": false, - "term": "Twig Type", - "value": "video_ref_witness" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "^WITVIDREF" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Set Drawings", - "queries": [ - { - "enabled": false, - "livelink": false, - "term": "Twig Type", - "value": "set_drawing" - }, - { - "enabled": true, - "livelink": false, - "term": "Twig Name", - "value": "^ARTSET" - } - ], - "type": "system" - } - ], - "description": "Reference presets.", - "path": "/plugin/data_source/shotgun/presets/reference", - "value": null - }, - "shot": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "expanded": false, - "name": "Latest Dailies", - "queries": [ - { - "enabled": true, - "term": "Lookback", - "value": "3 Days" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "My Dailies", - "queries": [ - { - "enabled": true, - "livelink": false, - "term": "Author", - "value": "${USERFULLNAME}" - }, - { - "enabled": true, - "term": "Lookback", - "value": "30 Days" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Sent To Client", - "queries": [ - { - "enabled": true, - "term": "Sent To Client", - "value": "True" - }, - { - "enabled": true, - "term": "Lookback", - "value": "7 Days" - }, - { - "enabled": false, - "term": "Latest Version", - "value": "True" - }, - { - "enabled": false, - "term": "Production Status", - "value": "Final" - }, - { - "enabled": false, - "term": "Production Status", - "value": "Final CBB" - }, - { - "enabled": false, - "term": "Production Status", - "value": "Final TC" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Prop Finals", - "queries": [ - { - "enabled": true, - "term": "Production Status", - "value": "Proposed Final" - }, - { - "enabled": true, - "term": "Production Status", - "value": "Proposed Final Pending" - }, - { - "enabled": true, - "term": "Production Status", - "value": "Proposed Final TF" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "All Finals", - "queries": [ - { - "enabled": true, - "term": "Production Status", - "value": "Final" - }, - { - "enabled": true, - "term": "Production Status", - "value": "Final - Other" - }, - { - "enabled": true, - "term": "Production Status", - "value": "Final - Trailer" - }, - { - "enabled": true, - "term": "Production Status", - "value": "Final CBB" - }, - { - "enabled": true, - "term": "Production Status", - "value": "Final Pending" - }, - { - "enabled": true, - "term": "Production Status", - "value": "Final Pending TC" - }, - { - "enabled": true, - "term": "Production Status", - "value": "Final Pending TF" - }, - { - "enabled": true, - "term": "Production Status", - "value": "Final TC" - }, - { - "enabled": true, - "term": "Production Status", - "value": "Final TF" - }, - { - "enabled": false, - "term": "Latest Version", - "value": "True" - } - ], - "type": "system" - } - ], - "description": "Shot presets.", - "path": "/plugin/data_source/shotgun/presets/shot", - "value": null - }, - "shot_tree": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [ - { - "expanded": false, - "name": "Latest", - "queries": [ - { - "dynamic": true, - "enabled": true, - "term": "Shot", - "value": "" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - }, - { - "enabled": true, - "term": "Flag Media", - "value": "Orange" - } - ], - "type": "system" - }, - { - "expanded": false, - "name": "Latest Client", - "queries": [ - { - "dynamic": true, - "enabled": true, - "term": "Shot", - "value": "097_tr_0140" - }, - { - "enabled": true, - "term": "Sent To Client", - "value": "True" - }, - { - "enabled": true, - "term": "Flag Media", - "value": "Orange" - } - ], - "type": "system" - } - ], - "description": "Shot Tree presets.", - "path": "/plugin/data_source/shotgun/presets/shot_tree", - "type": "system", - "value": null - } - }, - "project_id": { - "context": [ - "APPLICATION" - ], - "datatype": "int", - "default_value": -1, - "description": "Project id.", - "path": "/plugin/data_source/shotgun/project_id", - "value": 329 - }, - "project_presets": { - "context": [ - "APPLICATION" - ], - "datatype": "json", - "default_value": [], - "description": "Project presets.", - "path": "/plugin/data_source/shotgun/project_presets", - "value": null - }, - "server": { - "host": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "shotgun.dneg.com", - "description": "Shotgun host.", - "path": "/plugin/data_source/shotgun/server/host", - "value": "shotgun.dneg.com" - }, - "port": { - "context": [ - "APPLICATION" - ], - "datatype": "int", - "default_value": 0, - "description": "Shotgun host port.", - "path": "/plugin/data_source/shotgun/server/port", - "value": 0 - }, - "protocol": { - "context": [ - "APPLICATION" - ], - "datatype": "string", - "default_value": "https", - "description": "Connection protocol.", - "path": "/plugin/data_source/shotgun/server/protocol", - "value": "http" - }, - "timeout": { - "context": [ - "APPLICATION" - ], - "datatype": "int", - "default_value": 120, - "description": "Connection timeout.", - "path": "/plugin/data_source/shotgun/server/timeout", - "value": 120 - } - } - } - } - }, - "ui": { - "qml": { - "shotgun_browser_settings": { - "context": [ - "QML_UI" - ], - "datatype": "json", - "default_value": {}, - "description": "Prefs relating to window position.", - "path": "/ui/qml/shotgun_browser_settings", - "value": { - "__ignore__": true, - "height": 400, - "visibility": 0, - "width": 700, - "x": 100, - "y": 100 - } - } - } - } -} \ No newline at end of file diff --git a/share/preference/plugin_dneg_media_hook.json b/share/preference/plugin_dneg_media_hook.json index 0f15050e8..1501c4393 100644 --- a/share/preference/plugin_dneg_media_hook.json +++ b/share/preference/plugin_dneg_media_hook.json @@ -3,11 +3,14 @@ "dneg_media_hook": { "default_trim_slate_behaviour": { "path": "/plugin/dneg_media_hook/default_trim_slate_behaviour", - "default_value": "Use Metadata or Don't Trim First Frame", - "description": "If auto trim slate is disabled, or if auto trim slate metadata is missing, choose if pipeline movies have their first frame trimmed.", + "default_value": "Don't Trim First Frame", + "description": "If Auto Trim Slate slate is disabled, or if auto trim slate metadata is missing, choose if pipeline movies have their first frame trimmed by default.", "value": "Trim First Frame", - "datatype": "string", - "context": ["APPLICATION"] + "datatype": "multichoice string", + "context": ["APPLICATION"], + "category": "DNEG", + "display_name": "Default Slate Frame Trimming", + "options": ["Trim First Frame", "Don't Trim First Frame"] }, "auto_trim_slate": { "path": "/plugin/dneg_media_hook/auto_trim_slate", @@ -15,7 +18,19 @@ "description": "For 'dneg' movies, use embedded metadata to determine if there is a slate frame and trim it off if so", "value": true, "datatype": "bool", - "context": ["APPLICATION"] + "context": ["APPLICATION"], + "category": "DNEG", + "display_name": "Auto Trim Slate" + }, + "adjust_timecode": { + "path": "/plugin/dneg_media_hook/adjust_timecode", + "default_value": true, + "description": "Use timeline_range from pipequery to adjust timecode", + "value": true, + "datatype": "bool", + "context": ["APPLICATION"], + "category": "DNEG", + "display_name": "Adjust TimeCode" } } } diff --git a/share/preference/plugin_grading.json b/share/preference/plugin_grading.json index 6d0d2b346..66864c85c 100644 --- a/share/preference/plugin_grading.json +++ b/share/preference/plugin_grading.json @@ -1,11 +1,11 @@ { "plugin": { "grading": { - "grading_panel": { - "path": "/plugin/grading/grading_panel", - "default_value": "Basic", - "description": "Grading panel", - "value": "Basic", + "drawing_tool": { + "path": "/plugin/grading/drawing_tool", + "default_value": "Shape", + "description": "Drawing tool", + "value": "Shape", "datatype": "string", "context": ["APPLICATION"] }, diff --git a/share/preference/plugin_media_reader_openexr.json b/share/preference/plugin_media_reader_openexr.json index 5f6646e48..835edb99a 100644 --- a/share/preference/plugin_media_reader_openexr.json +++ b/share/preference/plugin_media_reader_openexr.json @@ -4,13 +4,15 @@ "OpenEXR":{ "max_exr_overscan_percent": { "path": "/plugin/media_reader/OpenEXR/max_exr_overscan_percent", - "default_value": 5.0, - "description": "Default overscan.", - "value": 5.0, + "default_value": 100.0, + "description": "Set the maximum amount of EXR overscan that is loaded. Setting to zero means no overscan is loaded, or set to a very high number to always load all overscan pixels. After changing this value, you may need to clear the xstudio cache ('Advanced' sub-menu in MediaList) to re-load the frames", + "value": 100.0, "minimum": 0.0, - "maximum": 100.0, + "maximum": 1000.0, "datatype": "double", - "context": ["APPLICATION"] + "context": ["APPLICATION"], + "category": "Playback", + "display_name": "EXR maximum overscan (%)" }, "readers_per_source": { "path": "/plugin/media_reader/OpenEXR/readers_per_source", diff --git a/share/preference/plugin_utility_disable_screensaver.json b/share/preference/plugin_utility_disable_screensaver.json deleted file mode 100644 index 3e9069a9f..000000000 --- a/share/preference/plugin_utility_disable_screensaver.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "plugin":{ - "utility":{ - "DisableScreensaver":{ - "enabled": { - "path": "/plugin/utility/DisableScreensaver/enabled", - "default_value": false, - "description": "Enable screensaver pause.", - "value": false, - "datatype": "bool", - "context": ["APPLICATION"] - }, - "enabled_when_playing": { - "path": "/plugin/utility/DisableScreensaver/enabled_when_playing", - "default_value": false, - "description": "Enable screensaver pause but only when playing.", - "value": false, - "datatype": "bool", - "context": ["APPLICATION"] - }, - "interval": { - "path": "/plugin/utility/DisableScreensaver/interval", - "default_value": 10, - "description": "Wake up interval in seconds.", - "value": 10, - "datatype": "int", - "context": ["APPLICATION"] - } - } - } - } -} diff --git a/share/preference/ui_qml.json b/share/preference/ui_qml.json index 47e87f85f..f8dbf019c 100644 --- a/share/preference/ui_qml.json +++ b/share/preference/ui_qml.json @@ -1,6 +1,56 @@ { "ui": { "qml":{ + "global_ui_scale_factor": { + "path": "/ui/qml/global_ui_scale_factor", + "default_value": 1.0, + "description": "Scales the entire xSTUDIO interface. Requires a restart after changing.", + "value": 1.0, + "datatype": "double", + "context": ["QML_UI"], + "category": "Interface", + "display_name": "Interface Scaling" + }, + "use_flat_theme": { + "path": "/ui/qml/use_flat_theme", + "default_value": true, + "description": "Use the flat theme, instead of gradient backgrounds, in the xSTUDIO interface.", + "value": true, + "datatype": "bool", + "context": ["QML_UI"], + "category": "Interface", + "display_name": "Use Flat Theme" + }, + "disable_quickview_windows": { + "path": "/ui/qml/disable_quickview_windows", + "default_value": false, + "description": "When media is added to the xSTUDIO session from an external command with the 'quick-view' flag to force the media to be loaded into a quickview pop-up window, the window will not be created if this option is set to On (the media is still added to the xSTUDIO session, however)", + "value": false, + "datatype": "bool", + "context": ["QML_UI"], + "category": "Interface", + "display_name": "Disable QuickView Windows" + }, + "ui_brightness": { + "path": "/ui/qml/ui_brightness", + "default_value": 60.0, + "description": "Decrease this value to make the xSTUDIO interface darker.", + "value": 60.0, + "datatype": "double", + "context": ["QML_UI"], + "category": "Interface", + "display_name": "UI Brightness" + }, + "ui_text_brightness": { + "path": "/ui/qml/ui_text_brightness", + "default_value": 90.0, + "description": "Decrease this value to make the xSTUDIO text elements darker.", + "value": 90.0, + "datatype": "double", + "context": ["QML_UI"], + "category": "Interface", + "display_name": "UI Text Brightness" + }, "custom_help_menu": { "path": "/ui/qml/custom_help_menu", "default_value": {"title": "", "url": ""}, @@ -9,14 +59,6 @@ "datatype": "json", "context": ["QML_UI"] }, - "enable_presentation_mode": { - "path": "/ui/qml/enable_presentation_mode", - "default_value": false, - "description": "Enable presentation mode.", - "value": false, - "datatype": "bool", - "context": ["QML_UI"] - }, "feedback": { "path": "/ui/qml/feedback", "default_value": "https://github.com/AcademySoftwareFoundation/xstudio/issues", @@ -33,12 +75,12 @@ "datatype": "string", "context": ["QML_UI"] }, - "autosave": { - "path": "/ui/qml/autosave", - "default_value": true, - "description": "Enable autosave.", - "value": true, - "datatype": "bool", + "copy_source_path": { + "path": "/ui/qml/copy_source_path", + "default_value": "SourceName", + "description": "Copy specified source path to clipboard.", + "value": "main_proxy0", + "datatype": "string", "context": ["QML_UI"] }, "start_play_on_load": { @@ -47,7 +89,8 @@ "description": "Start playing on load.", "value": true, "datatype": "bool", - "context": ["QML_UI"] + "context": ["QML_UI"], + "category": "Playback" }, "do_overlay_messages": { "path": "/ui/qml/do_overlay_messages", @@ -71,7 +114,8 @@ "description": "Warn user on quiting with unsaved session.", "value": true, "datatype": "bool", - "context": ["QML_UI"] + "context": ["QML_UI"], + "category": "General" }, "cycle_through_playlist": { "path": "/ui/qml/cycle_through_playlist", @@ -87,7 +131,8 @@ "description": "Restore play state after scrub.", "value": true, "datatype": "bool", - "context": ["QML_UI"] + "context": ["QML_UI"], + "category": "Playback" }, "view_top_toolbar": { "path": "/ui/qml/view_top_toolbar", @@ -97,197 +142,14 @@ "datatype": "bool", "context": ["QML_UI"] }, - "main_window_viewer": { - "path": "/ui/qml/main_window_viewer", - "default_value": "{}", - "description": "Prefs relating to viewer.", - "value": { - "__ignore__": true, - "transportControlsVisible": true, - "mediaInfoBarVisible": true, - "statusBarVisible": true, - "tabBarVisible": true, - "toolBarVisible": true, - "layoutBarVisible": true, - "viewportTitleBarVisible": true, - "compactMode": false, - "menuBarVisible": true - }, - "datatype": "json", - "context": ["QML_UI"] - }, - "second_window_viewer": { - "path": "/ui/qml/second_window_viewer", - "default_value": "{}", - "description": "Prefs relating to pop-out viewer.", - "value": { - "__ignore__": true, - "transportControlsVisible": true, - "mediaInfoBarVisible": true, - "statusBarVisible": true, - "tabBarVisible": true, - "toolBarVisible": true, - "layoutBarVisible": true, - "viewportTitleBarVisible": true, - "compactMode": true, - "menuBarVisible": false - }, - "datatype": "json", - "context": ["QML_UI"] - }, - "note_window_settings": { - "path": "/ui/qml/note_window_settings", - "default_value": "{}", - "description": "Prefs relating to window position.", - "value": { - "__ignore__": true, - "x": 100, - "y": 100, - "width": 700, - "height": 400, - "visibility": 0 - }, - "datatype": "json", - "context": ["QML_UI"] - }, - "main_window_settings": { - "path": "/ui/qml/main_window_settings", - "default_value": "{}", - "description": "Prefs relating to window position.", - "value": { - "__ignore__": true, - "x": 100, - "y": 100, - "width": 1200, - "height": 800, - "visibility": 1, - "layout_name": "review_layout", - "menu_bar_visible": 1 - }, - "datatype": "json", - "context": ["QML_UI"] - }, - "second_window_settings": { - "path": "/ui/qml/second_window_settings", - "default_value": "{}", - "description": "Prefs relating to window position.", - "value": { - "__ignore__": true, - "x": 100, - "y": 100, - "width": 950, - "height": 540, - "visibility": 0, - "layout_name": "presentation_layout", - "menu_bar_visible": 0 - }, - "datatype": "json", - "context": ["QML_UI"] - }, - "media_list_thumbnail_size": { - "path": "/ui/qml/media_list_thumbnail_size", - "default_value": 1.0, - "description": "Thumbnail size in media list.", - "value": 100.0, - "datatype": "double", - "context": ["QML_UI"] - }, - "main_window_edit_layout": { - "path": "/ui/qml/main_window_edit_layout", - "default_value": "{}", - "description": "A list of window splitter ratios to remember positions of split handles", - "value": { - "__ignore__": true, - "divider_positions": [0.75, 0.16, 0.24] - }, - "datatype": "json", - "context": ["QML_UI"] - }, - "main_window_review_layout": { - "path": "/ui/qml/main_window_review_layout", - "default_value": "{}", - "description": "A list of window splitter ratios to remember positions of split handles", - "value": { - "__ignore__": true, - "divider_positions": [0.18, 0.4] - }, - "datatype": "json", - "context": ["QML_UI"] - }, - "main_window_shotgun_layout": { - "path": "/ui/qml/main_window_shotgun_layout", - "default_value": "{}", - "description": "A list of window splitter ratios to remember positions of split handles", - "value": { - "__ignore__": true, - "divider_positions": [0.15, 0.7, 0.3] - }, - "datatype": "json", - "context": ["QML_UI"] - }, - "main_window_browse_layout": { - "path": "/ui/qml/main_window_browse_layout", - "default_value": "{}", - "description": "A list of window splitter ratios to remember positions of split handles", - "value": { - "__ignore__": true, - "divider_positions": [0.18] - }, - "datatype": "json", - "context": ["QML_UI"] - }, - "second_window_edit_layout": { - "path": "/ui/qml/second_window_edit_layout", - "default_value": "{}", - "description": "A list of window splitter ratios to remember positions of split handles", - "value": { - "__ignore__": true, - "divider_positions": [0.18, 0.7, 0.5] - }, - "datatype": "json", - "context": ["QML_UI"] - }, - "second_window_review_layout": { - "path": "/ui/qml/second_window_review_layout", - "default_value": "{}", - "description": "A list of window splitter ratios to remember positions of split handles", - "value": { - "__ignore__": true, - "divider_positions": [0.18, 0.7] - }, - "datatype": "json", - "context": ["QML_UI"] - }, - "second_window_browse_layout": { - "path": "/ui/qml/second_window_browse_layout", - "default_value": "{}", - "description": "A list of window splitter ratios to remember positions of split handles", - "value": { - "__ignore__": true, - "divider_positions": [0.18] - }, - "datatype": "json", - "context": ["QML_UI"] - }, - "colour_pipe": { - "path": "/ui/qml/colour_pipe", - "default_value": "{}", - "description": "Prefs relating to colour pipeline.", - "value": { - "__ignore__": true, - "view": 0, - "display": 0 - }, - "datatype": "json", - "context": ["QML_UI"] - }, "click_to_toggle_play": { "path": "/ui/qml/click_to_toggle_play", "default_value": false, "description": "Click to toggle playback.", "value": false, "datatype": "bool", - "context": ["QML_UI"] + "context": ["QML_UI"], + "category": "Playback" }, "timeline_units": { "path": "/ui/qml/timeline_units", @@ -310,8 +172,10 @@ "default_value": "#bb7700", "description": "Colour of accents.", "value": "#bb7700", - "datatype": "string", - "context": ["QML_UI"] + "datatype": "colour", + "options": { "Blue": "#307bf6", "Purple" : "#9b56a3", "Pink": "#e65d9c", "Red": "#ed5f5d", "Orange": "#e9883a", "Yellow": "#f3ba4b", "Green" : "#77b756", "Graphite" : "#999999" }, + "context": ["QML_UI"], + "category": "Interface" }, "latest_version": { "path": "/ui/qml/latest_version", @@ -337,93 +201,6 @@ "datatype": "string", "context": ["QML_UI"] }, - "second_window_fit_mode": { - "path": "/ui/qml/second_window_fit_mode", - "default_value": "best", - "description": "Fit mode.", - "value": "best", - "datatype": "string", - "context": ["QML_UI"] - }, - "control_values": { - "path": "/ui/qml/control_values", - "default_value": "{}", - "description": "Dump these.", - "value": "{}", - "datatype": "json", - "context": ["QML_UI"] - }, - "channel": { - "path": "/ui/qml/channel", - "default_value": 0, - "description": "XPlayer channel modes.", - "value": 0, - "datatype": "int" - }, - "xplayer_window": { - "path": "/ui/qml/xplayer_window", - "default_value": "{}", - "description": "XPlayer window geometries.", - "value": "{}", - "datatype": "json", - "context": ["QML_UI"] - }, - "compare": { - "path": "/ui/qml/compare", - "default_value": 0, - "description": "XPlayer compare modes.", - "value": 0, - "datatype": "int", - "context": ["QML_UI"] - }, - "exposure": { - "path": "/ui/qml/exposure", - "default_value": 0.0, - "description": "XPlayer exposure.", - "value": 0.0, - "datatype": "double", - "context": ["QML_UI"] - }, - "velocity": { - "path": "/ui/qml/velocity", - "default_value": 1.0, - "description": "XPlayer velocity.", - "value": 1.0, - "datatype": "double", - "context": ["QML_UI"] - }, - "source": { - "path": "/ui/qml/source", - "default_value": "none", - "description": "Media source.", - "value": "none", - "datatype": "string", - "context": ["QML_UI"] - }, - "view": { - "path": "/ui/qml/view", - "default_value": "", - "description": "OCIO view.", - "value": "", - "datatype": "string", - "context": ["QML_UI"] - }, - "display": { - "path": "/ui/qml/display", - "default_value": "", - "description": "OCIO display.", - "value": "", - "datatype": "string", - "context": ["QML_UI"] - }, - "popout_viewer_visible": { - "path": "/ui/qml/popout_viewer_visible", - "default_value": false, - "description": "The 2nd pop-out window is visible.", - "value": false, - "datatype": "bool", - "context": ["QML_UI"] - }, "recent_history": { "path": "/ui/qml/recent_history", "default_value": [], @@ -442,9 +219,9 @@ }, "default_playhead_compare_mode": { "path": "/ui/qml/default_playhead_compare_mode", - "default_value": "A/B", + "default_value": "Off", "description": "Default Compare Mode.", - "value": "A/B", + "value": "Off", "datatype": "string", "context": ["QML_UI"] }, @@ -456,97 +233,408 @@ "datatype": "string", "context": ["QML_UI"] }, + "tabs_visibility": { + "path": "/ui/qml/tabs_visibility", + "default_value": 2, + "description": "Visibility of tabs in layouts", + "value": 2, + "datatype": "int", + "context": ["QML_UI"] + }, + "floating_panel_window_positions": { + "path": "/ui/qml/floating_panel_window_positions", + "default_value": {}, + "description": "For remembering position and size of floating panel windows", + "value": {}, + "datatype": "json", + "context": ["QML_UI"] + }, "reskin_windows_and_panels_model": { "path": "/ui/qml/reskin_windows_and_panels_model", - "default_value": "{}", - "description": "Windows layouts", - "value": { + "default_value": { "children": [ { - "window_name": "main_window", - "width": 800, - "height": 800, - "current_layout": 0, - "position_x": 100, - "position_y": 100, "children": [ { - "layout_name": "Review", + "children": [ + { + "children": [ + { + "tab_view": "Viewport", + "user_data": [ + [ + "Annotate", + false, + "left" + ] + ] + } + ], + "current_tab": 0, + "tabs_hidden": false, + "user_data": { + "category": "Tree", + "left_panel_width": 267, + "project": "FUR", + "quick_load": "", + "right_panel_width": 200 + } + } + ], "enabled": true, + "layout_name": "Present" + }, + { "children": [ { - "split_horizontal": false, - "child_dividers": [0.5], + "child_dividers": [ + 0.2639399609144996 + ], "children": [ { - "split_horizontal": true, - "child_dividers": [0.5], + "child_dividers": [ + 0.5111100077629089 + ], "children": [ { - "current_tab": 2, "children": [ - { "tab_view" : "Media" }, - { "tab_view" : "Timeline" }, - { "tab_view" : "Viewport" } - ] + { + "tab_view": "Playlists" + } + ], + "current_tab": 0 }, { - "current_tab": 0, "children": [ - { "tab_view" : "Playlists" } + { + "current_tab": 0, + "tab_view": "Media", + "user_data": { + "cellSize": 200, + "viewType": "list" + } + } ] } - ] + ], + "split_horizontal": false }, { - "current_tab": 0, "children": [ - { "tab_view" : "Timeline" } + { + "current_tab": 0, + "tab_view": "Viewport", + "user_data": [ + [ + "Annotate", + false, + "left" + ] + ] + } ] } - ] + ], + "split_horizontal": true } - ] + ], + "enabled": true, + "layout_name": "Review" }, { - "layout_name": "Present", - "enabled": true, "children": [ { - "split_horizontal": false, - "child_dividers": [0.5], + "child_dividers": [ + 0.2543601384519992 + ], "children": [ { - "split_horizontal": true, - "child_dividers": [0.5], + "child_dividers": [ + 0.4885725844704628 + ], "children": [ { - "current_tab": 2, "children": [ - { "tab_view" : "Media" }, - { "tab_view" : "Timeline" }, - { "tab_view" : "Viewport" } - ] + { + "tab_view": "Playlists" + } + ], + "current_tab": 0 }, { + "children": [ + { + "tab_view": "Media", + "user_data": "87519a92-a3b7-4b57-899a-574c6a48357b" + } + ], "current_tab": 0, + "user_data": "Ref" + } + ], + "split_horizontal": false + }, + { + "child_dividers": [ + 0.7026894017605045 + ], + "children": [ + { "children": [ - { "tab_view" : "Playlists" } - ] + { + "tab_view": "Viewport", + "user_data": [ + [ + "Annotate", + false, + "left" + ] + ] + } + ], + "current_tab": 0, + "user_data": { + "category": "Tree", + "left_panel_width": 316, + "project": "FUR", + "quick_load": "Latest Anim", + "right_panel_width": 200 + } + }, + { + "children": [ + { + "tab_view": "Timeline" + } + ], + "current_tab": 0, + "user_data": "Stream" } + ], + "split_horizontal": false + } + ], + "split_horizontal": true + } + ], + "enabled": true, + "layout_name": "Timeline" + } + ], + "current_layout": 0, + "height": 1057, + "position_x": 181, + "position_y": 30, + "width": 2176, + "window_name": "main_window" + }, + { + "height": 813, + "position_x": 1269, + "position_y": 553, + "user_data": [ + [ + "Annotate", + true, + "right" + ] + ], + "width": 1074, + "window_name": "popout_window" + } + ] + }, + "description": "Windows layouts", + "value": { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "tab_view": "Viewport", + "user_data": [ + [ + "Annotate", + false, + "left" + ] ] + } + ], + "current_tab": 0, + "tabs_hidden": false, + "user_data": { + "category": "Tree", + "left_panel_width": 267, + "project": "FUR", + "quick_load": "", + "right_panel_width": 200 + } + } + ], + "enabled": true, + "layout_name": "Present" + }, + { + "children": [ + { + "child_dividers": [ + 0.2639399609144996 + ], + "children": [ + { + "child_dividers": [ + 0.5111100077629089 + ], + "children": [ + { + "children": [ + { + "tab_view": "Playlists" + } + ], + "current_tab": 0 + }, + { + "children": [ + { + "current_tab": 0, + "tab_view": "Media", + "user_data": { + "cellSize": 200, + "viewType": "list" + } + } + ] + } + ], + "split_horizontal": false }, { - "current_tab": 0, "children": [ - { "tab_view" : "Timeline" } + { + "current_tab": 0, + "tab_view": "Viewport", + "user_data": [ + [ + "Annotate", + false, + "left" + ] + ] + } ] } - ] + ], + "split_horizontal": true } - ] + ], + "enabled": true, + "layout_name": "Review" + }, + { + "children": [ + { + "child_dividers": [ + 0.2543601384519992 + ], + "children": [ + { + "child_dividers": [ + 0.4885725844704628 + ], + "children": [ + { + "children": [ + { + "tab_view": "Playlists" + } + ], + "current_tab": 0 + }, + { + "children": [ + { + "tab_view": "Media", + "user_data": "87519a92-a3b7-4b57-899a-574c6a48357b" + } + ], + "current_tab": 0, + "user_data": "Ref" + } + ], + "split_horizontal": false + }, + { + "child_dividers": [ + 0.7026894017605045 + ], + "children": [ + { + "children": [ + { + "tab_view": "Viewport", + "user_data": [ + [ + "Annotate", + false, + "left" + ] + ] + } + ], + "current_tab": 0, + "user_data": { + "category": "Tree", + "left_panel_width": 316, + "project": "FUR", + "quick_load": "Latest Anim", + "right_panel_width": 200 + } + }, + { + "children": [ + { + "tab_view": "Timeline" + } + ], + "current_tab": 0, + "user_data": "Stream" + } + ], + "split_horizontal": false + } + ], + "split_horizontal": true + } + ], + "enabled": true, + "layout_name": "Timeline" } - ] + ], + "current_layout": 1, + "height": 1057, + "position_x": 181, + "position_y": 30, + "width": 2176, + "window_name": "main_window" + }, + { + "height": 813, + "position_x": 1269, + "position_y": 553, + "user_data": [ + [ + "Annotate", + true, + "right" + ] + ], + "width": 1074, + "window_name": "popout_window" } ] }, @@ -556,109 +644,122 @@ "media_list_columns_config": { "path": "/ui/qml/media_list_columns_config", "default_value": "{}", - "description": "Media List Columns Configuration", + "description": "Default set-up for columns in a new media list panel", "value": { - "target_role_data_slot": 0, "children": [ - { - "title": "", - "size": 10, - "resizable": false, - "data_type": "flag", - "sortable": false - }, - { - "title": "", - "size": 40, - "resizable": false, - "data_type": "index", - "sortable": true - }, - { - "title": "Image", - "size": 70, - "resizable": true, - "data_type": "thumbnail", - "sortable": false - }, - { - "title": "File", - "size": 300, - "role_name": "pathRole", - "object": "MediaSource", - "resizable": true, - "data_type": "role_data", - "sortable": false, - "format_regex": ".+\\/([^\\/]+)$", - "format_out": "$1" - }, - { - "title": "Resolution", - "size": 70, - "object": "MediaSource", - "role_name": "resolutionRole", - "resizable": true, - "data_type": "role_data", - "sortable": false - }, - { - "title": "Notes", - "size": 50, - "resizable": false, - "data_type": "notes", - "sortable": false - }, - { - "title": "Shot", - "metadata_path": "/metadata/shotgun/shot/attributes/code", - "object": "Media", - "size": 120, - "data_type": "metadata", - "resizable": true, - "sortable": false - }, - { - "title": "Pipeline Step", - "metadata_path": "/metadata/shotgun/version/attributes/sg_pipeline_step", - "object": "Media", - "size": 120, - "data_type": "metadata", - "resizable": true, - "sortable": true - }, - { - "title": "Version", - "metadata_path": "/metadata/shotgun/version/attributes/sg_dneg_version", - "object": "Media", - "size": 80, - "data_type": "metadata", - "resizable": true, - "sortable": true - }, - { - "title": "Author", - "metadata_path": "/metadata/shotgun/version/relationships/created_by/data/name", - "object": "Media", - "size": 120, - "data_type": "metadata", - "resizable": true, - "sortable": true - }, - { - "title": "Date", - "metadata_path": "/metadata/shotgun/version/attributes/created_at", - "object": "Media", - "size": 120, - "data_type": "metadata", - "resizable": true, - "sortable": true, - "format_regex": "([0-9]+\\-[0-9]+\\-[0-9]+).+", - "format_out": "$1" - } - ]}, + { + "data_type": "flag", + "resizable": false, + "size": 10, + "sortable": false, + "title": "" + }, + { + "data_type": "index", + "resizable": false, + "size": 40, + "sortable": true, + "title": "" + }, + { + "data_type": "thumbnail", + "resizable": true, + "size": 81, + "sortable": false, + "title": "Image" + }, + { + "children": [ + { + "children": [ + { + "data_type": "media_standard_details", + "info_key": "File Path - MediaSource (Audio)", + "regex_format": "$2", + "regex_match": "(.+)/([^/]+)$" + } + ], + "data_type": "media_standard_details", + "info_key": "File Path - MediaSource (Image)", + "regex_format": "$2", + "regex_match": "(.+)/([^/]+)$" + } + ], + "data_type": "metadata", + "metadata_path": "/metadata/shotgun/version/attributes/code", + "object": "Media", + "position": "left", + "resizable": true, + "size": 282, + "sortable": true, + "title": "File" + }, + { + "data_type": "metadata", + "info_key": "Resolution - MediaSource (Image)", + "metadata_path": "/metadata/shotgun/shot/attributes/code", + "resizable": true, + "size": 110, + "sortable": false, + "title": "Shot / Asset" + }, + { + "data_type": "metadata", + "metadata_path": "/metadata/shotgun/version/attributes/sg_pipeline_step", + "object": "Media", + "regex_format": "$1", + "regex_match": "([0-9]+\\-[0-9]+\\-[0-9]+).+", + "resizable": true, + "size": 122, + "sortable": false, + "title": "Dept" + }, + { + "data_type": "notes", + "resizable": true, + "size": 100, + "sortable": false, + "title": "Notes" + }, + { + "data_type": "metadata", + "metadata_path": "/metadata/shotgun/version/relationships/created_by/data/name", + "object": "Media", + "resizable": true, + "size": 151, + "sortable": true, + "title": "Artist" + }, + { + "data_type": "media_standard_details", + "info_key": "Frame Range - MediaSource (Image)", + "resizable": true, + "size": 120, + "sortable": false, + "title": "Frame Range" + }, + { + "data_type": "metadata", + "metadata_path": "regex:/metadata/media/.*standard_fields/format", + "object": "MediaSource (Image)", + "resizable": true, + "size": 120, + "sortable": false, + "title": "Codec" + }, + { + "data_type": "media_standard_details", + "info_key": "Frame Rate - MediaSource (Image)", + "resizable": true, + "size": 70, + "sortable": false, + "title": "Frame Rate" + } + ] + }, "datatype": "json", "context": ["QML_UI"] } } } -} +} \ No newline at end of file diff --git a/share/preference/ui_viewport.json b/share/preference/ui_viewport.json index de7949de7..b1751f675 100644 --- a/share/preference/ui_viewport.json +++ b/share/preference/ui_viewport.json @@ -4,35 +4,54 @@ "filter_mode": { "path": "/ui/viewport/filter_mode", "default_value": "Auto Nearest/Bilinear", - "description": "Viewport Pixel Filter Mode", + "description": "Choose how pixels are sampled when displayed. 'Nearest' plots individual pixels to the screen, 'Biliniear' smoothly joins pixels. The 'Auto' mode will switch to Bilinear when you are zooming out from 1:1 zoom, reducing aliasing, but will show whole pixels when you are zooming closer to the image.", "value": "Auto Nearest/Bilinear", - "datatype": "string", - "context": ["APPLICATION"] + "datatype": "multichoice string", + "display_name": "Pixel Interpolation", + "options": ["Nearest Pixel", "Auto Nearest/Bilinear", "Bilinear"], + "context": ["APPLICATION"], + "category": "Viewport" }, "texture_mode": { "path": "/ui/viewport/texture_mode", "default_value": "Image Texture", - "description": "Viewport Low Level Texture mode - SSBO can give better performance on some systems but not all", + "description": "Set the way that image data is loaded onto your graphics processor. For playing high resolution formats this can affect playback performance in some cases. On some systems 'Texture' gives better performance. On others (generally modern and high end graphics cards) SSBO may give a better throughput.", "value": "Image Texture", - "value_range": ["Image Texture", "SSBO"], - "datatype": "string", - "context": ["APPLICATION"] + "options": ["Image Texture", "SSBO"], + "display_name": "GPU Texture Mode", + "datatype": "multichoice string", + "context": ["APPLICATION"], + "category": "Viewport" + }, + "num_textures": { + "path": "/ui/viewport/num_textures", + "default_value": 8, + "description": "This sets the number of images that xstudio creates on your graphics card for drawing video in the viewport. A higher number uses more video ram, but can improve performance when viewing grid/contact sheets with many tiles. Very high end machines could accept a value of 48, say, but lower powered laptops may only allow 4. AFTER CHANGING THIS VALUE YOU MUST RESTART XSTUDIO.", + "value": 8, + "display_name": "Number of Video Buffers on GPU", + "datatype": "int", + "context": ["APPLICATION"], + "category": "Viewport" }, "viewport_mouse_wheel_behaviour": { "path": "/ui/viewport/viewport_mouse_wheel_behaviour", "default_value": "Scrub Timeline", - "description": "Use mouse scroll wheel to scrub timeline (default) or zoom", + "description": "Choose wether rolling the mouse wheel in the viewport will scrub the playhead or zoom in/out of the image.", "value": "Scrub Timeline", - "datatype": "string", - "context": ["QML_UI"] + "options": ["Scrub Timeline", "Zoom Viewer"], + "display_name": "Mouse Wheel Action", + "datatype": "multichoice string", + "context": ["QML_UI"], + "category": "Viewport" }, "viewport_scrub_sensitivity": { "path": "/ui/viewport/viewport_scrub_sensitivity", "default_value": 2, - "description": "Set value to change the scrub pixel step for each frame", + "description": "When clicking and dragging left/right in the viewport, the playhead is 'scrubbed'. The sensitivity of this action (number of frames per pixel moved by the mouse) is set by this preference.", "value": 2, "datatype": "int", - "context": ["QML_UI"] + "context": ["QML_UI"], + "category": "Viewport" }, "enable_hud": { "path": "/ui/viewport/enable_hud", @@ -41,6 +60,14 @@ "value": false, "datatype": "bool", "context": ["QML_UI"] + }, + "hidden_toolbar_items": { + "path": "/ui/viewport/hidden_toolbar_items", + "default_value": false, + "description": "Hides items from the viewport toolbar", + "value": ["Gamma", "Saturation", "Rate", "Mirror"], + "datatype": "json", + "context": ["QML_UI"] } } } diff --git a/share/snippets/CMakeLists.txt b/share/snippets/CMakeLists.txt index 58ac8ac13..ccd08a3e5 100644 --- a/share/snippets/CMakeLists.txt +++ b/share/snippets/CMakeLists.txt @@ -9,20 +9,24 @@ macro(add_snip name) list(APPEND snippets ${CMAKE_BINARY_DIR}/bin/snippets/${name}) endmacro() -add_snip(demo.json) +add_snip(Demo/print_status.py) +add_snip(Demo/hello_world.py) +add_snip(Demo/clear_thumbnails.py) +add_snip(Demo/sort_playlists.py) +add_snip(Demo/dump_shots.py) +add_snip(Demo/capitalise_playlists.py) +add_snip(Demo/render_all_annotation.py) +add_snip(Demo/demo_mask_plugin.py) +add_snip(Demo/clear_sequence_colour.py) +add_snip(Demo/flag_clip_step.py) +add_snip(DNeg/export_flagged_clip_shots.py) add_custom_target( snippets-copy ALL DEPENDS ${snippets} ) -if(WIN32) - install(FILES - ${snippets} - DESTINATION - ${CMAKE_INSTALL_PREFIX}/snippets) -else() - install(FILES - ${snippets} - DESTINATION share/xstudio/snippets) -endif() +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + DESTINATION share/xstudio + FILES_MATCHING PATTERN "*.py" +) diff --git a/share/snippets/DNeg/Print_Client_Version_Names.py b/share/snippets/DNeg/Print_Client_Version_Names.py new file mode 100644 index 000000000..fbc67d833 --- /dev/null +++ b/share/snippets/DNeg/Print_Client_Version_Names.py @@ -0,0 +1,10 @@ +print("==== START ====") +def dump_client_names(items=XSTUDIO.api.session.selected_media): + for item in items: + try: + print(item.metadata["metadata"]["shotgun"]["version"]["attributes"]["sg_client_filename"]) + except: + pass + +dump_client_names() +print("==== END ====") diff --git a/share/snippets/DNeg/export_flagged_clip_shots.py b/share/snippets/DNeg/export_flagged_clip_shots.py new file mode 100644 index 000000000..72f622514 --- /dev/null +++ b/share/snippets/DNeg/export_flagged_clip_shots.py @@ -0,0 +1,50 @@ +from xstudio.api.session.playlist.timeline import Timeline, Stack, Track, Clip + +def collect_flagged_clips(item=XSTUDIO.api.session.viewed_container): + result = [] + if isinstance(item, Timeline) or isinstance(item, Stack) or isinstance(item, Track): + if item.enabled: + for child in item.children: + result.extend(collect_flagged_clips(child)) + elif isinstance(item, Clip): + if item.enabled and item.item_flag != "": + result.append([item.item_flag_colour, item]) + + return result + +def export_flagged_clip_names(item=XSTUDIO.api.session.viewed_container): + items = collect_flagged_clips(item) + + result = {} + for i in items: + if i[0] not in result: + result[i[0]] = set() + + result[i[0]].add(i[1].item_name) + + for i in sorted(result.keys()): + print(i+":") + for ii in sorted(result[i]): + print (ii) + +def export_flagged_clip_shots(item=XSTUDIO.api.session.viewed_container): + items = collect_flagged_clips(item) + + result = {} + for i in items: + if i[0] not in result: + result[i[0]] = set() + + try: + result[i[0]].add(i[1].media.metadata["metadata"]["shotgun"]["shot"]["attributes"]["code"]) + except: + pass + + for i in sorted(result.keys()): + print(i+":") + if(len(result[i])): + for ii in sorted(result[i]): + print (ii) + + +export_flagged_clip_shots() diff --git a/share/snippets/DNeg/remove_duplicate_sg_versions.py b/share/snippets/DNeg/remove_duplicate_sg_versions.py new file mode 100644 index 000000000..0fe7615dc --- /dev/null +++ b/share/snippets/DNeg/remove_duplicate_sg_versions.py @@ -0,0 +1,17 @@ +from xstudio.api.session.playlist import Playlist, Subset + +def remove_duplicate_sg_version(item=XSTUDIO.api.session.inspected_container): + if isinstance(item, Playlist) or isinstance(item, Subset): + dup = set() + for media in item.media: + try: + sid = media.metadata["metadata"]["shotgun"]["version"]["id"] + if sid not in dup: + dup.add(sid) + else: + print("Removing Duplicate", media.name) + item.remove_media(media) + except Exception as err: + pass + +remove_duplicate_sg_version() \ No newline at end of file diff --git a/share/snippets/Demo/capitalise_playlists.py b/share/snippets/Demo/capitalise_playlists.py new file mode 100644 index 000000000..6b754a1cb --- /dev/null +++ b/share/snippets/Demo/capitalise_playlists.py @@ -0,0 +1,2 @@ +from xstudio import demo +demo.capitalise(XSTUDIO.api.session) \ No newline at end of file diff --git a/share/snippets/Demo/clear_sequence_colour.py b/share/snippets/Demo/clear_sequence_colour.py new file mode 100644 index 000000000..265e878f7 --- /dev/null +++ b/share/snippets/Demo/clear_sequence_colour.py @@ -0,0 +1,12 @@ +from xstudio.api.session.playlist.timeline import Timeline, Stack, Track, Clip + +def clear_sequence_colour(item): + if isinstance(item, Timeline) or isinstance(item, Stack) or isinstance(item, Track): + for child in item.children: + clear_sequence_colour(child) + if isinstance(item, Track): + item.item_flag = "" + elif isinstance(item, Clip): + item.item_flag = "" + +clear_sequence_colour(XSTUDIO.api.session.viewed_container) \ No newline at end of file diff --git a/share/snippets/Demo/clear_thumbnails.py b/share/snippets/Demo/clear_thumbnails.py new file mode 100644 index 000000000..f23108945 --- /dev/null +++ b/share/snippets/Demo/clear_thumbnails.py @@ -0,0 +1,2 @@ +from xstudio import demo +demo.clear_thumbnail_cache(XSTUDIO.api) \ No newline at end of file diff --git a/share/snippets/Demo/demo_mask_plugin.py b/share/snippets/Demo/demo_mask_plugin.py new file mode 100644 index 000000000..4adc61f61 --- /dev/null +++ b/share/snippets/Demo/demo_mask_plugin.py @@ -0,0 +1,2 @@ +from xstudio import demo +demo.mask_plugin(XSTUDIO) \ No newline at end of file diff --git a/share/snippets/Demo/dump_shots.py b/share/snippets/Demo/dump_shots.py new file mode 100644 index 000000000..a49a11ada --- /dev/null +++ b/share/snippets/Demo/dump_shots.py @@ -0,0 +1,2 @@ +from xstudio import demo +demo.dump_shots(XSTUDIO.api.session) \ No newline at end of file diff --git a/share/snippets/Demo/flag_clip_step.py b/share/snippets/Demo/flag_clip_step.py new file mode 100644 index 000000000..e72389039 --- /dev/null +++ b/share/snippets/Demo/flag_clip_step.py @@ -0,0 +1,16 @@ +from xstudio.api.session.playlist.timeline import Timeline, Stack, Track, Clip + +def flag_clip_step(step, colour, item=XSTUDIO.api.session.viewed_container): + if isinstance(item, Timeline) or isinstance(item, Stack) or isinstance(item, Track): + for child in item.children: + flag_clip_step(step, colour, child) + elif isinstance(item, Clip): + try: + mstep = item.media.get_metadata("/metadata/shotgun/version/attributes/sg_pipeline_step") + if mstep == step: + item.item_flag = colour + except: + pass + +flag_clip_step("Layout", "#ff0000ff") +flag_clip_step("Prep", "#ff00ff00") \ No newline at end of file diff --git a/share/snippets/Demo/hello_world.py b/share/snippets/Demo/hello_world.py new file mode 100644 index 000000000..edaa35743 --- /dev/null +++ b/share/snippets/Demo/hello_world.py @@ -0,0 +1 @@ +print("Hello Word") \ No newline at end of file diff --git a/share/snippets/Demo/list_offline_media.py b/share/snippets/Demo/list_offline_media.py new file mode 100644 index 000000000..8a369ef22 --- /dev/null +++ b/share/snippets/Demo/list_offline_media.py @@ -0,0 +1,4 @@ +for playlist in XSTUDIO.api.session.playlists: + for m in playlist.media: + if not m.is_online: + print("'" + "','".join([playlist.name, m.name, str(m.media_source().media_reference.uri())]) + "'") \ No newline at end of file diff --git a/share/snippets/Demo/print_status.py b/share/snippets/Demo/print_status.py new file mode 100644 index 000000000..7c67901c5 --- /dev/null +++ b/share/snippets/Demo/print_status.py @@ -0,0 +1 @@ +XSTUDIO.api.print_status() \ No newline at end of file diff --git a/share/snippets/Demo/render_all_annotation.py b/share/snippets/Demo/render_all_annotation.py new file mode 100644 index 000000000..da256321a --- /dev/null +++ b/share/snippets/Demo/render_all_annotation.py @@ -0,0 +1,2 @@ +from xstudio import demo +demo.render_all_annotations(XSTUDIO.api.session) \ No newline at end of file diff --git a/share/snippets/Demo/sort_playlists.py b/share/snippets/Demo/sort_playlists.py new file mode 100644 index 000000000..4faaf916b --- /dev/null +++ b/share/snippets/Demo/sort_playlists.py @@ -0,0 +1,2 @@ +from xstudio import demo +demo.sort_session(XSTUDIO.api.session) \ No newline at end of file diff --git a/share/snippets/clip/DNeg/trim_to_comp_range.py b/share/snippets/clip/DNeg/trim_to_comp_range.py new file mode 100644 index 000000000..128dc0381 --- /dev/null +++ b/share/snippets/clip/DNeg/trim_to_comp_range.py @@ -0,0 +1,50 @@ +from xstudio.api.session.playlist.timeline import Timeline, Clip +from xstudio.core import FrameRange, FrameRateDuration, FrameRate + +def use_comp_range(item=XSTUDIO.api.session.viewed_container): + if isinstance(item, Timeline): + for i in item.selection: + if isinstance(i, Clip): + # get media metadata.. + crange = None + try: + meta = i.media.metadata["metadata"]["shotgun"] + try: + crange = meta["version"]["attributes"]["sg_comp_range"] + except: + pass + + if crange is None: + try: + crange = meta["version"]["attributes"]["sg_comp_in"] + "-" + meta["version"]["attributes"]["sg_comp_out"] + except: + pass + + if crange is None: + try: + crange = meta["shot"]["attributes"]["sg_comp_range"] + except: + pass + + if crange is not None: + start, end = crange.split("-") + start = int(start) + end = int(end) + cfr = i.trimmed_range.rate() + + # modify clip range.. + i.active_range = FrameRange( + FrameRateDuration( + start, + cfr + ), + FrameRateDuration( + end-start, + cfr + ) + ) + + except Exception as e: + print(e) + +use_comp_range() diff --git a/share/snippets/clip/DNeg/trim_to_cut_range.py b/share/snippets/clip/DNeg/trim_to_cut_range.py new file mode 100644 index 000000000..874930a52 --- /dev/null +++ b/share/snippets/clip/DNeg/trim_to_cut_range.py @@ -0,0 +1,50 @@ +from xstudio.api.session.playlist.timeline import Timeline, Clip +from xstudio.core import FrameRange, FrameRateDuration, FrameRate + +def use_clip_range(item=XSTUDIO.api.session.viewed_container): + if isinstance(item, Timeline): + for i in item.selection: + if isinstance(i, Clip): + # get media metadata.. + crange = None + try: + meta = i.media.metadata["metadata"]["shotgun"] + try: + crange = meta["version"]["attributes"]["sg_cut_range"] + except: + pass + + if crange is None: + try: + crange = meta["version"]["attributes"]["sg_cut_in"] + "-" + meta["version"]["attributes"]["sg_cut_out"] + except: + pass + + if crange is None: + try: + crange = meta["shot"]["attributes"]["sg_cut_range"] + except: + pass + + if crange is not None: + start, end = crange.split("-") + start = int(start) + end = int(end) + cfr = i.trimmed_range.rate() + + # modify clip range.. + i.active_range = FrameRange( + FrameRateDuration( + start, + cfr + ), + FrameRateDuration( + end-start, + cfr + ) + ) + + except Exception as e: + print(e) + +use_clip_range() diff --git a/share/snippets/clip/Demo/random_colour.py b/share/snippets/clip/Demo/random_colour.py new file mode 100644 index 000000000..9dfcac4e5 --- /dev/null +++ b/share/snippets/clip/Demo/random_colour.py @@ -0,0 +1,15 @@ +from xstudio.api.session.playlist.timeline import Timeline, Clip +import random +import colorsys + +def random_clip_colour(item=XSTUDIO.api.session.viewed_container): + if isinstance(item, Timeline): + for i in item.selection: + if isinstance(i, Clip): + c = random.randrange(0, 255) + + rgb = colorsys.hsv_to_rgb(c * (1.0/255.0), 1, 1) + i.item_flag = "#FF" + f"{int(rgb[0]*255.0):0{2}x}"+ f"{int(rgb[1]*255.0):0{2}x}"+ f"{int(rgb[2]*255.0):0{2}x}" + +random_clip_colour() + diff --git a/share/snippets/demo.json b/share/snippets/demo.json deleted file mode 100644 index 4f22e228e..000000000 --- a/share/snippets/demo.json +++ /dev/null @@ -1,57 +0,0 @@ -[ - { - "name": "Hello World", - "description": "Example.", - "menu": "Demo", - "script": "print(\"Hello Word\")" - }, - { - "name": "Status", - "description": "Example.", - "menu": "Demo", - "script": "XSTUDIO.api.print_status()" - }, - { - "name": "Clear Thumbnails", - "description": "Clear thumbnail caches.", - "menu": "Demo", - "script": "from xstudio import demo; demo.clear_thumbnail_cache(XSTUDIO.api)" - }, - { - "name": "Sort Playlists", - "description": "Example.", - "menu": "Demo", - "script": "from xstudio import demo; demo.sort_session(XSTUDIO.api.session)" - }, - { - "name": "Capitalise Playlists", - "description": "Example.", - "menu": "Demo", - "script": "from xstudio import demo; demo.capitalise(XSTUDIO.api.session)" - }, - { - "name": "Dump shots", - "description": "Dump shot metadata with flag.", - "menu": "Demo", - "script": "from xstudio import demo; demo.dump_shots(XSTUDIO.api.session)" - }, - { - "name": "Dump Shots Editorial", - "description": "Dump shot metadata with flag.", - "menu": "DNeg", - "script": "from xstudio import demo; demo.dump_shots_gruff(XSTUDIO.api.session)" - }, - { - "name": "Render all annotations", - "description": "Dump exr renders of all annotations in the session to tmp dir.", - "menu": "Demo", - "script": "from xstudio import demo; demo.render_all_annotations(XSTUDIO.api.session)" - } - , - { - "name": "Demo Mask Plugin", - "description": "Foo Noo Poo Noo", - "menu": "Demo", - "script": "from xstudio import demo; demo.mask_plugin(XSTUDIO)" - } -] \ No newline at end of file diff --git a/share/snippets/playlist/DNeg/name_from_media_shot.py b/share/snippets/playlist/DNeg/name_from_media_shot.py new file mode 100644 index 000000000..d9b389b01 --- /dev/null +++ b/share/snippets/playlist/DNeg/name_from_media_shot.py @@ -0,0 +1,11 @@ +def name_from_media_shot(items=XSTUDIO.api.session.selected_containers): + for item in items: + try: + media = item.media[0] + project = media.metadata["metadata"]["shotgun"]["version"]["relationships"]["project"]["data"]["name"] + shot = media.metadata["metadata"]["shotgun"]["version"]["relationships"]["entity"]["data"]["name"] + item.name = project+" - "+shot + except: + pass + +name_from_media_shot() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 53f9178a6..9434a3058 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,81 +2,84 @@ include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/macros.cmake") if(INSTALL_PYTHON_MODULE OR BUILD_DOCS) - add_src_and_test(python_module) + add_src_and_test_always(python_module) endif () -if(INSTALL_XSTUDIO) - add_src_and_test(audio) - add_src_and_test(bookmark) - add_src_and_test(caf_utility) - add_src_and_test(colour_pipeline) - add_src_and_test(conform) - add_src_and_test(contact_sheet) - add_src_and_test(data_source) - add_src_and_test(embedded_python) - add_src_and_test(event) - add_src_and_test(global) - add_src_and_test(http_client) - add_src_and_test(launch/xstudio) - add_src_and_test(history) - add_src_and_test(media) - add_src_and_test(media_cache) - add_src_and_test(media_hook) - add_src_and_test(media_metadata) - add_src_and_test(media_reader) - add_src_and_test(module) - add_src_and_test(playhead) - add_src_and_test(playlist) - add_src_and_test(plugin/colour_pipeline) - add_src_and_test(plugin/colour_op) - add_src_and_test(plugin/conform) - add_src_and_test(plugin/data_source) - add_src_and_test(plugin/hud) - add_src_and_test(plugin/media_hook) - add_src_and_test(plugin/media_metadata) - add_src_and_test(plugin/media_reader) - add_src_and_test(plugin/utility) - add_src_and_test(plugin/viewport_overlay) - add_src_and_test(plugin_manager) - add_src_and_test(scanner) - add_src_and_test(session) - add_src_and_test(shotgun_client) - add_src_and_test(studio) - add_src_and_test(subset) - add_src_and_test(sync) - add_src_and_test(tag) - add_src_and_test(thumbnail) - add_src_and_test(ui) +add_src_and_test(audio) +add_src_and_test(bookmark) +add_src_and_test(caf_utility) +add_src_and_test(colour_pipeline) +add_src_and_test(conform) +add_src_and_test(contact_sheet) +add_src_and_test(data_source) +add_src_and_test(embedded_python) +add_src_and_test(global) +add_src_and_test(http_client) +add_src_and_test(launch/xstudio) +add_src_and_test(history) +add_src_and_test(media_cache) +add_src_and_test(media_hook) +add_src_and_test(media_metadata) +add_src_and_test(media_reader) +add_src_and_test(module) +add_src_and_test(playhead) +add_src_and_test(playlist) +add_src_and_test(plugin/colour_pipeline) +add_src_and_test(plugin/colour_op) +add_src_and_test(plugin/conform) +add_subdirectory(plugin/data_source) +add_src_and_test(plugin/hud) +add_src_and_test(plugin/media_hook) +add_src_and_test(plugin/media_metadata) +add_src_and_test(plugin/media_reader) +add_src_and_test(plugin/utility) +add_src_and_test(plugin/viewport_overlay) +add_src_and_test(plugin_manager) +add_src_and_test(plugin/viewport_layout) +add_subdirectory(plugin/python_plugins) +add_src_and_test(scanner) +add_src_and_test(session) +add_src_and_test(shotgun_client) +add_src_and_test(studio) +add_src_and_test(subset) +add_src_and_test(thumbnail) +add_src_and_test(ui) - if(BUILD_GLX_DEMO) - add_src_and_test(demos/glx_minimal_demo) # WIP bare bones, no QT player window - endif(BUILD_GLX_DEMO) +if(BUILD_GLX_DEMO) + add_src_and_test(demos/glx_minimal_demo) # WIP bare bones, no QT player window +endif(BUILD_GLX_DEMO) - if(BUILD_GRADING_DEMO) - add_src_and_test(demos/colour_op_plugins/source_grading_demo) - endif(BUILD_GRADING_DEMO) +if(BUILD_GRADING_DEMO) + add_src_and_test(demos/colour_op_plugins/source_grading_demo) +endif(BUILD_GRADING_DEMO) +if(INSTALL_XSTUDIO) + add_custom_target( + preferences-copy ALL DEPENDS + ${PREFERENCE_FILES} + ) endif () -add_src_and_test(broadcast) -add_src_and_test(global_store) -add_src_and_test(json_store) -add_src_and_test(timeline) -add_src_and_test(utility) +# always build as needed by python module. +add_src_and_test_always(global_store) +add_src_and_test_always(json_store) +add_src_and_test_always(timeline) +add_src_and_test_always(utility) +add_src_and_test_always(media) if (BUILD_MINIMAL_DEMO) - add_src_and_test(demos/minimal_demo) + add_src_and_test_always(demos/minimal_demo) endif () -if (BUILD_PYSIDE2_DEMOS) - find_package(Shiboken2 QUIET) - find_package(PySide2 QUIET) +if (BUILD_PYSIDE_WIDGETS) + find_package(Shiboken6 QUIET) + find_package(PySide6 QUIET) - if (INSTALL_PYTHON_MODULE AND Shiboken2_FOUND AND PySide2_FOUND) - message("Pyside2 and Shiboken2 packages found. Adding PySide2 demo to targets!") - add_src_and_test(pyside2_module) - add_src_and_test(demos/pyside_demo) + if (INSTALL_PYTHON_MODULE AND Shiboken6_FOUND AND PySide6_FOUND) + message("PySide6 and Shiboken6 packages found. Adding PySide6 demo to targets!") + add_src_and_test_always(pyside6_module) + add_src_and_test_always(demos/pyside_demo) else() - message("Pyside2 and Shiboken2 packages NOT found. PySide2 demo will not be built.") + message("PySide6 and Shiboken6 packages NOT found. PySide6 demo will not be built.") endif () endif () diff --git a/src/audio/src/CMakeLists.txt b/src/audio/src/CMakeLists.txt index 153ca43e8..b481aa023 100644 --- a/src/audio/src/CMakeLists.txt +++ b/src/audio/src/CMakeLists.txt @@ -1,4 +1,4 @@ -project(audio_output VERSION 0.1.0 LANGUAGES CXX) +project(audio_output VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) if(WIN32) # Additional Windows-specific configuration here. elseif(APPLE) @@ -8,6 +8,8 @@ else() find_package(PulseAudio REQUIRED) endif() +find_package(CAF COMPONENTS core io) + set(SOURCES audio_output.cpp audio_output_actor.cpp @@ -16,7 +18,7 @@ set(SOURCES if(WIN32) list(APPEND SOURCES windows_audio_output_device.cpp) elseif(APPLE) - # TODO: Apple-specific configuration here. + list(APPEND SOURCES macos_audio_output_device.cpp) else() list(APPEND SOURCES linux_audio_output_device.cpp) endif() @@ -26,20 +28,23 @@ add_library(xstudio::audio_output ALIAS ${PROJECT_NAME}) default_options(${PROJECT_NAME}) -target_link_libraries(${PROJECT_NAME} +SET(LINK_DEPS PUBLIC xstudio::utility xstudio::media_reader - caf::core + xstudio::module + CAF::core ) if(WIN32) # Link against Windows Core Audio libraries. - target_link_libraries(${PROJECT_NAME} PUBLIC "avrt.lib" "mmdevapi.lib") + list(APPEND LINK_DEPS PUBLIC "avrt.lib" "mmdevapi.lib") elseif(APPLE) - # TODO: Apple-specific audio libs + list(APPEND LINK_DEPS "-framework CoreFoundation" "-framework AudioToolbox") else() - target_link_libraries(${PROJECT_NAME} PUBLIC pulse pulse-simple) + list(APPEND LINK_DEPS PUBLIC pulse pulse-simple) endif() -set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) +target_link_libraries(${PROJECT_NAME} ${LINK_DEPS}) + +set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) \ No newline at end of file diff --git a/src/audio/src/audio_output.cpp b/src/audio/src/audio_output.cpp index 1b4d2a37f..2c3b0b583 100644 --- a/src/audio/src/audio_output.cpp +++ b/src/audio/src/audio_output.cpp @@ -15,8 +15,35 @@ using namespace xstudio::utility; using namespace xstudio::global_store; using namespace xstudio; +// when blocks of audio samples aren't contiguous then to avoid a transient +// at the border between the blocks we fade out samples at the tail of the +// current block and the head of the next block. The window for this fade out +// is defined here. It still causes distortion but much less bad. +#define FADE_FUNC_SAMPS 256 + namespace { +// sine envelope for fading/blending samples to avoid transients heading +// to the soundcard +static std::vector fade_in_coeffs; +static std::vector fade_out_coeffs; + +struct MakeCoeffs { + MakeCoeffs() { + if (fade_in_coeffs.empty()) { + fade_in_coeffs.resize(FADE_FUNC_SAMPS); + fade_out_coeffs.resize(FADE_FUNC_SAMPS); + for (int i = 0; i < FADE_FUNC_SAMPS; ++i) { + fade_out_coeffs[i] = + (cos(float(i) * M_PI / float(FADE_FUNC_SAMPS)) + 1.0f) * 0.5f; + fade_in_coeffs[i] = 1.0f - fade_out_coeffs[i]; + } + } + } +}; + +static MakeCoeffs s_mk_coeffs; + template void changing_volume_adjust( std::vector &samples, const double volume, const double last_vol) { @@ -115,6 +142,15 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( const int num_channels, const int fade_in_out); +template +bool copy_samples_to_scrub_buffer( + T *&stream, + const long total_samps_needed, + long &total_samps_pushed, + media_reader::AudioBufPtr ¤t_buf, + const long buffer_offset_sample, + const long num_channels); + template void reverse_audio_buffer(const T *in, T *out, const int num_channels, const int num_samples); @@ -122,7 +158,7 @@ template media_reader::AudioBufPtr super_simple_respeed_audio_buffer(const media_reader::AudioBufPtr in, const float velocity); -void AudioOutputControl::prepare_samples_for_soundcard( +void AudioOutputControl::prepare_samples_for_soundcard_playback( std::vector &v, const long num_samps_to_push, const long microseconds_delay, @@ -131,18 +167,10 @@ void AudioOutputControl::prepare_samples_for_soundcard( try { - v.resize(num_samps_to_push * num_channels); - - memset(v.data(), 0, v.size() * sizeof(int16_t)); - int16_t *d = v.data(); long n = num_samps_to_push; long num_samps_pushed = 0; - - if (muted()) - return; - while (n > 0) { if (!current_buf_ && sample_data_.size()) { @@ -163,26 +191,31 @@ void AudioOutputControl::prepare_samples_for_soundcard( // is audio playback stable ? i.e. is the next sample buffer // continuous with the one we are about to play? - auto next_buf = pick_audio_buffer( + next_buf_ = pick_audio_buffer( next_sample_play_time + std::chrono::microseconds( int(round(current_buf_->duration_seconds() * 1000000.0))), false); fade_in_out_ = check_if_buffer_is_contiguous_with_previous_and_next( - current_buf_, next_buf, previous_buf_); + current_buf_, next_buf_, previous_buf_); + + /*if (fade_in_out_ != NoFade) { + if (previous_buf_) + std::cerr << "P " << to_string(previous_buf_->media_key()) << "\n"; + if (current_buf_) + std::cerr << "C " << to_string(current_buf_->media_key()) << "\n"; + if (next_buf_) + std::cerr << "N " << to_string(next_buf_->media_key()) << "\n\n"; + + }*/ } else { - // spdlog::warn("Break hit because current_buf_ is null after trying to pick - // " - // "an audio buffer."); fade_in_out_ = DoFadeHeadAndTail; break; } } else if (!current_buf_ && sample_data_.empty()) { - // spdlog::warn("Break hit because both current_buf_ and sample_data_ are - // empty."); break; } @@ -196,146 +229,247 @@ void AudioOutputControl::prepare_samples_for_soundcard( fade_in_out_); if (current_buf_pos_ == (long)current_buf_->num_samples()) { - // current buf is exhausted - // spdlog::info("Current buffer is exhausted."); + // current buf is exhausted, clear current_buf_ so we pick + // a new one on next pass through this loop previous_buf_ = current_buf_; current_buf_.reset(); } else { - // spdlog::warn("Break hit due to unspecified condition."); break; } } - - const float vol = volume(); - static float last_vol = vol; - if (last_vol != vol) { - changing_volume_adjust(v, vol / 100.0f, last_vol / 100.0f); + const float vol = volume(); + if (last_volume_ != vol) { + changing_volume_adjust(v, vol / 100.0f, last_volume_ / 100.0f); } else if (vol != 100.0f) { static_volume_adjust(v, vol / 100.0f); } - last_vol = vol; + last_volume_ = vol; } catch (std::exception &e) { spdlog::debug("{} {}", __PRETTY_FUNCTION__, e.what()); } } -void AudioOutputControl::queue_samples_for_playing( +void AudioOutputControl::prepare_samples_for_audio_scrubbing( const std::vector &audio_frames, - const bool playing, - const bool forwards, - const float velocity) { + const timebase::flicks playhead_position) { + + playhead_position_ = playhead_position; + + // audio_frames is unlikely to have a frame with a timestamp that exactly + // matches playhead_position. So we pick the first frame with a timestamp + // that is less than playhead position and then start copying samples from + // the appropriate position. + + // Also note that the timestamps in audio_frames correspond exactly to video + // frame timestamps ... however, because ffmpeg typically delivers audio in + // chunks that don't line up with video frames (for example, often an audio + // frame from ffmpeg is 1024 samples, regardless of sample rate and video + // frame rate). In the xstudio ffmpeg reader we are just gluing together + // the smaller audio frames to roughly match the beat of the video frames + // but when we do this we store the offset between the first audio stamp + // and the 'timeline_timestamp' so we can take account of the difference. + + // store bufs in our map + sample_data_.clear(); + for (const auto &_frame : audio_frames) { - if (!playing) { + if (!_frame) + continue; + const auto adjusted_timeline_timestamp = std::chrono::duration_cast( + _frame.timeline_timestamp() + _frame->time_delta_to_video_frame()); + + sample_data_[adjusted_timeline_timestamp] = _frame; + } + + // now pick nearest buffer + auto r = sample_data_.lower_bound(playhead_position_); + if (r != sample_data_.begin()) { + r--; + } + if (r == sample_data_.end()) + return; + + const long num_channels = r->second->num_channels(); + + // time diff from first audio sample to playhead position + auto delta = playhead_position_ - r->first; + + long samples_offset = long( + round(std::max(0.0, timebase::to_seconds(delta)) * double(r->second->sample_rate()))); + + + if (samples_offset < 0 || samples_offset >= r->second->num_samples()) { + // the offset into our 'best' audio buffer from the playhead_position_ + // to the timestamp for the audio buf is outside the duration of the + // audio buffer + // we haven't found an audio buffer that corresponds to the playhead_position_ + // so must clear the buffer (i.e. play no audio) + scrubbing_samples_buf_.clear(); return; } - playback_velocity_ = audio_repitch_ ? std::max(0.1f, velocity) : 1.0f; + // what's the video frame rate ? We can deduce this from the timeline + // timestamps which match corresponding video frames for the scrub. We + // should have been delivered 2 or more coniguouse audio frames for the + // scrub. We use the audio buffer duration as a fallback + timebase::flicks video_frame_duration = timebase::to_flicks(r->second->duration_seconds()); + auto r_plus = r; + r_plus++; + if (r_plus != sample_data_.end()) { + video_frame_duration = r_plus->first - r->first; + } - /* - // Earlier attempt at resampling in queue; needs a more reliable sample rate info and needs - sample rate from output device. if (audio_frames.size()) { auto audio_sample_rate = - audio_frames.front()->sample_rate(); if (audio_sample_rate == 0) { audio_sample_rate = - audio_frames.back()->sample_rate(); - } + // how many samples do we want to sound for this scrub event? + long num_scrub_samps = long( + round(timebase::to_seconds(video_frame_duration) * double(r->second->sample_rate()))); + + // now we fill our scrub samples buffer. + + // What if there are samples left in the buffer from the previous scrub event that + // haven't played out yet? We want to overwrite them, but we need to do a + // blend so there isn't a discontinuity with the last samples that were + // dispatched to the soundcard + const size_t data_in_buffer = scrubbing_samples_buf_.size(); + scrubbing_samples_buf_.resize(num_scrub_samps * num_channels); + + if (data_in_buffer < scrubbing_samples_buf_.size()) { + // any new samples added to buffer must be zero + memset( + scrubbing_samples_buf_.data() + data_in_buffer, + 0, + (scrubbing_samples_buf_.size() - data_in_buffer) * sizeof(int16_t)); + } - if (audio_sample_rate == 0) { - // If we can't get the sample rate from anything, use the last best guess. - // This seems to happen - audio_sample_rate = last_sample_rate_; - } else { - last_sample_rate_ = audio_sample_rate; - } + int16_t *samps = scrubbing_samples_buf_.data(); + long n = 0; + while (r != sample_data_.end() && r->second && + copy_samples_to_scrub_buffer( + samps, num_scrub_samps, n, r->second, samples_offset, num_channels)) { + samples_offset = 0; + r++; + } - // If our audio card does not match the source rate, we need to respeed/repitch the - samples. if (audio_sample_rate and audio_sample_rate != 96000L) { double sample_respeed = - (double)audio_sample_rate / 96000.0; playback_velocity_ *= sample_respeed; audio_repitch_ = - true; + // we need to modulate the last samples put into the buffer with an envelope + // that smoothly takes the amplitude to zero so we don't get transients + // going to the soundcard should we not be streaming more samples to + // the soundcard after this buffer is exhausted + + long fade_samps = std::min(long(FADE_FUNC_SAMPS), n); + samps = scrubbing_samples_buf_.data() + (n - fade_samps) * num_channels; + float *f = fade_out_coeffs.data() + long(FADE_FUNC_SAMPS) - fade_samps; + while (fade_samps) { + for (int chn = 0; chn < num_channels; ++chn) { + (*samps++) = round((*samps) * (*f)); } + f++; + fade_samps--; } - */ +} +long AudioOutputControl::copy_samples_to_buffer_for_scrubbing( + std::vector &v, const long num_samps_to_push) { + + size_t nn = std::min(v.size(), scrubbing_samples_buf_.size()); + if (!nn) + return 0; + memcpy(v.data(), scrubbing_samples_buf_.data(), nn * sizeof(int16_t)); + + if (nn < scrubbing_samples_buf_.size()) { + std::vector remaining_samps(scrubbing_samples_buf_.size() - nn); + memcpy( + remaining_samps.data(), + scrubbing_samples_buf_.data() + nn, + (scrubbing_samples_buf_.size() - nn) * sizeof(int16_t)); + scrubbing_samples_buf_ = std::move(remaining_samps); + } else { + scrubbing_samples_buf_.clear(); + } - for (const auto &a : audio_frames) { + if (volume() != 100.0f) { + static_volume_adjust(v, volume() / 100.0f); + } - auto audio_frame = a; - // if we're not playing and the last audio buffer played is the same as - // the one we're receiving now we don't queue it for playing. This is because - // a viewport refresh (for e.g. exposure scrubbing) results in the same - // image frame being broadcast by the playhead - if (!audio_frame || - (previous_buf_ && previous_buf_->media_key() == audio_frame->media_key()) || - (current_buf_ && current_buf_->media_key() == audio_frame->media_key()) || - !audio_frame->num_samples()) { + return (long)nn; +} - // spdlog::info("Audio frame skipped due to either being null, matching " - // "previous/current buffer or having no samples."); - continue; - } +void AudioOutputControl::queue_samples_for_playing( + const std::vector &audio_frames) { - // spdlog::info("Processing audio frame with media key: {}, num samples: {}, sample - // rate: {}, num channels: {}", - // audio_frame->media_key(), audio_frame->num_samples(), - // audio_frame->sample_rate(), audio_frame->num_channels()); + timebase::flicks t0; + if (audio_frames.size()) { + t0 = audio_frames[0].timeline_timestamp(); + } + timebase::flicks o; + for (const auto &_frame : audio_frames) { // xstudio stores a frame of audio samples for every video frame for any // given source (if the source has no video it is assigned a 'virtual' video - // frame rate to maintain this approach). However, audio frames generally + // frame rate to maintain this approach). + // + // However, audio frames generally // do not have the same duration as video frames, so there is always some // offset between when the video frame is shown and when the audio samples - // associated with that frame should sound. - const auto when_to_sound_audio = - audio_frame.when_to_display_ + audio_frame->time_delta_to_video_frame(); - - // have we already got these audio samples in our queue? If so erase and - // add back in to update the key - if (false) { - for (auto p = sample_data_.begin(); p != sample_data_.end(); ++p) { - if (p->second->media_key() == audio_frame->media_key()) { - // spdlog::info("Found and erasing existing audio sample from queue with the - // " - // "same media key."); - sample_data_.erase(p); - break; - } - } - } - + // associated with that frame should sound. When building sample_data_ + // map we take account of this difference, which was calculate in + // the fffmpeg reader when the audio samples are packaged up. + const auto adjusted_timeline_timestamp = std::chrono::duration_cast( + _frame.timeline_timestamp() + + (_frame ? _frame->time_delta_to_video_frame() : std::chrono::microseconds(0))); + + if (_frame) + t0 += timebase::to_flicks(_frame->duration_seconds()); + else + continue; - if (audio_repitch_ && playback_velocity_ != 1.0f) { - audio_frame = super_simple_respeed_audio_buffer( - audio_frame, fabs(playback_velocity_)); - } + media_reader::AudioBufPtr frame = + audio_repitch_ && playback_velocity_ != 1.0f + ? super_simple_respeed_audio_buffer(_frame, playback_velocity_) + : _frame; - if (!forwards) { + if (!playing_forward_) { - media_reader::AudioBufPtr reversed( - new media_reader::AudioBuffer(audio_frame->params())); + media_reader::AudioBufPtr reversed(new media_reader::AudioBuffer(frame->params())); reversed->allocate( - audio_frame->sample_rate(), - audio_frame->num_channels(), - audio_frame->num_samples(), - audio_frame->sample_format()); + frame->sample_rate(), + frame->num_channels(), + frame->num_samples(), + frame->sample_format()); reverse_audio_buffer( - (const int16_t *)audio_frame->buffer(), + (const int16_t *)frame->buffer(), (int16_t *)reversed->buffer(), - audio_frame->num_samples(), - audio_frame->num_channels()); + frame->num_samples(), + frame->num_channels()); - sample_data_[when_to_sound_audio] = reversed; + sample_data_[adjusted_timeline_timestamp] = reversed; reversed->set_reversed(true); - - reversed->set_display_timestamp_seconds(audio_frame->display_timestamp_seconds()); + reversed->set_display_timestamp_seconds(frame->display_timestamp_seconds()); } else { - sample_data_[when_to_sound_audio] = audio_frame; + sample_data_[adjusted_timeline_timestamp] = frame; } } } +void AudioOutputControl::playhead_position_changed( + const timebase::flicks playhead_position, + const bool forward, + const float velocity, + const bool playing, + utility::time_point when_position_changed) { + if (!playing_ && playhead_position == playhead_position_) { + // playhead hasn't moved + } + playhead_position_ = playhead_position; + playback_velocity_ = std::max(0.1f, velocity); + playing_ = playing; + playing_forward_ = forward; + playhead_position_update_tp_ = when_position_changed; +} + void AudioOutputControl::clear_queued_samples() { sample_data_.clear(); current_buf_.reset(); @@ -344,10 +478,60 @@ void AudioOutputControl::clear_queued_samples() { media_reader::AudioBufPtr AudioOutputControl::pick_audio_buffer( const utility::clock::time_point &tp, bool drop_old_buffers) { - auto r = sample_data_.lower_bound(tp); + // The idea here is we pick an audio buffer from sample_data_ to draw + // samples off and stream to the soundcard. - if (r == sample_data_.end()) - return media_reader::AudioBufPtr(); + // sample_data_ is a map of audio buffers stored against their play timestamp + // in the playhead timeline. So for example frame zero in the timeline + // has timestamp = 0. Frame 2 has timestamp = 41ms (for a 24fps source). + + // 'tp' here is a system clock timepoint that tells us when the last + // audio sample currently in the soundcard sample buffer will actually + // get played. + + // based on 'tp' - we estimate the playhead position when the + // first sample of the next audio buffer that we pick to put into the + // soundcard buffer will sound. + + // predict where we will be at the timepoint 'next_video_refresh' ... + const timebase::flicks delta = + std::chrono::duration_cast(tp - playhead_position_update_tp_); + const double v = (playing_forward_ ? 1.0f : -1.0f) * playback_velocity_; + + auto future_playhead_position = + timebase::to_flicks(v * timebase::to_seconds(delta)) + playhead_position_; + + + // during playback, we just pick the audio buffer immediately after + // the one that we last used. However, we check if this new buffer's + // position in the playback timeline matches well with our estimate of + // where the playhead will be when the audio samples hit the speakers. + // + // If it doesn't we continue and use a best match search below + + // let's step from the last audio buffer we used to the next... + auto p = sample_data_.find(last_buffer_pts_); + if (p != sample_data_.end()) { + if (playing_forward_) { + p++; + } else if (!playing_forward_ && p != sample_data_.begin()) { + p--; + } + + auto drift = timebase::to_seconds(future_playhead_position - p->first); + if (fabs(drift) < 0.05) { + if (drop_old_buffers) + last_buffer_pts_ = p->first; + return p->second; + } + } + + auto r = sample_data_.lower_bound(future_playhead_position); + + if (r == sample_data_.end()) { + auto r = media_reader::AudioBufPtr(); + return r; + } // gtp et the audio buf with a 'show' time that is CLOSEST // to now, need to look at the previous element to see if @@ -355,30 +539,37 @@ media_reader::AudioBufPtr AudioOutputControl::pick_audio_buffer( if (r != sample_data_.begin()) { auto r2 = r; r2--; - const auto d2 = tp - r2->first; - const auto d1 = r->first - tp; + const auto d2 = future_playhead_position - r2->first; + const auto d1 = r->first - future_playhead_position; if (d1 > d2) { r = r2; } } - media_reader::AudioBufPtr v = r->second; + media_reader::AudioBufPtr buf = r->second; + if (drop_old_buffers) + last_buffer_pts_ = r->first; - // if the audio buffer is not supposed to be played close to 'tp' we want to carry on and - // play silence until soundcard and audio buffers are in sync - const auto delta = - double(std::chrono::duration_cast(r->first - tp).count()) / - 1000000.0; - // if (0) {//fabs(delta) > v->duration_seconds()/2) { - // return media_reader::AudioBufPtr(); - // } + // what if our 'best' buffer, i.e. the one nearest to 'future_playhead_position' + // is still not close. Some innaccuracy is happening, e.g. buffers that we need + // haven't been delivered. We must play silence instead, + const auto t_mismatch = double(std::chrono::duration_cast( + r->first - future_playhead_position) + .count()) / + 1000000.0; - if (drop_old_buffers) { - r++; + // so if we are more than half the duration of the buffer out, return empty ptr + if (!buf || fabs(t_mismatch) > buf->duration_seconds() / 2) { + return media_reader::AudioBufPtr(); + } + + if (drop_old_buffers && playing_forward_) { sample_data_.erase(sample_data_.begin(), r); + } else if (drop_old_buffers) { + sample_data_.erase(r, sample_data_.end()); } - return v; + return buf; } AudioOutputControl::Fade @@ -438,6 +629,7 @@ AudioOutputControl::check_if_buffer_is_contiguous_with_previous_and_next( previous_buf_->display_timestamp_seconds()) / playback_velocity_ - previous_buf_->duration_seconds(); + if (fabs(delta) > 0.001) { result |= DoFadeHead; } @@ -450,11 +642,6 @@ AudioOutputControl::check_if_buffer_is_contiguous_with_previous_and_next( return (AudioOutputControl::Fade)result; } -// when blocks of audio samples aren't contiguous then to avoid a transient -// at the border between the blocks we fade out samples at the tail of the -// current block and the head of the next block. The window for this fade out -// is defined here. It still causes distortion but much less bad. -#define FADE_FUNC_SAMPS 128 template void copy_from_xstudio_audio_buffer_to_soundcard_buffer( @@ -466,14 +653,6 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( const int num_channels, const int fade_in_out) { - static std::vector fade_coeffs; - if (fade_coeffs.empty()) { - fade_coeffs.resize(FADE_FUNC_SAMPS); - for (int i = 0.0f; i < FADE_FUNC_SAMPS; ++i) { - fade_coeffs[i] = (sin(float(i) * M_PI / float(FADE_FUNC_SAMPS)) + 1.0f) * 0.5f; - } - } - if (fade_in_out == AudioOutputControl::NoFade) { if (((long)current_buf->num_samples() - (long)current_buf_position) > @@ -510,7 +689,7 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( current_buf_position < current_buf->num_samples()) { for (int chn = 0; chn < num_channels; ++chn) { - (*stream++) = T(round((*tt++) * fade_coeffs[current_buf_position])); + (*stream++) = T(round((*tt++) * fade_in_coeffs[current_buf_position])); } num_samples_to_copy--; current_buf_position++; @@ -537,7 +716,7 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( while (num_samples_to_copy && current_buf_position < current_buf->num_samples()) { const int i = current_buf->num_samples() - current_buf_position - 1; - const float f = i < FADE_FUNC_SAMPS ? fade_coeffs[i] : 1.0f; + const float f = i < FADE_FUNC_SAMPS ? fade_in_coeffs[i] : 1.0f; for (int chn = 0; chn < num_channels; ++chn) { (*stream++) = T(round((*tt++) * f)); @@ -551,6 +730,44 @@ void copy_from_xstudio_audio_buffer_to_soundcard_buffer( } } +template +bool copy_samples_to_scrub_buffer( + T *&stream, + const long total_samps_needed, + long &total_samps_pushed, + media_reader::AudioBufPtr ¤t_buf, + const long buffer_offset_sample, + const long num_channels) { + + + const long buffer_size_samples = current_buf->num_samples(); + long buffer_position = buffer_offset_sample; + + T *tt = ((T *)current_buf->buffer()) + buffer_offset_sample * num_channels; + + long n = total_samps_needed; + // when we fill the first samples of 'stream' we blend with any samples + // already in the buffer to avoid the transients + while (total_samps_pushed < FADE_FUNC_SAMPS && buffer_position < buffer_size_samples && n) { + + const float f = fade_in_coeffs[total_samps_pushed]; + // blend incoming samps with whatever samps are already in the buffer + for (int chn = 0; chn < num_channels; ++chn) { + (*stream++) = T(round((*tt++) * f)) + T(round((*stream) * (1.0f - f))); + } + buffer_position++; + total_samps_pushed++; + n--; + } + + long remaining_samples = std::min( + buffer_size_samples - buffer_position, total_samps_needed - total_samps_pushed); + memcpy(stream, tt, remaining_samples * sizeof(T) * num_channels); + stream += remaining_samples * num_channels; + total_samps_pushed += remaining_samples; + + return total_samps_pushed < total_samps_needed; +} template void reverse_audio_buffer(const T *in, T *out, const int num_samples, const int num_channels) { diff --git a/src/audio/src/audio_output_actor.cpp b/src/audio/src/audio_output_actor.cpp index cfbdd1da7..2079b56a5 100644 --- a/src/audio/src/audio_output_actor.cpp +++ b/src/audio/src/audio_output_actor.cpp @@ -1,5 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include + #include #include @@ -7,7 +9,6 @@ #include "xstudio/audio/audio_output_actor.hpp" #include "xstudio/global_store/global_store.hpp" #include "xstudio/media_reader/image_buffer.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/helpers.hpp" @@ -18,12 +19,175 @@ #ifdef _WIN32 #include "xstudio/audio/windows_audio_output_device.hpp" #endif +#ifdef __APPLE__ +#include "xstudio/audio/macos_audio_output_device.hpp" +#endif using namespace caf; using namespace xstudio::audio; using namespace xstudio::utility; using namespace xstudio; +/* Notes: + +In order to support multiple audio outputs (e.g. PC soundcard, SDI output card +etc.) receiving samples from multiple playing playheads, the audio output model +is a somewhat complex. The model for audio playback is described as follows: + +During playback a PlayheadActor delivers audio samples to the singleton +GlobalAudioOutputActor instance. + +The GlobalAudioOutputActor then forwards the samples data to AudioOutputActors +that then deliver the samples to sound devices. + +For the active playhead in the xSTUDIO's main UI, the audio samples from that +playhead are forwarded via the event_group of the GlobalAudioOutputActor. +The default PC audio AudioOutputActor, plus AudioOutputActor from video output +plugins (e.g. SDI card AV output plugin) will receive and play audio from this +event_group. + +For playheads belonging to the xSTUDIO 'quicview' light viewers, a separate +instance of the AudioOutputActor delivering to the PC audio is created and +samples from those playheads are delivered to these separate AudioOutputActor +instances. This allows independent audio playback in the quickviewer windows +from the main xSTUDIO UI. + +*/ + + +void AudioOutputActor::init() { + + // spdlog::debug("Created AudioOutputControlActor {}", OutputClassType::name()); + utility::print_on_exit(this, "AudioOutputControlActor"); + + audio_output_device_ = + spawn(caf::actor_cast(this), output_device_); + link_to(audio_output_device_); + + auto global_audio_actor = + system().registry().template get(audio_output_registry); + + if (is_global_) { + // we only want to receive audio via the global_audio_actor event group + // if we are marked as a global AudioOutputActor - this is how we play + // audio from whatever is playing in the xstudio main UI. + utility::join_event_group(this, global_audio_actor); + } + + behavior_.assign( + + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + + [=](utility::event_atom, playhead::play_atom, const bool is_playing) { + mail(utility::event_atom_v, playhead::play_atom_v, is_playing) + .send(audio_output_device_); + if (!is_playing) + clear_queued_samples(); + }, + + [=](get_samples_for_soundcard_atom, + const long num_samps_to_push, + const long microseconds_delay, + const int num_channels, + const int sample_rate) -> result> { + // this message is a request from the AudioOutputDeviceActor to + // get samples for the soundcard + + std::vector samples; + samples.resize(num_samps_to_push * num_channels); + memset(samples.data(), 0, samples.size() * sizeof(int16_t)); + + if (muted() || (!playing_ && !audio_scrubbing_)) + return samples; + + try { + + if (!playing_) { + + long n = copy_samples_to_buffer_for_scrubbing(samples, num_samps_to_push); + if (!n) { + // we have no audio to play back (e.g. scrubbed audio) + // If more than 2s have elapsed since last samples then + // by clearing 'samples' here this stops the playback + // loop feeding samples to the soundcard + if (std::chrono::duration_cast( + utility::clock::now() - last_audio_sounding_tp_) + .count() > 2) { + samples.clear(); + } + } else { + last_audio_sounding_tp_ = utility::clock::now(); + } + + } else { + prepare_samples_for_soundcard_playback( + samples, + num_samps_to_push, + microseconds_delay, + num_channels, + sample_rate); + } + + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } + return samples; + }, + [=](set_override_volume_atom, const float volume) { set_override_volume(volume); }, + [=](utility::event_atom, + module::change_attribute_event_atom, + const float volume, + const bool muted, + const bool repitch, + const bool scrubbing) { set_attrs(volume, muted, repitch, scrubbing); }, + [=](utility::event_atom, + playhead::sound_audio_atom, + const std::vector &audio_buffers, + const utility::Uuid &sub_playhead, + const bool scrubbing, + const timebase::flicks playhead_position) { + if (scrubbing) { + + prepare_samples_for_audio_scrubbing(audio_buffers, playhead_position); + mail(utility::event_atom_v, playhead::play_atom_v).send(audio_output_device_); + + } else { + + if (sub_playhead != sub_playhead_uuid_ || scrubbing) { + // sound is coming from a different source to + // previous time + clear_queued_samples(); + sub_playhead_uuid_ = sub_playhead; + } + queue_samples_for_playing(audio_buffers); + mail(utility::event_atom_v, playhead::play_atom_v).send(audio_output_device_); + } + }, + [=](utility::event_atom, + playhead::position_atom, + const timebase::flicks playhead_position, + const bool forward, + const float velocity, + const bool playing, + utility::time_point when_position_changed) { + // these event messages are very fine-grained, so we know very accurately the + // playhead position during playback + playhead_position_changed( + playhead_position, forward, velocity, playing, when_position_changed); + }, + [=](utility::event_atom, + audio::audio_samples_atom, + const std::vector &audio_buffers, + timebase::flicks playhead_position, + const utility::Uuid &playhead_uuid) { + + }); + + // kicks the global samples actor to update us with current volume etc. + mail(module::change_attribute_event_atom_v, caf::actor_cast(this)) + .send(global_audio_actor); +} + GlobalAudioOutputActor::GlobalAudioOutputActor(caf::actor_config &cfg) : caf::event_based_actor(cfg), module::Module("GlobalAudioOutputActor") { @@ -37,12 +201,10 @@ GlobalAudioOutputActor::GlobalAudioOutputActor(caf::actor_config &cfg) volume_ = add_float_attribute("volume", "volume", 100.0f, 0.0f, 100.0f, 0.05f); volume_->set_role_data(module::Attribute::PreferencePath, "/core/audio/volume"); - - // by setting static UUIDs on these module we only create them once in the UI - volume_->set_role_data(module::Attribute::Groups, nlohmann::json{"audio_output"}); + volume_->set_role_data(module::Attribute::UIDataModels, nlohmann::json{"audio_output"}); muted_ = add_boolean_attribute("muted", "muted", false); - muted_->set_role_data(module::Attribute::Groups, nlohmann::json{"audio_output"}); + muted_->set_role_data(module::Attribute::UIDataModels, nlohmann::json{"audio_output"}); muted_->set_role_data(module::Attribute::PreferencePath, "/core/audio/muted"); spdlog::debug("Created GlobalAudioOutputActor"); @@ -55,30 +217,95 @@ GlobalAudioOutputActor::GlobalAudioOutputActor(caf::actor_config &cfg) set_parent_actor_addr(actor_cast(this)); + mute_hotkey_ = register_hotkey("/", "Mute Audio", "Mute/Un-mute audio.", true, "Playback"); + behavior_.assign( [=](utility::get_event_group_atom) -> caf::actor { return event_group_; }, [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](playhead::play_atom, const bool is_playing) { - send(event_group_, utility::event_atom_v, playhead::play_atom_v, is_playing); + [=](playhead::play_atom, + const bool is_playing, + bool global, + const utility::Uuid &playhead_uuid) { + auto dest = global ? event_group_ : independent_output(playhead_uuid); + if (dest) { + mail(utility::event_atom_v, playhead::play_atom_v, is_playing).send(dest); + } + }, + [=](playhead::position_atom, + const timebase::flicks playhead_position, + const bool forward, + const float velocity, + const bool playing, + utility::time_point when_position_changed, + bool global, + const utility::Uuid &playhead_uuid) { + auto dest = global ? event_group_ : independent_output(playhead_uuid); + + if (dest) { + mail( + utility::event_atom_v, + playhead::position_atom_v, + playhead_position, + forward, + velocity, + playing, + when_position_changed) + .send(dest); + } }, [=](playhead::sound_audio_atom, const std::vector &audio_buffers, - const Uuid &sub_playhead, - const bool playing, - const bool forwards, - const float velocity) { - send( - event_group_, + const Uuid &sub_playhead_id, + bool global, + const utility::Uuid &playhead_uuid, + const bool scrubbing, + const timebase::flicks playhead_position) { + auto dest = global ? event_group_ : independent_output(playhead_uuid); + + if (dest) { + mail( + utility::event_atom_v, + playhead::sound_audio_atom_v, + audio_buffers, + sub_playhead_id, + scrubbing, + playhead_position) + .send(dest); + } + }, + [=](module::change_attribute_event_atom, caf::actor requester) { + mail( utility::event_atom_v, - playhead::sound_audio_atom_v, + module::change_attribute_event_atom_v, + volume_->value(), + muted_->value(), + audio_repitch_->value(), + audio_scrubbing_->value()) + .send(requester); + }, + [=](audio::audio_samples_atom, + const std::vector &audio_buffers, + timebase::flicks playhead_position, + const utility::Uuid &playhead_uuid) { + mail( + utility::event_atom_v, + audio::audio_samples_atom_v, audio_buffers, - sub_playhead, - playing, - forwards, - velocity); + playhead_position, + playhead_uuid) + .send(event_group_); + }, + [=](playhead::sound_audio_atom, const utility::Uuid &playhead_uuid, bool) { + // playhead is exiting, if the playhead has its own audid output + // actor, kill it now + auto p = independent_outputs_.find(playhead_uuid); + if (p != independent_outputs_.end()) { + send_exit(p->second, caf::exit_reason::user_shutdown); + independent_outputs_.erase(p); + } } ); @@ -91,14 +318,74 @@ void GlobalAudioOutputActor::on_exit() { system().registry().erase(audio_output_ void GlobalAudioOutputActor::attribute_changed(const utility::Uuid &attr_uuid, const int role) { // update and audio output clients with volume, mute etc. - send( - event_group_, + mail( utility::event_atom_v, module::change_attribute_event_atom_v, volume_->value(), muted_->value(), audio_repitch_->value(), - audio_scrubbing_->value()); + audio_scrubbing_->value()) + .send(event_group_); + + for (auto &p : independent_outputs_) { + mail( + utility::event_atom_v, + module::change_attribute_event_atom_v, + volume_->value(), + muted_->value(), + audio_repitch_->value(), + audio_scrubbing_->value()) + .send(p.second); + } Module::attribute_changed(attr_uuid, role); } + +void GlobalAudioOutputActor::hotkey_pressed( + const utility::Uuid &hotkey_uuid, const std::string &context, const std::string &window) { + + if (hotkey_uuid == mute_hotkey_) { + muted_->set_value(!muted_->value()); + } +} + +caf::actor GlobalAudioOutputActor::independent_output(const utility::Uuid &playhead_uuid) { + + auto p = independent_outputs_.find(playhead_uuid); + if (p != independent_outputs_.end()) { + return p->second; + } + + try { + JsonStore prefs_jsn; + auto prefs = global_store::GlobalStoreHelper(system()); + join_broadcast(this, prefs.get_group(prefs_jsn)); + +#ifdef __linux__ + auto output_actor = spawn( + std::shared_ptr( + new audio::LinuxAudioOutputDevice(prefs_jsn)), + false); +#elif defined(__APPLE__) + // TO DO + auto output_actor = spawn( + std::shared_ptr( + new audio::MacOSAudioOutputDevice(prefs_jsn)), + false); +#elif defined(_WIN32) + auto output_actor = spawn( + std::shared_ptr( + new audio::WindowsAudioOutputDevice(prefs_jsn)), + false); +#endif + + link_to(output_actor); + independent_outputs_[playhead_uuid] = output_actor; + return output_actor; + } catch (std::exception &e) { + independent_outputs_[playhead_uuid] = caf::actor(); + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + return caf::actor(); +} \ No newline at end of file diff --git a/src/audio/src/linux_audio_output_device.cpp b/src/audio/src/linux_audio_output_device.cpp index 9dc3e7e0c..8f0f0172a 100644 --- a/src/audio/src/linux_audio_output_device.cpp +++ b/src/audio/src/linux_audio_output_device.cpp @@ -16,6 +16,8 @@ LinuxAudioOutputDevice::~LinuxAudioOutputDevice() { disconnect_from_soundcard(); void LinuxAudioOutputDevice::disconnect_from_soundcard() { if (playback_handle_) { + int error; + pa_simple_flush(playback_handle_, &error); pa_simple_free(playback_handle_); } playback_handle_ = nullptr; @@ -23,6 +25,9 @@ void LinuxAudioOutputDevice::disconnect_from_soundcard() { void LinuxAudioOutputDevice::connect_to_soundcard() { + if (playback_handle_) + return; + num_channels_ = 2; std::string sound_card("default"); @@ -48,6 +53,15 @@ void LinuxAudioOutputDevice::connect_to_soundcard() { int error; + // TODO: Configure for low latency. Currently latency is about 130ms - too + // long! But reducing the buffer size always causes distortion and I have + // not yet worked out why. + + /*pa_buffer_attr buf_attrs; + buf_attrs.maxlength = buffer_size_*2*2; + buf_attrs.tlength = buffer_size_*2; + buf_attrs.prebuf = buffer_size_;*/ + /* Create a new playback stream */ if (!(playback_handle_ = pa_simple_new( nullptr, @@ -57,36 +71,44 @@ void LinuxAudioOutputDevice::connect_to_soundcard() { "playback", &pa_ss, nullptr, - nullptr, + nullptr, /*&buf_attrs,*/ &error))) { - fprintf(stderr, __FILE__ ": pa_simple_new() failed: %s\n", pa_strerror(error)); - std::stringstream ss; - ss << __FILE__ ": pa_simple_new() failed: " << pa_strerror(error); - throw std::runtime_error(ss.str().c_str()); + std::string err = fmt::format( + "{} pa_simple_new() failed: {} ", __PRETTY_FUNCTION__, pa_strerror(error)); + throw std::runtime_error(err.c_str()); } spdlog::debug("{} Connected to soundcard : {} ", __PRETTY_FUNCTION__, sound_card.c_str()); } -long LinuxAudioOutputDevice::desired_samples() { return buffer_size_; } +long LinuxAudioOutputDevice::desired_samples() { + + return buffer_size_; + + // TODO: proper buffer top-up logic along with configuring the buffer attrs + // as per above commented out code. Almost works but I get some distortion + /*int64_t l = latency_microseconds(); + int64_t approx_num_samples_in_souncard_buffer = (l*sample_rate_)/(1000000); + auto res = std::max(int64_t(0), int64_t(buffer_size_) - + approx_num_samples_in_souncard_buffer); return (long)res;*/ +} long LinuxAudioOutputDevice::latency_microseconds() { - pa_usec_t latency; + pa_usec_t latency = 0; int error; if (playback_handle_ && (latency = pa_simple_get_latency(playback_handle_, &error)) == (pa_usec_t)-1) { - std::stringstream ss; - ss << __FILE__ ": pa_simple_get_latency() failed: " << pa_strerror(error); - throw std::runtime_error(ss.str().c_str()); + spdlog::warn( + "{} pa_simple_get_latency() failed: {} ", __PRETTY_FUNCTION__, pa_strerror(error)); } return (long)latency; } -void LinuxAudioOutputDevice::push_samples(const void *sample_data, const long num_samples) { +bool LinuxAudioOutputDevice::push_samples(const void *sample_data, const long num_samples) { int error; // TODO: * 2 below is because we ASSUME 16bits per sample. Need to handle different @@ -95,6 +117,8 @@ void LinuxAudioOutputDevice::push_samples(const void *sample_data, const long nu pa_simple_write(playback_handle_, sample_data, (size_t)num_samples * 2, &error) < 0) { std::stringstream ss; ss << __FILE__ ": pa_simple_write() failed: " << pa_strerror(error); - throw std::runtime_error(ss.str().c_str()); + spdlog::warn("{} : {} ", __PRETTY_FUNCTION__, ss.str()); + return false; } -} + return true; +} \ No newline at end of file diff --git a/src/audio/src/macos_audio_output_device.cpp b/src/audio/src/macos_audio_output_device.cpp new file mode 100644 index 000000000..06c2b9ebc --- /dev/null +++ b/src/audio/src/macos_audio_output_device.cpp @@ -0,0 +1,316 @@ +#include +#include +#include +#include + +#include +#include + +#include "xstudio/audio/macos_audio_output_device.hpp" +#include "xstudio/global_store/global_store.hpp" +#include "xstudio/utility/logging.hpp" + +using namespace xstudio::audio; +using namespace xstudio::global_store; + +namespace xstudio { +namespace audio { +class MacOSAudioOutData { + public: + + MacOSAudioOutData() = default; + + ~MacOSAudioOutData() { + for (auto &b: unused_buffers_) { + AudioQueueFreeBuffer(audio_buffer_queue_, b); + } + AudioQueueDispose(audio_buffer_queue_, true); + } + + void start() { + if (running_) return; + auto r = AudioQueueStart(audio_buffer_queue_, nullptr); + last_sample_position_in_queue_ = 0; + if (r) { + throw r; + } + running_ = true; + } + + void stop() { + auto r = AudioQueueStop(audio_buffer_queue_, true); + if (r) { + throw r; + } + running_ = false; + } + + [[nodiscard]] bool running() const { return running_; } + + AudioQueueBufferRef get_buffer_ref(long size) { + AudioQueueBufferRef result = nullptr; + std::lock_guard l(mutex_); + auto b = unused_buffers_.begin(); + while (b != unused_buffers_.end()) { + + if ((*b)->mAudioDataBytesCapacity >= size) { + result = *b; + unused_buffers_.erase(b); + break; + } + b++; + } + if (!result) { + // round up size to something sensible, in case we are getting + // uneven blocks of audio samples from xstudio AudioControl. + // This means buffers are more likely to be re-useable + size = ((size/4096) + 1)*4096; + AudioQueueAllocateBuffer(audio_buffer_queue_, size, &result); + } + return result; + } + + void reuse_buffer(AudioQueueBufferRef r) { + std::lock_guard l(mutex_); + unused_buffers_.push_back(r); + } + + AudioQueueRef audio_buffer_queue_ = {nullptr}; + std::mutex mutex_; + std::vector unused_buffers_; + Float64 last_sample_position_in_queue_; + bool running_ = {false}; +}; +}} + +MacOSAudioOutputDevice::MacOSAudioOutputDevice(const utility::JsonStore &prefs) + : AudioOutputDevice(), prefs_(prefs) { + + try { + + // note - we use the pulse_audio sample rate pref to set the sample rate on + // our MacOS audio output. The reason is that this same preference tells the + // media reader (ffmpeg) what sample rate to convert audio frames to when + // they are delivered to xstudio. A better system for setting the intermediate + // rate for audio buffers and delivery to the sound card could be forthcoming! + buffer_size_ = preference_value(prefs_, "/core/audio/pulse_audio_prefs/buffer_size"); + sample_rate_ = preference_value(prefs_, "/core/audio/pulse_audio_prefs/sample_rate"); + + // currently we only really support 2 channels but 5 and 7 for surround can be added + // in easily but I don't have the gear to test this yet + num_channels_ = preference_value(prefs_, "/core/audio/pulse_audio_prefs/channels"); + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + +} + +MacOSAudioOutputDevice::~MacOSAudioOutputDevice() { + + disconnect_from_soundcard(); + audio_out_data_.reset(); +} + +void output_buffer_cb(void * inUserData, + AudioQueueRef inAQ, + AudioQueueBufferRef inBuffer) +{ + // we get this callback when the queue has finished with the given buffer. + // The API docs tell us tha this callback won't get called after + // AudioQueueDispose has returned - this is called in the AudioQueueDispose + // destructor so we should be ok (i think!) + ((MacOSAudioOutData *)inUserData)->reuse_buffer(inBuffer); +} + +void MacOSAudioOutputDevice::disconnect_from_soundcard() { + + if (!audio_out_data_) return; + + try { + + audio_out_data_->stop(); + + } catch (OSStatus &errcode) { + // OSStatus codes are 4 chars packed, and if you convert to this + // 4 char string they can be searched on the website osstatus.com + // weird. + char b[5]; + b[4] = 0; + memcpy(b, &errcode, sizeof(errcode)); + audio_out_data_.reset(); + throw std::runtime_error(fmt::format("Failed to create output audio queue with OSStatus error code {}",b).c_str()); + } + +} + +void MacOSAudioOutputDevice::connect_to_soundcard() { + + // already connected + if (audio_out_data_) { + + if (!audio_out_data_->running()) { + audio_out_data_->start(); + } + return; + } + + audio_out_data_.reset(new MacOSAudioOutData()); + + AudioStreamBasicDescription desc; + desc.mSampleRate = sample_rate_; + desc.mChannelsPerFrame = num_channels_; + + // xSTUDIO's audio sample buffer format is currently 16bit int. This is + // currently set by our ffmpeg media reader. Again, we can add more + // flexibility if we need to but for now hardcoding the format to + // something sensible like 16 bit works fine. + desc.mFormatID = kAudioFormatLinearPCM; + desc.mFormatFlags = kAudioFormatFlagIsSignedInteger; + desc.mBytesPerPacket = 4; + desc.mFramesPerPacket = 1; + desc.mBytesPerFrame = 4; + desc.mChannelsPerFrame = 2; + desc.mBitsPerChannel = 16; + + AudioQueueOutputCallback cb = output_buffer_cb; + UInt32 flags = 0; + AudioQueueRef the_queue; + CFRunLoopRef inCallbackRunLoop = NULL; + CFStringRef inCallbackRunLoopMode = NULL; + + try { + auto r = AudioQueueNewOutput( + &desc, + cb, + (void *)audio_out_data_.get(), // user data passed into output_buffer_cb + inCallbackRunLoop, + inCallbackRunLoopMode, + flags, + &(audio_out_data_->audio_buffer_queue_) + ); + if (r) throw r; + + /*UInt32 repitch = 1; + AudioQueueSetProperty(audio_out_data_->audio_buffer_queue_, + kAudioQueueProperty_EnableTimePitch, + (const void*)&repitch, + sizeof(repitch)); + + AudioQueueParameterValue pitch = 1200.0f; + AudioQueueSetParameter(audio_out_data_->audio_buffer_queue_, kAudioQueueParam_Pitch, pitch);*/ + audio_out_data_->start(); + + } catch (OSStatus &errcode) { + // OSStatus codes are 4 chars packed, and if you convert to this + // 4 char string they can be searched on the website osstatus.com + // weird. + char b[5]; + b[4] = 0; + memcpy(b, &errcode, sizeof(errcode)); + throw std::runtime_error(fmt::format("Failed to create output audio queue with OSStatus error code {}", b).c_str()); + } + // +} + +long MacOSAudioOutputDevice::desired_samples() { + + // latency_microseconds tells us the number of samples sitting in the buffer + // right now (measured as their a duration). If this amount is greater than + // the duration of the number of samples in our chosen buffer_size_ parameter + // then we return zero, meaning we don't want any more samples right now as + // we prefer to let the samples in the buffer to drain somewhat. + // The xstudio AudioController will wait for some amount of time (5ms) and then + // call this again as part of the audio output loop. + if (latency_microseconds() > aprx_buffer_water_level_microsecs_) return 0; + + // devide by 4 because 2 channels and 2 bytes per sample + return buffer_size_/4; + +} + +long MacOSAudioOutputDevice::latency_microseconds() { + + // This is used to inform xstudio how many samples are queued for + // playback at this instant in time - measured in microseconds + // i.e. the 'watermark' of the buffer or just how much audio is + // queued for playback. + + if (!audio_out_data_) return 0; + + AudioTimeStamp ts; + // this call fetches us the current sample time (i.e. the 'number' + // of the sample just played if the first sample since playback + // started was sample zero), among other things + AudioQueueGetCurrentTime( + audio_out_data_->audio_buffer_queue_, + nullptr, + &ts, + nullptr); + + if (audio_out_data_->last_sample_position_in_queue_ > ts.mSampleTime) { + // the last time we enqueued a buffer, we got information about the sample time for the first + // sample in that buffer - we subtract the current sample time from that an apply the sample + // rate. We also add the duration of the last buffer ... this tells us the total duration + // of the audio queued for playback + auto v = ((audio_out_data_->last_sample_position_in_queue_-ts.mSampleTime)*1000000)/sample_rate_; + return ((audio_out_data_->last_sample_position_in_queue_-ts.mSampleTime)*1000000)/sample_rate_; + } + + return 0; +} + + +bool MacOSAudioOutputDevice::push_samples(const void *sample_data, const long num_samples) { + + if (!audio_out_data_) return false; + + // hardcoded for 16bits per channel + const size_t num_bytes = num_samples*2; + AudioQueueBufferRef buf = audio_out_data_->get_buffer_ref(num_bytes); + buf->mAudioDataByteSize = num_bytes; + memcpy(buf->mAudioData, sample_data, num_bytes); + + // Let's fetch the current time for the queue + AudioTimeStamp ts, actual_play_time; + AudioQueueGetCurrentTime( + audio_out_data_->audio_buffer_queue_, + nullptr, + &ts, + nullptr); + + bool underrun = false; + + if (audio_out_data_->last_sample_position_in_queue_ && + audio_out_data_->last_sample_position_in_queue_ < ts.mSampleTime) { + // we have a buffer under-run situation ... the last samples we sent to + // the queue have already played out. + underrun = true; + } + + // Note A: in underrun situation, we tell the queue to play the samples NOW. If we don't + // do this, we never hear them because they are played after the last samples + // we sent, which are now some point in the past + AudioQueueEnqueueBufferWithParameters( + audio_out_data_->audio_buffer_queue_, + buf, + 0, + nullptr, + 0, + 0, + 0, + nullptr, + underrun ? &ts : nullptr, // Note A + &actual_play_time); + + // num_samples/2 = num_frames (2 chans per frame) + audio_out_data_->last_sample_position_in_queue_ = actual_play_time.mSampleTime + num_samples/2; + + // wait while most of the samples are played. The loop in xstudio that calls 'push_samples' + // expects us to block here while the samples are actually played, though it doesn't need + // to precisely match the duration time corresponding to num_samples because we want + // something less than that to give us headroom in that loop to do other calculations like + // resampling audio and so-on. + //std::this_thread::sleep_for(std::chrono::milliseconds(audio_out_data_->last_buffer_duration_microsecs_ / 1200)); + return true; +} diff --git a/src/audio/src/windows_audio_output_device.cpp b/src/audio/src/windows_audio_output_device.cpp index fb6771638..82ca14c07 100644 --- a/src/audio/src/windows_audio_output_device.cpp +++ b/src/audio/src/windows_audio_output_device.cpp @@ -1,3 +1,4 @@ +#define WIN32_LEAN_AND_MEAN #include #include #include @@ -31,6 +32,8 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( long sample_rate /* = 48000 */, int num_channels /* = 2 */) { + CoInitialize(NULL); + CComPtr device_enumerator; CComPtr audio_device; HRESULT hr; @@ -173,6 +176,8 @@ HRESULT WindowsAudioOutputDevice::initializeAudioClient( void WindowsAudioOutputDevice::initialize_sound_card() { + + sample_rate_ = 48000; // default values num_channels_ = 2; std::string sound_card("default"); @@ -194,17 +199,17 @@ void WindowsAudioOutputDevice::initialize_sound_card() { HRESULT hr = initializeAudioClient(sound_card, sample_rate_, num_channels_); if (FAILED(hr)) { - spdlog::error( + auto err_str = fmt::format( "{} Failed to initialize audio client: HRESULT=0x{:08x}", __PRETTY_FUNCTION__, hr); - return; // or handle the error as appropriate + throw(std::exception(err_str.c_str())); } // Get an IAudioRenderClient instance hr = audio_client_->GetService( __uuidof(IAudioRenderClient), reinterpret_cast(&render_client_)); if (FAILED(hr)) { - spdlog::error("Failed to get IAudioRenderClient: HRESULT=0x{:08x}", hr); - return; // or handle the error as appropriate + auto err_str = fmt::format("Failed to get IAudioRenderClient: HRESULT=0x{:08x}", hr); + throw(std::exception(err_str.c_str())); } audio_client_->Start(); @@ -212,6 +217,8 @@ void WindowsAudioOutputDevice::initialize_sound_card() { void WindowsAudioOutputDevice::connect_to_soundcard() { // We are already playing ;-D + if (!audio_client_) + initialize_sound_card(); } long WindowsAudioOutputDevice::desired_samples() { @@ -237,16 +244,29 @@ long WindowsAudioOutputDevice::desired_samples() { long WindowsAudioOutputDevice::latency_microseconds() { // Note: This will just return the latency that WASAPI reports, // which may not include all sources of latency - REFERENCE_TIME defaultDevicePeriod = 0, minimumDevicePeriod = 0; // initialize to 0 + /*REFERENCE_TIME defaultDevicePeriod = 0, minimumDevicePeriod = 0; // initialize to 0 HRESULT hr = audio_client_->GetDevicePeriod(&defaultDevicePeriod, &minimumDevicePeriod); if (FAILED(hr)) { spdlog::error("Failed to get device period from WASAPI with HRESULT: 0x{:08x}", hr); throw std::runtime_error("Failed to get device period"); } - return defaultDevicePeriod / 10; // convert 100-nanosecond units to microseconds + return defaultDevicePeriod / 10; // convert 100-nanosecond units to microseconds*/ + + UINT32 pad = 0; + HRESULT hr = audio_client_->GetCurrentPadding(&pad); + if (FAILED(hr)) { + throw std::runtime_error("Failed to get current padding from WASAPI"); + } + /*std::cerr << pad + << " " << (long(pad) * long(1000000)) / + long(sample_rate_) + << " ";*/ + return (long(pad) * long(1000000)) / long(sample_rate_); } -void WindowsAudioOutputDevice::push_samples(const void *sample_data, const long num_samples) { +bool WindowsAudioOutputDevice::push_samples(const void *sample_data, const long num_samples) { + + auto t0 = utility::clock::now(); int channel_count = num_channels_; @@ -255,13 +275,13 @@ void WindowsAudioOutputDevice::push_samples(const void *sample_data, const long "Invalid number of samples provided: {}. Expected a multiple of {}", num_samples, channel_count); - return; + return false; } // Ensure we have a valid render_client_ if (!render_client_) { // spdlog::error("Invalid Render Client"); - return; // Exit if no render client is set + return false; // Exit if no render client is set } // Retrieve the size (maximum capacity) of the endpoint buffer. @@ -269,7 +289,7 @@ void WindowsAudioOutputDevice::push_samples(const void *sample_data, const long HRESULT hr = audio_client_->GetBufferSize(&buffer_framecount); if (FAILED(hr)) { spdlog::error("Failed to get buffer size from WASAPI"); - return; + return false; } @@ -278,7 +298,7 @@ void WindowsAudioOutputDevice::push_samples(const void *sample_data, const long hr = audio_client_->GetCurrentPadding(&pad); if (FAILED(hr)) { spdlog::error("Failed to get current padding from WASAPI"); - return; + return false; } // Calculate the number of frames we can safely write into the buffer without overflow. @@ -295,7 +315,7 @@ void WindowsAudioOutputDevice::push_samples(const void *sample_data, const long hr = render_client_->GetBuffer(available_frames, &buffer); if (FAILED(hr)) { spdlog::error("GetBuffer failed with HRESULT: 0x{:08x}", hr); - throw std::runtime_error("Failed to get buffer from WASAPI"); + return false; } // Convert int16_t PCM data to float samples considering the interleaved format. @@ -312,12 +332,12 @@ void WindowsAudioOutputDevice::push_samples(const void *sample_data, const long hr = render_client_->ReleaseBuffer(frames_to_write, 0); if (FAILED(hr)) { spdlog::error("Failed to release buffer to WASAPI with HRESULT: 0x{:08x}", hr); - throw std::runtime_error("Failed to release buffer to WASAPI"); + return false; } - std::this_thread::sleep_for( - std::chrono::microseconds((long)(.5 / sample_rate_ * frames_to_write))); } else { // Avoid tight loop thrashing when we are out of samples. - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + std::this_thread::sleep_for(std::chrono::milliseconds(pad * 500 / sample_rate_)); } + + return true; } diff --git a/src/audio/test/audio_test.cpp b/src/audio/test/audio_test.cpp index 5005d4b83..28d0739de 100644 --- a/src/audio/test/audio_test.cpp +++ b/src/audio/test/audio_test.cpp @@ -10,7 +10,6 @@ #include "xstudio/media_cache/media_cache_actor.hpp" #include "xstudio/media_reader/media_reader_actor.hpp" #include "xstudio/playlist/playlist_actor.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/helpers.hpp" using namespace xstudio; @@ -43,20 +42,21 @@ TEST(AudioActorTest, Test) { auto playlist = f.self->spawn("Test"); auto srcuuid = Uuid::generate(); - f.self->anon_send( - playlist, - add_media_atom_v, - f.self->spawn( - "Media3", - Uuid(), - UuidActorVector({UuidActor( - srcuuid, - f.self->spawn( - "MediaSource3", - posix_path_to_uri(TEST_RESOURCE "/media/test.mov"), - utility::FrameRate(timebase::k_flicks_24fps), - srcuuid))})), - Uuid()); + f.self + ->mail( + add_media_atom_v, + f.self->spawn( + "Media3", + Uuid(), + UuidActorVector({UuidActor( + srcuuid, + f.self->spawn( + "MediaSource3", + posix_path_to_uri(TEST_RESOURCE "/media/test.mov"), + utility::FrameRate(timebase::k_flicks_24fps), + srcuuid))})), + Uuid()) + .send(playlist); using namespace std::chrono_literals; diff --git a/src/bookmark/src/CMakeLists.txt b/src/bookmark/src/CMakeLists.txt index b59338ad4..e755c248c 100644 --- a/src/bookmark/src/CMakeLists.txt +++ b/src/bookmark/src/CMakeLists.txt @@ -1,9 +1,9 @@ SET(LINK_DEPS xstudio::utility - xstudio::broadcast xstudio::json_store - caf::core + xstudio::media + CAF::core ) -create_component(bookmark 0.1.0 "${LINK_DEPS}") \ No newline at end of file +create_component(bookmark ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") \ No newline at end of file diff --git a/src/bookmark/src/bookmark.cpp b/src/bookmark/src/bookmark.cpp index 9468f766b..d38a7106d 100644 --- a/src/bookmark/src/bookmark.cpp +++ b/src/bookmark/src/bookmark.cpp @@ -39,11 +39,14 @@ Bookmark::Bookmark(const JsonStore &jsn) if (jsn.count("note") and not jsn["note"].is_null()) note_ = std::make_shared(jsn["note"]); - start_ = jsn.value("start", timebase::k_flicks_low); - duration_ = jsn.value("duration", timebase::k_flicks_max); - enabled_ = jsn.value("enabled", true); - owner_ = jsn.value("owner", utility::Uuid()); - visible_ = jsn.value("visible", true); + start_ = jsn.value("start", timebase::k_flicks_low); + duration_ = jsn.value("duration", timebase::k_flicks_max); + enabled_ = jsn.value("enabled", true); + owner_ = jsn.value("owner", utility::Uuid()); + visible_ = jsn.value("visible", true); + user_type_ = jsn.value("user_type", std::string()); + user_data_ = jsn.value("user_data", utility::JsonStore()); + created_ = jsn.value("created", utility::sysclock::now()); // N.B. AnnotationBase creation requires caf api comms with plugins and // is handled by the bookmark actor @@ -72,11 +75,14 @@ JsonStore Bookmark::serialise() const { jsn["annotation"]["plugin_uuid"] = plugin_uuid; } else jsn["annotation"] = nullptr; - jsn["start"] = start_; - jsn["duration"] = duration_; - jsn["enabled"] = enabled_; - jsn["owner"] = owner_; - jsn["visible"] = visible_; + jsn["start"] = start_; + jsn["duration"] = duration_; + jsn["enabled"] = enabled_; + jsn["owner"] = owner_; + jsn["visible"] = visible_; + jsn["user_type"] = user_type_; + jsn["user_data"] = user_data_; + jsn["created"] = created_; return jsn; } @@ -114,6 +120,21 @@ bool Bookmark::update(const BookmarkDetail &detail) { changed = true; } + if (detail.user_type_) { + user_type_ = *(detail.user_type_); + changed = true; + } + + if (detail.user_data_) { + user_data_ = *(detail.user_data_); + changed = true; + } + + if (detail.created_) { + created_ = *(detail.created_); + changed = true; + } + if (detail.author_) { if (not has_note()) create_note(); @@ -209,10 +230,14 @@ BookmarkDetail &BookmarkDetail::operator=(const Bookmark &other) { visible_ = other.visible_; start_ = other.start_; duration_ = other.duration_; - - has_note_ = other.has_note(); - has_annotation_ = other.has_annotation(); - owner_ = UuidActor(other.owner_, caf::actor()); + user_type_ = other.user_type_; + user_data_ = other.user_data_; + created_ = other.created_; + + has_note_ = other.has_note(); + has_annotation_ = other.has_annotation(); + annotation_hash_ = other.annotation_hash(); + owner_ = UuidActor(other.owner_, caf::actor()); if (*(has_note_)) { author_ = other.note_->author(); diff --git a/src/bookmark/src/bookmark_actor.cpp b/src/bookmark/src/bookmark_actor.cpp index d829a5aa4..db907bd1d 100644 --- a/src/bookmark/src/bookmark_actor.cpp +++ b/src/bookmark/src/bookmark_actor.cpp @@ -1,5 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include + #include #include "xstudio/atoms.hpp" @@ -20,6 +22,10 @@ using namespace caf; namespace {} // namespace +void BookmarkActor::on_exit() { + leave_event_group(this, json_store_); + json_store_ = caf::actor(); +} BookmarkActor::BookmarkActor(caf::actor_config &cfg, const utility::JsonStore &jsn) : caf::event_based_actor(cfg), base_(static_cast(jsn["base"])) { @@ -34,6 +40,7 @@ BookmarkActor::BookmarkActor(caf::actor_config &cfg, const utility::JsonStore &j std::chrono::milliseconds(50)); } link_to(json_store_); + join_event_group(this, json_store_); init(); if (jsn["base"].contains("annotation") and not jsn["base"]["annotation"].is_null()) { @@ -48,7 +55,9 @@ BookmarkActor::BookmarkActor( json_store_ = spawn( utility::Uuid::generate(), utility::JsonStore(), std::chrono::milliseconds(50)); + link_to(json_store_); + join_event_group(this, json_store_); init(); @@ -60,50 +69,18 @@ BookmarkActor::BookmarkActor( } } +caf::message_handler BookmarkActor::message_handler() { + return caf::message_handler{ + make_ignore_error_handler(), -void BookmarkActor::init() { - print_on_create(this, base_); - print_on_exit(this, base_); - - event_group_ = spawn(this); - link_to(event_group_); - - set_down_handler([=](down_msg &msg) { - // find in playhead list.. - if (msg.source == caf::actor_cast(owner_)) { - set_owner(caf::actor(), true); - send_exit(this, caf::exit_reason::user_shutdown); - } - }); - - attach_functor([=](const caf::error &reason) { - // this sends a dummy change event that will propagate from the owner, to playhead - // so that playhead knows bookmark frame ranges have changed, for example - auto owner = caf::actor_cast(owner_); - if (owner) - send(owner, utility::event_atom_v, remove_bookmark_atom_v, base_.uuid()); - }); - - behavior_.assign( - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - base_.make_ignore_error_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), - base_.make_last_changed_event_handler(event_group_, this), [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](json_store::get_json_atom atom, const std::string &path, const bool /*fanout*/) -> caf::result> { auto rp = make_response_promise>(); - request(json_store_, infinite, atom, path) + mail(atom, path) + .request(json_store_, infinite) .then( [=](const JsonStore &jsn) mutable { rp.deliver(std::make_pair(UuidActor(base_.uuid(), this), jsn)); @@ -115,27 +92,46 @@ void BookmarkActor::init() { }, + // watch for events... + [=](json_store::update_atom, + const JsonStore &change, + const std::string &path, + const JsonStore &full) { + if (current_sender() == json_store_) { + mail(json_store::update_atom_v, change, path, full).send(base_.event_group()); + } + }, + + [=](json_store::update_atom, const JsonStore &full) mutable { + if (current_sender() == json_store_) { + mail(json_store::update_atom_v, full).send(base_.event_group()); + } + }, + + [=](json_store::get_json_atom atom) { return mail(atom).delegate(json_store_); }, + [=](json_store::get_json_atom atom, const std::string &path) { - delegate(json_store_, atom, path); + return mail(atom, path).delegate(json_store_); }, [=](json_store::set_json_atom atom, const JsonStore &json) { - delegate(json_store_, atom, json); + return mail(atom, json).delegate(json_store_); }, [=](json_store::merge_json_atom atom, const JsonStore &json) { - delegate(json_store_, atom, json); + return mail(atom, json).delegate(json_store_); }, [=](json_store::set_json_atom atom, const JsonStore &json, const std::string &path) { - delegate(json_store_, atom, json, path); + return mail(atom, json, path).delegate(json_store_); }, [=](utility::serialise_atom) -> result { auto rp = make_response_promise(); - request(json_store_, infinite, json_store::get_json_atom_v, "") + mail(json_store::get_json_atom_v, "") + .request(json_store_, infinite) .then( [=](const JsonStore &meta) mutable { JsonStore jsn; @@ -156,21 +152,18 @@ void BookmarkActor::init() { // this 'reassociates' a bookmark with it's source media actor that // it is attached (associated) with. auto rp = make_response_promise(); - request( - system().registry().template get(studio_registry), - infinite, - session::session_atom_v) + mail(session::session_atom_v) + .request( + system().registry().template get(studio_registry), infinite) .then( [=](caf::actor session) mutable { - request(session, infinite, playlist::get_media_atom_v, base_.owner()) + mail(playlist::get_media_atom_v, base_.owner()) + .request(session, infinite) .then( [=](caf::actor media) mutable { utility::UuidActor ua(base_.owner(), media); - request( - caf::actor_cast(this), - infinite, - associate_bookmark_atom_v, - ua) + mail(associate_bookmark_atom_v, ua) + .request(caf::actor_cast(this), infinite) .then( [=](bool r) mutable { rp.deliver(r); }, [=](caf::error &err) mutable { rp.deliver(err); }); @@ -182,24 +175,20 @@ void BookmarkActor::init() { }, [=](bookmark_detail_atom) -> result { - auto detail = BookmarkDetail(base_); + auto detail = BookmarkDetail(base_); + detail.actor_addr_ = caf::actor_cast(this); if (not owner_) return detail; auto rp = make_response_promise(); - request( - caf::actor_cast(owner_), - infinite, - playlist::reflag_container_atom_v) + mail(playlist::reflag_container_atom_v) + .request(caf::actor_cast(owner_), infinite) .then( [=](const std::tuple &flag) mutable { detail.media_flag_ = std::get<1>(flag); - request( - caf::actor_cast(owner_), - infinite, - media::media_reference_atom_v, - utility::Uuid()) + mail(media::media_reference_atom_v, utility::Uuid()) + .request(caf::actor_cast(owner_), infinite) .then( [=](const std::pair result) mutable { (*(detail.owner_)).actor() = @@ -220,15 +209,14 @@ void BookmarkActor::init() { auto changed = base_.update(detail); if (detail.owner_) { base_.set_owner((*detail.owner_).uuid()); - anon_send( - caf::actor_cast(this), - associate_bookmark_atom_v, - *detail.owner_); + anon_mail(associate_bookmark_atom_v, *detail.owner_) + .send(caf::actor_cast(this)); } if (changed) { - send(event_group_, utility::event_atom_v, bookmark_change_atom_v, base_.uuid()); - base_.send_changed(event_group_, this); + mail(utility::event_atom_v, bookmark_change_atom_v, base_.uuid()) + .send(base_.event_group()); + base_.send_changed(); } return changed; }, @@ -260,8 +248,9 @@ void BookmarkActor::init() { if (base_.annotation_ == anno) return false; base_.annotation_ = anno; - send(event_group_, utility::event_atom_v, bookmark_change_atom_v, base_.uuid()); - // base_.send_changed(event_group_, this); + mail(utility::event_atom_v, bookmark_change_atom_v, base_.uuid()) + .send(base_.event_group()); + // base_.send_changed(); return true; }, @@ -286,7 +275,8 @@ void BookmarkActor::init() { [=](bookmark_detail_atom, get_annotation_atom) -> result { auto rp = make_response_promise(); - request(caf::actor_cast(this), infinite, bookmark_detail_atom_v) + mail(bookmark_detail_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](const BookmarkDetail &detail) mutable { auto data = new BookmarkAndAnnotation; @@ -302,13 +292,15 @@ void BookmarkActor::init() { [=](utility::duplicate_atom) -> result { auto rp = make_response_promise(); - request(json_store_, infinite, json_store::get_json_atom_v) + mail(json_store::get_json_atom_v) + .request(json_store_, infinite) .then( [=](const JsonStore &meta) mutable { auto uuid = utility::Uuid::generate(); auto actor = spawn(uuid, base_); - request(actor, infinite, json_store::set_json_atom_v, meta) + mail(json_store::set_json_atom_v, meta) + .request(actor, infinite) .then( [=](bool) mutable { rp.deliver(UuidActor(uuid, actor)); }, [=](const error &err) mutable { rp.deliver(err); }); @@ -323,7 +315,8 @@ void BookmarkActor::init() { return make_error(xstudio_error::error, "Bookmark unassociated."); auto rp = make_response_promise(); - request(caf::actor_cast(owner_), infinite, atom, utility::Uuid()) + mail(atom, utility::Uuid()) + .request(caf::actor_cast(owner_), infinite) .then( [=](const std::pair result) mutable { rp.deliver(result.second); @@ -339,7 +332,8 @@ void BookmarkActor::init() { auto rp = make_response_promise(); // need media ref - request(caf::actor_cast(this), infinite, media::media_reference_atom_v) + mail(media::media_reference_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](const utility::MediaReference &mediaref) mutable { // also need bookmark detail @@ -355,7 +349,23 @@ void BookmarkActor::init() { [=](caf::error &err) mutable { rp.deliver(err); }); return rp; - }); + }}; +} + +void BookmarkActor::init() { + print_on_create(this, base_); + print_on_exit(this, base_); + + attach_functor([=](const caf::error &reason) { + // this sends a dummy change event that will propagate from the owner, to playhead + // so that playhead knows bookmark frame ranges have changed, for example + // monitor_.dispose(); + + auto owner = caf::actor_cast(owner_); + if (owner) { + mail(utility::event_atom_v, remove_bookmark_atom_v, base_.uuid()).send(owner); + } + }); } void BookmarkActor::build_annotation_via_plugin(const utility::JsonStore &anno_data) { @@ -366,24 +376,19 @@ void BookmarkActor::build_annotation_via_plugin(const utility::JsonStore &anno_d return; auto pm = system().registry().template get(plugin_manager_registry); - request( - pm, - infinite, - plugin_manager::spawn_plugin_atom_v, - plugin_uuid, - utility::JsonStore()) + mail(plugin_manager::spawn_plugin_atom_v, plugin_uuid, utility::JsonStore()) + .request(pm, infinite) .then( [=](caf::actor annotations_plugin) { - request(annotations_plugin, infinite, build_annotation_atom_v, anno_data) + mail(build_annotation_atom_v, anno_data) + .request(annotations_plugin, infinite) .then( [=](AnnotationBasePtr &anno) { anno->bookmark_uuid_ = base_.uuid(); base_.annotation_ = anno; - send( - event_group_, - utility::event_atom_v, - bookmark_change_atom_v, - base_.uuid()); + mail( + utility::event_atom_v, bookmark_change_atom_v, base_.uuid()) + .send(base_.event_group()); }, [=](caf::error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -393,22 +398,17 @@ void BookmarkActor::build_annotation_via_plugin(const utility::JsonStore &anno_d // the desired plugin. base_.annotation_.reset( new AnnotationBase(anno_data, base_.uuid())); - send( - event_group_, - utility::event_atom_v, - bookmark_change_atom_v, - base_.uuid()); + mail( + utility::event_atom_v, bookmark_change_atom_v, base_.uuid()) + .send(base_.event_group()); }); }, [=](caf::error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); // see comment above. base_.annotation_.reset(new AnnotationBase(anno_data, base_.uuid())); - send( - event_group_, - utility::event_atom_v, - bookmark_change_atom_v, - base_.uuid()); + mail(utility::event_atom_v, bookmark_change_atom_v, base_.uuid()) + .send(base_.event_group()); }); } else { @@ -417,7 +417,8 @@ void BookmarkActor::build_annotation_via_plugin(const utility::JsonStore &anno_d "{} AnnotationBase serialised data does not include annotations plugin uuid", __PRETTY_FUNCTION__); base_.annotation_.reset(new AnnotationBase(anno_data, base_.uuid())); - send(event_group_, utility::event_atom_v, bookmark_change_atom_v, base_.uuid()); + mail(utility::event_atom_v, bookmark_change_atom_v, base_.uuid()) + .send(base_.event_group()); } } @@ -425,7 +426,7 @@ void BookmarkActor::set_owner(caf::actor owner, const bool dead) { // Note, owner is a MediaActor if (owner_) { - demonitor(caf::actor_cast(owner_)); + monitor_.dispose(); if (not dead) { auto src = caf::actor_cast(owner_); if (src) @@ -436,14 +437,22 @@ void BookmarkActor::set_owner(caf::actor owner, const bool dead) { owner_ = caf::actor_cast(owner); if (owner) { - monitor(owner); - request(event_group_, infinite, broadcast::join_broadcast_atom_v, owner) + monitor_ = monitor(owner, [this, addr = owner.address()](const error &) { + if (addr == owner_) { + // stop msg to media on killing bookmakr + owner_ = caf::actor_addr(); + quit(); + } + }); + + mail(broadcast::join_broadcast_atom_v, owner) + .request(base_.event_group(), infinite) .then( [=](const bool) mutable {}, [=](const error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); + mail(utility::event_atom_v, bookmark_change_atom_v, base_.uuid()) + .send(base_.event_group()); } - - send(event_group_, utility::event_atom_v, bookmark_change_atom_v, base_.uuid()); } diff --git a/src/bookmark/src/bookmarks_actor.cpp b/src/bookmark/src/bookmarks_actor.cpp index e5675cb78..88cf0f073 100644 --- a/src/bookmark/src/bookmarks_actor.cpp +++ b/src/bookmark/src/bookmarks_actor.cpp @@ -1,5 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include + #include #include "xstudio/atoms.hpp" @@ -12,6 +14,7 @@ #include "xstudio/utility/uuid.hpp" #include "xstudio/utility/csv.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" +#include "xstudio/thumbnail/thumbnail.hpp" using namespace xstudio; using namespace xstudio::utility; @@ -30,7 +33,7 @@ BookmarksActor::BookmarksActor(caf::actor_config &cfg, const utility::JsonStore auto uuid = utility::Uuid(key); auto actor = spawn(static_cast(value)); bookmarks_[uuid] = actor; - monitor(actor); + monitor_bookmark(actor); join_event_group(this, actor); } catch (const std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); @@ -53,69 +56,47 @@ caf::message_handler BookmarksActor::default_event_handler() { [=](utility::event_atom, bookmark_change_atom, const utility::UuidActor &) {}}; } +void BookmarksActor::on_exit() { + for (auto &it : bookmarks_) { + send_exit(it.second, caf::exit_reason::user_shutdown); + } +} -void BookmarksActor::init() { - print_on_create(this, base_); - print_on_exit(this, base_); - - event_group_ = spawn(this); - link_to(event_group_); - - // JsonStore category; - - // try { - // auto prefs = GlobalStoreHelper(system()); - // JsonStore j; - // join_broadcast(this, prefs.get_group(j)); - // category = preference_value(j, "/core/bookmark/category"); - // } catch (...) { - // } +void BookmarksActor::monitor_bookmark(const caf::actor &actor) { + monitor(actor, [this, addr = actor.address()](const error &) { + // find in playhead list.. + // why is this looping ? - set_down_handler([=](down_msg &msg) { - // find in playhead list.. - for (auto it = std::begin(bookmarks_); it != std::end(bookmarks_); ++it) { - if (msg.source == it->second) { - demonitor(it->second); - // spdlog::warn("bookmark exited {}", to_string(it->first)); - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, remove_bookmark_atom_v, it->first); - bookmarks_.erase(it); - break; + auto it = bookmarks_.begin(); + while (it != bookmarks_.end()) { + if (addr == it->second) { + base_.send_changed(); + mail(utility::event_atom_v, remove_bookmark_atom_v, it->first) + .send(base_.event_group()); + it = bookmarks_.erase(it); + } else { + it++; } } }); +} + + +caf::message_handler BookmarksActor::message_handler() { + return caf::message_handler{ + make_ignore_error_handler(), - behavior_.assign( - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - base_.make_ignore_error_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - // [=](json_store::update_atom, - // const JsonStore & /*change*/, - // const std::string & /*path*/, - // const JsonStore &full) { - // delegate(actor_cast(this), json_store::update_atom_v, full); - // }, - - // [=](json_store::update_atom, const JsonStore &js) { - // try { - // auto new_category = preference_value(js, - // "/core/bookmark/category"); if (new_category != category){ - // category = new_category; - // } - // } catch (const std::exception &err) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - // } - // }, + // json events from bookmarks + [=](json_store::update_atom, + const JsonStore & /*change*/, + const std::string & /*path*/, + const JsonStore &full) {}, + + // json events from bookmarks + [=](json_store::update_atom, const JsonStore &js) {}, [=](json_store::get_json_atom atom, const utility::UuidVector &uuids, @@ -173,12 +154,25 @@ void BookmarksActor::init() { auto clients = std::vector(); // check for dead bookmarks - for (const auto &i : bookmarks_) { + { + auto i = bookmarks_.begin(); + while (i != bookmarks_.end()) { + if (not i->second) { + i = bookmarks_.erase(i); + } else { + clients.push_back(i->second); + i++; + } + } + } + + // Big NOPE, you can't erase with an iterator in a loop like this + /*for (auto i = bookmarks_) { if (not i.second) bookmarks_.erase(i.first); else clients.push_back(i.second); - } + }*/ if (not clients.empty()) { fan_out_request(clients, infinite, serialise_atom_v) @@ -291,6 +285,63 @@ void BookmarksActor::init() { return make_error(xstudio_error::error, "Invalid uuid"); }, + [=](bookmark::render_annotations_atom, + const utility::Uuid bookmark_id, + const int width, + bool /*thumnail*/ + ) -> result { + // render the first frame of the given bookmark with overlay annotations + auto rp = make_response_promise(); + mail(bookmark_detail_atom_v, bookmark_id) + .request(caf::actor_cast(this), infinite) + .then( + [=](const BookmarkDetail &detail) mutable { + caf::actor owner = detail.owner_->actor(); + auto offscreen_renderer = system().registry().template get( + offscreen_viewport_registry); + + rp.delegate( + offscreen_renderer, + ui::viewport::render_viewport_to_image_atom_v, + owner, // media used to fetch image + *(detail.logical_start_frame_), // media frame + thumbnail::THUMBNAIL_FORMAT::TF_RGB24, // format + width, // output width + false, // 'auto scale' (where width is set by source image) + true // render annotations + ); + }, + [=](const caf::error &err) mutable { rp.deliver(err); }); + return rp; + }, + + [=](bookmark::render_annotations_atom, + const utility::Uuid bookmark_id, + const int width) -> result> { + // render the first frame of the given bookmark with overlay annotations + // and convert to a JPEG buffer + + auto rp = make_response_promise>(); + mail(render_annotations_atom_v, bookmark_id, width, true) + .request(caf::actor_cast(this), infinite) + .then( + [=](const thumbnail::ThumbnailBufferPtr &thumb) mutable { + auto thumbnail_manager = system().registry().template get( + thumbnail_manager_registry); + + // badly named/atomed message - this gets the thumnail + // manager to convert a thumb to JPEG (using Qt API) + rp.delegate( + thumbnail_manager, + media_reader::get_thumbnail_atom_v, + thumb, + 100 // jpeg quality = 100 + ); + }, + [=](const caf::error &err) mutable { rp.deliver(err); }); + return rp; + }, + [=](get_bookmark_atom, const utility::UuidList &uuids) -> result> { std::vector r; @@ -309,20 +360,14 @@ void BookmarksActor::init() { if (bookmarks_.count(uuid)) { // clone it.. auto rp = make_response_promise(); - request(bookmarks_[uuid], infinite, utility::duplicate_atom_v) + mail(utility::duplicate_atom_v) + .request(bookmarks_[uuid], infinite) .then( [=](const utility::UuidActor &ua) mutable { // assign to src. BookmarkDetail bd; bd.owner_ = src; - anon_send(ua.actor(), bookmark_detail_atom_v, bd); - // add to manager. - bookmarks_[ua.uuid()] = ua.actor(); - monitor(ua.actor()); - join_event_group(this, ua.actor()); - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, add_bookmark_atom_v, ua); - + anon_mail(bookmark_detail_atom_v, bd).send(ua.actor()); rp.deliver(ua); }, [=](const caf::error &err) mutable { rp.deliver(err); }); @@ -334,24 +379,25 @@ void BookmarksActor::init() { [=](add_bookmark_atom, const UuidActorVector &bookmarks) -> bool { for (const auto &i : bookmarks) { bookmarks_[i.uuid()] = i.actor(); - monitor(i.actor()); + monitor_bookmark(i.actor()); join_event_group(this, i.actor()); - send(event_group_, utility::event_atom_v, add_bookmark_atom_v, i); + mail(utility::event_atom_v, add_bookmark_atom_v, i).send(base_.event_group()); } - base_.send_changed(event_group_, this); + base_.send_changed(); return true; }, // create and assign. [=](add_bookmark_atom, const UuidActor &src) -> result { auto rp = make_response_promise(); - request(caf::actor_cast(this), infinite, add_bookmark_atom_v) + mail(add_bookmark_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](const utility::UuidActor &ua) mutable { // associate.. BookmarkDetail bd; bd.owner_ = src; - anon_send(ua.actor(), bookmark_detail_atom_v, bd); + anon_mail(bookmark_detail_atom_v, bd).send(ua.actor()); rp.deliver(ua); }, [=](const caf::error &err) mutable { rp.deliver(err); }); @@ -361,11 +407,11 @@ void BookmarksActor::init() { // change to bookmark [=](utility::event_atom, bookmark_change_atom, const utility::Uuid &uuid) { - send( - event_group_, + mail( utility::event_atom_v, bookmark_change_atom_v, - UuidActor(uuid, bookmarks_[uuid])); + UuidActor(uuid, bookmarks_[uuid])) + .send(base_.event_group()); }, [=](add_bookmark_atom) -> utility::UuidActor { @@ -373,15 +419,12 @@ void BookmarksActor::init() { auto actor = spawn(uuid); bookmarks_[uuid] = actor; - monitor(actor); + monitor_bookmark(actor); join_event_group(this, actor); - base_.send_changed(event_group_, this); - send( - event_group_, - utility::event_atom_v, - add_bookmark_atom_v, - UuidActor(uuid, actor)); + base_.send_changed(); + mail(utility::event_atom_v, add_bookmark_atom_v, UuidActor(uuid, actor)) + .send(base_.event_group()); return UuidActor(uuid, actor); }, @@ -396,11 +439,10 @@ void BookmarksActor::init() { // data. if (bookmark_serialised_data.contains("base") && bookmark_serialised_data["base"].contains("annotation")) { - request( - bookmarks_[bookmark_uuid], - infinite, + mail( bookmark::add_annotation_atom_v, utility::JsonStore(bookmark_serialised_data["base"]["annotation"])) + .request(bookmarks_[bookmark_uuid], infinite) .then( [=](bool) mutable { utility::UuidActor ua(bookmark_uuid, bookmarks_[bookmark_uuid]); @@ -416,15 +458,13 @@ void BookmarksActor::init() { auto actor = spawn(bookmark_serialised_data); bookmarks_[bookmark_uuid] = actor; - monitor(actor); + monitor_bookmark(actor); join_event_group(this, actor); - base_.send_changed(event_group_, this); - send( - event_group_, - utility::event_atom_v, - add_bookmark_atom_v, - UuidActor(bookmark_uuid, actor)); - anon_send(actor, associate_bookmark_atom_v, true); + base_.send_changed(); + mail( + utility::event_atom_v, add_bookmark_atom_v, UuidActor(bookmark_uuid, actor)) + .send(base_.event_group()); + anon_mail(associate_bookmark_atom_v, true).send(actor); rp.deliver(UuidActor(bookmark_uuid, actor)); } return rp; @@ -435,7 +475,7 @@ void BookmarksActor::init() { return std::vector(); auto rp = make_response_promise>(); - + auto w = map_value_to_vec(bookmarks_); fan_out_request( map_value_to_vec(bookmarks_), infinite, bookmark_detail_atom_v) .then( @@ -581,13 +621,58 @@ void BookmarksActor::init() { default_category_ = category; }, - [=](default_category_atom) -> std::string { return default_category_; }); + [=](default_category_atom) -> std::string { return default_category_; }, + + [=](media_reader::get_thumbnail_atom, + const BookmarkDetail detail, + int width, + caf::actor receiver) { + auto offscreen_renderer = + system().registry().template get(offscreen_viewport_registry); + if (!offscreen_renderer) { + spdlog::warn("{} : Offscreen viewport not found.", __PRETTY_FUNCTION__); + return; + } + if (!detail.owner_ || !detail.start_ | !detail.owner_->actor()) + return; + + mail( + ui::viewport::render_viewport_to_image_atom_v, + detail.owner_->actor(), + *(detail.start_), + thumbnail::THUMBNAIL_FORMAT::TF_RGB24, + width, + false, // autoscale (renders at source imate fomat if true) + true /*show annotations*/) + .request(offscreen_renderer, infinite) + .then( + [=](const thumbnail::ThumbnailBufferPtr &thumbnail) { + if (thumbnail) + anon_mail(media_reader::get_thumbnail_atom_v, detail, thumbnail) + .send(receiver); + else + spdlog::warn( + "{} {}", + __PRETTY_FUNCTION__, + "Null thumbanil returned by offscreen renderer."); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + }}; +} + + +void BookmarksActor::init() { + print_on_create(this, base_); + print_on_exit(this, base_); } void BookmarksActor::csv_export( caf::typed_response_promise>> rp) { // collect all bookmark details.. - request(actor_cast(this), infinite, bookmark_detail_atom_v, UuidVector()) + mail(bookmark_detail_atom_v, UuidVector()) + .request(actor_cast(this), infinite) .then( [=](std::vector details) mutable { std::vector> data; diff --git a/src/bookmark/test/CMakeLists.txt b/src/bookmark/test/CMakeLists.txt index b26da57c5..fd48e0481 100644 --- a/src/bookmark/test/CMakeLists.txt +++ b/src/bookmark/test/CMakeLists.txt @@ -3,7 +3,6 @@ include(CTest) SET(LINK_DEPS xstudio::bookmark xstudio::utility - xstudio::broadcast ) create_tests("${LINK_DEPS}") diff --git a/src/broadcast/src/CMakeLists.txt b/src/broadcast/src/CMakeLists.txt deleted file mode 100644 index 6d88fcd14..000000000 --- a/src/broadcast/src/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -SET(LINK_DEPS - xstudio::utility - caf::core -) - -SET(STATIC_LINK_DEPS - xstudio::utility_static - caf::core -) - -create_component_static(broadcast 0.1.0 "${LINK_DEPS}" "${STATIC_LINK_DEPS}") - diff --git a/src/broadcast/src/broadcast_actor.cpp b/src/broadcast/src/broadcast_actor.cpp deleted file mode 100644 index 0873a0f28..000000000 --- a/src/broadcast/src/broadcast_actor.cpp +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include "xstudio/broadcast/broadcast_actor.hpp" -#include "xstudio/atoms.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" -#include "xstudio/utility/uuid.hpp" - -using namespace xstudio; -using namespace xstudio::broadcast; -using namespace xstudio::utility; -using namespace caf; - -static std::atomic count{0}; -static std::atomic actor_count{0}; - -BroadcastActor::BroadcastActor(caf::actor_config &cfg, caf::actor owner) - : caf::event_based_actor(cfg) { - // count++; - // actor_count++; - if (owner) { - monitor(owner); - owner_ = caf::actor_cast(owner); - } - init(); -} - -// BroadcastActor::~BroadcastActor(){ -// count--; -// spdlog::error("{} count {} actor_count {}", __PRETTY_FUNCTION__, count, actor_count); -// } - -caf::message_handler BroadcastActor::default_event_handler() { - return {[=](broadcast_down_atom, const caf::actor_addr &) {}}; -} - -void BroadcastActor::init() { - // print_on_create(this, "BroadcastActor"); - // print_on_exit(this, "BroadcastActor"); - - set_default_handler( - [this](caf::scheduled_actor *, caf::message &msg) -> caf::skippable_result { - // UNCOMMENT TO DEBUG UNEXPECT MESSAGES - - // spdlog::warn("Got broadcast from {} {}", to_string(current_sender()), - // to_string(msg) - // ); - - if (current_sender() == nullptr or not current_sender()) { - for (const auto &i : subscribers_) { - // spdlog::warn("{} {} Anon Forward {}", - // to_string(caf::actor_cast(this)), - // to_string(current_sender()), to_string(caf::actor_cast(i))); - try { - send(caf::actor_cast(i), msg); - } catch (...) { - } - } - } else { - for (const auto &i : subscribers_) { - // we need to send as if we were delegating.. - try { - // spdlog::warn("{} {} Forward to {}", - // to_string(caf::actor_cast(this)), - // to_string(current_sender()), - // to_string(caf::actor_cast(i))); - - send_as( - caf::actor_cast(current_sender()), - caf::actor_cast(i), - msg); - } catch (...) { - } - } - } - return message{}; - }); - - set_down_handler([=](down_msg &msg) { - // owner gone time to die.. - if (msg.source == caf::actor_cast(owner_)) { - // spdlog::warn("owner dead"); - - demonitor(caf::actor_cast(owner_)); - owner_ = caf::actor_addr(); - send_exit(this, caf::exit_reason::user_shutdown); - } else { - // one of our subscribers.. - // spdlog::warn("possible subscriber dead {}", to_string(msg.source)); - auto subscriber = caf::actor_cast(msg.source); - if (subscribers_.count(subscriber)) { - demonitor(msg.source); - subscribers_.erase(subscriber); - // spdlog::warn("subscriber dead {} {}", - // to_string(caf::actor_cast(this)),to_string(subscriber)); - } - } - }); - - behavior_.assign( - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](leave_broadcast_atom) -> bool { - auto subscriber = caf::actor_cast(current_sender()); - if (subscribers_.count(subscriber)) { - demonitor(current_sender()); - subscribers_.erase(subscriber); - // spdlog::warn("subscriber leaving {} {}", - // to_string(caf::actor_cast(this)),to_string(current_sender())); - } - return true; - }, - [=](leave_broadcast_atom, caf::actor sub) -> bool { - auto subscriber = caf::actor_cast(sub); - if (subscribers_.count(subscriber)) { - demonitor(sub); - subscribers_.erase(subscriber); - // spdlog::warn("subscriber leaving {} {}", - // to_string(caf::actor_cast(this)),to_string(sub)); - } - return true; - }, - [=](join_broadcast_atom) -> bool { - auto subscriber = caf::actor_cast(current_sender()); - if (not subscribers_.count(subscriber)) { - monitor(current_sender()); - subscribers_.insert(subscriber); - // spdlog::warn("new subscriber {} {} {}", - // to_string(caf::actor_cast(this)), to_string(current_sender()), - // to_string(subscriber)); - } - return true; - }, - [=](join_broadcast_atom, caf::actor sub) -> bool { - auto subscriber = caf::actor_cast(sub); - - if (not subscribers_.count(subscriber)) { - - monitor(sub); - subscribers_.insert(subscriber); - // spdlog::warn("new subscriber {} {} {}", - // to_string(caf::actor_cast(this)), to_string(sub), - // to_string(subscriber)); - } - return true; - }); -} - -void BroadcastActor::on_exit() { - // spdlog::warn("notify subscribers or shutdown"); - for (const auto &i : subscribers_) { - try { - auto sub = caf::actor_cast(i); - if (sub) - anon_send(sub, broadcast_down_atom_v, caf::actor_cast(this)); - } catch (...) { - } - } - subscribers_.clear(); - // actor_count--; - // spdlog::error("{} count {} actor_count {}", __PRETTY_FUNCTION__, count, actor_count); -} diff --git a/src/broadcast/test/CMakeLists.txt b/src/broadcast/test/CMakeLists.txt deleted file mode 100644 index b534b3049..000000000 --- a/src/broadcast/test/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -include(CTest) - -SET(LINK_DEPS - xstudio::broadcast - caf::core -) - -create_tests("${LINK_DEPS}") - diff --git a/src/broadcast/test/broadcast_actor_test.cpp b/src/broadcast/test/broadcast_actor_test.cpp deleted file mode 100644 index a13a9939a..000000000 --- a/src/broadcast/test/broadcast_actor_test.cpp +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/broadcast/broadcast_actor.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/uuid.hpp" - -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::broadcast; - -using namespace caf; -using namespace std::chrono_literals; - -#include "xstudio/utility/serialise_headers.hpp" - - -ACTOR_TEST_SETUP() - -class TestActor : public caf::event_based_actor { - public: - TestActor(caf::actor_config &cfg, caf::actor broadcast) - : caf::event_based_actor(cfg), broadcast_(std::move(broadcast)) { - init(); - } - ~TestActor() override = default; - - const char *name() const override { return NAME.c_str(); } - void on_exit() override {} - - private: - inline static const std::string NAME = "TestActor"; - - void init() { - spdlog::warn("TestActor {}", to_string(caf::actor_cast(this))); - - request(broadcast_, infinite, join_broadcast_atom_v) - .then( - [=](const bool) { send(broadcast_, xstudio::global_store::autosave_atom_v); }, - [=](const caf::error &) {}); - - behavior_.assign( - [=](xstudio::global_store::autosave_atom) { - spdlog::warn( - "TestActor got autosave_atom broadcast from {}", - to_string(current_sender())); - }, - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) { - spdlog::warn("broadcast down recieved"); - }, - [=](xstudio::broadcast::leave_broadcast_atom) { - request(broadcast_, infinite, leave_broadcast_atom_v) - .then( - [=](const bool) { spdlog::warn("TestActor leave_broadcast_atom_v"); }, - [=](const caf::error &) {}); - }, - [=](xstudio::broadcast::join_broadcast_atom) { - request(broadcast_, infinite, join_broadcast_atom_v) - .then( - [=](const bool) { spdlog::warn("TestActor join_broadcast_atom"); }, - [=](const caf::error &) {}); - }); - } - - caf::behavior make_behavior() override { return behavior_; } - - private: - caf::behavior behavior_; - caf::actor broadcast_; -}; - - -TEST(BroadcastActorTest, Test) { - fixture f; - start_logger(); - auto b = f.self->spawn(); - auto c = f.self->spawn(b); - auto d = f.self->spawn(b); - - f.self->anon_send(c, xstudio::broadcast::leave_broadcast_atom_v); - f.self->send_exit(d, caf::exit_reason::user_shutdown); - - f.self->anon_send(b, xstudio::global_store::autosave_atom_v); - - f.self->anon_send(c, xstudio::broadcast::join_broadcast_atom_v); - f.self->anon_send(b, xstudio::global_store::autosave_atom_v); -} diff --git a/src/caf_utility/src/CMakeLists.txt b/src/caf_utility/src/CMakeLists.txt index 153647db0..cc3bd306a 100644 --- a/src/caf_utility/src/CMakeLists.txt +++ b/src/caf_utility/src/CMakeLists.txt @@ -1,10 +1,9 @@ find_package(fmt REQUIRED) SET(LINK_DEPS - caf::io - caf::core + CAF::io + CAF::core fmt::fmt ) -create_component(caf_utility 0.1.0 "${LINK_DEPS}") - +create_component(caf_utility ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") \ No newline at end of file diff --git a/src/colour_pipeline/src/CMakeLists.txt b/src/colour_pipeline/src/CMakeLists.txt index d5b2b8e10..ff37840c7 100644 --- a/src/colour_pipeline/src/CMakeLists.txt +++ b/src/colour_pipeline/src/CMakeLists.txt @@ -2,7 +2,7 @@ SET(LINK_DEPS xstudio::utility xstudio::plugin_manager xstudio::module - caf::core + CAF::core ) -create_component(colour_pipeline 0.1.0 "${LINK_DEPS}") +create_component(colour_pipeline ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/colour_pipeline/src/colour_cache_actor.cpp b/src/colour_pipeline/src/colour_cache_actor.cpp index b233fa5af..542b7ada5 100644 --- a/src/colour_pipeline/src/colour_cache_actor.cpp +++ b/src/colour_pipeline/src/colour_cache_actor.cpp @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include + #include "xstudio/atoms.hpp" #include "xstudio/media/media.hpp" #include "xstudio/colour_pipeline/colour_cache_actor.hpp" @@ -76,7 +78,8 @@ GlobalColourCacheActor::GlobalColourCacheActor(caf::actor_config &cfg) }, [=](json_store::update_atom, const JsonStore &js) { - delegate(actor_cast(this), json_store::update_atom_v, js, "", js); + return mail(json_store::update_atom_v, js, "", js) + .delegate(actor_cast(this)); }, [=](keys_atom) -> std::vector { return cache_.keys(); }, @@ -84,38 +87,58 @@ GlobalColourCacheActor::GlobalColourCacheActor(caf::actor_config &cfg) [=](preserve_atom, const std::string &key) -> bool { return cache_.preserve(key); }, [=](preserve_atom, const std::string &key, const time_point &time) -> bool { + // std::cerr << "K " << key << "\n"; return cache_.preserve(key, time); }, [=](preserve_atom, const std::string &key, const time_point &time, const Uuid &uuid) - -> bool { return cache_.preserve(key, time, uuid); }, + -> bool { + // std::cerr << "K1 " << key << "\n"; + return cache_.preserve(key, time, uuid); + }, [=](retrieve_atom, const std::string &key) -> ColourOperationDataPtr { + // std::cerr << "K2 " << key << "\n"; + return cache_.retrieve(key); }, - [=](retrieve_atom, const std::string &key, const time_point &time) - -> ColourOperationDataPtr { return cache_.retrieve(key, time); }, + [=](retrieve_atom, + const std::string &key, + const time_point &time) -> ColourOperationDataPtr { + // std::cerr << "K3 " << key << "\n"; + return cache_.retrieve(key, time); + }, [=](retrieve_atom, const std::string &key, const time_point &time, const Uuid &uuid) - -> ColourOperationDataPtr { return cache_.retrieve(key, time, uuid); }, + -> ColourOperationDataPtr { + // std::cerr << "K4 " << key << "\n"; + + return cache_.retrieve(key, time, uuid); + }, [=](size_atom) -> size_t { return cache_.size(); }, [=](store_atom, const std::string &key, ColourOperationDataPtr buf) -> bool { + // std::cerr << "K5 " << key << "\n"; return cache_.store(key, buf); }, [=](store_atom, const std::string &key, ColourOperationDataPtr buf, - const time_point &when) -> bool { return cache_.store(key, buf, when); }, + const time_point &when) -> bool { + // std::cerr << "K6 " << key << "\n"; + return cache_.store(key, buf, when); + }, [=](store_atom, const std::string &key, ColourOperationDataPtr buf, const time_point &when, const utility::Uuid &uuid) -> bool { + // std::cerr << "K7 " << key << "\n"; + return cache_.store(key, buf, when, false, uuid); }); } diff --git a/src/colour_pipeline/src/colour_operation.cpp b/src/colour_pipeline/src/colour_operation.cpp index b247ef271..71b8c407c 100644 --- a/src/colour_pipeline/src/colour_operation.cpp +++ b/src/colour_pipeline/src/colour_operation.cpp @@ -30,16 +30,24 @@ caf::message_handler ColourOpPlugin::message_handler_extensions() { // use the AVFrameID to get to the MediaSourceActor auto media_source = utility::UuidActor( - media_ptr.source_uuid_, caf::actor_cast(media_ptr.actor_addr_)); + media_ptr.source_uuid(), + caf::actor_cast(media_ptr.media_source_addr())); + + if (!media_source.actor()) { + // no media source (missing media etc.) - return empty object + rp.deliver(ColourOperationDataPtr()); + return rp; + } // now get to the MediaActor that owns the MediaSourceActor - request(media_source.actor(), infinite, utility::parent_atom_v) + mail(utility::parent_atom_v) + .request(media_source.actor(), infinite) .then( [=](caf::actor media_actor) mutable { - auto media = utility::UuidActor(media_ptr.media_uuid_, media_actor); + auto media = utility::UuidActor(media_ptr.media_uuid(), media_actor); ColourOperationDataPtr result = - colour_op_graphics_data(media_source, media_ptr.params_); + colour_op_graphics_data(media_source, media_ptr.params()); rp.deliver(result); }, @@ -60,11 +68,8 @@ caf::message_handler ColourOpPlugin::message_handler_extensions() { auto self = caf::actor_cast(this); auto count = std::make_shared(mptr_and_timepoints.size()); for (size_t i = 0; i < mptr_and_timepoints.size(); ++i) { - request( - self, - infinite, - get_colour_pipe_data_atom_v, - *(mptr_and_timepoints[i].second)) + mail(get_colour_pipe_data_atom_v, *(mptr_and_timepoints[i].second)) + .request(self, infinite) .then( [=](const ColourOperationDataPtr &d) mutable { (*result)[i] = d; @@ -87,6 +92,14 @@ caf::message_handler ColourOpPlugin::message_handler_extensions() { const utility::JsonStore &source_colour_params) { on_screen_source_ = utility::UuidActor(media_uuid, media_actor); onscreen_media_source_changed(on_screen_source_, source_colour_params); + }, + [=](playhead::media_source_atom, + caf::actor media_actor, + const utility::Uuid &media_uuid, + const utility::JsonStore &source_colour_params) -> bool { + on_screen_source_ = utility::UuidActor(media_uuid, media_actor); + onscreen_media_source_changed(on_screen_source_, source_colour_params); + return true; } /*, [=](media::media_source_atom, utility::UuidActor media_source, const utility::JsonStore @@ -99,16 +112,13 @@ void ColourOpPlugin::onscreen_source_colour_metadata_merge( const utility::JsonStore &additional_colour_params) { if (on_screen_source_.actor()) { - request( - on_screen_source_.actor(), infinite, colour_pipeline::get_colour_pipe_params_atom_v) + mail(colour_pipeline::get_colour_pipe_params_atom_v) + .request(on_screen_source_.actor(), infinite) .then( [=](utility::JsonStore params) { params.merge(additional_colour_params); - request( - on_screen_source_.actor(), - infinite, - colour_pipeline::set_colour_pipe_params_atom_v, - params) + mail(colour_pipeline::set_colour_pipe_params_atom_v, params) + .request(on_screen_source_.actor(), infinite) .then( [=](bool) { diff --git a/src/colour_pipeline/src/colour_pipeline.cpp b/src/colour_pipeline/src/colour_pipeline.cpp index 75d41569a..da2a1ba6f 100644 --- a/src/colour_pipeline/src/colour_pipeline.cpp +++ b/src/colour_pipeline/src/colour_pipeline.cpp @@ -1,6 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include +#include + #include "xstudio/colour_pipeline/colour_pipeline.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" @@ -21,24 +24,12 @@ std::string LUTDescriptor::as_string() const { ColourPipeline::ColourPipeline(caf::actor_config &cfg, const utility::JsonStore &init_settings) - : StandardPlugin( - cfg, init_settings.value("name", "ColourPipeline"), init_settings), - uuid_(utility::Uuid::generate()), - init_data_(init_settings) { + : StandardPlugin(cfg, "ColourPipeline", init_settings), uuid_(utility::Uuid::generate()) { cache_ = system().registry().template get(colour_cache_registry); // this ensures colour OPs get loaded load_colour_op_plugins(); - - if (!init_settings.value("is_worker", false)) { - delayed_anon_send( - caf::actor_cast(this), - std::chrono::seconds(1), - std::string("Make Workers")); - } else { - is_worker_ = true; - } } ColourPipeline::~ColourPipeline() {} @@ -53,79 +44,54 @@ size_t ColourOperationData::size() const { caf::message_handler ColourPipeline::message_handler_extensions() { - auto make_worker_func = [=] { - auto j = init_data_; - j["is_worker"] = true; - static int ct = 1; - std::stringstream nm; - nm << Module::name() << "_Worker" << ct++; - j["name"] = nm.str(); - auto worker = self_spawn(j); - link_to(worker); - if (worker) { - link_to_module( - worker, - true, // link_all_attrs - false, // both_ways - true // initial_push_sync - ); - workers_.push_back(worker); - } - return worker; - }; - return caf::message_handler( - [=](const std::string &make_workers) { - if (make_workers == "Make Workers" && allow_workers()) { - worker_pool_ = caf::actor_pool::make( - system().dummy_execution_unit(), - 4, - make_worker_func, - caf::actor_pool::round_robin()); - - thumbnail_processor_pool_ = caf::actor_pool::make( - system().dummy_execution_unit(), - 4, - make_worker_func, - caf::actor_pool::round_robin()); - - pixel_probe_worker_ = caf::actor_pool::make( - system().dummy_execution_unit(), - 1, - make_worker_func, - caf::actor_pool::round_robin()); - } + [=](colour_pipe_linearise_data_atom, + const media::AVFrameID &media_ptr) -> result { + /* Call virtual method to make the linearise transform data object */ + auto rp = make_response_promise(); + linearise_op_data(rp, media_ptr); + return rp; + }, + [=](colour_pipe_display_data_atom, + const media::AVFrameID &media_ptr) -> result { + /* Call virtual method to make the display transform data object */ + auto rp = make_response_promise(); + linear_to_display_op_data(rp, media_ptr); + return rp; }, + + // This call takes an image ptr, and returns a copy of the image ptr + // but with all colour management data filled in. + // Note that we do this in two steps. The ColourPipelineDataPtr carries + // static colour pipe data, like LUTs and shader code. This data is + // generally expensive to generate and is pre-prepared and cached by + // this actor before we need it a draw-time. + // The colour_pipe_unirorms is dynamic colour management data - stuff like + // the viewer exposure, gamma etc. This should be fast to compute and + // is not cached but provided on-demand. [=](get_colour_pipe_data_atom, - const media::AVFrameID &media_ptr, - const std::string stage) -> result { - try { - if (stage == "to_linear_op") { - const std::string lin_op_key = - linearise_op_hash(media_ptr.source_uuid_, media_ptr.params_); - - ColourOperationDataPtr linearise_data = - linearise_op_data(media_ptr.source_uuid_, media_ptr.params_); - linearise_data->order_index_ = std::numeric_limits::lowest(); - linearise_data->cache_id_ = lin_op_key; - anon_send( - cache_, media_cache::store_atom_v, lin_op_key, linearise_data); - return linearise_data; - - } else { - - const std::string display_op_key = - linear_to_display_op_hash(media_ptr.source_uuid_, media_ptr.params_); - - ColourOperationDataPtr display_op_data = - linear_to_display_op_data(media_ptr.source_uuid_, media_ptr.params_); - display_op_data->order_index_ = std::numeric_limits::max(); - display_op_data->cache_id_ = display_op_key; - return display_op_data; - } - } catch (std::exception &e) { - return make_error(xstudio_error::error, e.what()); - } + const media_reader::ImageBufPtr image) -> caf::result { + auto rp = make_response_promise(); + + mail(get_colour_pipe_data_atom_v, image.frame_id()) + .request(self(), infinite) + .then( + [=](const ColourPipelineDataPtr ptr) mutable { + media_reader::ImageBufPtr result = image; + result.colour_pipe_data_ = ptr; + + mail(colour_operation_uniforms_atom_v, result) + .request(self(), infinite) + .then( + [=](const utility::JsonStore &colour_pipe_uniforms) mutable { + result.colour_pipe_uniforms_ = colour_pipe_uniforms; + rp.deliver(result); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + + return rp; }, [=](get_colour_pipe_data_atom, @@ -157,7 +123,7 @@ caf::message_handler ColourPipeline::message_handler_extensions() { // colour transform (for example, the id should be based on the // media source colourspce and the OCIO View and Display properties // of the ColourPipeline. - std::string transform_id = fast_display_transform_hash(media_ptr); + std::string transform_id = std::to_string(fast_display_transform_hash(media_ptr)); // check if we have already received a request for this data but are // still waiting for workers to finish. Stores the response promise @@ -173,35 +139,46 @@ caf::message_handler ColourPipeline::message_handler_extensions() { return rp; } - // No cached data, so we need to get the workers to do generate the - // data - auto worker = worker_pool_ ? worker_pool_ : self(); - - request(worker, infinite, get_colour_pipe_data_atom_v, media_ptr, "to_linear_op") + mail(colour_pipe_linearise_data_atom_v, media_ptr) + .request(self(), infinite) .then( [=](ColourOperationDataPtr linearise_data) mutable { - request( - worker, - infinite, - get_colour_pipe_data_atom_v, - media_ptr, - "to_display_op") + mail(colour_pipe_display_data_atom_v, media_ptr) + .request(self(), infinite) .then( [=](ColourOperationDataPtr &to_display_data) mutable { + // when colour operations are applied in the + // GPU display shader, we need to know what + // order they happen in. Linearise operation + // always comes first. Display transform is + // the last to happen before fragments hit + // the screen. In between, we may have other + // operations (applied in linear space) + // like gamma, saturation or other grading + // ops + linearise_data->order_index_ = + std::numeric_limits::lowest(); + to_display_data->order_index_ = + std::numeric_limits::max(); + add_cache_keys( transform_id, - linearise_data->cache_id_, - to_display_data->cache_id_); - anon_send( - cache_, + linearise_data->cache_id(), + to_display_data->cache_id()); + + anon_mail( media_cache::store_atom_v, - to_display_data->cache_id_, - to_display_data); - anon_send( - cache_, + to_display_data->cache_id(), + to_display_data) + .urgent() + .send(cache_); + + anon_mail( media_cache::store_atom_v, - linearise_data->cache_id_, - linearise_data); + linearise_data->cache_id(), + linearise_data) + .urgent() + .send(cache_); finalise_colour_pipeline_data( media_ptr, @@ -222,8 +199,8 @@ caf::message_handler ColourPipeline::message_handler_extensions() { [=](display_colour_transform_hash_atom, - const media::AVFrameID &media_ptr) -> result { - auto rp = make_response_promise(); + const media::AVFrameID &media_ptr) -> result { + auto rp = make_response_promise(); if (thumbnail_processor_pool_) { rp.delegate( thumbnail_processor_pool_, display_colour_transform_hash_atom_v, media_ptr); @@ -236,19 +213,7 @@ caf::message_handler ColourPipeline::message_handler_extensions() { const media::AVFrameID &mptr, const thumbnail::ThumbnailBufferPtr &buf) -> result { auto rp = make_response_promise(); - if (thumbnail_processor_pool_) { - rp.delegate( - thumbnail_processor_pool_, - media_reader::process_thumbnail_atom_v, - mptr, - buf); - } else { - try { - rp.deliver(process_thumbnail(mptr, buf)); - } catch (const std::exception &err) { - rp.deliver(make_error(sec::runtime_error, err.what())); - } - } + process_thumbnail(rp, mptr, buf); return rp; }, [=](pixel_info_atom atom, @@ -274,35 +239,37 @@ caf::message_handler ColourPipeline::message_handler_extensions() { const media_reader::ImageBufPtr &image) -> result { auto rp = make_response_promise(); - if (worker_pool_) { - rp.delegate(worker_pool_, atom, image); - } else { - auto result = std::make_shared(); - for (const auto &op : image.colour_pipe_data_->operations()) { - result->merge(update_shader_uniforms(image, op->user_data_)); - } - auto rcount = std::make_shared((int)colour_op_plugins_.size()); + if (!image.colour_pipe_data_) { + rp.deliver(utility::JsonStore()); + return rp; + } - if (!colour_op_plugins_.size()) { - rp.deliver(*result); - return rp; - } - for (auto &colour_op_plugin : colour_op_plugins_) { + auto result = std::make_shared(); + for (const auto &op : image.colour_pipe_data_->operations()) { + result->merge(update_shader_uniforms(image, op->user_data_)); + } + auto rcount = std::make_shared((int)colour_op_plugins_.size()); - request(colour_op_plugin, infinite, colour_operation_uniforms_atom_v, image) - .then( - [=](const utility::JsonStore &uniforms) mutable { - result->merge(uniforms); - (*rcount)--; - if (!(*rcount)) { - rp.deliver(*result); - } - }, - [=](const caf::error &err) mutable { - (*rcount) = 0; - rp.deliver(err); - }); - } + if (!colour_op_plugins_.size()) { + rp.deliver(*result); + return rp; + } + for (auto &colour_op_plugin : colour_op_plugins_) { + + mail(colour_operation_uniforms_atom_v, image) + .request(colour_op_plugin, infinite) + .then( + [=](const utility::JsonStore &uniforms) mutable { + result->merge(uniforms); + (*rcount)--; + if (!(*rcount)) { + rp.deliver(*result); + } + }, + [=](const caf::error &err) mutable { + (*rcount) = 0; + rp.deliver(err); + }); } return rp; @@ -329,7 +296,8 @@ caf::message_handler ColourPipeline::message_handler_extensions() { const int idx = outer_idx++; - request(self, infinite, get_colour_pipe_data_atom_v, *(mptr_and_tp.second)) + mail(get_colour_pipe_data_atom_v, *(mptr_and_tp.second)) + .request(self, infinite) .then( [=](const ColourPipelineDataPtr &cpd) mutable { (*result)[idx] = cpd; @@ -346,32 +314,60 @@ caf::message_handler ColourPipeline::message_handler_extensions() { return rp; }, + [=](playhead::media_source_atom, + const utility::UuidActor &media_source) -> result { + // this message handler lets you force the colour pipeline actor + // to update based on a given media actor and get a response when + // it has finished updating. It's used by the offscreen viewport, + // for example, to make the OCIO view/display attributes to update + // for a given media source so we can show the view/display options + // in the snapshot dialog + auto rp = make_response_promise(); + + if (media_source) { + mail(get_colour_pipe_params_atom_v) + .request(media_source.actor(), infinite) + .then( + [=](const utility::JsonStore colour_params) mutable { + fan_out_request( + colour_op_plugins_, + infinite, + playhead::media_source_atom_v, + media_source.actor(), + media_source.uuid(), + colour_params) + .then( + [=](const std::vector) mutable { + media_source_changed(media_source, colour_params); + rp.deliver(true); + }, + [=](const caf::error &err) mutable { rp.deliver(err); }); + }, + [=](const caf::error &err) mutable { rp.deliver(err); }); + } else { + rp.deliver(false); + } + return rp; + }, [=](utility::event_atom, playhead::media_source_atom, caf::actor media_actor, const utility::Uuid &media_uuid) { - for (auto &worker : workers_) { - anon_send( - worker, - utility::event_atom_v, - playhead::media_source_atom_v, - media_actor, - media_uuid); - } // This message comes from the playhead when the onscreen (key) // media source has changed. if (media_actor and media_uuid) { - request(media_actor, infinite, get_colour_pipe_params_atom_v) + mail(get_colour_pipe_params_atom_v) + .request(media_actor, infinite) .then( [=](const utility::JsonStore &colour_params) mutable { for (auto &colour_op_plugin : colour_op_plugins_) { - anon_send( - colour_op_plugin, + anon_mail( utility::event_atom_v, playhead::media_source_atom_v, media_actor, media_uuid, - colour_params); + colour_params) + .send(colour_op_plugin); } media_source_changed(media_uuid, colour_params); @@ -382,12 +378,12 @@ caf::message_handler ColourPipeline::message_handler_extensions() { } }, [=](connect_to_viewport_atom, - caf::actor viewport_actor, const std::string &viewport_name, const std::string &viewport_toolbar_name, - bool connect) { + bool connect, + caf::actor viewport) { disable_linking(); - connect_to_viewport(viewport_name, viewport_toolbar_name, connect); + connect_to_viewport(viewport_name, viewport_toolbar_name, connect, viewport); enable_linking(); connect_to_ui(); }, @@ -401,51 +397,22 @@ caf::message_handler ColourPipeline::message_handler_extensions() { [=](media_reader::process_thumbnail_atom, const media::AVFrameID &mptr, const thumbnail::ThumbnailBufferPtr &buf) { - delegate( - thumbnail_processor_pool_, media_reader::process_thumbnail_atom_v, mptr, buf); + return mail(media_reader::process_thumbnail_atom_v, mptr, buf) + .delegate(thumbnail_processor_pool_); }, [=](json_store::update_atom, const utility::JsonStore & /*change*/, const std::string & /*path*/, const utility::JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full).delegate(actor_cast(this)); }, [=](ui::viewport::viewport_playhead_atom, caf::actor playhead) { // TODO: set something up to listen to playhead pre-compute colour }, - [=](json_store::update_atom, const utility::JsonStore &) mutable { - /*try { - size_t count = 6;//preference_value(j, - "/core/colour_pipeline/max_worker_count"); if (count > worker_count) { - spdlog::debug( - "colour_pipeline workers changed old {} new {}", worker_count, - count); while (worker_count < count) { anon_send( worker_pool_, sys_atom_v, - put_atom_v, - system().spawn()); - worker_count++; - } - } else if (count < worker_count) { - // get actors.. - spdlog::debug( - "colour_pipeline workers changed old {} new {}", worker_count, - count); worker_count = count; request(worker_pool_, infinite, sys_atom_v, - get_atom_v) .await( - [=](const std::vector &ws) { - for (auto i = worker_count; i < ws.size(); i++) { - anon_send(worker_pool_, sys_atom_v, delete_atom_v, ws[i]); - } - }, - [=](const error &err) { - throw std::runtime_error( - "Failed to find pool " + to_string(err)); - }); - } - } catch (...) { - }*/ - }, + [=](json_store::update_atom, const utility::JsonStore &) mutable {}, [=](utility::serialise_atom) -> utility::JsonStore { return serialise(); }, [=](ui::viewport::pre_render_gpu_hook_atom, - const int viewer_index) -> result { + const std::string &viewport_name) -> result { // This message handler overrides the one in PluginBase class. // op plugins themselves might have a GPUPreDrawHook that needs // to be passed back up to the Viewport object that is making this @@ -453,10 +420,10 @@ caf::message_handler ColourPipeline::message_handler_extensions() { // (see load_colour_op_plugins) we therefore need our own logic here. auto rp = make_response_promise(); if (colour_ops_loaded_) { - make_pre_draw_gpu_hook(rp, viewer_index); + make_pre_draw_gpu_hook(viewport_name, rp); } else { // add to a queue of these requests pending a response - hook_requests_.push_back(std::make_pair(rp, viewer_index)); + hook_requests_.push_back(std::make_pair(viewport_name, rp)); // load_colour_op_plugins() will respond to these requests // when all the plugins are loaded. } @@ -524,8 +491,7 @@ void ColourPipeline::finalise_colour_pipeline_data( to_display_data->order_index_ = std::numeric_limits::max(); result->add_operation(linearise_data); result->add_operation(to_display_data); - result->cache_id_ = linearise_data->cache_id_; - result->cache_id_ += to_display_data->cache_id_; + result->set_cache_id(linearise_data->cache_id() + to_display_data->cache_id()); add_colour_op_plugin_data(media_ptr, result, transform_id); } @@ -547,11 +513,14 @@ void ColourPipeline::add_colour_op_plugin_data( for (auto &colour_op_plugin : colour_op_plugins_) { - request(colour_op_plugin, infinite, get_colour_pipe_data_atom_v, media_ptr) + mail(get_colour_pipe_data_atom_v, media_ptr) + .request(colour_op_plugin, infinite) .then( [=](ColourOperationDataPtr colour_op_data) mutable { - result->add_operation(colour_op_data); - result->cache_id_ += colour_op_data->cache_id_; + if (colour_op_data) { + result->add_operation(colour_op_data); + result->set_cache_id(result->cache_id() + colour_op_data->cache_id()); + } (*rcount)--; if (!(*rcount)) { @@ -595,11 +564,10 @@ void ColourPipeline::load_colour_op_plugins() { // plugin_manager_registry is busy spawning 'this'. auto pm = system().registry().template get(plugin_manager_registry); - request( - pm, - infinite, + mail( utility::detail_atom_v, plugin_manager::PluginType(plugin_manager::PluginFlags::PF_COLOUR_OPERATION)) + .request(pm, infinite) .then( [=](const std::vector &colour_op_plugin_details) mutable { @@ -614,15 +582,11 @@ void ColourPipeline::load_colour_op_plugins() { } for (const auto &pd : colour_op_plugin_details) { - request( - pm, - infinite, - plugin_manager::spawn_plugin_atom_v, - pd.uuid_, - utility::JsonStore()) + mail(plugin_manager::spawn_plugin_atom_v, pd.uuid_, utility::JsonStore()) + .request(pm, infinite) .then( [=](caf::actor colour_op_actor) mutable { - anon_send(colour_op_actor, module::connect_to_ui_atom_v); + anon_mail(module::connect_to_ui_atom_v).send(colour_op_actor); colour_op_plugins_.push_back(colour_op_actor); // TODO: uncomment this when we've fixed colour grading @@ -638,7 +602,7 @@ void ColourPipeline::load_colour_op_plugins() { }, [=](const caf::error &err) mutable { for (auto &hr : hook_requests_) { - hr.first.deliver(err); + hr.second.deliver(err); } hook_requests_.clear(); }); @@ -646,7 +610,7 @@ void ColourPipeline::load_colour_op_plugins() { }, [=](const caf::error &err) mutable { for (auto &hr : hook_requests_) { - hr.first.deliver(err); + hr.second.deliver(err); } hook_requests_.clear(); }); @@ -677,14 +641,13 @@ class HookCollection : public plugin::GPUPreDrawHook { void ColourPipeline::make_pre_draw_gpu_hook( - caf::typed_response_promise rp, const int viewer_index) { + const std::string &viewport_name, + caf::typed_response_promise rp) { - // assumption: requests made in load_colour_op_plugins have finished - HookCollection *collection = new HookCollection(); - auto result = plugin::GPUPreDrawHookPtr(static_cast(collection)); + auto collection = std::make_shared(); if (colour_op_plugins_.empty()) { - rp.deliver(result); + rp.deliver(collection); return; } caf::scoped_actor sys(system()); @@ -695,8 +658,8 @@ void ColourPipeline::make_pre_draw_gpu_hook( auto count = std::make_shared(colour_op_plugins_.size()); for (auto &colour_op_plugin : colour_op_plugins_) { - request( - colour_op_plugin, infinite, ui::viewport::pre_render_gpu_hook_atom_v, viewer_index) + mail(ui::viewport::pre_render_gpu_hook_atom_v, viewport_name) + .request(colour_op_plugin, infinite) .then( [=](plugin::GPUPreDrawHookPtr &hook) mutable { if (hook) { @@ -705,7 +668,7 @@ void ColourPipeline::make_pre_draw_gpu_hook( } (*count)--; if (!(*count)) { - rp.deliver(result); + rp.deliver(collection); } }, [=](const caf::error &err) mutable { diff --git a/src/colour_pipeline/src/colour_pipeline_actor.cpp b/src/colour_pipeline/src/colour_pipeline_actor.cpp index 4a9e58b15..9c2f45469 100644 --- a/src/colour_pipeline/src/colour_pipeline_actor.cpp +++ b/src/colour_pipeline/src/colour_pipeline_actor.cpp @@ -1,10 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include "xstudio/atoms.hpp" #include "xstudio/colour_pipeline/colour_pipeline_actor.hpp" #include "xstudio/global_store/global_store.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/logging.hpp" @@ -36,15 +36,6 @@ GlobalColourPipelineActor::GlobalColourPipelineActor(caf::actor_config &cfg) load_colour_pipe_details(); set_parent_actor_addr(actor_cast(this)); - - set_down_handler([=](down_msg &msg) { - for (auto p = colour_piplines_.begin(); p != colour_piplines_.end(); ++p) { - if (p->second == msg.source) { - colour_piplines_.erase(p); - break; - } - } - }); } GlobalColourPipelineActor::~GlobalColourPipelineActor() { colour_piplines_.clear(); } @@ -54,23 +45,29 @@ caf::behavior GlobalColourPipelineActor::make_behavior() { [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) { // nop }, - [=](colour_pipeline_atom, const std::string &viewport_name) -> result { + [=](colour_pipeline_atom, const std::string &viewport_name) -> caf::actor { + if (colour_piplines_.find(viewport_name) != colour_piplines_.end()) { + return colour_piplines_[viewport_name]; + } + return caf::actor(); + }, + [=](colour_pipeline_atom, + const std::string &viewport_name, + const std::string &window_id) -> result { auto rp = make_response_promise(); auto init_data = prefs_jsn_; init_data["viewport_name"] = viewport_name; + init_data["window_id"] = window_id; make_colour_pipeline(default_plugin_name_, init_data, rp); return rp; }, [=](get_thumbnail_colour_pipeline_atom) -> result { auto rp = make_response_promise(); - if (colour_piplines_.find("viewport0") != colour_piplines_.end()) { - rp.deliver(colour_piplines_["viewport0"]); + if (colour_piplines_.find("thumbnail_processor") != colour_piplines_.end()) { + rp.deliver(colour_piplines_["thumbnail_processor"]); } else { - request( - caf::actor_cast(this), - infinite, - colour_pipeline_atom_v, - "viewport0") + mail(colour_pipeline_atom_v, "thumbnail_processor", "offscreen") + .request(caf::actor_cast(this), infinite) .then( [=](caf::actor colour_pipe) mutable { rp.deliver(colour_pipe); }, [=](caf::error &err) mutable { rp.deliver(err); }); @@ -81,24 +78,22 @@ caf::behavior GlobalColourPipelineActor::make_behavior() { const JsonStore & /*change*/, const std::string & /*path*/, const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full).delegate(actor_cast(this)); }, [=](json_store::update_atom, const JsonStore &js) { prefs_jsn_ = js; }, [=](media_reader::process_thumbnail_atom, const media::AVFrameID &mptr, const thumbnail::ThumbnailBufferPtr &buf) -> result { auto rp = make_response_promise(); - if (colour_piplines_.find("viewport0") != colour_piplines_.end()) { + if (colour_piplines_.find("thumbnail_processor") != colour_piplines_.end()) { rp.delegate( - colour_piplines_["viewport0"], + colour_piplines_["thumbnail_processor"], media_reader::process_thumbnail_atom_v, mptr, buf); } else { - request( - caf::actor_cast(this), - infinite, - get_thumbnail_colour_pipeline_atom_v) + mail(get_thumbnail_colour_pipeline_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](caf::actor colour_pipe) mutable { rp.delegate( @@ -168,7 +163,8 @@ void GlobalColourPipelineActor::make_colour_pipeline( } auto pm = system().registry().template get(plugin_manager_registry); - request(pm, infinite, plugin_manager::spawn_plugin_atom_v, uuid, jsn) + mail(plugin_manager::spawn_plugin_atom_v, uuid, jsn) + .request(pm, infinite) .await( [=](caf::actor colour_pipe) mutable { // link_to(colour_pipe); @@ -179,7 +175,19 @@ void GlobalColourPipelineActor::make_colour_pipeline( send_exit(colour_pipe, caf::exit_reason::user_shutdown); } else { colour_piplines_[viewport_name] = colour_pipe; - monitor(colour_pipe); + + monitor( + colour_pipe, [this, addr = colour_pipe.address()](const error &) { + for (auto p = colour_piplines_.begin(); + p != colour_piplines_.end(); + ++p) { + if (p->second == addr) { + colour_piplines_.erase(p); + break; + } + } + }); + rp.deliver(colour_pipe); } }, diff --git a/src/conform/src/CMakeLists.txt b/src/conform/src/CMakeLists.txt index cdf381bf0..35c91b655 100644 --- a/src/conform/src/CMakeLists.txt +++ b/src/conform/src/CMakeLists.txt @@ -1,9 +1,9 @@ SET(LINK_DEPS xstudio::utility - xstudio::broadcast xstudio::json_store xstudio::global_store - caf::core + xstudio::timeline + CAF::core ) -create_component(conform 0.1.0 "${LINK_DEPS}") +create_component(conform ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/conform/src/conform_manager_actor.cpp b/src/conform/src/conform_manager_actor.cpp index 8b307d96e..8936679bc 100644 --- a/src/conform/src/conform_manager_actor.cpp +++ b/src/conform/src/conform_manager_actor.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "xstudio/atoms.hpp" #include "xstudio/global_store/global_store.hpp" @@ -11,6 +12,8 @@ #include "xstudio/plugin_manager/plugin_manager.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/json_store.hpp" +#include "xstudio/timeline/item.hpp" +#include "xstudio/timeline/track_actor.hpp" #include "xstudio/utility/logging.hpp" using namespace xstudio; @@ -23,50 +26,83 @@ using namespace caf; ConformWorkerActor::ConformWorkerActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { - std::vector conformers; - // get hooks - { - auto pm = system().registry().template get(plugin_manager_registry); - scoped_actor sys{system()}; - auto details = request_receive>( - *sys, - pm, - utility::detail_atom_v, - plugin_manager::PluginType(plugin_manager::PluginFlags::PF_CONFORM)); - - for (const auto &i : details) { - if (i.enabled_) { - auto actor = request_receive( - *sys, pm, plugin_manager::spawn_plugin_atom_v, i.uuid_); - link_to(actor); - conformers.push_back(actor); - } - } - } + // { + // auto pm = system().registry().template get(plugin_manager_registry); + // scoped_actor sys{system()}; + // auto details = request_receive>( + // *sys, + // pm, + // utility::detail_atom_v, + // plugin_manager::PluginType(plugin_manager::PluginFlags::PF_CONFORM)); + + // for (const auto &i : details) { + // if (i.enabled_) { + // auto actor = request_receive( + // *sys, pm, plugin_manager::spawn_plugin_atom_v, i.uuid_); + // link_to(actor); + // conformers_.push_back(actor); + // } + // } + // } - // distribute to all conformers. + // // distribute to all conformers. + // required to assing conform plugins + // could be an issue is plugins are late. + anon_mail(conform_tasks_atom_v) + .delay(std::chrono::seconds(5)) + .send(caf::actor_cast(this), weak_ref); behavior_.assign( [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, [=](conform_tasks_atom) -> result> { - if (not conformers.empty()) { + if (not initialised_) { + // should be the first function called by the manager + auto pm = system().registry().template get(plugin_manager_registry); + scoped_actor sys{system()}; + try { + auto details = request_receive>( + *sys, + pm, + utility::detail_atom_v, + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_CONFORM)); + + for (const auto &i : details) { + if (i.enabled_) { + auto actor = request_receive( + *sys, pm, plugin_manager::spawn_plugin_atom_v, i.uuid_); + link_to(actor); + conformers_.push_back(actor); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + initialised_ = true; + } + + if (not conformers_.empty()) { auto rp = make_response_promise>(); - fan_out_request(conformers, infinite, conform_tasks_atom_v) + fan_out_request(conformers_, infinite, conform_tasks_atom_v) .then( [=](const std::vector> all_results) mutable { // compile results.. - auto results = std::set(); + auto dups = std::set(); + auto results = std::vector(); for (const auto &i : all_results) { for (const auto &j : i) { - results.insert(j); + if (dups.count(j)) + continue; + + dups.insert(j); + results.push_back(j); } } - rp.deliver( - std::vector(results.begin(), results.end())); + rp.deliver(results); }, [=](const error &err) mutable { rp.deliver(err); }); return rp; @@ -76,93 +112,1298 @@ ConformWorkerActor::ConformWorkerActor(caf::actor_config &cfg) : caf::event_base }, [=](conform_atom, - const std::string &conform_task, - const utility::JsonStore &conform_detail, + const UuidActor &source_playlist, + const UuidActor &source_timeline, + const UuidActorVector &tracks, + const UuidActor &target_playlist, + const UuidActor &target_timeline, + const UuidActor &conform_track) -> result { + auto rp = make_response_promise(); + + conform_tracks_to_sequence( + rp, + source_playlist, + source_timeline, + tracks, + target_playlist, + target_timeline, + conform_track); + + return rp; + }, + + // find matching clips in timeline + [=](conform_atom, + const std::string &key, + const UuidActor &clip, + const UuidActor &timeline) { + auto rp = make_response_promise(); + + find_matched(rp, key, clip, timeline); + + return rp; + }, + + // return media / sequence pairs. + // used when conforming media into new sequences + [=](conform_atom, const UuidActorVector &media) + -> result>>> { + auto rp = make_response_promise>>>(); + + get_media_sequences(rp, media); + + return rp; + }, + + [=](conform_atom, + const utility::JsonStore &conform_operations, const UuidActor &playlist, + const UuidActor &timeline, + const UuidActor &conform_track, const UuidActorVector &media) -> result { + auto rp = make_response_promise(); + + conform_to_timeline( + rp, conform_operations, playlist, timeline, conform_track, media); + + return rp; + }, + + // manipulate timeline to populate conform track + // and also other initial preparations + [=](conform_atom, + const UuidActor &timeline, + const bool only_create_conform_track) -> result { + // make worker gather all the information + auto rp = make_response_promise(); + prepare_sequence(rp, timeline, only_create_conform_track); + return rp; + }, + + [=](conform_atom, + const std::string &conform_task, + const utility::JsonStore &conform_operations, + const UuidActor &playlist, + const UuidActor &container, + const std::string &item_type, + const UuidActorVector &items, + const UuidVector &insert_before) -> result { // make worker gather all the information auto rp = make_response_promise(); - request(playlist.actor(), infinite, json_store::get_json_atom_v, "") - .then( - [=](const JsonStore &playlist_json) mutable { - // get all media json.. - fan_out_request( - vector_to_caf_actor_vector(media), - infinite, - json_store::get_json_atom_v, - utility::Uuid(), - "", - true) - .then( - [=](const std::vector> - media_json_reply) mutable { - // reorder into Conform request. - auto media_json = - std::vector>(); - std::map jsn_map; - for (const auto &i : media_json_reply) - jsn_map[i.first.uuid()] = i.second; - - for (const auto &i : media) - media_json.emplace_back( - std::make_tuple(jsn_map.at(i.uuid()))); - - rp.delegate( - caf::actor_cast(this), - conform_atom_v, - conform_task, - conform_detail, - ConformRequest(playlist, playlist_json, media_json)); - }, - [=](const error &err) mutable { rp.deliver(err); }); - }, - [=](const error &err) mutable { rp.deliver(err); }); + conform_to_media( + rp, + conform_task, + conform_operations, + playlist, + container, + item_type, + items, + insert_before); + + return rp; + }, + + [=](conform_atom, const ConformRequest &request) -> result { + auto rp = make_response_promise(); + + process_request(rp, request); return rp; }, [=](conform_atom, const std::string &conform_task, - const utility::JsonStore &conform_detail, const ConformRequest &request) -> result { - if (not conformers.empty()) { - auto rp = make_response_promise(); - fan_out_request( - conformers, infinite, conform_atom_v, conform_task, conform_detail, request) - .then( - [=](const std::vector all_results) mutable { - // compile results.. - auto result = ConformReply(); - result.items_.resize(request.items_.size()); + auto rp = make_response_promise(); - for (const auto &i : all_results) { - if (not i.items_.empty()) { - // insert values into result. - auto count = 0; - for (const auto &j : i.items_) { - // replace, don't sum results, so we only expect one - // result set in total from a plugin. - if (j and not result.items_[count]) - result.items_[count] = j; - count++; + process_task_request(rp, conform_task, request); + + return rp; + }); +} + +// similar to conform media to sequence. + +void ConformWorkerActor::conform_tracks_to_sequence( + caf::typed_response_promise rp, + const UuidActor &source_playlist, + const UuidActor &source_timeline, + const UuidActorVector &tracks, + const UuidActor &target_playlist, + const UuidActor &target_timeline, + const UuidActor &conform_track) { + try { + scoped_actor sys{system()}; + + auto timeline_prop = request_receive( + *sys, target_timeline.actor(), timeline::item_prop_atom_v); + + auto conform_track_uuid = Uuid(); + + if (conform_track.uuid().is_null()) { + if (timeline_prop.is_null() or not timeline_prop.count("conform_track_uuid")) + throw std::runtime_error("No conform track defined."); + conform_track_uuid = timeline_prop.value("conform_track_uuid", utility::Uuid()); + } else { + conform_track_uuid = conform_track.uuid(); + } + + if (conform_track_uuid.is_null()) + throw std::runtime_error("No conform track defined."); + + // find conform track.. + auto conform_track_item = request_receive( + *sys, target_timeline.actor(), timeline::item_atom_v, conform_track_uuid); + + // clear state of conform track items + conform_track_item.unbind(); + // conform_track_item.reset_actor(true); + conform_track_item.set_enabled(true); + conform_track_item.set_locked(false); + + auto clip_items = conform_track_item.find_all_items(timeline::IT_CLIP); + for (auto &i : clip_items) { + i.get().set_flag(""); + i.get().set_enabled(true); + i.get().set_locked(false); + } + + // populate track templates + // we override the conform track settings with the source tracks + auto ritems = std::vector(); + auto template_tracks = std::vector(); + + for (const auto &i : tracks) { + auto track_item = + request_receive(*sys, i.actor(), timeline::item_atom_v); + + // think that's the lot.. + conform_track_item.set_name(track_item.name()); + conform_track_item.set_locked(track_item.locked()); + conform_track_item.set_enabled(track_item.enabled()); + conform_track_item.set_flag(track_item.flag()); + conform_track_item.set_uuid(track_item.uuid()); + + // we now need to populate the ritems / clips.. + // but we need to associate the clips with the source correct track + template_tracks.push_back(conform_track_item); + + auto track_clip_items = track_item.find_all_items(timeline::IT_CLIP); + + for (auto i : track_clip_items) { + auto clip = i.get(); + auto clip_ua = clip.uuid_actor(); + + // get media actor from clip + auto media_ua = + request_receive(*sys, clip.actor(), playlist::get_media_atom_v); + + clip.unbind(); + clip.reset_actor(true); + + if (source_playlist.actor() != target_playlist.actor() and media_ua.actor()) { + // clone media.. + try { + // might not need this.. + media_ua = request_receive( + *sys, media_ua.actor(), duplicate_atom_v) + .second; + + try { + request_receive( + *sys, + target_playlist.actor(), + playlist::add_media_atom_v, + media_ua, + Uuid()); + } catch (const std::exception &err) { + spdlog::warn("add to playlist {}", err.what()); + } + } catch (const std::exception &err) { + spdlog::warn("duplicate media {}", err.what()); + } + } + + ritems.emplace_back( + ConformRequestItem(media_ua, media_ua, clip, track_item.uuid())); + } + } + + // clear state of conform track items + auto conform_operations = JsonStore(conform::ConformOperationsJSON); + conform_operations["create_media"] = false; + conform_operations["remove_media"] = false; + conform_operations["insert_media"] = true; + conform_operations["replace_clip"] = false; + conform_operations["new_track_name"] = ""; + conform_operations["remove_failed_clip"] = true; + // stop doubling up of double dips + conform_operations["only_one_clip_match"] = true; + + + auto crequest = + ConformRequest(target_playlist, target_timeline, template_tracks, ritems); + crequest.operations_ = conform_operations; + + // add template track clip_item medadata + for (const auto &i : clip_items) { + crequest.metadata_[i.get().uuid()] = i.get().prop(); + } + + // add ritem clip metadata + for (const auto &i : ritems) { + if (not i.clip_.uuid().is_null()) + crequest.metadata_[i.clip_.uuid()] = i.clip_.prop(); + } + + // need to populate all metadata. + conform_chain(rp, crequest); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(sec::runtime_error, err.what())); + } +} + +void ConformWorkerActor::process_task_request( + caf::typed_response_promise rp, + const std::string &conform_task, + const ConformRequest &request) { + if (not conformers_.empty()) { + // request.dump(); + fan_out_request( + conformers_, infinite, conform_atom_v, conform_task, request) + .then( + [=](const std::vector all_results) mutable { + // compile results.. + auto result = ConformReply(request); + result.items_.resize(request.items_.size()); + + for (const auto &i : all_results) { + if (not i.items_.empty()) { + // map actions already taken + result.operations_.update(i.operations_); + + // insert values into result. + auto count = 0; + for (const auto &j : i.items_) { + // replace, don't sum results, so we only expect one + // result set in total from a plugin. + if (j and not result.items_[count]) + result.items_[count] = j; + count++; + } + } + } + + result.request_ = request; + + // check for deletion request. + if (result.request_.item_type_ != "Clip" and + result.operations_.at("remove_media") == false and + result.request_.operations_.at("remove_media") == true) { + // not removed and is requested to remove.. + // we only remove if there is a replacement found + // iterate over source media, remove if results found. + result.operations_["remove_media"] = true; + auto count = 0; + for (const auto &i : result.request_.items_) { + if (result.items_[count] and not result.items_[count]->empty()) { + anon_mail(playlist::remove_media_atom_v, i.media_.uuid()) + .send(result.request_.container_.actor()); + } + count++; + } + } + + if (result.request_.item_type_ == "Clip" and + result.operations_.at("replace_clip") == false and + result.request_.operations_.at("replace_clip") == true) { + // not removed and is requested to remove.. + // we only remove if there is a replacement found + // iterate over source media, remove if results found. + result.operations_["replace_clip"] = true; + auto count = 0; + for (const auto &i : result.request_.items_) { + if (result.items_.at(count) and + not result.items_.at(count)->empty()) { + const auto &ritems = *(result.items_.at(count)); + const UuidActor &m = std::get<0>(ritems.at(0)); + + anon_mail(timeline::link_media_atom_v, m).send(i.item_.actor()); + } + count++; + } + } + + if (result.request_.item_type_ == "Clip" and + result.operations_.at("replace_clip") == true and + result.request_.operations_.at("remove_failed_clip") == true) { + + // remove clips that had no results. + result.operations_["remove_failed_clip"] = true; + auto count = 0; + for (const auto &i : result.request_.items_) { + if (not result.items_[count] or result.items_[count]->empty()) { + // candidate.. + // send deletion request to timeline using clips + // id/actor uuid spdlog::warn("send delete to {} {} for + // {} {}", + // to_string(result.request_.container_.actor()), + // to_string(result.request_.container_.uuid()), + // to_string(std::get<0>(i).actor()), + // to_string(std::get<0>(i).uuid()) + // ); + anon_mail( + timeline::erase_item_atom_v, i.item_.uuid(), true, true) + .send(result.request_.container_.actor()); + } + count++; + } + } + + // we've got a list of media actors ? + // if we're dealing with clips we should replace them ? + + rp.deliver(result); + }, + [=](const error &err) mutable { rp.deliver(err); }); + } else { + rp.deliver(ConformReply(request)); + } +} + +void ConformWorkerActor::process_request( + caf::typed_response_promise rp, const ConformRequest &request) { + try { + if (not conformers_.empty()) { + // request.dump(); + fan_out_request(conformers_, infinite, conform_atom_v, request) + .then( + [=](const std::vector all_results) mutable { + // compile results.. + auto result = ConformReply(request); + result.items_.resize(request.items_.size()); + + for (const auto &i : all_results) { + if (not i.items_.empty()) { + // reply might have modied the request.. (HACKY) + result.request_ = i.request_; + // result.request_.dump(); + // map actions already taken + result.operations_.update(i.operations_); + + // spdlog::warn("{} {}", request.items_.size(), + // i.items_.size()); insert values into result. + auto count = 0; + for (const auto &j : i.items_) { + // replace, don't sum results, so we only expect one + // result set in total from a plugin. + if (j and not result.items_[count]) { + // spdlog::warn("add result item{}", count); + result.items_[count] = j; } + count++; } } + } - rp.deliver(result); + if (result.request_.operations_.value("match_only", false)) + return rp.deliver(result); + + // result.request_ = request; + + // this is where the magic happens.. + // we've got a list of media + // and a list of clips associated with them. + + // we need to construct N tracks based off the supplied track + // replacing clip media with our media. we also need to rewite + // the result items with the new clip actors. + auto tracks = std::list(); + auto track_to_use = 0; + scoped_actor sys{system()}; + + auto unconformed_track = + result.request_.template_tracks_.at(track_to_use); + unconformed_track.reset_actor(true); + unconformed_track.clear(); + unconformed_track.set_name("Unconformed Media"); + + for (size_t i = 0; i < request.items_.size(); i++) { + const auto &media = request.items_.at(i).item_; + const auto &clip = request.items_.at(i).clip_; + const auto &clip_track_uuid = request.items_.at(i).clip_track_uuid_; + + // spdlog::warn("REQUESTED MEDIA {} {}", + // to_string(media.uuid()), + // static_cast(result.items_.at(i))); + + if (result.items_.at(i)) { + // clip matched. + // make sure media is in timeline. + + request_receive( + *sys, + result.request_.container_.actor(), + playlist::add_media_atom_v, + media, + Uuid()); + + // spdlog::warn("ADD MEDIA to SEQEUNCE {}", + // to_string(media.uuid())); + + auto replacemode = + result.request_.operations_.value("replace_clip", false); + // replace clip.. + // for(const auto &t: result.request_.template_tracks_) + // spdlog::warn("Template {}", to_string(t.uuid())); + + for (const auto &c : *(result.items_[i])) { + const auto clip_uuid = std::get<0>(c).uuid(); + // spdlog::warn("{}", to_string(clip_uuid)); + // for each clip asside them to this media. + auto trackit = tracks.begin(); + + // for(const auto &t: result.request_.template_tracks_) + // spdlog::warn("C {} T {}", + // to_string(clip_track_uuid),to_string(t.uuid())); + + + while (true) { + // still iterating + if (trackit == tracks.end()) { + // are we still bound to the real timeline ? + auto tmp = result.request_.template_tracks_.at( + track_to_use); + tmp.reset_actor(true); + + // zero out clip media. + auto clip_items = + tmp.find_all_items(timeline::IT_CLIP); + for (auto &i : clip_items) { + auto prop = i.get().prop(); + prop["media_uuid"] = Uuid(); + i.get().set_prop(prop); + i.get().set_flag(""); + i.get().set_enabled(true); + i.get().set_locked(false); + // if(i.get().active_range()) + // i.get().set_available_range(*(i.get().active_range())); + } + tracks.push_back(tmp); + trackit = tracks.begin(); + std::next(trackit, tracks.size() - 1); + if (track_to_use < + result.request_.template_tracks_.size() - 1) + track_to_use++; + } + + auto clipit = find_uuid(trackit->children(), clip_uuid); + auto clip_prop = clipit->prop(); + if ((clip_track_uuid.is_null() or + (clip_track_uuid == trackit->uuid())) and + clip_prop.value("media_uuid", Uuid()).is_null()) { + if (clip.item_type() == timeline::IT_CLIP) { + clipit->set_enabled(clip.enabled()); + clipit->set_locked(clip.locked()); + clipit->set_name(clip.name()); + clipit->set_flag(clip.flag()); + clip_prop = clip.prop(); + } + + clip_prop["media_uuid"] = media.uuid(); + clipit->set_prop(clip_prop); + + + if (replacemode and trackit == tracks.begin()) { + // find in source and really change it.. + // find clip actor.. + auto real_clip = find_item( + result.request_.template_tracks_.at(0) + .children(), + clip_uuid); + if (real_clip) { + // spdlog::warn("{} {}", + // (*real_clip)->name(), + // to_string((*real_clip)->actor())); + anon_mail( + timeline::link_media_atom_v, media) + .send((*real_clip)->actor()); + } + } + + break; + } else + trackit++; + } + } + } else { + // unconformed media. + // add to unconformed track. + try { + // spdlog::warn("Unconformed {}", to_string(media.uuid())); + request_receive( + *sys, + result.request_.container_.actor(), + playlist::add_media_atom_v, + media, + Uuid()); + + auto detail = + request_receive>( + *sys, + media.actor(), + media::media_reference_atom_v, + Uuid()); + + auto clip = timeline::Item( + timeline::IT_CLIP, "", unconformed_track.rate()); + + auto media_prop = R"({"media_uuid": null})"_json; + media_prop["media_uuid"] = media.uuid(); + clip.set_prop(media_prop); + unconformed_track.push_back(clip); + unconformed_track.refresh(); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + } + + // create new track actors and append to timeline. + + try { + auto track_actors = UuidActorVector(); + auto mediamap = UuidActorMap(); + + for (const auto &i : request.items_) + mediamap[i.item_.uuid()] = i.item_.actor(); + + if (result.request_.operations_.at("replace_clip") == true and + not tracks.empty()) { + // pop first track.. as this is a duplicate of our + // replacement track. force relink.. + // request_receive( + // *sys, + // result.request_.track_.actor(), + // timeline::link_media_atom_v, + // mediamap, + // true); + + tracks.pop_front(); + } + + auto new_track_name = result.request_.operations_.value( + "new_track_name", std::string()); + for (auto &i : tracks) { + i.merge_gaps(true); + i.reset_uuid(true); + if (not new_track_name.empty()) + i.set_name(new_track_name); + // reset id's.. + auto track_actor = spawn(i, i); + // spdlog::warn("NEW TRACK {} {} {}", + // to_string(track_actor), to_string(i.actor()), + // to_string(i.uuid())); + track_actors.push_back(i.uuid_actor()); + + request_receive( + *sys, + track_actor, + timeline::link_media_atom_v, + mediamap, + true); + } + + if (not unconformed_track.empty()) { + unconformed_track.merge_gaps(true); + unconformed_track.reset_uuid(true); + auto track_actor = spawn( + unconformed_track, unconformed_track); + // spdlog::warn("NEW TRACK {} {} {}", + // to_string(track_actor), to_string(i.actor()), + // to_string(i.uuid())); + track_actors.push_back(unconformed_track.uuid_actor()); + + request_receive( + *sys, + track_actor, + timeline::link_media_atom_v, + mediamap, + true); + } + + if (not track_actors.empty()) { + // get stack actor + auto stack = request_receive( + *sys, + result.request_.container_.actor(), + timeline::item_atom_v, + 0); + + std::reverse(track_actors.begin(), track_actors.end()); + request_receive( + *sys, + stack.actor(), + timeline::insert_item_atom_v, + 0, + track_actors); + } + // rebind media.. + // does the timeline even know at this point ? + // anon_mail(// timeline::link_media_atom_v, + // true).send(result.request_.container_.actor()); + + } catch (...) { + } + + rp.deliver(result); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + + } else { + rp.deliver(ConformReply(request)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(sec::runtime_error, err.what())); + } +} + + +void ConformWorkerActor::prepare_sequence( + caf::typed_response_promise rp, + const UuidActor &timeline, + const bool only_create_conform_track) { + try { + if (not conformers_.empty()) { + // request.dump(); + fan_out_request( + conformers_, infinite, conform_atom_v, timeline, only_create_conform_track) + .then( + [=](const std::vector all_results) mutable { + // compile results.. + auto result = false; + + for (const auto &i : all_results) + result |= i; + rp.deliver(result); + }, + [=](const error &err) mutable { rp.deliver(err); }); + } else { + rp.deliver(true); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(sec::runtime_error, err.what())); + } +} + +void ConformWorkerActor::conform_to_media( + caf::typed_response_promise rp, + const std::string &conform_task, + const utility::JsonStore &conform_operations, + const UuidActor &playlist, + const UuidActor &container, + const std::string &item_type, + const UuidActorVector &items, + const UuidVector &insert_before) { + auto ritems = std::vector(); + size_t count = 0; + + if (items.empty()) { + rp.deliver(ConformReply()); + } else { + for (const auto &i : items) { + auto before = Uuid(); + if (insert_before.size() > count) { + before = insert_before.at(count); + } + count++; + + if (item_type == "Clip") { + ritems.emplace_back(ConformRequestItem(i, UuidActor(), before)); + } else if (item_type == "Media") { + ritems.emplace_back(ConformRequestItem(i, i, before)); + } else { + rp.deliver(make_error(sec::runtime_error, "Unknown item type")); + } + } + + if (rp.pending()) { + auto crequest = ConformRequest(playlist, container, item_type, ritems); + crequest.operations_ = conform_operations; + conform_step_get_playlist_json(rp, conform_task, crequest); + } + } +} + +void ConformWorkerActor::get_media_sequences( + caf::typed_response_promise< + std::vector>>> rp, + const UuidActorVector &media) { + if (conformers_.empty()) { + rp.deliver( + std::vector>>( + media.size())); + } else { + // collect media metadata. + fan_out_request( + vector_to_caf_actor_vector(media), + infinite, + json_store::get_json_atom_v, + utility::Uuid(), + "", + true) + .then( + [=](const std::vector> media_metadata) mutable { + // order matters must match original order. + std::map meta_map; + for (const auto &i : media_metadata) + meta_map[i.first.uuid()] = i.second; + + std::vector> ordered_media_metadata; + for (const auto &i : media) + ordered_media_metadata.push_back(std::make_pair(i, meta_map[i.uuid()])); + + // request.dump(); + fan_out_request( + conformers_, infinite, conform_atom_v, ordered_media_metadata) + .then( + [=](const std::vector>>> + all_results) mutable { + // compile results.. + auto result = std::vector>>( + media.size()); + + for (const auto &i : all_results) { + auto index = 0; + for (const auto &j : i) { + if (not result[index] and j) + result[index] = j; + index++; + } + } + + rp.deliver(result); + }, + [=](const error &err) mutable { rp.deliver(err); }); + }, + [=](const error &err) mutable { rp.deliver(err); }); + } +} + + +void ConformWorkerActor::conform_to_timeline( + caf::typed_response_promise rp, + const utility::JsonStore &conform_operations, + const UuidActor &playlist, + const UuidActor &timeline, + const UuidActor &conform_track, + const UuidActorVector &media) { + // get copy of conform track. + try { + scoped_actor sys{system()}; + + auto timeline_prop = + request_receive(*sys, timeline.actor(), timeline::item_prop_atom_v); + + auto conform_track_uuid = conform_track.uuid(); + + if (conform_track.uuid().is_null()) { + if (timeline_prop.is_null() or not timeline_prop.count("conform_track_uuid")) + throw std::runtime_error("No conform track defined."); + conform_track_uuid = timeline_prop.value("conform_track_uuid", utility::Uuid()); + } + + if (conform_track_uuid.is_null()) + throw std::runtime_error("No conform track defined."); + + // find track.. + try { + // need media metadata populating. + auto ritems = std::vector(); + for (const auto &i : media) { + ritems.emplace_back(ConformRequestItem(i, i)); + } + + auto track_item = request_receive( + *sys, timeline.actor(), timeline::item_atom_v, conform_track_uuid); + + track_item.unbind(); + track_item.set_enabled(true); + track_item.set_locked(false); + + auto clip_items = track_item.find_all_items(timeline::IT_CLIP); + for (auto &i : clip_items) { + i.get().set_flag(""); + i.get().set_enabled(true); + i.get().set_locked(false); + } + + auto crequest = ConformRequest(playlist, timeline, track_item, ritems); + crequest.operations_ = conform_operations; + + for (const auto &i : clip_items) { + crequest.metadata_[i.get().uuid()] = i.get().prop(); + } + + // need to populate all metadata. + conform_chain(rp, crequest); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(sec::runtime_error, err.what())); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(sec::runtime_error, err.what())); + } +} + +void ConformWorkerActor::conform_chain( + caf::typed_response_promise rp, ConformRequest &conform_request) { + try { + // we step through various actions until we're done. + auto clip_items = + conform_request.template_tracks_.at(0).find_all_items(timeline::IT_CLIP); + bool require_template_clip_media_metadata = false; + bool require_media_metadata = false; + + for (const auto &i : clip_items) { + auto clip_media_uuid = i.get().prop().value("media_uuid", Uuid()); + if (not clip_media_uuid.is_null() and + not conform_request.metadata_.count(clip_media_uuid)) { + require_template_clip_media_metadata = true; + break; + } + } + + for (const auto &i : conform_request.items_) { + if (not i.media_.uuid().is_null() and + not conform_request.metadata_.count(i.media_.uuid())) { + require_media_metadata = true; + break; + } + } + + // populate playlist metadata + if (not conform_request.metadata_.count(conform_request.playlist_.uuid())) { + mail(json_store::get_json_atom_v, "") + .request(conform_request.playlist_.actor(), infinite) + .then( + [=](const JsonStore &playlist_metadata) mutable { + // we maybe processing timeline items.. + conform_request.metadata_[conform_request.playlist_.uuid()] = + playlist_metadata; + conform_chain(rp, conform_request); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + // get track clip media metadata + } else if ( + // populate track_template_clip_metadata + not clip_items.empty() and + not conform_request.metadata_.count(clip_items.begin()->get().uuid())) { + // request clip media from track + for (const auto &i : clip_items) + conform_request.metadata_[i.get().uuid()] = i.get().prop(); + conform_chain(rp, conform_request); + } else if (require_template_clip_media_metadata) { + // request clip media from track + // add clip actors with unique media + auto cactors = std::vector(); + auto media_uuids = std::set(); + for (const auto &i : clip_items) { + auto uuid = i.get().prop().value("media_uuid", Uuid()); + if (not uuid.is_null() and not media_uuids.count(uuid)) { + media_uuids.insert(uuid); + cactors.push_back(i.get().actor()); + } + } + + if (not cactors.empty()) { + // spdlog::warn("get media from clips {}", cactors.size()); + // get associated media actor for clip.. + fan_out_request( + cactors, infinite, playlist::get_media_atom_v, true) + .then( + [=](const std::vector item_media) mutable { + // got list of clip media actors. + // in this mode we just want the metadata.. + auto mactors = std::vector(); + for (const auto &i : item_media) { + if (i.second.actor()) + mactors.push_back(i.second.actor()); + } + + if (not mactors.empty()) { + // spdlog::warn("get media from clips {}", mactors.size()); + + fan_out_request( + mactors, + infinite, + json_store::get_json_atom_v, + utility::Uuid(), + "", + true) + .then( + [=](const std::vector> + media_metadata) mutable { + // add media metadata to request. + + // put json into map key media uuid + for (const auto &i : media_metadata) { + // spdlog::warn("{} {}", + // to_string(i.first.uuid()), i.second.dump(2)); + conform_request.metadata_[i.first.uuid()] = + i.second; + } + + conform_chain(rp, conform_request); + }, + [=](const error &err) mutable { + spdlog::warn( + "Failed to get template clip media metadata{} " + "{}", + __PRETTY_FUNCTION__, + to_string(err)); + rp.deliver(err); + }); + + } else { + rp.deliver( + make_error(sec::runtime_error, "Empty media json help.")); + } }, - [=](const error &err) mutable { rp.deliver(err); }); - return rp; + [=](const error &err) mutable { + spdlog::warn( + "Failed to get template clip media actors {} {}", + __PRETTY_FUNCTION__, + to_string(err)); + rp.deliver(err); + }); + + } else { + rp.deliver(make_error(sec::runtime_error, "No clip actors ?")); } - return ConformReply(); - }); + } else if (require_media_metadata) { + // populate request item media metadata. + + // if item != media it's a clip/media pair. + // in this scope it's always media.. + auto mactors = std::vector(); + for (const auto &i : conform_request.items_) { + if (i.media_.actor()) + mactors.push_back(i.media_.actor()); + } + + if (not mactors.empty()) { + fan_out_request( + mactors, infinite, json_store::get_json_atom_v, utility::Uuid(), "", true) + .then( + [=](const std::vector> + media_metadata) mutable { + // add media metadata to request. + + // put json into map key media uuid + for (const auto &i : media_metadata) + conform_request.metadata_[i.first.uuid()] = i.second; + + conform_chain(rp, conform_request); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + + } else { + rp.deliver(make_error(sec::runtime_error, "Empty media json help.")); + } + } else { + rp.delegate(caf::actor_cast(this), conform_atom_v, conform_request); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(sec::runtime_error, err.what())); + } +} + +void ConformWorkerActor::conform_step_get_playlist_json( + caf::typed_response_promise rp, + const std::string &conform_task, + ConformRequest conform_request) { + // spdlog::warn("conform_step_get_playlist_json"); + // conform_request.dump(); + + mail(json_store::get_json_atom_v, "") + .request(conform_request.playlist_.actor(), infinite) + .then( + [=](const JsonStore &playlist_metadata) mutable { + // we maybe processing timeline items.. + // conform_request.metadata_[conform_request.playlist_.uuid()] = + // playlist_metadata; + + if (conform_request.item_type_ == "Clip") { + // neet to get clip meta data and clip media if they exist.. + conform_step_get_clip_json(rp, conform_task, conform_request); + } else if (conform_request.item_type_ == "Media") { + conform_step_get_media_json(rp, conform_task, conform_request); + } else { + rp.deliver(make_error(sec::runtime_error, "Unknown item type")); + } + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); +} + +void ConformWorkerActor::conform_step_get_clip_json( + caf::typed_response_promise rp, + const std::string &conform_task, + ConformRequest &conform_request) { + // spdlog::warn("conform_step_get_clip_json"); + // conform_request.dump(); + + // request clip meta data. + auto actors = std::vector(); + for (const auto &i : conform_request.items_) + actors.push_back(i.item_.actor()); + + fan_out_request(actors, infinite, timeline::item_atom_v) + .then( + [=](const std::vector items) mutable { + for (const auto &i : items) + conform_request.metadata_[i.uuid()] = i.prop(); + + conform_step_get_clip_media(rp, conform_task, conform_request); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); +} + +void ConformWorkerActor::conform_step_get_clip_media( + caf::typed_response_promise rp, + const std::string &conform_task, + ConformRequest &conform_request) { + // spdlog::warn("conform_step_get_clip_media"); + // conform_request.dump(); + + // request clip meta data. + auto actors = std::vector(); + for (const auto &i : conform_request.items_) + actors.push_back(i.item_.actor()); + + // get associated media actor for clip.. + fan_out_request(actors, infinite, playlist::get_media_atom_v, true) + .then( + [=](const std::vector item_media) mutable { + auto clip_media_map = std::map(); + + for (const auto &i : item_media) { + if (i.second.actor()) + clip_media_map[i.first] = i.second; + } + + for (auto &i : conform_request.items_) { + auto cmedia = clip_media_map.find(i.item_.uuid()); + if (cmedia != std::end(clip_media_map)) + i.media_ = cmedia->second; + } + + conform_step_get_media_json(rp, conform_task, conform_request); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); +} + + +void ConformWorkerActor::conform_step_get_media_json( + caf::typed_response_promise rp, + const std::string &conform_task, + ConformRequest &conform_request) { + + // spdlog::warn("conform_step_get_media_json"); + // conform_request.dump(); + + auto actors = std::vector(); + for (const auto &i : conform_request.items_) { + auto actor = i.media_.actor(); + if (actor) + actors.push_back(i.media_.actor()); + } + + if (not actors.empty()) { + fan_out_request( + actors, infinite, json_store::get_json_atom_v, utility::Uuid(), "", true) + .then( + [=](const std::vector> media_metadata) mutable { + // add media metadata to request. + + // put json into map key media uuid + for (const auto &i : media_metadata) + conform_request.metadata_[i.first.uuid()] = i.second; + + // also get source names.. + conform_step_get_media_source(rp, conform_task, conform_request); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + } else { + // no media.. + rp.delegate( + caf::actor_cast(this), conform_atom_v, conform_task, conform_request); + } +} + +void ConformWorkerActor::conform_step_get_media_source( + caf::typed_response_promise rp, + const std::string &conform_task, + ConformRequest &conform_request) { + // spdlog::warn("conform_step_get_media_source"); + + // get media actors + auto actors = std::vector(); + for (const auto &i : conform_request.items_) + actors.push_back(i.media_.actor()); + + fan_out_request( + actors, infinite, media::current_media_source_atom_v, true) + .then( + [=](const std::vector>> + media_sources) mutable { + for (const auto &i : media_sources) { + if (conform_request.metadata_.count(i.first.uuid())) { + conform_request.metadata_[i.first.uuid()]["metadata"]["image_source"] = + json::array({i.second.first}); + conform_request.metadata_[i.first.uuid()]["metadata"]["audio_source"] = + json::array({i.second.second}); + } + } + + + // std::map> source_map; + + // for (const auto &i : media_sources) + // source_map[i.first.uuid()] = i.second; + + // for (auto &i : conform_request.items_) { + // auto msource = source_map.find(std::get<1>(i).uuid()); + // if (msource != std::end(source_map)) { + // std::get<1>(i)["metadata"]["image_source"] = msource->second.first; + // std::get<1>(i)["metadata"]["audio_source"] = msource->second.second; + // } + // } + + rp.delegate( + caf::actor_cast(this), + conform_atom_v, + conform_task, + conform_request); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); +} + +void ConformWorkerActor::find_matched( + caf::typed_response_promise rp, + const std::string &key, + const UuidActor &clip, + const UuidActor &timeline) { + + std::pair needle; + std::vector> haystack; + + // very heavy get all clips in timeline and all metadata. + + try { + scoped_actor sys{system()}; + + auto timeline_item = + request_receive(*sys, timeline.actor(), timeline::item_atom_v); + + auto clip_items = timeline_item.find_all_items(timeline::IT_CLIP); + for (const auto &i : clip_items) { + auto metadata = i.get().prop(); + + try { + auto media_meta = request_receive( + *sys, + i.get().actor(), + playlist::get_media_atom_v, + json_store::get_json_atom_v, + utility::Uuid(), + ""); + metadata.update(media_meta); + } catch (...) { + } + + if (i.get().uuid() == clip.uuid()) { + needle.first = clip; + needle.second = metadata; + } else { + haystack.push_back(std::make_pair(i.get().uuid_actor(), metadata)); + } + } + + // need to request media metadata as well.. + + + if (not conformers_.empty()) { + // request.dump(); + fan_out_request( + conformers_, infinite, conform_atom_v, key, needle, haystack) + .then( + [=](const std::vector all_results) mutable { + // compile results.. + auto result = UuidActorVector(); + auto dup = std::set(); + + for (const auto &i : all_results) { + for (const auto &j : i) { + if (not dup.count(j.uuid())) { + result.push_back(j); + dup.insert(j.uuid()); + } + } + } + + rp.deliver(result); + }, + [=](const error &err) mutable { rp.deliver(err); }); + } else { + rp.deliver(UuidActorVector()); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(sec::runtime_error, err.what())); + } } ConformManagerActor::ConformManagerActor(caf::actor_config &cfg, const utility::Uuid uuid) - : caf::event_based_actor(cfg), uuid_(std::move(uuid)) { - size_t worker_count = 5; + : caf::event_based_actor(cfg), uuid_(std::move(uuid)), module::Module("ConformManager") { spdlog::debug("Created ConformManagerActor."); print_on_exit(this, "ConformManagerActor"); @@ -170,58 +1411,200 @@ ConformManagerActor::ConformManagerActor(caf::actor_config &cfg, const utility:: auto prefs = GlobalStoreHelper(system()); JsonStore j; join_broadcast(this, prefs.get_group(j)); - worker_count = preference_value(j, "/core/conform/max_worker_count"); + worker_count_ = preference_value(j, "/core/conform/max_worker_count"); } catch (...) { } - spdlog::debug("ConformManagerActor worker_count {}", worker_count); + spdlog::debug("ConformManagerActor worker_count {}", worker_count_); event_group_ = spawn(this); link_to(event_group_); - auto pool = caf::actor_pool::make( - system().dummy_execution_unit(), - worker_count, +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + + pool_ = caf::actor_pool::make( + system(), + worker_count_, [&] { return system().spawn(); }, caf::actor_pool::round_robin()); - link_to(pool); + +#pragma GCC diagnostic pop + + link_to(pool_); system().registry().put(conform_registry, this); - behavior_.assign( + make_behavior(); + connect_to_ui(); + + data_.set_origin(true); + data_.bind_send_event_func([&](auto &&PH1, auto &&PH2) { + auto event = JsonStore(std::forward(PH1)); + auto undo_redo = std::forward(PH2); + + mail(utility::event_atom_v, json_store::sync_atom_v, data_uuid_, event) + .send(event_group_); + }); + + // my_menu_ = insert_menu_item("media_list_menu_", "Conform", "", 0.0f); + // compare_menu_ = insert_menu_item("media_list_menu_", "Compare", "Conform", 0.0f); + // replace_menu_ = insert_menu_item("media_list_menu_", "Replace", "Conform", 0.0f); + + // next_menu_item_ = insert_menu_item("media_list_menu_", "Next Version", "Conform", 0.0f); + // previous_menu_item_ = + // insert_menu_item("media_list_menu_", "Previous Version", "Conform", 0.0f); + // latest_menu_item_ = insert_menu_item("media_list_menu_", "Latest Version", "Conform", + // 0.0f); + + // trigger request for tasks.. + anon_mail(conform_tasks_atom_v, true) + .delay(std::chrono::seconds(5)) + .send(caf::actor_cast(this), weak_ref); +} + +caf::message_handler ConformManagerActor::message_handler_extensions() { + return caf::message_handler( make_get_event_group_handler(event_group_), [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + [=](utility::event_atom, + json_store::sync_atom, + const Uuid &uuid, + const JsonStore &event) { + if (uuid == data_uuid_) + data_.process_event(event, true, false, false); + }, + + [=](json_store::sync_atom) -> UuidVector { return UuidVector({data_uuid_}); }, + + [=](json_store::sync_atom, const Uuid &uuid) -> JsonStore { + if (uuid == data_uuid_) + return data_.as_json(); + + return JsonStore(); + }, + + // return media / sequence pairs. + // used when conforming media into new sequences + [=](conform_atom, const UuidActorVector &media) { + return mail(conform_atom_v, media).delegate(pool_); + }, + + [=](conform_atom, const std::string &conform_task, const ConformRequest &request) { + return mail(conform_atom_v, conform_task, request).delegate(pool_); + }, + + // manipulate timeline to populate conform track + // and also other initial preparations + [=](conform_atom, const UuidActor &timeline, const bool only_create_conform_track) { + return mail(conform_atom_v, timeline, only_create_conform_track).delegate(pool_); + }, + + // find matching clips in timeline [=](conform_atom, - const std::string &conform_task, - const utility::JsonStore &conform_detail, - const ConformRequest &request) { - delegate(pool, conform_atom_v, conform_task, conform_detail, request); + const std::string &key, + const UuidActor &clip, + const UuidActor &timeline) { + return mail(conform_atom_v, key, clip, timeline).delegate(pool_); }, [=](conform_atom, const std::string &conform_task, - const utility::JsonStore &conform_detail, + const utility::JsonStore &conform_operations, + const UuidActor &playlist, + const UuidActor &container, + const std::string &item_type, + const UuidActorVector &items, + const UuidVector &insert_before) { + return mail( + conform_atom_v, + conform_task, + conform_operations, + playlist, + container, + item_type, + items, + insert_before) + .delegate(pool_); + }, + + [=](conform_atom, + const utility::JsonStore &conform_operations, const UuidActor &playlist, + const UuidActor &timeline, + const UuidActor &conform_track, const UuidActorVector &media) { - delegate(pool, conform_atom_v, conform_task, conform_detail, playlist, media); + return mail( + conform_atom_v, + conform_operations, + playlist, + timeline, + conform_track, + media) + .delegate(pool_); }, - [=](conform_tasks_atom) -> result> { + [=](conform_atom, + const UuidActor &source_playlist, + const UuidActor &source_timeline, + const UuidActorVector &tracks, + const UuidActor &target_playlist, + const UuidActor &target_timeline, + const UuidActor &conform_track) { + return mail( + conform_atom_v, + source_playlist, + source_timeline, + tracks, + target_playlist, + target_timeline, + conform_track) + .delegate(pool_); + }, + + [=](conform_tasks_atom) { + return mail(conform_tasks_atom_v, false) + .delegate(caf::actor_cast(this)); + }, + + [=](conform_tasks_atom, const bool retry) -> result> { auto rp = make_response_promise>(); - request(pool, infinite, conform_tasks_atom_v) + mail(conform_tasks_atom_v) + .request(pool_, infinite) .then( [=](const std::vector &result) mutable { - if (tasks_ != result) { - tasks_ = result; - send( - event_group_, - utility::event_atom_v, - conform_tasks_atom_v, - tasks_); + // compare with model and replace as required. + // simple purge.. + try { + if (data_.at("children").size()) + data_.remove_rows(0, data_.at("children").size(), ""); + + if (not result.empty()) { + auto jsn = R"([])"_json; + for (const auto &i : result) { + auto item = R"({"name":null})"_json; + item["name"] = i; + jsn.push_back(item); + } + + data_.insert_rows(0, jsn.size(), jsn, ""); + } + + mail(utility::event_atom_v, conform_tasks_atom_v, result) + .send(event_group_); + + if (retry and result.empty()) + anon_mail(conform_tasks_atom_v) + .delay(std::chrono::seconds(5)) + .send(caf::actor_cast(this), weak_ref); + + + rp.deliver(result); + } catch (const std::exception &err) { + rp.deliver(make_error(sec::runtime_error, err.what())); } - rp.deliver(tasks_); }, [=](const error &err) mutable { rp.deliver(err); }); @@ -232,28 +1615,31 @@ ConformManagerActor::ConformManagerActor(caf::actor_config &cfg, const utility:: const JsonStore & /*change*/, const std::string & /*path*/, const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full).delegate(actor_cast(this)); }, [=](json_store::update_atom, const JsonStore &j) mutable { try { auto count = preference_value(j, "/core/conform/max_worker_count"); - if (count > worker_count) { - spdlog::debug("conform workers changed old {} new {}", worker_count, count); - while (worker_count < count) { - anon_send( - pool, sys_atom_v, put_atom_v, system().spawn()); - worker_count++; + if (count > worker_count_) { + spdlog::debug( + "conform workers changed old {} new {}", worker_count_, count); + while (worker_count_ < count) { + anon_mail(sys_atom_v, put_atom_v, system().spawn()) + .send(pool_); + worker_count_++; } - } else if (count < worker_count) { - spdlog::debug("conform workers changed old {} new {}", worker_count, count); + } else if (count < worker_count_) { + spdlog::debug( + "conform workers changed old {} new {}", worker_count_, count); // get actors.. - worker_count = count; - request(pool, infinite, sys_atom_v, get_atom_v) + worker_count_ = count; + mail(sys_atom_v, get_atom_v) + .request(pool_, infinite) .await( [=](const std::vector &ws) { - for (auto i = worker_count; i < ws.size(); i++) { - anon_send(pool, sys_atom_v, delete_atom_v, ws[i]); + for (auto i = worker_count_; i < ws.size(); i++) { + anon_mail(sys_atom_v, delete_atom_v, ws[i]).send(pool_); } }, [=](const error &err) { @@ -266,4 +1652,5 @@ ConformManagerActor::ConformManagerActor(caf::actor_config &cfg, const utility:: }); } + void ConformManagerActor::on_exit() { system().registry().erase(conform_registry); } diff --git a/src/conform/src/conformer.cpp b/src/conform/src/conformer.cpp index 19ef82ebd..d2c36650c 100644 --- a/src/conform/src/conformer.cpp +++ b/src/conform/src/conformer.cpp @@ -1,4 +1,3 @@ -// SPDX-License-Identifier: Apache-2.0 #include "xstudio/conform/conformer.hpp" using namespace xstudio; @@ -11,9 +10,30 @@ void Conformer::update_preferences(const utility::JsonStore &prefs) {} std::vector Conformer::conform_tasks() { return std::vector(); } -ConformReply Conformer::conform_request( - const std::string &conform_task, - const utility::JsonStore &conform_detail, - const ConformRequest &request) { - return ConformReply(); +ConformReply +Conformer::conform_request(const std::string &conform_task, const ConformRequest &request) { + return ConformReply(request); +} + +ConformReply Conformer::conform_request(const ConformRequest &request) { + return ConformReply(request); +} + +std::vector>> +Conformer::conform_find_timeline( + const std::vector> &media) { + return std::vector>>( + media.size()); +} + +bool Conformer::conform_prepare_timeline( + const UuidActor &timeline, const bool only_create_conform_track) { + return false; +} + +utility::UuidActorVector Conformer::find_matching( + const std::string &key, + const std::pair &needle, + const std::vector> &haystack) { + return utility::UuidActorVector(); } diff --git a/src/contact_sheet/src/CMakeLists.txt b/src/contact_sheet/src/CMakeLists.txt index a3842a02c..335f29248 100644 --- a/src/contact_sheet/src/CMakeLists.txt +++ b/src/contact_sheet/src/CMakeLists.txt @@ -1,8 +1,8 @@ SET(LINK_DEPS xstudio::media xstudio::utility - xstudio::broadcast - caf::core + xstudio::subset + CAF::core ) -create_component(contact_sheet 0.1.0 "${LINK_DEPS}") +create_component(contact_sheet ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/contact_sheet/src/contact_sheet.cpp b/src/contact_sheet/src/contact_sheet.cpp index 0ddaf095c..8baa78690 100644 --- a/src/contact_sheet/src/contact_sheet.cpp +++ b/src/contact_sheet/src/contact_sheet.cpp @@ -15,7 +15,6 @@ ContactSheet::ContactSheet(const JsonStore &jsn) : Container(static_cast(jsn["container"])), media_list_(static_cast(jsn["media"])) { playhead_rate_ = jsn["playhead_rate"]; - compare_mode_ = jsn["compare_mode"]; } JsonStore ContactSheet::serialise() const { @@ -23,7 +22,6 @@ JsonStore ContactSheet::serialise() const { jsn["container"] = Container::serialise(); jsn["playhead_rate"] = playhead_rate_; - jsn["compare_mode"] = compare_mode_; // identify actors that are media.. jsn["media"] = media_list_.serialise(); diff --git a/src/contact_sheet/src/contact_sheet_actor.cpp b/src/contact_sheet/src/contact_sheet_actor.cpp index 8fb5ffaa9..31baa2afe 100644 --- a/src/contact_sheet/src/contact_sheet_actor.cpp +++ b/src/contact_sheet/src/contact_sheet_actor.cpp @@ -16,94 +16,42 @@ using namespace xstudio; using namespace xstudio::utility; using namespace xstudio::contact_sheet; using namespace xstudio::playlist; +using namespace xstudio::subset; ContactSheetActor::ContactSheetActor( caf::actor_config &cfg, caf::actor playlist, const utility::JsonStore &jsn) - : caf::event_based_actor(cfg), - playlist_(caf::actor_cast(playlist)), - base_(static_cast(jsn["base"])) { + : SubsetActor(cfg, playlist, jsn) { - anon_send(this, playhead::source_atom_v, playlist, UuidUuidMap()); - - // need to scan playlist to relink our media.. init(); } ContactSheetActor::ContactSheetActor( caf::actor_config &cfg, caf::actor playlist, const std::string &name) - : caf::event_based_actor(cfg), - playlist_(caf::actor_cast(playlist)), - base_(name) { + : SubsetActor(cfg, playlist, name, utility::Uuid::generate(), "ContactSheet") { init(); } -void ContactSheetActor::add_media( - caf::actor actor, const utility::Uuid &uuid, const utility::Uuid &before_uuid) { - base_.insert_media(uuid, before_uuid); - actors_[uuid] = actor; - monitor(actor); -} - -bool ContactSheetActor::remove_media(caf::actor actor, const utility::Uuid &uuid) { - bool result = false; - - if (base_.remove_media(uuid)) { - demonitor(actor); - actors_.erase(uuid); - result = true; - } - - return result; -} - - void ContactSheetActor::init() { - print_on_create(this, base_); - print_on_exit(this, base_); - auto event_group_ = spawn(this); - auto change_event_group_ = spawn(this); - link_to(event_group_); - link_to(change_event_group_); + // here we join our own events channel. The 'change_event_group' just emits + // change events when the underlying container (i.e. the SubSet base class) + // changes, i.e. when media is added, removed or re-ordered + mail(get_change_event_group_atom_v) + .request(caf::actor_cast(this), infinite) + .then( + [=](caf::actor grp) { join_broadcast(this, grp); }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); - auto selection_actor_ = spawn( - "ContactSheetPlayheadSelectionActor", caf::actor_cast(this)); - link_to(selection_actor_); - - set_down_handler([=](down_msg &msg) { - // find in playhead list.. - for (auto it = std::begin(actors_); it != std::end(actors_); ++it) { - if (msg.source == it->second) { - spdlog::debug("Remove media {}", to_string(it->first)); - remove_media(it->second, it->first); - send(event_group_, utility::event_atom_v, change_atom_v); - base_.send_changed(event_group_, this); - break; - } - } - }); - - - behavior_.assign( - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - - [=](get_change_event_group_atom) -> caf::actor { return change_event_group_; }, + override_behaviour_ = caf::message_handler{ [=](duplicate_atom) -> result { // clone ourself.. - auto actor = spawn( - caf::actor_cast(playlist_), base_.name()); - anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); + auto actor = + spawn(caf::actor_cast(playlist_), base_.name()); + anon_mail(playhead::playhead_rate_atom_v, base_.playhead_rate()).send(actor); // get uuid from actor.. try { caf::scoped_actor sys(system()); @@ -127,453 +75,62 @@ void ContactSheetActor::init() { return make_error(xstudio_error::error, "Invalid uuid"); }, - [=](media::get_edit_list_atom, const Uuid &uuid) -> result { - std::vector actors; - for (const auto &i : base_.media()) - actors.push_back(actors_[i]); - - if (not actors.empty()) { - auto rp = make_response_promise(); - - fan_out_request( - actors, infinite, media::get_edit_list_atom_v, Uuid()) - .then( - [=](std::vector sections) mutable { - utility::EditList ordered_sections; - for (const auto &i : base_.media()) { - for (const auto &ii : sections) { - const auto &[ud, rt, tc] = ii.section_list()[0]; - if (ud == i) { - if (uuid.is_null()) - ordered_sections.push_back(ii.section_list()[0]); - else - ordered_sections.push_back({uuid, rt, tc}); - break; - } - } - } - rp.deliver(ordered_sections); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - - return rp; - } - - return result(utility::EditList()); - }, - - [=](playhead::playhead_rate_atom) -> FrameRate { return base_.playhead_rate(); }, - - [=](playhead::playhead_rate_atom, const FrameRate &rate) { - base_.set_playhead_rate(rate); - base_.send_changed(event_group_, this); - }, - - [=](playlist::selection_actor_atom) -> caf::actor { return selection_actor_; }, - - [=](playhead::source_atom) -> caf::actor { - return caf::actor_cast(playlist_); - }, - - [=](playhead::source_atom, caf::actor playlist, const UuidUuidMap &swap) -> bool { - for (const auto &i : actors_) - demonitor(i.second); - actors_.clear(); - playlist_ = caf::actor_cast(playlist); - - caf::scoped_actor sys(system()); - try { - auto media = request_receive>( - *sys, playlist, playlist::get_media_atom_v); - - // build map - UuidActorMap amap; - for (const auto &i : media) - amap[i.uuid()] = i.actor(); - - bool clean = false; - while (not clean) { - clean = true; - for (const auto &i : base_.media()) { - auto ii = (swap.count(i) ? swap.at(i) : i); - if (not amap.count(ii)) { - spdlog::error("Failed to find media in playlist {}", to_string(ii)); - base_.remove_media(i); - clean = false; - break; - } - } - } - // link - for (const auto &i : base_.media()) { - auto ii = (swap.count(i) ? swap.at(i) : i); - if (ii != i) { - base_.swap_media(i, ii); - } - actors_[ii] = amap[ii]; - monitor(amap[ii]); - } - } catch (const std::exception &e) { - spdlog::error("Failed to init ContactSheet {}", e.what()); - base_.clear(); - } - base_.send_changed(event_group_, this); - return true; - }, - - [=](playlist::add_media_atom, caf::actor actor, const Uuid &before_uuid) -> bool { - caf::scoped_actor sys(system()); - bool result = false; - try { - // get uuid.. - Uuid uuid = request_receive(*sys, actor, utility::uuid_atom_v); - // check playlist owns it.. - request_receive( - *sys, - caf::actor_cast(playlist_), - playlist::get_media_atom_v, - uuid); - add_media(actor, uuid, before_uuid); - send( - event_group_, - utility::event_atom_v, - playlist::add_media_atom_v, - UuidActor(uuid, actor)); - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return result; - }, - - [=](playlist::add_media_atom, - const Uuid &uuid, - const Uuid &before_uuid) -> result { - // get actor from playlist.. - auto rp = make_response_promise(); - - request( - caf::actor_cast(playlist_), infinite, playlist::get_media_atom_v, uuid) + [=](playlist::create_playhead_atom) -> UuidActor { + if (playhead_) + return playhead_; + auto uuid = utility::Uuid::generate(); + auto actor = spawn( + std::string("Contact Sheet Playhead"), + playhead::GLOBAL_AUDIO, + selection_actor_, + uuid, + caf::actor_cast(this)); + link_to(actor); + + anon_mail(playhead::playhead_rate_atom_v, base_.playhead_rate()).send(actor); + + playhead_ = UuidActor(uuid, actor); + + // now get the playhead to load our media + mail(playlist::get_media_atom_v) + .request(caf::actor_cast(this), infinite) .then( - [=](caf::actor actor) mutable { - add_media(actor, uuid, before_uuid); - send(event_group_, utility::event_atom_v, change_atom_v); - send( - change_event_group_, utility::event_atom_v, utility::change_atom_v); - send( - event_group_, - utility::event_atom_v, - playlist::add_media_atom_v, - UuidActor(uuid, actor)); - base_.send_changed(event_group_, this); - rp.deliver(true); + [=](const UuidActorVector &media) mutable { + mail(playhead::source_atom_v, media) + .request(playhead_.actor(), infinite) + .then( + [=](bool) mutable { + // restore the playhead state, if we have serialisation data + if (!playhead_serialisation_.is_null()) { + anon_mail( + module::deserialise_atom_v, playhead_serialisation_) + .send(playhead_.actor()); + } else { + anon_mail(playhead::compare_mode_atom_v, "Grid") + .send(playhead_.actor()); + } + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); }, - [=](error &err) mutable { + [=](caf::error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(false); }); - return rp; + return playhead_; }, - - [=](playlist::add_media_atom, - UuidVector media_uuids, - const utility::Uuid &uuid_before) -> result { - // need actors.. - auto rp = make_response_promise(); - - request(caf::actor_cast(playlist_), infinite, playlist::get_media_atom_v) + [=](utility::event_atom, utility::change_atom) { + if (!playhead_) + return; + mail(playlist::get_media_atom_v) + .request(caf::actor_cast(this), infinite) .then( - [=](const std::vector &actors) mutable { - bool changed = false; - for (const auto &i : media_uuids) { - for (const auto &ii : actors) { - if (ii.uuid() == i) { - changed = true; - add_media(ii.actor(), i, uuid_before); - send( - event_group_, - utility::event_atom_v, - playlist::add_media_atom_v, - UuidActor(i, ii.actor())); - break; - } - } - } - - if (changed) { - send(event_group_, utility::event_atom_v, change_atom_v); - send( - change_event_group_, - utility::event_atom_v, - utility::change_atom_v); - base_.send_changed(event_group_, this); - } - rp.deliver(changed); + [=](const UuidActorVector &media) mutable { + anon_mail(playhead::source_atom_v, media).send(playhead_.actor()); }, - [=](error &err) mutable { + [=](caf::error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(false); }); - - return rp; - }, - - - [=](playlist::add_media_atom, - std::vector media_actors, - const utility::Uuid &uuid_before) -> result { - for (const auto &i : media_actors) { - add_media(i.actor(), i.uuid(), uuid_before); - send(event_group_, utility::event_atom_v, playlist::add_media_atom_v, i); - } - send(event_group_, utility::event_atom_v, change_atom_v); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); - base_.send_changed(event_group_, this); - return true; - }, - - [=](playlist::create_playhead_atom) -> result { - if (playhead_) - return playhead_; - - auto rp = make_response_promise(); - auto uuid = utility::Uuid::generate(); - - auto playhead = spawn( - std::string("ContactSheet Playhead"), selection_actor_, uuid); - link_to(playhead); - - // anon_send(actor, playhead::source_atom_v, tactor); - anon_send(playhead, playhead::playhead_rate_atom_v, base_.playhead_rate()); - - playhead_ = UuidActor(uuid, playhead); - return playhead_; - }, - - [=](playlist::get_playhead_atom) { - delegate(caf::actor_cast(this), playlist::create_playhead_atom_v); - }, - - [=](playlist::get_media_atom) -> std::vector { - std::vector result; - - for (const auto &i : base_.media()) - result.emplace_back(UuidActor(i, actors_[i])); - - return result; - }, - - [=](playlist::get_media_atom, const Uuid &uuid) -> result { - if (base_.contains_media(uuid)) - return actors_[uuid]; - return make_error(xstudio_error::error, "Invalid uuid"); - }, - - [=](playlist::get_media_atom, const bool) -> result> { - if (not actors_.empty()) { - auto rp = make_response_promise>(); - // collect media data.. - std::vector actors; - for (const auto &i : actors_) - actors.push_back(i.second); - - fan_out_request(actors, infinite, utility::detail_atom_v) - .then( - [=](const std::vector details) mutable { - std::vector reordered_details; - - for (const auto &i : base_.media()) { - for (const auto &ii : details) { - if (ii.uuid_ == i) { - reordered_details.push_back(ii); - break; - } - } - } - - rp.deliver(reordered_details); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - - return rp; - } - std::vector result; - return result; - }, - - [=](playlist::get_media_uuid_atom) -> UuidVector { return base_.media_vector(); }, - - [=](playlist::move_media_atom atom, const Uuid &uuid, const Uuid &uuid_before) { - delegate( - actor_cast(this), atom, utility::UuidVector({uuid}), uuid_before); - }, - - [=](playlist::move_media_atom atom, - const UuidList &media_uuids, - const Uuid &uuid_before) { - delegate( - actor_cast(this), - atom, - utility::UuidVector(media_uuids.begin(), media_uuids.end()), - uuid_before); - }, - - [=](playlist::move_media_atom, - const UuidVector &media_uuids, - const Uuid &uuid_before) -> bool { - bool result = false; - for (auto uuid : media_uuids) { - result |= base_.move_media(uuid, uuid_before); - } - if (result) { - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); - } - return result; - }, - - [=](playlist::remove_media_atom atom, const utility::UuidList &uuids) { - delegate( - actor_cast(this), - atom, - utility::UuidVector(uuids.begin(), uuids.end())); - }, - - [=](playlist::remove_media_atom atom, const Uuid &uuid) { - delegate(actor_cast(this), atom, utility::UuidVector({uuid})); - }, - - [=](playlist::remove_media_atom, const utility::UuidVector &uuids) -> bool { - // this needs to propergate to children somehow.. - bool changed = false; - for (const auto &uuid : uuids) { - if (actors_.count(uuid) and remove_media(actors_[uuid], uuid)) { - spdlog::warn("remove {}", to_string(uuid)); - changed = true; - } - } - if (changed) { - send(event_group_, utility::event_atom_v, change_atom_v); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); - base_.send_changed(event_group_, this); - } - return changed; - }, - - [=](playlist::sort_alphabetically_atom) { sort_alphabetically(); }, - - [=](get_next_media_atom, - const utility::Uuid &after_this_uuid, - int skip_by) -> result { - const utility::UuidList media = base_.media(); - if (skip_by > 0) { - auto i = std::find(media.begin(), media.end(), after_this_uuid); - if (i == media.end()) { - // not found! - return make_error( - xstudio_error::error, - "playlist::get_next_media_atom called with uuid that is not in " - "subset"); - } - while (skip_by--) { - i++; - if (i == media.end()) { - i--; - break; - } - } - if (actors_.count(*i)) - return UuidActor(*i, actors_[*i]); - - } else { - auto i = std::find(media.rbegin(), media.rend(), after_this_uuid); - if (i == media.rend()) { - // not found! - return make_error( - xstudio_error::error, - "playlist::get_next_media_atom called with uuid that is not in " - "playlist"); - } - while (skip_by++) { - i++; - if (i == media.rend()) { - i--; - break; - } - } - - if (actors_.count(*i)) - return UuidActor(*i, actors_[*i]); - } - - return make_error( - xstudio_error::error, - "playlist::get_next_media_atom called with uuid for which no media actor " - "exists"); - }, - - [=](session::session_atom) { - delegate(caf::actor_cast(playlist_), session::session_atom_v); - }, - - [=](utility::serialise_atom) -> JsonStore { - JsonStore jsn; - jsn["base"] = base_.serialise(); - return jsn; - }); -} - -void ContactSheetActor::sort_alphabetically() { - - using SourceAndUuid = std::pair; - auto media_names_vs_uuids = - std::make_shared>>(); - - for (const auto &i : base_.media()) { - - // Pro tip: because i is a reference, it's the reference that is captured in our lambda - // below and therefore it is 'unstable' so we make a copy here and use that in the - // lambda as this is object-copied in the capture instead. - UuidActor media_actor(i, actors_[i]); - - request(media_actor.actor(), infinite, media::media_reference_atom_v, utility::Uuid()) - .await( - - [=](const std::pair &m_ref) mutable { - std::string path = uri_to_posix_path(m_ref.second.uri()); - path = std::string(path, path.rfind("/") + 1); - path = to_lower(path); - - (*media_names_vs_uuids).push_back(std::make_pair(path, media_actor.uuid())); - - if (media_names_vs_uuids->size() == base_.media().size()) { - - std::sort( - media_names_vs_uuids->begin(), - media_names_vs_uuids->end(), - [](const SourceAndUuid &a, const SourceAndUuid &b) -> bool { - return a.first < b.first; - }); - - utility::UuidList ordered_uuids; - for (const auto &p : (*media_names_vs_uuids)) { - ordered_uuids.push_back(p.second); - } - - anon_send( - caf::actor_cast(this), - move_media_atom_v, - ordered_uuids, - utility::Uuid()); - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - } -} + }}; +} \ No newline at end of file diff --git a/src/contact_sheet/test/contact_sheet_actor_test.cpp b/src/contact_sheet/test/contact_sheet_actor_test.cpp index 77b56eee6..8628a55a7 100644 --- a/src/contact_sheet/test/contact_sheet_actor_test.cpp +++ b/src/contact_sheet/test/contact_sheet_actor_test.cpp @@ -25,9 +25,7 @@ ACTOR_TEST_SETUP() TEST(ContactSheetActorTest, Test) { // fixture f; // auto tmp = f.self->spawn("Test"); - // f.self->anon_send( - // tmp, - // add_media_atom_v, + // f.self->anon_mail(// add_media_atom_v, // f.self->spawn( // "test", // Uuid(), @@ -35,16 +33,14 @@ TEST(ContactSheetActorTest, Test) { // "test", // posix_path_to_uri(TEST_RESOURCE "/media/test.{:04d}.ppm"), // FrameList(1, 10))), - // Uuid()); - // f.self->anon_send( - // tmp, - // add_media_atom_v, + // Uuid()).send(// tmp); + // f.self->anon_mail(// add_media_atom_v, // f.self->spawn( // "test", // Uuid(), // f.self->spawn( // "test", posix_path_to_uri(TEST_RESOURCE "/media/test.mov"))), - // Uuid()); + // Uuid()).send(// tmp); // f.self->request(tmp, std::chrono::seconds(10), name_atom_v).receive( // [&](const std::string &name) { diff --git a/src/data_source/src/CMakeLists.txt b/src/data_source/src/CMakeLists.txt index 2b4f2a6a7..f5afa61be 100644 --- a/src/data_source/src/CMakeLists.txt +++ b/src/data_source/src/CMakeLists.txt @@ -1,6 +1,6 @@ SET(LINK_DEPS xstudio::global_store - caf::core + CAF::core ) -create_component(data_source 0.1.0 "${LINK_DEPS}") +create_component(data_source ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/data_source/test/CMakeLists.txt b/src/data_source/test/CMakeLists.txt index 3153c607d..77ddf6a0f 100644 --- a/src/data_source/test/CMakeLists.txt +++ b/src/data_source/test/CMakeLists.txt @@ -2,7 +2,7 @@ include(CTest) SET(LINK_DEPS xstudio::data_source - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/demos/colour_op_plugins/source_grading_demo/src/CMakeLists.txt b/src/demos/colour_op_plugins/source_grading_demo/src/CMakeLists.txt index 4fa99e516..90466da1e 100644 --- a/src/demos/colour_op_plugins/source_grading_demo/src/CMakeLists.txt +++ b/src/demos/colour_op_plugins/source_grading_demo/src/CMakeLists.txt @@ -1,4 +1,4 @@ -project(grading_demo VERSION 0.1.0 LANGUAGES CXX) +project(grading_demo VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) find_package(Imath) @@ -23,4 +23,4 @@ target_link_libraries(${PROJECT_NAME} set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) -add_subdirectory(qml) \ No newline at end of file +add_plugin_qml(${PROJECT_NAME} qml) \ No newline at end of file diff --git a/src/demos/colour_op_plugins/source_grading_demo/src/grading_demo.cpp b/src/demos/colour_op_plugins/source_grading_demo/src/grading_demo.cpp index 66643618e..74ffebe41 100644 --- a/src/demos/colour_op_plugins/source_grading_demo/src/grading_demo.cpp +++ b/src/demos/colour_op_plugins/source_grading_demo/src/grading_demo.cpp @@ -11,7 +11,7 @@ namespace { const static utility::Uuid PLUGIN_UUID{"5598e01e-c6bc-4cf9-80ff-74bb560df12a"}; const char *glsl_shader_code = R"( -#version 430 core +#version 410 core uniform vec3 rgb_factor; vec4 colour_transform_op(vec4 rgba) { diff --git a/src/demos/colour_op_plugins/source_grading_demo/src/qml/CMakeLists.txt b/src/demos/colour_op_plugins/source_grading_demo/src/qml/CMakeLists.txt deleted file mode 100644 index da84ef77e..000000000 --- a/src/demos/colour_op_plugins/source_grading_demo/src/qml/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -project(grading_demo VERSION 0.1.0 LANGUAGES CXX) - -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/GradingDemo.1/ DESTINATION share/xstudio/plugin/qml/GradingDemo.1) - -add_custom_target(COPY_GRADE_DEMO_QML ALL) - -add_custom_command(TARGET COPY_GRADE_DEMO_QML POST_BUILD - COMMAND ${CMAKE_COMMAND} -E - copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/GradingDemo.1 ${CMAKE_BINARY_DIR}/bin/plugin/qml/GradingDemo.1) - diff --git a/src/demos/colour_op_plugins/source_grading_demo/src/qml/GradingDemo.1/GradingDemoButton.qml b/src/demos/colour_op_plugins/source_grading_demo/src/qml/GradingDemo.1/GradingDemoButton.qml index 292b3a0ad..5e1f78552 100644 --- a/src/demos/colour_op_plugins/source_grading_demo/src/qml/GradingDemo.1/GradingDemoButton.qml +++ b/src/demos/colour_op_plugins/source_grading_demo/src/qml/GradingDemo.1/GradingDemoButton.qml @@ -1,11 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.12 +import QtQuick +import QtQuick.Effects import xStudio 1.0 -import xstudio.qml.module 1.0 XsTrayButton { // prototype: true @@ -35,13 +33,15 @@ XsTrayButton { verticalAlignment: Qt.AlignVCenter } - DropShadow { + MultiEffect { anchors.fill: txt source: txt - verticalOffset: 2 - color: "black" - radius: 1 - samples: 3 + shadowEnabled: true + shadowColor: "black" + shadowVerticalOffset: 2 + // shadowHorizontalOffset: 2 + shadowScale: 1.3 + shadowBlur: 0.2 } property var gradingDemoDialog diff --git a/src/demos/colour_op_plugins/source_grading_demo/src/qml/GradingDemo.1/GradingDemoDialog/GradingDemoDialog.qml b/src/demos/colour_op_plugins/source_grading_demo/src/qml/GradingDemo.1/GradingDemoDialog/GradingDemoDialog.qml index 77cbd2e1a..6970151f1 100644 --- a/src/demos/colour_op_plugins/source_grading_demo/src/qml/GradingDemo.1/GradingDemoDialog/GradingDemoDialog.qml +++ b/src/demos/colour_op_plugins/source_grading_demo/src/qml/GradingDemo.1/GradingDemoDialog/GradingDemoDialog.qml @@ -1,16 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 +import QtQuick + +import QtQuick.Layouts + import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient -import xstudio.qml.module 1.0 + + import xStudio 1.0 XsWindow { @@ -26,13 +23,6 @@ XsWindow { minimumHeight: minimumWidth maximumHeight: minimumHeight - onVisibleChanged: { - if (!visible) { - // ensure keyboard events are returned to the viewport - sessionWidget.playerWidget.viewport.forceActiveFocus() - } - } - XsModuleAttributesModel { id: grading_demo_controls attributesGroupNames: "grading_demo_controls" diff --git a/src/demos/colour_op_plugins/source_grading_demo/test/CMakeLists.txt b/src/demos/colour_op_plugins/source_grading_demo/test/CMakeLists.txt index a73825679..66caa1970 100644 --- a/src/demos/colour_op_plugins/source_grading_demo/test/CMakeLists.txt +++ b/src/demos/colour_op_plugins/source_grading_demo/test/CMakeLists.txt @@ -1,7 +1,7 @@ include(CTest) SET(LINK_DEPS - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/demos/colour_op_plugins/viewer_solarise_effect/src/CMakeLists.txt b/src/demos/colour_op_plugins/viewer_solarise_effect/src/CMakeLists.txt index 7f64f46de..aa1c8417b 100644 --- a/src/demos/colour_op_plugins/viewer_solarise_effect/src/CMakeLists.txt +++ b/src/demos/colour_op_plugins/viewer_solarise_effect/src/CMakeLists.txt @@ -6,4 +6,4 @@ SET(LINK_DEPS Imath::Imath ) -create_plugin_with_alias(viewer_solarise_effect xstudio::colour_pipeline::viewer_solarise_effect 0.1.0 "${LINK_DEPS}") +create_plugin_with_alias(viewer_solarise_effect xstudio::colour_pipeline::viewer_solarise_effect ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/demos/colour_op_plugins/viewer_solarise_effect/src/viewer_solarise_effect.cpp b/src/demos/colour_op_plugins/viewer_solarise_effect/src/viewer_solarise_effect.cpp index b35e66609..33d8b809f 100644 --- a/src/demos/colour_op_plugins/viewer_solarise_effect/src/viewer_solarise_effect.cpp +++ b/src/demos/colour_op_plugins/viewer_solarise_effect/src/viewer_solarise_effect.cpp @@ -11,7 +11,7 @@ namespace { const static utility::Uuid PLUGIN_UUID{"300bd462-d30c-4d24-a854-439c9ae94495"}; const char *glsl_shader_code = R"( -#version 430 core +#version 410 core uniform float solarise; vec4 colour_transform_op(vec4 rgba) { @@ -51,7 +51,7 @@ SolariseOp::SolariseOp(caf::actor_config &cfg, const utility::JsonStore &init_se gamma_ = add_float_attribute("Solarise", "Solarise", 0.0f, 0.0f, 6.0f, 0.005f); gamma_->set_redraw_viewport_on_change(true); gamma_->set_role_data(module::Attribute::ToolbarPosition, 4.5f); - gamma_->set_role_data(module::Attribute::Groups, nlohmann::json{"any_toolbar"}); + gamma_->set_role_data(module::Attribute::UIDataModels, nlohmann::json{"any_toolbar"}); gamma_->set_role_data(module::Attribute::Activated, false); gamma_->set_role_data(module::Attribute::DefaultValue, 1.0f); gamma_->set_role_data(module::Attribute::ToolTip, "Set the viewport gamma"); diff --git a/src/demos/colour_op_plugins/viewer_solarise_effect/test/CMakeLists.txt b/src/demos/colour_op_plugins/viewer_solarise_effect/test/CMakeLists.txt index a73825679..66caa1970 100644 --- a/src/demos/colour_op_plugins/viewer_solarise_effect/test/CMakeLists.txt +++ b/src/demos/colour_op_plugins/viewer_solarise_effect/test/CMakeLists.txt @@ -1,7 +1,7 @@ include(CTest) SET(LINK_DEPS - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/demos/glx_minimal_demo/src/CMakeLists.txt b/src/demos/glx_minimal_demo/src/CMakeLists.txt index 92069b1dd..c496def1c 100644 --- a/src/demos/glx_minimal_demo/src/CMakeLists.txt +++ b/src/demos/glx_minimal_demo/src/CMakeLists.txt @@ -25,7 +25,7 @@ target_link_libraries(${PROJECT_NAME} xstudio::ui::opengl::viewport xstudio::utility PUBLIC - caf::core + CAF::core $<$:GLdispatch> OpenSSL::SSL ZLIB::ZLIB diff --git a/src/demos/glx_minimal_demo/src/main.cpp b/src/demos/glx_minimal_demo/src/main.cpp index dd849cb61..d7fd6520f 100644 --- a/src/demos/glx_minimal_demo/src/main.cpp +++ b/src/demos/glx_minimal_demo/src/main.cpp @@ -21,9 +21,7 @@ #include #include #include -#ifdef __linux__ #include -#endif #include #include #include @@ -168,7 +166,8 @@ GLXWindowViewportActor::GLXWindowViewportActor(caf::actor_config &cfg) jsn, caf::actor_cast(this), true, - ui::viewport::ViewportRendererPtr(new opengl::OpenGLViewportRenderer(true, false))); + ui::viewport::ViewportRendererPtr(new opengl::OpenGLViewportRenderer(true, false)), + "GLXViewport"); /* Provide a callback so the xstudio OpenGLViewportRenderer can tell this class when some property of the viewport has changed, or a redraw is needed, so the window @@ -210,15 +209,14 @@ void GLXWindowViewportActor::execute() { void GLXWindowViewportActor::resizeGL(int w, int h) { width = w; height = h; - anon_send( - caf::actor_cast(this), + anon_mail( ui::viewport::viewport_set_scene_coordinates_atom_v, Imath::V2f(0, 0), Imath::V2f(w, 0), Imath::V2f(w, h), Imath::V2f(0, h), - Imath::V2i(w, h), - 1.0f); + Imath::V2i(w, h)) + .send(caf::actor_cast(this)); } int main(int argc, char *argv[]) { @@ -304,6 +302,7 @@ int main(int argc, char *argv[]) { viewport_actor, std::chrono::milliseconds(2000), viewport::viewport_playhead_atom_v, + "viewport0", playhead); // start playback @@ -547,7 +546,7 @@ void GLXWindowViewportActor::event_loop_func() { XNextEvent(display, &ev); // see viewport.cpp ... this message will cause a redraw - anon_send(self, show_buffer_atom_v, true); + anon_mail(show_buffer_atom_v, true).send(self); // render(); switch (ev.type) { @@ -578,4 +577,4 @@ void GLXWindowViewportActor::close() { XDestroyWindow(display, win); XFreeColormap(display, cmap); XCloseDisplay(display); -} +} \ No newline at end of file diff --git a/src/demos/minimal_demo/src/CMakeLists.txt b/src/demos/minimal_demo/src/CMakeLists.txt index e862e39d3..64f6159c1 100644 --- a/src/demos/minimal_demo/src/CMakeLists.txt +++ b/src/demos/minimal_demo/src/CMakeLists.txt @@ -6,7 +6,7 @@ set(SOURCES find_package(OpenGL REQUIRED) find_package(GLEW REQUIRED) -find_package(Qt5 COMPONENTS Core Gui Widgets OpenGL REQUIRED) +find_package(Qt6 COMPONENTS Core Gui Widgets OpenGL REQUIRED) find_package(OpenSSL) find_package(ZLIB) @@ -28,10 +28,10 @@ target_link_libraries(${PROJECT_NAME} xstudio::ui::qt::viewport_widget xstudio::utility PUBLIC - caf::core + CAF::core $<$:GLdispatch> - Qt5::Gui - Qt5::Widgets + Qt6::Gui + Qt6::Widgets OpenSSL::SSL ZLIB::ZLIB ) diff --git a/src/demos/minimal_demo/src/minimal_player.cpp b/src/demos/minimal_demo/src/minimal_player.cpp index ea0e09f86..eb82770c9 100644 --- a/src/demos/minimal_demo/src/minimal_player.cpp +++ b/src/demos/minimal_demo/src/minimal_player.cpp @@ -108,7 +108,7 @@ int main(int argc, char **argv) { app.exec(); // cancel actors talking to them selves. - system.clock().cancel_all(); + // system.clock().cancel_all(); self->send_exit(global_actor, caf::exit_reason::user_shutdown); diff --git a/src/demos/py_remote_ui_demo/src/xStudioPythonRemoteUIDemo.py b/src/demos/py_remote_ui_demo/src/xStudioPythonRemoteUIDemo.py old mode 100755 new mode 100644 index 43992031b..412f26818 --- a/src/demos/py_remote_ui_demo/src/xStudioPythonRemoteUIDemo.py +++ b/src/demos/py_remote_ui_demo/src/xStudioPythonRemoteUIDemo.py @@ -2,21 +2,21 @@ # SPDX-License-Identifier: Apache-2.0 """Currently, a *very* rough demo showing remote control of xstudio with two -way communication to a PySide2 interface""" - -from PySide2.QtWidgets import QApplication -from PySide2.QtWidgets import QMainWindow -from PySide2.QtWidgets import QVBoxLayout, QHBoxLayout, QGridLayout -from PySide2.QtWidgets import QPushButton -from PySide2.QtWidgets import QWidget -from PySide2.QtWidgets import QCheckBox -from PySide2.QtWidgets import QSlider -from PySide2.QtWidgets import QLineEdit -from PySide2.QtQuickWidgets import QQuickWidget -from PySide2.QtWidgets import QFileDialog -from PySide2.QtCore import QUrl -from PySide2.QtCore import QTimer -from PySide2.QtCore import Qt +way communication to a PySide6 interface""" + +from PySide6.QtWidgets import QApplication +from PySide6.QtWidgets import QMainWindow +from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QGridLayout +from PySide6.QtWidgets import QPushButton +from PySide6.QtWidgets import QWidget +from PySide6.QtWidgets import QCheckBox +from PySide6.QtWidgets import QSlider +from PySide6.QtWidgets import QLineEdit +from PySide6.QtQuickWidgets import QQuickWidget +from PySide6.QtWidgets import QFileDialog +from PySide6.QtCore import QUrl +from PySide6.QtCore import QTimer +from PySide6.QtCore import Qt import sys import time import subprocess diff --git a/src/demos/pyside_demo/src/xStudioPySideDemo.py b/src/demos/pyside_demo/src/xStudioPySideDemo.py old mode 100755 new mode 100644 index 1accf99d5..bac9119b5 --- a/src/demos/pyside_demo/src/xStudioPySideDemo.py +++ b/src/demos/pyside_demo/src/xStudioPySideDemo.py @@ -1,41 +1,38 @@ #! /usr/bin/env python # SPDX-License-Identifier: Apache-2.0 -from PySide2.QtWidgets import QApplication -from PySide2.QtWidgets import QMainWindow -from PySide2.QtWidgets import QVBoxLayout, QHBoxLayout, QGridLayout -from PySide2.QtWidgets import QPushButton -from PySide2.QtWidgets import QWidget -from PySide2.QtWidgets import QCheckBox -from PySide2.QtWidgets import QSlider -from PySide2.QtWidgets import QLineEdit -from PySide2.QtQuickWidgets import QQuickWidget -from PySide2.QtWidgets import QFileDialog -from PySide2.QtCore import QUrl -from PySide2.QtCore import QTimer -from PySide2.QtCore import Qt -from xstudio.gui.__pyside2_xstudio import PlainViewport +from PySide6.QtWidgets import QApplication +from PySide6.QtWidgets import QMainWindow +from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout +from PySide6.QtWidgets import QPushButton +from PySide6.QtWidgets import QWidget +from PySide6.QtWidgets import QCheckBox +from PySide6.QtWidgets import QSlider +from PySide6.QtWidgets import QLineEdit +from PySide6.QtWidgets import QFileDialog +from PySide6.QtCore import QTimer +from PySide6.QtCore import Qt +from xstudio.gui import PySideQmlViewport, XstudioPyApp, PlainViewport import sys import time -import subprocess from xstudio.core import event_atom, add_media_atom, position_atom, duration_frames_atom from xstudio.connection import Connection -from xstudio.core import CompareMode -from xstudio.core import CompareMode from xstudio.api.session.media import Media +from xstudio.api.app import Viewport XSTUDIO = None class PlayheadUI(QWidget): - def __init__(self, parent): + def __init__(self, parent, _playhead): super(PlayheadUI, self).__init__(parent) self.layout = QHBoxLayout(self) self.frame = QLineEdit(self) self.layout.addWidget(self.frame) + self.playhead = _playhead self.slider = QSlider(Qt.Horizontal, self) self.slider.valueChanged.connect(self.position_changed) @@ -46,6 +43,7 @@ def __init__(self, parent): def set_duration(self, duration_frames): self.num_frames.setText(str(duration_frames)) + self.slider.setMinimum(0) self.slider.setMaximum(duration_frames) def set_position(self, position): @@ -55,22 +53,17 @@ def set_position(self, position): def position_changed(self, new_position): - if self.backend_position != new_position: - - playlist = XSTUDIO.api.session.playlists[0] - - # Ensure we have a playhead - if not playlist or not playlist.playheads: - return + if self.backend_position != new_position and self.slider.isSliderDown(): + self.playhead.position = new_position - playlist.playheads[0].position = new_position class PlaylistUI(QWidget): - def __init__(self, parent): + def __init__(self, parent, _playlist): super(PlaylistUI, self).__init__(parent) + self.playlist = _playlist self.layout = QVBoxLayout(self) self.playlistItems = [] @@ -91,13 +84,7 @@ def onCheckboxClicked(self, state): if checkbox.checkState() == Qt.Checked: selected_items.append(media_item.uuid) - playlist = XSTUDIO.api.session.playlists[0] - - # Ensure we have a playhead - if not playlist.playheads: - return - - playlist.playheads[0].source.set_selection(selected_items) + self.playlist.playhead_selection.set_selection(selected_items) class XsPysideDemo(QWidget): @@ -105,18 +92,29 @@ def __init__(self, parent): super(XsPysideDemo, self).__init__(parent) + # The xSTUDIO viewport needs a unique ID for the window that it is + # embedded in. This assists the handling of keystrokes, so we know whether + # the user hit the 'play' hotkey, for example, in this window or another + # window + window_id = str(parent) + + self.viewport = PlainViewport( + self, + window_id + ) + + self.playlist = None + self.playhead = None self.playhead_event_handler = None self.playlist_event_handler = None - self.event_loop_timer = QTimer(self) - self.event_loop_timer.timeout.connect(self.process_playhead_events) + self.makePlaylist() layout = QVBoxLayout(self) - self.viewport = PlainViewport(self) layout.addWidget(self.viewport, 1) - self.playlist_ui = PlaylistUI(self) + self.playlist_ui = PlaylistUI(self, self.playlist) layout.addWidget(self.playlist_ui) buttonLayout = QHBoxLayout() @@ -138,24 +136,19 @@ def __init__(self, parent): layout.addLayout(buttonLayout) - self.playhead_ui = PlayheadUI(self) + self.playhead_ui = PlayheadUI(self, self.playhead) layout.addWidget(self.playhead_ui) def onLoad(self): self.setPlaying(False) - # Something we're working on ... processing events coming from xstudio - # can kill the Qt event loop. - self.event_loop_timer.stop() - fname = QFileDialog.getOpenFileName( self, "Get media","","MOV Files (*.mov)" ) if fname != "": self.load_media(fname[0]) - self.event_loop_timer.start(200) def onSleep(self): @@ -163,12 +156,7 @@ def onSleep(self): def setPlaying(self, playing): - self.open_connection() - - if XSTUDIO.api.session.playlists: - playlist = XSTUDIO.api.session.playlists[0] - if playlist.playheads: - playlist.playheads[0].playing = playing + self.playhead.playing = playing def onStartPlaying(self, playing): @@ -178,76 +166,75 @@ def onStopPlaying(self): self.setPlaying(False) - def makePlayhead(self): - - playlist = XSTUDIO.api.session.playlists[0] + def makePlaylist(self): - # Ensure we have a playhead - if not playlist.playheads: - playlist.create_playhead() - - # Enter the 'string' compare mode and select all media so that loaded - # media is strung together - playlist.playheads[0].compare_mode = "String" + if not XSTUDIO.api.session.playlists: + XSTUDIO.api.session.create_playlist() + self.playlist = XSTUDIO.api.session.playlists[0] + self.playhead = self.playlist.playhead + + # here we put the playhead into 'String' compare mode + self.playhead.get_attribute("Compare").set_value("String") + + # this call is crucial. in xSTUDIO every playlist, subset, contact + # sheet and timeline has its own 'playhead' that controls playback + # and delivers video and audio. + # The xSTUDIO viewports have to be 'attached' to a playhead to + # recieve the video frames and display them. By default viewports + # will automaticall attach to the global active playhead, but this + # must be set via the API. Thus, the UI can decide which playlist + # or timeline etc. is being viewed/played. + # (QuickView windows are an exception, where they ignore updates + # to the global active playhead but use their own internal + # playhead.) + XSTUDIO.api.app.set_global_playhead( + self.playhead + ) + + if not self.playlist_event_handler: + self.playlist_event_handler = self.playlist.add_event_callback( + self.playlist_events + ) if not self.playhead_event_handler: - self.playhead_event_handler = playlist.playheads[0].add_handler( + self.playhead_event_handler = self.playhead.add_event_callback( self.playhead_events ) def load_media(self, filename): - self.open_connection() - - # Ensure we have a playlist - if not XSTUDIO.api.session.playlists: - XSTUDIO.api.session.create_playlist() - playlist = XSTUDIO.api.session.playlists[0] - - playlist = XSTUDIO.api.session.playlists[0] - - if not self.playlist_event_handler: - self.playlist_event_handler = playlist.add_handler( - self.playlist_events - ) - # Load the media item - media = playlist.add_media(filename) - self.makePlayhead() + self.playlist.add_media(filename) + + def playhead_events(self, event): - def playhead_events(self, sender, req_id, event): - if type(event[0]) == position_atom and type(event[1]) == int: - self.playhead_ui.set_position(event[1]) - elif (type(event[0]) == event_atom and + if len(event) >= 3 and type(event[1]) == position_atom and type(event[2]) == int: + self.playhead_ui.set_position(event[2]) + elif len(event) >= 3 and (type(event[0]) == event_atom and type(event[1]) == duration_frames_atom and type(event[2])) == int: self.playhead_ui.set_duration(event[2]) - def playlist_events(self, sender, req_id, event): + def playlist_events(self, event): if type(event[0]) == event_atom and type(event[1]) == add_media_atom: - # 3rd eement of event is a UuidActor of the new media item - self.playlist_ui.media_added(event[2].actor) + # 3rd eement of event is a UuidActorVector of the new media items + for media in event[2]: + self.playlist_ui.media_added(media.actor) +if __name__=="__main__": - def process_playhead_events(self): - print (".") - # use a short 2 millisecond timeout so that we don't hang the UI - XSTUDIO.process_events() - - def open_connection(self): + app = QApplication(sys.argv) - global XSTUDIO - if not XSTUDIO: - XSTUDIO = Connection(auto_connect=True) - self.event_loop_timer.start(200) + # this object is required to manage xstudio's core components + xstudio_py_app = XstudioPyApp() -if __name__=="__main__": + # here we connect to the xstudio backend. XSTUDIO serves as the API entry + # point + XSTUDIO = Connection(auto_connect=True) - app = QApplication(sys.argv) appWindow = QMainWindow() - centralWidget = XsPysideDemo(appWindow) appWindow.setCentralWidget(centralWidget) appWindow.resize(960, 540) diff --git a/src/demos/python_plugins/dnMask/__init__.py b/src/demos/python_plugins/dnMask/__init__.py deleted file mode 100644 index ff8cab651..000000000 --- a/src/demos/python_plugins/dnMask/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -from .mask_plugin import create_plugin_instance \ No newline at end of file diff --git a/src/demos/python_plugins/dnMask/mask_plugin.py b/src/demos/python_plugins/dnMask/mask_plugin.py deleted file mode 100755 index 7bd2ec2e1..000000000 --- a/src/demos/python_plugins/dnMask/mask_plugin.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/bin/env python -# SPDX-License-Identifier: Apache-2.0 - -from xstudio.connection import Connection -from xstudio.plugin import PluginBase -from xstudio.core import JsonStore, change_attribute_event_atom -import sys -import time, threading - -qml_folder_name = "qml/DnMask.1" - -button_qml = """ -DnMaskButton { - id: control - anchors.fill: parent -} -""" - -overlay_qml = """ -DnMaskOverlay { -} -""" - -class MaskPlugin(PluginBase): - - def __init__(self, connection): - - PluginBase.__init__( - self, - connection, - "MaskPlugin", - qml_folder=qml_folder_name) - - # Note we pass a json-like dictionary as the third argument when - # using 'add_attribute'. - # This describes the attribute data per 'role'. The role name is the key, - # and the role data is the corresponding value. The role name must be - # from the list of valid role names - these can be seen in attribute.hpp - # file in the xstudio source code. The role data can be string, float, - # int, bool, list(str) - no restrictions are placed on the type of the - # role data but for UI elements to work correctly you need to use the - # type that xSTUDIO expects. So the 'combo_box_options' role data should - # be a list(str). - self.mask = self.add_attribute( - "dnMask", #attr name - "Off", #attr intial value - { - "combo_box_options": ["Off", "1.77", "1.89", "2.0"], - "groups": ["main_toolbar", "popout_toolbar", "dnmask_values"], #same as calling 'expose_in_ui_attrs_group' with these values later - "qml_code": button_qml - }) - - # attribute to hold the current mask aspect - self.current_mask_aspect = self.add_attribute( - "Mask Aspect", - 1.77) - - self.current_mask_aspect.expose_in_ui_attrs_group(["dnmask_values"]); - - # attribute to hold the current mask safety - self.current_mask_safety = self.add_attribute( - "Mask Aspect", - 1.77) - - self.current_mask_safety.expose_in_ui_attrs_group(["dnmask_values"]); - - # attribute to hold the current mask safety - self.line_thickness = self.add_attribute( - "Mask Line Thickness", - 1.0, - { - "float_scrub_min": 0.5, - "float_scrub_max": 10.0, - "float_scrub_step": 0.25, - "float_display_decimals": 2 - }) - - self.line_thickness.expose_in_ui_attrs_group(["dnmask_settings"]); - self.line_thickness.set_tool_tip("Sets the thickness of the masking lines"); - self.line_thickness.set_redraw_viewport_on_change(True); - - # attribute to hold the current mask safety - self.line_opacity = self.add_attribute( - "Mask Line Opacity", - 1.0, - { - "float_scrub_min": 0.0, - "float_scrub_max": 1.0, - "float_scrub_step": 0.1, - "float_display_decimals": 1 - }) - - self.line_opacity.expose_in_ui_attrs_group(["dnmask_settings"]); - self.line_opacity.set_tool_tip( - "Sets the opacity of the masking lines. Set to zero to hide the lines" - ); - - - self.show_mask_label = self.add_attribute( - "Show Mask Label", - True) - self.show_mask_label.expose_in_ui_attrs_group(["dnmask_settings"]) - - self.mask_overlay = self.add_attribute( - "Mask Overlay", - "", - { - "qml_code": overlay_qml - }) - self.mask_overlay.expose_in_ui_attrs_group(["viewport_overlay_plugins"]); - - self.connect_to_ui() - - self.set_attribute_changed_event_handler(self.attribute_changed) - - def attribute_changed(self, attr): - - if attr == self.mask: - if str(attr.value()) == "1.77": - self.current_mask_aspect.set_value(1.77) - elif str(attr.value()) == "1.89": - self.current_mask_aspect.set_value(1.89) - elif str(attr.value()) == "2.0": - self.current_mask_aspect.set_value(2.0) - else: - self.current_mask_aspect.set_value(0.0) - -# This method is required by xSTUDIO -def create_plugin_instance(connection): - return MaskPlugin(connection) - - -if __name__=="__main__": - - XSTUDIO = Connection(auto_connect=True) - mask_plugin_instance = create_plugin_instance(XSTUDIO) - XSTUDIO.process_events_forever() \ No newline at end of file diff --git a/src/demos/python_plugins/dnMask/qml/DnMask.1/DnMaskButton.qml b/src/demos/python_plugins/dnMask/qml/DnMask.1/DnMaskButton.qml deleted file mode 100644 index cc653c1cd..000000000 --- a/src/demos/python_plugins/dnMask/qml/DnMask.1/DnMaskButton.qml +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.12 - -import xStudio 1.0 - -XsToolbarItem { - - id: control - property var value_: value ? value : "" - title_: "Mask" - hovered: mouse_area.containsMouse - showHighlighted: mouse_area.containsMouse | mouse_area.pressed | (activated != undefined && activated) - property int iconsize: XsStyle.menuItemHeight *.66 - - MouseArea { - id: mouse_area - anchors.fill: parent - hoverEnabled: true - onClicked: { - toggleShow() - widget_clicked() - } - } - - property alias popup_win: popup - - Rectangle { - - id: popup - width: combo_group.width - height: combo_group.height + settings_button.height + 20 - visible: false - z: 10000 - - onVisibleChanged: { - if (!visible) { - sessionWidget.deactivateWidgetHider() - } - } - - // we have to make this a child of the sessionWidget to allow us to - // use a mouseArea that covers the sessionWidget to close this pop-up - // when the user clicks outside the pop-up. - parent: sessionWidget - - border { - color: XsStyle.menuBorderColor - width: XsStyle.menuBorderWidth - } - color: XsStyle.mainBackground - radius: XsStyle.menuRadius - - ListView { - - id: combo_group - - // N.B the 'combo_box_options' attribute is propagated via the - // XsToolBox which instantiates the BasicViewportMaskButton - model: combo_box_options - height: count * XsStyle.menuItemHeight - width: 100 - y: 5 - - delegate: Rectangle { - - id: checkBoxItem - // Icon (radio or checkbox) - anchors.left: parent.left - height: XsStyle.menuItemHeight - width: popup.width - - property var checked: thetext.text === control.value_ - property var highlighted: mouse_area.containsMouse - - color: highlighted ? palette.highlight : "transparent" - gradient: styleGradient.accent_gradient - - Text { - id: thetext - anchors.left: iconbox.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 7 - text: (combo_box_options && index >= 0) ? combo_box_options[index] : "" - color: XsStyle.controlColor - font.family: XsStyle.controlTitleFontFamily - font.pixelSize: XsStyle.popupControlFontSize - } - - Rectangle { - - id: iconbox - width: iconsize - height: iconsize - - // color: menuItem.checked?XsStyle.highlightColor:"transparent" - color: checkBoxItem.checked ? palette.highlight : "transparent" - - anchors.left: parent.left - anchors.leftMargin: 7 - anchors.verticalCenter: parent.verticalCenter - // visible: menuItem.checkable - border.width: 1 // mycheckableDecorated?1:0 - border.color: checkBoxItem.checked ? XsStyle.hoverColor : checkBoxItem.highlighted ? XsStyle.hoverColor : XsStyle.mainColor - - radius: iconsize / 2 - Image { - id: checkIcon - visible: checkBoxItem.checked - sourceSize.height: XsStyle.menuItemHeight - sourceSize.width: XsStyle.menuItemHeight - source: "check" - width: iconsize // 2 - height: iconsize // 2 - anchors.centerIn: parent - } - ColorOverlay{ - id: colorolay - anchors.fill: checkIcon - source:checkIcon - visible: checkBoxItem.checked - color: checkBoxItem.highlighted?XsStyle.hoverColor:XsStyle.hoverColor - antialiasing: true - } - } - - MouseArea { - id: mouse_area - hoverEnabled: true - anchors.fill: parent - onClicked: { - value = thetext.text - popup.visible = false - } - - } - - } - - } - - Rectangle { - - id: divider - anchors.margins: 5 - anchors.left: parent.left - anchors.right: parent.right - anchors.top: combo_group.bottom - height: XsStyle.menuBorderWidth - color: XsStyle.menuBorderColor - } - - Rectangle { - - id: settings_button - anchors.left: parent.left - anchors.right: parent.right - anchors.top: divider.bottom - anchors.margins: 5 - height: XsStyle.menuItemHeight - property var highlighted: mouse_area2.containsMouse - color: highlighted ? palette.highlight : "transparent" - gradient: styleGradient.accent_gradient - - Text { - id: thetext - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "Mask Settings ..." - color: XsStyle.controlColor - font.family: XsStyle.controlTitleFontFamily - font.pixelSize: XsStyle.popupControlFontSize - } - - MouseArea { - id: mouse_area2 - hoverEnabled: true - anchors.fill: parent - onClicked: { - settings_dialog.show() - settings_dialog.raise() - popup.visible = false - } - } - } - - XsModuleAttributesDialog { - id: settings_dialog - title: "dnMask Settings" - attributesGroupNames: "dnmask_settings" - } - - - } - - function toggleShow() { - if(popup.visible) { - popup.visible = false - } - else{ - var popup_coords = mapToGlobal(0,0) - popup_coords = sessionWidget.mapFromGlobal(popup_coords.x,popup_coords.y) - popup.y = popup_coords.y-popup.height - popup.x = popup_coords.x - popup.visible = true - sessionWidget.activateWidgetHider(hideMe) - - } - } - - function hideMe() { - popup.visible = false - } -} diff --git a/src/demos/python_plugins/dnMask/qml/DnMask.1/DnMaskOverlay.qml b/src/demos/python_plugins/dnMask/qml/DnMask.1/DnMaskOverlay.qml deleted file mode 100644 index 730572e5f..000000000 --- a/src/demos/python_plugins/dnMask/qml/DnMask.1/DnMaskOverlay.qml +++ /dev/null @@ -1,170 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.3 -import QtQuick 2.14 -import QuickFuture 1.0 -import QuickPromise 1.0 - -// These imports are necessary to have access to custom QML types that are -// part of the xSTUDIO UI implementation. -import xStudio 1.0 -import xstudio.qml.module 1.0 - -// Our Overlay is based on a transparent rectangle that simply fills the -// xSTUDIO viewport. Within this we draw the overlay graphics as required. -Rectangle { - - // Note that viewport overlays are instanced by the Viewport QML instance - // which has the id 'viewport' and is visible to us here. To learn more - // about the viewport see files XsViewport.qml and qml_viewport.cpp from - // the xSTUDIO source code. - - id: control - color: "transparent" - width: viewport.width - height: viewport.height - - // The imageBoundaryInViewport property on the xSTUDIO Viewport class - // is a rectangle that indicates the pixel coordinates of the image - // boundary in the xSTUDIO viewport. - property var imageBox: viewport.imageBoundaryInViewport - - onImageBoxChanged: { - computeMask() - } - - // The XsModuleAttributes type provides the means to connect with - // attributes that were created on the backend plugin class instance. - // Note that the 'attributesGroupNames' property here must line up with - // one of the strings passed into the Attributer.expose_in_ui_attrs_group - // method when the plugin class is initialised. Then, if an attribute is - // called 'Mask Aspect' for example, it will be available as a property - // with an id of 'mask_aspect' - spaces are replaced with underscores and - // all letters are made lower case. - XsModuleAttributes { - - id: mask_settings - attributesGroupNames: "dnmask_settings" - onAttrAdded: control.computeMask() - onValueChanged: control.computeMask() - - } - - XsModuleAttributes { - - id: current_mask_spec - attributesGroupNames: "dnmask_values" - onAttrAdded: control.computeMask() - onValueChanged: control.computeMask() - - } - - // Properties on the XsModuleAttributes items are created at runtime after - // this item is 'completed' and therefore we need to map to local variables - // with checks as follows: - property var mask_aspect: current_mask_spec.mask_aspect ? current_mask_spec.mask_aspect : 0.0 - property var safety_percent: current_mask_spec.safety_percent ? current_mask_spec.safety_percent : 0.0 - property var mask_opacity: mask_settings.mask_opacity ? mask_settings.mask_opacity : 0.0 - property var mask_line_opacity: mask_settings.mask_line_opacity ? mask_settings.mask_line_opacity : 0.0 - property var mask_line_thickness: mask_settings.mask_line_thickness ? mask_settings.mask_line_thickness : 0.0 - property var mask_name: current_mask_spec.mask ? current_mask_spec.mask : "" - property var show_mask_label: mask_settings.show_mask_label ? mask_settings.show_mask_label : false - property bool mask_defined: false - - property var l: 0.0 - property var b: 0.0 - property var r: width - property var t: height - - visible: mask_defined - - function computeMask() { - - if (mask_aspect == undefined || mask_aspect == 0.0) { - mask_defined = false - return; - } else { - mask_defined = true - } - - // assuming mask should be 'width fitted' - l = imageBox.x - r = imageBox.x + imageBox.width - var h = imageBox.width/mask_aspect - b = imageBox.y + (imageBox.height-h)/2.0 - t = imageBox.y + imageBox.height/2 + h/2.0 - - if (safety_percent != 0.0) { - var xd = (r-l)*safety_percent/100.0 - l = l + xd - r = r - xd - var yd = (t-b)*safety_percent/100.0 - b = b + yd - t = t - yd - } - - } - - Rectangle { - id: bottom_masking_rect - opacity: mask_opacity - color: "black" - x: 0 - y: 0 - width: control.width - height: b - } - - Rectangle { - id: top_masking_rect - opacity: mask_opacity - color: "black" - x: 0 - y: t - width: control.width - height: control.height-t - } - - Rectangle { - id: left_masking_rect - opacity: mask_opacity - color: "black" - x: 0 - y: b - width: l - height: t-b - } - - Rectangle { - id: right_masking_rect - opacity: mask_opacity - color: "black" - x: r - y: b - width: control.width-x - height: t-b - } - - Rectangle { - id: lines - opacity: mask_line_opacity - color: "transparent" - border.color: "white" - border.width: mask_line_thickness - x: l-mask_line_thickness/2 - y: b-mask_line_thickness/2 - width: r-l+mask_line_thickness - height: t-b+mask_line_thickness - } - - Text { - text: mask_name - opacity: mask_line_opacity - visible: show_mask_label - color: "white" - anchors.left: lines.left - anchors.bottom: lines.top - anchors.bottomMargin: 4 - } - -} diff --git a/src/demos/python_plugins/dnMask/qml/DnMask.1/qmldir b/src/demos/python_plugins/dnMask/qml/DnMask.1/qmldir deleted file mode 100644 index 49d7d4976..000000000 --- a/src/demos/python_plugins/dnMask/qml/DnMask.1/qmldir +++ /dev/null @@ -1,4 +0,0 @@ -module DnMask - -DnMaskButton 1.0 DnMaskButton.qml -DnMaskOverlay 1.0 DnMaskOverlay.qml \ No newline at end of file diff --git a/src/demos/python_plugins/dnSetLoopRange/CMakeLists.txt b/src/demos/python_plugins/dnSetLoopRange/CMakeLists.txt deleted file mode 100644 index f85a14bec..000000000 --- a/src/demos/python_plugins/dnSetLoopRange/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -cmake_minimum_required(VERSION 3.0) - -# install python package into lib/python directory -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DESTINATION usr/lib/xstudio/python-plugins - FILES_MATCHING PATTERN "*.py" -) diff --git a/src/demos/python_plugins/dnSetLoopRange/__init__.py b/src/demos/python_plugins/dnSetLoopRange/__init__.py deleted file mode 100644 index ecd03312d..000000000 --- a/src/demos/python_plugins/dnSetLoopRange/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -from .set_loop_range import create_plugin_instance diff --git a/src/demos/python_plugins/dnSetLoopRange/set_loop_range.py b/src/demos/python_plugins/dnSetLoopRange/set_loop_range.py deleted file mode 100755 index a0fd41750..000000000 --- a/src/demos/python_plugins/dnSetLoopRange/set_loop_range.py +++ /dev/null @@ -1,187 +0,0 @@ -#!/bin/env python -# SPDX-License-Identifier: Apache-2.0 - -from xstudio.connection import Connection -from xstudio.plugin import PluginBase -from xstudio.core import JsonStore, change_attribute_event_atom -from xstudio.core import KeyboardModifier - -""" - -DnSetLoopRange - Demo xSTUDIO Python Plugin - -This simple plugin demostrates how to add menu items to xSTUDIO's main menus -which will trigger a callback when the user clicks on the menu item. Creating -a hotkey and associating it with the menu item is also illustrated. - -In this case the menu action will modify the loop frame range of the playhead -based on metadata of the current media item on the screen. Thus we also -demostrate how to access the on-screen media item, its metadata and modify -the playhead properties. - -Note that the metadata fields that we access in this plugin are specific to -DNEG's pipeline, and the metadata is added to the media item by the DNEG data -source plugin. Therefore this plugin will not work as-is and would need to -be modified to work with your own metadata layout. -""" - - -def map_render_frame_to_playhead_frame(render_frame, playhead): - - # For a given render frame number for the current on-screen media source, - # we need to find out where that frame is in the playhead timeline. - - # At DNEG, for a render that starts on frame 1001, say, we embed - # timecode in quicktime encodings of 00:00:41:17 so we 'know' - # the first frame is frame 1001. For EXRs (and other image based - # sequences) xSTUDIO adds phoney timecode, also of 00:00:41:17, - # to the media reference to keep the approach consistent. (If - # it's present in the EXR metadata, 'real' timecode - e.g. from - # the camera or audio, will still be untouched in the metadata - # dictionary) - source_first_frame_from_tc = playhead.on_screen_media.media_source( - ).media_reference.timecode().total_frames() - - # ... however, in xSTUDIO the first logical media frame of a - # quicktime will always be 1. For EXRs, it corresponds to the - # first framenumber in the file sequence (so it would be 1001). - source_first_logical_frame = playhead.on_screen_media.media_source( - ).media_reference.frame(0) - - # Thus in general, we expect this value to be zero for EXR (or - # other image based sequences) - map_media_frame_to_render_frame_num = source_first_frame_from_tc - \ - source_first_logical_frame - - # so this is the (render) frame number of the current on screen image - current_render_frame_num = playhead.media_frame + \ - map_media_frame_to_render_frame_num - - # Now we can get the simple mapping between the current playhead - # timeline frame (playhead.position) and the render_frame - return render_frame + playhead.position - current_render_frame_num - - -class DnSetLoopRange(PluginBase): - - def __init__(self, connection): - - PluginBase.__init__( - self, - connection, - "dnSetLoopRange") - - # Register a hotkey, passing in our callback function - loop_on_cut_hotkey_uuid = self.register_hotkey( - self.loop_on_cut_range, # the callback - 'P', - default_modifier=KeyboardModifier.ShiftModifier, - hotkey_name="Set Loop In/Out to Cut Range", - description="Hit this modifier to loop over the cut range for the given source", - auto_repeat=False, - component="Playback", - context="any") - - # Register a 2nd hotkey - loop_on_comp_hotkey_uuid = self.register_hotkey( - self.loop_on_comp_range, - 'P', - default_modifier=KeyboardModifier.ControlModifier, - hotkey_name="Set Loop In/Out to Comp Range", - description="Hit this modifier to loop over the comp range for the given source", - auto_repeat=False, - component="Playback", - context="any") - - # Extend the 'playback' menu - the key here is the menu_path - # argument. If you wanted to put this menu item in a sub-menu - # under the Playback menu, you could do menu_path="playback_menu|Pipeline Loop range" - # for example, i.e. use pipes to append sub menus. - self.add_menu_item( - menu_item_name="Set Loop to Cut Range", - menu_path="playback_menu", - menu_trigger_callback=self.loop_on_cut_range, - hotkey_uuid=loop_on_cut_hotkey_uuid) - - self.add_menu_item( - menu_item_name="Set Loop to Comp Range", - menu_path="playback_menu", - menu_trigger_callback=self.loop_on_comp_range, - hotkey_uuid=loop_on_comp_hotkey_uuid) - - self.connect_to_ui() - - def loop_on_cut_range(self, activated=True): - - # Here we access the metadata propery of the on screen media item. - playhead = super().current_playhead() - if activated and playhead.on_screen_media and playhead.on_screen_media.metadata: - - try: - # Here 'sg_cut_range' is custom metadata added to the media - # elsewhere (DNEG's data source plugin, in this case, which - # interfaces with the shotgrid database). The cut range is - # a string of two numbers separated with a dash - "1008-1040" - # for example. I've been a bit lazy with error checking - # here but it should give the gist. - cut_range = JsonStore(playhead.on_screen_media.metadata).get( - '/metadata/shotgun/shot/attributes/sg_cut_range') - - cut_in = int(cut_range.split("-")[0]) - cut_out = int(cut_range.split("-")[1]) - - # Map these numbers to the playhead timeline frame of reference - cut_in = map_render_frame_to_playhead_frame(cut_in, playhead) - cut_out = map_render_frame_to_playhead_frame(cut_out, playhead) - - # Now we can set the relevant properties on the playhead to - # apply the loop - if playhead.loop_in_point and cut_in == playhead.loop_in_point and playhead.loop_out_point == cut_out: - # ... toggle loop OFF if we're already looping on the range - playhead.use_loop_range = False - else: - playhead.loop_in_point = cut_in - playhead.loop_out_point = cut_out - playhead.use_loop_range = True - except Exception as e: - print (e) - - def loop_on_comp_range(self, activated=True): - - playhead = super().current_playhead() - if activated and playhead.on_screen_media and playhead.on_screen_media.metadata: - - try: - import json - comp_range = JsonStore(playhead.on_screen_media.metadata).get( - '/metadata/shotgun/shot/attributes/sg_comp_range') - comp_in = int(comp_range.split("-")[0]) - comp_out = int(comp_range.split("-")[1]) - - comp_in = map_render_frame_to_playhead_frame(comp_in, playhead) - comp_out = map_render_frame_to_playhead_frame( - comp_out, - playhead) - - if playhead.loop_in_point and comp_in == playhead.loop_in_point and playhead.loop_out_point == comp_out: - playhead.use_loop_range = False - else: - playhead.loop_in_point = comp_in - playhead.loop_out_point = comp_out - playhead.use_loop_range = True - except Exception as e: - print (e) - - -# This method is always required by python plugins with the given signature -def create_plugin_instance(connection): - return DnSetLoopRange(connection) - - -# This code allows you to execute this python script as an independent process -# to xstudio itself -if __name__ == "__main__": - - XSTUDIO = Connection(auto_connect=True) - create_plugin_instance(XSTUDIO) - XSTUDIO.process_events_forever() diff --git a/src/embedded_python/src/CMakeLists.txt b/src/embedded_python/src/CMakeLists.txt index 88d406b4c..3de1b2a83 100644 --- a/src/embedded_python/src/CMakeLists.txt +++ b/src/embedded_python/src/CMakeLists.txt @@ -1,12 +1,12 @@ -cmake_minimum_required(VERSION 3.12) -project(embedded_python VERSION 0.1.0 LANGUAGES CXX) +project(embedded_python VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) +find_package(Python COMPONENTS Interpreter Development) find_package(pybind11 CONFIG REQUIRED) # or `add_subdirectory(pybind11)` find_package(spdlog CONFIG REQUIRED) find_package(fmt CONFIG REQUIRED) find_package(Imath) -#find_package(OpenTime REQUIRED) -#find_package(OpenTimelineIO REQUIRED) + +find_package(Python REQUIRED COMPONENTS Interpreter Development) set(SOURCES embedded_python.cpp @@ -17,30 +17,38 @@ add_library(${PROJECT_NAME} SHARED ${SOURCES}) add_library(xstudio::embedded_python ALIAS ${PROJECT_NAME}) default_options(${PROJECT_NAME}) -if(UNIX) -target_compile_options(${PROJECT_NAME} - PRIVATE -fvisibility=hidden -) +if(NOT ${OTIO_SUBMODULE}) + find_package(OpenTime REQUIRED) + find_package(OpenTimelineIO REQUIRED) endif() -set_python_to_proper_build_type() +if(UNIX) + target_compile_options(${PROJECT_NAME} + PRIVATE -fvisibility=hidden + ) +endif() target_link_libraries(${PROJECT_NAME} PUBLIC - caf::core + CAF::core PRIVATE Imath::Imath xstudio::utility - xstudio::broadcast - pybind11::embed - #OTIO::opentime - #OTIO::opentimelineio + xstudio::global_store + xstudio::media + Python::Python + OTIO::opentime + OTIO::opentimelineio ) + + +set_python_to_proper_build_type() set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) if(WIN32) -install(DIRECTORY ${VCPKG_DIRECTORY}/../vcpkg_installed/x64-windows/tools/python3/DLLs/ DESTINATION ${CMAKE_INSTALL_PREFIX}/python) -install(DIRECTORY ${VCPKG_DIRECTORY}/../vcpkg_installed/x64-windows/tools/python3/Lib/ DESTINATION ${CMAKE_INSTALL_PREFIX}/python) -install(FILES python310._pth DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) +# this looks awkward! +install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../../../vcpkg_installed/x64-windows/tools/python3/DLLs/ DESTINATION python) +install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../../../vcpkg_installed/x64-windows/tools/python3/Lib/ DESTINATION python) +install(FILES python310._pth DESTINATION bin) endif() \ No newline at end of file diff --git a/src/embedded_python/src/embedded_python.cpp b/src/embedded_python/src/embedded_python.cpp index d4df3741a..b3a5ca657 100644 --- a/src/embedded_python/src/embedded_python.cpp +++ b/src/embedded_python/src/embedded_python.cpp @@ -1,5 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include +#define PYBIND11_DETAILED_ERROR_MESSAGES + #include // everything needed for embedding #include @@ -9,14 +12,18 @@ #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/logging.hpp" +#ifdef WIN32 +#else +#include +#include +#endif + using namespace xstudio; using namespace xstudio::embedded_python; using namespace xstudio::utility; using namespace pybind11::literals; namespace py = pybind11; -// EmbeddedPython *EmbeddedPython::s_instance_ = nullptr; - EmbeddedPython::EmbeddedPython(const std::string &name, EmbeddedPythonActor *parent) : Container(name, "EmbeddedPython"), parent_(parent) { s_instance_ = this; @@ -31,11 +38,55 @@ void EmbeddedPython::setup() { try { if (not Py_IsInitialized()) { spdlog::debug("py::initialize_interpreter"); - py::initialize_interpreter(); + + PyConfig config; + PyConfig_InitPythonConfig(&config); + +#ifndef _WIN32 + // Since we're running embedded python, we need to set the PYTHONHOME + // correctly at runtime. We can use dladdr to get to the filesystem + // location of the Py_IsInitialized symbol, say, to get the path of + // the python.dylib - this should be in the same location as the + // rest of the python installation + Dl_info info; + // Some of the libc headers miss `const` in `dladdr(const void*, Dl_info*)` + const int res = dladdr((void*)(&Py_IsInitialized), &info); + if (res) { + auto p = fs::path(info.dli_fname); + std::string python_home; + if (python_home.find("Contents/Frameworks") != std::string::npos) { + // String match will happen On MacOS install, here python + // installation is in Frameworks colder in the app bundle + python_home = p.parent_path(); + } else { + // Otherwise, we jump up twice to get above the 'lib' folder + // where python310.so is installed, as python home should + // be the parent folder of where the main python DSO is + // installed + python_home = p.parent_path().parent_path(); + } + std::wstring_convert> converter; + std::wstring wstr = converter.from_bytes(python_home.data()); + PyConfig_SetString(&config, &config.home, wstr.data()); + + std::string xstudio_python_path; + auto pythonpath_env = get_env("PYTHONPATH"); + if (pythonpath_env) { + xstudio_python_path = *pythonpath_env + ":"; + } + xstudio_python_path += utility::xstudio_resources_dir("python/lib/"); + wstr = converter.from_bytes(xstudio_python_path.data()); + PyConfig_SetString(&config, &config.pythonpath_env, wstr.data()); + } +#endif + + py::initialize_interpreter(&config); inited_ = true; } + if (Py_IsInitialized() and not setup_) { exec(R"( +import sys import xstudio from xstudio.connection import Connection from xstudio.core import * @@ -72,7 +123,6 @@ EmbeddedPython::~EmbeddedPython() { finalize(); } void EmbeddedPython::disconnect() { - message_handler_callbacks_.clear(); try { exec(R"( if 'XSTUDIO' in globals(): @@ -117,8 +167,8 @@ bool EmbeddedPython::connect(caf::actor actor) { // spdlog::warn("connect 1.5"); auto pyactor = py::cast(actor); // spdlog::warn("connect 2.5"); - auto local = py::dict("actor"_a = pyactor); - + py::dict local; + local["actor"] = pyactor; // spdlog::warn("connect 2"); exec(R"( XSTUDIO = Connection( @@ -207,13 +257,17 @@ bool EmbeddedPython::remove_session(const utility::Uuid &session_uuid) { bool EmbeddedPython::input_session( const utility::Uuid &session_uuid, const std::string &input) { if (sessions_.count(session_uuid)) { - std::string clean_input = replace_all(input, "'", R"(\')"); + std::string clean_input = rtrim(input); py::object scope = py::module::import("__main__").attr("__dict__"); py::exec( - "xstudio_sessions['" + to_string(session_uuid) + "'].interact_more('" + - rtrim(clean_input) + "')", + "xstudio_sessions['" + to_string(session_uuid) + "'].interact_more(\"\"\"" + + clean_input + "\"\"\")", scope); return true; + } else if (session_uuid.is_null()) { + std::string clean_input = rtrim(input); + py::object scope = py::module::import("__main__").attr("__dict__"); + py::exec(clean_input, scope); } return false; @@ -228,75 +282,4 @@ bool EmbeddedPython::input_ctrl_c_session(const utility::Uuid &session_uuid) { } return false; -} - -void EmbeddedPython::add_message_callback(const py::tuple &cb_particulars) { - - try { - - if (cb_particulars.size() == 2) { - - auto i = cb_particulars.begin(); - auto remote_actor = (*i).cast(); - i++; - auto callback_func = (*i).cast(); - auto addr = caf::actor_cast(remote_actor); - - message_handler_callbacks_[addr].push_back(callback_func); - parent_->join_broadcast(remote_actor); - - } else { - - if (cb_particulars.size() != 3) { - throw std::runtime_error("Set message callback expecting tuple of size 3 " - "(remote_actor, callack_func, py_plugin_name)."); - } - auto i = cb_particulars.begin(); - auto remote_actor = (*i).cast(); - i++; - auto callback_func = (*i).cast(); - i++; - auto plugin_name = (*i).cast(); - auto addr = caf::actor_cast(remote_actor); - - message_handler_callbacks_[addr].push_back(callback_func); - parent_->join_broadcast(remote_actor, plugin_name); - } - - } catch (std::exception &e) { - PyErr_SetString(PyExc_RuntimeError, e.what()); - } -} - -void EmbeddedPython::s_add_message_callback(const py::tuple &cb_particulars) { - if (s_instance_) { - s_instance_->add_message_callback(cb_particulars); - } -} - -PYBIND11_EMBEDDED_MODULE(XStudioExtensions, m) { - // `m` is a `py::module_` which is used to bind functions and classes - m.def("add_message_callback", &EmbeddedPython::s_add_message_callback); - py::class_(m, "CafMessage", py::module_local()); -} - -void EmbeddedPython::push_caf_message_to_py_callbacks(caf::actor sender, caf::message &m) { - - try { - - caf::message r = m; - py::tuple result(1); - PyTuple_SetItem(result.ptr(), 0, py::cast(r).release().ptr()); - - auto addr = caf::actor_cast(sender); - - for (auto &p : message_handler_callbacks_) { - for (auto &func : p.second) { - func(result); - } - } - - } catch (std::exception &e) { - PyErr_SetString(PyExc_RuntimeError, e.what()); - } -} +} \ No newline at end of file diff --git a/src/embedded_python/src/embedded_python_actor.cpp b/src/embedded_python/src/embedded_python_actor.cpp index d194fde59..c2d8c6006 100644 --- a/src/embedded_python/src/embedded_python_actor.cpp +++ b/src/embedded_python/src/embedded_python_actor.cpp @@ -1,15 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 #include #include +#include + +#include #include // everything needed for embedding #include // everything needed for embedding #include #include +#include -#ifdef BUILD_OTIO #include -#endif #include "xstudio/atoms.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" @@ -17,6 +19,7 @@ #include "xstudio/global_store/global_store.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" +#include "xstudio/embedded_python/python_thread_locker.hpp" using namespace xstudio; using namespace xstudio::utility; @@ -26,12 +29,10 @@ using namespace nlohmann; using namespace caf; using namespace pybind11::literals; +namespace fs = std::filesystem; namespace py = pybind11; -#ifdef BUILD_OTIO namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; -#endif - #ifdef __GNUC__ // Check if GCC compiler is being used #pragma GCC visibility push(hidden) @@ -83,15 +84,25 @@ void send_output( auto stde = output.stderr_string(); if (not stdo.empty() or not stde.empty()) { - act->send( - grp, - utility::event_atom_v, - python_output_atom_v, - uuid, - std::make_tuple(stdo, stde)); + act->mail( + utility::event_atom_v, python_output_atom_v, uuid, std::make_tuple(stdo, stde)) + .send(grp); } } +void py_print(const std::string &e) { + // Note . on Windows, using py::arg or _a literal is causing seg-fault + // but using a dict to set kwargs on py::print works fine. + auto py_stderr = py::module::import("sys").attr("stderr").cast(); +#ifdef _WIN32 + py::dict d; + d["file"] = py_stderr; + py::print(e, d); +#else + py::print(e, "file"_a = py_stderr); +#endif +} + class PyObjectRef { public: PyObjectRef(PyObject *obj) : obj_(obj) { @@ -107,11 +118,9 @@ class PyObjectRef { operator PyObject *() const { return obj_; } }; -#ifdef __GNUC__ // Check if GCC compiler is being used + #pragma GCC visibility pop -#else -#pragma warning(pop) -#endif + EmbeddedPythonActor::EmbeddedPythonActor(caf::actor_config &cfg, const utility::JsonStore &jsn) : caf::blocking_actor(cfg), base_(static_cast(jsn["base"]), this) { @@ -125,11 +134,65 @@ EmbeddedPythonActor::EmbeddedPythonActor(caf::actor_config &cfg, const std::stri init(); } +static void *s_run(void *self) { + ((EmbeddedPythonActor *)self)->main_loop(); + return nullptr; +} void EmbeddedPythonActor::act() { +#ifdef __apple__ + // Note: on MacOS, the default stack size for subthreads is 512K. + // We are seeing bus error when xstudio api module is imported. + // I suspect there is some problem with interdependency between + // our .py files causing some recursion that eventually halts + // on Linux but blows up on Mac with the smaller stack, + // but not sure about that. It could just be that the stack + // is not an appropriate size for running python interpreter + // in. + pthread_t _thread; + pthread_attr_t attr; + pthread_attr_init(&attr); + size_t stacksize; + + // increase stack to 10x default + pthread_attr_setstacksize(&attr, 524288 * 10); + + pthread_create(&_thread, &attr, s_run, this); + pthread_join(_thread, NULL); +#else + main_loop(); +#endif +} + +void EmbeddedPythonActor::main_loop() { bool running = true; + data_.set_origin(true); + data_.bind_send_event_func([&](auto &&PH1, auto &&PH2) { + auto event = JsonStore(std::forward(PH1)); + auto undo_redo = std::forward(PH2); + mail(utility::event_atom_v, json_store::sync_atom_v, data_uuid_, event) + .send(event_group_); + }); + + try { + auto prefs = GlobalStoreHelper(system()); + JsonStore j; + join_broadcast(prefs.get_group(j)); + update_preferences(j); + } catch (...) { + } + base_.setup(); + + // data_.insert_rows( + // 0, 1, + // R"([{ + // "name": "test", + // "path": "file:///u/al/.config/DNEG/xstudio/snippets/test.py" + // }])"_json + // ); + receive_while(running)( base_.make_set_name_handler(event_group_, this), base_.make_get_name_handler(), @@ -156,9 +219,7 @@ void EmbeddedPythonActor::act() { } } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -166,8 +227,7 @@ void EmbeddedPythonActor::act() { send_output(this, event_group_, out); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -189,9 +249,7 @@ void EmbeddedPythonActor::act() { } } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -199,8 +257,7 @@ void EmbeddedPythonActor::act() { send_output(this, event_group_, out); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -211,7 +268,130 @@ void EmbeddedPythonActor::act() { return result; }, -#ifdef BUILD_OTIO + [=](session::export_atom) -> result> { + // get otio supported export formats. + auto result = std::vector({"otio"}); + + if (not base_.enabled()) + return make_error(xstudio_error::error, "EmbeddedPython disabled"); + + std::string error; + + try { + PyStdErrOutStreamRedirect out{}; + try { + // Import the OTIO Python module. + auto p_module = + PyObjectRef(PyImport_ImportModule("opentimelineio.adapters")); + auto p_suffixes_with_defined_adapters = PyObjectRef( + PyObject_GetAttrString(p_module, "suffixes_with_defined_adapters")); + auto p_suffixes_with_defined_adapters_args = PyObjectRef(PyTuple_New(2)); + + PyTuple_SetItem(p_suffixes_with_defined_adapters_args, 0, Py_False); + PyTuple_SetItem(p_suffixes_with_defined_adapters_args, 1, Py_True); + + auto p_suffixes = PyObjectRef(PyObject_CallObject( + p_suffixes_with_defined_adapters, + p_suffixes_with_defined_adapters_args)); + + PyObject *repr = PyObject_Repr(p_suffixes); + if (repr) { + PyObject *str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~"); + if (str) { + const char *bytes = PyBytes_AS_STRING(str); + + for (const auto &i : + resplit(std::string(bytes), std::regex{"\\{'|'\\}|',\\s'"})) { + if (not i.empty()) + result.push_back(i); + } + // something like .. {'otiod', 'edl', 'mb', 'ma', 'kdenlive', + // 'otio', 'otioz', 'ale', 'm3u8', 'xml', 'aaf', 'fcpxml', 'svg', + // 'xges', 'rv'} + + Py_XDECREF(str); + } + Py_XDECREF(repr); + } + + std::sort(result.begin(), result.end()); + return result; + } catch (py::error_already_set &err) { + err.restore(); + error = err.what(); + py_print(error); + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + error = err.what(); + } + // send_output(this, event_group_, out, uuid); + } catch (py::error_already_set &err) { + err.restore(); + error = err.what(); + py_print(error); + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + error = err.what(); + } + + return make_error(xstudio_error::error, error); + }, + + // downgrade_manifest = otio.versioning.fetch_map("OTIO_CORE", "0.15.0") + // otio.adapters.write_to_file( + // sc, + // "/path/to/file.otio", + // target_schema_versions=downgrade_manifest + // ) + [=](session::export_atom, + const std::string &otio_str, + const caf::uri &upath, + const std::string &type, + const std::string &target_schema) -> result { + // get otio supported export formats. + if (not base_.enabled()) + return make_error(xstudio_error::error, "EmbeddedPython disabled"); + + std::string error; + + try { + PyStdErrOutStreamRedirect out{}; + otio::ErrorStatus error_status; + // Import the OTIO Python module. + auto path = uri_to_posix_path(upath); + + py::object read_from_string = + py::module_::import("opentimelineio.adapters").attr("read_from_string"); + py::object write_to_file = + py::module_::import("opentimelineio.adapters").attr("write_to_file"); + + py::object p_timeline = read_from_string(otio_str); + + if (target_schema.empty()) { + write_to_file(p_timeline, path); + } else { + py::object fetchmap = + py::module_::import("opentimelineio.versioning").attr("fetch_map"); + py::object vmap = fetchmap("OTIO_CORE", target_schema); + py::object result = + write_to_file(p_timeline, path, "target_schema_versions"_a = vmap); + } + + return true; + } catch (py::error_already_set &err) { + err.restore(); + error = err.what(); + py_print(error); + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + error = err.what(); + } + + return make_error(xstudio_error::error, error); + }, // import otio file return as otio xml string. // if already native format should be quick.. @@ -226,38 +406,16 @@ void EmbeddedPythonActor::act() { try { otio::ErrorStatus error_status; - auto p_module = - PyObjectRef(PyImport_ImportModule("opentimelineio.adapters")); - // Read the timeline into Python. - auto p_read_from_file = - PyObjectRef(PyObject_GetAttrString(p_module, "read_from_file")); - auto p_read_from_file_args = PyObjectRef(PyTuple_New(1)); - - const std::string file_name_normalized = uri_to_posix_path(path); - auto p_read_from_file_arg = PyUnicode_FromStringAndSize( - file_name_normalized.c_str(), file_name_normalized.size()); - if (!p_read_from_file_arg) { - throw std::runtime_error("cannot create arg"); - } - PyTuple_SetItem(p_read_from_file_args, 0, p_read_from_file_arg); - auto p_timeline = PyObjectRef( - PyObject_CallObject(p_read_from_file, p_read_from_file_args)); - - // Convert the Python timeline into a string and use that to create a C++ - // timeline. - auto p_to_json_string = - PyObjectRef(PyObject_GetAttrString(p_timeline, "to_json_string")); - auto p_json_string = - PyObjectRef(PyObject_CallObject(p_to_json_string, nullptr)); - - return PyUnicode_AsUTF8AndSize(p_json_string, nullptr); + py::object read_from_file = + py::module_::import("opentimelineio.adapters").attr("read_from_file"); + py::object p_timeline = read_from_file(file_name_normalized); + py::str jsn = p_timeline.attr("to_json_string")(); + return jsn; } catch (py::error_already_set &err) { err.restore(); error = err.what(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -267,9 +425,8 @@ void EmbeddedPythonActor::act() { // send_output(this, event_group_, out, uuid); } catch (py::error_already_set &err) { err.restore(); - error = err.what(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + error = err.what(); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -279,8 +436,6 @@ void EmbeddedPythonActor::act() { return make_error(xstudio_error::error, error); }, -#endif BUILD_OTIO - [=](python_create_session_atom, const bool interactive) -> result { if (not base_.enabled()) return make_error(xstudio_error::error, "EmbeddedPython disabled"); @@ -294,9 +449,8 @@ void EmbeddedPythonActor::act() { return session_uuid; } catch (py::error_already_set &err) { err.restore(); - error = err.what(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + error = err.what(); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); return make_error(xstudio_error::error, err.what()); } catch (const std::exception &err) { @@ -322,9 +476,7 @@ void EmbeddedPythonActor::act() { } catch (py::error_already_set &err) { err.restore(); error = err.what(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -334,9 +486,8 @@ void EmbeddedPythonActor::act() { send_output(this, event_group_, out, uuid); } catch (py::error_already_set &err) { err.restore(); - error = err.what(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + error = err.what(); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -355,8 +506,7 @@ void EmbeddedPythonActor::act() { return JsonStore(base_.eval(pystring, locals)); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); return make_error(xstudio_error::error, e.what()); } catch (const std::exception &err) { @@ -375,9 +525,7 @@ void EmbeddedPythonActor::act() { base_.eval_file(uri_to_posix_path(pyfile)); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -386,8 +534,7 @@ void EmbeddedPythonActor::act() { send_output(this, event_group_, out, uuid); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -402,8 +549,7 @@ void EmbeddedPythonActor::act() { return JsonStore(base_.eval_locals(pystring)); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); return make_error(xstudio_error::error, e.what()); } catch (const std::exception &err) { @@ -422,8 +568,7 @@ void EmbeddedPythonActor::act() { return JsonStore(base_.eval_locals(pystring, locals)); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); return make_error(xstudio_error::error, e.what()); } catch (const std::exception &err) { @@ -442,9 +587,7 @@ void EmbeddedPythonActor::act() { base_.exec(pystring); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -453,8 +596,7 @@ void EmbeddedPythonActor::act() { send_output(this, event_group_, out, uuid); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -469,8 +611,7 @@ void EmbeddedPythonActor::act() { return base_.remove_session(uuid); } catch (py::error_already_set &e) { e.restore(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(e.what(), "file"_a = py_stderr); + py_print(e.what()); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", e.what()); return make_error(xstudio_error::error, e.what()); } catch (const std::exception &err) { @@ -497,20 +638,21 @@ void EmbeddedPythonActor::act() { } catch (py::error_already_set &err) { err.restore(); error = err.what(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); - spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); - return make_error(xstudio_error::error, err.what()); + py_print(error); + // get console back to input mode.. + auto result = base_.input_session(uuid, ""); + send_output(this, event_group_, out, uuid); + return result; + // spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); + // return make_error(xstudio_error::error, err.what()); } catch (const std::exception &err) { return make_error(xstudio_error::error, err.what()); } send_output(this, event_group_, out, uuid); } catch (py::error_already_set &err) { err.restore(); - error = err.what(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + error = err.what(); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); return make_error(xstudio_error::error, err.what()); } catch (const std::exception &err) { @@ -535,9 +677,7 @@ void EmbeddedPythonActor::act() { } catch (py::error_already_set &err) { err.restore(); error = err.what(); - auto py_stderr = - py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); return make_error(xstudio_error::error, err.what()); } catch (const std::exception &err) { @@ -546,9 +686,8 @@ void EmbeddedPythonActor::act() { send_output(this, event_group_, out, uuid); } catch (py::error_already_set &err) { err.restore(); - error = err.what(); - auto py_stderr = py::module::import("sys").attr("stderr").cast(); - py::print(error, "file"_a = py_stderr); + error = err.what(); + py_print(error); spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, "Python error", error); return make_error(xstudio_error::error, err.what()); } catch (const std::exception &err) { @@ -564,21 +703,96 @@ void EmbeddedPythonActor::act() { return result(jsn); }, + [=](utility::event_atom, + json_store::sync_atom, + const Uuid &uuid, + const JsonStore &event) { + if (uuid == data_uuid_) + data_.process_event(event, true, false, false); + }, + + [=](json_store::sync_atom) -> UuidVector { return UuidVector({data_uuid_}); }, + + [=](media::rescan_atom) { refresh_snippets(snippet_paths_); }, + + [=](save_atom, const caf::uri &path, const std::string &content) -> result { + auto result = true; + try { + std::ofstream myfile; + myfile.open(uri_to_posix_path(path)); + myfile << content << std::endl; + myfile.close(); + refresh_snippets(snippet_paths_); + result = true; + } catch (const std::exception &err) { + return make_error(xstudio_error::error, err.what()); + } + return result; + }, + + [=](json_store::sync_atom, const Uuid &uuid) -> JsonStore { + if (uuid == data_uuid_) + return data_.as_json(); + + return JsonStore(); + }, + + [=](json_store::update_atom, + const JsonStore & /*change*/, + const std::string & /*path*/, + const JsonStore &full) { + return mail(json_store::update_atom_v, full).delegate(actor_cast(this)); + }, + + [=](json_store::update_atom, const JsonStore &j) mutable { + try { + update_preferences(j); + } catch (...) { + } + }, + [=](bool) {}, + + [=](embedded_python::python_exec_atom, + const utility::Uuid &plugin_uuid, + const std::string method_name, + const utility::JsonStore &packed_args) -> result { + return make_error(xstudio_error::error, "Python Callbacks Not Implemented Yet"); + }, + + [=](embedded_python::python_eval_atom, utility::BlindDataObjectPtr lock) { + // another thread wants to run some python (see py_context.cpp) ... + // the EmbeddedPythonActor normally holds the GIL while it's + // waiting for messages to process here. + + // We will release the GIL now.... + py::gil_scoped_release release; + + // Now we try and acquire a lock on the mutex ... this was already + // locked by the other thread that wants to run pyton. This other + // thread will unlock the mutex when it has acquired the GIL, + // finished executing it's python, and then released the GIL at + // which point here we can continue + PythonThreadLocker *py_locker = const_cast( + dynamic_cast(lock.get())); + + // This thread will hang here until the other thread has finished + // its stuff + std::lock_guard l(py_locker->mutex_); + + // Note: a much nicer solution would be that when this loop is idle + // the GIL is released. Don't know how to do that one yet. + }, + [&](exit_msg &em) { if (em.reason) { if (base_.enabled()) base_.disconnect(); fail_state(std::move(em.reason)); running = false; + system().registry().erase(embedded_python_registry); } - }, - others >> [=](message &msg) -> skippable_result { - auto sender = caf::actor_cast(current_sender()); - base_.push_caf_message_to_py_callbacks(sender, msg); - return message{}; - - // return sec::unexpected_message; }); + base_.finalize(); } @@ -594,7 +808,14 @@ void EmbeddedPythonActor::init() { void EmbeddedPythonActor::join_broadcast(caf::actor broadcast_group) { - send(broadcast_group, broadcast::join_broadcast_atom_v, caf::actor_cast(this)); + mail(broadcast::join_broadcast_atom_v, caf::actor_cast(this)) + .send(broadcast_group); +} + +void EmbeddedPythonActor::leave_broadcast(caf::actor broadcast_group) { + + mail(broadcast::leave_broadcast_atom_v, caf::actor_cast(this)) + .send(broadcast_group); } void EmbeddedPythonActor::join_broadcast( @@ -602,15 +823,119 @@ void EmbeddedPythonActor::join_broadcast( auto plugin_manager = system().registry().template get(plugin_manager_registry); - request(plugin_manager, infinite, plugin_manager::spawn_plugin_base_atom_v, plugin_name) + mail(plugin_manager::spawn_plugin_base_atom_v, plugin_name) + .request(plugin_manager, infinite) .receive( [=](caf::actor plugin_actor) { - send( - plugin_actor, - broadcast::join_broadcast_atom_v, - caf::actor_cast(this)); + mail(broadcast::join_broadcast_atom_v, caf::actor_cast(this)) + .send(plugin_actor); }, [=](caf::error &e) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(e)); }); } + + +void EmbeddedPythonActor::update_preferences(const utility::JsonStore &j) { + auto paths = preference_value(j, "/core/python/snippets/path"); + auto snips = std::vector(); + + for (const auto &path : paths) + snips.emplace_back(posix_path_to_uri(expand_envvars(path))); + + if (snips != snippet_paths_) { + refresh_snippets(snips); + snippet_paths_ = snips; + } +} + + +void EmbeddedPythonActor::refresh_snippets(const std::vector &paths) { + auto data = R"([])"_json; + + for (const auto &i : paths) { + for (const auto &j : refresh_snippet(fs::path(uri_to_posix_path(i)), "Snippets")) + data.push_back(j); + } + + // sort results.. + std::sort( + data.begin(), data.end(), [](const nlohmann::json &a, const nlohmann::json &b) -> bool { + return std::string( + a.at("menu_path").get() + a.at("name").get()) < + std::string( + b.at("menu_path").get() + b.at("name").get()); + }); + + data_.reset_data(data); +} + +nlohmann::json EmbeddedPythonActor::refresh_snippet( + const std::filesystem::path &path, + const std::string &menu_path_, + const std::string &snippet_type) { + auto result = R"([])"_json; + + if (!fs::is_directory(path)) + return result; + + try { + for (const auto &entry : fs::directory_iterator(path)) { + if (fs::is_regular_file(entry.status()) and entry.path().extension() == ".py") { + auto item = + R"({"id": null, "type": "script", "snippet_type": null, "name": null, "script_path":null})"_json; + item["id"] = Uuid::generate(); + item["snippet_type"] = snippet_type.empty() ? "Application" : snippet_type; + item["name"] = title_case(entry.path().stem().string()); + item["script_path"] = to_string(posix_path_to_uri(entry.path().string())); + item["menu_path"] = menu_path_; + result.push_back(item); + } else if (fs::is_directory(entry.status())) { + auto change_type = snippet_type; + auto menu_path = + menu_path_.empty() + ? title_case(entry.path().stem().string()) + : menu_path_ + "|" + title_case(entry.path().stem().string()); + + if (change_type.empty()) { + if (entry.path().stem() == "media") { + change_type = "Media"; + menu_path = ""; + } else if (entry.path().stem() == "playlist") { + change_type = "Playlist"; + menu_path = ""; + } else if (entry.path().stem() == "sequence") { + change_type = "Sequence"; + menu_path = ""; + } else if (entry.path().stem() == "track") { + change_type = "Track"; + menu_path = ""; + } else if (entry.path().stem() == "clip") { + change_type = "Clip"; + menu_path = ""; + } else { + change_type = "Application"; + } + } + + for (const auto &i : refresh_snippet(entry.path(), menu_path, change_type)) + result.push_back(i); + + // auto item = R"({"id": null, "type": "script","name": null, + // "children":[]})"_json; item["id"] = Uuid::generate(); item["name"] = + // entry.path().stem().string(); item["children"] = + // refresh_snippet(entry.path()); result.push_back(item); + } + } + + } catch (const std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + return result; +} +void EmbeddedPythonActor::delayed_callback(utility::Uuid &cb_id, const int microseconds_delay) { + anon_mail(cb_id) + .delay(std::chrono::microseconds(microseconds_delay)) + .send(caf::actor_cast(this), weak_ref); +} diff --git a/src/embedded_python/test/CMakeLists.txt b/src/embedded_python/test/CMakeLists.txt index 104de9e7d..063013dfc 100644 --- a/src/embedded_python/test/CMakeLists.txt +++ b/src/embedded_python/test/CMakeLists.txt @@ -1,5 +1,7 @@ include(CTest) + +find_package(Python COMPONENTS Interpreter Development) find_package(pybind11 REQUIRED) # or `add_subdirectory(pybind11)` find_package(spdlog REQUIRED) find_package(fmt REQUIRED) @@ -15,6 +17,7 @@ target_link_libraries(embedded_python_test xstudio::embedded_python pybind11::embed xstudio::utility + xstudio::media ${GTEST_LDFLAGS} ) add_test(embedded_python_tests embedded_python_test) @@ -29,6 +32,7 @@ target_link_libraries(embedded_python_actor_test xstudio::embedded_python pybind11::embed xstudio::utility + xstudio::media ${GTEST_LDFLAGS} ) add_test(embedded_python_tests embedded_python_actor_test) diff --git a/src/event/src/CMakeLists.txt b/src/event/src/CMakeLists.txt deleted file mode 100644 index 27ecd8f77..000000000 --- a/src/event/src/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -SET(LINK_DEPS - xstudio::utility - caf::core -) - -create_component(event 0.1.0 "${LINK_DEPS}") \ No newline at end of file diff --git a/src/event/src/event.cpp b/src/event/src/event.cpp deleted file mode 100644 index 2c4934c6a..000000000 --- a/src/event/src/event.cpp +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "xstudio/atoms.hpp" -#include "xstudio/event/event.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" -#include "xstudio/utility/string_helpers.hpp" - -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::event; -using namespace caf; diff --git a/src/event/test/CMakeLists.txt b/src/event/test/CMakeLists.txt deleted file mode 100644 index c183c9f39..000000000 --- a/src/event/test/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -include(CTest) - -SET(LINK_DEPS - xstudio::event -) - -create_tests("${LINK_DEPS}") - - diff --git a/src/event/test/event_test.cpp b/src/event/test/event_test.cpp deleted file mode 100644 index 544881d06..000000000 --- a/src/event/test/event_test.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include - -#include "xstudio/event/event.hpp" -#include "xstudio/utility/json_store.hpp" - -using namespace xstudio::utility; -using namespace xstudio::event; - - -TEST(EventTest, Test) { - auto e = Event("Test"); - - EXPECT_EQ(e.progress_text(), "Test"); - EXPECT_EQ(e.progress_text_percentage(), "Test"); - EXPECT_EQ(e.progress_text_range(), "Test"); - - e = Event("Test {}"); - - EXPECT_EQ(e.progress_text(), "Test {}"); - EXPECT_EQ(e.progress_text_percentage(), "Test 0.0%"); - EXPECT_EQ(e.progress_text_range(), "Test 0/100"); - - EXPECT_FALSE(e.contains(Uuid())); - - e.set_progress_maximum(100); - EXPECT_EQ(e.progress_text_percentage(), "Test 0.0%"); - e.set_progress(1); - EXPECT_EQ(e.progress_text_percentage(), "Test 1.0%"); - e.set_progress(100); - EXPECT_EQ(e.progress_text_percentage(), "Test 100.0%"); - EXPECT_TRUE(e.complete()); - - e.set_progress_minimum(0); - e.set_progress_maximum(20); - e.set_complete(); - EXPECT_EQ(e.progress_percentage(), 100.0); -} diff --git a/src/global/src/CMakeLists.txt b/src/global/src/CMakeLists.txt index 4f3da75c4..ebb7dd934 100644 --- a/src/global/src/CMakeLists.txt +++ b/src/global/src/CMakeLists.txt @@ -1,29 +1,37 @@ project(global VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) -find_package(pybind11 REQUIRED) # or `add_subdirectory(pybind11)` + +find_package(Python COMPONENTS Interpreter Development) +find_package(pybind11 CONFIG) # or `add_subdirectory(pybind11)` +find_package(fmt REQUIRED) set(SOURCES global_actor.cpp + xstudio_actor_system.cpp ) add_library(${PROJECT_NAME} SHARED ${SOURCES}) add_library(xstudio::global ALIAS ${PROJECT_NAME}) default_options(${PROJECT_NAME}) + if(UNIX) -target_compile_options(${PROJECT_NAME} - PRIVATE -fvisibility=hidden -) + # Note: this is causing link errors. What was it for? (Ted) + #target_compile_options(${PROJECT_NAME} + # PRIVATE -fvisibility=hidden + #) endif() set_python_to_proper_build_type() target_link_libraries(${PROJECT_NAME} PUBLIC + xstudio::bookmark xstudio::module xstudio::audio_output + xstudio::caf_utility xstudio::conform xstudio::embedded_python - xstudio::event xstudio::global_store + xstudio::media xstudio::media_cache xstudio::media_cache xstudio::media_hook @@ -34,15 +42,15 @@ target_link_libraries(${PROJECT_NAME} xstudio::scanner xstudio::session xstudio::studio - xstudio::sync xstudio::thumbnail xstudio::ui::model_data xstudio::ui::viewport xstudio::utility - caf::core - caf::io + CAF::core + CAF::io + fmt::fmt PRIVATE - pybind11::embed + Python::Python ) if(UNIX AND NOT APPLE) diff --git a/src/global/src/global_actor.cpp b/src/global/src/global_actor.cpp index b53db7dbe..c83a4d669 100644 --- a/src/global/src/global_actor.cpp +++ b/src/global/src/global_actor.cpp @@ -1,14 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 #include #include -#include +#include +#include #include #include - #include "xstudio/atoms.hpp" -#include "xstudio/audio/audio_output_actor.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" #include "xstudio/colour_pipeline/colour_cache_actor.hpp" #include "xstudio/colour_pipeline/colour_pipeline_actor.hpp" @@ -17,28 +16,28 @@ #include "xstudio/global/global_actor.hpp" #include "xstudio/global_store/global_store.hpp" #include "xstudio/global_store/global_store_actor.hpp" +#include "xstudio/media/media_metadata_manager_actor.hpp" #include "xstudio/media_cache/media_cache_actor.hpp" #include "xstudio/media_hook/media_hook_actor.hpp" #include "xstudio/media_metadata/media_metadata_actor.hpp" #include "xstudio/media_reader/media_reader_actor.hpp" -#include "xstudio/module/global_module_events_actor.hpp" #include "xstudio/playhead/playhead_global_events_actor.hpp" #include "xstudio/plugin_manager/plugin_manager_actor.hpp" #include "xstudio/scanner/scanner_actor.hpp" #include "xstudio/studio/studio_actor.hpp" -#include "xstudio/sync/sync_actor.hpp" #include "xstudio/thumbnail/thumbnail_manager_actor.hpp" #include "xstudio/ui/model_data/model_data_actor.hpp" #include "xstudio/ui/viewport/keypress_monitor.hpp" +#include "xstudio/ui/viewport/viewport_layout_plugin.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" // include for system (soundcard) audio output #ifdef __linux__ #include "xstudio/audio/linux_audio_output_device.hpp" -#elif __APPLE__ -// TO DO -#elif _WIN32 +#elif defined(__apple__) +#include "xstudio/audio/macos_audio_output_device.hpp" +#elif defined(_WIN32) #include "xstudio/audio/windows_audio_output_device.hpp" #endif @@ -48,11 +47,93 @@ using namespace xstudio::global; using namespace xstudio::utility; using namespace xstudio::global_store; -GlobalActor::GlobalActor(caf::actor_config &cfg, const utility::JsonStore &prefs) +APIActor::APIActor(caf::actor_config &cfg, const caf::actor &global) + : caf::event_based_actor(cfg), global_(global) { + + spdlog::debug("Created APIActor"); + print_on_exit(this, "APIActor"); + + behavior_.assign( + make_get_version_handler(), + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + + [=](get_application_mode_atom) -> result { + // don't expose global.. + auto rp = make_response_promise(); + + mail(get_application_mode_atom_v) + .request(global_, infinite) + .then( + [=](const std::string &result) mutable { rp.deliver(result); }, + [=](caf::error &err) mutable { rp.deliver(err); }); + + return rp; + }, + + [=](authenticate_atom) -> result { + if (allow_unauthenticated_ or + (current_sender() and current_sender()->node() == node())) + return global_; + + return make_error(xstudio_error::error, "Authentication required."); + }, + + [=](authenticate_atom, const std::string &key) -> result { + if (allow_unauthenticated_) + return global_; + else + for (const auto &i : authentication_keys_) + if (key == i) + return global_; + + return make_error(xstudio_error::error, "Authentication failed."); + }, + + [=](authenticate_atom, + const std::string &user, + const std::string &pass) -> result { + if (allow_unauthenticated_) + return global_; + else + for (const auto &i : authentication_passwords_) + if (user == i.at("user") and pass == i.at("password")) + return global_; + + return make_error(xstudio_error::error, "Authentication failed."); + }, + + [=](json_store::update_atom, + const JsonStore & /*change*/, + const std::string & /*path*/, + const JsonStore &full) { + return mail(json_store::update_atom_v, full).delegate(actor_cast(this)); + }, + + [=](json_store::update_atom, const JsonStore &j) mutable { + try { + allow_unauthenticated_ = + preference_value(j, "/core/api/authentication/allow_unauthenticated"); + + authentication_passwords_ = + preference_value(j, "/core/api/authentication/passwords"); + + authentication_keys_ = + preference_value(j, "/core/api/authentication/keys"); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + }); +} + +GlobalActor::GlobalActor( + caf::actor_config &cfg, const utility::JsonStore &prefs, const bool embedded_python) : caf::event_based_actor(cfg), rsm_(remote_session_path()) { - init(prefs); + init(prefs, embedded_python); } +GlobalActor::~GlobalActor() {} + int GlobalActor::publish_port( const int minimum, const int maximum, const std::string &bind_address, caf::actor a) { int port = -1; @@ -69,8 +150,7 @@ int GlobalActor::publish_port( return port; } - -void GlobalActor::init(const utility::JsonStore &prefs) { +void GlobalActor::init(const utility::JsonStore &prefs, const bool embedded_python) { // launch global actors.. // preferences first.. // this will need more configuration @@ -81,21 +161,20 @@ void GlobalActor::init(const utility::JsonStore &prefs) { // spawning the 'GlobalModuleAttrEventsActor' first because subsequent // actors might want to connect with it on creation .. see Module::connect_to_ui() - auto attr_evs = spawn(); - auto gsa = caf::actor(); - if (prefs.is_null()) { - gsa = spawn( + gsa_ = spawn( "GlobalStore", global_store::global_store_builder( - std::vector{xstudio_root("/preference")})); + std::vector{xstudio_resources_dir("preference")})); } else { - gsa = spawn("GlobalStore", prefs); + gsa_ = spawn("GlobalStore", prefs); } - auto sga = spawn(); - auto sgma = spawn(); + auto phev = spawn(); + auto keyboard_events = spawn(); auto ui_models = spawn(); + auto metadata_mgr = spawn(); + auto audio = spawn(); auto pm = spawn(); auto colour = spawn(); auto gmma = spawn(); @@ -105,14 +184,12 @@ void GlobalActor::init(const utility::JsonStore &prefs) { auto gcca = spawn(); auto gmha = spawn(); auto thumbnail = spawn(); - auto keyboard_events = spawn(); - auto audio = spawn(); - auto phev = spawn(); - auto pa = spawn("Python"); - auto scanner = spawn(); - auto conform = spawn(); + auto pa = + embedded_python ? spawn("Python") : caf::actor(); + auto scanner = spawn(); + auto conform = spawn(); + auto vpmgr = spawn(); - link_to(attr_evs); link_to(audio); link_to(colour); link_to(conform); @@ -122,29 +199,33 @@ void GlobalActor::init(const utility::JsonStore &prefs) { link_to(gmha); link_to(gmma); link_to(gmra); - link_to(gsa); link_to(keyboard_events); - link_to(pa); + if (embedded_python) + link_to(pa); link_to(phev); link_to(pm); link_to(scanner); - link_to(sga); - link_to(sgma); + link_to(metadata_mgr); link_to(thumbnail); link_to(ui_models); - + link_to(vpmgr); // Make default audio output #ifdef __linux__ - auto audio_out = spawn>(); - link_to(audio_out); -#elif __APPLE__ - // TO DO -#elif _WIN32 - auto audio_out = spawn>(); - link_to(audio_out); + auto audio_out = spawn_audio_output_actor(prefs); +#endif + +#ifdef _WIN32 + auto audio_out = spawn_audio_output_actor(prefs); +#endif + +#ifdef __apple__ + auto audio_out = spawn_audio_output_actor(prefs); #endif + system().registry().put(pc_audio_output_registry, audio_out); + link_to(audio_out); + python_enabled_ = false; connected_ = false; api_enabled_ = false; @@ -153,23 +234,30 @@ void GlobalActor::init(const utility::JsonStore &prefs) { port_maximum_ = 12345; bind_address_ = "127.0.0.1"; - sync_connected_ = false; - sync_api_enabled_ = false; - sync_port_ = -1; - sync_port_minimum_ = 12346; - sync_port_maximum_ = 12346; - sync_bind_address_ = "127.0.0.1"; - event_group_ = spawn(this); link_to(event_group_); + apia_ = spawn(this); + try { - auto prefs = GlobalStoreHelper(system()); JsonStore j; + auto prefs = GlobalStoreHelper(system()); join_broadcast(this, prefs.get_group(j)); // spawn resident plugins (application level) - anon_send(pm, plugin_manager::spawn_plugin_atom_v, j); - anon_send(this, json_store::update_atom_v, j); + anon_mail(plugin_manager::spawn_plugin_atom_v, j).send(pm); + anon_mail(json_store::update_atom_v, j).send(apia_); + anon_mail(json_store::update_atom_v, j).send(this); + + // here we set-up filepath remapping (only once here, at start-up) + file_map_regex_["/core/media_reader/filepath_map_regex_replace"] = + prefs.value("/core/media_reader/filepath_map_regex_replace"); + + file_map_regex_["/core/media_reader/user_filepath_map_regex_replace"] = + prefs.value( + "/core/media_reader/user_filepath_map_regex_replace"); + + utility::setup_filepath_remap_regex(file_map_regex_); + } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -188,24 +276,25 @@ void GlobalActor::init(const utility::JsonStore &prefs) { [=](busy_atom, const bool value) mutable { if (value) { busy_.insert(caf::actor_cast(current_sender())); - delegate( - actor_cast(this), status_atom_v, StatusType::ST_BUSY, true); + return mail(status_atom_v, StatusType::ST_BUSY, true) + .delegate(actor_cast(this)); } else { busy_.erase(caf::actor_cast(current_sender())); if (busy_.empty()) { - delegate( - actor_cast(this), - status_atom_v, - StatusType::ST_BUSY, - false); + return mail(status_atom_v, StatusType::ST_BUSY, false) + .delegate(actor_cast(this)); } else { - delegate( - actor_cast(this), status_atom_v, StatusType::ST_BUSY, true); + return mail(status_atom_v, StatusType::ST_BUSY, true) + .delegate(actor_cast(this)); } } }, - [=](get_actor_from_registry_atom, std::string actor_name) -> result { + [=](history::log_atom, const spdlog::level::level_enum level, const std::string &log) { + spdlog::log(level, log); + }, + + [=](get_actor_from_registry_atom, const std::string &actor_name) -> result { try { return system().registry().template get(actor_name); } catch (std::exception &e) { @@ -220,7 +309,7 @@ void GlobalActor::init(const utility::JsonStore &prefs) { status_ = status_ | field; else status_ = status_ & ~field; - send(event_group_, utility::event_atom_v, status_atom_v, status_); + mail(utility::event_atom_v, status_atom_v, status_).send(event_group_); return status_; }, @@ -228,24 +317,22 @@ void GlobalActor::init(const utility::JsonStore &prefs) { [=](global_store::autosave_atom, const bool enable) { // if (enable != session_autosave_) - // send(event_group_, utility::event_atom_v, autosave_atom_v, enable); + // mail(utility::event_atom_v, autosave_atom_v, enable).send(event_group_); if (session_autosave_ != enable) { session_autosave_ = enable; if (session_autosave_) - delayed_anon_send( - actor_cast(this), - std::chrono::seconds(session_autosave_interval_), - global_store::do_autosave_atom_v); + anon_mail(global_store::do_autosave_atom_v) + .delay(std::chrono::seconds(session_autosave_interval_)) + .send(actor_cast(this), weak_ref); } }, [=](global_store::do_autosave_atom) { // trigger next autosave if (session_autosave_) - delayed_anon_send( - actor_cast(this), - std::chrono::seconds(session_autosave_interval_), - global_store::do_autosave_atom_v); + anon_mail(global_store::do_autosave_atom_v) + .delay(std::chrono::seconds(session_autosave_interval_)) + .send(actor_cast(this), weak_ref); if (session_autosave_path_.empty()) { spdlog::warn("Autosave path not set, autosave skipped."); } else { @@ -253,11 +340,13 @@ void GlobalActor::init(const utility::JsonStore &prefs) { spdlog::debug("Skipping autosave whilst playing."); } else { // get session actor - request(studio_, infinite, session::session_atom_v) + mail(session::session_atom_v) + .request(studio_, infinite) .then( [=](caf::actor session) { // request path from session - request(session, infinite, session::path_atom_v) + mail(session::path_atom_v) + .request(session, infinite) .then( [=](const std::pair &path_time) { @@ -297,13 +386,13 @@ void GlobalActor::init(const utility::JsonStore &prefs) { saves.erase(saves.begin()); } - request( - session, - infinite, + mail( + global_store::save_atom_v, posix_path_to_uri(fspath.string()), session_autosave_hash_, false) + .request(session, infinite) .then( [=](const size_t hash) { if (hash != @@ -314,7 +403,8 @@ void GlobalActor::init(const utility::JsonStore &prefs) { auto prefs = global_store:: GlobalStoreHelper(system()); prefs.set_value( - fspath.string(), + to_string(posix_path_to_uri( + fspath.string())), "/core/session/autosave/" "last_auto_save"); prefs.save("APPLICATION"); @@ -361,8 +451,6 @@ void GlobalActor::init(const utility::JsonStore &prefs) { return studio_; }, - [=](get_api_mode_atom) -> std::string { return "FULL"; }, - [=](get_application_mode_atom) -> std::string { return (ui_studio_ ? "XSTUDIO_GUI" : "XSTUDIO"); }, @@ -371,7 +459,7 @@ void GlobalActor::init(const utility::JsonStore &prefs) { // 'colour' is the colour pipeline manager. To get to the // actual colour pipelin actor (OCIO plugin) we delegate to // the manager. Getting to the manager alon is not interesting. - delegate(colour, atom); + return mail(atom).delegate(colour); }, [=](get_global_audio_cache_atom) -> caf::actor { return gaca; }, @@ -381,7 +469,7 @@ void GlobalActor::init(const utility::JsonStore &prefs) { [=](get_global_playhead_events_atom) -> caf::actor { return phev; }, - [=](get_global_store_atom) -> caf::actor { return gsa; }, + [=](get_global_store_atom) -> caf::actor { return gsa_; }, [=](get_global_thumbnail_atom) -> caf::actor { return thumbnail; }, @@ -391,14 +479,23 @@ void GlobalActor::init(const utility::JsonStore &prefs) { [&](get_studio_atom) -> caf::actor { return studio_; }, [=](json_store::update_atom, - const JsonStore & /*change*/, - const std::string & /*path*/, + const JsonStore &change, + const std::string &path, const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + // THe 'user_filepath_map_regex_replace' preference can change during the + // session. Here we update the regex list that can re-map filepaths on-the-fly + if (path == "/core/media_reader/user_filepath_map_regex_replace/value") { + file_map_regex_["/core/media_reader/user_filepath_map_regex_replace"] = change; + utility::setup_filepath_remap_regex(file_map_regex_); + } else { + std::ignore = mail(json_store::update_atom_v, full) + .delegate(actor_cast(this)); + } }, - [=](json_store::update_atom, const JsonStore &j) mutable { + anon_mail(json_store::update_atom_v, j).send(apia_); + try { python_enabled_ = preference_value(j, "/core/python/enabled"); api_enabled_ = preference_value(j, "/core/api/enabled"); @@ -406,14 +503,9 @@ void GlobalActor::init(const utility::JsonStore &prefs) { port_maximum_ = preference_value(j, "/core/api/port_maximum"); bind_address_ = preference_value(j, "/core/api/bind_address"); - sync_api_enabled_ = preference_value(j, "/core/sync/enabled"); - sync_port_minimum_ = preference_value(j, "/core/sync/port_minimum"); - sync_port_maximum_ = preference_value(j, "/core/sync/port_maximum"); - sync_bind_address_ = - preference_value(j, "/core/sync/bind_address"); - session_autosave_interval_ = preference_value(j, "/core/session/autosave/interval"); + try { session_autosave_path_ = posix_path_to_uri(expand_envvars( preference_value(j, "/core/session/autosave/path"))); @@ -425,22 +517,20 @@ void GlobalActor::init(const utility::JsonStore &prefs) { preference_value(j, "/core/session/autosave/enabled"); if (session_autosave != session_autosave_) { - anon_send( - actor_cast(this), - global_store::autosave_atom_v, - session_autosave); + anon_mail(global_store::autosave_atom_v, session_autosave) + .send(actor_cast(this)); } disconnect_api(pa); - disconnect_sync_api(); connect_api(pa); - connect_sync_api(); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } }, - [=](last_changed_atom atom, const time_point &stp) { delegate(studio_, atom, stp); }, + [=](last_changed_atom atom, const time_point &stp) { + return mail(atom, stp).delegate(studio_); + }, [=](remote_session_name_atom, const std::string &session_name) { if (not session_name.empty() and session_name != remote_api_session_name_) { @@ -449,7 +539,7 @@ void GlobalActor::init(const utility::JsonStore &prefs) { rsm_.remove_session(remote_api_session_name_); remote_api_session_name_ = session_name; rsm_.create_session_file( - port_, false, remote_api_session_name_, "localhost", true); + port_, remote_api_session_name_, "localhost", true); spdlog::info( "API enabled on {}:{}, session name {}", @@ -464,43 +554,47 @@ void GlobalActor::init(const utility::JsonStore &prefs) { } }, - [=](session::session_atom _atom) { delegate(studio_, _atom); }, - - [=](session::session_atom _atom, caf::actor actor) { delegate(studio_, _atom, actor); }, + [=](session::session_atom _atom) { return mail(_atom).delegate(studio_); }, - [=](session::session_request_atom _atom, const std::string &path, const JsonStore &js) { - delegate(studio_, _atom, path, js); + [=](session::session_atom _atom, caf::actor actor) { + return mail(_atom, actor).delegate(studio_); }, - [=](bookmark::get_bookmark_atom atom) { delegate(studio_, atom); }, + [=](bookmark::get_bookmark_atom atom) { return mail(atom).delegate(studio_); } - [=](sync::get_sync_atom _atm) { delegate(sgma, _atm); }); + ); } void GlobalActor::on_exit() { // shutdown // clear autosave. - send(event_group_, exit_atom_v); + mail(exit_atom_v).send(event_group_); + + // this will not work as global store is already gone. auto prefs = global_store::GlobalStoreHelper(system()); - prefs.set_value("", "/core/session/autosave/last_auto_save", false); prefs.save("APPLICATION"); + if (system().has_middleman()) { - system().middleman().unpublish(actor_cast(this), port_); - system().middleman().unpublish(actor_cast(this), sync_port_); + system().middleman().unpublish(apia_, port_); } + send_exit(apia_, caf::exit_reason::user_shutdown); + send_exit(gsa_, caf::exit_reason::user_shutdown); + gsa_ = caf::actor(); + apia_ = caf::actor(); + system().registry().erase(global_registry); + system().registry().erase(pc_audio_output_registry); } void GlobalActor::connect_api(const caf::actor &embedded_python) { if (not connected_ and api_enabled_) { - port_ = publish_port(port_minimum_, port_maximum_, bind_address_, this); + port_ = publish_port(port_minimum_, port_maximum_, bind_address_, apia_); if (port_ != -1) { rsm_.remove_session(remote_api_session_name_); if (remote_api_session_name_.empty()) - remote_api_session_name_ = rsm_.create_session_file(port_, false); + remote_api_session_name_ = rsm_.create_session_file(port_); else - rsm_.create_session_file( - port_, false, remote_api_session_name_, "localhost", true); + rsm_.create_session_file(port_, remote_api_session_name_, "localhost", true); spdlog::info( "API enabled on {}:{}, session name {}", bind_address_, @@ -508,9 +602,8 @@ void GlobalActor::connect_api(const caf::actor &embedded_python) { remote_api_session_name_); connected_ = true; if (python_enabled_) { - // request(pa, infinite, connect_atom_v, - // actor_cast(this)).then( - request(embedded_python, infinite, connect_atom_v, port_) + mail(connect_atom_v, port_) + .request(embedded_python, infinite) .then( [=](const bool result) { if (result) @@ -532,39 +625,13 @@ void GlobalActor::connect_api(const caf::actor &embedded_python) { } } -void GlobalActor::connect_sync_api() { - if (not sync_connected_ and sync_api_enabled_) { - sync_port_ = - publish_port(sync_port_minimum_, sync_port_maximum_, sync_bind_address_, this); - if (sync_port_ != -1) { - rsm_.remove_session(remote_sync_session_name_); - if (remote_sync_session_name_.empty()) - remote_sync_session_name_ = rsm_.create_session_file(sync_port_, true); - else - rsm_.create_session_file( - sync_port_, true, remote_sync_session_name_, "localhost", true); - spdlog::info( - "SYNC API enabled on {}:{}, session name {}", - sync_bind_address_, - sync_port_, - remote_sync_session_name_); - sync_connected_ = true; - } else { - spdlog::warn( - "SYNC API failed to open {}:{}-{}", - sync_bind_address_, - sync_port_minimum_, - sync_port_maximum_); - } - } -} - void GlobalActor::disconnect_api(const caf::actor &embedded_python, const bool force) { if (connected_ and (force or not api_enabled_ or port_ > port_maximum_ or port_ < port_minimum_)) { connected_ = false; if (system().has_middleman() and python_enabled_) { - request(embedded_python, infinite, connect_atom_v, 0) + mail(connect_atom_v, 0) + .request(embedded_python, infinite) .then( [=](const bool result) { if (result) @@ -575,27 +642,13 @@ void GlobalActor::disconnect_api(const caf::actor &embedded_python, const bool f [=](const error &err) { spdlog::warn("Disconnected failed {}.", to_string(err)); }); - send(event_group_, api_exit_atom_v); + mail(api_exit_atom_v).send(event_group_); // wait..? - system().middleman().unpublish(actor_cast(this), port_); + system().middleman().unpublish(apia_, port_); rsm_.remove_session(remote_api_session_name_); spdlog::info("API disabled on port {}", port_); } port_ = -1; } } - -void GlobalActor::disconnect_sync_api(const bool force) { - if (sync_connected_ and - (force or not sync_api_enabled_ or sync_port_ > sync_port_maximum_ or - sync_port_ < sync_port_minimum_)) { - sync_connected_ = false; - if (system().has_middleman()) { - system().middleman().unpublish(actor_cast(this), sync_port_); - rsm_.remove_session(remote_sync_session_name_); - spdlog::info("SYNC API disabled on port {}", sync_port_); - } - sync_port_ = -1; - } -} diff --git a/src/global/src/xstudio_actor_system.cpp b/src/global/src/xstudio_actor_system.cpp new file mode 100644 index 000000000..ea79693c8 --- /dev/null +++ b/src/global/src/xstudio_actor_system.cpp @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#ifdef __apple__ +namespace caf { +namespace detail { + + template <> struct int_types_by_size<16> { + using unsigned_type = __uint128_t; + using signed_type = __int128; + }; + +} // namespace detail +} // namespace caf +#endif + +#include "xstudio/atoms.hpp" +#include "xstudio/bookmark/bookmark.hpp" +#include "xstudio/global/global_actor.hpp" +#include "xstudio/global/xstudio_actor_system.hpp" +#include "xstudio/global_store/global_store.hpp" +#include "xstudio/timeline/clip_actor.hpp" +#include "xstudio/timeline/track_actor.hpp" +#include "xstudio/timeline/gap_actor.hpp" +#include "xstudio/timeline/stack_actor.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/caf_helpers.hpp" +#include "xstudio/conform/conformer.hpp" +#include "xstudio/media_reader/audio_buffer.hpp" +#include "xstudio/media_reader/image_buffer.hpp" +#include "xstudio/shotgun_client/shotgun_client.hpp" +#include "xstudio/thumbnail/thumbnail.hpp" + +using namespace xstudio; + +static std::shared_ptr s_instance_ = nullptr; + +struct ExitTimeoutKiller { + + void start() { +#ifdef _WIN32 + spdlog::debug("ExitTimeoutKiller start ignored"); + } +#else + + + // lock the mutex ... + clean_actor_system_exit.lock(); + + // .. and start a thread to watch the mutex + exit_timeout = std::thread([&]() { + // wait for stop() to be called - 10s + if (!clean_actor_system_exit.try_lock_for(std::chrono::seconds(20))) { + // stop() wasn't called! Probably failed to exit actor_system, + // see main() function. Kill process. + spdlog::critical("xSTUDIO has not exited cleanly: killing process now"); + kill(0, SIGKILL); + } else { + clean_actor_system_exit.unlock(); + } + }); + } +#endif + + void stop() { +#ifdef _WIN32 + spdlog::debug("ExitTimeoutKiller stop ignored"); + } +#else + // unlock the mutex so exit_timeout won't time-out + clean_actor_system_exit.unlock(); + if (exit_timeout.joinable()) + exit_timeout.join(); + } + + std::timed_mutex clean_actor_system_exit; + std::thread exit_timeout; +#endif +}; + + +CafActorSystem *CafActorSystem::instance() { + if (!s_instance_) { + s_instance_.reset(new CafActorSystem()); + } + return s_instance_.get(); +} + +void CafActorSystem::exit() { + if (s_instance_) { + s_instance_.reset(); + } +} + +CafActorSystem::CafActorSystem() { + + ACTOR_INIT_GLOBAL_META() + core::init_global_meta_objects(); + io::middleman::init_global_meta_objects(); + + // The Buffer class uses a singleton instance of ImageBufferRecyclerCache + // which is held as a static shared ptr. Buffers access this when they are + // destroyed. This static shared ptr is part of the media_reader component + // but buffers might be cleaned up (destroyed) after the media_reader component + // is cleaned up on exit. So we make a copy here to ensure the ImageBufferRecyclerCache + // instance outlives any Buffer objects. + + // auto buffer_cache_handle = media_reader::Buffer::s_buf_cache; + + // As far as I can tell caf only allows config to be modified + // through cli args. We prefer the 'sharing' policy rather than + // 'stealing'. The latter results in multiple threads spinning + // aggressively pushing CPU load very high during playback. + + // const char *args[] = {argv[0], + // "--caf.work-stealing.aggressive-poll-attempts=0","--caf.logger.console.verbosity=trace"}; + + // # file { + // # # File name template for output log files. + // # path = "actor_log_[PID]_[TIMESTAMP]_[NODE].log" + // # # Format for rendering individual log file entries. + // # format = "%r %c %p %a %t %M %F:%L %m%n" + // # # Minimum severity of messages that are written to the log. One of: + // # # 'quiet', 'error', 'warning', 'info', 'debug', or 'trace'. + // # verbosity = "trace" + // # # A list of components to exclude in file output. + // # excluded-components = [] + // # }() + + // if debugging actor lifetimes + // const char *args[] = { + // argv[0], + // "--caf.scheduler.max-threads=128", + // "--caf.scheduler.policy=sharing", + // "--caf.logger.console.verbosity=warn", + // "--caf.logger.file.verbosity=debug", + // "--caf.logger.file.format=%r|%c|%p|%a|%t|%M|%F:%L|%m%n" + // }; + + // caf_config config{6, const_cast(args)}; + const char *args[] = { + "xstudio", + "--caf.scheduler.max-threads=128", + "--caf.scheduler.policy=sharing", + "--caf.logger.console.verbosity=warn"}; + + config_ = new caf_utility::caf_config{4, const_cast(args)}; + + config_->add_actor_type< + timeline::GapActor, + const std::string &, + const utility::FrameRateDuration &>("Gap"); + config_->add_actor_type("Stack"); + config_ + ->add_actor_type( + "Clip"); + config_->add_actor_type< + timeline::TrackActor, + const std::string &, + const utility::FrameRate &, + const media::MediaType &>("Track"); + + // create the actor system + the_system_ = new caf::actor_system(*config_); + + // store a reference to the actor system, so we can access it + // via static method anywhere else we need to (mainly, the python + // module instanced in the embedded python interpreter) + utility::ActorSystemSingleton::actor_system_ref(*the_system_); +} + +caf::actor CafActorSystem::__global_actor( + const std::string &name, const utility::JsonStore &prefs, const bool embedded_python) { + // here we create a new global actor or return the existing one if it has + // already been created + if (!global_actor_) { + caf::scoped_actor self{*(the_system_)}; + if (prefs.is_null()) { + utility::JsonStore basic_prefs; + if (!global_store::load_preferences(basic_prefs)) { + return caf::actor(); + } + global_actor_ = self->spawn(basic_prefs, embedded_python); + } else { + global_actor_ = self->spawn(prefs, embedded_python); + } + + // at this stage we ensure that a 'studio' actor (that manages sessions) + // exists + utility::request_receive( + *self, global_actor_, global::create_studio_atom_v, "XStudio"); + } + return global_actor_; +} + +CafActorSystem::~CafActorSystem() { + + ExitTimeoutKiller exit_timeout_killer; + exit_timeout_killer.start(); + + if (global_actor_) { + caf::scoped_actor self{*(the_system_)}; + self->send_exit(global_actor_, caf::exit_reason::user_shutdown); + global_actor_ = caf::actor(); + } + + // Uncomment to help debug case where shutdown is not clean and + // actors are not exiting +#ifdef NO_OP +// TO DO - Windows build not exiting cleanly. Need to fix. + while (the_system_->registry().running()) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + for (auto &i : the_system_->registry().named_actors()) { + spdlog::warn("{}", i.first); + } + spdlog::warn("running in registry {}", the_system_->registry().running()); + spdlog::warn("running actors {}", the_system_->base_metrics().running_actors->value()); + static int ct = 0; + if (ct > 10) { + std::exit(EXIT_SUCCESS); + } + ct++; + } +#endif + + // The caf actors should only clear up when they have completed doing any + // work still pending. For example, saving a session or saving the current + // state of the user prefs to the filesystem may still be happnening when + // we get to this destructor. + + // BUT - if actor cleanup fails with a dangling actor pointer, we will + // hang indefinitely on the next line. This is a bad situation and should + // always be addressed by developers if it's happening. However, the + // ExitTimeoutKiller will ensure that the zombie xstudio process won't hang + // around if such a bug does occur. + + delete the_system_; + delete config_; + exit_timeout_killer.stop(); +} \ No newline at end of file diff --git a/src/global/test/CMakeLists.txt b/src/global/test/CMakeLists.txt index 66ed913ca..cbff47804 100644 --- a/src/global/test/CMakeLists.txt +++ b/src/global/test/CMakeLists.txt @@ -2,7 +2,7 @@ include(CTest) SET(LINK_DEPS xstudio::global - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/global_store/src/CMakeLists.txt b/src/global_store/src/CMakeLists.txt index 1d8c95e04..1c5f64281 100644 --- a/src/global_store/src/CMakeLists.txt +++ b/src/global_store/src/CMakeLists.txt @@ -1,11 +1,11 @@ SET(LINK_DEPS xstudio::json_store - caf::core + CAF::core ) SET(STATIC_LINK_DEPS xstudio::json_store_static - caf::core + CAF::core ) if(UNIX AND NOT APPLE) @@ -13,4 +13,4 @@ if(UNIX AND NOT APPLE) list(APPEND STATIC_LINK_DEPS stdc++fs) endif() -create_component_static(global_store 0.1.0 "${LINK_DEPS}" "${STATIC_LINK_DEPS}") +create_component_static(global_store ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${STATIC_LINK_DEPS}") \ No newline at end of file diff --git a/src/global_store/src/global_store.cpp b/src/global_store/src/global_store.cpp index 5b6d583d4..80114bcde 100644 --- a/src/global_store/src/global_store.cpp +++ b/src/global_store/src/global_store.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "xstudio/atoms.hpp" #include "xstudio/global_store/global_store.hpp" @@ -21,8 +22,9 @@ namespace fs = std::filesystem; GlobalStoreHelper::GlobalStoreHelper(caf::actor_system &sys, const std::string ®_name) : JsonStoreHelper(sys, caf::actor()) { auto gs_actor = sys.registry().get(reg_name); - if (not gs_actor) + if (not gs_actor) { throw std::runtime_error("GlobalStore is not registered"); + } store_actor_ = caf::actor_cast(gs_actor); } @@ -65,12 +67,59 @@ void xstudio::global_store::set_global_store_def( js.set(static_cast(gsd), static_cast(gsd)); } +bool xstudio::global_store::load_preferences( + utility::JsonStore &prefs, + const bool load_user_prefs, + const std::vector &extra_prefs_paths, + const std::vector &override_prefs_paths) { + + // load application default prefs. These prefs must exist for xstudio to + // work! If this fails, app should exit. + if (not preference_load_defaults(prefs, xstudio_resources_dir("preference"))) { + spdlog::error( + "Failed to load application preferences {}", xstudio_resources_dir("preference")); + return false; + } + + // prefs files *might* be located in a 'preference' subfolder under XSTUDIO_PLUGIN_PATH + // folders + char *plugin_path = std::getenv("XSTUDIO_PLUGIN_PATH"); + if (plugin_path) { + for (const auto &p : xstudio::utility::split(plugin_path, ':')) { + if (fs::is_directory(p + "/preferences")) + preference_load_defaults(prefs, p + "/preferences"); + } + } + + // now set-up our list of preference paths to try and load over the top + // of the application defaults .... + std::vector pref_paths = extra_prefs_paths; + + if (load_user_prefs) { + // user preference files will override all other prefs except + // 'override' predfs + for (const auto &i : global_store::PreferenceContexts) + pref_paths.push_back(preference_path_context(i)); + } + + // These prefs files will override user prefs. This allows us to have per + // machine preference files, for example, to force certain layouts on + // special machines like in a playback suite, say. + for (const auto &p : override_prefs_paths) { + pref_paths.push_back(p); + } + + preference_load_overrides(prefs, pref_paths); + return true; +} + bool xstudio::global_store::preference_load_defaults( utility::JsonStore &js, const std::string &path) { bool result = false; try { for (const auto &entry : fs::directory_iterator(path)) { + if (not fs::is_regular_file(entry.status()) or not(get_path_extension(entry.path()) == ".json")) { continue; @@ -139,7 +188,6 @@ void load_from_list(const std::string &path, std::vector &overrides) { } } // parse json, should be jsonpointers and values.. -// parse json, should be jsonpointers and values.. void load_override(utility::JsonStore &json, const fs::path &path) { std::ifstream i(path); nlohmann::json j; @@ -150,7 +198,8 @@ void load_override(utility::JsonStore &json, const fs::path &path) { // should be dict .. for (auto it : j.items()) { try { - if (not ends_with(it.key(), "/value") and not ends_with(it.key(), "/locked")) { + if (not ends_with(it.key(), "/value") and not ends_with(it.key(), "/locked") and + not ends_with(it.key(), "/default_value")) { spdlog::warn("Property key is restricted {} {}", it.key(), path.string()); continue; } @@ -187,8 +236,9 @@ void load_override(utility::JsonStore &json, const fs::path &path) { // override it. json.set(it.value(), it.key()); - spdlog::debug( - "Property overriden {} {} {}", it.key(), to_string(it.value()), path.string()); + // spdlog::debug( + // "Property overriden {} {} {}", it.key(), to_string(it.value()), + // path.string()); // tag it. set_preference_overridden_path(json, path.string(), property); if (set_as_overridden) @@ -199,6 +249,7 @@ void load_override(utility::JsonStore &json, const fs::path &path) { } } } + void xstudio::global_store::preference_load_overrides( utility::JsonStore &js, const std::vector &paths) { // we get a collection of JSONPOINTERS and values. @@ -295,6 +346,7 @@ utility::JsonStore xstudio::global_store::get_preference_values( std::set prefs = get_preferences(json, context); for (auto i : prefs) { + try { /* // For now putting the get(i + "/overridden_value") in a try @@ -313,7 +365,6 @@ utility::JsonStore xstudio::global_store::get_preference_values( } catch (...) { } - if (not only_changed or is_overridden or override_path == tmp_path) js[i + "/value"] = json.get(i + "/value"); @@ -362,27 +413,45 @@ utility::JsonStore GlobalStoreHelper::get_existing_or_create_new_preference( const std::string &path, const utility::JsonStore &default_, const bool async, - const bool broacast_change, + const bool broadcast_change, const std::string &context) { + + // Preferences can be fully defined in a .json file that ships with the + // application. However, we've found this to be a burden when creating + // Python plugins which need preference driven attributes but where we + // don't want to be building .json files to install at the same time. + + // To get around this, here create a new preference entry if there isn't + // one already coming from the .json files. We have to fill in essential + // preference entry fields which are needed later when preferences are + // written to the users home dir when xstudio exits. + try { utility::JsonStore v = get(path); if (!v.contains("overridden_value")) { v["overridden_value"] = default_; + } + if (!v.contains("path")) { v["path"] = path; + } + if (!v.contains("context")) { v["context"] = std::vector({"APPLICATION"}); - JsonStoreHelper::set(v, path, async, broacast_change); } + JsonStoreHelper::set(v, path, async, broadcast_change); return v["value"]; } catch (...) { + // No such preference was found in the .json files that ship with the + // app OR with the .json pref files that are saved into the user's + // home dir utility::JsonStore v; v["value"] = default_; v["overridden_value"] = default_; v["path"] = path; v["context"] = std::vector({"APPLICATION"}); - JsonStoreHelper::set(v, path, async, broacast_change); + JsonStoreHelper::set(v, path, async, broadcast_change); } return default_; } \ No newline at end of file diff --git a/src/global_store/src/global_store_actor.cpp b/src/global_store/src/global_store_actor.cpp index 8e762e98b..701a1ec0c 100644 --- a/src/global_store/src/global_store_actor.cpp +++ b/src/global_store/src/global_store_actor.cpp @@ -1,5 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 +#include + #include "xstudio/atoms.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" #include "xstudio/global_store/global_store_actor.hpp" @@ -13,6 +15,53 @@ using namespace caf; namespace xstudio::global_store { +class GlobalStoreIOActor : public caf::event_based_actor { + public: + GlobalStoreIOActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) {} + const char *name() const override { return NAME.c_str(); } + + caf::message_handler message_handler() { + return caf::message_handler{ + [=](save_atom, + const JsonStore &data, + const std::string &path) -> caf::result { + try { + // check dir exists.. + std::ofstream o(path + ".tmp", std::ofstream::out | std::ofstream::trunc); + try { + o.exceptions(std::ios_base::failbit | std::ifstream::badbit); + + o << std::setw(4) << data.cref() << std::endl; + o.close(); + + fs::rename(path + ".tmp", path); + + spdlog::debug("Saved {}", path); + } catch (const std::exception &err) { + if (o.is_open()) { + o.close(); + fs::remove(path + ".tmp"); + } + return caf::result(make_error( + xstudio_error::error, + fmt::format("Failed to save {} {}", path, err.what()))); + } + } catch (const std::exception &err) { + return caf::result(make_error( + xstudio_error::error, + fmt::format("Failed to save {} {}", path, err.what()))); + } + return true; + }}; + } + + caf::behavior make_behavior() override { return message_handler(); } + + private: + inline static const std::string NAME = "GlobalStoreIOActor"; +}; + + GlobalStoreActor::GlobalStoreActor( caf::actor_config &cfg, const JsonStore &jsn, std::string reg_value) : caf::event_based_actor(cfg), @@ -44,74 +93,31 @@ GlobalStoreActor::GlobalStoreActor( init(); } -void GlobalStoreActor::init() { - // only parial.. - spdlog::debug("Created GlobalStoreActor {}", base_.name()); - print_on_exit(this, "GlobalStoreActor"); - - auto event_group_ = spawn(this); - link_to(event_group_); - auto jsonactor = - spawn(Uuid(), base_.preferences_, std::chrono::milliseconds(50)); - link_to(jsonactor); - - // link to store, so we can get our own settings. - try { - caf::scoped_actor sys(system()); - auto result = request_receive>( - *sys, jsonactor, utility::get_group_atom_v); - - request_receive(*sys, result.first, broadcast::join_broadcast_atom_v, this); - - base_.preferences_.set(result.second); - base_.autosave_interval_ = - preference_value(base_.preferences_, "/core/global_store/autosave_interval"); - } catch (...) { - } - - system().registry().put(reg_value_, this); - - behavior_.assign( - // base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), - - +caf::message_handler GlobalStoreActor::message_handler() { + return caf::message_handler{ [=](autosave_atom) -> bool { return base_.autosave_; }, [=](autosave_atom, const bool enable) { if (enable != base_.autosave_) - send(event_group_, utility::event_atom_v, autosave_atom_v, enable); + mail(utility::event_atom_v, autosave_atom_v, enable).send(base_.event_group()); base_.autosave_ = enable; if (base_.autosave_) - delayed_anon_send( - actor_cast(this), - std::chrono::seconds(base_.autosave_interval_), - do_autosave_atom_v); + anon_mail(do_autosave_atom_v) + .delay(std::chrono::seconds(base_.autosave_interval_)) + .send(actor_cast(this), weak_ref); }, [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) { - // if(msg.source == store_events) - // unsubscribe(); - }, - [=](do_autosave_atom) { if (base_.autosave_) - delayed_anon_send( - actor_cast(this), - std::chrono::seconds(base_.autosave_interval_), - do_autosave_atom_v); + anon_mail(do_autosave_atom_v) + .delay(std::chrono::seconds(base_.autosave_interval_)) + .send(actor_cast(this), weak_ref); for (const auto &context : PreferenceContexts) - request(actor_cast(this), infinite, save_atom_v, context) + mail(save_atom_v, context) + .request(actor_cast(this), infinite) .then( [=](const bool result) mutable { if (result) @@ -124,27 +130,37 @@ void GlobalStoreActor::init() { }); }, - [=](get_json_atom atom) { delegate(jsonactor, atom); }, + [=](get_json_atom atom) { return mail(atom).delegate(jsonactor_); }, - [=](get_json_atom atom, const std::string &path) { delegate(jsonactor, atom, path); }, + [=](get_json_atom atom, const std::string &path) { + return mail(atom, path).delegate(jsonactor_); + }, [=](json_store::update_atom atom, const JsonStore &change, const std::string &path, const JsonStore &full) { - send(event_group_, utility::event_atom_v, atom, change, path); - delegate(actor_cast(this), atom, full); + mail(utility::event_atom_v, atom, change, path).send(base_.event_group()); + return mail(atom, full).delegate(actor_cast(this)); }, [=](json_store::update_atom, const JsonStore &json) { base_.preferences_.set(json); try { - auto tmp = preference_value( - base_.preferences_, "/core/global_store/autosave_interval"); - if (tmp != base_.autosave_interval_) { + if (auto tmp = preference_value( + base_.preferences_, "/core/global_store/autosave_interval"); + tmp != base_.autosave_interval_) { base_.autosave_interval_ = tmp; spdlog::debug("autosave updated {}", base_.autosave_interval_); } + + if (auto tmp = preference_value( + base_.preferences_, "/core/global_store/autosave_enable"); + tmp != base_.autosave_) { + base_.autosave_ = tmp; + spdlog::debug("autosave enable updated {}", base_.autosave_); + } + } catch (...) { } }, @@ -162,32 +178,16 @@ void GlobalStoreActor::init() { // update our copy base_.preferences_.set( - request_receive(*sys, jsonactor, json_store::get_json_atom_v)); + request_receive(*sys, jsonactor_, json_store::get_json_atom_v)); // get things to store.. JsonStore prefs = get_preference_values( base_.preferences_, std::set{context}, true, path); - // check dir exists.. - std::ofstream o(path + ".tmp", std::ofstream::out | std::ofstream::trunc); - try { - o.exceptions(std::ios_base::failbit | std::ifstream::badbit); - - o << std::setw(4) << prefs.cref() << std::endl; - o.close(); + auto rp = make_response_promise(); + rp.delegate(ioactor_, save_atom_v, prefs, path); + return rp; - fs::rename(path + ".tmp", path); - - spdlog::debug("Saved {} {}", context, path); - } catch (const std::exception &err) { - if (o.is_open()) { - o.close(); - fs::remove(path + ".tmp"); - } - return caf::result(make_error( - xstudio_error::error, - fmt::format("Failed to save {} {}", context, err.what()))); - } } else { return caf::result(make_error( xstudio_error::error, fmt::format("Invalid context {}", context))); @@ -199,7 +199,7 @@ void GlobalStoreActor::init() { [=](utility::serialise_atom, const std::string &context) -> JsonStore { caf::scoped_actor sys(system()); base_.preferences_.set( - request_receive(*sys, jsonactor, json_store::get_json_atom_v)); + request_receive(*sys, jsonactor_, json_store::get_json_atom_v)); JsonStore result; try { @@ -216,21 +216,57 @@ void GlobalStoreActor::init() { return result; }, - [=](set_json_atom atom, const JsonStore &json) { delegate(jsonactor, atom, json); }, + [=](set_json_atom atom, const JsonStore &json) { + return mail(atom, json).delegate(jsonactor_); + }, [=](set_json_atom atom, const JsonStore &json, const std::string &path) { - delegate(jsonactor, atom, json, path); + return mail(atom, json, path).delegate(jsonactor_); }, [=](set_json_atom atom, const JsonStore &json, const std::string &path, - const bool broadcast) { delegate(jsonactor, atom, json, path, false, broadcast); }, + const bool broadcast) { + return mail(atom, json, path, false, broadcast).delegate(jsonactor_); + }, + + [=](utility::get_group_atom atom) { return mail(atom).delegate(jsonactor_); }}; +} - [=](utility::get_group_atom atom) { delegate(jsonactor, atom); } +void GlobalStoreActor::init() { + // only parial.. + spdlog::debug("Created GlobalStoreActor {}", base_.name()); + print_on_exit(this, "GlobalStoreActor"); + + jsonactor_ = + spawn(Uuid(), base_.preferences_, std::chrono::milliseconds(50)); + link_to(jsonactor_); + + // link to store, so we can get our own settings. + try { + caf::scoped_actor sys(system()); + auto result = request_receive>( + *sys, jsonactor_, utility::get_group_atom_v); + + request_receive( + *sys, std::get<1>(result), broadcast::join_broadcast_atom_v, this); + + base_.preferences_.set(std::get<2>(result)); + base_.autosave_interval_ = + preference_value(base_.preferences_, "/core/global_store/autosave_interval"); + base_.autosave_ = + preference_value(base_.preferences_, "/core/global_store/autosave_enable"); - ); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + ioactor_ = spawn(); + link_to(ioactor_); + + system().registry().put(reg_value_, this); } void GlobalStoreActor::on_exit() { system().registry().erase(reg_value_); } diff --git a/src/global_store/test/CMakeLists.txt b/src/global_store/test/CMakeLists.txt index cd31c0812..7d8ee3c99 100644 --- a/src/global_store/test/CMakeLists.txt +++ b/src/global_store/test/CMakeLists.txt @@ -2,6 +2,7 @@ include(CTest) SET(LINK_DEPS xstudio::global_store + xstudio::media ) create_tests("${LINK_DEPS}") diff --git a/src/history/test/CMakeLists.txt b/src/history/test/CMakeLists.txt index 42733dbde..e1450426d 100644 --- a/src/history/test/CMakeLists.txt +++ b/src/history/test/CMakeLists.txt @@ -2,6 +2,7 @@ include(CTest) SET(LINK_DEPS xstudio::utility + xstudio::media ) create_tests("${LINK_DEPS}") diff --git a/src/http_client/src/CMakeLists.txt b/src/http_client/src/CMakeLists.txt index fe988dbf8..188e471a7 100644 --- a/src/http_client/src/CMakeLists.txt +++ b/src/http_client/src/CMakeLists.txt @@ -4,14 +4,15 @@ find_package(ZLIB) SET(LINK_DEPS xstudio::utility - caf::core + xstudio::media + CAF::core OpenSSL::SSL ZLIB::ZLIB ) -create_component(http_client 0.1.0 "${LINK_DEPS}") +create_component(http_client ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") -# project(http_client VERSION 0.1.0 LANGUAGES CXX) +# project(http_client VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) # set(SOURCES # http_client.cpp @@ -26,7 +27,7 @@ create_component(http_client 0.1.0 "${LINK_DEPS}") # target_link_libraries(${PROJECT_NAME} # PUBLIC # xstudio::utility -# caf::core +# CAF::core # OpenSSL::SSL # ZLIB::ZLIB # ) diff --git a/src/http_client/src/http_client_actor.cpp b/src/http_client/src/http_client_actor.cpp index 12de88a35..22ab2c38f 100644 --- a/src/http_client/src/http_client_actor.cpp +++ b/src/http_client/src/http_client_actor.cpp @@ -74,6 +74,7 @@ HTTPWorker::HTTPWorker( const httplib::Headers &headers, const std::string &body, const std::string &content_type) -> result { + // spdlog::warn("http_delete_atom {}", path); try { httplib::Client cli(scheme_host_port.c_str()); cli.set_follow_location(true); @@ -104,15 +105,8 @@ HTTPWorker::HTTPWorker( const std::string &body, const std::string &content_type) -> result { auto rp = make_response_promise(); - request( - actor_cast(this), - infinite, - http_delete_atom_v, - scheme_host_port, - path, - headers, - body, - content_type) + mail(http_delete_atom_v, scheme_host_port, path, headers, body, content_type) + .request(actor_cast(this), infinite) .then( [=](const httplib::Response &response) mutable { if (response.status != 200) @@ -133,6 +127,8 @@ HTTPWorker::HTTPWorker( const std::string &path, const httplib::Headers &headers, const httplib::Params ¶ms) -> result { + // spdlog::warn("http_get_atom {}", path); + try { httplib::Client cli(scheme_host_port.c_str()); cli.set_follow_location(true); @@ -172,14 +168,8 @@ HTTPWorker::HTTPWorker( const httplib::Headers &headers, const httplib::Params ¶ms) -> result { auto rp = make_response_promise(); - request( - actor_cast(this), - infinite, - http_get_atom_v, - scheme_host_port, - path, - headers, - params) + mail(http_get_atom_v, scheme_host_port, path, headers, params) + .request(actor_cast(this), infinite) .then( [=](const httplib::Response &response) mutable { if (response.status != 200) @@ -202,6 +192,8 @@ HTTPWorker::HTTPWorker( const httplib::Params ¶ms, const std::string &body, const std::string &content_type) -> result { + // spdlog::warn("http_post_atom {}", path); + try { httplib::Client cli(scheme_host_port.c_str()); cli.set_follow_location(true); @@ -233,16 +225,8 @@ HTTPWorker::HTTPWorker( const std::string &body, const std::string &content_type) -> result { auto rp = make_response_promise(); - request( - actor_cast(this), - infinite, - http_post_atom_v, - scheme_host_port, - path, - headers, - params, - body, - content_type) + mail(http_post_atom_v, scheme_host_port, path, headers, params, body, content_type) + .request(actor_cast(this), infinite) .then( [=](const httplib::Response &response) mutable { if (response.status != 200) @@ -265,6 +249,8 @@ HTTPWorker::HTTPWorker( const httplib::Params ¶ms, const std::string &body, const std::string &content_type) -> result { + // spdlog::warn("http_put_atom {}", path); + try { httplib::Client cli(scheme_host_port.c_str()); cli.set_follow_location(true); @@ -303,16 +289,8 @@ HTTPWorker::HTTPWorker( const std::string &body, const std::string &content_type) -> result { auto rp = make_response_promise(); - request( - actor_cast(this), - infinite, - http_put_atom_v, - scheme_host_port, - path, - headers, - params, - body, - content_type) + mail(http_put_atom_v, scheme_host_port, path, headers, params, body, content_type) + .request(actor_cast(this), infinite) .then( [=](const httplib::Response &response) mutable { if (response.status != 200) @@ -346,21 +324,27 @@ void HTTPClientActor::init() { spdlog::debug("Created HTTPClientActor"); print_on_exit(this, "HTTPClientActor"); - // try { - // auto prefs = GlobalStoreHelper(system()); - // JsonStore j; - // join_broadcast(this, prefs.get_group(j)); - // worker_count = preference_value(j, "/core/media_hook/max_worker_count"); - // } catch(...) { - // } +// try { +// auto prefs = GlobalStoreHelper(system()); +// JsonStore j; +// join_broadcast(this, prefs.get_group(j)); +// worker_count = preference_value(j, "/core/media_hook/max_worker_count"); +// } catch(...) { +// } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + auto pool = caf::actor_pool::make( - system().dummy_execution_unit(), + system(), worker_count, [&] { return system().spawn( connection_timeout_, read_timeout_, write_timeout_); }, caf::actor_pool::round_robin()); + +#pragma GCC diagnostic pop + link_to(pool); behavior_.assign( @@ -368,14 +352,15 @@ void HTTPClientActor::init() { [=](http_delete_atom atom, const std::string &scheme_host_port, const std::string &path) { - return delegate(pool, atom, scheme_host_port, path, httplib::Headers(), "", ""); + return mail(atom, scheme_host_port, path, httplib::Headers(), "", "") + .delegate(pool); }, [=](http_delete_atom atom, const std::string &scheme_host_port, const std::string &path, const httplib::Headers &headers) { - return delegate(pool, atom, scheme_host_port, path, headers, "", ""); + return mail(atom, scheme_host_port, path, headers, "", "").delegate(pool); }, [=](http_delete_atom atom, @@ -384,20 +369,22 @@ void HTTPClientActor::init() { const httplib::Headers &headers, const std::string &body, const std::string &content_type) { - return delegate(pool, atom, scheme_host_port, path, headers, body, content_type); + return mail(atom, scheme_host_port, path, headers, body, content_type) + .delegate(pool); }, [=](http_delete_simple_atom atom, const std::string &scheme_host_port, const std::string &path) { - return delegate(pool, atom, scheme_host_port, path, httplib::Headers(), "", ""); + return mail(atom, scheme_host_port, path, httplib::Headers(), "", "") + .delegate(pool); }, [=](http_delete_simple_atom atom, const std::string &scheme_host_port, const std::string &path, const httplib::Headers &headers) { - return delegate(pool, atom, scheme_host_port, path, headers, "", ""); + return mail(atom, scheme_host_port, path, headers, "", "").delegate(pool); }, [=](http_delete_simple_atom atom, @@ -406,19 +393,21 @@ void HTTPClientActor::init() { const httplib::Headers &headers, const std::string &body, const std::string &content_type) { - return delegate(pool, atom, scheme_host_port, path, headers, body, content_type); + return mail(atom, scheme_host_port, path, headers, body, content_type) + .delegate(pool); }, [=](http_get_atom atom, const std::string &scheme_host_port, const std::string &path) { - return delegate( - pool, atom, scheme_host_port, path, httplib::Headers(), httplib::Params()); + return mail(atom, scheme_host_port, path, httplib::Headers(), httplib::Params()) + .delegate(pool); }, [=](http_get_atom atom, const std::string &scheme_host_port, const std::string &path, const httplib::Headers &headers) { - return delegate(pool, atom, scheme_host_port, path, headers, httplib::Params()); + return mail(atom, scheme_host_port, path, headers, httplib::Params()) + .delegate(pool); }, [=](http_get_atom atom, @@ -426,21 +415,22 @@ void HTTPClientActor::init() { const std::string &path, const httplib::Headers &headers, const httplib::Params ¶ms) { - return delegate(pool, atom, scheme_host_port, path, headers, params); + return mail(atom, scheme_host_port, path, headers, params).delegate(pool); }, [=](http_get_simple_atom atom, const std::string &scheme_host_port, const std::string &path) { - return delegate( - pool, atom, scheme_host_port, path, httplib::Headers(), httplib::Params()); + return mail(atom, scheme_host_port, path, httplib::Headers(), httplib::Params()) + .delegate(pool); }, [=](http_get_simple_atom atom, const std::string &scheme_host_port, const std::string &path, const httplib::Headers &headers) { - return delegate(pool, atom, scheme_host_port, path, headers, httplib::Params()); + return mail(atom, scheme_host_port, path, headers, httplib::Params()) + .delegate(pool); }, [=](http_get_simple_atom atom, @@ -448,27 +438,27 @@ void HTTPClientActor::init() { const std::string &path, const httplib::Headers &headers, const httplib::Params ¶ms) { - return delegate(pool, atom, scheme_host_port, path, headers, params); + return mail(atom, scheme_host_port, path, headers, params).delegate(pool); }, [=](http_post_atom atom, const std::string &scheme_host_port, const std::string &path) { - return delegate( - pool, - atom, - scheme_host_port, - path, - httplib::Headers(), - httplib::Params(), - "", - ""); + return mail( + atom, + scheme_host_port, + path, + httplib::Headers(), + httplib::Params(), + "", + "") + .delegate(pool); }, [=](http_post_atom atom, const std::string &scheme_host_port, const std::string &path, const httplib::Headers &headers) { - return delegate( - pool, atom, scheme_host_port, path, headers, httplib::Params(), "", ""); + return mail(atom, scheme_host_port, path, headers, httplib::Params(), "", "") + .delegate(pool); }, [=](http_post_atom atom, @@ -476,7 +466,7 @@ void HTTPClientActor::init() { const std::string &path, const httplib::Headers &headers, const httplib::Params ¶ms) { - return delegate(pool, atom, scheme_host_port, path, headers, params, "", ""); + return mail(atom, scheme_host_port, path, headers, params, "", "").delegate(pool); }, [=](http_post_atom atom, @@ -485,37 +475,37 @@ void HTTPClientActor::init() { const httplib::Headers &headers, const std::string &body, const std::string &content_type) { - return delegate( - pool, - atom, - scheme_host_port, - path, - headers, - httplib::Params(), - body, - content_type); + return mail( + atom, + scheme_host_port, + path, + headers, + httplib::Params(), + body, + content_type) + .delegate(pool); }, [=](http_post_simple_atom atom, const std::string &scheme_host_port, const std::string &path) { - return delegate( - pool, - atom, - scheme_host_port, - path, - httplib::Headers(), - httplib::Params(), - "", - ""); + return mail( + atom, + scheme_host_port, + path, + httplib::Headers(), + httplib::Params(), + "", + "") + .delegate(pool); }, [=](http_post_simple_atom atom, const std::string &scheme_host_port, const std::string &path, const httplib::Headers &headers) { - return delegate( - pool, atom, scheme_host_port, path, headers, httplib::Params(), "", ""); + return mail(atom, scheme_host_port, path, headers, httplib::Params(), "", "") + .delegate(pool); }, [=](http_post_simple_atom atom, @@ -523,7 +513,7 @@ void HTTPClientActor::init() { const std::string &path, const httplib::Headers &headers, const httplib::Params ¶ms) { - return delegate(pool, atom, scheme_host_port, path, headers, params, "", ""); + return mail(atom, scheme_host_port, path, headers, params, "", "").delegate(pool); }, [=](http_post_simple_atom atom, @@ -532,35 +522,35 @@ void HTTPClientActor::init() { const httplib::Headers &headers, const std::string &body, const std::string &content_type) { - return delegate( - pool, - atom, - scheme_host_port, - path, - headers, - httplib::Params(), - body, - content_type); + return mail( + atom, + scheme_host_port, + path, + headers, + httplib::Params(), + body, + content_type) + .delegate(pool); }, [=](http_put_atom atom, const std::string &scheme_host_port, const std::string &path) { - return delegate( - pool, - atom, - scheme_host_port, - path, - httplib::Headers(), - httplib::Params(), - "", - ""); + return mail( + atom, + scheme_host_port, + path, + httplib::Headers(), + httplib::Params(), + "", + "") + .delegate(pool); }, [=](http_put_atom atom, const std::string &scheme_host_port, const std::string &path, const httplib::Headers &headers) { - return delegate( - pool, atom, scheme_host_port, path, headers, httplib::Params(), "", ""); + return mail(atom, scheme_host_port, path, headers, httplib::Params(), "", "") + .delegate(pool); }, [=](http_put_atom atom, @@ -568,7 +558,7 @@ void HTTPClientActor::init() { const std::string &path, const httplib::Headers &headers, const httplib::Params ¶ms) { - return delegate(pool, atom, scheme_host_port, path, headers, params, "", ""); + return mail(atom, scheme_host_port, path, headers, params, "", "").delegate(pool); }, [=](http_put_atom atom, @@ -577,15 +567,15 @@ void HTTPClientActor::init() { const httplib::Headers &headers, const std::string &body, const std::string &content_type) { - return delegate( - pool, - atom, - scheme_host_port, - path, - headers, - httplib::Params(), - body, - content_type); + return mail( + atom, + scheme_host_port, + path, + headers, + httplib::Params(), + body, + content_type) + .delegate(pool); }, [=](http_put_atom atom, @@ -595,30 +585,30 @@ void HTTPClientActor::init() { const std::string &body, const httplib::Params ¶ms, const std::string &content_type) { - return delegate( - pool, atom, scheme_host_port, path, headers, params, body, content_type); + return mail(atom, scheme_host_port, path, headers, params, body, content_type) + .delegate(pool); }, [=](http_put_simple_atom atom, const std::string &scheme_host_port, const std::string &path) { - return delegate( - pool, - atom, - scheme_host_port, - path, - httplib::Headers(), - httplib::Params(), - "", - ""); + return mail( + atom, + scheme_host_port, + path, + httplib::Headers(), + httplib::Params(), + "", + "") + .delegate(pool); }, [=](http_put_simple_atom atom, const std::string &scheme_host_port, const std::string &path, const httplib::Headers &headers) { - return delegate( - pool, atom, scheme_host_port, path, headers, httplib::Params(), "", ""); + return mail(atom, scheme_host_port, path, headers, httplib::Params(), "", "") + .delegate(pool); }, [=](http_put_simple_atom atom, @@ -626,7 +616,7 @@ void HTTPClientActor::init() { const std::string &path, const httplib::Headers &headers, const httplib::Params ¶ms) { - return delegate(pool, atom, scheme_host_port, path, headers, params, "", ""); + return mail(atom, scheme_host_port, path, headers, params, "", "").delegate(pool); }, [=](http_put_simple_atom atom, @@ -635,15 +625,15 @@ void HTTPClientActor::init() { const httplib::Headers &headers, const std::string &body, const std::string &content_type) { - return delegate( - pool, - atom, - scheme_host_port, - path, - headers, - httplib::Params(), - body, - content_type); + return mail( + atom, + scheme_host_port, + path, + headers, + httplib::Params(), + body, + content_type) + .delegate(pool); }, [=](http_put_simple_atom atom, @@ -653,7 +643,7 @@ void HTTPClientActor::init() { const std::string &body, const httplib::Params ¶ms, const std::string &content_type) { - return delegate( - pool, atom, scheme_host_port, path, headers, params, body, content_type); + return mail(atom, scheme_host_port, path, headers, params, body, content_type) + .delegate(pool); }); } diff --git a/src/json_store/src/CMakeLists.txt b/src/json_store/src/CMakeLists.txt index d08eb8f0f..28980f1d2 100644 --- a/src/json_store/src/CMakeLists.txt +++ b/src/json_store/src/CMakeLists.txt @@ -1,13 +1,11 @@ SET(LINK_DEPS - caf::core - xstudio::broadcast + CAF::core xstudio::utility ) SET(STATIC_LINK_DEPS - xstudio::broadcast_static xstudio::utility_static - caf::core + CAF::core ) -create_component_static(json_store 0.1.0 "${LINK_DEPS}" "${STATIC_LINK_DEPS}") +create_component_static(json_store ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${STATIC_LINK_DEPS}") diff --git a/src/json_store/src/json_store_actor.cpp b/src/json_store/src/json_store_actor.cpp index cc3e873fa..abd75683c 100644 --- a/src/json_store/src/json_store_actor.cpp +++ b/src/json_store/src/json_store_actor.cpp @@ -16,6 +16,25 @@ using namespace xstudio::utility; using namespace xstudio; using namespace caf; +namespace { + +void recursive_get_all_paths( + std::string curr_path, + std::vector &allpaths, + nlohmann::json::const_iterator b, + nlohmann::json::const_iterator e) { + allpaths.push_back(curr_path); + for (auto p = b; p != e; ++p) { + if (p.value().is_object() && !p.value().is_array()) { + recursive_get_all_paths( + curr_path + "/" + p.key(), allpaths, p.value().cbegin(), p.value().cend()); + } else { + allpaths.push_back(curr_path + "/" + p.key()); + } + } +} +} // namespace + JsonStoreActor::JsonStoreActor( caf::actor_config &cfg, const Uuid &uuid, @@ -33,21 +52,60 @@ JsonStoreActor::JsonStoreActor( link_to(broadcast_); behavior_.assign( + make_get_event_group_handler(broadcast_), [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, [=](get_json_atom) -> JsonStore { return json_store_; }, [=](get_json_atom, const std::string &path) -> caf::result { try { + + if (path.find("regex:") == 0) { + // if the 'path' starts with 'regex:' we do regex matching + // between path and the actual paths available in the json + // returning the data at the first path that matches + std::vector allpaths; + recursive_get_all_paths( + "", allpaths, json_store_.cbegin(), json_store_.cend()); + std::regex path_re(std::string(path, 6)); // strip the 'regex:' token + std::cmatch m; + for (const auto &a : allpaths) { + if (std::regex_match(a.c_str(), m, path_re)) { + std::string np = a; + return JsonStore(json_store_.get(np)); + } + } + // Instead of returning an error which can cause spamming + // of the log, we can will a null here as we haven't + // managed a reg-ex match to metadata path. + return JsonStore(); + /*return make_error( + xstudio_error::error, + std::string("Failed to do regex json path match to ") + + std::string(path, 6));*/ + } std::string np = path; return JsonStore(json_store_.get(np)); + } catch (const std::exception &e) { return make_error( xstudio_error::error, std::string("get_json_atom ") + e.what()); } }, + [=](get_json_atom, const std::vector &paths) -> JsonStore { + JsonStore result; + for (const auto &path : paths) { + try { + result[path] = json_store_.get(path); + } catch (...) { + result[path] = nlohmann::json(); + } + } + return result; + }, + [=](jsonstore_change_atom) { - send(broadcast_, update_atom_v, json_store_); + mail(update_atom_v, json_store_).send(broadcast_); update_pending_ = false; }, @@ -60,8 +118,7 @@ JsonStoreActor::JsonStoreActor( }, [=](patch_atom, const JsonStore &json) -> bool { - const JsonStore j = json; - json_store_ = json_store_.patch(j); + json_store_ = json_store_.patch(json); broadcast_change(); return true; }, @@ -72,12 +129,11 @@ JsonStoreActor::JsonStoreActor( broadcast_change(); return true; }, - [=](utility::serialise_atom) -> JsonStore { return json_store_; }, [=](set_json_atom atom, const JsonStore &json, const std::string &path) { std::string p = path; - delegate(caf::actor_cast(this), atom, json, p, false); + return mail(atom, json, p, false).delegate(caf::actor_cast(this)); }, [=](set_json_atom, const JsonStore &json) -> bool { @@ -103,6 +159,7 @@ JsonStoreActor::JsonStoreActor( return true; }, + [=](set_json_atom, const JsonStore &json, const std::string &path, @@ -124,22 +181,24 @@ JsonStoreActor::JsonStoreActor( [=](subscribe_atom, const std::string &path, caf::actor _actor) -> caf::result { // delegate to reader, return promise ? - std::string p = path; - auto rp = make_response_promise(); - this->request(_actor, caf::infinite, utility::get_group_atom_v) + auto rp = make_response_promise(); + mail(utility::get_group_atom_v) + .request(_actor, caf::infinite) .then( - [&, p, _actor, rp](const std::pair &data) mutable { - const auto [grp, json] = data; + [&, path, _actor, rp]( + const std::tuple &data) mutable { + const auto [jsa, grp, json] = data; actor_group_[actor_cast(_actor)] = grp; - group_path_[grp] = p; + group_path_[grp] = path; - this->request(grp, caf::infinite, broadcast::join_broadcast_atom_v) + mail(broadcast::join_broadcast_atom_v) + .request(grp, caf::infinite) .then( [=](const bool) mutable {}, [=](const error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); - json_store_.set(json, p); + json_store_.set(json, path); broadcast_change(); rp.deliver(true); }, @@ -158,7 +217,7 @@ JsonStoreActor::JsonStoreActor( const JsonStore & /*change*/, const std::string & /*path*/, const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full).delegate(actor_cast(this)); }, [=](update_atom, const JsonStore &json) { @@ -174,8 +233,8 @@ JsonStoreActor::JsonStoreActor( broadcast_change(); }, - [=](utility::get_group_atom) -> std::pair { - return std::make_pair(broadcast_, json_store_); + [=](utility::get_group_atom) -> std::tuple { + return std::make_tuple(caf::actor_cast(this), broadcast_, json_store_); }, [=](utility::uuid_atom) -> Uuid { return uuid_; }); @@ -193,11 +252,13 @@ void JsonStoreActor::broadcast_change( std::string p = path; if (broadcast_delay_.count() and async) { if (not update_pending_) { - delayed_anon_send(this, broadcast_delay_, jsonstore_change_atom_v); + anon_mail(jsonstore_change_atom_v) + .delay(broadcast_delay_) + .send(caf::actor_cast(this), weak_ref); update_pending_ = true; } } else { // minor change, send now (DANGER MAYBE CAUSE ASYNC ISSUES) - send(broadcast_, update_atom_v, change, p, json_store_); + mail(update_atom_v, change, p, json_store_).send(broadcast_); } } diff --git a/src/json_store/src/json_store_handler.cpp b/src/json_store/src/json_store_handler.cpp new file mode 100644 index 000000000..25e4adda0 --- /dev/null +++ b/src/json_store/src/json_store_handler.cpp @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/atoms.hpp" +#include "xstudio/json_store/json_store_handler.hpp" +#include "xstudio/json_store/json_store_actor.hpp" +#include "xstudio/utility/helpers.hpp" + +using namespace caf; +using namespace xstudio::json_store; +using namespace xstudio::utility; +using namespace xstudio; + +caf::message_handler JsonStoreHandler::default_event_handler() { + return JsonStoreActor::default_event_handler(); +} + +JsonStoreHandler::JsonStoreHandler( + caf::event_based_actor *act, + caf::actor event_group, + const utility::Uuid &uuid, + const utility::JsonStore &json, + const std::chrono::milliseconds &delay) { + + actor_ = act; + event_group_ = event_group; + + json_store_ = actor_->spawn(uuid, json, delay); + actor_->link_to(json_store_); + utility::join_event_group(act, json_store_); +} + + +caf::message_handler JsonStoreHandler::message_handler() { + return caf::message_handler({ + [=](json_store::get_json_atom _get_atom) { + return actor_->mail(_get_atom).delegate(json_store_); + }, + + [=](json_store::get_json_atom _get_atom, const std::string &path, bool) { + return actor_->mail(_get_atom, path).delegate(json_store_); + }, + + [=](json_store::get_json_atom _get_atom, const std::string &path) { + return actor_->mail(_get_atom, path).delegate(json_store_); + }, + + [=](utility::get_group_atom _get_group_atom) { + return actor_->mail(_get_group_atom).delegate(json_store_); + }, + + [=](json_store::update_atom, + const JsonStore &change, + const std::string &path, + const JsonStore &full) { + if (actor_->current_sender() == json_store_) + actor_->mail(json_store::update_atom_v, change, path, full).send(event_group_); + }, + + [=](json_store::update_atom, const JsonStore &full) mutable { + if (actor_->current_sender() == json_store_) + actor_->mail(json_store::update_atom_v, full).send(event_group_); + }, + + [=](json_store::set_json_atom atom, const JsonStore &json) { + return actor_->mail(atom, json).delegate(json_store_); + }, + + [=](json_store::set_json_atom atom, const JsonStore &json, const std::string &path) { + return actor_->mail(atom, json, path).delegate(json_store_); + }, + + }); +} diff --git a/src/json_store/src/json_store_helper.cpp b/src/json_store/src/json_store_helper.cpp index f201d42d0..5a084bb35 100644 --- a/src/json_store/src/json_store_helper.cpp +++ b/src/json_store/src/json_store_helper.cpp @@ -17,7 +17,8 @@ JsonStore JsonStoreHelper::get(const std::string &path) const { if (not a) throw std::runtime_error("JsonStoreHelper is dead"); - system_->request(a, infinite, get_json_atom_v, path) + system_->mail(get_json_atom_v, path) + .request(a, infinite) .receive( [&](const JsonStore &_json) { res = _json; }, [&](const error &err) { throw std::runtime_error(to_string(err)); }); @@ -33,27 +34,53 @@ void JsonStoreHelper::set( if (not a) throw std::runtime_error("JsonStoreHelper is dead"); if (async) { - system_->send(a, set_json_atom_v, V, path, broadcast_change); + system_->mail(set_json_atom_v, V, path, broadcast_change).send(a); } else { - system_->request(a, infinite, set_json_atom_v, V, path, broadcast_change) + system_->mail(set_json_atom_v, V, path, broadcast_change) + .request(a, infinite) .receive( [&](const bool &) {}, [&](const error &err) { throw std::runtime_error(to_string(err)); }); } } +caf::actor JsonStoreHelper::get_jsonactor() const { + auto a = caf::actor_cast(store_actor_); + caf::actor result; + + if (not a) + throw std::runtime_error("JsonStoreHelper is dead"); + + system_->mail(utility::get_group_atom_v) + .request(a, infinite) + .receive( + [&](const std::tuple &data) { + const auto [jsa, grpa, json] = data; + result = jsa; + }, + [&](const error &err) { throw std::runtime_error(to_string(err)); }); + + return result; +}; + caf::actor JsonStoreHelper::get_group(utility::JsonStore &V) const { auto a = caf::actor_cast(store_actor_); caf::actor grp; + if (not a) throw std::runtime_error("JsonStoreHelper is dead"); - system_->request(a, infinite, utility::get_group_atom_v) + + system_->mail(utility::get_group_atom_v) + .request(a, infinite) .receive( - [&](const std::pair &data) { - grp = data.first; - V = data.second; + [&](const std::tuple &data) { + const auto [jsa, grpa, json] = data; + + grp = grpa; + V = json; }, [&](const error &err) { throw std::runtime_error(to_string(err)); }); + return grp; } } // namespace xstudio::json_store diff --git a/src/json_store/test/CMakeLists.txt b/src/json_store/test/CMakeLists.txt index faef73fee..9bc35bcd3 100644 --- a/src/json_store/test/CMakeLists.txt +++ b/src/json_store/test/CMakeLists.txt @@ -2,6 +2,7 @@ include(CTest) SET(LINK_DEPS xstudio::json_store + xstudio::media ) create_tests("${LINK_DEPS}") diff --git a/src/launch/xstudio/src/CMakeLists.txt b/src/launch/xstudio/src/CMakeLists.txt index 097e41559..182fdaeaf 100644 --- a/src/launch/xstudio/src/CMakeLists.txt +++ b/src/launch/xstudio/src/CMakeLists.txt @@ -1,23 +1,33 @@ project(xstudio VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) +find_package(OpenGL REQUIRED) +find_package(GLEW REQUIRED) +find_package(Qt6 COMPONENTS Core Quick Gui Widgets OpenGL OpenGLWidgets Svg REQUIRED) +find_package(OpenSSL) +find_package(ZLIB) +find_package(fmt REQUIRED) + +if(WIN32) set(SOURCES xstudio.cpp - ../../../../ui/qml/reskin/qml_reskin.qrc + xstudio_win_resource.rc ../../../../ui/qml/xstudio/qml.qrc -) - + ) +else() +set(SOURCES + xstudio.cpp + ../../../../ui/qml/xstudio/qml.qrc + ) +endif() if(WIN32) # Add the /bigobj option for xstudio.cpp set_source_files_properties(xstudio.cpp PROPERTIES COMPILE_FLAGS "/bigobj") endif() -find_package(OpenGL REQUIRED) -find_package(GLEW REQUIRED) -find_package(Qt5 COMPONENTS Core Quick Gui Widgets OpenGL REQUIRED) -find_package(OpenSSL) -find_package(ZLIB) -#find_package(OpenTime REQUIRED) -#find_package(OpenTimelineIO REQUIRED) +if(NOT ${OTIO_SUBMODULE}) + find_package(OpenTime REQUIRED) + find_package(OpenTimelineIO REQUIRED) +endif() set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) @@ -33,49 +43,54 @@ else() configure_file(xstudio.sh.in xstudio.sh @ONLY) endif() -default_options(${PROJECT_NAME}) +default_options_local(${PROJECT_NAME}) target_link_libraries(${PROJECT_NAME} PRIVATE xstudio::caf_utility xstudio::global + xstudio::ui::model_data xstudio::ui::opengl::viewport xstudio::ui::qml::bookmark + xstudio::ui::qml::conform xstudio::ui::qml::embedded_python - xstudio::ui::qml::event xstudio::ui::qml::global_store xstudio::ui::qml::helper xstudio::ui::qml::log - xstudio::ui::qml::module - xstudio::ui::qml::playhead - xstudio::ui::model_data xstudio::ui::qml::session xstudio::ui::qml::studio - xstudio::ui::qml::tag xstudio::ui::qml::viewport - xstudio::ui::viewport xstudio::ui::qt::viewport_widget + xstudio::ui::viewport + xstudio::media + xstudio::media_reader xstudio::utility + fmt::fmt PUBLIC - caf::core + fmt::fmt + spdlog::spdlog + CAF::core + CAF::io $<$:GLdispatch> - Qt5::Gui - Qt5::Core - Qt5::Qml - Qt5::Quick - Qt5::Widgets + # quickfuture + Qt6::Core + Qt6::Gui + Qt6::Quick + Qt6::OpenGLWidgets + Qt6::Widgets + Qt6::Svg OpenSSL::SSL ZLIB::ZLIB - #OTIO::opentime - #OTIO::opentimelineio - quickfuture ) if(WIN32) target_link_libraries(${PROJECT_NAME} PRIVATE dbghelp) +elseif(NOT APPLE) + target_link_libraries(${PROJECT_NAME} PUBLIC uuid) endif() if(WIN32) + set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" @@ -83,24 +98,59 @@ if(WIN32) VS_DEBUGGER_ENVIRONMENT XSTUDIO_ROOT=${CMAKE_BINARY_DIR}/bin/$<$:Debug>$<$:Release> LINK_DEPENDS_NO_SHARED true ) -else() - set_target_properties(${PROJECT_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" - OUTPUT_NAME "${PROJECT_NAME}.bin" - LINK_DEPENDS_NO_SHARED true - ) -endif() -install(TARGETS ${PROJECT_NAME} + install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin) -if(WIN32) install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/xstudio.bat DESTINATION bin RENAME xstudio) + + get_target_property(_qmake_executable Qt6::qmake IMPORTED_LOCATION) + get_filename_component(_qt_bin_dir "${_qmake_executable}" DIRECTORY) + find_program(windeployqt_exe windeployqt HINTS "${_qt_bin_dir}") + configure_file(windeploy.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/windeploy.cmake @ONLY) + install(SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/windeploy.cmake) + +elseif(APPLE) + + set_target_properties(${PROJECT_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/MacOS" + OUTPUT_NAME "${PROJECT_NAME}.bin" + LINK_DEPENDS_NO_SHARED true + ) + + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources") + + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/") + + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/xstudio_app.ico "${CMAKE_BINARY_DIR}/xSTUDIO.app/Contents/Resources/") + + get_target_property(_qmake_executable Qt6::qmake IMPORTED_LOCATION) + get_filename_component(_qt_bin_dir "${_qmake_executable}" DIRECTORY) + find_program(macdeployqt_exe macdeployqt HINTS "${_qt_bin_dir}") + configure_file(macdeploy.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/macdeploy.cmake @ONLY) + install(SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/macdeploy.cmake) + else() + + set_target_properties(${PROJECT_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + OUTPUT_NAME "${PROJECT_NAME}.bin" + LINK_DEPENDS_NO_SHARED true + ) + + install(TARGETS ${PROJECT_NAME} + RUNTIME DESTINATION bin) + install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/xstudio.sh DESTINATION bin @@ -110,4 +160,5 @@ else() ${CMAKE_CURRENT_SOURCE_DIR}/xstudio_desktop_integration.sh DESTINATION bin RENAME xstudio_desktop_integration) -endif() + +endif() \ No newline at end of file diff --git a/src/launch/xstudio/src/Info.plist b/src/launch/xstudio/src/Info.plist new file mode 100644 index 000000000..57dc1e23c --- /dev/null +++ b/src/launch/xstudio/src/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + xstudio.bin + CFBundleIconFile + xstudio_app.ico + CFBundleIdentifier + com.googlecode.skia.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + xSTUDIO + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSMinimumSystemVersion + ${MACOSX_DEPLOYMENT_TARGET} + NSMainNibFile + HelloWorld + NSPrincipalClass + NSApplication + + \ No newline at end of file diff --git a/src/launch/xstudio/src/macdeploy.cmake.in b/src/launch/xstudio/src/macdeploy.cmake.in new file mode 100644 index 000000000..45d0c270b --- /dev/null +++ b/src/launch/xstudio/src/macdeploy.cmake.in @@ -0,0 +1,3 @@ +message("Running @macdeployqt_exe@ with args: ${CMAKE_BINARY_DIR}/xSTUDIO.app -qmldir=@CMAKE_SOURCE_DIR@/ui") +execute_process(COMMAND "@macdeployqt_exe@" ${CMAKE_BINARY_DIR}/xSTUDIO.app -qmldir=@CMAKE_SOURCE_DIR@/ui + WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}") \ No newline at end of file diff --git a/src/launch/xstudio/src/windeploy.cmake.in b/src/launch/xstudio/src/windeploy.cmake.in new file mode 100644 index 000000000..96ec0940d --- /dev/null +++ b/src/launch/xstudio/src/windeploy.cmake.in @@ -0,0 +1,3 @@ +message("Running windeployqt... with args: ${CMAKE_INSTALL_PREFIX}/bin/xstudio.exe --qmldir @CMAKE_SOURCE_DIR@/ui") +execute_process(COMMAND "@windeployqt_exe@" ${CMAKE_INSTALL_PREFIX}/bin/xstudio.exe --qmldir @CMAKE_SOURCE_DIR@/ui + WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}") \ No newline at end of file diff --git a/src/launch/xstudio/src/xstudio.cpp b/src/launch/xstudio/src/xstudio.cpp index 76501dc46..8b968ccbd 100644 --- a/src/launch/xstudio/src/xstudio.cpp +++ b/src/launch/xstudio/src/xstudio.cpp @@ -5,18 +5,16 @@ #include #include #include -#ifdef __linux__ -#include -#endif #include #include #include #include -#ifdef __linux__ + +#ifndef _WIN32 +#include #include #endif - #ifndef CPPHTTPLIB_OPENSSL_SUPPORT #define CPPHTTPLIB_OPENSSL_SUPPORT #endif @@ -25,22 +23,20 @@ #endif #include +#include #include #include +#include #include "xstudio/atoms.hpp" #include "xstudio/caf_utility/caf_setup.hpp" #include "xstudio/global/global_actor.hpp" +#include "xstudio/global/xstudio_actor_system.hpp" #include "xstudio/global_store/global_store.hpp" #include "xstudio/playhead/playhead_actor.hpp" #include "xstudio/session/session_actor.hpp" #include "xstudio/studio/studio_actor.hpp" -#include "xstudio/timeline/clip_actor.hpp" -#include "xstudio/timeline/track_actor.hpp" -#include "xstudio/timeline/gap_actor.hpp" -#include "xstudio/timeline/stack_actor.hpp" #include "xstudio/utility/caf_helpers.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/frame_list.hpp" #include "xstudio/utility/sequence.hpp" #include "xstudio/utility/helpers.hpp" @@ -59,27 +55,11 @@ CAF_PUSH_WARNINGS #include #include #include +#include +#include CAF_POP_WARNINGS -#include "xstudio/ui/mouse.hpp" -#include "xstudio/ui/qml/bookmark_model_ui.hpp" //NOLINT -#include "xstudio/ui/qml/embedded_python_ui.hpp" //NOLINT -#include "xstudio/ui/qml/event_ui.hpp" //NOLINT -#include "xstudio/ui/qml/global_store_model_ui.hpp" //NOLINT -#include "xstudio/ui/qml/helper_ui.hpp" //NOLINT -#include "xstudio/ui/qml/hotkey_ui.hpp" //NOLINT -#include "xstudio/ui/qml/log_ui.hpp" //NOLINT -#include "xstudio/ui/qml/model_data_ui.hpp" //NOLINT -#include "xstudio/ui/qml/module_data_ui.hpp" //NOLINT -#include "xstudio/ui/qml/module_menu_ui.hpp" //NOLINT -#include "xstudio/ui/qml/module_ui.hpp" //NOLINT -#include "xstudio/ui/qml/qml_viewport.hpp" //NOLINT -#include "xstudio/ui/qml/session_model_ui.hpp" //NOLINT -#include "xstudio/ui/qml/snapshot_model_ui.hpp" //NOLINT -#include "xstudio/ui/qml/shotgun_provider_ui.hpp" -#include "xstudio/ui/qml/studio_ui.hpp" //NOLINT -#include "xstudio/ui/qml/thumbnail_provider_ui.hpp" -#include "xstudio/ui/qt/offscreen_viewport.hpp" //NOLINT +#include "xstudio/ui/qml/studio_ui.hpp" //NOLINT using namespace std; using namespace caf; @@ -95,50 +75,12 @@ using namespace xstudio; bool shutdown_xstudio = false; -struct ExitTimeoutKiller { - - void start() { -#ifdef _WIN32 - spdlog::debug("ExitTimeoutKiller start ignored"); - } -#else - - - // lock the mutex ... - clean_actor_system_exit.lock(); - - // .. and start a thread to watch the mutex - exit_timeout = std::thread([&]() { - // wait for stop() to be called - 10s - if (!clean_actor_system_exit.try_lock_for(std::chrono::seconds(10))) { - // stop() wasn't called! Probably failed to exit actor_system, - // see main() function. Kill process. - spdlog::critical("xSTUDIO has not exited cleanly: killing process now"); - kill(0, SIGKILL); - } else { - clean_actor_system_exit.unlock(); - } - }); - } +#ifdef __apple__ +// this is required to overcome a very odd link +// error on MaxOS (intel) +uint32_t OPENSSL_ia32cap_P[4] = {0}; #endif - void stop() { -#ifdef _WIN32 - spdlog::debug("ExitTimeoutKiller stop ignored"); - } -#else - // unlock the mutex so exit_timeout won't time-out - clean_actor_system_exit.unlock(); - if (exit_timeout.joinable()) - exit_timeout.join(); - } - - std::timed_mutex clean_actor_system_exit; - std::thread exit_timeout; -#endif - -} exit_timeout_killer; - #ifdef _WIN32 #include @@ -180,7 +122,6 @@ void handler(int sig) { // print out all the frames to stderr fprintf(stderr, "Error: signal %d:\n", sig); backtrace_symbols_fd(array, size, STDERR_FILENO); - exit(1); } #endif @@ -208,6 +149,128 @@ caf::behavior connect_to_remote(caf::event_based_actor *self) { }; } +void xstudioQtMessageHandler( + QtMsgType type, const QMessageLogContext &context, const QString &msg) { + QByteArray localMsg = msg.toLocal8Bit(); + const char *file = context.file ? context.file : ""; + const char *function = context.function ? context.function : ""; + + if (!strcmp("qml", context.category)) { + // qml messages are type = QtDebugMsg but we always want to see these. + spdlog::info("QML: {} ({}:{}, {})", localMsg.constData(), file, context.line, function); + return; + } + + switch (type) { + case QtDebugMsg: + spdlog::debug( + "Qt -- {} ({}:{}, {})", localMsg.constData(), file, context.line, function); + break; + case QtInfoMsg: + spdlog::info( + "Qt -- {} ({}:{}, {})", localMsg.constData(), file, context.line, function); + break; + case QtWarningMsg: + // for now, supressing Qt warnings as we get some spurious stuff from QML that has been + // impossible to track down! Might be fixed with Qt6 + spdlog::debug( + "Qt -- {} ({}:{}, {})", localMsg.constData(), file, context.line, function); + break; + case QtCriticalMsg: + spdlog::critical( + "Qt -- {} ({}:{}, {})", localMsg.constData(), file, context.line, function); + break; + case QtFatalMsg: + spdlog::error( + "Qt -- {} ({}:{}, {})", localMsg.constData(), file, context.line, function); + break; + default: + spdlog::info( + "Qt -- {} ({}:{}, {})", localMsg.constData(), file, context.line, function); + break; + } +} + +void execute_xstudio_ui( + const bool disble_vsync, + const float ui_scale_factor, + const bool silence_qt_warnings, + int argc, + char **argv + ) { + + // apply global UI scaling preference here by setting the + // QT_SCALE_FACTOR env var before creating the QApplication + if (ui_scale_factor != 1.0f) { + std::string fstr = fmt::format("{}", ui_scale_factor); + qputenv("QT_SCALE_FACTOR", fstr.c_str()); + } + + QSurfaceFormat format; +#ifdef __OPENGL_4_1__ + // MacOS is limited to OpenGL 4.1, of course + format.setVersion(4, 1); + format.setProfile(QSurfaceFormat::CoreProfile); +#endif + QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL); + + if (disble_vsync) { + format.setSwapInterval(0); + } else { + format.setSwapInterval(1); + } + QSurfaceFormat::setDefaultFormat(format); + + + if (silence_qt_warnings) { + qInstallMessageHandler(xstudioQtMessageHandler); + } + + QApplication app(argc, argv); + app.setOrganizationName("DNEG"); + app.setOrganizationDomain("dneg.com"); + app.setApplicationVersion(PROJECT_VERSION); + app.setApplicationName("xStudio"); + app.setWindowIcon(QIcon(":images/xstudio_logo_256_v1.svg")); + + + QQmlApplicationEngine engine; + + ui::qml::setup_xstudio_qml_emgine(static_cast(&engine), CafActorSystem::system()); + + const QUrl url(QStringLiteral("qrc:/main.qml")); + + QObject::connect( + &engine, + &QQmlApplicationEngine::objectCreated, + &app, + [url](QObject *obj, const QUrl &objUrl) { + if (!obj && url == objUrl) { + QCoreApplication::exit(-1); + } + }, + Qt::QueuedConnection); + + engine.load(url); + spdlog::info("XStudio UI launched."); + + #ifdef _WIN32 + // Note: this is horrible thing is here to force a direct dependency on QOpenGLWidget + // for xstudio.exe. We need it for Windows packaging. + // 'windeployqt.exe' otherwise does not recognise that Qt6OpenGLWidgets.dll is a + // dependency of xstudio.exe and we get a runtime failure. I don't understand why + // because studio_qml.dll is a direct dependency, viewport_widget.dll is a dependency + // of that and Qt6OpenGLWidgets.dll is a dependency of viewport_widget.dll + QOpenGLWidget * dummy = new QOpenGLWidget(); + delete dummy; +#endif + + app.exec(); + + spdlog::get("xstudio")->sinks().pop_back(); + +} + struct CLIArguments { args::ArgumentParser parser = {"xstudio. v" PROJECT_VERSION, "Launchs xstudio."}; @@ -215,14 +278,25 @@ struct CLIArguments { args::PositionalList media_paths = {parser, "PATH", "Path to media"}; - args::Flag headless = {parser, "headless", "Headless mode, no UI", {'e', "headless"}}; - args::Flag player = {parser, "player", "Player mode, minimal UI", {'p', "player"}}; + args::Flag headless = {parser, "headless", "Headless mode, no UI", {'e', "headless"}}; + args::Flag user_prefs_off = { + parser, + "user-prefs-off", + "Skip loading and saving of user preferences.", + {"user-prefs-off"}}; + args::Flag quick_view = { parser, "quick-view", "Open a quick-view for each supplied media item", {'l', "quick-view"}}; + args::Flag silence_qt_warnings = { + parser, + "silence-qt-warnings", + "Silences QT warnings normally printed into the terminal. Some QT warnings are not consequential. Use this flag in a wrapper script to suppress all QT warnings.", + {"silence-qt-warnings"}}; + std::unordered_map cmMapValues{ {"none", "Off"}, {"ab", "A/B"}, @@ -239,6 +313,8 @@ struct CLIArguments { args::Group playhead = {parser, "Playback options"}; args::ValueFlag play_rate = {playhead, "RATE", "Playback rate", {'f', "fps"}}; args::ValueFlag media_rate = {playhead, "RATE", "Media rate", {"mfps"}}; + args::ValueFlag in_point = {playhead, "INPT", "In Point (frames)", {"in"}}; + args::ValueFlag out_point = {playhead, "OUTPT", "Out Point (frames)", {"out"}}; args::Group remote_session = {parser, "Remote session options"}; args::ValueFlag remote_host = { @@ -255,18 +331,15 @@ struct CLIArguments { args::ValueFlagList cli_pref_paths = { misc, "PATH", "Path to preferences", {"pref"}}; + + args::ValueFlagList cli_override_pref_paths = { + misc, "PATH", "Path to preferences that override user preferences", {"override-pref"}}; + args::Flag debug = {misc, "debug", "Debugging mode", {"debug"}}; args::ValueFlag logfile = { misc, "PATH", "Write session log to file", {"log-file"}}; args::Flag disable_vsync = { misc, "disable-vsync", "Disable sync to video refresh", {"disable-vsync"}}; - args::Flag reskin = { - misc, "reskin", "Launch with the new user interface (under construction)", {"reskin"}}; - args::Flag share_opengl_contexts = { - misc, - "share-gl-context", - "Share the same openGl context between main viewer and pop-out viewer.", - {"share-gl-context"}}; args::CompletionFlag completion = {parser, {"complete"}}; void parse_args(int argc, char **argv) { @@ -296,23 +369,24 @@ struct Launcher { #else setenv("QML_IMPORT_TRACE", "0", true); #endif + signal(SIGSEGV, handler); start_logger( cli_args.debug.Matched() ? spdlog::level::debug : spdlog::level::info, args::get(cli_args.logfile)); prefs = load_preferences(); + scoped_actor self{system}; // uses commandline args. actions["new_instance"] = cli_args.new_session.Matched(); actions["headless"] = cli_args.headless.Matched(); actions["debug"] = cli_args.debug.Matched(); - actions["player"] = cli_args.player.Matched(); + actions["user_prefs_off"] = cli_args.user_prefs_off.Matched(); actions["quick_view"] = cli_args.quick_view.Matched(); actions["disable_vsync"] = cli_args.disable_vsync.Matched(); - actions["reskin"] = cli_args.reskin.Matched(); - actions["share_opengl_contexts"] = cli_args.share_opengl_contexts.Matched(); - actions["compare"] = static_cast(args::get(cli_args.compare)); + actions["compare"] = static_cast(args::get(cli_args.compare)); + actions["silence_qt_warnings"] = cli_args.silence_qt_warnings.Matched(); // check for xstudio url.. if (args::get(cli_args.media_paths).size() == 1 and @@ -320,10 +394,19 @@ struct Launcher { if (uri_request == "open_session") { actions["open_session"] = true; auto p = uri_params.find("path"); + auto u = uri_params.find("uri"); if (p != uri_params.end()) actions["open_session_path"] = p->second; - else { - spdlog::error("Open session failed, path= required"); + else if (u != uri_params.end()) { + auto uri = caf::make_uri(u->second); + if (uri) + actions["open_session_path"] = uri_to_posix_path(*uri); + else { + spdlog::error("Open session failed, invalid URI {}", u->second); + std::exit(EXIT_FAILURE); + } + } else { + spdlog::error("Open session failed, path= or uri= required"); std::exit(EXIT_FAILURE); } } else if (uri_request == "add_media") { @@ -334,9 +417,9 @@ struct Launcher { // check for unassigned media. auto media = uri_params.equal_range("media"); if (media.first != uri_params.end()) { - actions["playlists"]["Untitled Playlist"] = nlohmann::json::array(); + actions["playlists"]["__DEFAULT_PUSH_PLAYLIST__"] = nlohmann::json::array(); for (auto m = media.first; m != media.second; ++m) { - actions["playlists"]["Untitled Playlist"].push_back(m->second); + actions["playlists"]["__DEFAULT_PUSH_PLAYLIST__"].push_back(m->second); } } @@ -361,6 +444,12 @@ struct Launcher { if (cli_args.play_rate.Matched()) actions["set_play_rate"] = static_cast(args::get(cli_args.play_rate)); + if (cli_args.in_point.Matched()) + actions["in_frame"] = static_cast(args::get(cli_args.in_point)); + + if (cli_args.out_point.Matched()) + actions["out_frame"] = static_cast(args::get(cli_args.out_point)); + if (args::get(cli_args.media_paths).size() == 1 and is_session(args::get(cli_args.media_paths)[0])) { actions["open_session"] = true; @@ -368,7 +457,7 @@ struct Launcher { } else { // check for media. auto playlist_name = args::get(cli_args.playlist_name).empty() - ? "Untitled Playlist" + ? "__DEFAULT_PUSH_PLAYLIST__" : args::get(cli_args.playlist_name); actions["playlists"][playlist_name] = nlohmann::json::array(); if (not args::get(cli_args.media_paths).empty()) @@ -383,50 +472,34 @@ struct Launcher { // failed reuse, spawn new actor.. if (not global_actor) { - // force player if single media.. - // auto count = 0; - // for (const auto &p : actions["playlists"].items()) - // count += p.value().size(); - // if(count == 1) - // actions["player"] = true; - // DIRTY HACK.. We need a way of controling this from the backend.. - // if(actions.value("player",false)) - - - // If the presentation mode is enabled on startup, set it - if (prefs.get("/ui/qml/enable_presentation_mode/value")) { - prefs.set( - "presentation_layout", "/ui/qml/main_window_settings/value/layout_name"); - } - - prefs.set(0, "/ui/qml/second_window_settings/value/visibility"); actions["new_instance"] = true; - - global_actor = self->spawn(static_cast(prefs)); - - // auto gsa = - // system.registry().get(global_store_registry); - - // self->anon_send(gsa, json_store::set_json_atom_v, static_cast(prefs)); - - request_receive(*self, global_actor, create_studio_atom_v, "XStudio"); + global_actor = CafActorSystem::global_actor(true, "XStudio", static_cast(prefs)); // this isn't great, the api is already running at this point.. // so we have to toggle it.. if (not actions["session_name"].empty()) - self->anon_send( - global_actor, + anon_mail( remote_session_name_atom_v, - static_cast(actions["session_name"])); + static_cast(actions["session_name"])) + .send(global_actor); + } // check for session file .. if (actions["open_session"]) { try { + + spdlog::stopwatch sw2; + JsonStore js = utility::open_session(actions["open_session_path"].get()); + spdlog::info( + "File {} loaded in {:.3} seconds.", + actions["open_session_path"].get(), + sw2); + if (actions["new_instance"]) { spdlog::stopwatch sw; auto new_session = self->spawn( @@ -459,16 +532,16 @@ struct Launcher { request_receive(*self, global_actor, session::session_atom_v); if (actions.count("set_play_rate")) - self->anon_send( - session, + anon_mail( playhead::playhead_rate_atom_v, - FrameRate(1.0 / static_cast(actions["set_play_rate"]))); + FrameRate(1.0 / static_cast(actions["set_play_rate"]))) + .send(session); if (actions.count("set_media_rate")) - self->anon_send( - session, + anon_mail( session::media_rate_atom_v, - FrameRate(1.0 / static_cast(actions["set_media_rate"]))); + FrameRate(1.0 / static_cast(actions["set_media_rate"]))) + .send(session); auto pm = request_receive(*self, global_actor, global::get_plugin_manager_atom_v); @@ -479,32 +552,11 @@ struct Launcher { if (p.value().empty()) continue; - caf::actor playlist; - - // If playlist name is "Untitled Playlist" (in other words no playlist - // was named to add media to) then try and get the current playlist - if (p.key() == "Untitled Playlist" and not actions["new_instance"]) { - try { - playlist = request_receive( - *self, session, session::current_playlist_atom_v); - } catch (...) { - try { - playlist = request_receive( - *self, session, session::get_playlist_atom_v); - } catch (...) { - } - } - } - - if (!playlist) - playlist = request_receive( - *self, session, session::get_playlist_atom_v, p.key()); - - // if not create it - if (!playlist) - playlist = request_receive( - *self, session, session::add_playlist_atom_v, p.key()) - .second.actor(); + // get the playlist to push media to. If p.key() is empty, session + // will use user prefs to decide which playlist to return. + caf::actor playlist = request_receive( + *self, session, session::get_push_playlist_atom_v, p.key()); + ; send_media( self, @@ -514,15 +566,18 @@ struct Launcher { p.value(), not actions["new_instance"], actions["compare"], - actions["quick_view"]); + actions["quick_view"], + actions["in_frame"], + actions["out_frame"]); media_sent = true; } // reset modified state.. if (actions["new_instance"]) { - self->delayed_anon_send( - session, std::chrono::seconds(3), last_changed_atom_v, utility::time_point()); + anon_mail(last_changed_atom_v, utility::time_point()) + .delay(std::chrono::seconds(3)) + .send(session); } else if (not media_sent) { // No op.. warn user.. spdlog::warn("You are already running xStudio, use -n to launch a new instance."); @@ -530,32 +585,25 @@ struct Launcher { } JsonStore load_preferences() { + std::vector pref_paths; for (const auto &p : args::get(cli_args.cli_pref_paths)) { pref_paths.push_back(p); } - - for (const auto &i : global_store::PreferenceContexts) - pref_paths.push_back(preference_path_context(i)); + std::vector override_paths; + for (const auto &p : args::get(cli_args.cli_override_pref_paths)) { + override_paths.push_back(p); + } auto prefs = JsonStore(); - if (not preference_load_defaults(prefs, xstudio_root("/preference"))) { - spdlog::error( - "Failed to load application preferences {}", xstudio_root("/preference")); + if (!global_store::load_preferences( + prefs, + !cli_args.user_prefs_off.Matched(), + pref_paths, + override_paths)) { + // xstudio will not work if application preferences could not be loaded std::exit(EXIT_FAILURE); } - - // prefs files *might* be located in a 'preference' subfolder under XSTUDIO_PLUGIN_PATH - // folders - char *plugin_path = std::getenv("XSTUDIO_PLUGIN_PATH"); - if (plugin_path) { - for (const auto &p : xstudio::utility::split(plugin_path, ':')) { - if (fs::is_directory(p + "/preferences")) - preference_load_defaults(prefs, p + "/preferences"); - } - } - - preference_load_overrides(prefs, pref_paths); return prefs; } @@ -588,7 +636,9 @@ struct Launcher { "session_name": "", "open_session": false, "debug": false, - "playlists": {} + "playlists": {}, + "in_frame": null, + "out_frame": null })"_json; JsonStore prefs; @@ -601,7 +651,6 @@ struct Launcher { std::string open_session_path; caf::actor global_actor; - std::vector> build_targets() { std::vector> targets; RemoteSessionManager rsm(remote_session_path()); @@ -655,7 +704,9 @@ struct Launcher { const std::vector &media, const bool remote, const std::string compare_mode, - const bool open_quick_view) { + const bool open_quick_view, + const nlohmann::json in_frame, + const nlohmann::json out_frame) { std::vector> uri_fl; std::vector files; @@ -690,21 +741,37 @@ struct Launcher { try { // test for dir.. try { - if (fs::is_directory(p)) { - auto items = scan_posix_path(p); + + // at this point, we must convert paths from local to + // absolute + std::string _p = p; + if (fs::exists(_p) && fs::path(_p).is_relative()) { + _p = fs::absolute(_p).string(); + } + if (fs::is_directory(_p)) { + auto items = scan_posix_path(_p); uri_fl.insert(uri_fl.end(), items.begin(), items.end()); continue; - } else if (fs::is_regular_file(p)) { - files.push_back(p); + } else if (fs::is_regular_file(_p)) { + files.push_back(_p); continue; } + } catch ([[maybe_unused]] const std::exception &err) { } // add to scan list.. FrameList fl; - caf::uri uri = parse_cli_posix_path(p, fl, true); - uri_fl.emplace_back(std::make_pair(uri, fl)); + if (p.find("http") == 0) { + // TODO: extend parse_cli_posix_path to handle http protocol. + auto uri = caf::make_uri(p); + if (uri) { + uri_fl.emplace_back(std::make_pair(*uri, fl)); + } + } else { + caf::uri uri = parse_cli_posix_path(p, fl, true); + uri_fl.emplace_back(std::make_pair(uri, fl)); + } } catch (const std::exception &e) { spdlog::error("Failed to load media '{}'", e.what()); @@ -730,16 +797,17 @@ struct Launcher { // set the playhead to the given compare mode. The compare mode // attribute is called 'Compare' - we can set it using this handy // message handler inherited from the Module class. - self->anon_send( - playhead, + anon_mail( module::change_attribute_value_atom_v, std::string("Compare"), utility::JsonStore(compare_mode), - true); + true) + .send(playhead); } for (const auto &i : uri_fl) { try { + // spdlog::warn("{}", to_string(i.first)); added_media.push_back(request_receive( *self, playlist, @@ -758,37 +826,72 @@ struct Launcher { } // get the actor that is responsible for selecting items from the playlist - // for viewing - if (not open_quick_view && not compare_mode.empty()) { + // for viewing. If we're doing quickview, we don't want to fiddle with + // what's on screen though as the media is going to be shown in a + // quickview window anyway. + if (not open_quick_view) { + auto playhead_selection_actor = request_receive(*self, playlist, playlist::selection_actor_atom_v); if (playhead_selection_actor) { + // Reset the current selection so that the added media is what is // selected. - UuidList selection; - for (auto &new_media : added_media) { - selection.push_back(new_media.uuid()); + UuidVector selection; + if (not compare_mode.empty()) { + // if we're doing a compare, select all the new media for + // comparison + selection.reserve(added_media.size()); + for (auto &new_media : added_media) { + selection.push_back(new_media.uuid()); + } + } else if (added_media.size()) { + // otherwise, select the first media item to whack on screen + selection.push_back(added_media.front()); } - self->anon_send( - playhead_selection_actor, playlist::select_media_atom_v, selection); + anon_mail(playlist::select_media_atom_v, selection) + .send(playhead_selection_actor); } } - // to ensure what we've added appears on screen we need to - // make the playlist the 'current' one - i.e. the one being viewer - anon_send(session, session::current_playlist_atom_v, playlist); - + if (not open_quick_view) { + // now set in/out loop points if specified + const int in = in_frame.is_number() ? in_frame.get() : -1; + const int out = out_frame.is_number() ? out_frame.get() : -1; + caf::actor playhead = + request_receive(*self, playlist, playlist::get_playhead_atom_v) + .actor(); + if (playhead && (in != -1 || out != -1)) { + // we delay the send because the playhead will update its in/out + // points when new media is shown, so we need to wait until the + // new media is set-up in the playhead + if (out != -1) + anon_mail(playhead::simple_loop_end_atom_v, out) + .delay(std::chrono::milliseconds(500)) + .send(playhead); + if (in != -1) + anon_mail(playhead::simple_loop_start_atom_v, in) + .delay(std::chrono::milliseconds(500)) + .send(playhead); + + anon_mail(playhead::use_loop_range_atom_v, true) + .delay(std::chrono::milliseconds(500)) + .send(playhead); + } + } // even if 'open_quick_view' is false, we send a message to the session // because auto-opening of quickview can be controlled via a preference - - anon_send( - session, - ui::open_quickview_window_atom_v, - added_media, - compare_mode, - open_quick_view); + if (open_quick_view) { + anon_mail( + ui::open_quickview_window_atom_v, + added_media, + compare_mode, + JsonStore(in_frame), + JsonStore(out_frame)) + .send(session); + } } caf::actor try_reuse_session() { @@ -802,15 +905,19 @@ struct Launcher { // try open ? auto connecting = system.spawn(connect_to_remote); try { - auto a = request_receive_wait( + auto remote = request_receive_wait( *self, connecting, std::chrono::seconds(2), caf::connect_atom_v, host, port); + + auto auth = request_receive_wait( + *self, remote, std::chrono::seconds(2), authenticate_atom_v); + spdlog::info("Connected to session '{}' at {}:{}", name, host, port); - return a; + return auth; } catch (const std::exception &err) { spdlog::debug("Failed to connect '{}'", err.what()); } @@ -845,272 +952,91 @@ struct Launcher { int main(int argc, char **argv) { - ACTOR_INIT_GLOBAL_META() - core::init_global_meta_objects(); - io::middleman::init_global_meta_objects(); - - // As far as I can tell caf only allows config to be modified - // through cli args. We prefer the 'sharing' policy rather than - // 'stealing'. The latter results in multiple threads spinning - // aggressively pushing CPU load very high during playback. - - // const char *args[] = {argv[0], - // "--caf.work-stealing.aggressive-poll-attempts=0","--caf.logger.console.verbosity=trace"}; - - const char *args[] = { - argv[0], - "--caf.scheduler.max-threads=128", - "--caf.scheduler.policy=sharing", - "--caf.logger.console.verbosity=trace"}; + // this call sets up the CAF actor system + CafActorSystem::instance(); + + try { - caf_config config{4, const_cast(args)}; + // Launcher class handles CLI parsing and communication with already running + // xstudio instances + Launcher l(argc, argv, CafActorSystem::system()); - config.add_actor_type("Gap"); - config.add_actor_type("Stack"); - config.add_actor_type( - "Clip"); - config.add_actor_type( - "Track"); - - { - - try { - - // create the actor system - actor_system system(config); - - // store a reference to the actor system, so we can access it - // via static method anywhere else we need to (mainly, the python - // module instanced in the embedded python interpreter) - utility::ActorSystemSingleton::actor_system_ref(system); - - scoped_actor self{system}; - Launcher l(argc, argv, system); + // add to current session and exit + if (not l.actions["new_instance"]) { + stop_logger(); + std::exit(EXIT_SUCCESS); + } - // add to current session and exit - if (not l.actions["new_instance"]) { - stop_logger(); - std::exit(EXIT_SUCCESS); - } +#ifndef _WIN32 + struct sigaction sigIntHandler; + sigIntHandler.sa_handler = my_handler; + sigemptyset(&sigIntHandler.sa_mask); + sigIntHandler.sa_flags = 0; + sigaction(SIGINT, &sigIntHandler, nullptr); +#endif - if (l.actions["headless"]) { - system.await_actors_before_shutdown(true); - // TODO: Ahead Fix - // struct sigaction sigIntHandler; + if (l.actions["headless"]) { - // sigIntHandler.sa_handler = my_handler; - // sigemptyset(&sigIntHandler.sa_mask); - // sigIntHandler.sa_flags = 0; + // Headless mode. - // sigaction(SIGINT, &sigIntHandler, nullptr); + scoped_actor self{CafActorSystem::system()}; - while (not shutdown_xstudio) { - // we should be able to shutdown via a API call.. - try { - request_receive(*self, l.global_actor, status_atom_v); - } catch (...) { - // global actor is dead, probably shutdown via API - break; - } - std::this_thread::sleep_for(1s); + while (not shutdown_xstudio) { + // we should be able to shutdown via a API call.. + try { + request_receive(*self, l.global_actor, status_atom_v); + } catch (...) { + // global actor is dead, probably shutdown via API + break; } - // cancel inflight events. - system.clock().cancel_all(); - if (shutdown_xstudio) - self->send_exit(l.global_actor, caf::exit_reason::user_shutdown); std::this_thread::sleep_for(1s); - } else { - system.await_actors_before_shutdown(true); - - QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); - // QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); - // QCoreApplication::setAttribute(Qt::AA_UseOpenGLES); - // QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL); + } + // cancel inflight events. + // system.clock().cancel_all(); + if (shutdown_xstudio) + self->send_exit(l.global_actor, caf::exit_reason::user_shutdown); + std::this_thread::sleep_for(1s); - if (l.actions["disable_vsync"]) { - QSurfaceFormat format; - format.setSwapInterval(0); - QSurfaceFormat::setDefaultFormat(format); - } + } else { - if (l.actions["share_opengl_contexts"]) { - QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); - } + // Run the QApplication and launch UI + execute_xstudio_ui( + l.actions["disable_vsync"], + l.prefs.get("/ui/qml/global_ui_scale_factor").value("value", 1.0f), + l.actions["silence_qt_warnings"], + argc, + argv + ); - QApplication app(argc, argv); - app.setOrganizationName("DNEG"); - app.setOrganizationDomain("dneg.com"); - app.setApplicationVersion(PROJECT_VERSION); - app.setApplicationName("xStudio"); - app.setWindowIcon(QIcon(":images/xstudio_logo_256_v1.svg")); - - // auto palette = app.palette(); - // palette.setColor(QPalette::Normal, QPalette::Highlight, QColor("Red")); - // palette.setColor(QPalette::Normal, QPalette::HighlightedText, QColor("Red")); - // app.setPalette(palette); - - qmlRegisterType("xstudio.qml.semver", 1, 0, "SemVer"); - qmlRegisterType( - "xstudio.qml.cursor_pos_provider", 1, 0, "CursorPosProvider"); - qmlRegisterType("xstudio.qml.viewport", 1, 0, "XsHotkey"); - qmlRegisterType("xstudio.qml.viewport", 1, 0, "XsHotkeysInfo"); - qmlRegisterType( - "xstudio.qml.viewport", 1, 0, "XsHotkeyReference"); - - qmlRegisterType("xstudio.qml.viewport", 1, 0, "Viewport"); - - qmlRegisterType( - "xstudio.qml.bookmarks", 1, 0, "XsBookmarkCategories"); - qmlRegisterType( - "xstudio.qml.bookmarks", 1, 0, "XsBookmarkModel"); - qmlRegisterType( - "xstudio.qml.bookmarks", 1, 0, "XsBookmarkFilterModel"); - - qmlRegisterType( - "xstudio.qml.embedded_python", 1, 0, "EmbeddedPython"); - - qmlRegisterType("xstudio.qml.uuid", 1, 0, "QMLUuid"); - qmlRegisterType("xstudio.qml.clipboard", 1, 0, "Clipboard"); - - qmlRegisterType( - "xstudio.qml.module", 1, 0, "XsModuleAttributesModel"); - qmlRegisterType( - "xstudio.qml.module", 1, 0, "XsOrderedModuleAttributesModel"); - qmlRegisterType( - "xstudio.qml.module", 1, 0, "XsModuleAttributes"); - qmlRegisterType("xstudio.qml.module", 1, 0, "XsModuleMenu"); - qmlRegisterType("xstudio.qml.event", 1, 0, "XsEvent"); - - qmlRegisterType( - "xstudio.qml.global_store_model", 1, 0, "XsGlobalStoreModel"); - qmlRegisterType("xstudio.qml.helpers", 1, 0, "XsModelProperty"); - qmlRegisterType("xstudio.qml.helpers", 1, 0, "XsModelRowCount"); - qmlRegisterType( - "xstudio.qml.helpers", 1, 0, "XsModelPropertyMap"); - qmlRegisterType( - "xstudio.qml.helpers", 1, 0, "XsModelNestedPropertyMap"); - qmlRegisterType( - "xstudio.qml.helpers", 1, 0, "XsModelPropertyTree"); - - qmlRegisterType("xstudio.qml.session", 1, 0, "XsSessionModel"); - - qmlRegisterType("xstudio.qml.models", 1, 0, "XsSnapshotModel"); - - qmlRegisterType("xstudio.qml.models", 1, 0, "XsMenusModel"); - qmlRegisterType("xstudio.qml.models", 1, 0, "XsModuleData"); - qmlRegisterType( - "xstudio.qml.models", 1, 0, "XsReskinPanelsLayoutModel"); - qmlRegisterType( - "xstudio.qml.models", 1, 0, "XsMediaListColumnsModel"); - - qmlRegisterType("xstudio.qml.models", 1, 0, "XsViewsModel"); - - qmlRegisterType("xstudio.qml.models", 1, 0, "XsMenuModelItem"); - - qRegisterMetaType("QQmlPropertyMap*"); - - // Add a CafSystemObject to the application - this is QObject that simply - // holds a reference to the actor system so that we can access the system - // in Qt main loop - new CafSystemObject(&app, system); - - const QUrl url( - l.actions["reskin"] ? QStringLiteral("qrc:/main_reskin.qml") - : QStringLiteral("qrc:/main.qml")); - - QQmlApplicationEngine engine; - engine.addImageProvider(QLatin1String("thumbnail"), new ThumbnailProvider); - engine.addImageProvider(QLatin1String("shotgun"), new ShotgunProvider); - engine.rootContext()->setContextProperty( - "applicationDirPath", QGuiApplication::applicationDirPath()); - - auto helper = new Helpers(&engine); - engine.rootContext()->setContextProperty("helpers", helper); - - auto studio = new StudioUI(system, &app); - engine.rootContext()->setContextProperty("studio", studio); - - auto logger = new LogModel(&engine); - auto proxylogger = new LogFilterModel(&engine); - proxylogger->setSourceModel(logger); - - engine.rootContext()->setContextProperty( - "CurrentDirPath", QString(QDir::currentPath())); - engine.rootContext()->setContextProperty("logger", proxylogger); - // connect logger. - auto logsink = std::make_shared(logger); - spdlog::get("xstudio")->sinks().push_back(logsink); - - engine.addImportPath("qrc:///"); - engine.addImportPath("qrc:///extern"); - - // gui plugins.. - engine.addImportPath(QStringFromStd(xstudio_root("/plugin/qml"))); - engine.addPluginPath(QStringFromStd(xstudio_root("/plugin/qml"))); - - char *plugin_path = std::getenv("XSTUDIO_PLUGIN_PATH"); - if (plugin_path) { - for (const auto &p : xstudio::utility::split(plugin_path, ':')) { - engine.addPluginPath(QStringFromStd(p + "/qml")); - engine.addImportPath(QStringFromStd(p + "/qml")); - } - } + // save state.. BUT NOT WHEN user_prefs_off + if (not l.actions.value("user_prefs_off", false)) { - QObject::connect( - &engine, - &QQmlApplicationEngine::objectCreated, - &app, - [url](QObject *obj, const QUrl &objUrl) { - if (!obj && url == objUrl) { - QCoreApplication::exit(-1); - } - }, - Qt::QueuedConnection); - - engine.load(url); - spdlog::info("XStudio UI launched."); - - app.exec(); - // fingers crossed... - // need to stop monitoring or we'll be sending events to a dead QtObject - spdlog::get("xstudio")->sinks().pop_back(); - // save state.. BUT NOT WHEN USING PLAYER MODE.. (THIS needs work) - // if we store prefs then we affect the normal mode.. (DUAL mode for prefs ?) - if (not l.actions.value("player", false)) { - for (const auto &context : global_store::PreferenceContexts) { - try { - request_receive( - *self, - system.registry().get(global_store_registry), - global_store::save_atom_v, - context); - } catch (const std::exception &err) { - spdlog::warn("Failed to save prefs {}", err.what()); - } + scoped_actor self{CafActorSystem::system()}; + for (const auto &context : global_store::PreferenceContexts) { + try { + request_receive( + *self, + CafActorSystem::system().registry().get(global_store_registry), + global_store::save_atom_v, + context); + } catch (const std::exception &err) { + spdlog::warn("Failed to save prefs {}", err.what()); } } - // cancel actors talking to them selves. - system.clock().cancel_all(); - self->send_exit(l.global_actor, caf::exit_reason::user_shutdown); - std::this_thread::sleep_for(1s); } - // in the case where ther are actors that are still 'alive' - // we do not exit this scope as actor_system will block in - // its destructor (due to await_actors_before_shutdown(true)). - // The exit_timeout_killer will kill the process after some - // delay so we don't have zombie xstudio instances running. - exit_timeout_killer.start(); + l.global_actor = caf::actor(); - } catch (const std::exception &err) { - spdlog::critical("{} {}", __PRETTY_FUNCTION__, err.what()); - stop_logger(); - std::exit(EXIT_FAILURE); } - exit_timeout_killer.stop(); + } catch (const std::exception &err) { + spdlog::critical("{} {}", __PRETTY_FUNCTION__, err.what()); + stop_logger(); + std::exit(EXIT_FAILURE); } - stop_logger(); + CafActorSystem::exit(); + stop_logger(); std::exit(EXIT_SUCCESS); + } diff --git a/src/launch/xstudio/src/xstudio.sh.in b/src/launch/xstudio/src/xstudio.sh.in old mode 100755 new mode 100644 index 2c6957f0c..9acb9f695 --- a/src/launch/xstudio/src/xstudio.sh.in +++ b/src/launch/xstudio/src/xstudio.sh.in @@ -12,7 +12,7 @@ then export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$XSTUDIO_ROOT/lib else export XSTUDIO_ROOT=@CMAKE_INSTALL_PREFIX@/share/xstudio - export LD_LIBRARY_PATH=$XSTUDIO_ROOT/lib:/home/ted/Qt/5.15.2/gcc_64/lib:$LD_LIBRARY_PATH + export LD_LIBRARY_PATH=$XSTUDIO_ROOT/lib:$LD_LIBRARY_PATH export PYTHONPATH=@CMAKE_INSTALL_PREFIX@/lib/python:$PYTHONPATH fi fi @@ -21,4 +21,4 @@ xstudio_desktop_integration SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -exec ${SCRIPT_DIR}/xstudio.bin "$@" +exec ${SCRIPT_DIR}/xstudio.bin "$@" \ No newline at end of file diff --git a/src/launch/xstudio/src/xstudio_app.ico b/src/launch/xstudio/src/xstudio_app.ico new file mode 100644 index 000000000..e3282a168 Binary files /dev/null and b/src/launch/xstudio/src/xstudio_app.ico differ diff --git a/src/launch/xstudio/src/xstudio_desktop_integration.sh b/src/launch/xstudio/src/xstudio_desktop_integration.sh old mode 100755 new mode 100644 diff --git a/src/launch/xstudio/src/xstudio_file.ico b/src/launch/xstudio/src/xstudio_file.ico new file mode 100644 index 000000000..aed1466b7 Binary files /dev/null and b/src/launch/xstudio/src/xstudio_file.ico differ diff --git a/src/launch/xstudio/src/xstudio_win_resource.rc b/src/launch/xstudio/src/xstudio_win_resource.rc new file mode 100644 index 000000000..639c05430 --- /dev/null +++ b/src/launch/xstudio/src/xstudio_win_resource.rc @@ -0,0 +1 @@ +ID1_ICON1 ICON DISCARDABLE "xstudio_app.ico" \ No newline at end of file diff --git a/src/launch/xstudio/test/CMakeLists.txt b/src/launch/xstudio/test/CMakeLists.txt index a278e704b..ccc1d7784 100644 --- a/src/launch/xstudio/test/CMakeLists.txt +++ b/src/launch/xstudio/test/CMakeLists.txt @@ -1,7 +1,7 @@ include(CTest) SET(LINK_DEPS - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/media/src/CMakeLists.txt b/src/media/src/CMakeLists.txt index cb6853881..5dd42eb39 100644 --- a/src/media/src/CMakeLists.txt +++ b/src/media/src/CMakeLists.txt @@ -1,10 +1,14 @@ SET(LINK_DEPS xstudio::json_store - xstudio::playhead xstudio::utility - caf::core + CAF::core ) -create_component(media 0.1.0 "${LINK_DEPS}") +SET(STATIC_LINK_DEPS + xstudio::utility_static + xstudio::json_store_static + CAF::core +) +create_component_static(media ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${STATIC_LINK_DEPS}") \ No newline at end of file diff --git a/src/media/src/media.cpp b/src/media/src/media.cpp index 12192bf95..c7cb20b67 100644 --- a/src/media/src/media.cpp +++ b/src/media/src/media.cpp @@ -7,6 +7,22 @@ using namespace xstudio::media; using namespace xstudio::utility; +MediaKey::MediaKey(const std::string &o) : std::string(o) { + hash_ = std::hash{}(o); +} + +MediaKey::MediaKey( + const std::string &key_format, + const caf::uri &uri, + const int frame, + const std::string &stream_id) + : std::string(fmt::format( + fmt::runtime(key_format), + to_string(uri), + (frame == std::numeric_limits::min() ? 0 : frame), + stream_id)) { + hash_ = std::hash{}(static_cast(*this)); +} Media::Media(const JsonStore &jsn) : Container(static_cast(jsn["container"])) { diff --git a/src/media/src/media_actor.cpp b/src/media/src/media_actor.cpp index 11f3997f4..c5fb30ac1 100644 --- a/src/media/src/media_actor.cpp +++ b/src/media/src/media_actor.cpp @@ -1,10 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include + #include #include +#include #include "xstudio/atoms.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" +#include "xstudio/global_store/global_store.hpp" #include "xstudio/json_store/json_store_actor.hpp" #include "xstudio/media/media_actor.hpp" #include "xstudio/playhead/sub_playhead.hpp" @@ -31,6 +35,8 @@ void MediaActor::deserialise(const JsonStore &jsn) { } link_to(json_store_); + // join out metadata handler. propagate events from it. + join_event_group(this, json_store_); for (const auto &[key, value] : jsn.at("actors").items()) { if (value.at("base").at("container").at("type") == "MediaSource") { @@ -39,10 +45,8 @@ void MediaActor::deserialise(const JsonStore &jsn) { media_sources_[ukey] = spawn(static_cast(value)); - send( - media_sources_[ukey], - utility::parent_atom_v, - UuidActor(base_.uuid(), this)); + mail(utility::parent_atom_v, UuidActor(base_.uuid(), this)) + .send(media_sources_[ukey]); link_to(media_sources_[ukey]); join_event_group(this, media_sources_[ukey]); } catch (const std::exception &e) { @@ -67,7 +71,7 @@ MediaActor::MediaActor(caf::actor_config &cfg, const JsonStore &jsn, const bool init(); if (async) - anon_send(this, module::deserialise_atom_v, jsn); + anon_mail(module::deserialise_atom_v, jsn).send(this); else deserialise(jsn); } @@ -86,6 +90,9 @@ MediaActor::MediaActor( utility::Uuid::generate(), utility::JsonStore(), std::chrono::milliseconds(50)); link_to(json_store_); + // join out metadata handler. propagate events from it. + join_event_group(this, json_store_); + init(); utility::UuidActorVector good_sources; @@ -103,7 +110,7 @@ MediaActor::MediaActor( good_sources.push_back(i); } - anon_send(caf::actor_cast(this), add_media_source_atom_v, good_sources); + anon_mail(add_media_source_atom_v, good_sources).send(caf::actor_cast(this)); } caf::message_handler MediaActor::default_event_handler() { @@ -115,53 +122,69 @@ caf::message_handler MediaActor::default_event_handler() { const std::tuple &) {}, [=](utility::event_atom, add_media_source_atom, const utility::UuidActorVector &) {}, [=](utility::event_atom, media_status_atom, const MediaStatus ms) {}, - [=](utility::event_atom, bookmark::bookmark_change_atom, const utility::Uuid &) {}}; + [=](json_store::update_atom, + const JsonStore &, + const std::string &, + const JsonStore &) {}, + [=](json_store::update_atom, const JsonStore &) {}, + [=](utility::event_atom, media::media_display_info_atom, const utility::JsonStore &) { + }}; } -void MediaActor::init() { - // only parial.. - event_group_ = spawn(this); - link_to(event_group_); - - print_on_create(this, base_); - print_on_exit(this, base_); - - behavior_.assign( - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), +caf::message_handler MediaActor::message_handler() { + return caf::message_handler{ [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, // [=](bookmark::associate_bookmark_atom atom, caf::actor bookmarks) { - // delegate(bookmarks, atom, UuidActor(base_.uuid(), actor_cast(this))); + // mail(atom, UuidActor(base_.uuid(), + // actor_cast(this))).delegate(bookmarks); // }, [=](module::deserialise_atom, const utility::JsonStore &jsn) { deserialise(jsn); }, + [=](utility::event_atom, + media::media_display_info_atom, + const utility::JsonStore &metadata_filter_sets) { + media_list_columns_config_ = + utility::json_to_tree(metadata_filter_sets, "children"); + anon_mail(media_display_info_atom_v).send(this); + }, + [=](utility::event_atom, bookmark::bookmark_change_atom, const utility::Uuid &bookamrk_uuid) { - send( - event_group_, - utility::event_atom_v, - bookmark::bookmark_change_atom_v, - bookamrk_uuid); + if (std::find(bookmark_uuids_.begin(), bookmark_uuids_.end(), bookamrk_uuid) == + bookmark_uuids_.end()) { + bookmark_uuids_.emplace_back(bookamrk_uuid); + mail( + utility::event_atom_v, + bookmark::bookmark_change_atom_v, + base_.uuid(), + bookmark_uuids_) + .send(base_.event_group()); + } + mail(utility::event_atom_v, bookmark::bookmark_change_atom_v, bookamrk_uuid) + .send(base_.event_group()); }, + [=](bookmark::get_bookmarks_atom) -> utility::UuidList { return bookmark_uuids_; }, + [=](utility::event_atom, bookmark::remove_bookmark_atom, const utility::Uuid &bookmark_uuid) { - send( - event_group_, - utility::event_atom_v, - bookmark::bookmark_change_atom_v, - bookmark_uuid); + auto p = std::find(bookmark_uuids_.begin(), bookmark_uuids_.end(), bookmark_uuid); + if (p != bookmark_uuids_.end()) { + bookmark_uuids_.erase(p); + mail( + utility::event_atom_v, + bookmark::bookmark_change_atom_v, + base_.uuid(), + bookmark_uuids_) + .send(base_.event_group()); + } + + mail(utility::event_atom_v, bookmark::bookmark_change_atom_v, bookmark_uuid) + .send(base_.event_group()); }, [=](rescan_atom atom) -> result { @@ -255,36 +278,47 @@ void MediaActor::init() { }, [=](add_media_source_atom, const UuidActor &source_media) -> result { + auto rp = make_response_promise(); + if (source_media.uuid().is_null() or not source_media.actor()) { spdlog::warn( "{} Invalid source {} {}", __PRETTY_FUNCTION__, to_string(source_media.uuid()), to_string(source_media.actor())); - return make_error(xstudio_error::error, "Invalid MediaSource"); + rp.deliver(make_error(xstudio_error::error, "Invalid MediaSource")); + return rp; } media_sources_[source_media.uuid()] = source_media.actor(); link_to(source_media.actor()); join_event_group(this, source_media.actor()); base_.add_media_source(source_media.uuid()); - auto_set_current_source(media::MT_IMAGE); - auto_set_current_source(media::MT_AUDIO); - base_.send_changed(event_group_, this); - send(source_media.actor(), utility::parent_atom_v, UuidActor(base_.uuid(), this)); + mail(playhead::media_source_atom_v) + .request(caf::actor_cast(this), infinite) + .then( + [=](bool) mutable { + base_.send_changed(); + mail(utility::parent_atom_v, UuidActor(base_.uuid(), this)) + .send(source_media.actor()); - send( - event_group_, - utility::event_atom_v, - add_media_source_atom_v, - UuidActorVector({source_media})); + mail( + utility::event_atom_v, + add_media_source_atom_v, + UuidActorVector({source_media})) + .send(base_.event_group()); - return source_media.uuid(); + rp.deliver(source_media.uuid()); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + + return rp; }, [=](add_media_source_atom atom, caf::actor source_media) -> result { auto rp = make_response_promise(); - request(source_media, infinite, uuid_atom_v) + mail(uuid_atom_v) + .request(source_media, infinite) .then( [=](const Uuid &uuid) mutable { rp.delegate( @@ -295,6 +329,8 @@ void MediaActor::init() { }, [=](add_media_source_atom, const UuidActorVector &sources) -> result { + auto rp = make_response_promise(); + UuidActorVector good_sources; for (const auto &source_media : sources) { @@ -310,19 +346,31 @@ void MediaActor::init() { link_to(source_media.actor()); join_event_group(this, source_media.actor()); base_.add_media_source(source_media.uuid()); - auto_set_current_source(media::MT_IMAGE); - auto_set_current_source(media::MT_AUDIO); - send( - source_media.actor(), - utility::parent_atom_v, - UuidActor(base_.uuid(), this)); + mail(utility::parent_atom_v, UuidActor(base_.uuid(), this)) + .send(source_media.actor()); + mail( + utility::event_atom_v, + add_media_source_atom_v, + UuidActorVector({source_media})) + .send(base_.event_group()); } } - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, add_media_source_atom_v, good_sources); + base_.send_changed(); + mail(utility::event_atom_v, add_media_source_atom_v, good_sources) + .send(base_.event_group()); - return true; + // select a current media source if necessary + mail(playhead::media_source_atom_v) + .request(caf::actor_cast(this), infinite) + .then( + [=](bool) mutable { + base_.send_changed(); + rp.deliver(true); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + + return rp; }, [=](add_media_source_atom, @@ -342,12 +390,10 @@ void MediaActor::init() { : spawn( (ext.empty() ? "UNKNOWN" : ext), uri, frame_list, rate, source_uuid); - anon_send(source, media_metadata::get_metadata_atom_v); - request( - actor_cast(this), - infinite, - add_media_source_atom_v, - UuidActor(source_uuid, source)) + // anon_mail(media_metadata::get_metadata_atom_v).send(source); + + mail(add_media_source_atom_v, UuidActor(source_uuid, source)) + .request(actor_cast(this), infinite) .then( [=](const Uuid &uuid) mutable { rp.deliver(UuidActor(uuid, source)); }, [=](const error &err) mutable { rp.deliver(err); }); @@ -365,7 +411,8 @@ void MediaActor::init() { // order in source_refs. We also want the names to reflect // source_names. Here goes ... - request(caf::actor_cast(this), infinite, media_reference_atom_v) + mail(media_reference_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](const std::vector &existing_source_refs) mutable { @@ -412,11 +459,84 @@ void MediaActor::init() { return rp; }, - [=](current_media_source_atom) -> caf::result { - if (base_.empty() or not media_sources_.count(base_.current())) - return make_error(xstudio_error::error, "No MediaSources"); + [=](current_media_source_atom, const bool current_names) + -> caf::result>> { + auto rp = make_response_promise< + std::pair>>(); + + if (base_.empty() or not media_sources_.count(base_.current())) { + rp.deliver(std::make_pair( + UuidActor(base_.uuid(), this), + std::make_pair(std::string(), std::string()))); + } + + auto gtype = MT_IMAGE; + if (not media_sources_.count(base_.current(MT_IMAGE))) + gtype = MT_AUDIO; + + mail(name_atom_v) + .request(media_sources_.at(base_.current(gtype)), infinite) + .then( + [=](const std::string &name) mutable { + if (base_.current(gtype) == base_.current(MT_AUDIO) or + not media_sources_.count(base_.current(MT_AUDIO))) { + rp.deliver(std::make_pair( + UuidActor(base_.uuid(), this), std::make_pair(name, name))); + } else { + mail(name_atom_v) + .request(media_sources_.at(base_.current(MT_AUDIO)), infinite) + .then( + [=](const std::string &aname) mutable { + rp.deliver(std::make_pair( + UuidActor(base_.uuid(), this), + std::make_pair(name, aname))); + }, + [=](error &err) mutable { + rp.deliver(std::make_pair( + UuidActor(base_.uuid(), this), + std::make_pair(std::string(), std::string()))); + spdlog::warn( + "{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } + }, + [=](error &err) mutable { + rp.deliver(std::make_pair( + UuidActor(base_.uuid(), this), + std::make_pair(std::string(), std::string()))); + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); - return UuidActor(base_.current(), media_sources_.at(base_.current())); + return rp; + }, + + // dead.. + // [=](current_media_source_atom) -> caf::result { + // if (base_.empty() or not media_sources_.count(base_.current())) + // return make_error(xstudio_error::error, "No MediaSources"); + + // return UuidActor(base_.current(), media_sources_.at(base_.current())); + // }, + + + [=](current_media_source_atom) + -> caf::result>> { + auto iua = UuidActor(); + auto aua = UuidActor(); + if (media_sources_.count(base_.current(MT_IMAGE))) + iua = UuidActor( + base_.current(MT_IMAGE), media_sources_.at(base_.current(MT_IMAGE))); + + if (media_sources_.count(base_.current(MT_AUDIO))) + aua = UuidActor( + base_.current(MT_AUDIO), media_sources_.at(base_.current(MT_AUDIO))); + + if (iua.uuid().is_null()) + iua = aua; + else if (aua.uuid().is_null()) + aua = iua; + + return std::make_pair(UuidActor(base_.uuid(), this), std::make_pair(iua, aua)); }, [=](current_media_source_atom, const MediaType media_type) -> caf::result { @@ -432,34 +552,77 @@ void MediaActor::init() { // might need this when adding initial media source ? // do we need to specify which media type is changing ? if (result) { - base_.send_changed(event_group_, this); - send( - event_group_, + base_.send_changed(); + anon_mail(media_display_info_atom_v).send(this); + mail( utility::event_atom_v, current_media_source_atom_v, UuidActor( base_.current(media_type), media_sources_.at(base_.current(media_type))), - media_type); + media_type) + .send(base_.event_group()); } return result; }, - [=](current_media_source_atom atom, const Uuid &uuid) { + [=](utility::event_atom, + media_reader::get_thumbnail_atom, + thumbnail::ThumbnailBufferPtr buf) {}, + + [=](media_reader::get_thumbnail_atom, + float position) -> result { + if (base_.empty() or not media_sources_.count(base_.current(MT_IMAGE))) + return make_error(xstudio_error::error, "No MediaSources"); + + auto rp = make_response_promise(); + mail(media_reader::get_thumbnail_atom_v, position) + .request(media_sources_.at(base_.current(media::MT_IMAGE)), infinite) + .then( + [=](thumbnail::ThumbnailBufferPtr &buf) mutable { + rp.deliver(buf); + mail(utility::event_atom_v, media_reader::get_thumbnail_atom_v, buf) + .send(base_.event_group()); + }, + [=](error &err) mutable { rp.deliver(err); }); + return rp; + }, + + [=](media_reader::get_thumbnail_atom, + float position) -> result { + if (base_.empty() or not media_sources_.count(base_.current(MT_IMAGE))) + return make_error(xstudio_error::error, "No MediaSources"); + + auto rp = make_response_promise(); + mail(media_reader::get_thumbnail_atom_v, position) + .request(media_sources_.at(base_.current(media::MT_IMAGE)), infinite) + .then( + [=](thumbnail::ThumbnailBufferPtr &buf) mutable { + rp.deliver(buf); + mail(utility::event_atom_v, media_reader::get_thumbnail_atom_v, buf) + .send(base_.event_group()); + }, + [=](error &err) mutable { rp.deliver(err); }); + return rp; + }, + + [=](current_media_source_atom, const Uuid &uuid) -> bool { auto result = base_.set_current(uuid, MT_IMAGE); result |= base_.set_current(uuid, MT_AUDIO); // might need this when adding initial media source ? // do we need to specify which media type is changing ? if (result) { - base_.send_changed(event_group_, this); - send( - event_group_, + anon_mail(media_display_info_atom_v).send(this); + anon_mail(human_readable_info_atom_v, true).send(this); + base_.send_changed(); + mail( utility::event_atom_v, current_media_source_atom_v, UuidActor(base_.current(), media_sources_.at(base_.current())), - MT_IMAGE); + MT_IMAGE) + .send(base_.event_group()); } return result; @@ -467,70 +630,6 @@ void MediaActor::init() { [=](timeline::duration_atom, const timebase::flicks &new_duration) -> bool { return false; }, - [=](get_edit_list_atom atom, - const MediaType media_type, - const Uuid &uuid) -> caf::result { - if (base_.empty() or not media_sources_.count(base_.current(media_type))) - return make_error(xstudio_error::error, "No MediaSources"); - - auto rp = make_response_promise(); - - // Audio media source must have same duration as video media source. - // We force that here. - if (media_type == media::MT_IMAGE || - not media_sources_.count(base_.current(media::MT_IMAGE))) { - rp.delegate( - media_sources_.at(base_.current(media_type)), - atom, - media_type, - (uuid.is_null() ? base_.uuid() : uuid)); - } else { - - request( - media_sources_.at(base_.current(media::MT_AUDIO)), - infinite, - atom, - media::MT_AUDIO, - uuid.is_null() ? base_.uuid() : uuid) - .then( - [=](utility::EditList audio_edl) mutable { - request( - media_sources_.at(base_.current(media::MT_IMAGE)), - infinite, - atom, - media::MT_IMAGE, - uuid.is_null() ? base_.uuid() : uuid) - .then( - [=](const utility::EditList &video_edl) mutable { - ClipList audio_clips = audio_edl.section_list(); - ClipList video_clips = video_edl.section_list(); - if (audio_clips.size() != video_clips.size()) { - // audio doesn't match video so just return - // unmodified audio EDL - rp.deliver(audio_edl); - return; - } - - ClipList modified_audio_clips; - for (size_t i = 0; i < audio_clips.size(); ++i) { - modified_audio_clips.push_back(audio_clips[i]); - modified_audio_clips.back() - .frame_rate_and_duration_.set_seconds( - video_clips[i] - .frame_rate_and_duration_.seconds()); - modified_audio_clips.back().timecode_ = - video_clips[i].timecode_; - } - rp.deliver(utility::EditList(modified_audio_clips)); - }, - [=](error &err) mutable { rp.deliver(err); }); - }, - [=](error &err) mutable { rp.deliver(err); }); - } - - - return rp; - }, [=](get_media_pointer_atom atom, const MediaType media_type) -> caf::result> { @@ -547,85 +646,47 @@ void MediaActor::init() { const int logical_frame) -> caf::result { if (base_.empty() or not media_sources_.count(base_.current(media_type))) return make_error(xstudio_error::error, "No MediaSources"); - auto rp = make_response_promise(); rp.delegate( media_sources_.at(base_.current(media_type)), atom, media_type, logical_frame); + return rp; }, [=](get_media_pointers_atom atom, const MediaType media_type, const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate) -> caf::result { - auto rp = make_response_promise(); - - request( - caf::actor_cast(this), - infinite, - get_edit_list_atom_v, + const utility::FrameRate &override_rate) -> caf::result { + if (base_.empty() or not media_sources_.count(base_.current(media_type))) + return make_error(xstudio_error::error, "No MediaSources"); + auto rp = make_response_promise(); + rp.delegate( + media_sources_.at(base_.current(media_type)), + atom, media_type, - utility::Uuid()) - .then( - [=](const utility::EditList &edl) mutable { - const auto clip = edl.section_list()[0]; - const int num_clip_frames = clip.frame_rate_and_duration_.frames( - tsm == TimeSourceMode::FIXED ? override_rate : FrameRate()); - const utility::Timecode tc = clip.timecode_; - - request( - caf::actor_cast(this), - infinite, - atom, - media_type, - media::LogicalFrameRanges({{0, num_clip_frames - 1}}), - override_rate) - .then( - [=](const media::AVFrameIDs &ids) mutable { - media::FrameTimeMap result; - auto time_point = timebase::flicks(0); - for (int f = 0; f < num_clip_frames; f++) { - result[time_point] = ids[f]; - const_cast(result[time_point].get()) - ->playhead_logical_frame_ = f; - const_cast(ids[f].get()) - ->timecode_ = tc + f; - time_point += - tsm == TimeSourceMode::FIXED - ? override_rate - : clip.frame_rate_and_duration_.rate(); - } - rp.deliver(result); - }, - [=](error &err) mutable { rp.deliver(err); }); - }, - [=](error &err) mutable { rp.deliver(err); }); + tsm, + override_rate); + return rp; }, [=](get_media_pointers_atom atom, const MediaType media_type, const LogicalFrameRanges &ranges, - const FrameRate &override_rate) -> caf::result { + const FrameRate &override_rate, + const utility::Uuid &clip_uuid) -> caf::result { if (base_.empty() or not media_sources_.count(base_.current(media_type))) return make_error(xstudio_error::error, "No MediaSources"); auto rp = make_response_promise(); // we need to ensure the media detail has been acquired before // we can deliver AVFrameIDs - request( - caf::actor_cast(this), - infinite, - acquire_media_detail_atom_v, - override_rate) + mail(acquire_media_detail_atom_v, override_rate) + .request(caf::actor_cast(this), infinite) .then( [=](bool) mutable { - request( - media_sources_.at(base_.current(media_type)), - infinite, - atom, - media_type, - ranges) + mail(atom, media_type, ranges, clip_uuid) + .request(media_sources_.at(base_.current(media_type)), infinite) .then( [=](const media::AVFrameIDs &ids) mutable { rp.deliver(ids); }, [=](error &err) mutable { rp.deliver(err); }); @@ -634,6 +695,50 @@ void MediaActor::init() { return rp; }, + [=](get_media_pointers_atom atom, + const MediaType media_type, + const LogicalFrameRanges &ranges, + const FrameRate &override_rate) { + // no clip ID provided + return mail(atom, media_type, ranges, override_rate, utility::Uuid()) + .delegate(caf::actor_cast(this)); + }, + + [=](utility::rate_atom, const media::MediaType media_type) -> caf::result { + auto rp = make_response_promise(); + mail(media_reference_atom_v, media_type) + .request(caf::actor_cast(this), infinite) + .then( + [=](const MediaReference &ref) mutable { rp.deliver(ref.rate()); }, + [=](error &err) mutable { rp.deliver(err); }); + + return rp; + }, + + [=](media_reference_atom atom, + const media::MediaType media_type) -> caf::result { + if (base_.empty() or not media_sources_.count(base_.current(media_type))) + return make_error(xstudio_error::error, "No MediaSources"); + + auto rp = make_response_promise(); + rp.delegate(media_sources_.at(base_.current(media_type)), atom); + return rp; + }, + + [=](media_reference_atom atom, + const media::MediaType media_type, + const Uuid &uuid) -> caf::result> { + if (base_.empty() or not media_sources_.count(base_.current(media_type))) + return make_error(xstudio_error::error, "No MediaSources"); + + auto rp = make_response_promise>(); + rp.delegate( + media_sources_.at(base_.current(media_type)), + atom, + (uuid.is_null() ? base_.uuid() : uuid)); + return rp; + }, + [=](media_reference_atom) -> caf::result> { if (base_.empty()) return make_error(xstudio_error::error, "No MediaSources"); @@ -673,16 +778,6 @@ void MediaActor::init() { return rp; }, - [=](media::source_offset_frames_atom) -> int { - // needed for SubPlayhead when playing media direct - return 0; - }, - - [=](media::source_offset_frames_atom, const int) -> bool { - // needed for SubPlayhead when playing media direct - return false; - }, - [=](playlist::reflag_container_atom) -> std::tuple { return std::make_tuple(base_.flag(), base_.flag_text()); }, @@ -690,12 +785,12 @@ void MediaActor::init() { [=](playlist::reflag_container_atom, const std::string &flag_colour, std::string &flag_text) { - delegate( - actor_cast(this), - playlist::reflag_container_atom_v, - std::make_tuple( - std::optional(flag_colour), - std::optional(flag_text))); + return mail( + playlist::reflag_container_atom_v, + std::make_tuple( + std::optional(flag_colour), + std::optional(flag_text))) + .delegate(actor_cast(this)); }, @@ -715,13 +810,13 @@ void MediaActor::init() { } if (changed) { - send( - event_group_, + mail( utility::event_atom_v, playlist::reflag_container_atom_v, base_.uuid(), - std::make_tuple(base_.flag(), base_.flag_text())); - base_.send_changed(event_group_, this); + std::make_tuple(base_.flag(), base_.flag_text())) + .send(base_.event_group()); + base_.send_changed(); } return changed; }, @@ -739,6 +834,12 @@ void MediaActor::init() { return make_error(xstudio_error::error, "Invalid MediaSource Uuid"); }, + [=](get_media_source_atom, const Uuid &uuid, bool) -> caf::result { + if (media_sources_.count(uuid)) + return media_sources_.at(uuid); + return caf::actor(); + }, + [=](get_media_source_names_atom) -> caf::result>> { if (base_.empty()) @@ -795,6 +896,49 @@ void MediaActor::init() { return rp; }, + [=](current_media_stream_atom, const MediaType media_type) -> result { + auto rp = make_response_promise(); + if (base_.empty()) + rp.deliver(make_error(xstudio_error::error, "No MediaSources")); + else + rp.delegate( + media_sources_.at(base_.current(media_type)), + media::current_media_stream_atom_v, + media_type); + + return rp; + }, + + [=](current_media_stream_atom, + const MediaType media_type, + const Uuid &uuid) -> result { + auto rp = make_response_promise(); + if (base_.empty()) + rp.deliver(make_error(xstudio_error::error, "No MediaSources")); + else + rp.delegate( + media_sources_.at(base_.current(media_type)), + media::current_media_stream_atom_v, + media_type, + uuid); + + return rp; + }, + + [=](get_media_stream_atom, + const MediaType media_type) -> result> { + auto rp = make_response_promise>(); + if (base_.empty()) + rp.deliver(make_error(xstudio_error::error, "No MediaSources")); + else + rp.delegate( + media_sources_.at(base_.current(media_type)), + media::get_media_stream_atom_v, + media_type); + + return rp; + }, + [=](get_media_source_names_atom, const media::MediaType mt) -> caf::result>> { if (base_.empty()) @@ -873,7 +1017,8 @@ void MediaActor::init() { const bool /*fanout*/) -> caf::result> { if (uuid.is_null() or uuid == base_.uuid()) { auto rp = make_response_promise>(); - request(json_store_, infinite, atom, path) + mail(atom, path) + .request(json_store_, infinite) .then( [=](const JsonStore &jsn) mutable { rp.deliver(std::make_pair(UuidActor(base_.uuid(), this), jsn)); @@ -885,7 +1030,8 @@ void MediaActor::init() { return rp; } else if (media_sources_.count(uuid)) { auto rp = make_response_promise>(); - request(media_sources_.at(uuid), infinite, atom, path) + mail(atom, path) + .request(media_sources_.at(uuid), infinite) .then( [=](const JsonStore &jsn) mutable { rp.deliver( @@ -907,13 +1053,14 @@ void MediaActor::init() { if (!try_source_actors) { rp.delegate(caf::actor_cast(this), atom, utility::Uuid(), path); } else { - request(json_store_, infinite, atom, path) + mail(atom, path) + .request(json_store_, infinite) .then( [=](const JsonStore &r) mutable { rp.deliver(r); }, [=](error &) mutable { // our own store doesn't have data at 'path'. Try the // current media source as a fallback - rp.delegate(media_sources_.at(base_.current()), atom, path); + rp.delegate(media_sources_.at(base_.current()), atom, path, true); }); } return rp; @@ -939,18 +1086,26 @@ void MediaActor::init() { return make_error(xstudio_error::error, "No MediaSources"); auto rp = make_response_promise(); - rp.delegate(media_sources_.at(base_.current()), atom, path); + rp.delegate(json_store_, atom, path); return rp; }, - [=](json_store::get_json_atom atom, const std::string &path) -> caf::result { - if (base_.empty() or not media_sources_.count(base_.current())) - return make_error(xstudio_error::error, "No MediaSources"); + [=](json_store::get_json_atom atom, + const std::vector &paths) -> caf::result { + // multi json store value request + auto rp = make_response_promise(); + rp.delegate(json_store_, atom, paths); + return rp; + }, + + /*[=](json_store::get_json_atom atom, const std::string &path) -> caf::result + { if (base_.empty() or not media_sources_.count(base_.current())) return + make_error(xstudio_error::error, "No MediaSources"); auto rp = make_response_promise(); rp.delegate(media_sources_.at(base_.current()), atom, path); return rp; - }, + },*/ [=](json_store::set_json_atom atom, const utility::Uuid &uuid, @@ -981,34 +1136,32 @@ void MediaActor::init() { }, [=](media::acquire_media_detail_atom atom, - const FrameRate &default_rate) -> caf::result { - if (base_.empty() or not media_sources_.count(base_.current())) - return make_error(xstudio_error::error, "No MediaSources"); - - // auto m_hook_actor = system().registry().template - // get(media_hook_registry); if (m_hook_actor) { - // anon_send( - // m_hook_actor, - // media_hook::gather_media_sources_atom_v, - // caf::actor_cast(this) - // ); - // } - + const FrameRate default_rate) -> caf::result { auto rp = make_response_promise(); - rp.delegate(media_sources_.at(base_.current()), atom, default_rate); - return rp; - }, - [=](media::invalidate_cache_atom atom) -> caf::result { - if (base_.empty() or not media_sources_.count(base_.current())) - return make_error(xstudio_error::error, "No MediaSources"); + // first, make sure we have a media source + mail(playhead::media_source_atom_v) + .request(caf::actor_cast(this), infinite) + .then( + [=](bool) mutable { + // ensures media sources have had their details filled in and + // we've set the media sources (image and audio) where possible + if (base_.empty() or not media_sources_.count(base_.current())) { + rp.deliver(make_error(xstudio_error::error, "No MediaSources")); + return; + } + + rp.delegate(media_sources_.at(base_.current()), atom, default_rate); + }, + [=](caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); - auto rp = make_response_promise(); - rp.delegate(media_sources_.at(base_.current()), atom); return rp; }, - [=](media_cache::keys_atom atom) -> caf::result { + [=](media::invalidate_cache_atom atom) -> caf::result { if (base_.empty() or not media_sources_.count(base_.current())) return make_error(xstudio_error::error, "No MediaSources"); @@ -1017,40 +1170,6 @@ void MediaActor::init() { return rp; }, - [=](media_cache::keys_atom atom, - const MediaType media_type) -> caf::result { - if (base_.empty() or not media_sources_.count(base_.current(media_type))) - return make_error(xstudio_error::error, "No MediaSources"); - - auto rp = make_response_promise(); - rp.delegate(media_sources_.at(base_.current(media_type)), atom, media_type); - return rp; - }, - - [=](media_cache::keys_atom atom, - const MediaType media_type, - const int logical_frame) -> caf::result { - if (base_.empty() or not media_sources_.count(base_.current(media_type))) - return make_error(xstudio_error::error, "No MediaSources"); - - auto rp = make_response_promise(); - rp.delegate( - media_sources_.at(base_.current(media_type)), atom, media_type, logical_frame); - return rp; - }, - - [=](media_cache::keys_atom atom, - const MediaType media_type, - const std::vector &logical_frames) -> caf::result { - if (base_.empty() or not media_sources_.count(base_.current(media_type))) - return make_error(xstudio_error::error, "No MediaSources"); - - auto rp = make_response_promise(); - rp.delegate( - media_sources_.at(base_.current(media_type)), atom, media_type, logical_frames); - return rp; - }, - [=](media_hook::get_media_hook_atom atom) -> caf::result { if (base_.empty()) return make_error(xstudio_error::error, "No MediaSources"); @@ -1091,6 +1210,36 @@ void MediaActor::init() { return rp; }, + [=](playhead::media_source_atom) -> result { + // ensures media sources have had their details filled in and + // we've set the media sources (image and audio) where possible + auto rp = make_response_promise(); + mail(playhead::media_source_atom_v, media::MT_IMAGE) + .request(caf::actor_cast(this), infinite) + .then( + [=](bool) mutable { + mail(playhead::media_source_atom_v, media::MT_AUDIO) + .request(caf::actor_cast(this), infinite) + .then( + [=](bool) mutable { rp.deliver(true); }, + [=](caf::error &err) mutable { rp.deliver(err); }); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + return rp; + }, + + [=](playhead::media_source_atom, const media::MediaType mt) -> result { + // ensures media sources have had their details filled in and + // we've set the media sources (image OR audio) where possible + auto rp = make_response_promise(); + if (base_.empty() or not media_sources_.count(base_.current(mt))) { + auto_set_current_source(mt, rp); + } else { + rp.deliver(true); + } + return rp; + }, + [=](playhead::media_source_atom, const std::string &source_name, const media::MediaType mt) -> result { @@ -1115,66 +1264,22 @@ void MediaActor::init() { return rp; }, - [=](utility::duplicate_atom atom) { - delegate(caf::actor_cast(this), atom, caf::actor(), caf::actor()); + [=](utility::duplicate_atom) -> result { + auto rp = make_response_promise(); + duplicate(rp, caf::actor(), caf::actor()); + return rp; }, + [=](utility::duplicate_atom, caf::actor src_bookmarks, caf::actor dst_bookmarks) -> result { auto rp = make_response_promise(); - - auto uuid = utility::Uuid::generate(); - auto actor = spawn(base_.name(), uuid); - - caf::scoped_actor sys(system()); - auto json = - request_receive(*sys, json_store_, json_store::get_json_atom_v); - anon_send(actor, json_store::set_json_atom_v, Uuid(), json, ""); - anon_send( - actor, - playlist::reflag_container_atom_v, - std::make_tuple( - std::optional(base_.flag()), - std::optional(base_.flag_text()))); - // sometimes caf makes my head bleed. see below. - - auto source_count = std::make_shared(); - (*source_count) = base_.media_sources().size(); - for (const Uuid i : base_.media_sources()) { - - request(media_sources_.at(i), infinite, utility::duplicate_atom_v) - .then( - [=](UuidActor media_src) mutable { - request(actor, infinite, add_media_source_atom_v, media_src.actor()) - .then( - [=](const Uuid &) mutable { - if (i == base_.current()) { - anon_send( - actor, - current_media_source_atom_v, - media_src.uuid()); - } - (*source_count)--; - if (!*source_count) { - // done! - auto ua = UuidActor(uuid, actor); - if (src_bookmarks) - clone_bookmarks_to( - ua, src_bookmarks, dst_bookmarks); - rp.deliver(UuidUuidActor(base_.uuid(), ua)); - } - }, - [=](const error &err) mutable { rp.deliver(err); }); - }, - [=](const error &err) mutable { rp.deliver(err); }); - } - - + duplicate(rp, src_bookmarks, dst_bookmarks); return rp; }, [=](utility::event_atom, add_media_stream_atom, const UuidActor &) { - base_.send_changed(event_group_, this); + base_.send_changed(); }, [=](utility::event_atom, media_status_atom, const MediaStatus ms) { @@ -1185,7 +1290,7 @@ void MediaActor::init() { // "media_status_atom {} {}", // to_string(current_sender()), // static_cast(ms)); - send(event_group_, utility::event_atom_v, media_status_atom_v, ms); + mail(utility::event_atom_v, media_status_atom_v, ms).send(base_.event_group()); } }, @@ -1193,25 +1298,25 @@ void MediaActor::init() { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, // to_string(caf::actor_cast(this))); pending_change_ = false; - send(event_group_, utility::event_atom_v, utility::change_atom_v); + // update any display info about this media item used in the + // front end + anon_mail(media_display_info_atom_v).send(this); + mail(utility::event_atom_v, utility::change_atom_v).send(base_.event_group()); }, [=](utility::event_atom, utility::change_atom) { // propagate changes upwards if (not pending_change_) { pending_change_ = true; - delayed_send( - this, - std::chrono::milliseconds(250), - utility::event_atom_v, - change_atom_v, - true); + mail(utility::event_atom_v, change_atom_v, true) + .delay(std::chrono::milliseconds(250)) + .send(this); } }, [=](utility::event_atom, utility::last_changed_atom, const time_point &) { // propagate changes upwards - send(this, utility::event_atom_v, utility::change_atom_v); + mail(utility::event_atom_v, utility::change_atom_v).send(this); }, [=](utility::event_atom, utility::name_atom, const std::string & /*name*/) {}, @@ -1219,7 +1324,8 @@ void MediaActor::init() { [=](utility::serialise_atom) -> result { auto rp = make_response_promise(); - request(json_store_, infinite, json_store::get_json_atom_v, "") + mail(json_store::get_json_atom_v, "") + .request(json_store_, infinite) .then( [=](const JsonStore &meta) mutable { std::vector clients; @@ -1266,7 +1372,60 @@ void MediaActor::init() { rp.deliver(std::move(err)); }); return rp; - }); + }, + + [=](json_store::update_atom, + const JsonStore &change, + const std::string &path, + const JsonStore &full) { + if (current_sender() == json_store_) + mail(json_store::update_atom_v, change, path, full).send(base_.event_group()); + }, + + [=](json_store::update_atom, const JsonStore &full) mutable { + if (current_sender() == json_store_) + mail(json_store::update_atom_v, full).send(base_.event_group()); + }, + + [=](media_display_info_atom, + const utility::JsonStore &item_query_info) -> result { + auto rp = make_response_promise(); + display_info_item(item_query_info, rp); + return rp; + }, + [=](human_readable_info_atom, bool update) -> result { + auto rp = make_response_promise(); + update_human_readable_details(rp); + return rp; + }, + [=](media_display_info_atom) -> result { + auto rp = make_response_promise(); + mail(human_readable_info_atom_v, true) + .request(caf::actor_cast(this), infinite) + .then( + [=](const utility::JsonStore &human_readable) mutable { + build_media_list_info(rp); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + return rp; + }}; +} + + +void MediaActor::init() { + // only parial.. + print_on_create(this, base_); + print_on_exit(this, base_); + + // get updates from the GlobalMetadataManager instance that manages the + // filters used to get metadata from the MediaActors to the UI layer. + auto metadata_filter_actor = home_system().registry().template get( + global_media_metadata_manager_registry); + + // this message kicks the metadata filter actor to send us the dictionary + // for filtering our metadata + mail(media::media_display_info_atom_v, true).send(metadata_filter_actor); + join_event_group(this, metadata_filter_actor); } void MediaActor::add_or_rename_media_source( @@ -1284,13 +1443,11 @@ void MediaActor::add_or_rename_media_source( if (existing_ref == ref) { if (media_sources_.count(*source_uuid)) { - anon_send(media_sources_.at(*source_uuid), utility::name_atom_v, name); + anon_mail(utility::name_atom_v, name).send(media_sources_.at(*source_uuid)); } if (set_as_current_source) { - anon_send( - caf::actor_cast(this), - current_media_source_atom_v, - *source_uuid); + anon_mail(current_media_source_atom_v, *source_uuid) + .send(caf::actor_cast(this)); } return; } @@ -1301,18 +1458,13 @@ void MediaActor::add_or_rename_media_source( auto new_source = spawn(name, ref.uri(), ref.frame_list(), ref.rate(), uuid); - request( - caf::actor_cast(this), - infinite, - add_media_source_atom_v, - utility::UuidActor(uuid, new_source)) + mail(add_media_source_atom_v, utility::UuidActor(uuid, new_source)) + .request(caf::actor_cast(this), infinite) .then( [=](const Uuid &source_uuid) { if (set_as_current_source) { - anon_send( - caf::actor_cast(this), - current_media_source_atom_v, - source_uuid); + anon_mail(current_media_source_atom_v, source_uuid) + .send(caf::actor_cast(this)); } }, [=](const error &err) mutable { @@ -1321,27 +1473,50 @@ void MediaActor::add_or_rename_media_source( } void MediaActor::clone_bookmarks_to( - const utility::UuidActor &ua, caf::actor src_bookmark, caf::actor dst_bookmark) { + caf::typed_response_promise rp, + const utility::UuidUuidActor &uua, + caf::actor src_bookmark, + caf::actor dst_bookmark) { // use global session (should be safe.. ish) - try { - scoped_actor sys{system()}; - try { - UuidActorVector copied; - // // duplicate bookmarks if any - auto bookmarks = utility::request_receive( - *sys, src_bookmark, bookmark::get_bookmarks_atom_v, base_.uuid()); - for (const auto &i : bookmarks) { - copied.push_back(utility::request_receive( - *sys, src_bookmark, utility::duplicate_atom_v, i.uuid(), ua)); - } - utility::request_receive( - *sys, dst_bookmark, bookmark::add_bookmark_atom_v, copied); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } + // DON'T USE REQUEST RECEIVE!! + + mail(bookmark::get_bookmarks_atom_v, base_.uuid()) + .request(src_bookmark, infinite) + .then( + [=](const UuidActorVector &bookmarks) mutable { + if (bookmarks.empty()) + rp.deliver(uua); + else { + auto copied = std::make_shared(); + auto count = std::make_shared(bookmarks.size()); + for (const auto &i : bookmarks) { + mail(utility::duplicate_atom_v, i.uuid(), uua.second) + .request(src_bookmark, infinite) + .then( + [=](const UuidActor &new_bookmark) mutable { + copied->push_back(new_bookmark); + (*count)--; + if (not(*count)) { + anon_mail(bookmark::add_bookmark_atom_v, *copied) + .send(dst_bookmark); + rp.deliver(uua); + } + }, + [=](const error &err) mutable { + (*count)--; + if (not(*count)) { + anon_mail(bookmark::add_bookmark_atom_v, *copied) + .send(dst_bookmark); + rp.deliver(uua); + } + }); + } + } + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); } void MediaActor::switch_current_source_to_named_source( @@ -1358,6 +1533,9 @@ void MediaActor::switch_current_source_to_named_source( scoped_actor sys{system()}; auto sources = utility::map_value_to_vec(media_sources_); auto source_uuid = utility::Uuid(); + + const utility::Uuid current_source_id = base_.current(media_type); + for (auto &source : sources) { auto source_container_detail = utility::request_receive(*sys, source, detail_atom_v); @@ -1367,8 +1545,16 @@ void MediaActor::switch_current_source_to_named_source( if (source_container_detail.name_ == source_name && !source_stream_details.empty()) { // name matches and the source has a stream of type media_type - source_uuid = source_container_detail.uuid_; - break; + if (source_uuid.is_null()) { + source_uuid = source_container_detail.uuid_; + } + + // the name of the current source id matches 'source_name'. + // Nothing to do! + if (current_source_id == source_container_detail.uuid_) { + rp.deliver(true); + return; + } } } @@ -1405,12 +1591,8 @@ void MediaActor::switch_current_source_to_named_source( << "\" but no such source was found."; rp.deliver(make_error(xstudio_error::error, ss.str())); } else if (not source_uuid.is_null()) { - request( - caf::actor_cast(this), - infinite, - current_media_source_atom_v, - source_uuid, - media_type) + mail(current_media_source_atom_v, source_uuid, media_type) + .request(caf::actor_cast(this), infinite) .then( [=](const bool result) mutable { return rp.deliver(result); }, [=](error &err) mutable { @@ -1426,17 +1608,24 @@ void MediaActor::switch_current_source_to_named_source( } } -void MediaActor::auto_set_current_source(const media::MediaType media_type) { + +void MediaActor::auto_set_current_source( + const media::MediaType media_type, caf::typed_response_promise rp) { auto set_current = [=](const media::MediaType mt, const utility::Uuid &uuid) mutable { if (base_.set_current(uuid, mt)) { - send( - event_group_, + anon_mail(media_display_info_atom_v).send(this); + mail( utility::event_atom_v, current_media_source_atom_v, UuidActor( base_.current(media_type), media_sources_.at(base_.current(media_type))), - media_type); + media_type) + .send(base_.event_group()); + if (rp.pending()) + rp.deliver(true); + } else if (rp.pending()) { + rp.deliver(false); } }; @@ -1461,29 +1650,435 @@ void MediaActor::auto_set_current_source(const media::MediaType media_type) { // source - the reason is that xSTUDIO always needs and image source to set // frame rate and duration, even if it can't provide images. set_current(media::MT_IMAGE, base_.media_sources().front()); + } else if (rp.pending()) { + rp.deliver(false); } + } else { + rp.deliver(false); } }; // TODO: do these requests asynchronously, as it could be heavy and slow // loading of big playlists etc - std::set sources_matching_media_type; - caf::scoped_actor sys(system()); - for (auto source_uuid : base_.media_sources()) { + auto sources_matching_media_type = std::make_shared>(); + auto count = std::make_shared(media_sources_.size()); + + // caf::scoped_actor sys(system()); + if (base_.media_sources().empty()) { + rp.deliver(false); + } else { + for (auto source_uuid : base_.media_sources()) { + auto source_actor = media_sources_[source_uuid]; + + mail(detail_atom_v, media_type) + .request(source_actor, infinite) + .then( + [=](const std::vector &stream_details) mutable { + if (stream_details.size()) + sources_matching_media_type->insert(source_uuid); + + (*count)--; + if (not *count) + auto_set_sources_mt(*sources_matching_media_type); + }, + [=](caf::error err) mutable { + (*count)--; + if (not *count) + auto_set_sources_mt(*sources_matching_media_type); + }); + + // try { + // auto stream_details = request_receive>( + // *sys, source_actor, detail_atom_v, media_type); + + // if (stream_details.size()) + // sources_matching_media_type.insert(source_uuid); + // } catch (...) { + // } + } + } + + + // auto_set_sources_mt(sources_matching_media_type); +} - auto source_actor = media_sources_[source_uuid]; +void MediaActor::update_human_readable_details( + caf::typed_response_promise rp) { + + // The goal with this function is make a simple dictionary with the most + // useful info about the media - filename, duration, frame rate, resolution + // etc. This is then used to (partly) make the 'display_info' data that + // is used to show info about the media in the xSTUDIO UI, or perhaps used + // in plguins via API + + // we're making 4 async requests to other actors, so we need to store the + // result in a shared ptr captured by the lambda and deliver the response + // only when each request has returned a response + auto result = std::make_shared(); + auto response_count = std::make_shared(0); + + auto check_deliver = [=]() mutable { + (*response_count)++; + if (*response_count == 4) { + if (human_readable_info_ != *result) { + human_readable_info_ = *result; + // rebuild our display info data + anon_mail(media_display_info_atom_v).send(this); + } + rp.deliver(human_readable_info_); + } + }; + + // get info about IMAGE stream + mail(get_stream_detail_atom_v, media::MT_IMAGE) + .request(caf::actor_cast(this), infinite) + .then( + [=](const StreamDetail &image_stream_detail) mutable { + auto &r = *result; + + r["Duration / seconds - MediaSource (Image)"] = + image_stream_detail.duration_.seconds(); + r["Duration / frames - MediaSource (Image)"] = + image_stream_detail.duration_.frames(); + r["Frame Rate - MediaSource (Image)"] = + fmt::format("{:.2f}", image_stream_detail.duration_.rate().to_fps()); + r["Name - MediaSource (Image)"] = image_stream_detail.name_; + r["Resolution - MediaSource (Image)"] = fmt::format( + "{}x{}:{}", + image_stream_detail.resolution_.x, + image_stream_detail.resolution_.y, + image_stream_detail.pixel_aspect_); + r["Pixel Aspect - MediaSource (Image)"] = image_stream_detail.pixel_aspect_; + check_deliver(); + }, + [=](caf::error err) mutable { check_deliver(); }); - try { - auto stream_details = request_receive>( - *sys, source_actor, detail_atom_v, media_type); + // get info about AUDIO stream + mail(get_stream_detail_atom_v, media::MT_AUDIO) + .request(caf::actor_cast(this), infinite) + .then( + [=](const StreamDetail &audio_stream_detail) mutable { + auto &r = *result; + r["Duration / seconds - MediaSource (Audio)"] = + audio_stream_detail.duration_.seconds(); + r["Name - MediaSource (Audio)"] = audio_stream_detail.name_; + check_deliver(); + }, + [=](caf::error err) mutable { check_deliver(); }); + + // get info about IMAGE source + if (media_sources_.count(base_.current(MT_IMAGE))) { + mail(media_reference_atom_v) + .request(media_sources_[base_.current(MT_IMAGE)], infinite) + .then( + [=](const MediaReference &ref) mutable { + auto &r = *result; + + // The uri for frame based formats is a bit ugly ... here replace the frame + // expr with some hashes + static std::regex re(R"(\.\{\:[0-9]+d\}\.)"); + r["File Path - MediaSource (Image)"] = + std::regex_replace(uri_to_posix_path(ref.uri()), re, ".####."); + r["Timecode - MediaSource (Image)"] = ref.timecode().to_string(); + r["Frame Range - MediaSource (Image)"] = to_string(ref.frame_list()); + + check_deliver(); + }, + [=](caf::error err) mutable { check_deliver(); }); + } else { + check_deliver(); + } - if (stream_details.size()) - sources_matching_media_type.insert(source_uuid); - } catch (...) { + // get info about AUDIO source + if (media_sources_.count(base_.current(MT_AUDIO))) { + mail(media_reference_atom_v) + .request(media_sources_[base_.current(MT_AUDIO)], infinite) + .then( + [=](const MediaReference &ref) mutable { + auto &r = *result; + r["File Path - MediaSource (Audio)"] = uri_to_posix_path(ref.uri()); + r["Timecode - MediaSource (Audio)"] = ref.timecode().to_string(); + r["Frame Range - MediaSource (Image)"] = to_string(ref.frame_list()); + check_deliver(); + }, + [=](caf::error err) mutable { check_deliver(); }); + } else { + check_deliver(); + } +} + +void MediaActor::display_info_item( + const JsonStore item_query_info, caf::typed_response_promise rp) { + + // fetch specific info about the media, current media source or current + // media stream (either a metadata value or a value from the + // human_readable_info_ json dict). + // + // item_query_info is json that is a segment of the + // /ui/qml/media_list_column_configuration preference and tells us whether to + // fetch a metadata value or on of our entries in the human_readable_info_ + // dictionary + // + // We also allow the facility to to a regex replace on the resulting metadata + // value to format it in some way convenient for the media list. + // + // We can also define indefinite 'fallbacks' to try if our metadata fetch + // fails to match something in the metadata. + // + // In this example for "File" we show a particular shotgun metadata value, + // but if that isn't present the we fallback onto the media path, with a + // regex to show the filename only and not the full filesystem path. + /*{ + "title": "File", + "metadata_path": "/metadata/shotgun/version/attributes/code", + "data_type": "metadata", + "size": 360, + "object": "Media", + "resizable": true, + "sortable": true, + "position": "left", + "fallback": { + "info_key": "File Path - MediaSource (Image)", + "data_type": "media_standard_details", + "regex_match": "(.+)\/([^\/]+)$", + "regex_format": "$2", + "fallback": { + "info_key": "File Path - MediaSource (Audio)", + "data_type": "media_standard_details", + "regex_match": "(.+)\/([^\/]+)$", + "regex_format": "$2" + } } } + */ + + // lambda to recurse into the 'fallback' + auto fallback = [=]() mutable { + if (item_query_info.contains("children") && item_query_info["children"].size() && + item_query_info["children"].is_array()) { + display_info_item(item_query_info["children"][0], rp); + } else { + rp.deliver(JsonStore()); + } + }; + + // lambda to apply optional regex replace formatting + auto do_regex_format = [=](const JsonStore &data) -> JsonStore { + if (item_query_info.contains("regex_match") && + item_query_info.contains("regex_format")) { + try { + std::regex re(item_query_info.value("regex_match", "")); + auto fonk = JsonStore(std::regex_replace( + data.is_string() ? data.get() : data.dump(), + re, + item_query_info.value("regex_format", ""))); + return fonk; + } catch (const std::regex_error &e) { + return JsonStore(e.what()); + } + } + return data; + }; + + const std::string data_type = item_query_info.value("data_type", ""); + if (data_type == "flag") { - auto_set_sources_mt(sources_matching_media_type); + rp.deliver(JsonStore(base_.flag())); + + } else if (data_type == "metadata") { + + const std::string metadata_path = item_query_info.value("metadata_path", ""); + + // "object" should be Media, MediaSource or MediaStream to tell us + // which one we should do the metadata search on. For example, codec + // related metadata will be found on the MediaStream. File metadata + // might be found on the MediaSource. Pipeline metadata (version stream + // metadata) will be attached to Media. + const std::string object = item_query_info.value("object", ""); + + auto get_metadata_value = [=](caf::actor target) mutable { + mail(json_store::get_json_atom_v, metadata_path) + .request(target, infinite) + .then( + [=](const JsonStore &data) mutable { rp.deliver(do_regex_format(data)); }, + [=](caf::error &err) mutable { fallback(); }); + }; + + if (object == "MediaSource (Image)" && + media_sources_.count(base_.current(media::MT_IMAGE))) { + // ask for metadata from MediaSource + get_metadata_value(media_sources_.at(base_.current(media::MT_IMAGE))); + } else if ( + object == "MediaSource (Audio)" && + media_sources_.count(base_.current(media::MT_AUDIO))) { + // ask for metadata from MediaSource + get_metadata_value(media_sources_.at(base_.current(media::MT_AUDIO))); + } else if (object == "Image Stream") { + // ask for metadata from MediaStream - as it stands, the streams + // have no metadata, by the way! The stream metadata all lives on + // the media source. + mail(current_media_stream_atom_v, media::MT_IMAGE) + .request(caf::actor_cast(this), infinite) + .then( + [=](caf::actor t) mutable { get_metadata_value(t); }, + [=](caf::error &err) mutable { fallback(); }); + } else if (object == "Audio Stream") { + // ask for metadata from MediaStream + mail(current_media_stream_atom_v, media::MT_AUDIO) + .request(caf::actor_cast(this), infinite) + .then( + [=](caf::actor t) mutable { get_metadata_value(t); }, + [=](caf::error &err) mutable { fallback(); }); + } else { + // By defaul use media level json store + get_metadata_value(json_store_); + } + } else if (data_type == "media_standard_details") { + auto key = item_query_info.value("info_key", ""); + if (human_readable_info_.contains(key)) { + rp.deliver(do_regex_format(human_readable_info_[key])); + } else { + fallback(); + } + } else { + fallback(); + } +} + +inline void dump_tree3(const utility::JsonTree &node, const int depth = 0) { + std::cerr << fmt::format("{:>{}} {}", " ", depth * 4, node.data().dump()) << "\n"; + for (const auto &i : node.base()) + dump_tree3(i, depth + 1); +} + +void MediaActor::build_media_list_info(caf::typed_response_promise rp) { + + // empty json array for results of the filter sets + auto result = std::make_shared(R"([])"_json); + + // because do this by firing off a bunch of asynchronous requests, we count + // the number of results we get until it matches num_metadata_filter_results_ + // before delivering the response promise + auto result_countdown = std::make_shared(media_list_columns_config_.size()); + + auto check_if_finished = [=]() mutable { + (*result_countdown)--; + if (*result_countdown == 0) { + // we've received reposnses to all the requests made in the + // loop below. Boradcast + if (media_list_columns_info_ != *result) { + media_list_columns_info_ = *result; + + mail(utility::event_atom_v, media_display_info_atom_v, media_list_columns_info_) + .send(base_.event_group()); + } + rp.deliver(*result); + } + }; + + // empty json array for results of the filter set items + result->push_back(R"([])"_json); + + int item_idx = 0; + + for (const auto &j : media_list_columns_config_.base()) { + + (*result)[item_idx] = nlohmann::json(); + const utility::JsonStore metadata_filter = utility::tree_to_json(j); + mail(media_display_info_atom_v, metadata_filter) + .request(caf::actor_cast(this), infinite) + .then( + [=](utility::JsonStore &data) mutable { + (*result)[item_idx] = data; + check_if_finished(); + }, + [=](caf::error &err) mutable { check_if_finished(); }); + item_idx++; + } +} + +void MediaActor::duplicate( + caf::typed_response_promise rp, + caf::actor src_bookmarks, + caf::actor dst_bookmarks) { + auto uuid = utility::Uuid::generate(); + auto actor = spawn(base_.name(), uuid); + + // don't use request receive.. + mail(json_store::get_json_atom_v) + .request(json_store_, infinite) + .then( + [=](const JsonStore &jsn) mutable { + anon_mail(json_store::set_json_atom_v, Uuid(), jsn, "").send(actor); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + + anon_mail( + playlist::reflag_container_atom_v, + std::make_tuple( + std::optional(base_.flag()), + std::optional(base_.flag_text()))) + .send(actor); + // sometimes caf makes my head bleed. see below. + + // auto source_count = std::make_shared(); + // (*source_count) = base_.media_sources().size(); + + if (!media_sources_.size()) { + auto uua = UuidUuidActor(base_.uuid(), UuidActor(uuid, actor)); + + if (src_bookmarks) + clone_bookmarks_to(rp, uua, src_bookmarks, dst_bookmarks); + else + rp.deliver(uua); + return; + } + + fan_out_request( + map_value_to_vec(media_sources_), infinite, utility::duplicate_atom_v) + .then( + [=](const std::vector dmedia_srcs) mutable { + std::map dmedia_srcs_map; + + for (const auto &i : dmedia_srcs) + dmedia_srcs_map[i.first] = i.second; + + UuidActorVector new_media_srcs; + + for (const auto &i : base_.media_sources()) + new_media_srcs.push_back(dmedia_srcs_map[i]); + + // bulk add srcs. + mail(add_media_source_atom_v, new_media_srcs) + .request(actor, infinite) + .then( + [=](const bool) mutable { + // set current source. + + // anon_mail(// current_media_source_atom_v, + // dmedia_srcs_map[base_.current()].uuid()).send(// actor); + mail( + current_media_source_atom_v, + dmedia_srcs_map[base_.current()].uuid()) + .request(actor, infinite) + .then( + [=](const bool) mutable { + auto uua = + UuidUuidActor(base_.uuid(), UuidActor(uuid, actor)); + + if (src_bookmarks) + clone_bookmarks_to( + rp, uua, src_bookmarks, dst_bookmarks); + else + rp.deliver(uua); + }, + [=](const caf::error &err) mutable { rp.deliver(err); }); + }, + [=](const error &err) mutable { rp.deliver(err); }); + }, + [=](const caf::error &err) mutable { rp.deliver(err); }); } diff --git a/src/media/src/media_metadata_manager_actor.cpp b/src/media/src/media_metadata_manager_actor.cpp new file mode 100644 index 000000000..de112f19a --- /dev/null +++ b/src/media/src/media_metadata_manager_actor.cpp @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#include "xstudio/atoms.hpp" +#include "xstudio/broadcast/broadcast_actor.hpp" +#include "xstudio/media/media_metadata_manager_actor.hpp" +#include "xstudio/utility/helpers.hpp" + +#include + +using namespace xstudio; +using namespace xstudio::media; + +namespace { +inline void dump_tree2(const utility::JsonTree &node, const int depth = 0) { + for (const auto &i : node.base()) + dump_tree2(i, depth + 1); +} + +static const std::vector relevant_keys = { + "metadata_path", "data_type", "object", "info_key", "regex_match", "regex_format"}; + +utility::JsonStore make_metadata_extraction_info_dict(const utility::JsonTree &node) { + + utility::JsonStore result; + const auto data = node.data(); + for (const auto &k : relevant_keys) { + if (data.contains(k)) + result[k] = data[k]; + } + result["children"] = R"([])"_json; + + for (const auto &i : node.base()) { + result["children"].push_back(make_metadata_extraction_info_dict(i)); + } + return result; +} +} // namespace + +GlobalMetadataManager::GlobalMetadataManager(caf::actor_config &cfg) + : caf::event_based_actor(cfg) { + + utility::print_on_create(this, "GlobalMetadataManager"); + utility::print_on_exit(this, "GlobalMetadataManager"); + + system().registry().put(global_media_metadata_manager_registry, this); + + event_group_ = spawn(this); + link_to(event_group_); + + // We have a 'model' managed by the GlobalUIModelData called + // "media metata exposure model". This model gives us info about the media + // metadata values that we are interested in showing in the UI and also + // data about HOW we want to display that info. For example, the columns of + // information in the MediaList panels in the xSTUDIO UI are driven by this + // model. + // On the backend, we need all MediaActor instances to be kept up-to-date + // about what metadata we want to display in the UI. That's what this here + // class is here to do. It inspects the items in the + // "media metata exposure model" and boils this down into a list of metadata + // that MediaActors should broadcast. + // This class broadcasts this list of interesting metadata to its event + // group (to which MediaActors will subscribe) - the MediaActors will use + // the list to build their dictionary of metadata to broadcast to the UI. + + auto central_models_data_actor = + home_system().registry().template get(global_ui_model_data_registry); + + mail( + ui::model_data::register_model_data_atom_v, + "media metata exposure model", + "/ui/qml/media_list_columns_config", + caf::actor_cast(this)) + .request(central_models_data_actor, infinite) + .then( + [=](const utility::JsonStore &data) { + try { + metadata_config_ = utility::json_to_tree(data, "children"); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + }, + [=](caf::error &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(e)); }); + + behavior_.assign( + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + + [=](utility::get_event_group_atom atom) -> caf::actor { return event_group_; }, + + [=](utility::event_atom, + xstudio::ui::model_data::set_node_data_atom, + const std::string &model_name, + const std::string &path, + const utility::JsonStore &data) { + try { + + nlohmann::json::json_pointer ptr(path); + auto *node = pointer_to_tree(metadata_config_, "children", ptr); + if (!node) { + std::stringstream ss; + ss << "failed to set data for path " << path << " in " << model_name + << "\n"; + throw std::runtime_error(ss.str().c_str()); + } + auto &j = node->data(); + utility::JsonTree new_node = json_to_tree(data, "children"); + *node = new_node; + config_updated(); + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + }, + [=](utility::event_atom, + xstudio::ui::model_data::set_node_data_atom, + const std::string &model_name, + const std::string path, + const utility::JsonStore &data, + const std::string role, + const utility::Uuid &uuid_role_data) { + nlohmann::json::json_pointer ptr(path); + + try { + nlohmann::json::json_pointer ptr(path); + auto *node = pointer_to_tree(metadata_config_, "children", ptr); + if (!node) { + std::stringstream ss; + ss << "failed to set data for path " << path << " in " << model_name + << "\n"; + throw std::runtime_error(ss.str().c_str()); + } + auto &j = node->data(); + + bool changed = false; + utility::Uuid uuid_role_data; + if (j.is_object() && !role.empty()) { + if (!j.contains(role) || j[role] != data) { + changed = true; + j[role] = data; + } + } else if (role.empty()) { + *node = utility::json_to_tree(data, "children"); + changed = true; + } + + if (changed) { + config_updated(); + } + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + }, + [=](utility::event_atom, + xstudio::ui::model_data::model_data_atom, + const std::string &model_name, + const utility::JsonStore &data) -> bool { + try { + + metadata_config_ = utility::json_to_tree(data, "children"); + config_updated(); + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + return true; + }, + [=](utility::event_atom, + ui::model_data::menu_node_activated_atom, + const std::string path, + const utility::JsonStore &menu_item_data, + const std::string &user_data, + const bool from_hotkey) { + // ignore, menu data events not relevant + }, + [=](utility::event_atom, + xstudio::ui::model_data::set_node_data_atom, + const std::string model_name, + const std::string path, + const std::string role, + const utility::JsonStore &data, + const utility::Uuid &menu_item_uuid) { + // ignore, menu data events not relevant + }, + [=](xstudio::ui::model_data::set_node_data_atom, + const std::string &menu_model_name, + const std::string &submenu, + const float submenu_position) { + // ignore, menu data events not relevant + }, + [=](media::media_display_info_atom) -> utility::JsonStore { + return metadata_extraction_config_; + }, + [=](media::media_display_info_atom, bool es_event) { + mail( + utility::event_atom_v, + media::media_display_info_atom_v, + metadata_extraction_config_) + .send(caf::actor_cast(current_sender())); + }, + + [=](const caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }, + [=](caf::message &msg) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(msg)); }); +} + +void GlobalMetadataManager::config_updated() { + utility::JsonStore extraction_dict = make_metadata_extraction_info_dict(metadata_config_); + + if (extraction_dict != metadata_extraction_config_) { + metadata_extraction_config_ = extraction_dict; + mail( + utility::event_atom_v, + media::media_display_info_atom_v, + metadata_extraction_config_) + .send(event_group_); + } +} + +void GlobalMetadataManager::on_exit() { + system().registry().erase(global_media_metadata_manager_registry); +} \ No newline at end of file diff --git a/src/media/src/media_source.cpp b/src/media/src/media_source.cpp index 031222cca..841d59cf5 100644 --- a/src/media/src/media_source.cpp +++ b/src/media/src/media_source.cpp @@ -25,6 +25,8 @@ MediaSource::MediaSource(const JsonStore &jsn) for (const auto &i : jsn["audio_streams"]) { audio_streams_.push_back(i); } + + partial_seq_behaviour_ = jsn.value("partial_seq_behaviour", PS_COLLAPSE_TO_ON_DISK_FRAMES); } MediaSource::MediaSource( @@ -76,6 +78,8 @@ JsonStore MediaSource::serialise() const { jsn["audio_streams"].push_back(i); } + jsn["partial_seq_behaviour"] = partial_seq_behaviour(); + return jsn; } diff --git a/src/media/src/media_source_actor.cpp b/src/media/src/media_source_actor.cpp index f4dd3db09..551676cc0 100644 --- a/src/media/src/media_source_actor.cpp +++ b/src/media/src/media_source_actor.cpp @@ -1,5 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include + #include #include #include @@ -34,6 +36,11 @@ caf::message_handler MediaSourceActor::default_event_handler() { return { [=](utility::event_atom, media_status_atom, const MediaStatus) {}, [=](utility::event_atom, change_atom) {}, + [=](json_store::update_atom, + const JsonStore &, + const std::string &, + const JsonStore &) {}, + [=](json_store::update_atom, const JsonStore &) {}, [=](utility::event_atom, media_metadata::get_metadata_atom, const utility::JsonStore &) {}}; @@ -49,8 +56,14 @@ MediaSourceActor::MediaSourceActor(caf::actor_config &cfg, const JsonStore &jsn) utility::Uuid::generate(), static_cast(jsn["store"]), std::chrono::milliseconds(50)); + // What happens if media was added to the session before it was on-disk, but now it + // is on disk? We need to re-scan for media metadata. Setting the + // media_detadata_up_to_date_ flag here will allow for this. + media_metadata_up_to_date_ = + jsn["store"].contains("metadata") && jsn["store"]["metadata"].contains("media"); } link_to(json_store_); + join_event_group(this, json_store_); bool re_aquire_detail = false; for (const auto &[key, value] : jsn["actors"].items()) { @@ -93,9 +106,11 @@ MediaSourceActor::MediaSourceActor( json_store_ = spawn( utility::Uuid::generate(), utility::JsonStore(), std::chrono::milliseconds(50)); link_to(json_store_); + join_event_group(this, json_store_); + // need this on creation or other functions randomly fail, as streams aren't configured.. - anon_send(actor_cast(this), acquire_media_detail_atom_v, rate); + anon_mail(acquire_media_detail_atom_v, rate).send(actor_cast(this)); // acquire_detail(rate); init(); @@ -113,9 +128,10 @@ MediaSourceActor::MediaSourceActor( json_store_ = spawn( utility::Uuid::generate(), utility::JsonStore(), std::chrono::milliseconds(50)); link_to(json_store_); + join_event_group(this, json_store_); // need this on creation or other functions randomly fail, as streams aren't configured.. - anon_send(actor_cast(this), acquire_media_detail_atom_v, rate); + anon_mail(acquire_media_detail_atom_v, rate).send(actor_cast(this)); // acquire_detail(rate); init(); @@ -134,13 +150,15 @@ MediaSourceActor::MediaSourceActor( json_store_ = spawn( utility::Uuid::generate(), utility::JsonStore(), std::chrono::milliseconds(50)); link_to(json_store_); + join_event_group(this, json_store_); - MediaReference mr = base_.media_reference(); - mr.set_timecode_from_frames(); - base_.set_media_reference(mr); + // MediaReference mr = base_.media_reference(); + // mr.set_timecode_from_frames(); + // base_.set_media_reference(mr); // special case , when duplicating, as that'll suppy streams. - // anon_send(actor_cast(this), acquire_media_detail_atom_v, media_reference.rate()); + // anon_mail(acquire_media_detail_atom_v, + // media_reference.rate().send(actor_cast(this))); init(); } @@ -159,12 +177,14 @@ void MediaSourceActor::update_media_detail() { auto _uri = base_.media_reference().uri(0, frame); if (not _uri) throw std::runtime_error("Invalid frame index"); - request(gmra, infinite, get_media_detail_atom_v, *_uri, actor_cast(this)) + mail(get_media_detail_atom_v, *_uri, actor_cast(this)) + .request(gmra, infinite) .then( [=](const MediaDetail md) mutable { for (auto strm : media_streams_) { - request(strm.second, infinite, get_stream_detail_atom_v) + mail(get_stream_detail_atom_v) + .request(strm.second, infinite) .then( [=](const StreamDetail &old_detail) { for (const auto &stream_detail : md.streams_) { @@ -172,7 +192,7 @@ void MediaSourceActor::update_media_detail() { stream_detail.media_type_ == old_detail.media_type_) { // update the media stream actor's details - send(strm.second, stream_detail); + mail(stream_detail).send(strm.second); } } }, @@ -212,159 +232,181 @@ void MediaSourceActor::acquire_detail( if (pending_stream_detail_requests_.size() > 1) return; + auto main_method = [=]() mutable { + try { + auto gmra = system().registry().template get(media_reader_registry); + if (gmra) { + int frame; + auto _uri = base_.media_reference().uri(0, frame); + if (not _uri) + throw std::runtime_error("Invalid frame index"); + mail(get_media_detail_atom_v, *_uri, actor_cast(this)) + .request(gmra, infinite) + .then( + [=](const MediaDetail &md) mutable { + base_.set_reader(md.reader_); + + bool media_ref_set = false; + for (auto stream_detail : md.streams_) { + // HACK!!! + + auto uuid = utility::Uuid::generate(); + auto stream = spawn(stream_detail, uuid); + link_to(stream); + join_event_group(this, stream); + media_streams_[uuid] = stream; + base_.add_media_stream(stream_detail.media_type_, uuid); + + if (!media_ref_set) { + // Note - it looks like we have separate media references + // for each stream, but we don't there is a single media + // reference for a MediaSource - as such, we don't support + // streams with different durations and/or frame rates but + // we can address that (if we have to). + update_stream_media_reference( + stream_detail, uuid, rate, md.timecode_); + media_ref_set = true; + } - try { - auto gmra = system().registry().template get(media_reader_registry); - if (gmra) { - int frame; - auto _uri = base_.media_reference().uri(0, frame); - if (not _uri) - throw std::runtime_error("Invalid frame index"); - request( - gmra, infinite, get_media_detail_atom_v, *_uri, actor_cast(this)) - .then( - [=](const MediaDetail &md) mutable { - base_.set_reader(md.reader_); - - bool media_ref_set = false; - for (auto i : md.streams_) { - // HACK!!! - - auto uuid = utility::Uuid::generate(); - auto stream = spawn(i, uuid); - link_to(stream); - join_event_group(this, stream); - media_streams_[uuid] = stream; - base_.add_media_stream(i.media_type_, uuid); - - if (!media_ref_set) { - // Note - it looks like we have separate media references for - // each stream, but we don't there is a single media reference - // for a MediaSource - as such, we don't support streams with - // different durations and/or frame rates but we can address - // that (if we have to). - update_stream_media_reference(i, uuid, rate, md.timecode_); - media_ref_set = true; + mail( + utility::event_atom_v, + add_media_stream_atom_v, + UuidActor(uuid, stream)) + .send(base_.event_group()); + + spdlog::debug( + "Media {} fps, {} frames {} timecode.", + base_.media_reference().rate().to_fps(), + base_.media_reference().frame_count(), + to_string(base_.media_reference().timecode())); } - send( - event_group_, - utility::event_atom_v, - add_media_stream_atom_v, - UuidActor(uuid, stream)); - - spdlog::debug( - "Media {} fps, {} frames {} timecode.", - base_.media_reference().rate().to_fps(), - base_.media_reference().frame_count(), - to_string(base_.media_reference().timecode())); - } + if (md.streams_.empty()) { + if (base_.media_status() == MS_ONLINE) { + anon_mail(media_status_atom_v, MS_MISSING).send(this); + } + } - request( - actor_cast(this), - infinite, - media_metadata::get_metadata_atom_v) - .then( - [=](const bool) mutable { - anon_send(this, media_hook::get_media_hook_atom_v); - }, - [=](const caf::error &err) mutable { - spdlog::debug( - "{} {} {}", - __PRETTY_FUNCTION__, - to_string(err), - to_string(base_.media_reference().uri())); - anon_send(this, media_hook::get_media_hook_atom_v); - }); - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); + mail(media_metadata::get_metadata_atom_v) + .request(actor_cast(this), infinite) + .then( + [=](const bool) mutable { + anon_mail(media_hook::get_media_hook_atom_v).send(this); + }, + [=](const caf::error &err) mutable { + spdlog::critical( + "{} {} {}", + __PRETTY_FUNCTION__, + to_string(err), + to_string(base_.media_reference().uri())); + anon_mail(media_hook::get_media_hook_atom_v).send(this); + }); - for (auto &_rp : pending_stream_detail_requests_) { - _rp.deliver(true); - } - pending_stream_detail_requests_.clear(); - }, - [=](const error &err) mutable { - // set media status.. - // set duration to one frame. Or things get upset. - // base_.media_reference().set_duration( - // FrameRateDuration(1, base_.media_reference().duration().rate())); - spdlog::debug("{} {}", __PRETTY_FUNCTION__, to_string(err)); - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); - base_.set_error_detail(to_string(err)); - for (auto &_rp : pending_stream_detail_requests_) { - _rp.deliver(false); - } - pending_stream_detail_requests_.clear(); - }); - } else { + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v) + .send(base_.event_group()); + + for (auto &_rp : pending_stream_detail_requests_) { + _rp.deliver(true); + } + pending_stream_detail_requests_.clear(); + }, + [=](const error &err) mutable { + // set media status.. + // set duration to one frame. Or things get upset. + // base_.media_reference().set_duration( + // FrameRateDuration(1, + // base_.media_reference().duration().rate())); + spdlog::debug("{} {}", __PRETTY_FUNCTION__, to_string(err)); + if (base_.error_detail() != to_string(err)) { + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v) + .send(base_.event_group()); + base_.set_error_detail(to_string(err)); + } + for (auto &_rp : pending_stream_detail_requests_) { + _rp.deliver(false); + } + pending_stream_detail_requests_.clear(); + base_.set_media_status(MS_UNREADABLE); + }); + } else { + for (auto &_rp : pending_stream_detail_requests_) { + _rp.deliver(false); + } + pending_stream_detail_requests_.clear(); + } + } catch (const std::exception &err) { + base_.set_error_detail(err.what()); for (auto &_rp : pending_stream_detail_requests_) { _rp.deliver(false); } pending_stream_detail_requests_.clear(); } - } catch (const std::exception &err) { - base_.set_error_detail(err.what()); - for (auto &_rp : pending_stream_detail_requests_) { - _rp.deliver(false); - } - pending_stream_detail_requests_.clear(); + }; + + // before we run the main acquire detail stuff, we need to check the + // partial sequence bahviour preference + auto prefs_actor = system().registry().template get(global_store_registry); + if (prefs_actor) { + mail(get_json_atom_v, "/core/session/partial_sequence_behaviour/value") + .request(prefs_actor, infinite) + .then( + [=](const utility::JsonStore &partial_seq_behaviour) mutable { + if (partial_seq_behaviour.is_string()) { + auto p = + partialSeqNameMap.find(partial_seq_behaviour.get()); + if (p != partialSeqNameMap.end()) { + base_.set_partial_seq_behaviour(p->second); + } + } + main_method(); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + main_method(); + }); + } else { + main_method(); } } + void MediaSourceActor::update_media_status() { auto scanner = system().registry().template get(scanner_registry); if (scanner) { - anon_send(scanner, media_status_atom_v, base_.media_reference(), this); + anon_mail(media_status_atom_v, base_.media_reference(), this).send(scanner); if (base_.checksum().second == 0) - anon_send(scanner, checksum_atom_v, this, base_.media_reference()); + anon_mail(checksum_atom_v, this, base_.media_reference()).send(scanner); } } -void MediaSourceActor::init() { - print_on_create(this, base_); - print_on_exit(this, base_); - - update_media_status(); - - event_group_ = spawn(this); - link_to(event_group_); - -// set an empty dict for colour_pipeline, as we request this at various -// times and need a placeholder or we get warnings if it's not there -#pragma message "This should not be here, this is plugin specific." - request(json_store_, infinite, json_store::get_json_atom_v, "/colour_pipeline") - .then( - [=](const JsonStore &) {}, - [=](const error &) { - // we'll get this error if there is no dict already - anon_send( - json_store_, - json_store::set_json_atom_v, - utility::JsonStore(), - "/colour_pipeline"); - }); - +caf::message_handler MediaSourceActor::message_handler() { auto thumbnail_manager = system().registry().get(thumbnail_manager_registry); - behavior_.assign( - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), + return caf::message_handler{ [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + [=](json_store::update_atom, + const JsonStore &change, + const std::string &path, + const JsonStore &full) { + if (current_sender() == json_store_) + mail(json_store::update_atom_v, change, path, full).send(base_.event_group()); + }, + + [=](json_store::update_atom, const JsonStore &full) mutable { + if (current_sender() == json_store_) + mail(json_store::update_atom_v, full).send(base_.event_group()); + }, + [=](acquire_media_detail_atom) -> result { auto rp = make_response_promise(); + acquire_detail(base_.media_reference().rate(), rp); // why ? - // send(event_group_, utility::event_atom_v, utility::name_atom_v, base_.name()); + // mail(utility::event_atom_v, utility::name_atom_v, + // base_.name()).send(base_.event_group()); return rp; }, @@ -372,7 +414,8 @@ void MediaSourceActor::init() { auto rp = make_response_promise(); acquire_detail(rate, rp); // why ? - // send(event_group_, utility::event_atom_v, utility::name_atom_v, base_.name()); + // mail(utility::event_atom_v, utility::name_atom_v, + // base_.name()).send(base_.event_group()); return rp; }, @@ -381,22 +424,21 @@ void MediaSourceActor::init() { [=](media_status_atom, const MediaStatus status) -> bool { if (base_.media_status() != status) { base_.set_media_status(status); - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, media_status_atom_v, status); + base_.send_changed(); + mail(utility::event_atom_v, media_status_atom_v, status) + .send(base_.event_group()); } return true; }, [=](add_media_stream_atom, caf::actor media_stream) -> result { auto rp = make_response_promise(); - request(media_stream, infinite, uuid_atom_v) + mail(uuid_atom_v) + .request(media_stream, infinite) .then( [=](const Uuid &uuid) mutable { - request( - actor_cast(this), - infinite, - add_media_stream_atom_v, - UuidActor(uuid, media_stream)) + mail(add_media_stream_atom_v, UuidActor(uuid, media_stream)) + .request(actor_cast(this), infinite) .then( [=](const UuidActor &ua) mutable { rp.deliver(ua); }, [=](const error &err) mutable { rp.deliver(err); }); @@ -408,19 +450,17 @@ void MediaSourceActor::init() { [=](add_media_stream_atom, const utility::UuidActor &media_stream) -> result { auto rp = make_response_promise(); - request(media_stream.actor(), infinite, get_media_type_atom_v) + mail(get_media_type_atom_v) + .request(media_stream.actor(), infinite) .then( [=](const MediaType &mt) mutable { join_event_group(this, media_stream.actor()); link_to(media_stream.actor()); media_streams_[media_stream.uuid()] = media_stream.actor(); base_.add_media_stream(mt, media_stream.uuid()); - base_.send_changed(event_group_, this); - send( - event_group_, - utility::event_atom_v, - add_media_stream_atom_v, - media_stream); + base_.send_changed(); + mail(utility::event_atom_v, add_media_stream_atom_v, media_stream) + .send(base_.event_group()); rp.deliver(media_stream); }, [=](const error &err) mutable { rp.deliver(err); }); @@ -428,18 +468,20 @@ void MediaSourceActor::init() { }, [=](colour_pipeline::get_colour_pipe_params_atom) { - delegate(json_store_, json_store::get_json_atom_v, "/colour_pipeline"); + return mail(json_store::get_json_atom_v, "/colour_pipeline").delegate(json_store_); }, [=](colour_pipeline::set_colour_pipe_params_atom, const utility::JsonStore ¶ms) { - delegate(json_store_, json_store::set_json_atom_v, params, "/colour_pipeline"); - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + return mail(json_store::set_json_atom_v, params, "/colour_pipeline") + .delegate(json_store_); }, [=](current_media_stream_atom, const MediaType media_type) -> result { auto rp = make_response_promise(); - request(caf::actor_cast(this), infinite, acquire_media_detail_atom_v) + mail(acquire_media_detail_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](bool) mutable { if (media_streams_.count(base_.current(media_type))) @@ -455,8 +497,8 @@ void MediaSourceActor::init() { [=](current_media_stream_atom, const MediaType media_type, const Uuid &uuid) -> bool { auto result = base_.set_current(media_type, uuid); if (result) { - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); } return result; }, @@ -465,7 +507,8 @@ void MediaSourceActor::init() { const MediaType media_type) -> caf::result> { auto rp = make_response_promise>(); // call actuire_detail to make sure we have inspected streams etc. first - request(caf::actor_cast(this), infinite, acquire_media_detail_atom_v) + mail(acquire_media_detail_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](bool) mutable { if (base_.empty()) { @@ -508,8 +551,8 @@ void MediaSourceActor::init() { stream_detail.media_type_ == media_type) { auto result = base_.set_current(media_type, strm.first); if (result) { - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); } return result; } @@ -517,326 +560,143 @@ void MediaSourceActor::init() { return false; }, - [=](get_edit_list_atom, + [=](get_media_pointer_atom atom, const MediaType media_type, - const Uuid &uuid) -> result { - auto rp = make_response_promise(); - request(caf::actor_cast(this), infinite, acquire_media_detail_atom_v) + const int logical_frame) -> caf::result { + auto rp = make_response_promise(); + mail(acquire_media_detail_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](bool) mutable { if (base_.current(media_type).is_null()) { rp.deliver(make_error(xstudio_error::error, "No streams")); } - - if (uuid.is_null()) - rp.deliver(utility::EditList({EditListSection( - base_.uuid(), - base_.media_reference(base_.current(media_type)).duration(), - base_.media_reference(base_.current(media_type)).timecode())})); - return rp.deliver(utility::EditList({EditListSection( - uuid, - base_.media_reference(base_.current(media_type)).duration(), - base_.media_reference(base_.current(media_type)).timecode())})); + auto dur = base_.media_reference(base_.current(media_type)).duration(); + LogicalFrameRanges ranges; + ranges.emplace_back(logical_frame, logical_frame); + mail(get_media_pointers_atom_v, media_type, ranges) + .request(caf::actor_cast(this), infinite) + .then( + [=](const media::AVFrameIDs &ids) mutable { + if (ids.size() && ids[0]) { + rp.deliver(*(ids[0])); + } else { + rp.deliver( + make_error(xstudio_error::error, "No frame")); + } + }, + [=](const error &err) mutable { rp.deliver(err); }); }, [=](const error &err) mutable { rp.deliver(err); }); return rp; }, - [=](get_media_pointer_atom, - const MediaType media_type) -> result> { - auto rp = make_response_promise>(); - - if (base_.current(media_type).is_null()) { - rp.deliver(make_error(xstudio_error::error, "No streams")); - return rp; - } - - request( - media_streams_.at(base_.current(media_type)), - infinite, - get_stream_detail_atom_v) + [=](get_media_pointers_atom atom, + const MediaType media_type, + const utility::TimeSourceMode tsm, + const utility::FrameRate &override_rate) -> caf::result { + auto rp = make_response_promise(); + mail(acquire_media_detail_atom_v) + .request(caf::actor_cast(this), infinite) .then( - [=](const StreamDetail &detail) mutable { - auto timecode = - base_.media_reference(base_.current(media_type)).timecode(); - if (media_type == MT_IMAGE) { - request( - json_store_, - infinite, - json_store::get_json_atom_v, - "/colour_pipeline") - .then( - [=](const JsonStore &meta) mutable { - try { - std::vector results; - auto first_frame = *( - base_.media_reference(base_.current(media_type)) - .frame(0)); - for (const auto &i : - base_ - .media_reference(base_.current(media_type)) - .uris()) { - results.emplace_back(media::AVFrameID( - i.first, - i.second, - first_frame, - base_ - .media_reference( - base_.current(media_type)) - .rate(), - detail.name_, - detail.key_format_, - base_.reader(), - caf::actor_cast(this), - meta, - base_.uuid(), - parent_uuid_, - media_type)); - results.back().timecode_ = timecode; - timecode = timecode + 1; - } - - rp.deliver(results); - } catch (const std::exception &e) { - rp.deliver( - make_error(xstudio_error::error, e.what())); - } - }, - [=](error &) mutable { - try { - std::vector results; - auto first_frame = *( - base_.media_reference(base_.current(media_type)) - .frame(0)); - for (const auto &i : - base_ - .media_reference(base_.current(media_type)) - .uris()) { - results.emplace_back(media::AVFrameID( - i.first, - i.second, - first_frame, - base_ - .media_reference( - base_.current(media_type)) - .rate(), - detail.name_, - detail.key_format_, - base_.reader(), - caf::actor_cast(this), - utility::JsonStore(), - utility::Uuid(), - parent_uuid_, - media_type)); - results.back().timecode_ = timecode; - timecode = timecode + 1; - } - - rp.deliver(results); - } catch (const std::exception &e) { - rp.deliver( - make_error(xstudio_error::error, e.what())); - } - }); - } else { - - std::vector results; - auto first_frame = - *(base_.media_reference(base_.current(media_type)).frame(0)); - for (const auto &i : - base_.media_reference(base_.current(media_type)).uris()) { - results.emplace_back(media::AVFrameID( - i.first, - i.second, - first_frame, - base_.media_reference(base_.current(media_type)).rate(), - detail.name_, - detail.key_format_, - base_.reader(), - caf::actor_cast(this), - utility::JsonStore(), - base_.uuid(), - parent_uuid_, - media_type)); - results.back().timecode_ = timecode; - timecode = timecode + 1; - } - - rp.deliver(results); + [=](bool) mutable { + if (base_.current(media_type).is_null()) { + rp.deliver(make_error(xstudio_error::error, "No streams")); + return; } + const auto dur = + base_.media_reference(base_.current(media_type)).duration(); + LogicalFrameRanges ranges; + ranges.emplace_back(0, dur.frames() - 1); + mail(atom, media_type, ranges) + .request(caf::actor_cast(this), infinite) + .then( + [=](const media::AVFrameIDs ids) mutable { + media::FrameTimeMap *result = new media::FrameTimeMap; + timebase::flicks t(0); + for (const auto &fid : ids) { + (*result)[t] = fid; + t += dur.rate().to_flicks(); + } + rp.deliver(media::FrameTimeMapPtr(result)); + }, + [=](const error &err) mutable { rp.deliver(err); }); }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + [=](const error &err) mutable { rp.deliver(err); }); return rp; }, - [=](get_media_pointer_atom, + [=](get_media_pointers_atom, const MediaType media_type, - const int logical_frame) -> result { - auto rp = make_response_promise(); - - if (base_.current(media_type).is_null()) { - rp.deliver(make_error(xstudio_error::error, "No streams")); - return rp; - } - - request( - media_streams_.at(base_.current(media_type)), - infinite, - get_stream_detail_atom_v) - .then( - [=](const StreamDetail &detail) mutable { - try { - int frame; - auto _uri = base_.media_reference(base_.current(media_type)) - .uri(logical_frame, frame); - if (not _uri) { - throw std::runtime_error("Invalid frame index"); - } + const LogicalFrameRanges &ranges, + const utility::Uuid clip_uuid) -> caf::result { + auto rp = make_response_promise(); - if (media_type == MT_IMAGE) { - // get colours params - request( - json_store_, - infinite, - json_store::get_json_atom_v, - "/colour_pipeline") - .then( - [=](const JsonStore &meta) mutable { - rp.deliver(media::AVFrameID( - *_uri, - frame, - *(base_ - .media_reference( - base_.current(media_type)) - .frame(0)), - base_.media_reference(base_.current(media_type)) - .rate(), - detail.name_, - detail.key_format_, - base_.reader(), - caf::actor_cast(this), - meta, - base_.uuid(), - parent_uuid_, - media_type)); - }, - [=](error &) mutable { - rp.deliver(media::AVFrameID( - *_uri, - frame, - *(base_ - .media_reference( - base_.current(media_type)) - .frame(0)), - base_.media_reference(base_.current(media_type)) - .rate(), - detail.name_, - detail.key_format_, - base_.reader(), - caf::actor_cast(this), - utility::JsonStore(), - utility::Uuid(), - parent_uuid_, - media_type)); - }); - } else { - - rp.deliver(media::AVFrameID( - *_uri, - frame, - *(base_.media_reference(base_.current(media_type)) - .frame(0)), - base_.media_reference(base_.current(media_type)).rate(), - detail.name_, - detail.key_format_, - base_.reader(), - caf::actor_cast(this), - utility::JsonStore(), - base_.uuid(), - parent_uuid_, - media_type)); - } - } catch (const std::exception &e) { - rp.deliver(make_error(xstudio_error::error, e.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + get_media_pointers_for_frames(media_type, ranges, rp, clip_uuid); return rp; }, [=](get_media_pointers_atom, const MediaType media_type, - const LogicalFrameRanges &ranges) -> caf::result { - if (base_.empty()) { - if (base_.error_detail().empty()) { - return make_error(xstudio_error::error, "No MediaStreams"); - } else { - return make_error(xstudio_error::error, base_.error_detail()); - } - } - - auto rp = make_response_promise(); - get_media_pointers_for_frames(media_type, ranges, rp); - return rp; - }, - - [=](media_reader::cancel_thumbnail_request_atom atom, const utility::Uuid job_uuid) { - anon_send(thumbnail_manager, atom, job_uuid); + const LogicalFrameRanges &ranges) { + return mail(get_media_pointers_atom_v, media_type, ranges, utility::Uuid()) + .delegate(caf::actor_cast(this)); }, [=](media_reader::get_thumbnail_atom, - float position, - const utility::Uuid job_uuid, - caf::actor requester) { + float position) -> result { + auto rp = make_response_promise(); int frame = (int)round(float(base_.media_reference().frame_count()) * position); frame = std::max(0, std::min(frame, base_.media_reference().frame_count() - 1)); - request( - caf::actor_cast(this), - infinite, - get_media_pointer_atom_v, - media::MediaType::MT_IMAGE, - frame) + mail(get_media_pointer_atom_v, media::MediaType::MT_IMAGE, frame) + .request(caf::actor_cast(this), infinite) .then( [=](const media::AVFrameID &mp) mutable { - request( - thumbnail_manager, - infinite, - media_reader::get_thumbnail_atom_v, - mp, - job_uuid) + mail(media_reader::get_thumbnail_atom_v, mp) + .request(thumbnail_manager, infinite) .then( [=](const thumbnail::ThumbnailBufferPtr &buf) mutable { - anon_send( - requester, buf, position, job_uuid, std::string()); + // mail(utility::event_atom_v, + // media_reader::get_thumbnail_atom_v, + // buf).send(base_.event_group()); + rp.deliver(buf); }, - [=](error &err) mutable { - anon_send( - requester, - thumbnail::ThumbnailBufferPtr(), - 0.0f, - job_uuid, - to_string(err)); - }); + [=](error &err) mutable { rp.deliver(err); }); }, - [=](error &err) mutable { - anon_send( - requester, - thumbnail::ThumbnailBufferPtr(), - 0.0f, - job_uuid, - to_string(err)); - }); + [=](error &err) mutable { rp.deliver(err); }); + return rp; }, [=](media_reference_atom) -> MediaReference { return base_.media_reference(); }, - [=](media_reference_atom, const MediaReference &mr) -> bool { - base_.set_media_reference(mr); - // update state.. - update_media_status(); - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); + [=](media_reference_atom, MediaReference mr) -> bool { + if (mr != base_.media_reference()) { + base_.set_media_reference(mr); + uri_status_cache_.clear(); + + // update state.. + update_media_status(); + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + } else { + return false; + } + return true; + }, + + [=](media_reference_atom, MediaReference mr, bool force_change_signal) -> bool { + if (mr != base_.media_reference() || force_change_signal) { + base_.set_media_reference(mr); + uri_status_cache_.clear(); + + // update state.. + update_media_status(); + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + } else { + return false; + } return true; }, @@ -846,6 +706,15 @@ void MediaSourceActor::init() { return std::pair(uuid, base_.media_reference()); }, + [=](get_media_stream_atom, + const MediaType media_type, + bool current) -> result { + if (media_streams_.count(base_.current(media_type))) { + return media_streams_[base_.current(media_type)]; + } + return make_error(xstudio_error::error, "No MediaStreams"); + }, + [=](get_media_stream_atom, const MediaType media_type) -> std::vector { std::vector sm; @@ -855,6 +724,37 @@ void MediaSourceActor::init() { return sm; }, + [=](get_media_stream_atom, const int stream_index) -> result { + // get the stream actor for the given index + auto rp = make_response_promise(); + auto count = std::make_shared(media_streams_.size()); + for (const auto &p : media_streams_) { + + caf::actor media_stream = p.second; + mail(get_stream_detail_atom_v) + .request(p.second, infinite) + .then( + [=](const StreamDetail &detail) mutable { + if (detail.index_ == stream_index) { + rp.deliver(media_stream); + } + (*count)--; + if (!*count && rp.pending()) { + rp.deliver(make_error( + xstudio_error::error, "No stream matching index.")); + } + }, + [=](caf::error err) mutable { + (*count)--; + if (!*count && rp.pending()) { + rp.deliver(err); + } + }); + } + + return rp; + }, + [=](get_media_stream_atom, const Uuid &uuid) -> result { if (media_streams_.count(uuid)) return media_streams_.at(uuid); @@ -868,134 +768,168 @@ void MediaSourceActor::init() { [=](get_stream_detail_atom, const MediaType media_type) -> result { if (media_streams_.count(base_.current(media_type))) { auto rp = make_response_promise(); - request( - media_streams_.at(base_.current(media_type)), - infinite, - get_stream_detail_atom_v) + + mail(get_stream_detail_atom_v) + .request(media_streams_.at(base_.current(media_type)), infinite) .then( [=](const StreamDetail &sd) mutable { rp.deliver(sd); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); + return rp; } // mark as bad source.. - if (base_.media_status() == MS_ONLINE) { - anon_send(this, media_status_atom_v, MS_MISSING); - } + // if (base_.media_status() == MS_ONLINE) { + // anon_mail(media_status_atom_v, MS_MISSING).send(this); + // } return result(make_error(xstudio_error::error, "No streams")); }, - [=](json_store::get_json_atom atom, const std::string &path) { - delegate(json_store_, atom, path); - // metadata changed - need to broadcast an update - // base_.send_changed(event_group_, this); - // send(event_group_, utility::event_atom_v, change_atom_v); + [=](json_store::get_json_atom, + const std::string &path, + std::vector children_to_try) -> result { + // used in message handler immediately below for recursive call to another actor to + // get json data from children. Calling 'request' in a chain/loop in the way we + // want is pretty ugly to do in terms of the code which is why I use this + // recursive trick + auto rp = make_response_promise(); + mail(json_store::get_json_atom_v, path) + .request(children_to_try.front(), infinite) + .then( + [=](const utility::JsonStore &j) mutable { rp.deliver(j); }, + [=](error &err) mutable { + // we got an error, probably because the child we tried + // does not have data at the given path. Continue trying + // children until exhausted + children_to_try.erase(children_to_try.begin()); + if (children_to_try.size()) { + rp.delegate( + caf::actor_cast(this), + json_store::get_json_atom_v, + path, + children_to_try); + } else { + rp.deliver(err); + } + }); + return rp; + }, + + [=](json_store::get_json_atom _get_atom, + const std::string &path, + bool try_children) -> result { + // request for metadata. Is the metadata path actually valid for us or for one + // of our MediaStream children? Try children in order of current iamge source + // then current media source. + + // This message handler is used by UI layer to display metadata fields in the + // xSTUDIO media list + auto rp = make_response_promise(); + std::vector children_to_try({json_store_}); + + if (try_children) { + if (media_streams_.count(base_.current(MT_IMAGE))) + children_to_try.push_back(media_streams_.at(base_.current(MT_IMAGE))); + if (media_streams_.count(base_.current(MT_AUDIO))) + children_to_try.push_back(media_streams_.at(base_.current(MT_AUDIO))); + } + + rp.delegate(caf::actor_cast(this), _get_atom, path, children_to_try); + return rp; + }, + + [=](json_store::get_json_atom atom) { return mail(atom).delegate(json_store_); }, + + [=](json_store::get_json_atom atom, + const std::string &path) -> result { + auto rp = make_response_promise(); + + if (path.find("/metadata/media") == 0) { + // ensure metadata has indeed been read before passing to json store + mail(media_metadata::get_metadata_atom_v) + .request(caf::actor_cast(this), infinite) + .then( + [=](bool) mutable { rp.delegate(json_store_, atom, path); }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + } else { + rp.delegate(json_store_, atom, path); + } + return rp; + }, + + [=](json_store::get_json_atom atom, + const std::vector &paths) -> caf::result { + // multi json store value request + auto rp = make_response_promise(); + if (!media_metadata_up_to_date_) { + mail(media_metadata::get_metadata_atom_v) + .request(caf::actor_cast(this), infinite) + .then( + [=](bool) mutable { rp.delegate(json_store_, atom, paths); }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + } else { + rp.delegate(json_store_, atom, paths); + } + return rp; }, [=](json_store::set_json_atom atom, const JsonStore &json) { - delegate(json_store_, atom, json); // metadata changed - need to broadcast an update - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + return mail(atom, json).delegate(json_store_); }, [=](json_store::merge_json_atom atom, const JsonStore &json) { - delegate(json_store_, atom, json); // metadata changed - need to broadcast an update - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + return mail(atom, json).delegate(json_store_); }, [=](json_store::set_json_atom atom, const JsonStore &json, const std::string &path) { - delegate(json_store_, atom, json, path); // metadata changed - need to broadcast an update - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + return mail(atom, json, path).delegate(json_store_); }, [=](media::invalidate_cache_atom) -> caf::result { auto rp = make_response_promise(); - // build list of our possible cache keys.. - request(caf::actor_cast(this), infinite, media_cache::keys_atom_v) - .then( - [=](const media::MediaKeyVector &keys) mutable { - auto image_cache = - system().registry().template get(image_cache_registry); - auto audio_cache = - system().registry().template get(audio_cache_registry); - std::vector caches; - - if (image_cache) - caches.push_back(image_cache); - if (audio_cache) - caches.push_back(audio_cache); - if (caches.empty()) { - rp.deliver(media::MediaKeyVector()); - return; - } - - fan_out_request( - caches, infinite, media_cache::erase_atom_v, keys) - .then( - [=](std::vector erased_keys) mutable { - media::MediaKeyVector result; - for (const auto &i : erased_keys) - result.insert(result.end(), i.begin(), i.end()); - rp.deliver(result); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - - return rp; - }, - - [=](media_cache::keys_atom) -> caf::result { - auto rp = make_response_promise(); - deliver_frames_media_keys(rp, MT_IMAGE, std::vector()); - return rp; - }, - - [=](media_cache::keys_atom, const MediaType media_type) -> caf::result { - auto rp = make_response_promise(); - deliver_frames_media_keys(rp, media_type, std::vector()); - return rp; - }, + media::MediaKeyVector keys(all_requested_frames_.size()); + std::copy(all_requested_frames_.begin(), all_requested_frames_.end(), keys.begin()); + + auto image_cache = + system().registry().template get(image_cache_registry); + auto audio_cache = + system().registry().template get(audio_cache_registry); + std::vector caches; + + if (image_cache) + caches.push_back(image_cache); + if (audio_cache) + caches.push_back(audio_cache); + if (caches.empty()) { + rp.deliver(media::MediaKeyVector()); + return rp; + } - [=](media_cache::keys_atom, - const MediaType media_type, - const int logical_frame) -> caf::result { - auto rp = make_response_promise(); - - request( - caf::actor_cast(this), - infinite, - media_cache::keys_atom_v, - media_type, - std::vector({logical_frame})) + fan_out_request( + caches, infinite, media_cache::erase_atom_v, keys) .then( - [=](const MediaKeyVector &r) mutable { - if (r.size()) { - rp.deliver(r[0]); - } else { - rp.deliver(make_error(xstudio_error::error, "No keys for frames")); - } + [=](std::vector erased_keys) mutable { + media::MediaKeyVector result; + for (const auto &i : erased_keys) + result.insert(result.end(), i.begin(), i.end()); + rp.deliver(result); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); return rp; }, - [=](media_cache::keys_atom, - const MediaType media_type, - const std::vector &logical_frames) -> caf::result { - auto rp = make_response_promise(); - deliver_frames_media_keys(rp, media_type, logical_frames); - return rp; - }, - [=](media_hook::get_media_hook_atom) -> caf::result { auto rp = make_response_promise(); auto m_actor = system().registry().template get(media_hook_registry); @@ -1003,7 +937,8 @@ void MediaSourceActor::init() { if (not m_actor) { rp.deliver(false); } else { - request(m_actor, infinite, media_hook::get_media_hook_atom_v, this) + mail(media_hook::get_media_hook_atom_v, this) + .request(m_actor, infinite) .then( [=](const bool done) mutable { rp.deliver(done); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); @@ -1013,18 +948,24 @@ void MediaSourceActor::init() { }, [=](media_metadata::get_metadata_atom) -> caf::result { + auto rp = make_response_promise(); + if (media_metadata_up_to_date_) { + rp.deliver(true); + return rp; + } + auto m_actor = system().registry().template get(media_metadata_registry); - if (not m_actor) - return caf::result(false); - - auto rp = make_response_promise(); + if (not m_actor) { + rp.deliver(false); + return rp; + } try { + if (not base_.media_reference().container()) { int file_frame; auto first_uri = base_.media_reference().uri(0, file_frame); - // #pragma message "Currently only reading metadata on first frame for image // sequences" @@ -1032,25 +973,26 @@ void MediaSourceActor::init() { // big or multiple sequences if (first_uri) { - request(m_actor, infinite, get_metadata_atom_v, *first_uri, file_frame) + mail(get_metadata_atom_v, *first_uri, file_frame) + .request(m_actor, infinite) .then( [=](const std::pair &meta) mutable { - request( - json_store_, - infinite, + mail( json_store::set_json_atom_v, meta.first, "/metadata/media/@" + std::to_string(meta.second), true) + .request(json_store_, infinite) .then( [=](const bool &done) mutable { + media_metadata_up_to_date_ = true; rp.deliver(done); // notify any watchers that metadata is updated - send( - event_group_, + mail( utility::event_atom_v, get_metadata_atom_v, - meta.first); + meta.first) + .send(base_.event_group()); }, [=](error &err) mutable { rp.deliver(std::move(err)); @@ -1075,8 +1017,8 @@ void MediaSourceActor::init() { if(i == frames.size()-1) { rp.deliver(done); // notify any watchers that - metadata is updated send(event_group_, utility::event_atom_v, - get_metadata_atom_v, meta.first); + metadata is updated mail(utility::event_atom_v, + get_metadata_atom_v, meta.first).send(base_.event_group()); } }, [=](error& err) mutable { @@ -1092,25 +1034,28 @@ void MediaSourceActor::init() { }*/ } else { - request( - m_actor, infinite, get_metadata_atom_v, base_.media_reference().uri()) + mail(get_metadata_atom_v, base_.media_reference().uri()) + .request(m_actor, infinite) .then( [=](const std::pair &meta) mutable { - request( - json_store_, - infinite, + send_stream_metadata_to_stream_actors(meta.first); + + mail( + json_store::set_json_atom_v, meta.first, "/metadata/media/@") + .request(json_store_, infinite) .then( [=](const bool &done) mutable { + media_metadata_up_to_date_ = true; rp.deliver(done); // notify any watchers that metadata is updated - send( - event_group_, + mail( utility::event_atom_v, get_metadata_atom_v, - meta.first); + meta.first) + .send(base_.event_group()); }, [=](error &err) mutable { rp.deliver(std::move(err)); @@ -1138,15 +1083,15 @@ void MediaSourceActor::init() { auto rp = make_response_promise(); auto m_actor = system().registry().template get(media_metadata_registry); - request(m_actor, infinite, get_metadata_atom_v, *_uri) + mail(get_metadata_atom_v, *_uri) + .request(m_actor, infinite) .then( [=](const std::pair &meta) mutable { - request( - json_store_, - infinite, + mail( json_store::set_json_atom_v, meta.first, "/metadata/media/@" + std::to_string(meta.second)) + .request(json_store_, infinite) .then( [=](const bool &done) mutable { rp.deliver(done); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); @@ -1155,72 +1100,9 @@ void MediaSourceActor::init() { return rp; }, - [=](utility::duplicate_atom) -> result { - auto rp = make_response_promise(); - auto uuid = utility::Uuid::generate(); - auto actor = spawn( - base_.name(), base_.reader(), base_.media_reference(), uuid); - - // using a lambda to try and make this more, err, 'readable' - auto copy_metadata = [=](UuidActor destination, - caf::typed_response_promise rp) { - request(json_store_, infinite, json_store::get_json_atom_v) - .then( - [=](const JsonStore &meta) mutable { - request( - destination.actor(), - infinite, - json_store::set_json_atom_v, - meta) - .then( - [=](bool) mutable { rp.deliver(destination); }, - [=](const error &err) mutable { rp.deliver(err); }); - }, - [=](const error &err) mutable { rp.deliver(err); }); - }; - - // duplicate streams.. - if (media_streams_.size()) { - auto source_count = std::make_shared(); - (*source_count) = media_streams_.size(); - for (auto p : media_streams_) { - request(p.second, infinite, utility::duplicate_atom_v) - .await( - [=](UuidActor stream) mutable { - // add the stream to the duplicated media_source_actor - request(actor, infinite, add_media_stream_atom_v, stream) - .await( - [=](UuidActor) mutable { - // set the current stream as required - if (p.first == base_.current(MT_IMAGE)) { - - anon_send( - actor, - current_media_stream_atom_v, - MT_IMAGE, - stream.uuid()); - - } else if (p.first == base_.current(MT_AUDIO)) { - - anon_send( - actor, - current_media_stream_atom_v, - MT_AUDIO, - stream.uuid()); - } - - (*source_count)--; - if (!(*source_count)) { - copy_metadata(UuidActor(uuid, actor), rp); - } - }, - [=](const error &err) mutable { rp.deliver(err); }); - }, - [=](const error &err) mutable { rp.deliver(err); }); - } - } else { - copy_metadata(UuidActor(uuid, actor), rp); - } + [=](utility::duplicate_atom) -> result { + auto rp = make_response_promise(); + duplicate(rp); return rp; }, @@ -1229,7 +1111,7 @@ void MediaSourceActor::init() { [=](utility::event_atom, utility::name_atom, const std::string & /*name*/) {}, [=](utility::get_group_atom _get_group_atom) { - delegate(json_store_, _get_group_atom); + return mail(_get_group_atom).delegate(json_store_); }, [=](media::checksum_atom) -> std::pair { @@ -1240,76 +1122,114 @@ void MediaSourceActor::init() { // force thumbnail update on change. Might cause double update.. auto old_size = base_.checksum().second; if (base_.checksum(checksum) and old_size) { - send( - event_group_, - utility::event_atom_v, - media_status_atom_v, - base_.media_status()); + mail(utility::event_atom_v, media_status_atom_v, base_.media_status()) + .send(base_.event_group()); // trigger re-eval of reader.. - request( - caf::actor_cast(this), - infinite, - get_media_pointer_atom_v, - MT_IMAGE, - static_cast(0)) + mail(get_media_pointer_atom_v, MT_IMAGE, static_cast(0)) + .request(caf::actor_cast(this), infinite) .then( [=](const media::AVFrameID &tmp) { auto global_media_reader = system().registry().template get( media_reader_registry); - anon_send(global_media_reader, retire_readers_atom_v, tmp); + anon_mail(retire_readers_atom_v, tmp).send(global_media_reader); }, [=](const error &err) {}); } }, + [=](media::pixel_aspect_atom, const double new_aspect) { + if (media_streams_.count(base_.current(MT_IMAGE))) + anon_mail(media::pixel_aspect_atom_v, new_aspect) + .send(media_streams_.at(base_.current(MT_IMAGE))); + }, + [=](media::rescan_atom atom) -> result { auto rp = make_response_promise(); + if (!base_.media_reference().container()) { + // before we do the rescan, we need to update the partial_sequence_behaviour + // in case it has changed since the last scan of the frames + auto prefs_actor = + system().registry().template get(global_store_registry); + if (prefs_actor) { + mail(get_json_atom_v, "/core/session/partial_sequence_behaviour/value") + .request(prefs_actor, infinite) + .then( + [=](const utility::JsonStore &partial_seq_behaviour) mutable { + if (partial_seq_behaviour.is_string()) { + auto p = partialSeqNameMap.find( + partial_seq_behaviour.get()); + if (p != partialSeqNameMap.end()) { + if (p->second != base_.partial_seq_behaviour()) { + base_.set_partial_seq_behaviour(p->second); + rp.delegate( + caf::actor_cast(this), atom, true); + } + } + } + rp.delegate(caf::actor_cast(this), atom, false); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.delegate(caf::actor_cast(this), atom, false); + }); + } else { + rp.delegate(caf::actor_cast(this), atom, false); + } + } else { + rp.delegate(caf::actor_cast(this), atom, false); + } + return rp; + }, + + + [=](media::rescan_atom atom, bool force_change_signal) -> result { + auto rp = make_response_promise(); // trigger status update update_media_status(); auto scanner = system().registry().template get(scanner_registry); if (scanner) { - request(scanner, infinite, atom, base_.media_reference()) + mail(atom, base_.media_reference()) + .request(scanner, infinite) .then( - [=](const MediaReference &mr) mutable { - if (mr != base_.media_reference()) { - request( - caf::actor_cast(this), - infinite, - media_reference_atom_v, - mr) - .then( - [=](const bool) mutable { - // rebuild hash (file might have changed) - auto scanner = - system().registry().template get( - scanner_registry); - if (scanner) - anon_send( - scanner, - checksum_atom_v, - this, - base_.media_reference()); - - anon_send(this, invalidate_cache_atom_v); - rp.deliver(base_.media_reference()); - }, - [=](const error &err) mutable { rp.deliver(err); }); - } else { - auto scanner = system().registry().template get( - scanner_registry); - if (scanner) - anon_send( - scanner, - checksum_atom_v, - this, - base_.media_reference()); - anon_send(this, invalidate_cache_atom_v); - rp.deliver(base_.media_reference()); + [=](MediaReference mr) mutable { + if (!mr.container() && base_.partial_seq_behaviour() != + PS_COLLAPSE_TO_ON_DISK_FRAMES) { + + // at this point, media_reference will contain a list of + // numbered frames that are on disk only. If we don't have + // PS_COLLAPSE_TO_ON_DISK_FRAMES behaviour, we now tell the + // media_reference to fill in the gaps in the frame numbers + // (even if they are not on disk) so that we can use held frame + // behaviour. + mr.fill_partial_sequences(); } + + // Note: not using 'force_change_signal' - instead, passing true in + // this request which will make us emit a change_atom event, forcing + // Playhead/Timeline etc. to rebuild the frames list. Thus, if more + // frames are on-disk since last time, we will check for them (when + // plahead requests AVFrameIDs) again. + mail(media_reference_atom_v, mr, true /*force_change_signal*/) + .request(caf::actor_cast(this), infinite) + .then( + [=](const bool) mutable { + // rebuild hash (file might have changed) + auto scanner = + system().registry().template get( + scanner_registry); + if (scanner) + anon_mail( + checksum_atom_v, this, base_.media_reference()) + .send(scanner); + + anon_mail(invalidate_cache_atom_v).send(this); + rp.deliver(base_.media_reference()); + }, + [=](const error &err) mutable { rp.deliver(err); }); }, [=](const error &err) mutable { rp.deliver(err); }); } else { @@ -1324,23 +1244,25 @@ void MediaSourceActor::init() { [=](utility::parent_atom, const UuidActor &parent) { parent_uuid_ = parent.uuid(); parent_ = actor_cast(parent.actor()); - base_.send_changed(event_group_, this); + base_.send_changed(); }, // deprecated [=](utility::parent_atom, caf::actor parent) { - request(parent, infinite, utility::uuid_atom_v) + mail(utility::uuid_atom_v) + .request(parent, infinite) .then( [=](const utility::Uuid &parent_uuid) { parent_uuid_ = parent_uuid; }, ERR_HANDLER_FUNC); parent_ = actor_cast(parent); - base_.send_changed(event_group_, this); + base_.send_changed(); }, [=](utility::serialise_atom) -> result { auto rp = make_response_promise(); - request(json_store_, infinite, json_store::get_json_atom_v, "") + mail(json_store::get_json_atom_v, "") + .request(json_store_, infinite) .then( [=](const JsonStore &meta) mutable { std::vector clients; @@ -1374,158 +1296,325 @@ void MediaSourceActor::init() { }, [=](error &err) mutable { rp.deliver(std::move(err)); }); return rp; - }); + }}; +} + + +void MediaSourceActor::init() { + print_on_create(this, base_); + print_on_exit(this, base_); + + update_media_status(); + + // set an empty dict for colour_pipeline, as we request this at various + // times and need a placeholder or we get warnings if it's not there + mail(json_store::get_json_atom_v, "/colour_pipeline") + .request(json_store_, infinite) + .then( + [=](const JsonStore &) {}, + [=](const error &) { + // we'll get this error if there is no dict already + anon_mail(json_store::set_json_atom_v, utility::JsonStore(), "/colour_pipeline") + .send(json_store_); + }); } void MediaSourceActor::get_media_pointers_for_frames( const MediaType media_type, const LogicalFrameRanges &ranges, - caf::typed_response_promise rp) { + caf::typed_response_promise rp, + const utility::Uuid clip_uuid) { + + // func ptr to complete the task + auto do_get_pointers = [=]() mutable { + // fetch colour management data + mail(json_store::get_json_atom_v, "/colour_pipeline") + .request(json_store_, infinite) + .then( + [=](const JsonStore &colour_mgmt_data) mutable { + // fetch media detail + if (!media_streams_.contains(base_.current(media_type))) { + rp.deliver(make_error(xstudio_error::error, "No streams")); + return; + } + + mail(get_stream_detail_atom_v) + .request(media_streams_.at(base_.current(media_type)), infinite) + .then( + [=](const StreamDetail &detail) mutable { + get_media_pointers_for_frames( + media_type, + ranges, + rp, + clip_uuid, + colour_mgmt_data, + detail); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + }; + + // before we can deliver frame pointers to allow playback, ensure + // that we have completed the aquisition of the media detail to + // inspect the source, get duration, assign default Image/Audio + // streams etc. + + mail(acquire_media_detail_atom_v) + .request(caf::actor_cast(this), infinite) + .then( + [=](bool) mutable { do_get_pointers(); }, + [=](const error &err) mutable { + // we proceed on error, in order to make blank frames + do_get_pointers(); + }); +} + +void MediaSourceActor::get_media_pointers_for_frames( + const MediaType media_type, + const LogicalFrameRanges &ranges, + caf::typed_response_promise rp, + const utility::Uuid clip_uuid, + const utility::JsonStore &colour_mgmt_data, + const StreamDetail &media_detail) { + + // make a blank frame id that nevertheless includes media source actor uuid + // and address - we use this if we are trying to resolve a frame ID + // that is outside the frame range of the media reference. If media is not + // on disk, this is guaranteed to happen. We still want the frameIDs to + // include the media uuid, media source uuid & source actor address so + // we know stuff about the media source that is *supposed* to be onscreen. + media::AVFrameID blank = *(media::make_blank_frame( + media_type, + parent_uuid_, + base_.uuid(), + clip_uuid, + parent_, + caf::actor_cast(this))); + blank.set_frame_status(media::FS_NOT_ON_DISK); + if (base_.current(media_type).is_null()) { + // in the case where there is no source, return list of empty frames. // This is useful for sources that have no audio or no video, to keep // them compatible with the video based frame request/deliver playback // system + auto blank_ptr = std::make_shared(blank); media::AVFrameIDs result; for (const auto &i : ranges) { for (auto ii = i.first; ii <= i.second; ii++) - result.emplace_back(media::make_blank_frame(media_type) - // std::shared_ptr( - // new media::AVFrameID() - // ) - ); + result.emplace_back(blank_ptr); } rp.deliver(result); return; } - // get colours params ... only need this for media_type == MT_IMAGE though - request(json_store_, infinite, json_store::get_json_atom_v, "/colour_pipeline") - .then( - [=](const JsonStore &meta) mutable { - request( - media_streams_.at(base_.current(media_type)), - infinite, - get_stream_detail_atom_v) - .then( - [=](const StreamDetail &detail) mutable { - media::AVFrameIDs result; - media::AVFrameID mptr; - auto timecode = - base_.media_reference(base_.current(media_type)).timecode(); - - for (const auto &i : ranges) { - for (auto logical_frame = i.first; logical_frame <= i.second; - logical_frame++) { - // the try block catches posible 'out_of_range' - // exception coming from MediaReference::uri() - try { - - int frame; - auto _uri = - base_.media_reference(base_.current(media_type)) - .uri(logical_frame, frame); - - - if (not _uri) - throw std::runtime_error("Time out of range"); - - if (mptr.is_nil()) { - mptr = media::AVFrameID( - *_uri, - frame, - *(base_ - .media_reference( - base_.current(media_type)) - .frame(0)), - base_.media_reference(base_.current(media_type)) - .rate(), - detail.name_, - detail.key_format_, - base_.reader(), - caf::actor_cast(this), - meta, - base_.uuid(), - parent_uuid_, - media_type); - } else { - mptr.uri_ = *_uri; - mptr.frame_ = frame; - mptr.key_ = media::MediaKey( - detail.key_format_, *_uri, frame, detail.name_); - } + auto blank_ptr = std::make_shared(blank); - mptr.timecode_ = timecode; - mptr.playhead_logical_frame_ = logical_frame; - timecode = timecode + 1; - result.emplace_back( - std::shared_ptr( - new media::AVFrameID(mptr))); - } catch ([[maybe_unused]] const std::exception &e) { - result.emplace_back( - media::make_blank_frame(media_type)); - } - } - } - rp.deliver(result); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + media::AVFrameIDs result; + media::AVFrameID base_frame_id; + auto timecode = base_.media_reference(base_.current(media_type)).timecode(); + + int prev_range_last = 0; + for (const auto &i : ranges) { + + // We are providing frameIds for a set of logical + // frame ranges. We need to account for the ranges + // to increment the timecode for the 'gaps' between + // the ranges + if (prev_range_last < i.first) { + timecode = timecode + (i.first - prev_range_last); + } + + for (auto logical_frame = i.first; logical_frame <= i.second; logical_frame++) { + // the try block catches posible 'out_of_range' + // exception coming from MediaReference::uri() + try { + + int frame, keyframe; + FrameStatus frame_status; + auto _uri = uri_for_logical_frame( + media_type, logical_frame, frame, keyframe, frame_status); + + if (base_frame_id.is_nil()) { + // TODO: less hideous creation of AVFrameID + base_frame_id = media::AVFrameID( + _uri, + frame, + *(base_.media_reference(base_.current(media_type)).frame(0)), + frame_status, + media_detail.pixel_aspect_, + base_.media_reference(base_.current(media_type)).rate(), + media_detail.name_, + media_detail.key_format_, + base_.reader(), + parent_, + caf::actor_cast(this), + colour_mgmt_data, + base_.uuid(), + parent_uuid_, + clip_uuid, + media_type, + timecode); + } + + result.emplace_back(new media::AVFrameID( + base_frame_id, + _uri, + frame, + keyframe, + media_detail.key_format_, + frame_status, + timecode)); + + all_requested_frames_.insert(result.back()->key()); + + timecode = timecode + 1; + + } catch ([[maybe_unused]] const std::exception &e) { + // spdlog::warn("{}", e.what()); + result.emplace_back(blank_ptr); + } + } + } + rp.deliver(result); } -void MediaSourceActor::deliver_frames_media_keys( - caf::typed_response_promise rp, +caf::uri MediaSourceActor::uri_for_logical_frame( const MediaType media_type, - const std::vector logical_frames) { - if (base_.empty()) { - if (base_.error_detail().empty()) { - rp.deliver(make_error(xstudio_error::error, "No MediaStreams")); + const int logical_frame, + int &frame, + int &keyframe, + FrameStatus &frame_status) { + + const auto &media_ref = base_.media_reference(base_.current(media_type)); + + auto _uri = media_ref.uri(logical_frame, frame); + if (not _uri) + throw std::runtime_error("Time out of range"); + keyframe = frame; + frame_status = FS_ON_DISK; + + if (!media_ref.container() && base_.partial_seq_behaviour() == PS_DONT_HOLD_FRAME) { + + auto p = uri_status_cache_.find(logical_frame); + if (p != uri_status_cache_.end()) { + frame_status = p->second.status_; + keyframe = p->second.frame_; + return p->second.uri_; + } + // is this uri on disk? + const auto path = fs::path(utility::uri_to_posix_path(*_uri)); + if (fs::exists(path)) { + // store so we don't need to check again + uri_status_cache_[logical_frame] = UriStatus(*_uri, FS_ON_DISK, frame); + frame_status = FS_ON_DISK; } else { - rp.deliver(make_error(xstudio_error::error, base_.error_detail())); + uri_status_cache_[logical_frame] = UriStatus(*_uri, FS_NOT_ON_DISK, frame); + frame_status = FS_NOT_ON_DISK; } - return; - } + return *_uri; - auto stream = base_.current(media_type); - if (stream.is_null()) { - rp.deliver(make_error(xstudio_error::error, "No Stream for MediaType")); - return; - } + } else if (!media_ref.container() && base_.partial_seq_behaviour() == PS_HOLD_FRAME) { - request(media_streams_.at(stream), infinite, get_stream_detail_atom_v) - .then( - [=](const StreamDetail &detail) mutable { - MediaKeyVector result; - if (logical_frames.empty()) { - - // if logical frames is empty, we return keys for ALL the frames in the - // source frame range - auto uris = base_.media_reference().uris(); - for (const auto &u : uris) { - result.emplace_back( - MediaKey(detail.key_format_, u.first, u.second, detail.name_)); - } + // To enable 'held frame' behaviour, we need to know if the requested + // frame is on-disk, and if not, whether there is another frame in + // the sequence that IS on disk that we can use as the held frame. - } else { + // We don't want to be stat'ing the filesystem any harder than we need + // to so as such I have added a map 'uri_status_cache_'. This stores + // the uri against the logical frame and whether it is on-disk or + // a held frame. - for (const int logical_frame : logical_frames) { - int frame; - try { - auto _uri = base_.media_reference().uri(logical_frame, frame); - if (not _uri) - throw std::runtime_error("Time out of range"); - - result.emplace_back( - MediaKey(detail.key_format_, *_uri, frame, detail.name_)); - } catch (...) { - result.emplace_back(MediaKey()); - } - } + auto p = uri_status_cache_.find(logical_frame); + if (p != uri_status_cache_.end()) { + frame_status = p->second.status_; + keyframe = p->second.frame_; + return p->second.uri_; + } + // is this uri on disk? + const auto path = fs::path(utility::uri_to_posix_path(*_uri)); + if (fs::exists(path)) { + // store so we don't need to check again + uri_status_cache_[logical_frame] = UriStatus(*_uri, FS_ON_DISK, frame); + frame_status = FS_ON_DISK; + return *_uri; + } + + const int frame_count = media_ref.frame_list().count(); + + // frame is NOT on disk. Now we enact 'held frame' behaviour by finding + // the nearest frame that IS on disk (if any) + + // first check if parent folder is on disk ... + auto parent_dir = path.parent_path(); + if (!fs::exists(parent_dir)) { + // parent folder is not on disk. Let's fill out the entire status + // cache with not-on-disk status for the entire sequence + const std::vector all_frames = media_ref.frame_list().frames(); + for (int i = 0; i < frame_count; ++i) { + int f; + auto _uri = media_ref.uri(i, f); + if (_uri) { + uri_status_cache_[logical_frame] = UriStatus(*_uri, FS_NOT_ON_DISK, f); } - rp.deliver(result); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + } + frame_status = FS_NOT_ON_DISK; + return *_uri; + } + + // for a given frame, check if it's in the cache - if not check if it's on + // disk. If it is, return the URI + auto held_frame_check = + [=, &keyframe](const int search_frame) mutable -> std::optional { + auto p = uri_status_cache_.find(search_frame); + if (p != uri_status_cache_.end() && p->second.status_ != FS_NOT_ON_DISK) { + // we've found a frame that is either on-disk or is + // a HELD frame. Return the uri + keyframe = p->second.frame_; + return p->second.uri_; + } + + // not cached. Check if is on disk + int f; + auto search_uri = media_ref.uri(search_frame, f); + const auto path = fs::path(utility::uri_to_posix_path(*search_uri)); + if (fs::exists(path)) { + uri_status_cache_[search_frame] = UriStatus(*search_uri, FS_ON_DISK, f); + keyframe = f; + return search_uri; + } + return {}; + }; + + // let's search forwards through the frame range until we find a uri + // that IS on disk and use that + + for (int search_frame = (logical_frame - 1); search_frame >= 0; --search_frame) { + + auto r = held_frame_check(search_frame); + if (r) { + frame_status = FS_HELD_FRAME; + uri_status_cache_[logical_frame] = UriStatus(*r, FS_HELD_FRAME, keyframe); + return *r; + } + } + + // backwards search didn't get a result. Now search forwards: + for (int search_frame = logical_frame; search_frame < frame_count; ++search_frame) { + + auto r = held_frame_check(search_frame); + if (r) { + frame_status = FS_HELD_FRAME; + uri_status_cache_[logical_frame] = UriStatus(*r, FS_HELD_FRAME, keyframe); + return *r; + } + } + + // search for an on-disk frame to hold on this (missing) frame has failed + frame_status = FS_NOT_ON_DISK; + } + return *_uri; } void MediaSourceActor::update_stream_media_reference( @@ -1539,6 +1628,16 @@ void MediaSourceActor::update_stream_media_reference( if (not media_reference.timecode().total_frames()) media_reference.set_timecode(timecode); + if (!media_reference.container() && + base_.partial_seq_behaviour() != PS_COLLAPSE_TO_ON_DISK_FRAMES) { + + // at this point, media_reference will contain a list of numbered frames that + // are on disk only. If we don't have PS_COLLAPSE_TO_ON_DISK_FRAMES behaviour, + // we now tell the media_reference to fill in the gaps in the frame numbers (even + // if they are not on disk) so that we can use held frame behaviour. + media_reference.fill_partial_sequences(); + } + // we don't know duration, either movie or single frame if (not media_reference.duration().duration().count()) { // movie.. @@ -1590,4 +1689,103 @@ void MediaSourceActor::update_stream_media_reference( media_reference.set_timecode_from_frames(); } base_.set_media_reference(media_reference); -} \ No newline at end of file + uri_status_cache_.clear(); +} + +void MediaSourceActor::send_stream_metadata_to_stream_actors(const utility::JsonStore &meta) { + // the metadata object returned by ffprobe is for the whole file with the metadata for + // each stream also included in an array. Here we search for the stream metadata and + // pass it to the relevant MediaStreamActor to own. + + if (meta.contains("streams") and meta["streams"].is_array()) { + const auto streams = meta["streams"]; + for (const auto &stream : streams) { + if (stream.contains("index") and stream["index"].is_number_integer()) { + // stream is a ref.. we need a local copy to use in our response + // (which gets copied again by the [=]) + utility::JsonStore stream_metadata = stream; + int idx = stream_metadata["index"].get(); + mail(get_media_stream_atom_v, idx) + .request(caf::actor_cast(this), infinite) + .then( + [=](caf::actor stream) { + anon_mail( + json_store::set_json_atom_v, + stream_metadata, + "/metadata/stream/@") + .send(stream); + }, + [=](caf::error &err) {}); + } + } + } +} + +void MediaSourceActor::duplicate(caf::typed_response_promise rp) { + auto uuid = utility::Uuid::generate(); + auto actor = + spawn(base_.name(), base_.reader(), base_.media_reference(), uuid); + + // using a lambda to try and make this more, err, 'readable' + auto copy_metadata = [=](UuidActor destination, + caf::typed_response_promise rp) { + mail(json_store::get_json_atom_v) + .request(json_store_, infinite) + .then( + [=](const JsonStore &meta) mutable { + mail(json_store::set_json_atom_v, meta) + .request(destination.actor(), infinite) + .then( + [=](bool) mutable { + rp.deliver(UuidUuidActor(base_.uuid(), destination)); + }, + [=](const error &err) mutable { rp.deliver(err); }); + }, + [=](const error &err) mutable { rp.deliver(err); }); + }; + + // duplicate streams.. + if (media_streams_.size()) { + auto source_count = std::make_shared(); + (*source_count) = media_streams_.size(); + for (auto p : media_streams_) { + mail(utility::duplicate_atom_v) + .request(p.second, infinite) + .await( + [=](UuidActor stream) mutable { + // add the stream to the duplicated media_source_actor + mail(add_media_stream_atom_v, stream) + .request(actor, infinite) + .await( + [=](UuidActor) mutable { + // set the current stream as required + if (p.first == base_.current(MT_IMAGE)) { + + anon_mail( + current_media_stream_atom_v, + MT_IMAGE, + stream.uuid()) + .send(actor); + + } else if (p.first == base_.current(MT_AUDIO)) { + + anon_mail( + current_media_stream_atom_v, + MT_AUDIO, + stream.uuid()) + .send(actor); + } + + (*source_count)--; + if (!(*source_count)) { + copy_metadata(UuidActor(uuid, actor), rp); + } + }, + [=](const error &err) mutable { rp.deliver(err); }); + }, + [=](const error &err) mutable { rp.deliver(err); }); + } + } else { + copy_metadata(UuidActor(uuid, actor), rp); + } +} diff --git a/src/media/src/media_stream.cpp b/src/media/src/media_stream.cpp index a63ef42b6..74cd383b8 100644 --- a/src/media/src/media_stream.cpp +++ b/src/media/src/media_stream.cpp @@ -9,11 +9,11 @@ using namespace xstudio::utility; MediaStream::MediaStream(const JsonStore &jsn) : utility::Container(static_cast(jsn["container"])) { + detail_.duration_ = jsn["duration"]; detail_.key_format_ = jsn["key_format"]; detail_.media_type_ = media_type_from_string(jsn["media_type"]); detail_.name_ = name(); - // older versions of xstudio did not serialise these values. MediaStreamActor // takes care of re-scanning for the data in this case if (jsn.contains("resolution")) @@ -39,4 +39,4 @@ JsonStore MediaStream::serialise() const { jsn["stream_index"] = detail_.index_; return jsn; -} \ No newline at end of file +} diff --git a/src/media/src/media_stream_actor.cpp b/src/media/src/media_stream_actor.cpp index 1d0e3f5c1..08adf3129 100644 --- a/src/media/src/media_stream_actor.cpp +++ b/src/media/src/media_stream_actor.cpp @@ -1,11 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include + #include #include #include "xstudio/atoms.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" -#include "xstudio/json_store/json_store_actor.hpp" +#include "xstudio/json_store/json_store_handler.hpp" #include "xstudio/media/media_actor.hpp" #include "xstudio/playhead/playhead_actor.hpp" #include "xstudio/utility/logging.hpp" @@ -21,52 +23,38 @@ using namespace caf; MediaStreamActor::MediaStreamActor(caf::actor_config &cfg, const JsonStore &jsn) : caf::event_based_actor(cfg), base_(static_cast(jsn["base"])) { - if (not jsn.count("store") or jsn["store"].is_null()) { - json_store_ = spawn(utility::Uuid::generate()); - } else { - json_store_ = spawn( - utility::Uuid::generate(), static_cast(jsn["store"])); - } - link_to(json_store_); + jsn_handler_ = JsonStoreHandler( + dynamic_cast(this), + base_.event_group(), + utility::Uuid::generate(), + not jsn.count("store") or jsn["store"].is_null() + ? JsonStore() + : static_cast(jsn["store"])); init(); } MediaStreamActor::MediaStreamActor( - caf::actor_config &cfg, const StreamDetail &detail, const utility::Uuid &uuid) + caf::actor_config &cfg, + const StreamDetail &detail, + const utility::Uuid &uuid, + const JsonStore &meta) : caf::event_based_actor(cfg), base_(detail) { + + jsn_handler_ = JsonStoreHandler( + dynamic_cast(this), + base_.event_group(), + utility::Uuid::generate(), + meta); + if (not uuid.is_null()) base_.set_uuid(uuid); - json_store_ = spawn(utility::Uuid::generate()); - link_to(json_store_); - init(); } -void MediaStreamActor::init() { - // only parial.. - // spdlog::debug( - // "Created MediaStreamActor {} {} {}", - // base_.name(), - // to_readable_string(base_.media_type()), - // to_string(base_.duration())); - print_on_create(this, base_); - print_on_exit(this, base_); - - auto event_group_ = spawn(this); - link_to(event_group_); - - behavior_.assign( - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), +caf::message_handler MediaStreamActor::message_handler() { + return caf::message_handler{ [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, [=](get_media_type_atom) -> MediaType { return base_.media_type(); }, @@ -75,24 +63,47 @@ void MediaStreamActor::init() { [=](const StreamDetail &detail) { base_.set_detail(detail); }, - [=](json_store::get_json_atom _get_atom, const std::string &path) { - return delegate(json_store_, _get_atom, path); + [=](json_store::set_json_atom atom, const JsonStore &json) { + // metadata changed - need to broadcast an update + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + return mail(atom, json).delegate(jsn_handler_.json_actor()); }, - [=](utility::duplicate_atom) -> UuidActor { - // clone ourself.. - const auto uuid = utility::Uuid::generate(); - const auto actor = spawn(base_.detail(), uuid); - return UuidActor(uuid, actor); + [=](json_store::set_json_atom atom, const JsonStore &json, const std::string &path) { + // metadata changed - need to broadcast an update + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + return mail(atom, json, path).delegate(jsn_handler_.json_actor()); }, - [=](utility::get_group_atom _get_group_atom) { - return delegate(json_store_, _get_group_atom); + [=](media::pixel_aspect_atom, const double new_aspect) { + StreamDetail detail = base_.detail(); + detail.pixel_aspect_ = new_aspect; + base_.set_detail(detail); + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + }, + + [=](utility::duplicate_atom) -> result { + // clone ourself.. + auto rp = make_response_promise(); + + mail(json_store::get_json_atom_v, "") + .request(jsn_handler_.json_actor(), infinite) + .then([=](const JsonStore &meta) mutable { + const auto uuid = utility::Uuid::generate(); + const auto actor = spawn(base_.detail(), uuid, meta); + rp.deliver(UuidActor(uuid, actor)); + }); + + return rp; }, [=](utility::serialise_atom) -> result { auto rp = make_response_promise(); - request(json_store_, infinite, json_store::get_json_atom_v, "") + mail(json_store::get_json_atom_v, "") + .request(jsn_handler_.json_actor(), infinite) .then([=](const JsonStore &meta) mutable { JsonStore jsn; jsn["store"] = meta; @@ -102,5 +113,17 @@ void MediaStreamActor::init() { }); return rp; - }); + }}; +} + + +void MediaStreamActor::init() { + // only parial.. + // spdlog::debug( + // "Created MediaStreamActor {} {} {}", + // base_.name(), + // to_readable_string(base_.media_type()), + // to_string(base_.duration())); + print_on_create(this, base_); + print_on_exit(this, base_); } diff --git a/src/media/test/CMakeLists.txt b/src/media/test/CMakeLists.txt index 6c32b6408..46e52c35e 100644 --- a/src/media/test/CMakeLists.txt +++ b/src/media/test/CMakeLists.txt @@ -4,7 +4,7 @@ SET(LINK_DEPS xstudio::media xstudio::audio_output xstudio::global - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/media/test/media_playhead_actor_test.cpp b/src/media/test/media_playhead_actor_test.cpp index 23469591a..b81914dd3 100644 --- a/src/media/test/media_playhead_actor_test.cpp +++ b/src/media/test/media_playhead_actor_test.cpp @@ -8,7 +8,6 @@ #include "xstudio/media_reader/media_reader.hpp" #include "xstudio/playhead/sub_playhead.hpp" #include "xstudio/playhead/playhead_actor.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/helpers.hpp" using namespace xstudio::utility; diff --git a/src/media/test/media_serialize_test.cpp b/src/media/test/media_serialize_test.cpp index 5d0ca8627..7dd7e99c0 100644 --- a/src/media/test/media_serialize_test.cpp +++ b/src/media/test/media_serialize_test.cpp @@ -5,7 +5,6 @@ #include "xstudio/atoms.hpp" #include "xstudio/media/media.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/json_store.hpp" @@ -23,7 +22,14 @@ TEST(StreamDetailSerializerTest, Test) { binary_serializer::container_type buf; binary_serializer bs{f.system, buf}; - StreamDetail u1(FrameRateDuration(10, 1.0), "test_stream", MT_IMAGE, "{0}@{1}/{2}"); + StreamDetail u1( + FrameRateDuration(10, 1.0), + "test_stream", + MT_IMAGE, + "{0}@{1}/{2}", + Imath::V2f(1920, 1080), + 1.0f, + 1.0); StreamDetail u2; EXPECT_EQ(u1, u1) << "Creation from string should be equal"; @@ -63,7 +69,7 @@ TEST(MediaPointerSerializerTest, Test) { binary_serializer::container_type buf; binary_serializer bs{f.system, buf}; - AVFrameID u1( + /*AVFrameID u1( posix_path_to_uri("cookham"), 10, 1, @@ -84,5 +90,5 @@ TEST(MediaPointerSerializerTest, Test) { e = bd.apply(u2); EXPECT_TRUE(e) << "unable to deserialize" << to_string(bd.get_error()) << std::endl; - EXPECT_EQ(u1, u2) << "Creation from string should be equal"; + EXPECT_EQ(u1, u2) << "Creation from string should be equal";*/ } diff --git a/src/media_cache/src/CMakeLists.txt b/src/media_cache/src/CMakeLists.txt index 58a2babf5..b3337b8f9 100644 --- a/src/media_cache/src/CMakeLists.txt +++ b/src/media_cache/src/CMakeLists.txt @@ -1,4 +1,4 @@ -project(media_cache VERSION 0.1.0 LANGUAGES CXX) +project(media_cache VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) set(SOURCES media_cache_actor.cpp @@ -11,8 +11,9 @@ default_options(${PROJECT_NAME}) target_link_libraries(${PROJECT_NAME} PUBLIC + xstudio::media xstudio::global_store - caf::core + CAF::core ) set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) diff --git a/src/media_cache/src/media_cache_actor.cpp b/src/media_cache/src/media_cache_actor.cpp index bdd51ad06..61cc6fab4 100644 --- a/src/media_cache/src/media_cache_actor.cpp +++ b/src/media_cache/src/media_cache_actor.cpp @@ -1,7 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 +#include + #include #include +#ifndef __apple__ #include +#endif #include "xstudio/atoms.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" @@ -35,15 +39,30 @@ class TrimActor : public caf::event_based_actor { }; TrimActor::TrimActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { - behavior_.assign([=](unpreserve_atom, const size_t count) { + + // trim memory usage, as when idle it'll slowly creep up until we run out of memory. + anon_mail(unpreserve_atom_v).delay(std::chrono::minutes(10)).send(this, weak_ref); + + behavior_.assign( + [=](unpreserve_atom, const size_t count) { // spdlog::stopwatch sw; #ifdef _WIN32 - _heapmin(); -#else - malloc_trim(64); + _heapmin(); +#elif defined(__linux__) + malloc_trim(64); #endif - // spdlog::warn("Release {:.3f}", sw); - }); + // spdlog::warn("Release {:.3f}", sw); + }, + [=](unpreserve_atom) { +#ifdef _WIN32 + _heapmin(); +#elif defined(__linux__) + malloc_trim(64); +#endif + anon_mail(unpreserve_atom_v).delay(std::chrono::minutes(10)).send(this, weak_ref); + } + + ); } GlobalImageCacheActor::GlobalImageCacheActor(caf::actor_config &cfg) @@ -58,8 +77,10 @@ GlobalImageCacheActor::GlobalImageCacheActor(caf::actor_config &cfg) auto prefs = GlobalStoreHelper(system()); JsonStore j; join_broadcast(this, prefs.get_group(j)); - max_count = preference_value(j, "/core/image_cache/max_count"); - max_size = preference_value(j, "/core/image_cache/max_size") * 1024 * 1024; + max_count = preference_value(j, "/core/image_cache/max_count"); + max_size = preference_value(j, "/core/image_cache/max_size") * 1024 * 1024; + reset_idle_ = std::chrono::minutes( + preference_value(j, "/core/image_cache/release_on_idle")); } catch (...) { } @@ -75,10 +96,23 @@ GlobalImageCacheActor::GlobalImageCacheActor(caf::actor_config &cfg) auto trim = spawn(); link_to(trim); + anon_mail(clear_atom_v, true).delay(std::chrono::minutes(1)).send(this, weak_ref); + + // For cache benchmarking + // cache_.noisy = true; + behavior_.assign( [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + [=](clear_atom, const bool idle_check) { + if (reset_idle_.count() and not cache_.empty() and + utility::clock::now() - last_activity_ > reset_idle_) { + anon_mail(clear_atom_v).send(this); + } + anon_mail(clear_atom_v, true).delay(std::chrono::minutes(1)).send(this, weak_ref); + }, [=](clear_atom) -> bool { cache_.clear(); + anon_mail(unpreserve_atom_v, static_cast(0)).send(trim); return true; }, @@ -103,11 +137,13 @@ GlobalImageCacheActor::GlobalImageCacheActor(caf::actor_config &cfg) const JsonStore & /*change*/, const std::string & /*path*/, const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full).delegate(actor_cast(this)); }, [=](json_store::update_atom, const JsonStore &js) { try { + reset_idle_ = std::chrono::minutes( + preference_value(js, "/core/image_cache/release_on_idle")); auto new_count = preference_value(js, "/core/image_cache/max_count"); auto new_size = preference_value(js, "/core/image_cache/max_size") * 1024 * 1024; @@ -126,14 +162,14 @@ GlobalImageCacheActor::GlobalImageCacheActor(caf::actor_config &cfg) if (not erased_keys_.empty() or not new_keys_.empty()) { // force purge of memory.. if (not erased_keys_.empty()) - anon_send(trim, unpreserve_atom_v, erased_keys_.size()); + anon_mail(unpreserve_atom_v, erased_keys_.size()).send(trim); - send( - event_group_, + mail( utility::event_atom_v, media_cache::keys_atom_v, media::MediaKeyVector(new_keys_.begin(), new_keys_.end()), - media::MediaKeyVector(erased_keys_.begin(), erased_keys_.end())); + media::MediaKeyVector(erased_keys_.begin(), erased_keys_.end())) + .send(event_group_); } new_keys_.clear(); @@ -161,8 +197,9 @@ GlobalImageCacheActor::GlobalImageCacheActor(caf::actor_config &cfg) const media::AVFrameIDsAndTimePoints &mpts, const Uuid &uuid) -> media::AVFrameIDsAndTimePoints { media::AVFrameIDsAndTimePoints result; + result.reserve(mpts.size()); for (const auto &p : mpts) { - if (!cache_.preserve(p.second->key_, p.first, uuid)) { + if (!cache_.preserve(p.second->key(), p.first, uuid)) { result.push_back(p); } } @@ -177,43 +214,59 @@ GlobalImageCacheActor::GlobalImageCacheActor(caf::actor_config &cfg) }, [=](retrieve_atom, const media::MediaKey &key) -> media_reader::ImageBufPtr { + last_activity_ = utility::clock::now(); return cache_.retrieve(key); }, [=](retrieve_atom, const media::AVFrameIDsAndTimePoints &mptr_and_timepoints) -> std::vector { - std::vector result(mptr_and_timepoints.size()); - auto r = result.begin(); + std::vector result; + result.reserve(mptr_and_timepoints.size()); + + last_activity_ = utility::clock::now(); + for (const auto &p : mptr_and_timepoints) { - *r = cache_.retrieve(p.second->key_, p.first); - (*r).when_to_display_ = p.first; - r++; + result.emplace_back(cache_.retrieve(p.second->key(), p.first)); + result.back().when_to_display_ = p.first; } return result; }, - [=](retrieve_atom, const media::MediaKey &key, const time_point &time) - -> media_reader::ImageBufPtr { return cache_.retrieve(key, time); }, + [=](retrieve_atom, + const media::MediaKey &key, + const time_point &time) -> media_reader::ImageBufPtr { + last_activity_ = utility::clock::now(); + return cache_.retrieve(key, time); + }, [=](retrieve_atom, const media::MediaKey &key, const time_point &time, const Uuid &uuid) - -> media_reader::ImageBufPtr { return cache_.retrieve(key, time, uuid); }, + -> media_reader::ImageBufPtr { + last_activity_ = utility::clock::now(); + return cache_.retrieve(key, time, uuid); + }, [=](size_atom) -> size_t { return cache_.size(); }, [=](store_atom, const media::MediaKey &key, media_reader::ImageBufPtr buf) -> bool { + last_activity_ = utility::clock::now(); return cache_.store(key, buf); }, [=](store_atom, const media::MediaKey &key, const media_reader::ImageBufPtr &buf, - const time_point &when) -> bool { return cache_.store(key, buf, when); }, + const time_point &when) -> bool { + last_activity_ = utility::clock::now(); + + return cache_.store(key, buf, when); + }, [=](store_atom, const media::MediaKey &key, const media_reader::ImageBufPtr &buf, const time_point &when, const utility::Uuid &uuid) -> bool { + last_activity_ = utility::clock::now(); return cache_.store(key, buf, when, false, uuid); }, @@ -222,6 +275,7 @@ GlobalImageCacheActor::GlobalImageCacheActor(caf::actor_config &cfg) const media_reader::ImageBufPtr &buf, const time_point &when, const utility::Uuid &uuid) -> bool { + last_activity_ = utility::clock::now(); return cache_.store(key, buf, when, false, uuid); }, [=](store_atom, @@ -230,6 +284,7 @@ GlobalImageCacheActor::GlobalImageCacheActor(caf::actor_config &cfg) const time_point &when, const utility::Uuid &uuid, const time_point &cache_out_date_tp) -> bool { + last_activity_ = utility::clock::now(); return cache_.store(key, buf, when, uuid, cache_out_date_tp); }, @@ -238,18 +293,18 @@ GlobalImageCacheActor::GlobalImageCacheActor(caf::actor_config &cfg) void GlobalImageCacheActor::update_changes( const media::MediaKeyVector &store, const media::MediaKeyVector &erase) { - for (const auto &i : store) { - new_keys_.insert(i); + + new_keys_.insert(store.begin(), store.end()); + for (const auto &i : store) erased_keys_.erase(i); - } - for (const auto &i : erase) { + erased_keys_.insert(erase.begin(), erase.end()); + for (const auto &i : erase) new_keys_.erase(i); - erased_keys_.insert(i); - } + if (not update_pending_ and (not new_keys_.empty() or not erased_keys_.empty())) { update_pending_ = true; - delayed_anon_send(this, std::chrono::milliseconds(250), keys_atom_v, true); + anon_mail(keys_atom_v, true).delay(std::chrono::milliseconds(250)).send(this, weak_ref); } } @@ -268,8 +323,10 @@ GlobalAudioCacheActor::GlobalAudioCacheActor(caf::actor_config &cfg) auto prefs = GlobalStoreHelper(system()); JsonStore j; join_broadcast(this, prefs.get_group(j)); - max_count = preference_value(j, "/core/audio_cache/max_count"); - max_size = preference_value(j, "/core/audio_cache/max_size") * 1024 * 1024; + max_count = preference_value(j, "/core/audio_cache/max_count"); + max_size = preference_value(j, "/core/audio_cache/max_size") * 1024 * 1024; + reset_idle_ = std::chrono::minutes( + preference_value(j, "/core/audio_cache/release_on_idle")); } catch (...) { } @@ -281,8 +338,19 @@ GlobalAudioCacheActor::GlobalAudioCacheActor(caf::actor_config &cfg) auto event_group_ = spawn(this); link_to(event_group_); + anon_mail(clear_atom_v, true).delay(std::chrono::minutes(1)).send(this, weak_ref); + behavior_.assign( [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + + [=](clear_atom, const bool idle_check) { + if (reset_idle_.count() and not cache_.empty() and + utility::clock::now() - last_activity_ > reset_idle_) { + anon_mail(clear_atom_v).send(this); + } + anon_mail(clear_atom_v, true).delay(std::chrono::minutes(1)).send(this, weak_ref); + }, + [=](clear_atom) -> bool { cache_.clear(); return true; @@ -306,11 +374,14 @@ GlobalAudioCacheActor::GlobalAudioCacheActor(caf::actor_config &cfg) const JsonStore & /*change*/, const std::string & /*path*/, const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full).delegate(actor_cast(this)); }, [=](json_store::update_atom, const JsonStore &js) { try { + reset_idle_ = std::chrono::minutes( + preference_value(js, "/core/audio_cache/release_on_idle")); + auto new_count = preference_value(js, "/core/audio_cache/max_count"); auto new_size = preference_value(js, "/core/audio_cache/max_size") * 1024 * 1024; @@ -327,12 +398,12 @@ GlobalAudioCacheActor::GlobalAudioCacheActor(caf::actor_config &cfg) [=](keys_atom, bool) { if (not erased_keys_.empty() or not new_keys_.empty()) - send( - event_group_, + mail( utility::event_atom_v, media_cache::keys_atom_v, media::MediaKeyVector(new_keys_.begin(), new_keys_.end()), - media::MediaKeyVector(erased_keys_.begin(), erased_keys_.end())); + media::MediaKeyVector(erased_keys_.begin(), erased_keys_.end())) + .send(event_group_); new_keys_.clear(); erased_keys_.clear(); @@ -352,8 +423,9 @@ GlobalAudioCacheActor::GlobalAudioCacheActor(caf::actor_config &cfg) const media::AVFrameIDsAndTimePoints &mpts, const Uuid &uuid) -> media::AVFrameIDsAndTimePoints { media::AVFrameIDsAndTimePoints result; + result.reserve(mpts.size()); for (const auto &p : mpts) { - if (!cache_.preserve(p.second->key_, p.first, uuid)) { + if (!cache_.preserve(p.second->key(), p.first, uuid)) { result.push_back(p); } } @@ -365,42 +437,59 @@ GlobalAudioCacheActor::GlobalAudioCacheActor(caf::actor_config &cfg) [=](retrieve_atom, const media::AVFrameIDsAndTimePoints &mptr_and_timepoints) -> std::vector { - std::vector result(mptr_and_timepoints.size()); - auto r = result.begin(); + std::vector result; + last_activity_ = utility::clock::now(); + + result.reserve(mptr_and_timepoints.size()); + for (const auto &p : mptr_and_timepoints) { - *r = cache_.retrieve(p.second->key_, p.first); - (*r).when_to_display_ = p.first; - r++; + result.emplace_back(cache_.retrieve(p.second->key(), p.first)); + result.back().when_to_display_ = p.first; } + return result; }, [=](retrieve_atom, const media::MediaKey &key) -> media_reader::AudioBufPtr { + last_activity_ = utility::clock::now(); return cache_.retrieve(key); }, - [=](retrieve_atom, const media::MediaKey &key, const time_point &time) - -> media_reader::AudioBufPtr { return cache_.retrieve(key, time); }, + [=](retrieve_atom, + const media::MediaKey &key, + const time_point &time) -> media_reader::AudioBufPtr { + last_activity_ = utility::clock::now(); + + return cache_.retrieve(key, time); + }, [=](retrieve_atom, const media::MediaKey &key, const time_point &time, const Uuid &uuid) - -> media_reader::AudioBufPtr { return cache_.retrieve(key, time, uuid); }, + -> media_reader::AudioBufPtr { + last_activity_ = utility::clock::now(); + return cache_.retrieve(key, time, uuid); + }, [=](size_atom) -> size_t { return cache_.size(); }, [=](store_atom, const media::MediaKey &key, media_reader::AudioBufPtr buf) -> bool { + last_activity_ = utility::clock::now(); return cache_.store(key, buf); }, [=](store_atom, const media::MediaKey &key, media_reader::AudioBufPtr buf, - const time_point &when) -> bool { return cache_.store(key, buf, when); }, + const time_point &when) -> bool { + last_activity_ = utility::clock::now(); + return cache_.store(key, buf, when); + }, [=](store_atom, const media::MediaKey &key, media_reader::AudioBufPtr buf, const time_point &when, const utility::Uuid &uuid) -> bool { + last_activity_ = utility::clock::now(); return cache_.store(key, buf, when, false, uuid); }, @@ -410,6 +499,7 @@ GlobalAudioCacheActor::GlobalAudioCacheActor(caf::actor_config &cfg) const time_point &when, const utility::Uuid &uuid, const time_point &cache_out_date_tp) -> bool { + last_activity_ = utility::clock::now(); return cache_.store(key, buf, when, uuid, cache_out_date_tp); }, @@ -421,17 +511,17 @@ void GlobalAudioCacheActor::on_exit() { system().registry().erase(audio_cache_re void GlobalAudioCacheActor::update_changes( const media::MediaKeyVector &store, const media::MediaKeyVector &erase) { - for (const auto &i : store) { - new_keys_.insert(i); + + new_keys_.insert(store.begin(), store.end()); + for (const auto &i : store) erased_keys_.erase(i); - } - for (const auto &i : erase) { + erased_keys_.insert(erase.begin(), erase.end()); + for (const auto &i : erase) new_keys_.erase(i); - erased_keys_.insert(i); - } + if (not update_pending_ and (not new_keys_.empty() or not erased_keys_.empty())) { update_pending_ = true; - delayed_anon_send(this, std::chrono::milliseconds(250), keys_atom_v, true); + anon_mail(keys_atom_v, true).delay(std::chrono::milliseconds(250)).send(this, weak_ref); } } diff --git a/src/media_cache/test/CMakeLists.txt b/src/media_cache/test/CMakeLists.txt index af7d52b88..2a82407ae 100644 --- a/src/media_cache/test/CMakeLists.txt +++ b/src/media_cache/test/CMakeLists.txt @@ -2,7 +2,7 @@ include(CTest) SET(LINK_DEPS xstudio::media_cache - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/media_hook/src/CMakeLists.txt b/src/media_hook/src/CMakeLists.txt index ca84febda..09d37311b 100644 --- a/src/media_hook/src/CMakeLists.txt +++ b/src/media_hook/src/CMakeLists.txt @@ -1,12 +1,14 @@ SET(LINK_DEPS xstudio::global_store - caf::core + CAF::core xstudio::media ) if(UNIX) - list(APPEND LINK_DEPS stdc++fs dl) + list(APPEND LINK_DEPS dl) + if (NOT APPLE) + list(APPEND LINK_DEPS stdc++fs) + endif() endif() -create_component(media_hook 0.1.0 "${LINK_DEPS}") - +create_component(media_hook ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") \ No newline at end of file diff --git a/src/media_hook/src/media_hook_actor.cpp b/src/media_hook/src/media_hook_actor.cpp index a5c44caa8..e9e3c123e 100644 --- a/src/media_hook/src/media_hook_actor.cpp +++ b/src/media_hook/src/media_hook_actor.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "xstudio/atoms.hpp" #include "xstudio/global_store/global_store.hpp" @@ -25,8 +26,6 @@ using namespace caf; MediaHookWorkerActor::MediaHookWorkerActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { - std::vector hooks; - // get hooks { auto pm = system().registry().template get(plugin_manager_registry); @@ -42,7 +41,7 @@ MediaHookWorkerActor::MediaHookWorkerActor(caf::actor_config &cfg) auto actor = request_receive( *sys, pm, plugin_manager::spawn_plugin_atom_v, i.uuid_); link_to(actor); - hooks.push_back(actor); + hooks_.push_back(actor); } } } @@ -57,10 +56,10 @@ MediaHookWorkerActor::MediaHookWorkerActor(caf::actor_config &cfg) const FrameRate &media_rate, const std::vector &source_refs, std::vector &source_names) -> result { - if (not hooks.empty()) { + if (not hooks_.empty()) { auto rp = make_response_promise(); fan_out_request( - hooks, + hooks_, infinite, media_hook::gather_media_sources_atom_v, media_actor, @@ -93,27 +92,81 @@ MediaHookWorkerActor::MediaHookWorkerActor(caf::actor_config &cfg) return UuidActorVector(); }, + [=](get_clip_hook_atom, const caf::actor &clip, int retry_count) -> result { + auto rp = make_response_promise(); + + if (hooks_.empty()) { + rp.deliver(true); + } else { + // we get the clip prop not the clip "metadata", though we might want both ? + + // get clip meta.. + mail(timeline::item_prop_atom_v) + .request(clip, infinite) + .then( + [=](const JsonStore &clip_meta) mutable { + // get media meta + mail(playlist::get_media_atom_v, get_json_atom_v, Uuid(), "") + .request(clip, infinite) + .then( + [=](const JsonStore &media_meta) mutable { + if (media_meta.is_null() and retry_count) { + // try again later.. + mail(get_clip_hook_atom_v, clip, retry_count - 1) + .delay(2s) + .request( + caf::actor_cast(this), infinite) + .then( + [=](const bool result) mutable { + rp.deliver(result); + }, + [=](error &err) mutable { + rp.deliver(std::move(err)); + }); + } else + do_clip_hook(rp, clip, clip_meta, media_meta); + }, + [=](const error &) mutable { + // error is valid if no media assiged + do_clip_hook(rp, clip, clip_meta, JsonStore()); + }); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + } + + return rp; + }, + + + // update clip metadat when media changes. + [=](get_clip_hook_atom, const caf::actor &clip) -> result { + auto rp = make_response_promise(); + rp.delegate(caf::actor_cast(this), get_clip_hook_atom_v, clip, 5); + return rp; + }, + [=](get_media_hook_atom, caf::actor media_source) -> result { auto rp = make_response_promise(); - if (hooks.empty()) { + if (hooks_.empty()) { rp.deliver(true); return rp; } - - request(media_source, infinite, json_store::get_json_atom_v, "") + mail(json_store::get_json_atom_v, "") + .request(media_source, infinite) .then( [=](const JsonStore &jsn) mutable { - request(media_source, infinite, media::media_reference_atom_v, Uuid()) + mail(media::media_reference_atom_v, Uuid()) + .request(media_source, infinite) .then( [=](const std::pair &ref) mutable { - // dispatch request to hooks.. + // dispatch request to hooks_.. // merge collected metadata. const auto &[uuid, mr] = ref; fan_out_request( - hooks, + hooks_, infinite, get_media_hook_atom_v, UuidActor(uuid, media_source), @@ -130,17 +183,15 @@ MediaHookWorkerActor::MediaHookWorkerActor(caf::actor_config &cfg) // push to source. if (not c.is_null()) { - anon_send( - media_source, - json_store::merge_json_atom_v, - c); + anon_mail(json_store::merge_json_atom_v, c) + .send(media_source); if (c.count("colour_pipeline")) { - anon_send( - media_source, + anon_mail( colour_pipeline:: set_colour_pipe_params_atom_v, utility::JsonStore( - c["colour_pipeline"])); + c["colour_pipeline"])) + .send(media_source); } } @@ -155,10 +206,65 @@ MediaHookWorkerActor::MediaHookWorkerActor(caf::actor_config &cfg) }, [=](error &err) mutable { rp.deliver(std::move(err)); }); + return rp; + }, + + [=](detect_display_atom, + const std::string &name, + const std::string &model, + const std::string &manufacturer, + const std::string &serialNumber, + const utility::JsonStore &jsn) -> result { + auto rp = make_response_promise(); + + if (not hooks_.empty()) { + fan_out_request( + hooks_, + infinite, + media_hook::detect_display_atom_v, + name, + model, + manufacturer, + serialNumber, + jsn) + .then( + [=](const std::string &display) mutable { rp.deliver(display); }, + [=](const error &err) mutable { rp.deliver(err); }); + return rp; + } + return rp; }); } +void MediaHookWorkerActor::do_clip_hook( + caf::typed_response_promise rp, + const caf::actor &clip_actor, + const utility::JsonStore &clip_meta, + const utility::JsonStore &media_meta) { + + fan_out_request( + hooks_, infinite, get_clip_hook_atom_v, clip_meta, media_meta) + .then( + [=](std::vector cr) mutable { + // merge Stores. + JsonStore new_clip_meta(clip_meta); + for (const auto &i : cr) { + if (not i.is_null()) + new_clip_meta.update(i); + } + + // push to source. + if (not new_clip_meta.is_null()) { + anon_mail(timeline::item_prop_atom_v, new_clip_meta, true).send(clip_actor); + } + + rp.deliver(true); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} + + GlobalMediaHookActor::GlobalMediaHookActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { size_t worker_count = 5; @@ -175,11 +281,17 @@ GlobalMediaHookActor::GlobalMediaHookActor(caf::actor_config &cfg) spdlog::debug("GlobalMediaHookActor worker_count {}", worker_count); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + auto pool = caf::actor_pool::make( - system().dummy_execution_unit(), + system(), worker_count, [&] { return system().spawn(); }, caf::actor_pool::round_robin()); + +#pragma GCC diagnostic pop + link_to(pool); system().registry().put(media_hook_registry, this); @@ -190,8 +302,8 @@ GlobalMediaHookActor::GlobalMediaHookActor(caf::actor_config &cfg) // [=](gather_media_sources_atom, const std::vector &existing_refs, // std::vector &source_names, caf::actor media_actor) // { - // delegate(pool, gather_media_sources_atom_v, existing_refs, source_names, - // media_actor); + // mail(gather_media_sources_atom_v, existing_refs, source_names, + // media_actor).delegate(pool); // }, [=](utility::serialise_atom) -> result { @@ -199,11 +311,10 @@ GlobalMediaHookActor::GlobalMediaHookActor(caf::actor_config &cfg) // this makes a dict of the media hook plugin(s) uuid vs versions // which lets us know if we need to re-reun media hook plugins auto pm = system().registry().template get(plugin_manager_registry); - request( - pm, - infinite, + mail( utility::detail_atom_v, plugin_manager::PluginType(plugin_manager::PluginFlags::PF_MEDIA_HOOK)) + .request(pm, infinite) .then( [=](const std::vector &details) mutable { utility::JsonStore result; @@ -224,11 +335,10 @@ GlobalMediaHookActor::GlobalMediaHookActor(caf::actor_config &cfg) // this makes a dict of the media hook plugin(s) uuid vs versions // which lets us know if we need to re-reun media hook plugins auto pm = system().registry().template get(plugin_manager_registry); - request( - pm, - infinite, + mail( utility::detail_atom_v, plugin_manager::PluginType(plugin_manager::PluginFlags::PF_MEDIA_HOOK)) + .request(pm, infinite) .then( [=](const std::vector &details) mutable { bool matched = true; @@ -254,11 +364,13 @@ GlobalMediaHookActor::GlobalMediaHookActor(caf::actor_config &cfg) const FrameRate &media_rate) -> result { // try to hide actor stuff.. auto rp = make_response_promise(); - request(media, infinite, media::media_reference_atom_v) + mail(media::media_reference_atom_v) + .request(media, infinite) .then( [=](const std::vector &existing_refs) mutable { // get media references - request(media, infinite, media::get_media_source_names_atom_v) + mail(media::get_media_source_names_atom_v) + .request(media, infinite) .then( [=](const std::vector> &source_names) mutable { @@ -279,14 +391,18 @@ GlobalMediaHookActor::GlobalMediaHookActor(caf::actor_config &cfg) }, [=](get_media_hook_atom _get_media_hook_atom, caf::actor media_source) { - delegate(pool, _get_media_hook_atom, media_source); + return mail(_get_media_hook_atom, media_source).delegate(pool); + }, + + [=](get_clip_hook_atom _atom, const caf::actor &clip_actor) { + return mail(_atom, clip_actor).delegate(pool); }, [=](json_store::update_atom, const JsonStore & /*change*/, const std::string & /*path*/, const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full).delegate(actor_cast(this)); }, [=](json_store::update_atom, const JsonStore &j) mutable { @@ -295,22 +411,21 @@ GlobalMediaHookActor::GlobalMediaHookActor(caf::actor_config &cfg) if (count > worker_count) { spdlog::debug("hook workers changed old {} new {}", worker_count, count); while (worker_count < count) { - anon_send( - pool, - sys_atom_v, - put_atom_v, - system().spawn()); + anon_mail( + sys_atom_v, put_atom_v, system().spawn()) + .send(pool); worker_count++; } } else if (count < worker_count) { spdlog::debug("hook workers changed old {} new {}", worker_count, count); // get actors.. worker_count = count; - request(pool, infinite, sys_atom_v, get_atom_v) + mail(sys_atom_v, get_atom_v) + .request(pool, infinite) .await( [=](const std::vector &ws) { for (auto i = worker_count; i < ws.size(); i++) { - anon_send(pool, sys_atom_v, delete_atom_v, ws[i]); + anon_mail(sys_atom_v, delete_atom_v, ws[i]).send(pool); } }, [=](const error &err) { @@ -320,6 +435,26 @@ GlobalMediaHookActor::GlobalMediaHookActor(caf::actor_config &cfg) } } catch (...) { } + }, + + [=](detect_display_atom, + const std::string &name, + const std::string &model, + const std::string &manufacturer, + const std::string &serialNumber, + const utility::JsonStore &jsn) -> result { + auto rp = make_response_promise(); + + rp.delegate( + pool, + media_hook::detect_display_atom_v, + name, + model, + manufacturer, + serialNumber, + jsn); + + return rp; }); } diff --git a/src/media_hook/test/CMakeLists.txt b/src/media_hook/test/CMakeLists.txt index 8b1303acd..f3b500918 100644 --- a/src/media_hook/test/CMakeLists.txt +++ b/src/media_hook/test/CMakeLists.txt @@ -3,7 +3,7 @@ include(CTest) SET(LINK_DEPS xstudio::media xstudio::media_hook - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/media_metadata/src/CMakeLists.txt b/src/media_metadata/src/CMakeLists.txt index a468a0a68..6ae082372 100644 --- a/src/media_metadata/src/CMakeLists.txt +++ b/src/media_metadata/src/CMakeLists.txt @@ -1,12 +1,15 @@ SET(LINK_DEPS xstudio::global_store - caf::core + CAF::core ) if(WIN32) #list(APPEND LINK_DEPS ghc_filesystem) # Link against the MSVSLX implementation for Windows elseif(UNIX) - list(APPEND LINK_DEPS stdc++fs dl) # Link against stdc++fs for Linux + list(APPEND LINK_DEPS dl) # Link against stdc++fs for Linux + if (NOT APPLE) + list(APPEND LINK_DEPS stdc++fs) # Link against stdc++fs for Linux + endif() endif() -create_component(media_metadata 0.1.0 "${LINK_DEPS}") +create_component(media_metadata ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/media_metadata/src/media_metadata_actor.cpp b/src/media_metadata/src/media_metadata_actor.cpp index 21a543a70..861a51981 100644 --- a/src/media_metadata/src/media_metadata_actor.cpp +++ b/src/media_metadata/src/media_metadata_actor.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "xstudio/atoms.hpp" #include "xstudio/global_store/global_store.hpp" @@ -152,11 +153,17 @@ GlobalMediaMetadataActor::GlobalMediaMetadataActor(caf::actor_config &cfg) } catch (...) { } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + auto pool = caf::actor_pool::make( - system().dummy_execution_unit(), + system(), worker_count, [&] { return system().spawn(); }, caf::actor_pool::round_robin()); + +#pragma GCC diagnostic pop + link_to(pool); system().registry().put(media_metadata_registry, this); @@ -164,32 +171,33 @@ GlobalMediaMetadataActor::GlobalMediaMetadataActor(caf::actor_config &cfg) behavior_.assign( [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, [=](get_metadata_atom _get_metadata_atom, const caf::uri &_uri) { - delegate(pool, _get_metadata_atom, _uri, std::numeric_limits::min()); + return mail(_get_metadata_atom, _uri, std::numeric_limits::min()) + .delegate(pool); }, [=](get_metadata_atom _get_metadata_atom, const caf::uri &_uri, const std::string &force_plugin) { - delegate( - pool, _get_metadata_atom, _uri, std::numeric_limits::min(), force_plugin); + return mail(_get_metadata_atom, _uri, std::numeric_limits::min(), force_plugin) + .delegate(pool); }, [=](get_metadata_atom _get_metadata_atom, const caf::uri &_uri, const int frame) { - delegate(pool, _get_metadata_atom, _uri, frame); + return mail(_get_metadata_atom, _uri, frame).delegate(pool); }, [=](get_metadata_atom _get_metadata_atom, const caf::uri &_uri, const int frame, const std::string &force_plugin) { - delegate(pool, _get_metadata_atom, _uri, frame, force_plugin); + return mail(_get_metadata_atom, _uri, frame, force_plugin).delegate(pool); }, [=](json_store::update_atom, const JsonStore & /*change*/, const std::string & /*path*/, const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full).delegate(actor_cast(this)); }, [=](json_store::update_atom, const JsonStore &j) mutable { @@ -203,11 +211,9 @@ GlobalMediaMetadataActor::GlobalMediaMetadataActor(caf::actor_config &cfg) worker_count, count); while (worker_count < count) { - anon_send( - pool, - sys_atom_v, - put_atom_v, - system().spawn()); + anon_mail( + sys_atom_v, put_atom_v, system().spawn()) + .send(pool); worker_count++; } } else if (count < worker_count) { @@ -217,11 +223,12 @@ GlobalMediaMetadataActor::GlobalMediaMetadataActor(caf::actor_config &cfg) worker_count, count); worker_count = count; - request(pool, infinite, sys_atom_v, get_atom_v) + mail(sys_atom_v, get_atom_v) + .request(pool, infinite) .await( [=](const std::vector &ws) { for (auto i = worker_count; i < ws.size(); i++) { - anon_send(pool, sys_atom_v, delete_atom_v, ws[i]); + anon_mail(sys_atom_v, delete_atom_v, ws[i]).send(pool); } }, [=](const error &err) { diff --git a/src/media_metadata/test/CMakeLists.txt b/src/media_metadata/test/CMakeLists.txt index 3246ee7dc..c25beebc3 100644 --- a/src/media_metadata/test/CMakeLists.txt +++ b/src/media_metadata/test/CMakeLists.txt @@ -3,7 +3,8 @@ include(CTest) SET(LINK_DEPS xstudio::media_metadata xstudio::global - caf::core + fmt::fmt + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/media_metadata/test/media_metadata_actor_test.cpp b/src/media_metadata/test/media_metadata_actor_test.cpp index 28676fd02..68ca76ae3 100644 --- a/src/media_metadata/test/media_metadata_actor_test.cpp +++ b/src/media_metadata/test/media_metadata_actor_test.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include #include +#include #include "xstudio/atoms.hpp" #include "xstudio/global/global_actor.hpp" diff --git a/src/media_reader/src/CMakeLists.txt b/src/media_reader/src/CMakeLists.txt index acd6c4501..57e1cf31d 100644 --- a/src/media_reader/src/CMakeLists.txt +++ b/src/media_reader/src/CMakeLists.txt @@ -2,15 +2,17 @@ SET(LINK_DEPS xstudio::media xstudio::media_cache xstudio::global_store - xstudio::broadcast - caf::core + xstudio::utility + CAF::core ) if(WIN32) #list(APPEND LINK_DEPS ghc_filesystem) # Link against the MSVSLX implementation for Windows -elseif(UNIX AND NOT APPLE) - list(APPEND LINK_DEPS stdc++fs dl) # Link against stdc++fs for Linux +elseif(UNIX) + list(APPEND LINK_DEPS dl) # Link against stdc++fs for Linux + if (NOT APPLE) + list(APPEND LINK_DEPS stdc++fs) # Link against stdc++fs for Linux + endif() endif() -create_component(media_reader 0.1.0 "${LINK_DEPS}") - +create_component(media_reader ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") \ No newline at end of file diff --git a/src/media_reader/src/cacheing_media_reader_actor.cpp b/src/media_reader/src/cacheing_media_reader_actor.cpp index 36c84a5de..d06788277 100644 --- a/src/media_reader/src/cacheing_media_reader_actor.cpp +++ b/src/media_reader/src/cacheing_media_reader_actor.cpp @@ -1,4 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 +#include + #include "xstudio/media_reader/cacheing_media_reader_actor.hpp" #include "xstudio/global_store/global_store.hpp" #include "xstudio/atoms.hpp" @@ -6,6 +8,7 @@ #include "xstudio/utility/json_store.hpp" #include "xstudio/utility/time_cache.hpp" #include "xstudio/thumbnail/thumbnail.hpp" +#include "xstudio/ui/opengl/shader_program_base.hpp" using namespace xstudio::media_reader; using namespace xstudio::media; @@ -13,6 +16,71 @@ using namespace xstudio::json_store; using namespace xstudio::utility; using namespace xstudio::global_store; +namespace { + +static Uuid blankshader_uuid{"d6c8722b-dc2a-42f9-981d-a2485c6ceea1"}; + +static std::string blankshader{R"( +#version 330 core +uniform int blank_width; +uniform int dummy; + +// forward declaration +uvec4 get_image_data_4bytes(int byte_address); + +vec4 fetch_rgba_pixel(ivec2 image_coord) +{ + int bytes_per_pixel = 4; + int pixel_bytes_offset_in_texture_memory = (image_coord.x + image_coord.y*blank_width)*bytes_per_pixel; + uvec4 c = get_image_data_4bytes(pixel_bytes_offset_in_texture_memory); + return vec4(float(c.x)/255.0f,float(c.y)/255.0f,float(c.z)/255.0f,1.0f); +} +)"}; + +static xstudio::ui::viewport::GPUShaderPtr + blank_shader(new xstudio::ui::opengl::OpenGLShader(blankshader_uuid, blankshader)); + +ImageBufPtr make_blank_image() { + + ImageBufPtr buf; + int width = 192; + int height = 108; + size_t size = width * height; + int bytes_per_channel = 1; + + // we are totally free to choose the pixel layout, but we need to unpack + // in the shader. RGBA 4 bytes matches underlying texture format, so most + // simple option. + int bytes_per_pixel = 4 * bytes_per_channel; + + JsonStore jsn; + jsn["blank_width"] = width; + + buf.reset(new ImageBuffer(blankshader_uuid, jsn)); + buf->allocate(size * bytes_per_pixel); + buf->set_shader(blank_shader); + buf->set_image_dimensions(Imath::V2i(width, height)); + + std::array c = {16, 16, 16}; + int i = 0; + uint8_t *b = (uint8_t *)buf->buffer(); + while (i < size) { + if (((i / 16) & 1) == (i / (192 * 16) & 1)) { + b[0] = c[0]; + b[1] = c[1]; + b[2] = c[2]; + } else { + b[0] = 0; + b[1] = 0; + b[2] = 0; + } + b += 4; // buf is implicitly rgba + ++i; + } + return buf; +} + +} // namespace CachingMediaReaderActor::CachingMediaReaderActor( caf::actor_config &cfg, @@ -51,9 +119,17 @@ CachingMediaReaderActor::CachingMediaReaderActor( if (not audio_cache_) audio_cache_ = system().registry().template get(audio_cache_registry); + // a re-useable blank image to store against failed image reads + blank_image_ = make_blank_image(); + behavior_.assign( [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + [=](get_image_atom, const ImageBufPtr &blank) { + // not a get_image request, but setting the blank_image + blank_image_ = blank; + }, + [=](get_image_atom) { // get the next pending urgent image request if (!urgent_worker_busy_ && pending_get_image_requests_.size()) { @@ -64,52 +140,53 @@ CachingMediaReaderActor::CachingMediaReaderActor( [=](get_image_atom, const media::AVFrameID &mptr, bool pin, - const utility::Uuid &playhead_uuid) -> result { + const utility::Uuid &playhead_uuid, + const timebase::flicks playhead_position) -> result { auto rp = make_response_promise(); // first, check if the image we want is cached - request(image_cache_, infinite, media_cache::retrieve_atom_v, mptr.key_) + mail(media_cache::retrieve_atom_v, mptr.key()) + .request(image_cache_, infinite) .then( [=](media_reader::ImageBufPtr buf) mutable { if (buf) { rp.deliver(buf); } else { - request(urgent_worker_, infinite, get_image_atom_v, mptr) + mail(get_image_atom_v, mptr) + .request(urgent_worker_, infinite) .then( [=](media_reader::ImageBufPtr buf) mutable { rp.deliver(buf); // store the image in our cache - anon_send( - image_cache_, + anon_mail( media_cache::store_atom_v, - mptr.key_, + mptr.key(), buf, utility::clock::now() + (pin ? std::chrono::minutes(10) : std::chrono::minutes(0)), - playhead_uuid); + playhead_uuid) + .urgent() + .send(image_cache_); }, [=](const caf::error &err) mutable { // make an empty image buffer that holds the error // message - std::stringstream err_msg; - std::string caf_error_string = to_string(err); - // strip the caf error formatting - if (caf_error_string.find("error(\"") != - std::string::npos) { - caf_error_string = std::string(caf_error_string, 7); - // strip off the ") at the end too - caf_error_string = std::string( - caf_error_string, - 0, - caf_error_string.length() - 2); - } - - err_msg << "Error loading file \"" - << to_string(mptr.uri_) - << "\": " << caf_error_string; - - media_reader::ImageBufPtr buf( - new media_reader::ImageBuffer(err_msg.str())); + media_reader::ImageBufPtr buf = + make_error_buffer(err, mptr); + + // store the failed image in our cache so we don't + // keep trying to load it + anon_mail( + media_cache::store_atom_v, + mptr.key(), + buf, + utility::clock::now() + + (pin ? std::chrono::minutes(10) + : std::chrono::minutes(0)), + playhead_uuid) + .urgent() + .send(image_cache_); + rp.deliver(buf); }); } @@ -124,26 +201,28 @@ CachingMediaReaderActor::CachingMediaReaderActor( const utility::Uuid playhead_uuid) -> result { auto rp = make_response_promise(); // first, check if the image we want is cached - request(audio_cache_, infinite, media_cache::retrieve_atom_v, mptr.key_) + mail(media_cache::retrieve_atom_v, mptr.key()) + .request(audio_cache_, infinite) .then( [=](media_reader::AudioBufPtr buf) mutable { if (buf) { rp.deliver(buf); } else { - request(urgent_worker_, infinite, get_audio_atom_v, mptr) + mail(get_audio_atom_v, mptr) + .request(urgent_worker_, infinite) .then( [=](media_reader::AudioBufPtr buf) mutable { rp.deliver(buf); // store the image in our cache - anon_send( - audio_cache_, + anon_mail( media_cache::store_atom_v, - mptr.key_, + mptr.key(), buf, utility::clock::now() + (pin ? std::chrono::minutes(10) : std::chrono::minutes(0)), - playhead_uuid); + playhead_uuid) + .send(audio_cache_); }, [=](const caf::error &) mutable { // deliver an empty buffer @@ -157,20 +236,22 @@ CachingMediaReaderActor::CachingMediaReaderActor( [=](get_image_atom, const media::AVFrameID &mptr, - caf::actor playhead, - const utility::Uuid playhead_uuid, - const time_point &tp) { - receive_image_buffer_request(mptr, playhead, playhead_uuid, tp); + const utility::Uuid playhead_uuid) -> result { + return receive_image_buffer_request(mptr, playhead_uuid); }, [=](read_precache_image_atom, const media::AVFrameID &mptr) -> result { // note the caller (GlobalMediaReaderActor) handles the cacheing // of this image buffer auto rp = make_response_promise(); - request(precache_worker_, infinite, get_image_atom_v, mptr) + mail(get_image_atom_v, mptr) + .request(precache_worker_, infinite) .then( [=](media_reader::ImageBufPtr buf) mutable { rp.deliver(buf); }, - [=](const caf::error &err) mutable { rp.deliver(err); }); + [=](const caf::error &err) mutable { + rp.deliver(make_error_buffer(err, mptr)); + }); + return rp; }, @@ -178,7 +259,8 @@ CachingMediaReaderActor::CachingMediaReaderActor( // note the caller (GlobalMediaReaderActor) handles the cacheing // of this image buffer auto rp = make_response_promise(); - request(precache_worker_, infinite, get_audio_atom_v, mptr) + mail(get_audio_atom_v, mptr) + .request(precache_worker_, infinite) .then( [=](media_reader::AudioBufPtr buf) mutable { rp.deliver(buf); }, [=](const caf::error &err) mutable { rp.deliver(err); }); @@ -186,7 +268,7 @@ CachingMediaReaderActor::CachingMediaReaderActor( }, [=](get_media_detail_atom atom, const caf::uri &_uri) { - delegate(urgent_worker_, atom, _uri); + return mail(atom, _uri).delegate(urgent_worker_); } ); @@ -196,78 +278,101 @@ void CachingMediaReaderActor::do_urgent_get_image() { auto p = pending_get_image_requests_.begin(); const media::AVFrameID mptr = p->second.mptr_; - caf::actor playhead = p->second.playhead_; - auto tp = p->second.time_point_; + auto rp = p->second.response_promise_; auto playhead_uuid = p->first; pending_get_image_requests_.erase(p); urgent_worker_busy_ = true; - request(urgent_worker_, infinite, get_image_atom_v, mptr) + mail(get_image_atom_v, mptr) + .request(urgent_worker_, infinite) .then( [=](media_reader::ImageBufPtr buf) mutable { // send the image back to the playhead that requested it - send(playhead, push_image_atom_v, buf, mptr, tp); + rp.deliver(buf); // store the image in our cache - anon_send( - image_cache_, + anon_mail( media_cache::store_atom_v, - mptr.key_, + mptr.key(), buf, utility::clock::now(), - playhead_uuid); + playhead_uuid) + .urgent() + .send(image_cache_); // perhaps more urgent requests are now pending urgent_worker_busy_ = false; - anon_send(this, get_image_atom_v); + anon_mail(get_image_atom_v).send(this); }, [=](const caf::error &err) mutable { - std::stringstream err_msg; - std::string caf_error_string = to_string(err); - // strip the caf error formatting - if (caf_error_string.find("error(\"") != std::string::npos) { - caf_error_string = std::string(caf_error_string, 7); - // strip off the ") at the end too - caf_error_string = - std::string(caf_error_string, 0, caf_error_string.length() - 2); - } - - err_msg << "Error loading file \"" << to_string(mptr.uri_) - << "\": " << caf_error_string; - // make an empty image buffer that holds the error message - media_reader::ImageBufPtr buf(new media_reader::ImageBuffer(err_msg.str())); + media_reader::ImageBufPtr buf = make_error_buffer(err, mptr); + rp.deliver(buf); - // send the image back to the playhead that requested it - send(playhead, push_image_atom_v, buf, mptr, tp); + // store the failed image in our cache so we don't + // keep trying to load it + anon_mail( + media_cache::store_atom_v, + mptr.key(), + buf, + utility::clock::now(), + playhead_uuid) + .urgent() + .send(image_cache_); urgent_worker_busy_ = false; - anon_send(this, get_image_atom_v); + anon_mail(get_image_atom_v).send(this); }); } -void CachingMediaReaderActor::receive_image_buffer_request( - const media::AVFrameID &mptr, - caf::actor playhead, - const utility::Uuid playhead_uuid, - const time_point &tp) { +caf::typed_response_promise CachingMediaReaderActor::receive_image_buffer_request( + const media::AVFrameID &mptr, const utility::Uuid playhead_uuid) { + auto rt = make_response_promise(); // first, check if the image we want is cached - request(image_cache_, infinite, media_cache::retrieve_atom_v, mptr.key_) + mail(media_cache::retrieve_atom_v, mptr.key()) + .request(image_cache_, infinite) .then( [=](media_reader::ImageBufPtr buf) mutable { if (buf) { - std::string path = uri_to_posix_path(mptr.uri_); // send the image back to the playhead that requested it - send(playhead, push_image_atom_v, buf, mptr, tp); + rt.deliver(buf); } else { // image is not cached. Update the request to load the image - pending_get_image_requests_[playhead_uuid] = - ImmediateImageReqest(mptr, playhead, tp); - send(this, get_image_atom_v); + pending_get_image_requests_[playhead_uuid] = ImmediateImageReqest(mptr, rt); + mail(get_image_atom_v).send(this); } }, [=](const caf::error &err) mutable { - spdlog::warn("Failed cache retrieve buffer {} {}", mptr.key_, to_string(err)); + rt.deliver(err); + spdlog::warn("Failed cache retrieve buffer {} {}", mptr.key(), to_string(err)); }); + return rt; +} + +ImageBufPtr CachingMediaReaderActor::make_error_buffer( + const caf::error &err, const media::AVFrameID &mptr) { + + + // make an empty image buffer that holds the error + // message + std::stringstream err_msg; + std::string caf_error_string = to_string(err); + // strip the caf error formatting + if (caf_error_string.find("error(\"") != std::string::npos) { + caf_error_string = std::string(caf_error_string, 7); + // strip off the ") at the end too + caf_error_string = std::string(caf_error_string, 0, caf_error_string.length() - 2); + } + + ImageBufPtr err_buf = blank_image_; + err_buf.set_frame_id(mptr); + err_buf.set_error_details(caf_error_string); + return err_buf; + + /*err_msg << "Error loading file \"" + << to_string(mptr.uri()) + << "\": " << caf_error_string; + + return media_reader::ImageBufPtr(new media_reader::ImageBuffer(err_msg.str()));*/ } \ No newline at end of file diff --git a/src/media_reader/src/frame_request_queue.cpp b/src/media_reader/src/frame_request_queue.cpp index d187e4300..941495505 100644 --- a/src/media_reader/src/frame_request_queue.cpp +++ b/src/media_reader/src/frame_request_queue.cpp @@ -16,8 +16,8 @@ void FrameRequestQueue::add_frame_request( bool matches_existing_request = false; for (auto pp = queue_.rbegin(); pp != queue_.rend(); ++pp) { - if ((*pp)->requested_frame_->frame_ == frame_info.frame_ && - (*pp)->requested_frame_->uri_ == frame_info.uri_) { + if ((*pp)->requested_frame_->frame() == frame_info.frame() && + (*pp)->requested_frame_->uri() == frame_info.uri()) { if ((*pp)->required_by_ > required_by) { (*pp)->required_by_ = required_by; } @@ -47,9 +47,9 @@ void FrameRequestQueue::add_frame_requests( for (const auto &p : frames_info) { const std::shared_ptr &frame_info = (p.second); - const utility::time_point &required_by = p.first; + const utility::time_point &when_we_want_it = p.first; queue_.emplace_back( - new FrameRequest(frame_info, required_by, requesting_playhead_uuid)); + new FrameRequest(frame_info, when_we_want_it, requesting_playhead_uuid)); } std::sort( diff --git a/src/media_reader/src/media_detail_and_thumbnail_reader_actor.cpp b/src/media_reader/src/media_detail_and_thumbnail_reader_actor.cpp index 8963a2f6c..fd7bcf376 100644 --- a/src/media_reader/src/media_detail_and_thumbnail_reader_actor.cpp +++ b/src/media_reader/src/media_detail_and_thumbnail_reader_actor.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 #include #include -#include +#include +#include #include "xstudio/atoms.hpp" #include "xstudio/global_store/global_store.hpp" @@ -65,16 +66,11 @@ MediaDetailAndThumbnailReaderActor::MediaDetailAndThumbnailReaderActor( [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, [=](get_media_detail_atom) { - // here we process a thumbnail request once for every 4 'get_media_detail' requests - // - this is to balance thumbnail delivery to the UI (which is nice but not - // essential) against building media sources (which is essential) - if (num_detail_requests_since_thumbnail_request_ > 4 && - !thumbnail_request_queue_.empty()) { - process_get_thumbnail_queue(); - } else if (not media_detail_request_queue_.empty()) { + // Continue processing requests in the queue in a loop (loop + // happens as we ping ourselves another get_media_detail_atom when + // we've completed a request in the queue) + if (not media_detail_request_queue_.empty()) { process_get_media_detail_queue(); - } else { - process_get_thumbnail_queue(); } }, @@ -107,49 +103,35 @@ MediaDetailAndThumbnailReaderActor::MediaDetailAndThumbnailReaderActor( auto rp = make_response_promise(); media_detail_request_queue_.emplace(_uri, key, rp); if (start) - anon_send( - caf::actor_cast(this), - get_media_detail_atom_v); // starts loop to chew through the request queue + anon_mail(get_media_detail_atom_v) + .send(caf::actor_cast( + this)); // starts loop to chew through the request queue return rp; }, [=](get_thumbnail_atom, const media::AVFrameID &mptr, const size_t size) -> result { - bool start = queues_empty(); - auto rp = make_response_promise(); - thumbnail_request_queue_.emplace(mptr, size, rp); - if (start) - anon_send( - caf::actor_cast(this), - get_media_detail_atom_v); // starts loop to chew through the request queue + auto rp = make_response_promise(); + get_thumbnail(rp, mptr, size); return rp; }, [=](utility::uuid_atom) -> Uuid { return uuid_; }); } -void MediaDetailAndThumbnailReaderActor::process_get_thumbnail_queue() { - - if (!thumbnail_request_queue_.size()) - return; - - num_detail_requests_since_thumbnail_request_ = 0; - - const auto thumbnail_request = thumbnail_request_queue_.front(); - thumbnail_request_queue_.pop(); - - media::AVFrameID mptr = thumbnail_request.media_pointer_; - const size_t size = thumbnail_request.size_; - caf::typed_response_promise rp = thumbnail_request.rp_; +void MediaDetailAndThumbnailReaderActor::get_thumbnail( + caf::typed_response_promise rp, + const media::AVFrameID &mptr, + const size_t size) { try { fan_out_request( plugins_, infinite, media_reader::supported_atom_v, - mptr.uri_, - utility::get_signature(mptr.uri_)) + mptr.uri(), + utility::get_signature(mptr.uri())) .then( [=](std::vector> supt) mutable { // find the best media reader plugin to read this file ... @@ -163,56 +145,58 @@ void MediaDetailAndThumbnailReaderActor::process_get_thumbnail_queue() { } if (best_match == MRC_NO) { - spdlog::warn("{} Unsupported format.", __PRETTY_FUNCTION__); rp.deliver(make_error(media_error::unsupported, "Unsupported format")); - continue_processing_queue(); } else { get_thumbnail_from_reader_plugin( - plugins_map_[best_reader_plugin_uuid], mptr, size, rp); + plugins_map_[best_reader_plugin_uuid], rp, mptr, size); } }, [=](const caf::error &err) mutable { spdlog::warn("{} {}", err.category(), to_string(err)); rp.deliver(err); - continue_processing_queue(); }); } catch (std::exception &e) { rp.deliver(make_error(media_error::unsupported, e.what())); - // spdlog::info("{} {}", __PRETTY_FUNCTION__, e.what()); - continue_processing_queue(); } } void MediaDetailAndThumbnailReaderActor::get_thumbnail_from_reader_plugin( caf::actor &reader_plugin, - const media::AVFrameID mptr, - const size_t size, - caf::typed_response_promise rp) { + caf::typed_response_promise rp, + const media::AVFrameID &mptr, + const size_t size) { auto colour_pipe_manager = system().registry().get(colour_pipeline_registry); - request(reader_plugin, infinite, get_thumbnail_atom_v, mptr, size) + mail(get_thumbnail_atom_v, mptr, size) + .request(reader_plugin, infinite) .then( [=](const thumbnail::ThumbnailBufferPtr &buf) mutable { if (buf && buf->format() == thumbnail::THUMBNAIL_FORMAT::TF_RGB24) rp.deliver(buf); else if (buf) { - // send to colour pipeline.. - rp.delegate(colour_pipe_manager, process_thumbnail_atom_v, mptr, buf); + // send to colour pipeline.. (requires RGB64 format, i.e. floating pt) + // Colour pipeline will convert float images to display space. + mail(process_thumbnail_atom_v, mptr, buf) + .request(colour_pipe_manager, infinite) + .then( + [=](const thumbnail::ThumbnailBufferPtr &buf) mutable { + rp.deliver(buf); + }, + [=](const caf::error &err) mutable { rp.deliver(err); }); } else { - if (mptr.actor_addr_) { - auto dest = caf::actor_cast(mptr.actor_addr_); + if (mptr.media_source_addr()) { + auto dest = caf::actor_cast(mptr.media_source_addr()); if (dest) - anon_send(dest, media_status_atom_v, MediaStatus::MS_UNSUPPORTED); + anon_mail(media_status_atom_v, MediaStatus::MS_UNSUPPORTED) + .send(dest); } rp.deliver(make_error( media_error::corrupt, "thumbnail loaded returned empty buffer.")); } - continue_processing_queue(); }, [=](const caf::error &err) mutable { spdlog::error("{} {}", err.category(), to_string(err)); rp.deliver(err); - continue_processing_queue(); }); } @@ -221,7 +205,6 @@ void MediaDetailAndThumbnailReaderActor::process_get_media_detail_queue() { if (media_detail_request_queue_.empty()) return; - num_detail_requests_since_thumbnail_request_++; const auto media_detail_request = media_detail_request_queue_.front(); media_detail_request_queue_.pop(); @@ -253,11 +236,8 @@ void MediaDetailAndThumbnailReaderActor::process_get_media_detail_queue() { rp.deliver(make_error(media_error::unsupported, "Unsupported format")); continue_processing_queue(); } else { - request( - plugins_map_[best_reader_plugin_uuid], - infinite, - get_media_detail_atom_v, - _uri) + mail(get_media_detail_atom_v, _uri) + .request(plugins_map_[best_reader_plugin_uuid], infinite) .then( [=](const MediaDetail &md) mutable { media_detail_cache_age_[_uri] = utility::clock::now(); @@ -267,18 +247,22 @@ void MediaDetailAndThumbnailReaderActor::process_get_media_detail_queue() { }, [=](const caf::error &err) mutable { rp.deliver(err); + // spdlog::warn("HERE {} {}", to_string(_uri), + // to_string(err)); send_error_to_source(key, err); continue_processing_queue(); }); } }, [=](const caf::error &err) mutable { + // spdlog::warn("HERE 2 {} {}", to_string(_uri), to_string(err)); rp.deliver(err); send_error_to_source(key, err); continue_processing_queue(); }); } catch (std::exception &e) { auto err = make_error(xstudio_error::error, e.what()); + // spdlog::warn("HERE 3 {} {}", to_string(_uri), to_string(err)); send_error_to_source(key, err); continue_processing_queue(); rp.deliver(err); @@ -294,16 +278,16 @@ void MediaDetailAndThumbnailReaderActor::send_error_to_source( from_integer(err.code(), me); switch (me) { case media_error::corrupt: - anon_send(dest, media_status_atom_v, MediaStatus::MS_CORRUPT); + anon_mail(media_status_atom_v, MediaStatus::MS_CORRUPT).send(dest); break; case media_error::unsupported: - anon_send(dest, media_status_atom_v, MediaStatus::MS_UNSUPPORTED); + anon_mail(media_status_atom_v, MediaStatus::MS_UNSUPPORTED).send(dest); break; case media_error::unreadable: - anon_send(dest, media_status_atom_v, MediaStatus::MS_UNREADABLE); + anon_mail(media_status_atom_v, MediaStatus::MS_UNREADABLE).send(dest); break; case media_error::missing: - anon_send(dest, media_status_atom_v, MediaStatus::MS_MISSING); + anon_mail(media_status_atom_v, MediaStatus::MS_MISSING).send(dest); break; } } @@ -313,6 +297,6 @@ void MediaDetailAndThumbnailReaderActor::send_error_to_source( void MediaDetailAndThumbnailReaderActor::continue_processing_queue() { if (!queues_empty()) { - anon_send(caf::actor_cast(this), get_media_detail_atom_v); + anon_mail(get_media_detail_atom_v).send(caf::actor_cast(this)); } } diff --git a/src/media_reader/src/media_reader.cpp b/src/media_reader/src/media_reader.cpp index dba3d4db1..bc78f554e 100644 --- a/src/media_reader/src/media_reader.cpp +++ b/src/media_reader/src/media_reader.cpp @@ -9,6 +9,7 @@ #include #include "xstudio/media_reader/media_reader.hpp" +#include "xstudio/media_reader/image_buffer_set.hpp" #include "xstudio/thumbnail/thumbnail.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" @@ -45,78 +46,78 @@ namespace fs = std::filesystem; * reader down while it waits for the cache to re-cycle image buffers, we are * going to do it at a lower level within the ImageBuffer allocate method */ -class ImageBufferRecyclerCache { - public: - void store_unwanted_buffer(Buffer::BufferDataPtr &buf, const size_t size) { - - // uncomment this to turn the whole thing off - // return; - - mutex_.lock(); - while ((total_size_ + size) > max_size_) { - - // simply delete oldest buffers - if user is playing through a timeline - // of mixed formats it's likely that the cache is deleting frames - // of a different size to the one that the readers are currently asking - // for. However, it's going to be quite complex to address that - // problem and we have to fallback on the OS memory managment coping - // with rapid allocation/deallocation ok. The general case is that - // the xStudio session is loaded with common format sources, though. - auto p = size_by_time_.begin(); - if (p == size_by_time_.end()) - break; - const size_t s = p->second; - if (!recycle_buffer_bin_[s].empty()) { - recycle_buffer_bin_[s].pop_back(); - if (recycle_buffer_bin_[s].empty()) { - recycle_buffer_bin_.erase(recycle_buffer_bin_.find(s)); - } - total_size_ -= s; +void ImageBufferRecyclerCache::store_unwanted_buffer( + Buffer::BufferDataPtr &buf, const size_t size) { + + // uncomment this to turn the whole thing off + // return; + mutex_.lock(); + while ((total_size_ + size) > max_size_) { + + // simply delete oldest buffers - if user is playing through a timeline + // of mixed formats it's likely that the cache is deleting frames + // of a different size to the one that the readers are currently asking + // for. However, it's going to be quite complex to address that + // problem and we have to fallback on the OS memory managment coping + // with rapid allocation/deallocation ok. The general case is that + // the xStudio session is loaded with common format sources, though. + auto p = size_by_time_.begin(); + if (p == size_by_time_.end()) + break; + const size_t s = p->second; + if (!recycle_buffer_bin_[s].empty()) { + recycle_buffer_bin_[s].pop_back(); + if (recycle_buffer_bin_[s].empty()) { + recycle_buffer_bin_.erase(recycle_buffer_bin_.find(s)); } - size_by_time_.erase(p); + total_size_ -= s; } - auto tp = utility::clock::now(); - recycle_buffer_bin_[size].push_back(buf); - total_size_ += size; - size_by_time_[tp] = size; - mutex_.unlock(); + size_by_time_.erase(p); } + auto tp = utility::clock::now(); + recycle_buffer_bin_[size].push_back(buf); + total_size_ += size; + size_by_time_[tp] = size; + mutex_.unlock(); +} + +Buffer::BufferDataPtr +ImageBufferRecyclerCache::fetch_recycled_buffer(const size_t required_size) { + + Buffer::BufferDataPtr r; + mutex_.lock(); + auto p = recycle_buffer_bin_.find(required_size); + if (p != recycle_buffer_bin_.end() && !p->second.empty()) { - Buffer::BufferDataPtr fetch_recycled_buffer(const size_t required_size) { - - Buffer::BufferDataPtr r; - mutex_.lock(); - auto p = recycle_buffer_bin_.find(required_size); - if (p != recycle_buffer_bin_.end() && !p->second.empty()) { - r = p->second.back(); - p->second.pop_back(); - if (p->second.empty()) - recycle_buffer_bin_.erase(p); - total_size_ -= required_size; + auto q = size_by_time_.begin(); + while (q != size_by_time_.end()) { + if (q->second == required_size) { + size_by_time_.erase(q); + break; + } + q++; } - mutex_.unlock(); - return r; + r = p->second.back(); + p->second.pop_back(); + if (p->second.empty()) + recycle_buffer_bin_.erase(p); + total_size_ -= required_size; } + mutex_.unlock(); - size_t max_size_ = { - 512 * 1024 * - 1024}; // 0.5GB - probably doesn't need to be that big, and need to set this with a pref - size_t total_size_ = {0}; - typedef std::vector Buffers; - std::map recycle_buffer_bin_; - std::map size_by_time_; - std::mutex mutex_; -}; + return r; +} -static ImageBufferRecyclerCache s_buffer_recycler_; +std::shared_ptr Buffer::s_buf_cache = + std::make_shared(); -Buffer::~Buffer() { s_buffer_recycler_.store_unwanted_buffer(buffer_, size_); } +Buffer::~Buffer() { s_buf_cache->store_unwanted_buffer(buffer_, size_); } xstudio::media_reader::byte *Buffer::allocate(const size_t size) { if (size_ != size) { - buffer_ = s_buffer_recycler_.fetch_recycled_buffer(size); + buffer_ = s_buf_cache->fetch_recycled_buffer(size); if (!buffer_) { buffer_.reset(new BufferData(size)); } @@ -131,7 +132,7 @@ void Buffer::resize(const size_t size) { allocate(size); if (old_buffer) { memcpy(buffer(), old_buffer->data_.get(), std::min(old_size, size_)); - s_buffer_recycler_.store_unwanted_buffer(old_buffer, old_size); + s_buf_cache->store_unwanted_buffer(old_buffer, old_size); } } @@ -154,6 +155,48 @@ xstudio::media_reader::byte *ImageBuffer::allocate(const size_t _size) { return Buffer::allocate(padded_size); } +utility::JsonStore ImageBufPtr::metadata() const { + // the idea here is we add in a few useful metadata fields ontop of the + // metadata that is carried by the underlying pointer (the ImageBuffer). + // These fields have context that is outside the raw media that the fra,e + // came from (e.g. timecode, pixel aspect which can be overridden or are + // coming from the timeline that the media is being played from) + utility::JsonStore result = get() ? get()->metadata() : utility::JsonStore(); + + result["uri"] = to_string(frame_id().uri()); + result["frame"] = frame_id().frame(); + result["pixel aspect"] = frame_id().pixel_aspect(); + result["frame rate"] = fmt::format("{:.3f}", frame_id().rate().to_fps()); + result["timecode"] = to_string(frame_id().timecode()); + result["timecode frame"] = int(frame_id().timecode()); + result["error"] = frame_id().error(); + return result; +} + + +void ImageBufDisplaySet::finalise() { + + utility::JsonStore image_info = nlohmann::json::parse("[]"); + images_hash_ = 0; + for (int i = 0; i < num_onscreen_images(); ++i) { + const auto &im = onscreen_image(i); + nlohmann::json r; + if (im) { + r["image_size_in_pixels"] = im->image_size_in_pixels(); + // r["image_pixels_bounding_box"] = im->image_pixels_bounding_box(); + r["pixel_aspect"] = im.frame_id().pixel_aspect(); + images_hash_ += im.frame_id().key().hash(); + } + image_info.push_back(r); + } + + as_json_.clear(); + as_json_["image_info"] = image_info; + as_json_["hero_image_index"] = hero_sub_playhead_index_; + as_json_["prev_hero_image_index"] = previous_hero_sub_playhead_index_; + hash_ = int64_t(std::hash{}(as_json_.dump())); +} + MediaReader::MediaReader(std::string name, const utility::JsonStore &) : name_(std::move(name)) {} @@ -165,7 +208,7 @@ AudioBufPtr MediaReader::audio(const media::AVFrameID &) { return AudioBufPtr(); thumbnail::ThumbnailBufferPtr MediaReader::thumbnail(const media::AVFrameID &mp, const size_t) { throw std::runtime_error( - "Thumbnail generation not supported for this format. " + mp.reader_); + "Thumbnail generation not supported for this format. " + mp.reader()); return thumbnail::ThumbnailBufferPtr(); } diff --git a/src/media_reader/src/media_reader_actor.cpp b/src/media_reader/src/media_reader_actor.cpp index 6ad7c1d93..b288488ec 100644 --- a/src/media_reader/src/media_reader_actor.cpp +++ b/src/media_reader/src/media_reader_actor.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 #include #include -#include +#include +#include #include "xstudio/atoms.hpp" #include "xstudio/global_store/global_store.hpp" @@ -177,8 +178,11 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( } } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + pool_ = caf::actor_pool::make( - system().dummy_execution_unit(), + system(), 5, [&] { return system().spawn(plugins_, plugins_map_); }, caf::actor_pool::round_robin()); @@ -186,17 +190,28 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( system().registry().put(media_reader_registry, this); - delayed_anon_send(this, std::chrono::seconds(max_source_age_), retire_readers_atom_v); + anon_mail(retire_readers_atom_v).delay(std::chrono::seconds(max_source_age_)).send(this); image_cache_ = system().registry().template get(image_cache_registry); audio_cache_ = system().registry().template get(audio_cache_registry); - auto media_detail_and_thumbnail_reader_pool = caf::actor_pool::make( - system().dummy_execution_unit(), + auto media_detail_reader_pool = caf::actor_pool::make( + system(), 4, // hardcoding to 4 media detail fetchers [&] { return system().spawn(); }, caf::actor_pool::round_robin()); - link_to(media_detail_and_thumbnail_reader_pool); + link_to(media_detail_reader_pool); + + auto thumbnail_reader_pool = caf::actor_pool::make( + system(), + 1, // hardcoding to 1 thumbnail reader. The reader doesn't actually do + // the work, it just delegates to the global media reader but via a + // queue of requests + [&] { return system().spawn(); }, + caf::actor_pool::round_robin()); + link_to(thumbnail_reader_pool); + +#pragma GCC diagnostic pop behavior_.assign( [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, @@ -208,12 +223,10 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( // this marks all cache entries for this playhead as 'stale' by // moving their timestamps to 1 hour in the past - hence they // will be dropped if the cache fills up - anon_send(image_cache_, media_cache::unpreserve_atom_v, playhead_uuid); + anon_mail(media_cache::unpreserve_atom_v, playhead_uuid).send(image_cache_); - /*anon_send( - audio_cache_, - media_cache::unpreserve_atom_v, - playhead_uuid);*/ + /*anon_mail(media_cache::unpreserve_atom_v, + playhead_uuid).send(audio_cache_);*/ return true; }, @@ -227,47 +240,42 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( // moving their timestamps to 1 hour in the past - hence they // will be dropped if the cache fills up - anon_send(image_cache_, media_cache::unpreserve_atom_v, playhead_uuid); - - /*anon_send( - audio_cache_, - media_cache::unpreserve_atom_v, - playhead_uuid);*/ + anon_mail(media_cache::unpreserve_atom_v, playhead_uuid).send(image_cache_); } return true; }, - [=](const group_down_msg &) {}, - [=](retire_readers_atom, const media::AVFrameID &mptr) -> bool { - return prune_reader(reader_key(mptr.uri_, mptr.actor_addr_)); + return prune_reader(reader_key(mptr.uri(), mptr.media_source_addr())); }, [=](get_image_atom, const media::AVFrameID &mptr, const bool pin, // stamp the frame 10 minutes in the future so it sticks in the cache - const utility::Uuid &playhead_uuid) -> result { + const utility::Uuid &playhead_uuid, + const timebase::flicks plahead_position) -> result { auto rp = make_response_promise(); - request(image_cache_, infinite, media_cache::retrieve_atom_v, mptr.key_) + mail(media_cache::retrieve_atom_v, mptr.key()) + .request(image_cache_, infinite) .then( [=](media_reader::ImageBufPtr buf) mutable { if (buf) { rp.deliver(buf); } else { // check for existing reader. - auto reader = - check_cached_reader(reader_key(mptr.uri_, mptr.actor_addr_)); + auto reader = check_cached_reader( + reader_key(mptr.uri(), mptr.media_source_addr())); if (reader) { // was using await, not sure why, but I've changed it to then - request( - *reader, - infinite, + mail( get_image_atom_v, mptr, pin, - playhead_uuid) + playhead_uuid, + plahead_position) + .request(*reader, infinite) .then( [=](media_reader::ImageBufPtr buf) mutable { rp.deliver(buf); @@ -281,23 +289,25 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( }); } else { // request new reader instance. - request( - pool_, infinite, get_reader_atom_v, mptr.uri_, mptr.reader_) + auto tp = utility::clock::now(); + mail(get_reader_atom_v, mptr.uri(), mptr.reader()) + .request(pool_, infinite) .then( [=](caf::actor &new_reader) mutable { new_reader = add_reader( new_reader, - reader_key(mptr.uri_, mptr.actor_addr_)); + reader_key( + mptr.uri(), mptr.media_source_addr())); // was using await, not sure why, but I've changed // it to then - request( - new_reader, - infinite, + mail( get_image_atom_v, mptr, pin, - playhead_uuid) + playhead_uuid, + plahead_position) + .request(new_reader, infinite) .then( [=](media_reader::ImageBufPtr buf) mutable { rp.deliver(buf); @@ -312,7 +322,7 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( }); }, [=](const caf::error &err) mutable { - send_error_to_source(mptr.actor_addr_, err); + send_error_to_source(mptr.media_source_addr(), err); media_reader::ImageBufPtr buf( new media_reader::ImageBuffer(to_string(err))); @@ -337,7 +347,8 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( // made via another channel, we don't bother asking readers for // these images we just get the cache to return whatever it has (and // blanks if the images aren't cached) - delegate(image_cache_, media_cache::retrieve_atom_v, mptr_and_timepoints); + return mail(media_cache::retrieve_atom_v, mptr_and_timepoints) + .delegate(image_cache_); }, [=](get_audio_atom, @@ -347,7 +358,8 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( // similar to images,during playback we need to fetch several audio frames // ahead of time due to soundcard latency. We assume that the 'precache' // mechanism has already decoded and stored those frames. - delegate(audio_cache_, media_cache::retrieve_atom_v, mptr_and_timepoints); + return mail(media_cache::retrieve_atom_v, mptr_and_timepoints) + .delegate(audio_cache_); }, [=](get_image_atom, @@ -355,72 +367,121 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( caf::actor playhead, const utility::Uuid playhead_uuid, const utility::time_point &tp, - const int /*logical_frame*/ - ) { - request(image_cache_, infinite, media_cache::retrieve_atom_v, mptr.key_) + const timebase::flicks playhead_position) { + mail(media_cache::retrieve_atom_v, mptr.key()) + .request(image_cache_, infinite) .then( [=](const media_reader::ImageBufPtr &buf) mutable { if (buf) { - send(playhead, push_image_atom_v, buf, mptr, tp); + mail(push_image_atom_v, buf, mptr, tp, playhead_position) + .send(playhead); } else { - auto reader = - check_cached_reader(reader_key(mptr.uri_, mptr.actor_addr_)); + auto reader = check_cached_reader( + reader_key(mptr.uri(), mptr.media_source_addr())); if (reader) { - anon_send( - *reader, - get_image_atom_v, - mptr, - playhead, - playhead_uuid, - tp); + mail(get_image_atom_v, mptr, playhead_uuid) + .request(*reader, infinite) + .then( + [=](const media_reader::ImageBufPtr &buf) mutable { + mail( + push_image_atom_v, + buf, + mptr, + tp, + playhead_position) + .send(playhead); + }, + [=](caf::error &err) {}); } else { + + // This prevents a load of requests for new readers to pile up + // on pool_ when the same image is requested multiple times + auto request_details = + std::make_shared(); + auto rkey = reader_key(mptr.uri(), mptr.media_source_addr()); + request_details->mptr = mptr; + request_details->playhead = playhead; + request_details->playhead_uuid = playhead_uuid; + request_details->tp = tp; + request_details->playhead_position = playhead_position; + immediate_frame_requests_[rkey].push_back(request_details); + + if (immediate_frame_requests_[rkey].size() > 1) + return; + // get reader.. - request( - pool_, infinite, get_reader_atom_v, mptr.uri_, mptr.reader_) + auto tp2 = utility::clock::now(); + mail(get_reader_atom_v, mptr.uri(), mptr.reader()) + .request(pool_, infinite) .then( [=](caf::actor &new_reader) mutable { - new_reader = add_reader( - new_reader, - reader_key(mptr.uri_, mptr.actor_addr_)); - anon_send( - new_reader, - get_image_atom_v, - mptr, - playhead, - playhead_uuid, - tp); + new_reader = add_reader(new_reader, rkey); + + auto p = immediate_frame_requests_.find(rkey); + if (p == immediate_frame_requests_.end()) + return; + auto rr = p->second; + immediate_frame_requests_.erase(p); + + for (size_t i = 0; i < rr.size(); ++i) { + + std::shared_ptr r = + rr[i]; + + mail( + get_image_atom_v, r->mptr, r->playhead_uuid) + .request(new_reader, infinite) + .then( + [=](const media_reader::ImageBufPtr + &buf) mutable { + mail( + push_image_atom_v, + buf, + r->mptr, + r->tp, + r->playhead_position) + .send(r->playhead); + }, + [=](caf::error &err) {}); + } }, [=](const caf::error &err) mutable { - send_error_to_source(mptr.actor_addr_, err); + send_error_to_source(mptr.media_source_addr(), err); media_reader::ImageBufPtr buf( new media_reader::ImageBuffer(to_string(err))); - send(playhead, push_image_atom_v, buf, mptr, tp); + mail( + push_image_atom_v, + buf, + mptr, + tp, + playhead_position) + .send(playhead); }); } } }, [=](const caf::error &err) mutable { spdlog::warn( - "Failed cache retrieve buffer {} {}", mptr.key_, to_string(err)); + "Failed cache retrieve buffer {} {}", mptr.key(), to_string(err)); }); }, [=](get_media_detail_atom _get_media_detail_atom, const caf::uri &_uri, const caf::actor_addr &key) { - delegate(media_detail_and_thumbnail_reader_pool, _get_media_detail_atom, _uri, key); + return mail(_get_media_detail_atom, _uri, key).delegate(media_detail_reader_pool); }, [=](get_thumbnail_atom atom, const media::AVFrameID &mptr, const size_t size) { - delegate(media_detail_and_thumbnail_reader_pool, atom, mptr, size); + return mail(atom, mptr, size).delegate(thumbnail_reader_pool); }, [=](json_store::update_atom, const JsonStore & /*change*/, const std::string & /*path*/, const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full).delegate(actor_cast(this)); }, [&](json_store::update_atom, const JsonStore &json) { @@ -442,56 +503,64 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( [=](playback_precache_atom, const media::AVFrameIDsAndTimePoints media_ptrs, - const Uuid &playhead_uuid) -> result { + const Uuid &playhead_uuid, + const media::MediaType mt) -> result { // we've received fresh lookahead read requests from the playhead // during playback. We want to ask the cache actors if they already // have those frames, and if not we need to queue read requests to // start reading/decoding those frames + auto rp = make_response_promise(); + if (mt == MT_IMAGE) { + mail(media_cache::preserve_atom_v, media_ptrs, playhead_uuid) + .request(image_cache_, std::chrono::seconds(1)) + .await( + [=](const media::AVFrameIDsAndTimePoints + media_ptrs_not_in_image_cache) mutable { + if (media_ptrs_not_in_image_cache.size()) { + + // clear all pending requests + playback_precache_request_queue_.clear_pending_requests( + playhead_uuid); + background_precache_request_queue_.clear_pending_requests( + playhead_uuid); + playback_precache_request_queue_.add_frame_requests( + media_ptrs_not_in_image_cache, playhead_uuid); - auto rp = make_response_promise(); - request( - image_cache_, - std::chrono::seconds(1), - media_cache::preserve_atom_v, - media_ptrs, - playhead_uuid) - .await( - [=](const media::AVFrameIDsAndTimePoints - media_ptrs_not_in_image_cache) mutable { - request( - audio_cache_, - std::chrono::seconds(1), - media_cache::preserve_atom_v, - media_ptrs, - playhead_uuid) - .await( - [=](const media::AVFrameIDsAndTimePoints - &media_ptrs_not_in_audio_cache) mutable { - if (media_ptrs_not_in_image_cache.size() || - media_ptrs_not_in_audio_cache.size()) { - - // clear all pending requests - playback_precache_request_queue_.clear_pending_requests( - playhead_uuid); - background_precache_request_queue_ - .clear_pending_requests(playhead_uuid); - - playback_precache_request_queue_.add_frame_requests( - media_ptrs_not_in_image_cache, playhead_uuid); - playback_precache_request_queue_.add_frame_requests( - media_ptrs_not_in_audio_cache, playhead_uuid); - - if (media_ptrs.size()) - background_cached_ref_timepoint_[playhead_uuid] = - media_ptrs.front().first; - continue_precacheing(); - } - rp.deliver(true); - }, - [=](const caf::error &err) mutable { rp.deliver(err); }); - }, - [=](const caf::error &err) mutable { rp.deliver(err); }); + if (media_ptrs.size()) + background_cached_ref_timepoint_[playhead_uuid] = + media_ptrs.front().first; + continue_precacheing(); + } + rp.deliver(true); + }, + [=](const caf::error &err) mutable { rp.deliver(err); }); + } else { + mail(media_cache::preserve_atom_v, media_ptrs, playhead_uuid) + .request(audio_cache_, std::chrono::seconds(1)) + .await( + [=](const media::AVFrameIDsAndTimePoints + &media_ptrs_not_in_audio_cache) mutable { + if (media_ptrs_not_in_audio_cache.size()) { + + // clear all pending requests + playback_precache_request_queue_.clear_pending_requests( + playhead_uuid); + background_precache_request_queue_.clear_pending_requests( + playhead_uuid); + + playback_precache_request_queue_.add_frame_requests( + media_ptrs_not_in_audio_cache, playhead_uuid); + + if (media_ptrs.size()) + background_cached_ref_timepoint_[playhead_uuid] = + media_ptrs.front().first; + continue_precacheing(); + } + rp.deliver(true); + }, + [=](const caf::error &err) mutable { rp.deliver(err); }); + } return rp; }, @@ -501,12 +570,15 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( auto rp = make_response_promise(); auto tt = utility::clock::now(); - // we've been told to start background cacheing, so assume playback - // read-ahead can be cancelled. Keep a note of the timepoint of the - // first frame in the request queue - anything with an older time - // in the cache can be discarded when the cache is full + // we've been told to start background cacheing - this should only happen + // when playback has halted and the user isn't actively scrubbing the + // playhead, so we assume playback read-ahead can be cancelled. + // We keep a note of the timepoint of the first frame in the request + // queue - anything with an older time in the cache can be discarded + // when the cache is full if (mptrs.size()) { - request(image_cache_, infinite, media_cache::unpreserve_atom_v, playhead_uuid) + mail(media_cache::unpreserve_atom_v, playhead_uuid) + .request(image_cache_, infinite) .then( [=](bool) mutable { background_precache_request_queue_.clear_pending_requests( @@ -530,8 +602,9 @@ GlobalMediaReaderActor::GlobalMediaReaderActor( [=](retire_readers_atom) { prune_readers(); - delayed_anon_send( - this, std::chrono::seconds(max_source_age_), retire_readers_atom_v); + anon_mail(retire_readers_atom_v) + .delay(std::chrono::seconds(max_source_age_)) + .send(this); }, [=](do_precache_work_atom) { do_precache(); }, @@ -575,25 +648,21 @@ caf::actor GlobalMediaReaderActor::get_reader( if (cached_reader) return *cached_reader; - caf::actor reader; - - try { - scoped_actor sys{system()}; - // spdlog::stopwatch sw; - reader = request_receive(*sys, pool_, get_reader_atom_v, _uri, hint); - if (reader) - reader = add_reader(reader, key); - - // spdlog::warn("get_reader {} {:.3f}", to_string(_uri), sw); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return reader; + return caf::actor(); } std::string GlobalMediaReaderActor::reader_key(const caf::uri &_uri, const caf::actor_addr &_key) const { + + // if _uri is for a 'blank' frame, we don't want to use the media source + // actor address as a key to cache the reader. The reason is that we might + // get blank frames for a media source that is still building itself and + // later we get non-blank frames from it. In other words, if uri is for + // a blank frame, the uri for the given _key may change later. + static const auto blank_uri = *caf::make_uri("xstudio://blank/?colour=gray"); + if (_uri == blank_uri) + return to_string(_uri); + if (_key) { return to_string(_key); } @@ -708,16 +777,11 @@ void GlobalMediaReaderActor::do_precache() { // which would otherwise block this crucial actor caf::actor cache_actor = - mptr->media_type_ == media::MediaType::MT_IMAGE ? image_cache_ : audio_cache_; + mptr->media_type() == media::MediaType::MT_IMAGE ? image_cache_ : audio_cache_; mark_playhead_waiting_for_precache_result(playhead_uuid); - request( - cache_actor, - std::chrono::milliseconds(500), - media_cache::preserve_atom_v, - mptr->key_, - predicted_time, - playhead_uuid) + mail(media_cache::preserve_atom_v, mptr->key(), predicted_time, playhead_uuid) + .request(cache_actor, std::chrono::milliseconds(500)) .then( [=](const bool exists) mutable { @@ -725,15 +789,40 @@ void GlobalMediaReaderActor::do_precache() { // already have in the cache, but might still have work to do mark_playhead_received_precache_result(playhead_uuid); // if (is_background_cache) { - // keep_cache_hot(mptr.key_, predicted_time, playhead_uuid); + // keep_cache_hot(mptr.key(), predicted_time, playhead_uuid); // } continue_precacheing(); } else { try { - auto reader = get_reader(mptr->uri_, mptr->actor_addr_, mptr->reader_); + auto reader = + get_reader(mptr->uri(), mptr->media_source_addr(), mptr->reader()); if (not reader) { - mark_playhead_received_precache_result(playhead_uuid); - continue_precacheing(); + // we need a new reader + mail(get_reader_atom_v, mptr->uri(), mptr->reader()) + .request(pool_, infinite) + .then( + [=](caf::actor new_reader) mutable { + auto key = + reader_key(mptr->uri(), mptr->media_source_addr()); + new_reader = add_reader(new_reader, key); + if (cache_actor == image_cache_) { + read_and_cache_image( + new_reader, + *fr, + cache_out_of_date_threshold, + is_background_cache); + } else { + read_and_cache_audio( + new_reader, + *fr, + cache_out_of_date_threshold, + is_background_cache); + } + }, + [=](caf::error &err) { + mark_playhead_received_precache_result(playhead_uuid); + continue_precacheing(); + }); } else { if (cache_actor == image_cache_) { read_and_cache_image( @@ -767,7 +856,7 @@ void GlobalMediaReaderActor::do_precache() { }, [=](const caf::error &err) { mark_playhead_received_precache_result(playhead_uuid); - spdlog::warn("Failed preserve buffer {} {}", mptr->key_, to_string(err)); + spdlog::warn("Failed preserve buffer {} {}", mptr->key(), to_string(err)); }); } @@ -777,10 +866,8 @@ void GlobalMediaReaderActor::keep_cache_hot( const utility::Uuid &playhead_uuid) { background_cached_frames_[playhead_uuid].push_back(std::make_pair(new_entry, tp)); - /*anon_send( - image_cache_, - media_cache::preserve_atom_v, - background_cached_frames_[playhead_uuid]);*/ + /*anon_mail(media_cache::preserve_atom_v, + background_cached_frames_[playhead_uuid]).send(image_cache_);*/ } void GlobalMediaReaderActor::read_and_cache_image( @@ -793,21 +880,21 @@ void GlobalMediaReaderActor::read_and_cache_image( const time_point predicted_time = fr.required_by_; const utility::Uuid playhead_uuid = fr.requesting_playhead_uuid_; - request(reader, std::chrono::seconds(60), read_precache_image_atom_v, *mptr) + mail(read_precache_image_atom_v, *mptr) + .request(reader, std::chrono::seconds(60)) .then( [=](media_reader::ImageBufPtr buf) mutable { // store the image in our cache. We use a different store message // if background cacheing if (is_background_cache) { - request( - image_cache_, - std::chrono::milliseconds(500), + mail( media_cache::store_atom_v, - mptr->key_, + mptr->key(), buf, predicted_time, playhead_uuid, cache_out_of_date_threshold) + .request(image_cache_, std::chrono::milliseconds(500)) .then( [=](const bool stored) { mark_playhead_received_precache_result(playhead_uuid); @@ -826,14 +913,13 @@ void GlobalMediaReaderActor::read_and_cache_image( spdlog::warn("Cache store error {}", to_string(err)); }); } else { - request( - image_cache_, - std::chrono::milliseconds(500), + mail( media_cache::store_atom_v, - mptr->key_, + mptr->key(), buf, predicted_time, playhead_uuid) + .request(image_cache_, std::chrono::milliseconds(500)) .then( [=](const bool stored) { if (!stored) { @@ -856,7 +942,7 @@ void GlobalMediaReaderActor::read_and_cache_image( }, [=](const caf::error &err) mutable { mark_playhead_received_precache_result(playhead_uuid); - send_error_to_source(mptr->actor_addr_, err); + send_error_to_source(mptr->media_source_addr(), err); // we might still have more work to do so keep going continue_precacheing(); }); @@ -872,19 +958,19 @@ void GlobalMediaReaderActor::read_and_cache_audio( const time_point predicted_time = fr.required_by_; const utility::Uuid playhead_uuid = fr.requesting_playhead_uuid_; - request(reader, std::chrono::seconds(60), read_precache_audio_atom_v, *mptr) + mail(read_precache_audio_atom_v, *mptr) + .request(reader, std::chrono::seconds(60)) .then( [=](media_reader::AudioBufPtr buf) mutable { // store the image in our cache - request( - audio_cache_, - std::chrono::milliseconds(500), + mail( media_cache::store_atom_v, - mptr->key_, + mptr->key(), buf, predicted_time, playhead_uuid, cache_out_of_date_threshold) + .request(audio_cache_, std::chrono::milliseconds(500)) .then( [=](const bool stored) { mark_playhead_received_precache_result(playhead_uuid); @@ -896,7 +982,8 @@ void GlobalMediaReaderActor::read_and_cache_audio( } else { continue_precacheing(); if (is_background_cache) { - // keep_cache_hot(mptr.key_, predicted_time, playhead_uuid); + // keep_cache_hot(mptr.key(), predicted_time, + // playhead_uuid); } } }, @@ -907,7 +994,7 @@ void GlobalMediaReaderActor::read_and_cache_audio( }, [=](const caf::error &err) mutable { mark_playhead_received_precache_result(playhead_uuid); - send_error_to_source(mptr->actor_addr_, err); + send_error_to_source(mptr->media_source_addr(), err); // we might still have more work to do so keep going continue_precacheing(); }); @@ -922,7 +1009,7 @@ void GlobalMediaReaderActor::continue_precacheing() { // coming into this actor can get blocked. This is why a slight delay is // employed here - to ensure new incoming messages can get handled and not // left in the queue - anon_send(caf::actor_cast(this), do_precache_work_atom_v); + anon_mail(do_precache_work_atom_v).send(caf::actor_cast(this)); } void GlobalMediaReaderActor::mark_playhead_waiting_for_precache_result( @@ -958,16 +1045,16 @@ void GlobalMediaReaderActor::send_error_to_source( from_integer(err.code(), me); switch (me) { case media_error::corrupt: - anon_send(dest, media_status_atom_v, MediaStatus::MS_CORRUPT); + anon_mail(media_status_atom_v, MediaStatus::MS_CORRUPT).send(dest); break; case media_error::unsupported: - anon_send(dest, media_status_atom_v, MediaStatus::MS_UNSUPPORTED); + anon_mail(media_status_atom_v, MediaStatus::MS_UNSUPPORTED).send(dest); break; case media_error::unreadable: - anon_send(dest, media_status_atom_v, MediaStatus::MS_UNREADABLE); + anon_mail(media_status_atom_v, MediaStatus::MS_UNREADABLE).send(dest); break; case media_error::missing: - anon_send(dest, media_status_atom_v, MediaStatus::MS_MISSING); + anon_mail(media_status_atom_v, MediaStatus::MS_MISSING).send(dest); break; } } diff --git a/src/media_reader/test/CMakeLists.txt b/src/media_reader/test/CMakeLists.txt index b75916883..1f3cf0fc9 100644 --- a/src/media_reader/test/CMakeLists.txt +++ b/src/media_reader/test/CMakeLists.txt @@ -4,7 +4,7 @@ SET(LINK_DEPS xstudio::playhead xstudio::media_reader xstudio::colour_pipeline - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/media_reader/test/media_reader_actor_test.cpp b/src/media_reader/test/media_reader_actor_test.cpp index 5679dcc00..861e86428 100644 --- a/src/media_reader/test/media_reader_actor_test.cpp +++ b/src/media_reader/test/media_reader_actor_test.cpp @@ -118,7 +118,7 @@ media::AVFrameID(badpath)).receive( // media::AVFrameID mptr(path); // for (int i = 1; i < 2000; ++i) { -// mptr.frame_ = i; +// mptr.frame() = i; // f.self->request(tmp, std::chrono::seconds(10), get_image_atom_v, mptr).receive( // [&](ImageBufPtrbuf) { // EXPECT_TRUE(buf); diff --git a/src/module/src/CMakeLists.txt b/src/module/src/CMakeLists.txt index f683259f5..092016d9a 100644 --- a/src/module/src/CMakeLists.txt +++ b/src/module/src/CMakeLists.txt @@ -3,7 +3,7 @@ SET(LINK_DEPS xstudio::utility xstudio::global_store xstudio::ui::base - caf::core + CAF::core ) -create_component(module 0.1.0 "${LINK_DEPS}") +create_component(module ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/module/src/attribute.cpp b/src/module/src/attribute.cpp index 3c53a38fe..bae827f70 100644 --- a/src/module/src/attribute.cpp +++ b/src/module/src/attribute.cpp @@ -18,7 +18,7 @@ Attribute::Attribute( role_data_[Title].set(title); role_data_[AbbrTitle].set(abbr_title); role_data_[UuidRole].set(utility::Uuid::generate()); - role_data_[Groups].set(std::vector()); + role_data_[UIDataModels].set(std::vector()); } void Attribute::set_owner(Module *owner) { owner_ = owner; } @@ -40,6 +40,13 @@ nlohmann::json Attribute::role_data_as_json(const int role) const { return p->second.to_json(); } +void Attribute::update_role_data_from_json( + const int role, const nlohmann::json &data, const bool notify) { + + if (role != UuidRole) + set_role_data(role, data, notify); +} + void Attribute::notify_change( const int role, const nlohmann::json &data, const bool self_notify) { if (owner_) { @@ -73,7 +80,7 @@ void Attribute::update_from_json(const nlohmann::json &data, const bool notify) bool Attribute::belongs_to_groups(const std::vector &group_names) const { bool rt = false; - const auto attr_group_names = get_role_data>(Groups); + const auto attr_group_names = get_role_data>(UIDataModels); for (const auto &p : attr_group_names) { if (std::find(group_names.begin(), group_names.end(), p) != group_names.end()) { rt = true; @@ -116,23 +123,24 @@ void Attribute::set_preference_path(const std::string &preference_path) { void Attribute::expose_in_ui_attrs_group(const std::string &group_name, bool expose) { if (expose) { - if (!has_role_data(Groups)) { - set_role_data(Groups, std::vector({"group_name"})); + if (!has_role_data(UIDataModels)) { + set_role_data(UIDataModels, std::vector({group_name})); return; } - auto n = role_data_[Groups].get>(); + auto n = role_data_[UIDataModels].get>(); for (const auto &g : n) { if (g == group_name) return; } n.push_back(group_name); - set_role_data(Groups, n); - } else if (has_role_data(Groups)) { - auto n = role_data_[Groups].get>(); + set_role_data(UIDataModels, n); + + } else if (has_role_data(UIDataModels)) { + auto n = role_data_[UIDataModels].get>(); for (auto p = n.begin(); p != n.end(); ++p) { if (*p == group_name) { n.erase(p); - set_role_data(Groups, n); + set_role_data(UIDataModels, n); return; } } diff --git a/src/module/src/global_module_events_actor.cpp b/src/module/src/global_module_events_actor.cpp deleted file mode 100644 index ff829418e..000000000 --- a/src/module/src/global_module_events_actor.cpp +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include "xstudio/atoms.hpp" -#include "xstudio/utility/logging.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/module/global_module_events_actor.hpp" -#include "xstudio/broadcast/broadcast_actor.hpp" - -#include - -using namespace xstudio::utility; -using namespace xstudio::module; -using namespace xstudio; - -GlobalModuleAttrEventsActor::GlobalModuleAttrEventsActor(caf::actor_config &cfg) - : caf::event_based_actor(cfg) { - - print_on_create(this, "GlobalModuleAttrEventsActor"); - print_on_exit(this, "GlobalModuleAttrEventsActor"); - - system().registry().put(module_events_registry, this); - module_backend_events_group_ = spawn(this); - module_ui_events_group_ = spawn(this); - playhead_group_ = spawn(this); - - link_to(module_backend_events_group_); - link_to(module_ui_events_group_); - link_to(playhead_group_); - - behavior_.assign( - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](module_ui_events_group_atom) -> caf::actor { return module_ui_events_group_; }, - [=](utility::get_group_atom) -> caf::actor { return module_backend_events_group_; }, - [=](redraw_viewport_group_atom) -> caf::actor { return playhead_group_; }, - [=](full_attributes_description_atom, - const AttributeSet &attrs, - const utility::Uuid &requester_uuid) { - // An attribute owner in the backend is sending a full description of it's module - send( - module_backend_events_group_, - full_attributes_description_atom_v, - attrs, - requester_uuid); - }, - - [=](full_attributes_description_atom, const AttributeSet &attrs) { - // An attribute owner in the backend is sending a full description of it's module - send(module_backend_events_group_, full_attributes_description_atom_v, attrs); - }, - - [=](join_module_attr_events_atom, caf::actor events_group) { - join_broadcast(this, events_group); - }, - - [=](leave_module_attr_events_atom, caf::actor events_group) { - leave_broadcast(this, events_group); - }, - - [=](remove_attrs_from_ui_atom, const std::vector &attr_uuids) { - send(module_backend_events_group_, remove_attrs_from_ui_atom_v, attr_uuids); - }, - - [=](request_full_attributes_description_atom, - const std::vector &attrs_group_names, - const utility::Uuid &requester_uuid) { - // N.B. some actors that are also a Module are Qt mixin actors ... such actors - // cannot have message handlers that return a response. So instead, to request the - // description of the Attributes that actors own we do it passively like this, - // sending a message asking for the attrs and they should send a message back which - // is handled in the 'attributes_atom' msm handler here - send( - module_ui_events_group_, - request_full_attributes_description_atom_v, - attrs_group_names, - requester_uuid); - }, - - [=](request_menu_attributes_description_atom, - const std::string &root_menu_name, - const utility::Uuid &requester_uuid) { - // see comment immediately above - send( - module_ui_events_group_, - request_menu_attributes_description_atom_v, - root_menu_name, - requester_uuid); - }, - - [=](change_attribute_request_atom, - const utility::Uuid &attr_uuid, - const int role, - const utility::JsonStore &value) { - send( - module_ui_events_group_, - change_attribute_request_atom_v, - attr_uuid, - role, - value); - }, - [=](attribute_deleted_event_atom, const utility::Uuid &attr_uuid) { - // An attribute has been deleted at the backend, notify attribute watchers - send(module_backend_events_group_, attribute_deleted_event_atom_v, attr_uuid); - }, - - [=](playhead::redraw_viewport_atom) { - send(playhead_group_, playhead::redraw_viewport_atom_v); - }, - [=](change_attribute_event_atom, - const utility::Uuid &module_uuid, - const utility::Uuid &attr_uuid, - const int role, - const utility::JsonStore &role_value, - const bool redraw_viewport) { - // This is notification that an attribute has actually changed - we pass it on to - // attribute watchers (like UI componenets) so that they can update to reflect the - // change - send( - module_backend_events_group_, - change_attribute_event_atom_v, - module_uuid, - attr_uuid, - role, - role_value); - - // Also a special playheads group will be sent a redraw message if required. - if (redraw_viewport) { - send(playhead_group_, playhead::redraw_viewport_atom_v); - } - }); -} - -void GlobalModuleAttrEventsActor::on_exit() { - system().registry().erase(module_events_registry); -} diff --git a/src/module/src/module.cpp b/src/module/src/module.cpp index 1e6d9f576..192b6baa8 100644 --- a/src/module/src/module.cpp +++ b/src/module/src/module.cpp @@ -1,5 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include + #include "xstudio/atoms.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" #include "xstudio/utility/logging.hpp" @@ -12,6 +14,7 @@ #include "xstudio/playhead/playhead_global_events_actor.hpp" #include +#include using namespace xstudio::utility; using namespace xstudio::module; @@ -22,7 +25,7 @@ caf::behavior delayed_resend(caf::event_based_actor *) { return {[](update_attribute_in_preferences_atom, caf::actor_addr module) { auto mod = caf::actor_cast(module); if (mod) { - anon_send(mod, update_attribute_in_preferences_atom_v); + anon_mail(update_attribute_in_preferences_atom_v).send(mod); } }}; } @@ -32,16 +35,55 @@ Module::Module(const std::string name, const utility::Uuid &uuid) : name_(std::move(name)), module_uuid_(uuid) {} Module::~Module() { + disconnect_from_ui(); - global_module_events_actor_ = caf::actor(); - keypress_monitor_actor_ = caf::actor(); + keypress_monitor_actor_ = caf::actor(); +} + +void Module::parent_actor_exiting() { + + if (self()) { + + disconnect_from_ui(); + + auto central_models_data_actor = + self()->home_system().registry().template get( + global_ui_model_data_registry); + + if (central_models_data_actor) { + + // make sure attribute data is removed from UI data models that expose + // attrs in the UI layer + for (auto &attribute : attributes_) { + + if (attribute->has_role_data(Attribute::UIDataModels)) { + + try { + + auto groups = attribute->get_role_data>( + Attribute::UIDataModels); + + for (const auto &group : groups) { + anon_mail( + ui::model_data::deregister_model_data_atom_v, + group, + attribute->uuid(), + caf::actor()) + .send(central_models_data_actor); + } + } catch (...) { + } + } + } + } + } } void Module::set_parent_actor_addr(caf::actor_addr addr) { + parent_actor_addr_ = addr; - if (!module_events_group_) { - module_events_group_ = self()->home_system().spawn(self()); + if (!attribute_events_group_) { attribute_events_group_ = self()->home_system().spawn(self()); try { @@ -51,12 +93,10 @@ void Module::set_parent_actor_addr(caf::actor_addr addr) { auto a = caf::actor_cast(self()); if (a) { join_broadcast(a, prefs.get_group(j)); - a->link_to(module_events_group_); } else { auto b = caf::actor_cast(self()); if (b) { join_broadcast(b, prefs.get_group(j)); - b->link_to(module_events_group_); } } update_attrs_from_preferences(j); @@ -68,37 +108,6 @@ void Module::set_parent_actor_addr(caf::actor_addr addr) { // we can't add hotkeys until the parent actor has been set. Subclasses of // Module should define hotkeys in the virtual register_hotkeys() function - - - // Some 'Modules' might try and register their hotkeys before they have been - // hooked in to an actor - here we are able to register tham - if (unregistered_hotkeys_.size()) { - if (!keypress_monitor_actor_) { - keypress_monitor_actor_ = - self()->home_system().registry().template get(keyboard_events); - } - - for (auto &hk : unregistered_hotkeys_) { - hk.add_watcher(addr); - anon_send( - keypress_monitor_actor_, ui::keypress_monitor::register_hotkey_atom_v, hk); - } - unregistered_hotkeys_.clear(); - } - - /*if (self()) { - self()->attach_functor([=](const caf::error &reason) { - spdlog::debug( - "STANKSTONK {} exited: {}", - name(), - to_string(reason)); - cleanup(); - spdlog::debug( - "STINKDONK {} exited: {}", - name(), - to_string(reason)); - }); - }*/ } void Module::delete_attribute(const utility::Uuid &attribute_uuid) { @@ -126,23 +135,24 @@ void Module::link_to_module( if (intial_push_sync) { + scoped_actor sys{self()->home_system()}; // send state of all attrs to 'other_module' so it can update its copies as required for (auto &attribute : attributes_) { if (link_all_attrs || linked_attrs_.find(attribute->uuid()) != linked_attrs_.end()) { - anon_send( - other_module, + anon_mail( change_attribute_value_atom_v, attribute->get_role_data(Attribute::Title), utility::JsonStore(attribute->role_data_as_json(Attribute::Value)), - true); + true) + .send(other_module); } } } if (both_ways) - anon_send( - other_module, module::link_module_atom_v, self(), link_all_attrs, false, false); + anon_mail(module::link_module_atom_v, self(), link_all_attrs, false, false) + .send(other_module); } } @@ -161,7 +171,7 @@ void Module::unlink_module(caf::actor other_module) { } if (found_link) { - anon_send(other_module, module::link_module_atom_v, self(), false); + anon_mail(module::link_module_atom_v, self(), false).send(other_module); } } @@ -184,8 +194,6 @@ FloatAttribute *Module::add_float_attribute( float_step, float_display_decimals, fscrub_sensitivity)); - rt->set_owner(this); - add_attribute(static_cast(rt)); return rt; @@ -200,7 +208,15 @@ StringChoiceAttribute *Module::add_string_choice_attribute( auto *rt(new StringChoiceAttribute( title, abbr_title, value, options, abbr_options.empty() ? options : abbr_options)); // rt->set_role_data(module::Attribute::StringChoicesEnabled, std::vector{}, false); - rt->set_owner(this); + add_attribute(static_cast(rt)); + return rt; +} + +IntegerVecAttribute * +Module::add_int_vec_attribute(const std::string &title, const std::string &abbr_title) { + + auto *rt(new IntegerVecAttribute(title, abbr_title, std::vector())); + // rt->set_role_data(module::Attribute::StringChoicesEnabled, std::vector{}, false); add_attribute(static_cast(rt)); return rt; } @@ -208,7 +224,6 @@ StringChoiceAttribute *Module::add_string_choice_attribute( JsonAttribute *Module::add_json_attribute( const std::string &title, const std::string &abbr_title, const nlohmann::json &value) { auto *rt(new JsonAttribute(title, abbr_title, value)); - rt->set_owner(this); add_attribute(static_cast(rt)); return rt; } @@ -216,8 +231,6 @@ JsonAttribute *Module::add_json_attribute( BooleanAttribute *Module::add_boolean_attribute( const std::string &title, const std::string &abbr_title, const bool value) { auto *rt(new BooleanAttribute(title, abbr_title, value)); - rt->set_owner(this); - add_attribute(static_cast(rt)); return rt; } @@ -225,8 +238,6 @@ BooleanAttribute *Module::add_boolean_attribute( StringAttribute *Module::add_string_attribute( const std::string &title, const std::string &abbr_title, const std::string &value) { auto rt = new StringAttribute(title, abbr_title, value); - rt->set_owner(this); - add_attribute(static_cast(rt)); return rt; } @@ -234,12 +245,11 @@ StringAttribute *Module::add_string_attribute( IntegerAttribute *Module::add_integer_attribute( const std::string &title, const std::string &abbr_title, - const int value, - const int int_min, - const int int_max) { + const int64_t value, + const int64_t int_min, + const int64_t int_max) { auto rt = new IntegerAttribute(title, abbr_title, value, int_min, int_max); - rt->set_owner(this); add_attribute(static_cast(rt)); return rt; } @@ -248,7 +258,6 @@ IntegerAttribute *Module::add_integer_attribute( QmlCodeAttribute * Module::add_qml_code_attribute(const std::string &name, const std::string &qml_code) { auto rt = new QmlCodeAttribute(name, qml_code); - rt->set_owner(this); add_attribute(static_cast(rt)); return rt; } @@ -258,23 +267,58 @@ ColourAttribute *Module::add_colour_attribute( const std::string &abbr_title, const utility::ColourTriplet &value) { auto rt = new ColourAttribute(title, abbr_title, value); - rt->set_owner(this); + add_attribute(static_cast(rt)); + return rt; +} +Vec4fAttribute *Module::add_vec4f_attribute( + const std::string &title, const std::string &abbr_title, const Imath::V4f &value) { + auto rt = new Vec4fAttribute(title, abbr_title, value); add_attribute(static_cast(rt)); return rt; } +Vec4fAttribute *Module::add_vec4f_attribute( + const std::string &title, + const std::string &abbr_title, + const Imath::V4f &value, + const Imath::V4f &min, + const Imath::V4f &max, + const Imath::V4f &step) { + auto rt = new Vec4fAttribute(title, abbr_title, value); + add_attribute(static_cast(rt)); + rt->set_role_data(Attribute::DefaultValue, value); + rt->set_role_data(Attribute::FloatScrubMin, min); + rt->set_role_data(Attribute::FloatScrubMax, max); + rt->set_role_data(Attribute::FloatScrubStep, step); + return rt; +} + +FloatVectorAttribute *Module::add_float_vector_attribute( + const std::string &title, + const std::string &abbr_title, + const std::vector &value, + const std::vector &min, + const std::vector &max, + const std::vector &step) { + auto rt = new FloatVectorAttribute(title, abbr_title, value); + add_attribute(static_cast(rt)); + rt->set_role_data(Attribute::DefaultValue, value); + rt->set_role_data(Attribute::FloatScrubMin, min); + rt->set_role_data(Attribute::FloatScrubMax, max); + rt->set_role_data(Attribute::FloatScrubStep, step); + return rt; +} + ActionAttribute * Module::add_action_attribute(const std::string &title, const std::string &abbr_title) { auto rt = new ActionAttribute(title, abbr_title); - rt->set_owner(this); - add_attribute(static_cast(rt)); return rt; } -bool Module::remove_attribute(const utility::Uuid &attribute_uuid) { +void Module::remove_attribute(const utility::Uuid &attribute_uuid) { auto attr = get_attribute(attribute_uuid); if (attr) { @@ -283,15 +327,13 @@ bool Module::remove_attribute(const utility::Uuid &attribute_uuid) { self()->home_system().registry().template get( global_ui_model_data_registry); - if (attr->has_role_data(Attribute::Groups)) { - auto groups = attr->get_role_data>(Attribute::Groups); + if (attr->has_role_data(Attribute::UIDataModels)) { + auto groups = + attr->get_role_data>(Attribute::UIDataModels); for (const auto &group_name : groups) { - anon_send( - central_models_data_actor, - ui::model_data::remove_rows_atom_v, - group_name, - attribute_uuid); + anon_mail(ui::model_data::remove_rows_atom_v, group_name, attribute_uuid) + .send(central_models_data_actor); } } for (auto p = attributes_.begin(); p != attributes_.end(); p++) { @@ -306,7 +348,6 @@ bool Module::remove_attribute(const utility::Uuid &attribute_uuid) { "{}: No attribute with id {}", __PRETTY_FUNCTION__, to_string(attribute_uuid)) .c_str()); } - return true; } utility::JsonStore Module::serialise() const { @@ -380,12 +421,40 @@ caf::message_handler Module::message_handler() { caf::message_handler h( {[=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + [=](attribute_role_data_atom, + const utility::Uuid &attr_uuid, + const std::string &role_name) -> result { + try { + int role = Attribute::role_index(role_name); + for (const auto &p : attributes_) { + if (p->uuid() == attr_uuid) { + if (p->has_role_data(role)) { + return p->role_data_as_json(role); + } else { + std::stringstream ss; + ss << "Request for attribute role data \"" + << Attribute::role_name(role) << "\" on attr \"" + << p->get_role_data(Attribute::Title) + << "\" on module \"" << name_ + << "\" : attribute does not have this role data."; + return caf::make_error(xstudio_error::error, ss.str().c_str()); + } + } + } + const std::string err = std::string("Request for attribute on module \"") + + name_ + + std::string("\" using unknown attribute uuid."); + return caf::make_error(xstudio_error::error, err); + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } + }, + [=](attribute_role_data_atom, utility::Uuid attr_uuid, const std::string &role_name, const utility::JsonStore &value) -> result { try { - for (const auto &p : attributes_) { if (p->uuid() == attr_uuid) { @@ -407,7 +476,11 @@ caf::message_handler Module::message_handler() { } }, - [=](utility::get_event_group_atom) -> caf::actor { return attribute_events_group_; }, + [=](utility::uuid_atom) -> utility::Uuid { return uuid(); }, + + [=](utility::get_event_group_atom, bool) -> caf::actor { + return attribute_events_group_; + }, [=](broadcast::join_broadcast_atom, caf::actor subscriber) -> result { try { @@ -543,6 +616,18 @@ caf::message_handler Module::message_handler() { return true; }, + [=](attribute_value_atom, const utility::Uuid &attr_uuid, bool full) + -> result { + auto attr = get_attribute(attr_uuid); + if (attr) { + return attr->as_json(); + } + const std::string err = std::string("No such attribute \"") + + to_string(attr_uuid) + std::string(" on ") + name_; + return caf::make_error(xstudio_error::error, err); + }, + + [=](attribute_value_atom, const std::string &attr_title) -> result { auto attr = get_attribute(attr_title); @@ -640,7 +725,6 @@ caf::message_handler Module::message_handler() { return true; }, - [=](attribute_uuids_atom) -> std::vector { std::vector rt; for (auto &attr : attributes_) { @@ -649,34 +733,6 @@ caf::message_handler Module::message_handler() { return rt; }, - [=](request_full_attributes_description_atom) -> utility::JsonStore { - utility::JsonStore r; - for (auto &attr : attributes_) { - r[attr->get_role_data(Attribute::Title)] = attr->as_json(); - } - return r; - }, - - [=](request_full_attributes_description_atom, - const std::vector &attr_group, - const utility::Uuid &requester_uuid) { - anon_send( - module_events_group_, - full_attributes_description_atom_v, - full_module(attr_group), - requester_uuid); - }, - - [=](request_menu_attributes_description_atom, - const std::string &root_menu_name, - const utility::Uuid &requester_uuid) { - anon_send( - module_events_group_, - full_attributes_description_atom_v, - menu_attrs(root_menu_name), - requester_uuid); - }, - [=](link_module_atom, caf::actor linkwith, bool all_attrs, @@ -704,7 +760,8 @@ caf::message_handler Module::message_handler() { const bool auto_repeat) { key_pressed(key, context, auto_repeat); }, [=](ui::keypress_monitor::mouse_event_atom, const ui::PointerEvent &e) { - if (connected_viewports_.find(e.context()) != connected_viewports_.end() && + if (connected_viewport_names_.find(e.context()) != + connected_viewport_names_.end() && !pointer_event(e)) { // pointer event was not used } @@ -716,8 +773,7 @@ caf::message_handler Module::message_handler() { const std::string &hotkey_name, const std::string &description, const bool auto_repeat, - const std::string &component, - const std::string &context) -> result { + const std::string &component) -> result { try { return register_hotkey( default_keycode, @@ -725,8 +781,7 @@ caf::message_handler Module::message_handler() { hotkey_name, description, auto_repeat, - component, - context); + component); } catch (std::exception &e) { return make_error(xstudio_error::error, e.what()); } @@ -738,8 +793,7 @@ caf::message_handler Module::message_handler() { const std::string &hotkey_name, const std::string &description, const bool auto_repeat, - const std::string &component, - const std::string &context) -> result { + const std::string &component) -> result { int default_keycode = -1; for (const auto &p : ui::Hotkey::key_names) { if (p.second == key_name) { @@ -761,8 +815,7 @@ caf::message_handler Module::message_handler() { hotkey_name, description, auto_repeat, - component, - context); + component); } catch (std::exception &e) { return make_error(xstudio_error::error, e.what()); } @@ -771,18 +824,38 @@ caf::message_handler Module::message_handler() { [=](ui::keypress_monitor::hotkey_event_atom, const utility::Uuid uuid, bool activated, - const std::string &context) { - anon_send( - attribute_events_group_, - ui::keypress_monitor::hotkey_event_atom_v, - uuid, - activated, - context); - - if (activated && connected_to_ui_ && - connected_viewports_.find(context) != connected_viewports_.end()) - hotkey_pressed(uuid, context); - else + const std::string &context, + const std::string &window) { + anon_mail( + ui::keypress_monitor::hotkey_event_atom_v, uuid, activated, context, window) + .send(attribute_events_group_); + + if (activated && connected_to_ui_ /*&& + connected_viewport_names_.find(context) != connected_viewport_names_.end()*/) { + hotkey_pressed(uuid, context, window); + + for (auto attr_uuid : dock_widget_attributes_) { + module::Attribute *attr = get_attribute(attr_uuid); + if (attr->has_role_data(Attribute::HotkeyUuid)) { + if (uuid == + attr->get_role_data(Attribute::HotkeyUuid)) { + // user has hit a hotkey that toggles a docking toolbox on/off. To + // get notification in the UI (QML) layer, we set the 'UserData' + // role data on the attr that holds data about the dock widget. + // There is a QML item in the UI layer that is watching for changes + // to this data - it will get an update and can then toggle the + // toolbar shown/hidden + nlohmann::json event; + event["context"] = context; + // random num ensures the data changes every time + // so notification mechanism is triggered + event["id"] = (double)rand() / RAND_MAX; + attr->set_role_data(Attribute::UserData, event); + } + } + } + + } else if (!activated) hotkey_released(uuid, context); }, @@ -803,7 +876,6 @@ caf::message_handler Module::message_handler() { true, false // do not broadcast the change * ); - // * if we set a pref and we don't suppress broadcast the // GlobalStore actor that manages prefs re-broadcasts the // new preference and it comes back up to us ... this can @@ -813,12 +885,14 @@ caf::message_handler Module::message_handler() { } attrs_waiting_to_update_prefs_.clear(); }, - [=](module::current_viewport_playhead_atom, caf::actor_addr) {}, + [=](module::current_viewport_playhead_atom, const std::string &name, caf::actor_addr) + -> bool { return true; }, [=](ui::viewport::connect_to_viewport_toolbar_atom, const std::string &viewport_name, const std::string &viewport_toolbar_name, + caf::actor viewport, bool connect) { - connect_to_viewport(viewport_name, viewport_toolbar_name, connect); + connect_to_viewport(viewport_name, viewport_toolbar_name, connect, viewport); }, [=](utility::name_atom) -> std::string { return name(); }, [=](utility::event_atom, @@ -826,12 +900,14 @@ caf::message_handler Module::message_handler() { caf::actor media, caf::actor media_source) { on_screen_media_changed(media, media_source); - anon_send( - attribute_events_group_, - utility::event_atom_v, - playhead::show_atom_v, - media, - media_source); + anon_mail(utility::event_atom_v, playhead::show_atom_v, media, media_source) + .send(attribute_events_group_); + }, + [=](utility::event_atom, + ui::viewport::viewport_atom, + const std::string viewport_name, + caf::actor viewport) { + // this event message happens when a new viewport is created }, [=](utility::event_atom, xstudio::ui::model_data::set_node_data_atom, @@ -845,11 +921,26 @@ caf::message_handler Module::message_handler() { const utility::JsonStore &data, const std::string role, const utility::Uuid &uuid_role_data) { + // this update is incoming from the UI layer when attributes + // that are exposed in JsonTree model data get changed by + // QML. It could be general data model or menu specifi one. + // For menu specific one we need to fiddle a bit to map the + // role data. try { + + + int role_index; + if (role == "is_checked" || role == "current_choice") + role_index = Attribute::Value; + else if (role == "choices") + role_index = Attribute::Value; + else + role_index = Attribute::role_index(role); Attribute *attr = get_attribute(uuid_role_data); if (attr) { - attr->set_role_data(Attribute::role_index(role), data); + attr->set_role_data(role_index, data); } + } catch (std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } @@ -857,9 +948,147 @@ caf::message_handler Module::message_handler() { [=](utility::event_atom, xstudio::ui::model_data::model_data_atom, const std::string &model_name, - const utility::JsonStore &data) {} + const utility::JsonStore &data) -> bool { return true; }, + + [=](utility::event_atom, + ui::model_data::menu_node_activated_atom, + const std::string path, + const utility::JsonStore &menu_item_data, + const std::string &user_data, + const bool from_hotkey) { + if (from_hotkey) { + // N.B. hotkeys can trigger menu events. This is useful in + // the QML layer where we might define a menu item with an + // associated hotkey. However, for Modules we create hotkeys + // and get events separet to the menu system. The only thing + // we might use is tag a hotkey on a menu item so the hotkey + // sequence is made visible on the menu item but the + // mechanism for handling it is throug the hotkey_event_atom + // messages. + return; + } + + try { + if (menu_item_data.contains("uuid")) { + + bool event_used = false; + utility::Uuid uuid(menu_item_data["uuid"]); + // is the menu item representing a toggle attribute? + auto *attr = get_attribute(uuid); + if (attr) { + try { + if (attr->get_role_data(Attribute::Type) == + "OnOffToggle") { + attr->set_role_data( + Attribute::Value, + !attr->get_role_data(Attribute::Value)); + event_used = true; + } + } catch (...) { + // not a bool attr + } + } + + // in the case where the menu item isn't linked to a toggle + // attr we run our callback that subclasses use to implement + // their own response to a menu item action + menu_item_activated(menu_item_data, user_data); + anon_mail( + ui::model_data::menu_node_activated_atom_v, menu_item_data, user_data) + .send(attribute_events_group_); + } + } catch (std::exception &e) { + //std::cerr << "EE " << e.what() << "\n"; + } + }, + + [=](utility::event_atom, + xstudio::ui::model_data::set_node_data_atom, + const std::string model_name, + const std::string path, + const std::string role, + const utility::JsonStore &data, + const utility::Uuid &menu_item_uuid) { + try { + Attribute *attr = get_attribute(menu_item_uuid); + if (attr) { + if (role == "is_checked" && data.is_boolean()) { + attr->set_role_data(Attribute::Value, data.get()); + } else if (role == "current_choice" && data.is_string()) { + attr->set_role_data(Attribute::Value, data.get()); + } else { + throw std::runtime_error("bad data"); + } + } + + } catch (std::exception &e) { + spdlog::warn( + "Module set_node_data_atom handler - {} {} {} {} {}", + e.what(), + model_name, + path, + role, + data.dump()); + } + }, + [=](module_add_menu_item_atom, + const std::string &menu_model_name, + const std::string &menu_path, + const float menu_item_position, + const utility::Uuid &hotkey) -> result { + try { + return insert_hotkey_into_menu( + menu_model_name, menu_path, menu_item_position, hotkey); + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } + }, + [=](module_add_menu_item_atom, + const std::string &menu_model_name, + const std::string &menu_text, + const std::string &menu_path, + const float menu_item_position, + const utility::Uuid &attr_id, + const bool divider, + const utility::Uuid &hotkey, + const std::string &user_data) -> result { + try { + return insert_menu_item( + menu_model_name, + menu_text, + menu_path, + menu_item_position, + attr_id.is_null() ? nullptr : get_attribute(attr_id), + divider, + hotkey, + user_data); + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } + }, + [=](module_remove_menu_item_atom, const utility::Uuid &menu_id) { + for (const auto &p : menu_items_) { + + for (const auto uuid : p.second) { + if (uuid == menu_id) { + auto central_models_data_actor = + self()->home_system().registry().template get( + global_ui_model_data_registry); + anon_mail(ui::model_data::remove_node_atom_v, p.first, menu_id) + .send(central_models_data_actor); + return; + } + } + } + }, + [=](xstudio::ui::model_data::set_node_data_atom, + const std::string &menu_model_name, + const std::string &submenu, + const float submenu_position) { + set_submenu_position_in_parent(menu_model_name, submenu, submenu_position); + }, + [=](reset_module_atom) { reset(); }}); - }); return h.or_else(playhead::PlayheadGlobalEventsActor::default_event_handler()); } @@ -872,40 +1101,28 @@ void Module::notify_change( Attribute *attr = get_attribute(attr_uuid); - if (module_events_group_) { - anon_send( - module_events_group_, - change_attribute_event_atom_v, - module_uuid_, - attr_uuid, - role, - value, - redraw_viewport); - - anon_send( - attribute_events_group_, - change_attribute_event_atom_v, - module_uuid_, - attr_uuid, - role, - value); - - if (attr->has_role_data(Attribute::Groups)) { + if (attribute_events_group_) { + + anon_mail(change_attribute_event_atom_v, module_uuid_, attr_uuid, role, value) + .send(attribute_events_group_); + + if (attr->has_role_data(Attribute::UIDataModels)) { auto central_models_data_actor = self()->home_system().registry().template get( global_ui_model_data_registry); - auto groups = attr->get_role_data>(Attribute::Groups); + auto groups = + attr->get_role_data>(Attribute::UIDataModels); for (const auto &group_name : groups) { - anon_send( - central_models_data_actor, + anon_mail( ui::model_data::set_node_data_atom_v, group_name, attr->uuid(), Attribute::role_name(role), value, - self()); + self()) + .send(central_models_data_actor); } } attribute_changed(attr_uuid, role, self_notify); @@ -952,16 +1169,17 @@ void Module::notify_change( // example) because it will hang the actor system if the Viewport is // destroyed before the delayed message is received. - /*delayed_anon_send( - self(), std::chrono::seconds(2), update_attribute_in_preferences_atom_v);*/ + /*anon_mail(update_attribute_in_preferences_atom_v).delay(std::chrono::seconds(2)).send(self());*/ // To get around this problem we can do this shenannegans instead... auto resender = self()->home_system().spawn(delayed_resend); - delayed_anon_send( - resender, - std::chrono::seconds(2), - update_attribute_in_preferences_atom_v, - parent_actor_addr_); + anon_mail(update_attribute_in_preferences_atom_v, parent_actor_addr_) + .delay(std::chrono::seconds(2)) + .send(resender); + + // delayed_anon_mail(// std::chrono::seconds(2), + // update_attribute_in_preferences_atom_v, + // parent_actor_addr_).send(// resender); } attrs_waiting_to_update_prefs_.insert(attr_uuid); @@ -970,29 +1188,57 @@ void Module::notify_change( void Module::attribute_changed(const utility::Uuid &attr_uuid, const int role_id, bool notify) { - module::Attribute *attr = get_attribute(attr_uuid); + if (attr && attr->has_role_data(Attribute::MenuPaths)) { + update_attribute_menu_item_data(attr); + } - if (role_id == Attribute::Groups && attr) { + if (role_id == Attribute::UIDataModels && attr) { auto central_models_data_actor = self()->home_system().registry().template get( global_ui_model_data_registry); - auto groups = attr->get_role_data>(Attribute::Groups); + auto groups = attr->get_role_data>(Attribute::UIDataModels); for (const auto &group_name : groups) { - anon_send( - central_models_data_actor, + anon_mail( ui::model_data::register_model_data_atom_v, group_name, utility::JsonStore(attr->as_json()), attr_uuid, Attribute::role_name(Attribute::ToolbarPosition), - self()); + self()) + .send(central_models_data_actor); } } + if (dock_widget_attributes_.find(attr_uuid) != dock_widget_attributes_.end() && + role_id == module::Attribute::Activated) { + + // this happens when the user has clicked on the button that shows/hides + // a dockable toolbar (e.g. annotation tools) OR when a toolbar goes + // offscreen + module::Attribute *attr = get_attribute(attr_uuid); + int activated = 0; + std::string name; + try { + activated = attr->get_role_data(module::Attribute::Activated); + name = attr->get_role_data(module::Attribute::Title); + } catch (...) { + } + + if (activated == 1) { + viewport_dockable_widget_activated(name); + } else if (activated == 0) { + viewport_dockable_widget_deactivated(name); + } + + // silently set the 'Activated' role data to -1 so the UI can set it + // to 0 or 1 (again) and we still get notification + attr->set_role_data(module::Attribute::Activated, -1, false); + } + // This is where the 'linking' mechanism is enacted. We send a change_attribute // message to linked modules. if (linking_disabled_ || role_id != Attribute::Value) { @@ -1017,14 +1263,14 @@ void Module::attribute_changed(const utility::Uuid &attr_uuid, const int role_id module::Attribute *attr = get_attribute(attr_uuid); auto attr_name = attr->get_role_data(module::Attribute::Title); auto attr_role_data = attr->role_data_as_json(role_id); - anon_send( - linked_module, + anon_mail( module::change_attribute_value_atom_v, attr_name, role_id, notify, utility::JsonStore(attr_role_data), - my_adress); + my_adress) + .send(linked_module); } } for (auto &linked_module_addr : fully_linked_modules_) { @@ -1036,14 +1282,14 @@ void Module::attribute_changed(const utility::Uuid &attr_uuid, const int role_id module::Attribute *attr = get_attribute(attr_uuid); auto attr_name = attr->get_role_data(module::Attribute::Title); auto attr_role_data = attr->role_data_as_json(role_id); - anon_send( - linked_module, + anon_mail( module::change_attribute_value_atom_v, attr_name, role_id, notify, utility::JsonStore(attr_role_data), - my_adress); + my_adress) + .send(linked_module); } if (notify) @@ -1073,14 +1319,32 @@ Attribute *Module::get_attribute(const std::string &attr_title) { return r; } +utility::Uuid Module::register_hotkey( + const std::string sequence, + const std::string &hotkey_name, + const std::string &description, + const bool auto_repeat, + const std::string &component) { + try { + + int key, modifier; + ui::Hotkey::sequence_to_key_and_modifier(sequence, key, modifier); + return register_hotkey(key, modifier, hotkey_name, description, auto_repeat, component); + + } catch (std::exception &e) { + spdlog::warn("{} : {} {}", name(), __PRETTY_FUNCTION__, e.what()); + } + + return utility::Uuid(); +} + utility::Uuid Module::register_hotkey( int default_keycode, int default_modifier, const std::string &hotkey_name, const std::string &description, const bool auto_repeat, - const std::string &component, - const std::string &context) { + const std::string &component) { if (self()) { if (!keypress_monitor_actor_) { @@ -1094,27 +1358,22 @@ utility::Uuid Module::register_hotkey( hotkey_name, component == "MODULE_NAME" ? name() : component, description, - context, + "any", // window auto_repeat, caf::actor_cast(self())); - anon_send(keypress_monitor_actor_, ui::keypress_monitor::register_hotkey_atom_v, hk); + anon_mail(ui::keypress_monitor::register_hotkey_atom_v, hk) + .send(keypress_monitor_actor_); return hk.uuid(); } else { - - unregistered_hotkeys_.emplace_back( - default_keycode, - default_modifier, + spdlog::warn( + "{} Attempt to register hotkey {} in module {} before module has been initialised " + "with a parent actor", + __PRETTY_FUNCTION__, hotkey_name, - component == "MODULE_NAME" ? name() : component, - description, - context, - auto_repeat, - caf::actor_addr()); - - return unregistered_hotkeys_.back().uuid(); + name()); } return utility::Uuid(); @@ -1125,61 +1384,40 @@ void Module::remove_hotkey(const utility::Uuid & /*hotkey_uuid*/) {} void Module::grab_mouse_focus() { if (keypress_monitor_actor_ && self()) { - anon_send(keypress_monitor_actor_, grab_all_mouse_input_atom_v, self(), true); + anon_mail(grab_all_mouse_input_atom_v, self(), true).send(keypress_monitor_actor_); } } void Module::release_mouse_focus() { if (keypress_monitor_actor_ && self()) { - anon_send(keypress_monitor_actor_, grab_all_mouse_input_atom_v, self(), false); + anon_mail(grab_all_mouse_input_atom_v, self(), false).send(keypress_monitor_actor_); } } void Module::grab_keyboard_focus() { if (keypress_monitor_actor_ && self()) { - anon_send(keypress_monitor_actor_, grab_all_keyboard_input_atom_v, self(), true); + anon_mail(grab_all_keyboard_input_atom_v, self(), true).send(keypress_monitor_actor_); } } void Module::release_keyboard_focus() { if (keypress_monitor_actor_ && self()) { - anon_send(keypress_monitor_actor_, grab_all_keyboard_input_atom_v, self(), false); + anon_mail(grab_all_keyboard_input_atom_v, self(), false).send(keypress_monitor_actor_); } } -void Module::listen_to_playhead_events(const bool listen) { - - // join the global playhead events group - this tells us when the playhead that should - // be on screen changes, among other things - auto ph_events = - self()->home_system().registry().template get(global_playhead_events_actor); - if (listen) - anon_send(ph_events, broadcast::join_broadcast_atom_v, self()); - else - anon_send(ph_events, broadcast::leave_broadcast_atom_v, self()); -} - void Module::connect_to_ui() { // if necessary, get the global module events actor and the associated events groups - if ((!global_module_events_actor_ || !keypress_monitor_actor_ || - !keyboard_and_mouse_group_) && - self()) { + if ((!keypress_monitor_actor_ || !keyboard_and_mouse_group_) && self()) { try { - global_module_events_actor_ = - self()->home_system().registry().template get( - module_events_registry); - scoped_actor sys{self()->home_system()}; - ui_attribute_events_group_ = utility::request_receive( - *sys, global_module_events_actor_, module_ui_events_group_atom_v); - keypress_monitor_actor_ = self()->home_system().registry().template get(keyboard_events); keyboard_and_mouse_group_ = utility::request_receive( @@ -1198,12 +1436,10 @@ void Module::connect_to_ui() { auto a = caf::actor_cast(self()); if (a) { join_broadcast(a, keyboard_and_mouse_group_); - join_broadcast(a, ui_attribute_events_group_); } else { auto b = caf::actor_cast(self()); if (b) { join_broadcast(b, keyboard_and_mouse_group_); - join_broadcast(b, ui_attribute_events_group_); } else { spdlog::warn( "{} {}", @@ -1224,10 +1460,6 @@ void Module::connect_to_ui() { register_hotkeys(); - anon_send( - global_module_events_actor_, join_module_attr_events_atom_v, module_events_group_); - anon_send(global_module_events_actor_, full_attributes_description_atom_v, full_module()); - // here we set-up a tree model that holds the state of attributes that we want // to make visible in the UI (Qt/QML) layer using QAbstractItemModel. // The tree lives in a central actor that is a middleman between us and the @@ -1241,17 +1473,18 @@ void Module::connect_to_ui() { for (auto &a : attributes_) { - if (a->has_role_data(Attribute::Groups)) { - auto groups = a->get_role_data>(Attribute::Groups); + if (a->has_role_data(Attribute::UIDataModels)) { + auto groups = a->get_role_data>(Attribute::UIDataModels); for (const auto &group_name : groups) { - anon_send( - central_models_data_actor, + + anon_mail( ui::model_data::register_model_data_atom_v, group_name, utility::JsonStore(a->as_json()), a->uuid(), Attribute::role_name(Attribute::ToolbarPosition), - self()); + self()) + .send(central_models_data_actor); } } } @@ -1262,12 +1495,11 @@ void Module::disconnect_from_ui() { if (!connected_to_ui_) return; - if (keyboard_and_mouse_group_ && ui_attribute_events_group_ && self()) { + if (keyboard_and_mouse_group_ && self()) { // got to be a better way of doing this? auto a = caf::actor_cast(self()); if (a) { leave_broadcast(a, keyboard_and_mouse_group_); - leave_broadcast(a, ui_attribute_events_group_); } else { auto b = caf::actor_cast(self()); if (b) { @@ -1280,41 +1512,32 @@ void Module::disconnect_from_ui() { } } - // tell the UI middleman to remove our attributes from its data models + // Note: Ted, May 2024 - commenting out the below. If a module (like the + // playhead) is disconnected from the UI (because it's not being viewed + // in a viewport) we still want it's attributes to be live in the front + // end because things like a timeline that might not be in the viewport + // but active in the UI needs to know where the playhead is etc. - auto central_models_data_actor = + // tell the UI middleman to remove our attributes from its data models + /*auto central_models_data_actor = self()->home_system().registry().template get( global_ui_model_data_registry); for (auto &a : attributes_) { - if (a->has_role_data(Attribute::Groups)) { - auto groups = a->get_role_data>(Attribute::Groups); - for (const auto &group_name : groups) { - anon_send( - central_models_data_actor, - ui::model_data::register_model_data_atom_v, + if (a->has_role_data(Attribute::UIDataModels)) { + auto groups = + a->get_role_data>(Attribute::UIDataModels); for (const auto + &group_name : groups) { anon_mail(ui::model_data::deregister_model_data_atom_v, group_name, a->uuid(), - self()); + self()).send(central_models_data_actor); } } - } - } - - if (global_module_events_actor_) { - anon_send( - global_module_events_actor_, leave_module_attr_events_atom_v, module_events_group_); - std::vector attr_uuids; - for (const auto &p : attributes_) { - attr_uuids.push_back(p->uuid()); - } - anon_send(global_module_events_actor_, remove_attrs_from_ui_atom_v, attr_uuids); + }*/ } - keyboard_and_mouse_group_ = caf::actor(); - ui_attribute_events_group_ = caf::actor(); - global_module_events_actor_ = caf::actor(); + keyboard_and_mouse_group_ = caf::actor(); if (connected_to_ui_) { connected_to_ui_ = false; @@ -1329,7 +1552,7 @@ void Module::update_attr_from_preference(const std::string &path, const JsonStor try { - attr->set_role_data(Attribute::Value, change, false); + attr->set_role_data(Attribute::Value, change, true); } catch (std::exception &e) { spdlog::warn("{} failed to set preference {}", __PRETTY_FUNCTION__, e.what()); @@ -1341,7 +1564,7 @@ void Module::update_attr_from_preference(const std::string &path, const JsonStor try { - attr->set_role_data(Attribute::Value, change, false); + attr->set_role_data(Attribute::Value, change, true); // wipe the 'InitOnlyPreferencePath' data so we never update again // when preferences are updated attr->delete_role_data(Attribute::InitOnlyPreferencePath); @@ -1361,32 +1584,62 @@ void Module::update_attrs_from_preferences(const utility::JsonStore &entire_pref try { - auto pref_path = attr->get_role_data(Attribute::PreferencePath); + auto pref_path = attr->get_role_data(Attribute::PreferencePath); + + try { + + if (entire_prefs_dict.get(pref_path + "/value").is_null()) { + + // if get "/default_value" throws an exception, we silence + // the warning as it means we have a preference that isn't + // forward declared in the default prefs files, and that + // is ok for module preferences as these can have a + // preference path for saving their value without the + // pref being declared in the .json pref files that + // are part of the codebase + std::ignore = entire_prefs_dict.get(pref_path + "/default_value"); + } + + } catch (...) { + continue; + } + auto pref_value = global_store::preference_value( entire_prefs_dict, pref_path); - attr->set_role_data(Attribute::Value, pref_value, true); } catch (std::exception &e) { - spdlog::warn("{} failed to set preference {}", __PRETTY_FUNCTION__, e.what()); + spdlog::warn( + "{} failed to set preference {} : attr - {}", + __PRETTY_FUNCTION__, + e.what(), + attr->role_data_as_json(Attribute::PreferencePath).dump()); } } else if (attr->has_role_data(Attribute::InitOnlyPreferencePath)) { try { - auto pref_path = attr->get_role_data(Attribute::InitOnlyPreferencePath); - auto pref_value = global_store::preference_value( - entire_prefs_dict, pref_path); - attr->set_role_data(Attribute::Value, pref_value, false); + if (!entire_prefs_dict.get(pref_path + "/value").is_null() || + !entire_prefs_dict.get(pref_path + "/default_value").is_null()) { - // wipe the 'InitOnlyPreferencePath' data so we never update again - // when preferences are updated - attr->delete_role_data(Attribute::InitOnlyPreferencePath); + auto pref_value = global_store::preference_value( + entire_prefs_dict, pref_path); + + attr->set_role_data(Attribute::Value, pref_value, false); + + // wipe the 'InitOnlyPreferencePath' data so we never update again + // when preferences are updated + attr->delete_role_data(Attribute::InitOnlyPreferencePath); + } } catch (std::exception &e) { - spdlog::warn("{} failed to set preference {}", __PRETTY_FUNCTION__, e.what()); + spdlog::warn( + "{} failed to set preference {} : attr2 - {}", + __PRETTY_FUNCTION__, + e.what(), + attr->role_data_as_json(Attribute::PreferencePath).dump()); } } } @@ -1397,6 +1650,7 @@ void Module::add_multichoice_attr_to_menu( const std::string top_level_menu, const std::string menu_path, const std::string before) { + // RESKIN DEPRECATE std::string full_path = top_level_menu + "|" + menu_path; std::vector menu_paths; if (attr->has_role_data(module::Attribute::MenuPaths)) { @@ -1409,6 +1663,7 @@ void Module::add_multichoice_attr_to_menu( void Module::add_boolean_attr_to_menu( BooleanAttribute *attr, const std::string top_level_menu, const std::string before) { + // RESKIN DEPRECATE std::vector menu_paths; if (attr->has_role_data(module::Attribute::MenuPaths)) { menu_paths = @@ -1418,6 +1673,315 @@ void Module::add_boolean_attr_to_menu( attr->set_role_data(module::Attribute::MenuPaths, nlohmann::json(menu_paths)); } +utility::JsonStore Module::attribute_menu_item_data(Attribute *attr) { + // for an attribute that is exposed in a UI menu, build a json dict that + // describes that attribute data required for the menu model + + utility::JsonStore menu_item_data; + if (attr->get_role_data(Attribute::Type) == "ComboBox") { + menu_item_data["current_choice"] = attr->get_role_data(Attribute::Value); + /*auto choices = nlohmann::json::parse("[]"); + for (const auto &c : multi_choice->options()) { + choices.insert(choices.begin() + choices.size(), 1, c); + }*/ + menu_item_data["choices"] = + attr->get_role_data>(Attribute::StringChoices); + menu_item_data["menu_item_type"] = "multichoice"; + + if (attr->has_role_data(Attribute::StringChoicesIds)) { + menu_item_data["choices_ids"] = + attr->get_role_data>(Attribute::StringChoicesIds); + } + + if (attr->has_role_data(Attribute::StringChoicesEnabled)) { + menu_item_data["combo_box_options_enabled"] = + attr->get_role_data>(Attribute::StringChoicesEnabled); + } + + + } else if (attr->get_role_data(Attribute::Type) == "RadioGroup") { + menu_item_data["current_choice"] = attr->get_role_data(Attribute::Value); + /*auto choices = nlohmann::json::parse("[]"); + for (const auto &c : multi_choice->options()) { + choices.insert(choices.begin() + choices.size(), 1, c); + }*/ + menu_item_data["choices"] = + attr->get_role_data>(Attribute::StringChoices); + menu_item_data["menu_item_type"] = "radiogroup"; + + if (attr->has_role_data(Attribute::StringChoicesIds)) { + menu_item_data["choices_ids"] = + attr->get_role_data>(Attribute::StringChoicesIds); + } + + if (attr->has_role_data(Attribute::StringChoicesEnabled)) { + menu_item_data["combo_box_options_enabled"] = + attr->get_role_data>(Attribute::StringChoicesEnabled); + } + + } else if (attr->get_role_data(Attribute::Type) == "OnOffToggle") { + menu_item_data["is_checked"] = attr->get_role_data(Attribute::Value); + menu_item_data["menu_item_type"] = "toggle"; + } else { + throw std::runtime_error("Attribute type not suitable for menu control."); + } + menu_item_data["uuid"] = attr->uuid(); + menu_item_data["menu_item_enabled"] = attr->has_role_data(Attribute::Enabled) + ? attr->get_role_data(Attribute::Enabled) + : true; + return menu_item_data; +} + +utility::Uuid Module::insert_menu_item( + const std::string &menu_model_name, + const std::string &menu_text, + const std::string &menu_path, + const float menu_item_position, + Attribute *attr, + const bool divider, + const utility::Uuid &hotkey, + const std::string &user_data) { + try { + auto central_models_data_actor = + self()->home_system().registry().template get( + global_ui_model_data_registry); + + utility::JsonStore menu_item_data; + if (attr) { + menu_item_data = attribute_menu_item_data(attr); + // For now using this 'RESKIN' dummy token to stop menus from 'old' skin + // messing things up for the reskin. Alternatively (hack alert) if we + // want to use the value of the attribute itself to set the menu item + // name we start the menu path with USE_ATTR_VALUE + std::string new_menu_path; + if (menu_text == "USE_ATTR_VALUE") { + new_menu_path = "USE_ATTR_VALUE|" + menu_model_name + "|" + menu_path + "|" + + attr->get_role_data(Attribute::Value); + } else { + new_menu_path = "RESKIN|" + menu_model_name + "|" + menu_path + "|" + menu_text; + } + if (attr->has_role_data(Attribute::MenuPaths)) { + auto paths = + attr->get_role_data>(Attribute::MenuPaths); + paths.push_back(new_menu_path); + attr->set_role_data(Attribute::MenuPaths, paths); + } else { + attr->set_role_data( + Attribute::MenuPaths, std::vector({new_menu_path})); + } + } else { + // a menu item that is not linked by to an attribute - it simply + // has a uuid which, when the user clicks on the menu item, we run + // a callback to menu_item_activated virtual method + menu_item_data["menu_item_type"] = divider ? "divider" : "button"; + menu_item_data["uuid"] = utility::Uuid::generate(); + } + + if (attr && menu_text == "USE_ATTR_VALUE") { + menu_item_data["name"] = attr->get_role_data(Attribute::Value); + } else if (menu_text != "") { + menu_item_data["name"] = menu_text; + } + + menu_item_data["menu_item_position"] = menu_item_position; + if (!hotkey.is_null()) { + menu_item_data["hotkey_uuid"] = hotkey; + } + if (!user_data.empty()) { + menu_item_data["user_data"] = user_data; + } + + if (!menu_item_data.contains("menu_item_enabled")) { + menu_item_data["menu_item_enabled"] = true; + } + menu_item_data["menu_item_context"] = std::string(); + + auto a = caf::actor_cast(self()); + if (a) { + a->mail( + ui::model_data::insert_or_update_menu_node_atom_v, + menu_model_name, + menu_path, + menu_item_data, + self()) + .send(central_models_data_actor); + } else { + anon_mail( + ui::model_data::insert_or_update_menu_node_atom_v, + menu_model_name, + menu_path, + menu_item_data, + self()) + .send(central_models_data_actor); + } + + menu_items_[menu_model_name].push_back(menu_item_data["uuid"]); + return menu_item_data["uuid"]; + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + return utility::Uuid(); +} + +utility::Uuid Module::insert_hotkey_into_menu( + const std::string &menu_model_name, + const std::string &menu_path, + const float menu_item_position, + const utility::Uuid &hotkey) { + try { + + auto central_models_data_actor = + self()->home_system().registry().template get( + global_ui_model_data_registry); + + utility::JsonStore menu_item_data; + menu_item_data["menu_item_type"] = "button"; + auto menu_item_id = utility::Uuid::generate(); + menu_item_data["uuid"] = menu_item_id; + menu_item_data["hotkey_uuid"] = hotkey; + menu_item_data["menu_item_position"] = menu_item_position; + + auto a = caf::actor_cast(self()); + if (a) { + a->mail( + ui::model_data::insert_or_update_menu_node_atom_v, + menu_model_name, + menu_path, + menu_item_data, + self()) + .send(central_models_data_actor); + } else { + anon_mail( + ui::model_data::insert_or_update_menu_node_atom_v, + menu_model_name, + menu_path, + menu_item_data, + self()) + .send(central_models_data_actor); + } + + menu_items_[menu_model_name].push_back(menu_item_id); + return menu_item_id; + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + return utility::Uuid(); +} + +void Module::remove_all_menu_items(const std::string &menu_model_name) { + if (menu_items_.find(menu_model_name) != menu_items_.end()) { + auto central_models_data_actor = + self()->home_system().registry().template get( + global_ui_model_data_registry); + + for (const auto &uuid : menu_items_[menu_model_name]) { + anon_mail(ui::model_data::remove_node_atom_v, menu_model_name, uuid) + .send(central_models_data_actor); + } + } +} + +void Module::update_attribute_menu_item_data(Attribute *attr) { + + try { + auto central_models_data_actor = + self()->home_system().registry().template get( + global_ui_model_data_registry); + + utility::JsonStore menu_item_data = attribute_menu_item_data(attr); + + const auto paths = attr->get_role_data>(Attribute::MenuPaths); + for (const auto &path : paths) { + auto sections = utility::split(path, '|'); + if (sections.size() >= 2 && + (sections.front() == "RESKIN" || sections.front() == "USE_ATTR_VALUE")) { + + bool use_val = sections.front() == "USE_ATTR_VALUE"; + sections.erase(sections.begin()); + + // when an attribute is first exposed in a menu, we add a string to the + // MenuPaths attribute data ... at the front of the string is + // the menu model name (i.e. the name of the set of data that + // is driving a particular pop-up menu or menu bar), at the + // back is the menu item name (i.e. what is displayed in the + // menu) and the middle is the 'menu path' which is this list + // of sub-menu names under which the menu item is inserted. + + if (use_val) { + menu_item_data["name"] = attr->get_role_data(Attribute::Value); + } else { + menu_item_data["name"] = sections.back(); + } + std::string menu_path; + for (size_t i = 1; i < (sections.size() - 1); ++i) { + menu_path = sections[i] + (i == (sections.size() - 1) ? "" : "|"); + } + + anon_mail( + ui::model_data::insert_or_update_menu_node_atom_v, + sections.front(), // menu model name + menu_path, + menu_item_data, + self()) + .send(central_models_data_actor); + } + } + + } catch (std::exception &e) { + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, e.what(), attr->as_json().dump()); + } +} + +void Module::remove_menu_item(const std::string &menu_model_name, const utility::Uuid item_id) { + auto central_models_data_actor = self()->home_system().registry().template get( + global_ui_model_data_registry); + + anon_mail(ui::model_data::remove_node_atom_v, menu_model_name, item_id) + .send(central_models_data_actor); +} + +utility::Uuid Module::insert_menu_divider( + const std::string &menu_model_name, + const std::string &menu_path, + const float menu_item_position) { + auto central_models_data_actor = self()->home_system().registry().template get( + global_ui_model_data_registry); + + utility::JsonStore menu_item_data; + + menu_item_data["menu_item_position"] = menu_item_position; + menu_item_data["menu_item_type"] = "divider"; + menu_item_data["uuid"] = utility::Uuid::generate(); + + anon_mail( + ui::model_data::insert_or_update_menu_node_atom_v, + menu_model_name, + menu_path, + menu_item_data, + self()) + .send(central_models_data_actor); + + return menu_item_data["uuid"]; +} + +void Module::set_submenu_position_in_parent( + const std::string &menu_model_name, + const std::string &submenu, + const float submenu_position) { + + auto central_models_data_actor = self()->home_system().registry().template get( + global_ui_model_data_registry); + + anon_mail( + ui::model_data::insert_or_update_menu_node_atom_v, + menu_model_name, + submenu, + submenu_position) + .send(central_models_data_actor); +} + void Module::make_attribute_visible_in_viewport_toolbar( Attribute *attr, const bool make_visible) { if (make_visible) { @@ -1433,19 +1997,19 @@ void Module::make_attribute_visible_in_viewport_toolbar( if (central_models_data_actor) { - for (const auto &viewport_name : connected_viewports_) { + for (const auto &viewport_name : connected_viewport_names_) { std::string toolbar_name = viewport_name + "_toolbar"; attr->expose_in_ui_attrs_group(toolbar_name, true); - anon_send( - central_models_data_actor, + anon_mail( ui::model_data::register_model_data_atom_v, toolbar_name, utility::JsonStore(attr->as_json()), attr->uuid(), Attribute::role_name(Attribute::ToolbarPosition), - self()); + self()) + .send(central_models_data_actor); } } @@ -1463,24 +2027,26 @@ void Module::make_attribute_visible_in_viewport_toolbar( global_ui_model_data_registry); if (central_models_data_actor) { - for (const auto &viewport_name : connected_viewports_) { + for (const auto &viewport_name : connected_viewport_names_) { std::string toolbar_name = viewport_name + "_toolbar"; attr->expose_in_ui_attrs_group(toolbar_name, false); - anon_send( - central_models_data_actor, - ui::model_data::register_model_data_atom_v, + anon_mail( + ui::model_data::deregister_model_data_atom_v, toolbar_name, attr->uuid(), - caf::actor()); + caf::actor()) + .send(central_models_data_actor); } } } } void Module::redraw_viewport() { - anon_send(module_events_group_, playhead::redraw_viewport_atom_v); + for (auto &vp : connected_viewports_) { + anon_mail(playhead::redraw_viewport_atom_v).send(vp); + } } @@ -1489,6 +2055,7 @@ Attribute *Module::add_attribute( const utility::JsonStore &value, const utility::JsonStore &role_data) { Attribute *attr = nullptr; + if (role_data.contains("combo_box_options")) { // we need to do some specific type checking if the caller is trying @@ -1542,9 +2109,21 @@ Attribute *Module::add_attribute( attr = static_cast( add_string_attribute(title, title, value.get())); - } else if (value.is_object() || value.is_null()) { + } else if ( + value.is_array() && value.size() == 5 && value[0].is_string() && + value[0].get() == "colour") { - attr = static_cast(add_json_attribute(title, nlohmann::json("{}"))); + auto c = value.get(); + attr = static_cast(add_colour_attribute(title, title, c)); + + } else if (value.is_null()) { + + attr = static_cast(add_json_attribute(title, title, value.ref())); + attr->set_role_data(Attribute::Value, value); + + } else if (value.is_object() || value.is_array()) { + + attr = static_cast(add_json_attribute(title, title, value.ref())); attr->set_role_data(Attribute::Value, value); } else { @@ -1565,41 +2144,83 @@ Attribute *Module::add_attribute( void Module::expose_attribute_in_model_data( Attribute *attr, const std::string &model_name, const bool expose) { - attr->expose_in_ui_attrs_group(model_name, connect); + attr->expose_in_ui_attrs_group(model_name, expose); auto central_models_data_actor = self()->home_system().registry().template get( global_ui_model_data_registry); try { if (expose) { - anon_send( - central_models_data_actor, + anon_mail( ui::model_data::register_model_data_atom_v, model_name, utility::JsonStore(attr->as_json()), attr->uuid(), Attribute::role_name(Attribute::ToolbarPosition), - self()); + self()) + .send(central_models_data_actor); } else { // this removes the attribute from the model of name // 'model_name' - anon_send( - central_models_data_actor, - ui::model_data::register_model_data_atom_v, - model_name, - attr->uuid(), - self()); + anon_mail( + ui::model_data::deregister_model_data_atom_v, model_name, attr->uuid(), self()) + .send(central_models_data_actor); } } catch (std::exception &) { } } void Module::connect_to_viewport( - const std::string &viewport_name, const std::string &viewport_toolbar_name, bool connect) { - + const std::string &viewport_name, + const std::string &viewport_toolbar_name, + bool connect, + caf::actor viewport) { + + // note: we call connected_viewports_changed for the benefit of PlayheadActors. + // If the number of connected viewports has gone to zero, the playhead can + // disconnect from the UI. + // If the number of connected viewports has gone from zero to 1, the playhead + // can connect to the UI + + size_t n = connected_viewports_.size(); if (connect) { - connected_viewports_.insert(viewport_name); - } else if (connected_viewports_.find(viewport_name) != connected_viewports_.end()) { - connected_viewports_.erase(connected_viewports_.find(viewport_name)); + auto a = caf::actor_cast(self()); + + if (a) { + auto act_addr = caf::actor_cast(viewport); + if (auto sit = monitor_.find(act_addr); sit == std::end(monitor_)) { + monitor_[act_addr] = + a->monitor(viewport, [this, addr = viewport.address()](const error &) { + auto act_addr = caf::actor_cast(addr); + if (auto mit = monitor_.find(act_addr); mit != std::end(monitor_)) + monitor_.erase(mit); + + auto p = connected_viewports_.find(caf::actor_cast(addr)); + if (p != connected_viewports_.end()) { + connected_viewports_.erase(p); + if (connected_viewports_.empty()) connected_viewports_changed(connected_viewports_); + } + }); + } + } + + connected_viewport_names_.insert(viewport_name); + connected_viewports_.insert(viewport); + + } else if ( + connected_viewport_names_.find(viewport_name) != connected_viewport_names_.end()) { + + if (auto it = monitor_.find(caf::actor_cast(viewport)); + it != std::end(monitor_)) { + it->second.dispose(); + monitor_.erase(it); + } + + connected_viewport_names_.erase(connected_viewport_names_.find(viewport_name)); + connected_viewports_.erase(connected_viewports_.find(viewport)); + } + + if ((!n && connected_viewports_.size()) || (n && connected_viewports_.empty())) { + connected_viewports_changed(connected_viewports_); } for (const auto &toolbar_attr_id : attrs_in_toolbar_) { @@ -1610,7 +2231,11 @@ void Module::connect_to_viewport( } } -void Module::add_attribute(Attribute *attr) { attributes_.emplace_back(attr); } +void Module::add_attribute(Attribute *attr) { + attributes_.emplace_back(attr); + attr->set_role_data(module::Attribute::ModuleUuid, uuid(), false); + attr->set_owner(this); +} utility::JsonStore Module::public_state_data() { @@ -1623,6 +2248,135 @@ utility::JsonStore Module::public_state_data() { data["children"].push_back(attr->as_json()); } - // std::cerr << data.dump(2) << "\n"; + // //std::cerr << data.dump(2) << "\n"; return data; } + +void Module::register_ui_panel_qml( + const std::string &panel_name, + const std::string &qml_code, + const float position_in_menu, + const std::string &viewport_popout_button_icon, + const float &viewport_popout_button_position, + const utility::Uuid toggle_hotkey_id) { + + auto central_models_data_actor = self()->home_system().registry().template get( + global_ui_model_data_registry); + + utility::JsonStore data; + data["view_name"] = panel_name; + data["view_qml_source"] = qml_code; + data["position"] = position_in_menu; + /*anon_mail(ui::model_data::insert_rows_atom_v, + "views model", // the model called 'views model' is what's used to build the panels menu + "/", // add to root + data, + 0, // row + 1, // count + caf::actor()).send(central_models_data_actor);*/ + + scoped_actor sys{self()->home_system()}; + try { + + anon_mail( + ui::model_data::insert_rows_atom_v, + "view widgets", // the model called 'view widgets' is what's used to build the + // panels menu + "", // (path) add to root + 0, // row + 1, // count + data) + .send(central_models_data_actor); + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + if (!viewport_popout_button_icon.empty() && viewport_popout_button_position != -1.0f) { + + utility::JsonStore data; + data["view_name"] = panel_name; + data["icon_path"] = viewport_popout_button_icon; + data["view_qml_source"] = qml_code; + data["button_position"] = viewport_popout_button_position; + data["window_is_visible"] = false; + data["hotkey_uuid"] = toggle_hotkey_id; + + try { + + anon_mail( + ui::model_data::insert_rows_atom_v, + "popout windows", // the model called 'view widgets' is what's used to build the + // panels menu + "", // (path) add to root + 0, // row + 1, // count + data) + .send(central_models_data_actor); + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + } +} + +Attribute *Module::register_viewport_dockable_widget( + const std::string &widget_name, + const std::string &button_icon_qrc_path, + const std::string &button_tooltip, + const float button_position, + const bool enabled, + const std::string &left_right_dockable_widget_qml, + const std::string &top_bottom_dockable_widget_qml, + const utility::Uuid toggle_widget_visible_hotkey) { + + auto attr = new QmlCodeAttribute(widget_name, left_right_dockable_widget_qml); + attr->set_role_data(Attribute::IconPath, button_icon_qrc_path); + attr->set_role_data(Attribute::ToolbarPosition, button_position); + attr->set_role_data(Attribute::ToolTip, button_tooltip); + attr->set_role_data(Attribute::Activated, -1); + attr->set_role_data(Attribute::Enabled, enabled); + + if (!left_right_dockable_widget_qml.empty()) { + attr->set_role_data( + Attribute::LeftRightDockWidgetQmlCode, left_right_dockable_widget_qml); + } + if (!top_bottom_dockable_widget_qml.empty()) { + attr->set_role_data( + Attribute::TopBottomDockWidgetQmlCode, top_bottom_dockable_widget_qml); + } + + if (!toggle_widget_visible_hotkey.is_null()) { + attr->set_role_data(Attribute::HotkeyUuid, toggle_widget_visible_hotkey); + } + add_attribute(static_cast(attr)); + expose_attribute_in_model_data(attr, "dockable viewport toolboxes", true); + dock_widget_attributes_.insert(attr->uuid()); + return attr; +} + +void Module::register_singleton_qml(const std::string &qml_code) { + + auto central_models_data_actor = self()->home_system().registry().template get( + global_ui_model_data_registry); + + utility::JsonStore data; + data["source"] = qml_code; + + scoped_actor sys{self()->home_system()}; + try { + + anon_mail( + ui::model_data::insert_rows_atom_v, + "singleton items", // the model called 'singleton items' is what's used to build the + // singleton + "", // (path) add to root + 0, // row + 1, // count + data) + .send(central_models_data_actor); + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} \ No newline at end of file diff --git a/src/module/src/typed_attributes.cpp b/src/module/src/typed_attributes.cpp index b73efe738..c59495964 100644 --- a/src/module/src/typed_attributes.cpp +++ b/src/module/src/typed_attributes.cpp @@ -50,10 +50,10 @@ QmlCodeAttribute::QmlCodeAttribute(const std::string &title, const std::string & IntegerAttribute::IntegerAttribute( const std::string &title, const std::string &abbr_title, - const int value, - const int int_min, - const int int_max) - : TypeAttribute(title, abbr_title, value, type_name(Attribute::IntegerAttribute)) { + const int64_t value, + const int64_t int_min, + const int64_t int_max) + : TypeAttribute(title, abbr_title, value, type_name(Attribute::IntegerAttribute)) { role_data_[IntegerMin] = int_min; role_data_[IntegerMax] = int_max; } @@ -65,7 +65,22 @@ ColourAttribute::ColourAttribute( : TypeAttribute( title, abbr_title, value, type_name(Attribute::ColourAttribute)) {} +Vec4fAttribute::Vec4fAttribute( + const std::string &title, const std::string &abbr_title, const Imath::V4f &value) + : TypeAttribute( + title, abbr_title, value, type_name(Attribute::Vec4fAttribute)) {} + JsonAttribute::JsonAttribute( const std::string &title, const std::string &abbr_title, const nlohmann::json &value) : TypeAttribute( - title, abbr_title, value, type_name(Attribute::JsonAttribute)) {} \ No newline at end of file + title, abbr_title, value, type_name(Attribute::JsonAttribute)) {} + +IntegerVecAttribute::IntegerVecAttribute( + const std::string &title, const std::string &abbr_title, const std::vector &value) + : TypeAttribute>( + title, abbr_title, value, type_name(Attribute::IntegerAttribute)) {} + +FloatVectorAttribute::FloatVectorAttribute( + const std::string &title, const std::string &abbr_title, const std::vector &value) + : TypeAttribute>( + title, abbr_title, value, type_name(Attribute::FloatVectorAttribute)) {} diff --git a/src/module/test/CMakeLists.txt b/src/module/test/CMakeLists.txt index 2c62b7c33..74836cb18 100644 --- a/src/module/test/CMakeLists.txt +++ b/src/module/test/CMakeLists.txt @@ -3,7 +3,7 @@ include(CTest) SET(LINK_DEPS xstudio::module xstudio::global - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/playhead/src/CMakeLists.txt b/src/playhead/src/CMakeLists.txt index edf1dea48..106976492 100644 --- a/src/playhead/src/CMakeLists.txt +++ b/src/playhead/src/CMakeLists.txt @@ -3,7 +3,8 @@ SET(LINK_DEPS xstudio::global_store xstudio::utility xstudio::module - caf::core + xstudio::media + CAF::core ) -create_component(playhead 0.1.0 "${LINK_DEPS}") +create_component(playhead ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/playhead/src/edit_list_actor.cpp b/src/playhead/src/edit_list_actor.cpp deleted file mode 100644 index 3196ab23c..000000000 --- a/src/playhead/src/edit_list_actor.cpp +++ /dev/null @@ -1,415 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/broadcast/broadcast_actor.hpp" -#include "xstudio/playhead/edit_list_actor.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" -#include "xstudio/utility/uuid.hpp" - -using namespace xstudio; -using namespace xstudio::playhead; -using namespace xstudio::utility; -using namespace xstudio::timeline; -using namespace caf; - -EditListActor::EditListActor( - caf::actor_config &cfg, - const std::string &name, - const std::vector &ordered_media_sources, - const media::MediaType mt) - : caf::event_based_actor(cfg), source_actors_(ordered_media_sources), frames_offset_(0) { - - spdlog::debug("Created EditListActor {}", name); - print_on_exit(this, "EditListActor"); - - event_group_ = spawn(this); - link_to(event_group_); - - // this bit is blocking ... we are getting the edit list from each source and building - // a linear edit of these into our 'edit_list_' object. We're also getting the uuids - caf::scoped_actor sys(system()); - for (auto media_source : ordered_media_sources) { - - // we try and get the edit list for the desired media type ... however, if - // we can't provide one (say we want MT_IMAGE and the media_source only - // provides MT_AUDIO sources) we retry and continue. This means that the - // playhead can 'play' a source that has no video and audio only - the audio - // only source will provide empty video frames so playback can still happen. - try { - auto edl = request_receive( - *sys, media_source, media::get_edit_list_atom_v, mt, Uuid()); - edit_list_.extend(edl); - } catch (...) { - try { - auto edl = request_receive( - *sys, - media_source, - media::get_edit_list_atom_v, - mt == media::MT_AUDIO ? media::MT_IMAGE : media::MT_AUDIO, - Uuid()); - edit_list_.extend(edl); - } catch (...) { - } - } - - try { - - auto uuid = - request_receive(*sys, media_source, utility::uuid_atom_v); - - source_actors_per_uuid_[uuid] = media_source; - join_event_group(this, media_source); - } catch (std::exception &e) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, e.what()); - } - } - behavior_.assign( - [=](utility::event_atom, utility::change_atom) { - // send(event_group_, utility::event_atom_v, utility::change_atom_v); - }, - - [=](utility::event_atom, - media::add_media_source_atom, - const utility::UuidActorVector &uav) { - send(event_group_, utility::event_atom_v, media::add_media_source_atom_v, uav); - }, - - [=](utility::event_atom, utility::last_changed_atom, const time_point &) { - send(event_group_, utility::event_atom_v, utility::change_atom_v); - }, - [=](utility::event_atom, - playlist::reflag_container_atom, - const utility::Uuid &, - const std::tuple &) {}, - [=](utility::event_atom, - bookmark::bookmark_change_atom, - const utility::Uuid &bookmark_uuid) { - send( - event_group_, - utility::event_atom_v, - bookmark::bookmark_change_atom_v, - bookmark_uuid); - }, - [=](utility::event_atom, - media::current_media_source_atom, - UuidActor &, - const media::MediaType) { - send(event_group_, utility::event_atom_v, utility::change_atom_v); - }, - - [=](utility::event_atom, media::media_status_atom, const media::MediaStatus ms) {}, - - [=](const error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }, - - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) {}, - - [=](timeline::duration_atom, const timebase::flicks &d) -> bool { - if (d != timebase::k_flicks_zero_seconds) { - spdlog::warn( - "{} {}", __PRETTY_FUNCTION__, "Attempt to reset duration on EditListActor"); - } - return false; - }, - - [=](duration_flicks_atom, - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate) -> result { - return edit_list_.duration_flicks(tsm, override_rate); - }, - - [=](duration_frames_atom, - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate) -> result { - return (int)edit_list_.duration_frames(tsm, override_rate); - }, - - [=](media::get_edit_list_atom, const Uuid & /*uuid*/) -> utility::EditList { - return edit_list_; - }, - - [=](media::get_media_pointer_atom atom, const int logical_frame) { - delegate(caf::actor_cast(this), atom, media::MT_IMAGE, logical_frame); - }, - - [=](media::get_media_pointer_atom, - const media::MediaType media_type, - const int logical_frame) -> result { - auto rp = make_response_promise(); - - try { - int clip_frame; - EditListSection section = - edit_list_.media_frame(logical_frame - frames_offset_, clip_frame); - - if (source_actors_per_uuid_.find(section.media_uuid_) != - source_actors_per_uuid_.end()) { - request( - source_actors_per_uuid_[section.media_uuid_], - infinite, - media::get_media_pointer_atom_v, - media_type, - clip_frame) - .then( - [=](const media::AVFrameID &mp) mutable { rp.deliver(mp); }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - } else { - rp.deliver(media::AVFrameID()); - } - } catch (std::exception &e) { - rp.deliver(make_error(xstudio_error::error, e.what())); - } - return rp; - }, - - [=](media::get_media_pointers_atom, - const media::MediaType media_type, - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate) -> result { - auto rp = make_response_promise(); - auto result = std::make_shared(); - recursive_deliver_all_media_pointers( - tsm, override_rate, media_type, 0, timebase::flicks(0), result, rp); - return rp; - }, - - [=](media::source_offset_frames_atom) -> int { return 0; }, - - [=](media::source_offset_frames_atom, const int o) -> bool { - if (o) { - - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - "Attempt to set retime offset on EditListActor which can't be retimed " - "directly."); - } - return true; - }, - - [=](playhead::flicks_to_logical_frame_atom, - const timebase::flicks flicks, - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate) -> result { - // convert between type limits - if (flicks == - timebase::flicks(std::numeric_limits::lowest())) - return std::numeric_limits::lowest(); - if (flicks == timebase::flicks(std::numeric_limits::max())) - return std::numeric_limits::max(); - - try { - return edit_list_.logical_frame(tsm, flicks, override_rate); - } catch (std::exception &e) { - return make_error(xstudio_error::error, e.what()); - } - }, - - [=](playlist::get_media_uuid_atom) -> utility::Uuid { - if (source_actors_per_uuid_.size() == 1) { - return source_actors_per_uuid_.begin()->first; - } else { - return utility::Uuid(); - } - }, - - [=](playhead::logical_frame_to_flicks_atom, - const int logical_frame, - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate) -> result { - // convert between type limits - if (logical_frame == std::numeric_limits::lowest()) - return timebase::flicks(std::numeric_limits::lowest()); - if (logical_frame == std::numeric_limits::max()) - return timebase::flicks(std::numeric_limits::max()); - - try { - return edit_list_.flicks_from_frame(tsm, logical_frame, override_rate); - } catch (std::exception &e) { - return make_error(xstudio_error::error, e.what()); - } - }, - - [=](playhead::media_atom, const int logical_frame) -> result { - auto rp = make_response_promise(); - try { - int clip_frame; - EditListSection section = - edit_list_.media_frame(logical_frame - frames_offset_, clip_frame); - if (source_actors_per_uuid_.find(section.media_uuid_) != - source_actors_per_uuid_.end()) { - rp.deliver(source_actors_per_uuid_[section.media_uuid_]); - } else { - rp.deliver(caf::actor()); - } - } catch (std::exception &e) { - if (!strcmp(e.what(), "No frames left") && source_actors_per_uuid_.size()) { - rp.deliver(source_actors_per_uuid_.begin()->second); - } else { - rp.deliver(make_error(xstudio_error::error, e.what())); - } - } - return rp; - }, - - [=](playhead::skip_through_sources_atom, - const int skip_by, - const int ref_frame) -> result { - try { - - EditListSection section = - edit_list_.skip_sections(ref_frame - frames_offset_, skip_by); - - return section.media_uuid_; - - } catch (std::exception &e) { - return make_error(xstudio_error::error, e.what()); - } - }, - - [=](rate_atom, const int logical_frame) -> result { - try { - return edit_list_.frame_rate_at_frame(logical_frame); - } catch (std::exception &e) { - return make_error(xstudio_error::error, e.what()); - } - }, - - [=](utility::event_atom, timeline::item_atom, const utility::JsonStore &changes, bool) { - // ignoring timeline events - }, - - [=](utility::get_event_group_atom) -> caf::actor { return event_group_; }); -} - -caf::actor EditListActor::media_source_actor(const utility::Uuid &source_uuid) { - auto p = source_actors_per_uuid_.find(source_uuid); - if (p != source_actors_per_uuid_.end()) { - return p->second; - } - return caf::actor(); -} - -void EditListActor::recursive_deliver_all_media_pointers( - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate, - const media::MediaType media_type, - const int clip_index, - const timebase::flicks clip_start_time_point, - std::shared_ptr result, - caf::typed_response_promise rp) { - - // this function is crucial. It works by recursively self calling until the - // 'clip_index' is greater than the number of clips (incrementind clip_index) - // in this edit list. - // We use it to get a full list of media pointers for this edit list from - // start to finish. A 'AVFrameID' is a struct that contains all the - // information we need to read/retrieve a frame of video/audio data. Along - // with the media pointers we also get the associated timepoint for each - // media pointer, that is where it lies on the timeline (or when they should - // be displayed if we start playback from time=0) - - if (clip_index >= (int)edit_list_.size()) { - - // we're done, we've exhausted the number of clips. Now we need something - // funky ... we meed to add a timepoint at the end so we know when the - // last frame's duration runs out. To understand this, imagine we have - // a source with a single frame. It shows at time=0 but it should be - // on screen for 1/fps seconds ... so we add a timepoint with a null - // media pointer at this time. Note that we increment time_point for - // every frame that's been added to result, so it time_point is already - // where we need it - (*result)[clip_start_time_point].reset(); - - rp.deliver(*result); - return; - } - - // the TP of the first frame in the current clip - timebase::flicks time_point = clip_start_time_point; - - // get a copy of the current clip - const auto clip = edit_list_.section_list()[clip_index]; - - const utility::Timecode tc = clip.timecode_; - - // number of logical frames in the clip - const int num_clip_frames = clip.frame_rate_and_duration_.frames( - tsm == TimeSourceMode::FIXED ? override_rate : FrameRate()); - - // get the media actor (or other source actor type) for the clip - const utility::Uuid &uuid = clip.media_uuid_; - caf::actor media_source = media_source_actor(uuid); - - if (media_source) { - - // now we get media pointers for the number of frames in the clip - // (bear in mind, the media source might have more or fewer frames - // than our 'clip'- the clip is a time slice of the media source's - // total possible duration - - request( - media_source, - infinite, - media::get_media_pointers_atom_v, - media_type, - media::LogicalFrameRanges({{0, num_clip_frames - 1}}), - override_rate) - .await( - [=](const media::AVFrameIDs &mps) mutable { - if ((int)mps.size() != num_clip_frames) { - rp.deliver(make_error( - xstudio_error::error, - "EditListActor::recursive_deliver_all_media_pointers media " - "pointers returned by media source not matching requested number " - "of frames.")); - return; - } - - for (int f = 0; f < num_clip_frames; f++) { - const int playhead_logical_frame = result->size(); - (*result)[time_point] = mps[f]; - const_cast((*result)[time_point].get()) - ->playhead_logical_frame_ = playhead_logical_frame; - const_cast(mps[f].get())->timecode_ = tc + f; - time_point += tsm == TimeSourceMode::FIXED - ? override_rate - : clip.frame_rate_and_duration_.rate(); - } - - recursive_deliver_all_media_pointers( - tsm, override_rate, media_type, clip_index + 1, time_point, result, rp); - }, - [=](error &err) mutable { - // something is wrong with the media source ... let's print it's - // name and the error and continue - - auto blank_frame = media::make_blank_frame(media_type); - auto *m_ptr = const_cast(blank_frame.get()); - m_ptr->error_ = to_string(err); - - for (int f = 0; f < num_clip_frames; f++) { - (*result)[time_point] = blank_frame; - time_point += tsm == TimeSourceMode::FIXED - ? override_rate - : clip.frame_rate_and_duration_.rate(); - } - - recursive_deliver_all_media_pointers( - tsm, override_rate, media_type, clip_index + 1, time_point, result, rp); - }); - } else { - - for (int f = 0; f < num_clip_frames; f++) { - (*result)[time_point] = media::make_blank_frame(media_type); - time_point += tsm == TimeSourceMode::FIXED ? override_rate - : clip.frame_rate_and_duration_.rate(); - } - - // shouldn't we deliver on the promise... - recursive_deliver_all_media_pointers( - tsm, override_rate, media_type, clip_index + 1, time_point, result, rp); - } -} diff --git a/src/playhead/src/playhead.cpp b/src/playhead/src/playhead.cpp index d34a722c2..c1d4fc0a4 100644 --- a/src/playhead/src/playhead.cpp +++ b/src/playhead/src/playhead.cpp @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include "xstudio/playhead/playhead.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/atoms.hpp" @@ -14,8 +13,7 @@ using namespace xstudio::playhead; using namespace xstudio::utility; PlayheadBase::PlayheadBase(const std::string &name, const utility::Uuid uuid) - : Container(name, "PlayheadBase", uuid), - Module(name, uuid), + : Module(name, uuid), playhead_rate_(timebase::k_flicks_24fps), position_(0), loop_start_(timebase::k_flicks_low), @@ -25,32 +23,16 @@ PlayheadBase::PlayheadBase(const std::string &name, const utility::Uuid uuid) add_attributes(); } -PlayheadBase::PlayheadBase(const JsonStore &jsn) - : Container(static_cast(jsn["container"])), - Module("PlayheadBase"), - play_rate_mode_(jsn["play_rate_mode"]), - playhead_rate_(timebase::k_flicks_24fps), - position_(jsn["position"]), - loop_start_(jsn["loop_start"]), - loop_end_(jsn["loop_end"]) -// load, unwanted behaviour -{ - add_attributes(); - if (jsn.find("module") != jsn.end()) { - Module::deserialise(jsn["module"]); - } - set_loop(jsn["loop"]); -} +PlayheadBase::~PlayheadBase() {} void PlayheadBase::add_attributes() { - compare_mode_ = add_string_choice_attribute("Compare", "Cpm", compare_mode_names); - compare_mode_->set_value("Off"); - compare_mode_->set_role_data( - module::Attribute::InitOnlyPreferencePath, "/ui/qml/default_playhead_compare_mode"); - image_source_ = add_string_choice_attribute("Source", "Src", "", {}, {}); + image_source_name_ = add_string_attribute("Source Name", "Src", ""); + + image_stream_ = add_string_choice_attribute("Stream", "Strm", "", {}, {}); + audio_source_ = add_string_choice_attribute("Audio Source", "Aud Src", "", {}, {}); velocity_ = add_float_attribute("Rate", "Rate", 1.0f, 0.1f, 16.0f, 0.05f); @@ -60,22 +42,19 @@ void PlayheadBase::add_attributes() { "Set playback speed. Double-click to toggle between last set value and default (1.0)");*/ - velocity_multiplier_ = - add_float_attribute("Velocity Multiplier", "FFWD", 1.0f, 1.0f, 16.0f, 1.0f); + velocity_multiplier_ = add_integer_attribute("Velocity Multiplier", "FFWD", 1, 1, 16); forward_ = add_boolean_attribute("forward", "fwd", true); playing_ = add_boolean_attribute("playing", "playing", false); auto_align_mode_ = - add_string_choice_attribute("Auto Align", "Auto Align", auto_align_mode_names); + add_string_choice_attribute("Auto Align", "Algn.", auto_align_mode_names); - auto_align_mode_->set_value("Alignment Off"); - auto_align_mode_->set_role_data( - module::Attribute::PreferencePath, "/core/playhead/align_mode"); + auto_align_mode_->set_value("Off"); max_compare_sources_ = - add_integer_attribute("Max Compare Sources", "Max Compare Sources", 9, 2, 32); + add_integer_attribute("Max Compare Sources", "Max Compare Sources", 16, 4, 32); max_compare_sources_->set_role_data( module::Attribute::PreferencePath, "/core/playhead/max_compare_sources"); @@ -90,94 +69,124 @@ void PlayheadBase::add_attributes() { viewport_scrub_sensitivity_->set_role_data( module::Attribute::PreferencePath, "/ui/viewport/viewport_scrub_sensitivity"); - compare_mode_->set_role_data(module::Attribute::Groups, nlohmann::json{"playhead"}); - velocity_->set_role_data(module::Attribute::Groups, nlohmann::json{"playhead"}); - - image_source_->set_role_data( - module::Attribute::Groups, nlohmann::json{"image_source", "playhead"}); - audio_source_->set_role_data( - module::Attribute::Groups, nlohmann::json{"audio_source", "playhead"}); - - playing_->set_role_data(module::Attribute::Groups, nlohmann::json{"playhead"}); - forward_->set_role_data(module::Attribute::Groups, nlohmann::json{"playhead"}); - auto_align_mode_->set_role_data( - module::Attribute::Groups, nlohmann::json{"playhead_align_mode"}); + module::Attribute::UIDataModels, nlohmann::json{"playhead_align_mode"}); velocity_->set_role_data(module::Attribute::ToolbarPosition, 3.0f); - compare_mode_->set_role_data(module::Attribute::ToolbarPosition, 9.0f); velocity_->set_role_data(module::Attribute::DefaultValue, 1.0f); - compare_mode_->set_role_data( - module::Attribute::ToolTip, - "Select viewer Compare Mode. Shift-select multiple Media items to compare them."); - - play_hotkey_ = register_hotkey( - int(' '), ui::NoModifier, "Start/Stop Playback", "This hotkey will toggle playback"); + loop_mode_ = add_string_choice_attribute( + "Loop Mode", "Loop Mode", "Loop", utility::map_key_to_vec(loop_modes_)); - play_forwards_hotkey_ = register_hotkey( - int('L'), - ui::NoModifier, - "Play Forwards, Accellerate", - "This hotkey starts forward playback, repeated presses thereon increase the playback " - "speed"); - - play_backwards_hotkey_ = register_hotkey( - int('J'), - ui::NoModifier, - "Play Backwards, Accellerate", - "This hotkey starts backward playback, repeated presses thereon increase the playback " - "speed"); - - stop_play_hotkey_ = - register_hotkey(int('K'), ui::NoModifier, "Stop Playback", "Stops Playback"); - - reset_hotkey_ = register_hotkey( - int('R'), - ui::ControlModifier, - "Reset PlayheadBase", - "Resets the playhead properties, to normal playback speed and forwards playing"); - - - loop_mode_ = add_integer_attribute("Loop Mode", "Loop Mode", LM_LOOP, 0, 4); loop_start_frame_ = add_integer_attribute("Loop Start Frame", "Loop Start Frame", 0); loop_end_frame_ = add_integer_attribute("Loop End Frame", "Loop End Frame", 0); playhead_logical_frame_ = add_integer_attribute("Logical Frame", "Logical Frame", 0); + playhead_position_seconds_ = + add_float_attribute("Position Seconds", "Position Seconds", 0.0); + playhead_position_flicks_ = add_integer_attribute("Position Flicks", "Position Flicks", 0); + + playhead_rate_attr_ = add_float_attribute("Frame Rate", "Frame Rate", 24.0); playhead_media_logical_frame_ = add_integer_attribute("Media Logical Frame", "Media Logical Frame", 0); playhead_media_frame_ = add_integer_attribute("Media Frame", "Media Frame", 0); duration_frames_ = add_integer_attribute("Duration Frames", "Duration Frames", 0); - current_source_frame_timecode_ = - add_string_attribute("Current Source Timecode", "Current Source Timecode", ""); + duration_seconds_ = add_float_attribute("Duration Seconds", "Duration Seconds", 0.0); + cached_frames_ = add_json_attribute("Cached Frames"); + bookmarked_frames_ = add_int_vec_attribute("Bookmarked Frames"); + + media_transition_frames_ = add_int_vec_attribute("Media Transition Frames"); + + source_alignment_values_ = add_int_vec_attribute("Source Alignment Frames"); + + current_frame_timecode_ = add_string_attribute("Timecode", "Timecode", ""); + + current_frame_timecode_as_frame_ = + add_integer_attribute("Timecode As Frame", "Timecode As Frame", 0); + current_media_uuid_ = add_string_attribute("Current Media Uuid", "Current Media Uuid", ""); current_media_source_uuid_ = add_string_attribute("Current Media Source Uuid", "Current Media Source Uuid", ""); - do_looping_ = add_boolean_attribute("Do Looping", "Do Looping", true); + loop_range_enabled_ = + add_boolean_attribute("Enable Loop Range", "Enable Loop Range", false); + + user_is_frame_scrubbing_ = + add_boolean_attribute("User Is Frame Scrubbing", "User Is Frame Scrubbing", false); + + pinned_source_mode_ = + add_boolean_attribute("Pinned Source Mode", "Pinned Source Mode", true); // this attr tracks the global 'Audio Delay Millisecs' preference audio_delay_millisecs_ = - add_integer_attribute("Audio Delay Millisecs", "Audio Delay Millisecs", 0, -1000, 1000); + add_integer_attribute("Audio Delay Millisecs", "Aud. Delay", 0, -1000, 1000); audio_delay_millisecs_->set_role_data( module::Attribute::PreferencePath, "/core/audio/audio_latency_millisecs"); + audio_delay_millisecs_->set_role_data(module::Attribute::ToolbarPosition, 20.0f); + audio_delay_millisecs_->set_role_data(module::Attribute::DefaultValue, 0); + key_playhead_index_ = add_integer_attribute("Key Playhead Index", "Key Playhead Index", 0); + num_sub_playheads_ = add_integer_attribute("Num Sub Playheads", "Num Sub Playheads", 0); + click_to_toggle_play_ = + add_boolean_attribute("Click to Toggle Play", "Play on Click", false); + click_to_toggle_play_->set_role_data( + module::Attribute::PreferencePath, "/ui/qml/click_to_toggle_play"); + + source_offset_frames_ = + add_integer_attribute("Source Offset Frames", "Source Offset Frames", 0); + + connect_to_ui_attr_ = + add_boolean_attribute("Force Connect To UI", "Force Connect To UI", false); + + timeline_mode_ = add_boolean_attribute("Timeline Mode", "Timeline Mode", false); + + // Compare mode needs custom QML code for instatiation into the toolbar as + // the choices are determined through viewport layout plugins + compare_mode_ = add_string_attribute("Compare", "Compare", "Off"); + compare_mode_->set_tool_tip("Access compare mode controls"); + compare_mode_->set_role_data(module::Attribute::Type, "QmlCode"); + compare_mode_->set_role_data( + module::Attribute::QmlCode, + R"(import xStudio 1.0 + XsViewerCompareModeButton {})"); + compare_mode_->set_role_data(module::Attribute::ToolbarPosition, 9.0f); } JsonStore PlayheadBase::serialise() const { - JsonStore jsn; - jsn["container"] = Container::serialise(); - jsn["position"] = position_.count(); - jsn["loop"] = loop_mode_->value(); - jsn["play_rate_mode"] = play_rate_mode_; - jsn["loop_start"] = loop_start_.count(); - jsn["loop_end"] = loop_end_.count(); - jsn["use_loop_range"] = use_loop_range(); - jsn["module"] = Module::serialise(); + JsonStore jsn; + jsn["name"] = Module::name(); + jsn["velocity"] = velocity_->value(); + jsn["position"] = position_.count(); + jsn["compare_mode"] = compare_mode_->value(); + jsn["auto_align_mode"] = auto_align_mode_->value(); + jsn["source_alignment_values"] = source_alignment_values_->value(); + jsn["loop_range_enabled"] = loop_range_enabled_->value(); + jsn["loop_mode"] = loop_mode_->value(); + jsn["loop_start"] = loop_start_.count(); + jsn["loop_end"] = loop_end_.count(); return jsn; } +void PlayheadBase::deserialise(const JsonStore &jsn) { + + if (jsn.is_null()) + return; + velocity_->set_value(jsn.value("velocity", velocity_->value())); + compare_mode_->set_value(jsn.value("compare_mode", compare_mode_->value()), false); + auto_align_mode_->set_value(jsn.value("auto_align_mode", auto_align_mode_->value()), false); + source_alignment_values_->set_value( + jsn.value("source_alignment_values", source_alignment_values_->value())); + position_ = timebase::flicks(jsn.value("position", position_.count())); + loop_start_ = timebase::flicks(jsn.value("loop_start", loop_start_.count())); + loop_end_ = timebase::flicks(jsn.value("loop_end", loop_end_.count())); + loop_mode_->set_value(jsn.value("loop_mode", loop_mode_->value())); + loop_range_enabled_->set_value( + jsn.value("loop_range_enabled", loop_range_enabled_->value())); + + deserialised_ = true; +} + PlayheadBase::OptionalTimePoint PlayheadBase::play_step() { const auto now = utility::clock::now(); @@ -249,6 +258,102 @@ PlayheadBase::OptionalTimePoint PlayheadBase::play_step() { return {}; } +void PlayheadBase::register_hotkeys() { + + play_hotkey_ = register_hotkey( + int(' '), + ui::NoModifier, + "Start/Stop Playback", + "This hotkey will toggle playback", + false, + "Playback"); + + play_forwards_hotkey_ = register_hotkey( + int('L'), + ui::NoModifier, + "Play Forwards, Accellerate", + "This hotkey starts forward playback, repeated presses thereon increase the playback " + "speed", + false, + "Playback"); + + play_backwards_hotkey_ = register_hotkey( + int('J'), + ui::NoModifier, + "Play Backwards, Accellerate", + "This hotkey starts backward playback, repeated presses thereon increase the playback " + "speed", + false, + "Playback"); + + stop_play_hotkey_ = register_hotkey( + int('K'), ui::NoModifier, "Stop Playback", "Stops Playback", false, "Playback"); + + toggle_loop_range_ = register_hotkey( + int('P'), + ui::NoModifier, + "Toggle Loop Range", + "Use this hotkey to turn on/off the loop range in/out points that let you loop on a " + "specified range in the timeline.", + false, + "Playback"); + + set_loop_in_ = register_hotkey( + int('I'), + ui::NoModifier, + "Set Loop to In Frame", + "Hit this hotkey to set the 'in' frame of the loop range.", + false, + "Playback"); + + set_loop_out_ = register_hotkey( + int('O'), + ui::NoModifier, + "Set Loop to Out Frame", + "Hit this hotkey to set the 'out' frame of the loop range.", + false, + "Playback"); + + step_forward_ = register_hotkey( + "Right", "Step Forward", "Steps the playhead forward by one frame.", false, "Playback"); + + step_backward_ = register_hotkey( + "Left", + "Step Backward", + "Steps the playhead backward by one frame.", + false, + "Playback"); + + jump_to_first_frame_ = register_hotkey( + "Home", + "Jump to First Frame", + "Jump the playhead to the first in the playhead range.", + false, + "Playback"); + + jump_to_last_frame_ = register_hotkey( + "End", + "Jump to Last Frame", + "Jump the playhead to the very last frame in the playhead range.", + false, + "Playback"); + + cycle_image_layer_up_ = register_hotkey( + "Ctrl+Up", + "Cycle Image Layer / EXR Part (Up)", + "Cycle backwards through image layers (EXR parts) for the current on-screen source", + false, + "Playback"); + + cycle_image_layer_down_ = register_hotkey( + "Ctrl+Down", + "Cycle Image Layer / EXR Part (Down)", + "Cycle forwards through image layers (EXR parts) for the current on-screen source", + false, + "Playback"); +} + + timebase::flicks PlayheadBase::adjusted_position() const { if (!playing()) return position_; @@ -262,6 +367,10 @@ timebase::flicks PlayheadBase::adjusted_position() const { const timebase::flicks out = use_loop_range() and loop_end_ != timebase::k_flicks_max ? loop_end_ : duration_; + if (out <= in) { + return in; + } + // somewhat fiddly - we are advancing the position by 'delta' but what if // this wraps through the in/out points ... and what if it wraps more than // the whole duration of the loop in/out region? @@ -380,6 +489,7 @@ timebase::flicks PlayheadBase::adjusted_position() const { return position_ + delta; } + void PlayheadBase::set_playing(const bool play) { if (play != playing()) { @@ -398,10 +508,10 @@ void PlayheadBase::set_playing(const bool play) { if (forward()) { if (position_ == out) - position_ = in; + set_position(in); } else { if (position_ == in) - position_ = out; + set_position(out); } } @@ -467,13 +577,16 @@ timebase::flicks PlayheadBase::clamp_timepoint_to_loop_range(const timebase::fli return rt; } -void PlayheadBase::set_position(const timebase::flicks p) { position_ = p; } +void PlayheadBase::set_position(const timebase::flicks p) { + position_ = p; + position_set_tp_ = utility::clock::now(); +} bool PlayheadBase::set_use_loop_range(const bool use_loop_range) { bool position_changed = false; if (this->use_loop_range() != use_loop_range) { - do_looping_->set_value(use_loop_range); + loop_range_enabled_->set_value(use_loop_range); if (use_loop_range) { if (position_ < loop_start_) { set_position(loop_start_); @@ -488,6 +601,7 @@ bool PlayheadBase::set_use_loop_range(const bool use_loop_range) { } bool PlayheadBase::set_loop_start(const timebase::flicks loop_start) { + loop_start_ = loop_start; bool position_changed = false; if (loop_end_ < loop_start_) { @@ -499,6 +613,11 @@ bool PlayheadBase::set_loop_start(const timebase::flicks loop_start) { set_position(loop_start_); position_changed = true; } + + if (loop_start_ != timebase::k_flicks_low) { + set_use_loop_range(true); + } + return position_changed; } @@ -515,6 +634,11 @@ bool PlayheadBase::set_loop_end(const timebase::flicks loop_end) { set_position(loop_end_); position_changed = true; } + + if (loop_end_ != timebase::k_flicks_max) { + set_use_loop_range(true); + } + return position_changed; } @@ -545,74 +669,75 @@ AutoAlignMode PlayheadBase::auto_align_mode() const { return rt; } -CompareMode PlayheadBase::compare_mode() const { - CompareMode rt = CM_OFF; - const std::string cmpm = compare_mode_->value(); +void PlayheadBase::set_assembly_mode(const AssemblyMode mode) { assembly_mode_ = mode; } - for (auto opt : compare_mode_names) { - if (std::get<1>(opt) == cmpm) - rt = std::get<0>(opt); +void PlayheadBase::set_auto_align_mode(const AutoAlignMode mode) { + for (auto &a : auto_align_mode_names) { + if (std::get<0>(a) == mode) { + auto_align_mode_->set_value(std::get<1>(a)); + break; + } } - return rt; } -void PlayheadBase::set_compare_mode(const CompareMode mode) { - std::string compare_str; - - for (auto opt : compare_mode_names) { - if (std::get<0>(opt) == mode) - compare_str = std::get<1>(opt); - } - - compare_mode_->set_value(compare_str); +void PlayheadBase::disconnect_from_ui() { + set_playing(false); + Module::disconnect_from_ui(); } bool PlayheadBase::pointer_event(const ui::PointerEvent &e) { bool used = false; + if (e.type() == ui::EventType::ButtonDown && e.buttons() == ui::Signature::Button::Left) { - if (e.type() == ui::Signature::EventType::ButtonDown && - e.buttons() == ui::Signature::Button::Left) { - + click_timepoint_ = utility::clock::now(); drag_start_x_ = e.x(); drag_start_playhead_position_ = position_; used = true; was_playing_when_scrub_started_ = playing(); set_playing(false); + user_is_frame_scrubbing_->set_value(true); - } else if ( - e.type() == ui::Signature::EventType::Drag && - e.buttons() == ui::Signature::Button::Left) { - - if (e.x() < e.width() and e.x() >= 0) { + } else if (e.type() == ui::EventType::Drag && e.buttons() == ui::Signature::Button::Left) { - float delta_x = e.x() - drag_start_x_; - auto new_position = drag_start_playhead_position_; - int nb_frames = abs(round(delta_x * viewport_scrub_sensitivity_->value() / 20)); - - if (delta_x < 0.0 and drag_start_playhead_position_.count() and drag_start_x_) { - new_position -= nb_frames * playhead_rate_; - } else if (delta_x > 0.0 and drag_start_x_ != e.width() - 1) { - new_position += nb_frames * playhead_rate_; - } + float delta_x = e.x() - drag_start_x_; + auto new_position = drag_start_playhead_position_; + int nb_frames = abs(round(delta_x * viewport_scrub_sensitivity_->value() / 20)); - // clamp to duration. - new_position = - max(timebase::k_flicks_zero_seconds, - min(new_position, duration_ - playhead_rate_.to_flicks())); - if (self()) - anon_send(self(), scrub_frame_atom_v, new_position); + if (delta_x < 0.0 and drag_start_playhead_position_.count() and drag_start_x_) { + new_position -= nb_frames * playhead_rate_.to_flicks(); + } else if (delta_x > 0.0 and drag_start_x_ != e.width() - 1) { + new_position += nb_frames * playhead_rate_.to_flicks(); } + // clamp to duration. + new_position = + max(timebase::k_flicks_zero_seconds, + min(new_position, duration_ - playhead_rate_.to_flicks())); + if (self()) + anon_mail(jump_atom_v, new_position).send(self()); + used = true; - } else if (e.type() == ui::Signature::EventType::ButtonRelease) { + } else if (e.type() == ui::EventType::ButtonRelease) { + + const auto milliseconds_since_press = + std::chrono::duration_cast( + utility::clock::now() - click_timepoint_) + .count(); + + if (milliseconds_since_press < 200 && click_to_toggle_play_->value()) { + // it was a quick click, and 'click_to_toggle_play_' is on ... + set_playing(!was_playing_when_scrub_started_); - if (was_playing_when_scrub_started_ && restore_play_state_after_scrub_->value()) { + } else if ( + was_playing_when_scrub_started_ && restore_play_state_after_scrub_->value()) { set_playing(true); was_playing_when_scrub_started_ = false; } + + user_is_frame_scrubbing_->set_value(false); } return used; @@ -624,7 +749,7 @@ void PlayheadBase::play_faster(const bool forwards) { set_forward(forwards); velocity_multiplier_->set_value(1.0f); if (self()) - anon_send(self(), play_atom_v, true); + anon_mail(play_atom_v, true).send(self()); } else if (forwards != forward_->value()) { forward_->set_value(forwards); velocity_multiplier_->set_value(1.0f); @@ -634,7 +759,15 @@ void PlayheadBase::play_faster(const bool forwards) { } void PlayheadBase::hotkey_pressed( - const utility::Uuid &hotkey_uuid, const std::string &context) { + const utility::Uuid &hotkey_uuid, const std::string &context, const std::string &window) { + + // If the context starts with 'viewport' the hotkey was hit while a viewport + // had mouse focus. If that viewport is NOT attached to this playhead we + // ignore the hotkey + if (context.find("viewport") == 0 && + active_viewports_.find(context) == active_viewports_.end()) { + return; + } if (hotkey_uuid == play_hotkey_) { forward_->set_value(true); @@ -645,17 +778,24 @@ void PlayheadBase::hotkey_pressed( play_faster(false); } else if (hotkey_uuid == stop_play_hotkey_) { set_playing(false); - } else if (hotkey_uuid == reset_hotkey_) { - velocity_multiplier_->set_value(1.0f); - velocity_->set_value(1.0f); - forward_->set_value(true); + } else if (hotkey_uuid == toggle_loop_range_) { + loop_range_enabled_->set_value(!loop_range_enabled_->value()); } } +void PlayheadBase::reset() { + velocity_multiplier_->set_value(1.0f); + velocity_->set_value(1.0f); + forward_->set_value(true); +} + void PlayheadBase::set_duration(const timebase::flicks duration) { duration_ = duration; } void PlayheadBase::connect_to_viewport( - const std::string &viewport_name, const std::string &viewport_toolbar_name, bool connect) { + const std::string &viewport_name, + const std::string &viewport_toolbar_name, + bool connect, + caf::actor viewport) { // this playhead needs to be connected (exposed) in a given toolbar // attributes group, so that the compare, source and velocity attrs @@ -666,8 +806,85 @@ void PlayheadBase::connect_to_viewport( expose_attribute_in_model_data( audio_source_, viewport_toolbar_name + "_audio_source", connect); - expose_attribute_in_model_data(compare_mode_, viewport_toolbar_name, connect); expose_attribute_in_model_data(velocity_, viewport_toolbar_name, connect); - Module::connect_to_viewport(viewport_name, viewport_toolbar_name, connect); + expose_attribute_in_model_data(compare_mode_, viewport_toolbar_name, connect); + + // Here we can add attrs to show up in the viewer context menu (right click) + + std::string viewport_context_menu_model_name = viewport_name + "_context_menu"; + + if (connect) { + + // add menu items to the viewport pop-up menu - since there can be + // multiple viewports, there can be multiple menu items active in the UI. + // We get a callback + + // Divider + insert_menu_item(viewport_context_menu_model_name, "", "", 4.5f, nullptr, true); + + insert_menu_item( + viewport_context_menu_model_name, + "In Frame", + "Set Loop", + 1.0f, + nullptr, + false, + set_loop_in_); + + insert_menu_item( + viewport_context_menu_model_name, + "Out Frame", + "Set Loop", + 2.0f, + nullptr, + false, + set_loop_out_); + + + // Divider + insert_menu_item(viewport_context_menu_model_name, "", "Set Loop", 2.5f, nullptr, true); + + set_submenu_position_in_parent(viewport_context_menu_model_name, "Set Loop", 5.0f); + + + insert_menu_item( + viewport_context_menu_model_name, + "Enable Loop", + "", + 7.0f, + loop_range_enabled_, + false, + toggle_loop_range_); + + + } else { + + remove_all_menu_items(viewport_context_menu_model_name); + } + + Module::connect_to_viewport(viewport_name, viewport_toolbar_name, connect, viewport); + + if (connect) { + active_viewports_.insert(viewport_name); + } else { + auto p = active_viewports_.find(viewport_name); + if (p != active_viewports_.end()) { + active_viewports_.erase(p); + } + } +} + +void PlayheadBase::menu_item_activated( + const utility::JsonStore &menu_item_data, const std::string &user_data) { + // use the hotkey handling that's set-up in PlayheadActor + + if (menu_item_data["name"] == "In Frame") { + + hotkey_pressed(set_loop_in_, "", ""); + + } else if (menu_item_data["name"] == "Out Frame") { + + hotkey_pressed(set_loop_out_, "", ""); + } } diff --git a/src/playhead/src/playhead_actor.cpp b/src/playhead/src/playhead_actor.cpp index e37fccbd4..c7e27056b 100644 --- a/src/playhead/src/playhead_actor.cpp +++ b/src/playhead/src/playhead_actor.cpp @@ -1,8 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 #include #include +#include #include +#include #include #include "xstudio/atoms.hpp" @@ -10,13 +12,12 @@ #include "xstudio/bookmark/bookmark.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" #include "xstudio/global_store/global_store.hpp" +#include "xstudio/media/media_actor.hpp" #include "xstudio/media_reader/media_reader_actor.hpp" #include "xstudio/playhead/sub_playhead.hpp" -#include "xstudio/playhead/edit_list_actor.hpp" #include "xstudio/playhead/playhead_actor.hpp" -#include "xstudio/playhead/retime_actor.hpp" +#include "xstudio/playhead/string_out_actor.hpp" #include "xstudio/plugin_manager/plugin_manager.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" @@ -27,8 +28,7 @@ using namespace xstudio::playhead; using namespace xstudio::media_reader; using namespace caf; -static caf::actor media_actor_; - +namespace { /* Simple one-trick actor that enacts the playback loop. */ class PlayLoopActor : public caf::event_based_actor { public: @@ -39,20 +39,25 @@ class PlayLoopActor : public caf::event_based_actor { [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, [=](step_atom) { // step the playhead - request(playhead, infinite, step_atom_v) + mail(step_atom_v) + .request(playhead, infinite) .then( [=](const PlayheadBase::OptionalTimePoint &tp) { if (tp) { // playhead is still going, trigger the next step by sending a // step atom to ourselves if (tp > utility::clock::now()) { - scheduled_anon_send( - caf::actor_cast(this), *tp, step_atom_v); + anon_mail(step_atom_v) + .schedule(*tp) + .send(caf::actor_cast(this)); + // scheduled_anon_send( + // caf::actor_cast(this), *tp, step_atom_v); } else { // I *think* a scheduled send to a time point in the past // will not get sent, hence we brute force continue in case // there has been some delays in the mailbox - anon_send(caf::actor_cast(this), step_atom_v); + anon_mail(step_atom_v) + .send(caf::actor_cast(this)); } } else { send_exit(this, caf::exit_reason::user_shutdown); @@ -62,7 +67,7 @@ class PlayLoopActor : public caf::event_based_actor { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(e)); }); }); - delayed_anon_send(this, std::chrono::milliseconds(5), step_atom_v); + anon_mail(step_atom_v).delay(std::chrono::milliseconds(5)).send(this); } ~PlayLoopActor() override = default; @@ -73,12 +78,73 @@ class PlayLoopActor : public caf::event_based_actor { caf::behavior make_behavior() override { return behavior_; } }; +std::vector to_actor_vector(const utility::UuidActorVector &v) { + std::vector result; + for (const auto &a : v) { + result.push_back(a.actor()); + } + return result; +} + +utility::UuidVector to_uuid_vector(const utility::UuidActorVector &v) { + utility::UuidVector result; + for (const auto &a : v) { + result.push_back(a.uuid()); + } + return result; +} + +bool check_actor_down(caf::actor actor_down, utility::UuidActor &v) { + if (v.actor() == actor_down) { + v = utility::UuidActor(); + return true; + } + return false; +} + +bool check_actor_down(caf::actor actor_down, utility::UuidActorVector &v) { + + const size_t sz = v.size(); + auto p = v.begin(); + while (p != v.end()) { + if (p->actor() == actor_down) + p = v.erase(p); + else + p++; + } + return sz != v.size(); +} + +} // namespace + + +PlayheadActor::PlayheadActor(caf::actor_config &cfg, const std::string &name) + : caf::event_based_actor(cfg), + PlayheadBase(name, std::move(utility::Uuid::generate())), + audio_path_(NO_AUDIO), + offscreen_only_(true) { + + init(); + set_parent_actor_addr(actor_cast(this)); +} + PlayheadActor::PlayheadActor( - caf::actor_config &cfg, const utility::JsonStore &jsn, caf::actor playlist_selection) - : caf::event_based_actor(cfg), PlayheadBase(static_cast(jsn["base"])) { + caf::actor_config &cfg, + const std::string &name, + const AudioPath audio_path, + caf::actor playlist_selection, + const utility::Uuid uuid, + caf::actor_addr parent_playlist) + : caf::event_based_actor(cfg), + PlayheadBase(name, std::move(uuid)), + audio_path_(audio_path), + parent_playlist_(parent_playlist) { + init(); set_parent_actor_addr(actor_cast(this)); - connect_to_playlist_selection_actor(playlist_selection); + playlist_selection_addr_ = caf::actor_cast(playlist_selection); + anon_mail(playlist::selection_actor_atom_v, playlist_selection) + .send(actor_cast(this)); // for every attribute we expose it in frontend model data, where the id // of the model data set is the uuid of the module here. This means if we have @@ -93,58 +159,21 @@ PlayheadActor::PlayheadActor( expose_attribute_in_model_data( attr.get(), std::string("{") + to_string(Module::uuid()) + std::string("}"), true); } -} - -PlayheadActor::PlayheadActor( - caf::actor_config &cfg, - const std::string &name, - caf::actor playlist_selection, - const utility::Uuid uuid) - : caf::event_based_actor(cfg), PlayheadBase(name, std::move(uuid)) { - - init(); - set_parent_actor_addr(actor_cast(this)); - connect_to_playlist_selection_actor(playlist_selection); - // see comment in other constructor above - for (auto &attr : attributes_) { - expose_attribute_in_model_data( - attr.get(), std::string("{") + to_string(Module::uuid()) + std::string("}"), true); - } + make_source_menu_model(); } +void PlayheadActor::on_exit() { parent_actor_exiting(); } + void PlayheadActor::init() { // get global reader and steal mrm.. spdlog::debug("Created PlayheadActor {}", name()); - print_on_exit(this, name()); - - set_exit_handler([=](scheduled_actor *a, caf::exit_msg &m) { - disconnect_from_ui(); - audio_output_actor_ = caf::actor(); - empty_clip_ = caf::actor(); - image_cache_ = caf::actor(); - key_playhead_ = caf::actor(); - pre_reader_ = caf::actor(); - sub_playheads_.clear(); - source_wrappers_.clear(); - source_actors_.clear(); - default_exit_handler(a, m); - }); + event_group_ = spawn(this); + link_to(event_group_); - set_down_handler([=](down_msg &msg) { - /*if (msg.source == playlist_selection_) { - demonitor(playlist_selection_); - playlist_selection_ = caf::actor(); - leave_broadcast(this, source_events_); - send(this, utility::event_atom_v, utility::change_atom_v); - } else {*/ - // child playhead kills itself if its source goes down - for (auto &ph : sub_playheads_) { - if (ph == msg.source) { - anon_send(this, utility::event_atom_v, utility::change_atom_v); - } - } + attach_functor([=](const caf::error &reason) { + spdlog::debug("PLAYHEAD exited: {}", to_string(reason)); }); try { @@ -169,59 +198,94 @@ void PlayheadActor::init() { } broadcast_ = spawn(this); - event_group_ = spawn(this); fps_moniotor_group_ = spawn(this); viewport_events_group_ = spawn(this); playhead_media_events_group_ = spawn(this); link_to(broadcast_); - link_to(event_group_); link_to(fps_moniotor_group_); link_to(viewport_events_group_); link_to(playhead_media_events_group_); - try { - - scoped_actor sys{system()}; - - // Here's we join the viewport refraw group of the global module events actor. Thus, - // a component like the colour pipeline can ensure the viewport is redrawn - auto attrs_events_actor = - system().registry().template get(module_events_registry); - auto group = request_receive( - *sys, attrs_events_actor, module::redraw_viewport_group_atom_v); - request_receive(*sys, group, broadcast::join_broadcast_atom_v, this); - - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - // ensure we have a source and a child playhead, due to many messages // delagated to the child playhead - empty_clip_ = - spawn("EmptyClip blank", std::vector(), media::MT_IMAGE); - link_to(empty_clip_); + empty_clip_ = utility::UuidActor(utility::Uuid::generate(), spawn()); + // link_to(empty_clip_.actor()); make_child_playhead(empty_clip_); switch_key_playhead(0); - audio_output_actor_ = system().registry().template get(audio_output_registry); + if (audio_path_ != playhead::NO_AUDIO) { + audio_output_actor_ = + system().registry().template get(audio_output_registry); + } + playhead_events_actor_ = system().registry().template get(global_playhead_events_actor); pre_reader_ = system().registry().template get(media_reader_registry); + // set_default_handler( + // [this](caf::scheduled_actor *, caf::message &msg) -> caf::skippable_result { + // // UNCOMMENT TO DEBUG UNEXPECT MESSAGES + // spdlog::warn( + // "Got unwanted messate from {} {}", to_string(current_sender()), + // to_string(msg)); + + // mail(utility::name_atom_v) + // .request(caf::actor_cast(current_sender()), infinite) + // .then( + // [=](const std::string &nm) { std::cerr << "NAME " << nm << "\n"; }, + // [=](caf::error &err) { std::cerr << "NAME " << to_string(err) << "\n"; + // }); + + // return message{}; + // }); + + apply_compare_prefs(); + behavior_.assign( - make_set_name_handler(event_group_, this), - make_get_name_handler(), - make_last_changed_getter(), - make_last_changed_setter(event_group_, this), - make_last_changed_event_handler(event_group_, this), - make_get_uuid_handler(), - make_get_type_handler(), - make_get_event_group_handler(event_group_), - make_get_detail_handler(this, event_group_), - - [=](actual_playback_rate_atom atom) { delegate(key_playhead_, atom); }, + [=](caf::exit_msg &msg) { + disconnect_from_ui(); + + if (audio_path_ == playhead::INDEPENDENT_AUDIO && audio_output_actor_) { + // this tells global audio output that we are exiting + mail(sound_audio_atom_v, uuid(), false).send(audio_output_actor_); + } + + audio_output_actor_ = caf::actor(); + empty_clip_ = utility::UuidActor(); + image_cache_ = caf::actor(); + hero_sub_playhead_ = utility::UuidActor(); + pre_reader_ = caf::actor(); + audio_src_ = utility::UuidActor(); + + // for (auto &i : sub_playheads_) { + // unlink_from(i.actor()); + // send_exit(i.actor(), caf::exit_reason::user_shutdown); + // } + // send_exit(audio_playhead_, caf::exit_reason::user_shutdown); + + sub_playheads_.clear(); + source_actors_.clear(); + previous_source_actors_.clear(); + string_audio_sources_.clear(); + timeline_track_actors_.clear(); + dynamic_source_actors_.clear(); + + // default_exit_handler(a, m); + + audio_playhead_ = caf::actor(); + + quit(msg.reason); + }, + + [=](utility::get_event_group_atom) -> caf::actor { return event_group_; }, + + [=](utility::parent_atom) -> caf::actor_addr { return parent_playlist_; }, + + [=](actual_playback_rate_atom atom) { + return mail(atom).delegate(hero_sub_playhead_.actor()); + }, [=](clear_precache_requests_atom) -> result { auto rp = make_response_promise(); @@ -232,9 +296,11 @@ void PlayheadActor::init() { [=](const error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }, [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) { - // if(msg.source == store_events) - // unsubscribe(); + + [=](compare_mode_atom) -> std::string { return compare_mode_->value(); }, + + [=](compare_mode_atom, const std::string &compare_mode) { + compare_mode_->set_value(compare_mode); }, [=](dropped_frame_atom) { throttle(); }, @@ -243,6 +309,14 @@ void PlayheadActor::init() { [=](viewport_events_group_atom) -> caf::actor { return viewport_events_group_; }, + [=](playlist::selection_actor_atom, caf::actor selection_actor) -> result { + // allows us to connect to a selection actor, fetch the current selection, and + // return a result. We use this to set-up our source list before deserialisation + auto rp = make_response_promise(); + connect_to_playlist_selection_actor(selection_actor, rp); + return rp; + }, + /* move all child playheads to current position */ [=](jump_atom) { update_child_playhead_positions(true); }, @@ -257,31 +331,37 @@ void PlayheadActor::init() { // in a loop between sub-playhead and parent playhead like this: auto sub_playhead = caf::actor_cast(sub_playhead_addr); for (const auto &i : sub_playheads_) { - if (i == sub_playhead) { - anon_send( - i, + if (i.actor() == sub_playhead) { + anon_mail( jump_atom_v, position(), forward(), velocity(), playing(), true, - connected_to_ui()); + connected_to_ui(), + user_is_frame_scrubbing_->value()) + .send(i.actor()); } } }, + [=](jump_atom, const int64_t frame) { + return mail(jump_atom_v, (int)frame).delegate(caf::actor_cast(this)); + }, + [=](jump_atom, const int frame) -> result { auto rp = make_response_promise(); // by requesting duration from self we ensure that we have updated // internal data about source duration, incase this jump message // has come immediately after a change in the source, for example - request(caf::actor_cast(this), infinite, duration_flicks_atom_v) + mail(duration_flicks_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](const timebase::flicks duration) mutable { if (duration != timebase::k_flicks_zero_seconds) { - request( - key_playhead_, infinite, logical_frame_to_flicks_atom_v, frame) + mail(logical_frame_to_flicks_atom_v, frame, true) + .request(hero_sub_playhead_.actor(), infinite) .then( [=](const timebase::flicks flicks) mutable { @@ -300,40 +380,57 @@ void PlayheadActor::init() { return rp; }, - [=](scrub_frame_atom, const int frame) { - request(key_playhead_, infinite, logical_frame_to_flicks_atom_v, frame) + [=](jump_atom, const timebase::flicks flicks) -> result { + auto rp = make_response_promise(); + // by requesting duration from self we ensure that we have updated + // internal data about source duration, incase this jump message + // has come immediately after a change in the source, for example + mail(duration_flicks_atom_v) + .request(caf::actor_cast(this), infinite) .then( - - [=](const timebase::flicks flicks) { - set_position(flicks); - update_child_playhead_positions(true, true); + [=](const timebase::flicks duration) mutable { + if (duration != timebase::k_flicks_zero_seconds) { + set_position(flicks); + update_child_playhead_positions(true); + } else { + set_position(timebase::k_flicks_zero_seconds); + } + // we only deliver the result when we have waited for + // the key playhead to update its position. This means + // that whever made the original request can be sure + // that the playhead is ready to deliver the frame for + // the requested 'flicks' position + mail( + jump_atom_v, + position(), + forward(), + velocity(), + playing(), + true, + connected_to_ui(), + user_is_frame_scrubbing_->value()) + .request(hero_sub_playhead_.actor(), infinite) + .then( + [=]() mutable { rp.deliver(true); }, + [=](const caf::error &err) mutable { rp.deliver(err); }); }, - [=](const error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - }, - - [=](scrub_frame_atom, const timebase::flicks flicks) { - set_position(flicks); - update_child_playhead_positions(true, true); - }, - - [=](jump_atom, const timebase::flicks flicks) -> unit_t { - set_position(flicks); - update_child_playhead_positions(true); - return unit; + [=](const caf::error &err) mutable { rp.deliver(err); }); + return rp; }, [=](jump_atom, const utility::Uuid &media_uuid) { jump_to_source(media_uuid); }, - [=](key_child_playhead_atom) { - // fetch the Uuid of the key_child_playehad - delegate(key_playhead_, uuid_atom_v); + [=](key_child_playhead_atom) -> utility::UuidVector { + // fetch the Uuids of the playheads. The last entry tells + // us the key playhead address + utility::UuidVector r = to_uuid_vector(sub_playheads_); + r.push_back(hero_sub_playhead_.uuid()); + return r; }, [=](key_playhead_index_atom) -> int { for (size_t i = 0; i < sub_playheads_.size(); ++i) { - if (sub_playheads_[i] == key_playhead_) + if (sub_playheads_[i] == hero_sub_playhead_) return (int)i; } return -1; @@ -347,90 +444,115 @@ void PlayheadActor::init() { } }, - [=](logical_frame_atom atom) { delegate(key_playhead_, atom); }, + [=](logical_frame_atom atom) { + return mail(atom).delegate(hero_sub_playhead_.actor()); + }, [=](loop_atom) -> LoopMode { return playhead::LoopMode(loop()); }, [=](loop_atom, const LoopMode loop) -> unit_t { set_loop(loop); - notify_loop_end_changed(); - notify_loop_start_changed(); - send(event_group_, utility::event_atom_v, loop_atom_v, loop); return unit; }, - [=](media::source_offset_frames_atom atom) { delegate(key_playhead_, atom); }, + [=](media::source_offset_frames_atom atom) { + return mail(atom).delegate(hero_sub_playhead_.actor()); + }, [=](media::source_offset_frames_atom atom, caf::actor sub_playhead, const int offset) { // pass up to the main playhead that the offset has changed - if (sub_playhead == key_playhead_) { - send(event_group_, utility::event_atom_v, atom, offset); + if (sub_playhead == hero_sub_playhead_) { + + source_offset_frames_->set_value(offset, false); // the cached frames display might need updating rebuild_cached_frames_status(); } }, - [=](media::source_offset_frames_atom atom, const int offset) { - send(key_playhead_, atom, offset); - update_child_playhead_positions(true); - }, - [=](media_events_group_atom) -> caf::actor { return playhead_media_events_group_; }, - [=](media_atom atom) { delegate(key_playhead_, atom); }, + [=](media_atom atom) { return mail(atom).delegate(hero_sub_playhead_.actor()); }, + + [=](media_source_atom atom) { return mail(atom).delegate(hero_sub_playhead_.actor()); }, - [=](media_source_atom atom) { delegate(key_playhead_, atom); }, + [=](media_source_atom atom, bool) { + return mail(atom, true).delegate(hero_sub_playhead_.actor()); + }, - [=](media_cache::cached_frames_atom) { - send( - event_group_, - utility::event_atom_v, - media_cache::cached_frames_atom_v, - cached_frames_ranges_); + // this is a 'hack'. We need to force the playhead to re-broadcast info about + // the current media source when setting up a new viewport. Calling + // connected_to_ui_changed will do what we want. see viewport_frame_queue_actor.cpp + [=](media_source_atom atom, bool, bool) { + if (connected_to_ui()) { + connected_to_ui_changed(); + } }, [=](bookmark::get_bookmarks_atom) { - send( - event_group_, - utility::event_atom_v, - bookmark::get_bookmarks_atom_v, - bookmark_frames_ranges_); + mail(utility::event_atom_v, bookmark::get_bookmarks_atom_v, bookmark_frames_ranges_) + .send(event_group_); - send( - playhead_media_events_group_, - utility::event_atom_v, - bookmark::get_bookmarks_atom_v, - bookmark_frames_ranges_); + mail(utility::event_atom_v, bookmark::get_bookmarks_atom_v, bookmark_frames_ranges_) + .send(playhead_media_events_group_); }, [=](utility::event_atom, bookmark::get_bookmarks_atom, const std::vector> &bookmark_ranges) { - if (caf::actor_cast(current_sender()) == key_playhead_) { + if (caf::actor_cast(current_sender()) == hero_sub_playhead_.actor()) { + bookmark_frames_ranges_ = bookmark_ranges; - send( - event_group_, + + std::vector ranges; + std::vector colours; + for (const auto &bm : bookmark_ranges) { + colours.push_back(std::get<1>(bm)); + ranges.push_back(std::get<2>(bm)); + ranges.push_back(std::get<3>(bm) - std::get<2>(bm)); + } + bookmarked_frames_->set_value(ranges); + bookmarked_frames_->set_role_data(module::Attribute::StringChoices, colours); + + mail( utility::event_atom_v, bookmark::get_bookmarks_atom_v, - bookmark_frames_ranges_); + bookmark_frames_ranges_) + .send(event_group_); - send( - playhead_media_events_group_, + mail( utility::event_atom_v, bookmark::get_bookmarks_atom_v, - bookmark_frames_ranges_); + bookmark_frames_ranges_) + .send(playhead_media_events_group_); + + // force fetch of a new frame with up-to-date bookmark/annotation + // sidecar data + anon_mail(jump_atom_v).send(this); } }, - [=](media_cache::keys_atom atom) { delegate(key_playhead_, atom); }, + [=](media_cache::keys_atom atom) { + return mail(atom).delegate(hero_sub_playhead_.actor()); + }, [=](play_atom) -> bool { return playing(); }, - [=](play_atom, const bool _play) -> unit_t { + [=](play_atom, const bool _play) -> bool { set_playing(_play); - return unit; + return true; + }, + + [=](play_atom, const float left_right_key_id) { + // the messsage comes in with a delay when user hits forward step + // or backward step hotkey. If the user is continuing to hold + // down the hotkey (wihtout lifting it since the delayed message + // was scheduled) then we can start playback + if (step_keypress_event_id_ == left_right_key_id) { + set_forward(step_keypress_event_id_ > 0.0f); + set_playing(true); + } }, [=](play_forward_atom) -> bool { return forward(); }, @@ -438,8 +560,8 @@ void PlayheadActor::init() { [=](play_forward_atom, const bool forward) -> unit_t { set_forward(forward); update_child_playhead_positions(true); - send(event_group_, utility::event_atom_v, play_forward_atom_v, forward); - send(fps_moniotor_group_, utility::event_atom_v, play_forward_atom_v, forward); + mail(utility::event_atom_v, play_forward_atom_v, forward).send(event_group_); + mail(utility::event_atom_v, play_forward_atom_v, forward).send(fps_moniotor_group_); return unit; }, @@ -448,9 +570,11 @@ void PlayheadActor::init() { [=](play_rate_mode_atom, const utility::TimeSourceMode play_rate_mode) -> result { auto rp = make_response_promise(); set_play_rate_mode(play_rate_mode); - send(event_group_, utility::event_atom_v, play_rate_mode_atom_v, play_rate_mode); + mail(utility::event_atom_v, play_rate_mode_atom_v, play_rate_mode) + .send(event_group_); // this will ensure the duration is updated before we deliver the response - request(caf::actor_cast(this), infinite, duration_flicks_atom_v) + mail(duration_flicks_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](timebase::flicks) mutable { rp.deliver(true); }, [=](const error &err) mutable { rp.deliver(err); }); @@ -465,15 +589,8 @@ void PlayheadActor::init() { [=](playhead::duration_frames_atom) { // spdlog::warn("playhead delegate duration_frames_atom {}", - // to_string(key_playhead_)); - delegate(key_playhead_, duration_frames_atom_v); - }, - - [=](playhead::skip_through_sources_atom, const int skip_by) -> bool { - if (source_actors_.size() <= 1) - return false; - skip_through_sources(skip_by); - return true; + // to_string(hero_sub_playhead_)); + return mail(duration_frames_atom_v).delegate(hero_sub_playhead_.actor()); }, [=](playhead_rate_atom) -> FrameRate { return playhead_rate(); }, @@ -481,9 +598,10 @@ void PlayheadActor::init() { [=](playhead_rate_atom, const FrameRate &rate) -> result { auto rp = make_response_promise(); set_playhead_rate(rate); - send(event_group_, utility::event_atom_v, playhead_rate_atom_v, rate); + mail(utility::event_atom_v, playhead_rate_atom_v, rate).send(event_group_); // this will ensure the duration is updated before we deliver the response - request(caf::actor_cast(this), infinite, duration_flicks_atom_v) + mail(duration_flicks_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](timebase::flicks) mutable { rp.deliver(true); }, [=](const error &err) mutable { rp.deliver(err); }); @@ -495,8 +613,34 @@ void PlayheadActor::init() { media::add_media_source_atom, const utility::UuidActorVector &) { current_media_changed(media_actor_, true); }, - [=](playlist::select_media_atom, const UuidList &selection) { - delegate(playlist_selection_, playlist::select_media_atom_v, selection); + [=](playlist::select_media_atom, const UuidVector &selection) { + return mail(playlist::select_media_atom_v, selection).delegate(playlist_selection_); + }, + + [=](playlist::select_media_atom, + const Uuid &media_id, + const int playhead_idx, + const bool add_select) { + // this message comes from the Viewport. It sends it when the user clicks on + // the viewport to select an image in a Grid layout. If we are not doing + // a contact sheet we don't want to modify the selection this way as it + // will immediately change what's in the Grid, for example + if (parent_playlist_ && contact_sheet_mode_) { + auto playlist = caf::actor_cast(parent_playlist_); + mail(playlist::selection_actor_atom_v) + .request(playlist, infinite) + .then( + [=](caf::actor selection_actor) { + if (selection_actor) + anon_mail(playlist::select_media_atom_v, UuidVector({media_id})) + .send(selection_actor); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } else { + switch_key_playhead(playhead_idx); + } }, [=](playhead::position_atom, @@ -516,10 +660,12 @@ void PlayheadActor::init() { // predict where we will be at the timepoint 'next_video_refresh' ... const timebase::flicks delta = std::chrono::duration_cast( - next_video_refresh - utility::clock::now()); + next_video_refresh - last_playhead_set_timepoint()); const double v = (forward() ? 1.0f : -1.0f) * velocity() * velocity_multiplier(); + const timebase::flicks estimated_playhead_position = timebase::to_flicks(v * timebase::to_seconds(delta)) + position(); + const timebase::flicks clamped_estimated_playhead_position = clamp_timepoint_to_loop_range(estimated_playhead_position); @@ -540,34 +686,39 @@ void PlayheadActor::init() { const utility::Timecode &tc) { // handles incoming notification from a child playhead that their // logical frame has changed - if (child == key_playhead_) { + if (child == hero_sub_playhead_.actor()) { playhead_logical_frame_->set_value(logical_frame, false); + playhead_position_seconds_->set_value(timebase::to_seconds(position())); + playhead_position_flicks_->set_value(position().count()); playhead_media_logical_frame_->set_value(media_logical_frame, false); - current_source_frame_timecode_->set_value(to_string(tc), false); + current_frame_timecode_->set_value(to_string(tc), false); + current_frame_timecode_as_frame_->set_value(tc.total_frames(), false); playhead_media_frame_->set_value(media_frame, false); - send( - playhead_media_events_group_, + mail( utility::event_atom_v, playhead::position_atom_v, position(), logical_frame, media_frame, media_logical_frame, - tc); + tc) + .send(playhead_media_events_group_); media_frame_per_media_uuid_[source_uuid] = media_logical_frame; - send( - event_group_, + + mail( utility::event_atom_v, position_atom_v, logical_frame, media_frame, media_logical_frame, + user_is_frame_scrubbing_->value(), media_rate, min(position(), duration() - playhead_rate()), - tc); + tc) + .send(event_group_); } }, @@ -582,15 +733,33 @@ void PlayheadActor::init() { // child playhead is broadcasting a new audio buffer [=](sound_audio_atom, const Uuid &child_playhead_uuid, - const std::vector &audio_buffers) { - anon_send( - audio_output_actor_, - sound_audio_atom_v, - audio_buffers, - child_playhead_uuid, - playing(), - forward(), - velocity()); + const std::vector &audio_buffers, + const bool scrubbing) { + if (audio_output_actor_ && + caf::actor_cast(current_sender()) == audio_playhead_) { + anon_mail( + sound_audio_atom_v, + audio_buffers, + child_playhead_uuid, + audio_path_ == playhead::GLOBAL_AUDIO, + uuid(), + scrubbing, + position()) + .send(audio_output_actor_); + } + }, + + [=](audio::audio_samples_atom, + const std::vector &audio_buffers, + timebase::flicks playhead_position) { + // the audio playhead broadcasts a smaller set of audio samples for + // the current playhead position - this is used for visualising + // the audio waveform, for example + if (audio_output_actor_ && + caf::actor_cast(current_sender()) == audio_playhead_) { + anon_mail(audio::audio_samples_atom_v, audio_buffers, playhead_position, uuid()) + .send(audio_output_actor_); + } }, // child playhead is broadcasting a new buffer @@ -610,25 +779,25 @@ void PlayheadActor::init() { revert_throttle(); } - send( - broadcast_, + mail( show_atom_v, child_playhead_uuid, buf, playhead_rate(), playing(), - is_onscreen_frame); + is_onscreen_frame) + .send(broadcast_); - if (child_playhead_uuid == key_playhead_uuid_) { + if (child_playhead_uuid == hero_sub_playhead_.uuid()) { // Tell anything measuring FPS that a new frame has been sent for display - send(fps_moniotor_group_, show_atom_v); + mail(show_atom_v).send(fps_moniotor_group_); if (playhead_events_actor_) { - send(playhead_events_actor_, show_atom_v, buf); + mail(show_atom_v, buf).send(playhead_events_actor_); } - send(viewport_events_group_, ui::show_buffer_atom_v, playing()); + mail(ui::show_buffer_atom_v, playing()).send(viewport_events_group_); } }, @@ -640,13 +809,27 @@ void PlayheadActor::init() { // playback doesn't stutter when we have to put a source on the screen that // hasn't been encountered yet and needs some heavy computation/IO for its colour // managment particulars - send( - broadcast_, - colour_pipeline_lookahead_atom_v, - frame_ids_for_colour_mgmnt_lookeahead); + mail(colour_pipeline_lookahead_atom_v, frame_ids_for_colour_mgmnt_lookeahead) + .send(broadcast_); + }, + + [=](buffer_atom, const utility::Uuid &id) -> result { + auto rp = make_response_promise(); + // fetch an image buffer for the given sub-playhead id + for (const auto &i : sub_playheads_) { + if (i.uuid() == id && (assembly_mode() != AM_ONE || i == hero_sub_playhead_)) { + rp.delegate(i.actor(), buffer_atom_v); + return rp; + } + } + rp.deliver(ImageBufPtr()); + return rp; }, - [=](buffer_atom) { delegate(key_playhead_, buffer_atom_v); }, + [=](buffer_atom) { + // fetch an image buffer from the hero sub-playhead + return mail(buffer_atom_v).delegate(hero_sub_playhead_.actor()); + }, // child playhead is broadcasting frames *about* to show on screen // during playback, so we can start uploading pixels to GPU ahead @@ -654,19 +837,30 @@ void PlayheadActor::init() { [=](show_atom, const Uuid &child_playhead_uuid, const std::vector &future_frames) { - send(broadcast_, show_atom_v, child_playhead_uuid, future_frames); + // see note in the other 'show_atom' handler + if (assembly_mode() == AM_ALL || assembly_mode() == AM_TEN || + child_playhead_uuid == hero_sub_playhead_.uuid()) { + mail(show_atom_v, child_playhead_uuid, future_frames).send(broadcast_); + } // have we got all the frames we expected? - if not, it's probably // because the lookahead read/cache can't keep up, + // Note - if we've got the first 4 'future_frames' that's enough + // for what the viewport needs in terms of doing async pixel transfer + // to the GPU. bool missing_future_frame = false; + int i = 0; for (const auto &frame : future_frames) { if (!frame) { missing_future_frame = true; break; } + ++i; + if (i == 4) + break; } - if (child_playhead_uuid == key_playhead_uuid_ && missing_future_frame) { + if (child_playhead_uuid == hero_sub_playhead_.uuid() && missing_future_frame) { throttle(); } else { revert_throttle(); @@ -675,24 +869,12 @@ void PlayheadActor::init() { [=](simple_loop_end_atom) { // loop end in frames of 'key' child - delegate(key_playhead_, flicks_to_logical_frame_atom_v, loop_end()); + return mail(flicks_to_logical_frame_atom_v, loop_end()) + .delegate(hero_sub_playhead_.actor()); }, [=](simple_loop_end_atom, const int loop_end_frame) { - request(key_playhead_, infinite, logical_frame_to_flicks_atom_v, loop_end_frame + 1) - .then( - - [=](const timebase::flicks loop_end_flicks) { - if (set_loop_end( - loop_end_flicks - PlayheadBase::playback_step_increment)) { - // position or loop end were also changed - notify_loop_start_changed(); - } - notify_loop_end_changed(); - }, - [=](const error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); + loop_end_frame_->set_value(loop_end_frame); }, [=](simple_loop_end_atom, const timebase::flicks loop_end_flicks) { @@ -705,24 +887,12 @@ void PlayheadActor::init() { [=](simple_loop_start_atom) { // loop OUT in frames of 'key' child - delegate(key_playhead_, flicks_to_logical_frame_atom_v, loop_start()); + return mail(flicks_to_logical_frame_atom_v, loop_start()) + .delegate(hero_sub_playhead_.actor()); }, [=](simple_loop_start_atom, const int loop_start_frame) { - request(key_playhead_, infinite, logical_frame_to_flicks_atom_v, loop_start_frame) - .then( - - [=](const timebase::flicks loop_start_flicks) { - if (set_loop_start(loop_start_flicks)) { - // position or loop end were also changed - notify_loop_end_changed(); - update_child_playhead_positions(false); - } - notify_loop_start_changed(); - }, - [=](const error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); + loop_start_frame_->set_value(loop_start_frame); }, [=](simple_loop_start_atom, const timebase::flicks loop_start_flicks) { @@ -751,17 +921,16 @@ void PlayheadActor::init() { // an empty opyional is returned if we aren't looping and we've hit // the end of the duration PlayheadBase::OptionalTimePoint next_step_timepoint = play_step(); + // make a note of the time that the playhead position was updated update_child_playhead_positions(false); if (_playing != playing()) { // the call to play_step can turn off playback if we are in 'play once' mode and // we have got to the end of the in/out point region - anon_send( - system().registry().template get(global_registry), - global::busy_atom_v, - playing()); - send(event_group_, utility::event_atom_v, play_atom_v, playing()); + anon_mail(global::busy_atom_v, playing()) + .send(system().registry().template get(global_registry)); + mail(utility::event_atom_v, play_atom_v, playing()).send(event_group_); } return next_step_timepoint; @@ -775,18 +944,15 @@ void PlayheadActor::init() { // range in 'flicks' ... so the child playhead will have to work out // when to loop round and which frames it loops out and in through and // then convert those frames back to flicks - request( - key_playhead_, - infinite, - step_atom_v, - position(), - step_frames, - loop() == LM_LOOP) + mail(step_atom_v, position(), step_frames, loop() == LM_LOOP) + .request(hero_sub_playhead_.actor(), infinite) .await( [=](const timebase::flicks new_flicks) mutable { + user_is_frame_scrubbing_->set_value(true); set_position(new_flicks); update_child_playhead_positions(true); + user_is_frame_scrubbing_->set_value(false); }, [=](const caf::error &err) mutable { spdlog::warn( @@ -795,6 +961,22 @@ void PlayheadActor::init() { }); }, + [=](skip_to_clip_atom, bool next_clip) { + auto rp = make_response_promise(); + // skip the playhead position to the start of the next clip, the + // beginning of the current clip, or if we are already at the beginning + // of the current clip then the beginning of the preceeding clip + mail(skip_to_clip_atom_v, position(), next_clip) + .request(hero_sub_playhead_.actor(), infinite) + .then( + [=](const timebase::flicks new_position) mutable { + set_position(new_position); + update_child_playhead_positions(true); + rp.deliver(true); + }, + [=](const caf::error &err) mutable { rp.deliver(err); }); + }, + [=](use_loop_range_atom) -> bool { return use_loop_range(); }, [=](use_loop_range_atom, const bool _use_loop_range) -> unit_t { @@ -807,10 +989,40 @@ void PlayheadActor::init() { notify_loop_start_changed(); notify_loop_end_changed(); - send(event_group_, utility::event_atom_v, use_loop_range_atom_v, use_loop_range()); + mail(utility::event_atom_v, use_loop_range_atom_v, use_loop_range()) + .send(event_group_); return unit; }, + [=](precache_atom) { + // send a staggered beat to each sub-playhead telling it to request the reader + // to load frames that will be on-screen soon. We stagger the message send so + // that the reader is not swamped by the requests coming from multiple sub-playheads + // at one time. Each sub-playhead is sent this message about once a second. + if (assembly_mode() == AM_ONE) { + // if we aren't comparing multiple sources, only make the + // 'hero' playhead trigger a precache + anon_mail(precache_atom_v).send(hero_sub_playhead_.actor()); + if (audio_playhead_) + anon_mail(precache_atom_v).send(audio_playhead_); + anon_mail(precache_atom_v).delay(std::chrono::milliseconds(1000)).send(this); + } else if (sub_playheads_.size()) { + sub_playhead_precache_idx_ = + (sub_playhead_precache_idx_ + 1) % sub_playheads_.size(); + anon_mail(precache_atom_v) + .send(sub_playheads_[sub_playhead_precache_idx_].actor()); + if (audio_playhead_ && + sub_playheads_[sub_playhead_precache_idx_] == hero_sub_playhead_) { + anon_mail(precache_atom_v).send(audio_playhead_); + } + if (playing()) { + anon_mail(precache_atom_v) + .delay(std::chrono::milliseconds(1000 / sub_playheads_.size())) + .send(this); + } + } + }, + [=](utility::event_atom, change_atom, caf::actor p, bool key_playhead) { // a child playhead's data has possibly changed, force an update // spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(p)); @@ -820,11 +1032,12 @@ void PlayheadActor::init() { if (key_playhead) { rebuild_cached_frames_status(); // this will trigger an update to the duration - anon_send(this, duration_flicks_atom_v); + anon_mail(duration_flicks_atom_v).send(this); // this will update the 'image_source_' attribute so it shows the correct // list of available sources in the UI, in case it has changed - request(key_playhead_, infinite, media_atom_v) + mail(media_atom_v) + .request(hero_sub_playhead_.actor(), infinite) .then( [=](caf::actor media_actor) { if (media_actor) @@ -838,15 +1051,19 @@ void PlayheadActor::init() { [=](utility::event_atom, change_atom, caf::actor p) { update_child_playhead_positions(true); - if (p == key_playhead_ and not child_playhead_changed_) { + if (p == hero_sub_playhead_.actor() and not child_playhead_changed_) { child_playhead_changed_ = true; - delayed_send( - this, - std::chrono::milliseconds(250), - utility::event_atom_v, - change_atom_v, - p, - true); + mail(utility::event_atom_v, change_atom_v, p, true) + .delay(std::chrono::milliseconds(250)) + .send(this); + } + }, + + [=](utility::event_atom, + playhead::media_frame_ranges_atom, + const std::vector &ranges) { + if (current_sender() == hero_sub_playhead_.actor()) { + media_transition_frames_->set_value(ranges); } }, @@ -861,38 +1078,41 @@ void PlayheadActor::init() { const utility::Uuid &media_uuid, const utility::Uuid &source_uuid, const int /*media_frame*/) { - if (sub_playhead == key_playhead_) { + if (sub_playhead == hero_sub_playhead_.actor()) { - if ((to_string(media_uuid) != current_media_uuid_->value() or - to_string(source_uuid) != current_media_source_uuid_->value()) and - media_source_actor) { + if (to_string(media_uuid) != current_media_uuid_->value() or + to_string(source_uuid) != current_media_source_uuid_->value()) { previous_source_uuid_ = current_media_source_uuid_->value(); + current_media_uuid_->set_value(to_string(media_uuid)); current_media_source_uuid_->set_value(to_string(source_uuid)); - request(media_source_actor, infinite, utility::parent_atom_v) - .then( - [=](caf::actor media_actor) { - current_media_changed(media_actor, true); - - send( - event_group_, - utility::event_atom_v, - media_source_atom_v, - UuidActor(media_uuid, media_actor)); - - if (connected_to_ui()) { - send( - broadcast_, + if (media_source_actor) { + mail(utility::parent_atom_v) + .request(media_source_actor, infinite) + .then( + [=](caf::actor media_actor) { + current_media_changed(media_actor, true); + + mail( utility::event_atom_v, media_source_atom_v, - media_source_actor, - source_uuid); - } - }, - [=](const error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); + UuidActor(media_uuid, media_actor)) + .send(event_group_); + + if (connected_to_ui()) { + mail( + utility::event_atom_v, + media_source_atom_v, + media_source_actor, + source_uuid) + .send(broadcast_); + } + }, + [=](const error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } update_playback_rate(); } } @@ -910,11 +1130,60 @@ void PlayheadActor::init() { const utility::Uuid &, const std::tuple &) {}, + [=](source_atom, const utility::UuidActorVector &sources) -> bool { + // This comes from contact sheet objects - the sources are all the + // media in the contact sheet + if (sources != dynamic_source_actors_) { + contact_sheet_mode_ = true; + dynamic_source_actors_ = sources; + new_source_list(); + } + return true; + }, + [=](utility::event_atom, source_atom, const std::vector &source_list) { // This comes from the 'PlaylistSelectionActor' and sets the // viewable(s) that this playhead will play. A child playhead is // made for each source in the source list - new_source_list(source_list); + if (!contact_sheet_mode_) { + dynamic_source_actors_ = to_uuid_actor_vec(source_list); + new_source_list(); + } else if (!source_list.empty()) { + // for contact sheet mode, we just make sure the 'hero playhead' respects + // the first item in source_list + for (int i = 0; i < dynamic_source_actors_.size(); ++i) { + if (dynamic_source_actors_[i].actor() == source_list[0]) { + switch_key_playhead(i); + break; + } + } + } + }, + + [=](source_atom, + const utility::UuidActor &timeline, + const utility::UuidActorVector &timeline_tracks) { + // this is broadcast from the timeline on change events, if the parent of the + // playhead is a timeline. We only rebuild if the track actors have changed. + if (timeline_actor_ != timeline || timeline_track_actors_ != timeline_tracks) { + timeline_actor_ = timeline; + timeline_track_actors_ = timeline_tracks; + + // for timelines, there is one global frame rate set by the timline + // itself - we need to get that now. + if (timeline_actor_) { + mail(utility::rate_atom_v) + .request(timeline_actor_.actor(), infinite) + .then( + [=](const utility::FrameRate &r) { + set_playhead_rate(r); + new_source_list(); + }, + [=](caf::error &err) { new_source_list(); }); + } else { + new_source_list(); + } + } }, [=](source_atom, const std::vector &source_list) -> result { @@ -923,25 +1192,53 @@ void PlayheadActor::init() { // This message can be pushed to a playhead so it can start p[laying // medioa - the contents of the incoming source_list must be MediaActor type // or a wrapper that has the message handlers for playback - new_source_list(source_list); + dynamic_source_actors_ = to_uuid_actor_vec(source_list); + + new_source_list(); + // spdlog::warn("Fanout orig {} dyn {} new {}", dynamic_source_actors_.size(), + // source_list.size(), sub_playheads_.size()); // calling source_atom on SubPlayhead and waiting for response // ensures that the SubPlayhead is 'up-to-date' in other wordfs // it has got all the data it needs from its source to start playback // like duration, timecode and AVFrameID list - fan_out_request(sub_playheads_, infinite, source_atom_v) - .then( - [=](const std::vector) mutable { - // now sending duration_flicks to ourselves means that duration, in/out - // points etc. are ensured to be up-to-date before we deliver the - // response - request( - caf::actor_cast(this), infinite, duration_flicks_atom_v) - .then( - [=](timebase::flicks) mutable { rp.deliver(true); }, - [=](const error &err) mutable { rp.deliver(err); }); - }, - [=](const error &err) mutable { rp.deliver(err); }); + + // Not sure if this is right, but stops it breaking when there are no sub playheads. + if (sub_playheads_.empty()) { + mail(duration_flicks_atom_v) + .request(caf::actor_cast(this), infinite) + .then( + [=](timebase::flicks) mutable { rp.deliver(true); }, + [=](const error &err) mutable { + spdlog::warn("{} duration {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + } else { + fan_out_request( + to_actor_vector(sub_playheads_), infinite, source_atom_v) + .then( + [=](const std::vector) mutable { + // now sending duration_flicks to ourselves means that duration, + // in/out points etc. are ensured to be up-to-date before we deliver + // the response + mail(duration_flicks_atom_v) + .request(caf::actor_cast(this), infinite) + .then( + [=](timebase::flicks) mutable { rp.deliver(true); }, + [=](const error &err) mutable { + spdlog::warn( + "{} duration {}", + __PRETTY_FUNCTION__, + to_string(err)); + rp.deliver(err); + }); + }, + [=](const error &err) mutable { + spdlog::warn("{} Fan {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + } + return rp; }, @@ -956,14 +1253,16 @@ void PlayheadActor::init() { const media::MediaType mt) { // these come from Media actors (to which we have subscribed to the event group) if (media_source.actor()) { - request(media_source.actor(), infinite, utility::name_atom_v) + mail(utility::name_atom_v) + .request(media_source.actor(), infinite) .then( [=](const std::string name) { updating_source_list_ = true; if (mt == media::MT_IMAGE) { - image_source_->set_value(name); + image_source_->set_value(to_string(media_source.uuid()), false); + image_source_name_->set_value(name); } else if (mt == media::MT_AUDIO) { - audio_source_->set_value(name); + audio_source_->set_value(to_string(media_source.uuid()), false); } updating_source_list_ = false; }, @@ -974,14 +1273,27 @@ void PlayheadActor::init() { updating_source_list_ = true; if (mt == media::MT_IMAGE) { image_source_->set_value("-"); + image_source_name_->set_value("-"); } else if (mt == media::MT_AUDIO) { audio_source_->set_value("-"); } updating_source_list_ = false; } }, + + [=](utility::event_atom, + media_reader::get_thumbnail_atom, + const thumbnail::ThumbnailBufferPtr &buf) {}, + [=](utility::event_atom, media::media_status_atom, const media::MediaStatus ms) {}, + [=](utility::event_atom, + media::media_display_info_atom, + const utility::JsonStore &, + caf::actor_addr &) {}, + + [=](utility::event_atom, media::media_display_info_atom, const utility::JsonStore &) {}, + // controls creation and destruction of children [&](utility::event_atom, utility::change_atom) { if (current_sender() != this) { @@ -989,10 +1301,10 @@ void PlayheadActor::init() { current_media_uuid_->set_value(to_string(utility::Uuid())); current_media_source_uuid_->set_value(to_string(utility::Uuid())); - send(this, jump_atom_v); - send(event_group_, utility::event_atom_v, utility::change_atom_v); + anon_mail(jump_atom_v).send(this); + mail(utility::event_atom_v, utility::change_atom_v).send(event_group_); } else { - send(this, jump_atom_v); + anon_mail(jump_atom_v).send(this); } }, @@ -1000,9 +1312,18 @@ void PlayheadActor::init() { [=](utility::get_group_atom) -> caf::actor { return broadcast_; }, + [=](broadcast::join_broadcast_atom atom, caf::actor joiner) { + return mail(atom, joiner).delegate(broadcast_); + }, + + [=](broadcast::leave_broadcast_atom, caf::actor leaver) { + return mail(broadcast::leave_broadcast_atom_v, leaver).delegate(broadcast_); + }, + [=](utility::rate_atom atom) -> result { auto rp = make_response_promise(); - request(key_playhead_, infinite, utility::rate_atom_v) + mail(utility::rate_atom_v) + .request(hero_sub_playhead_.actor(), infinite) .then( [=](const FrameRate &rate) mutable { rp.deliver(rate); }, [=](caf::error &) mutable { @@ -1015,80 +1336,216 @@ void PlayheadActor::init() { }, [=](utility::serialise_atom) -> result { - JsonStore jsn; - jsn["base"] = serialise(); - return jsn; - }, + auto rp = make_response_promise(); + + // We need to update the offsets per sub-playhead before completing + // serialisation ... + auto ct = std::make_shared(sub_playheads_.size()); + source_alignment_values_->set_value(std::vector(sub_playheads_.size(), 0)); + if (sub_playheads_.empty()) { + rp.deliver(serialise()); + return rp; + } - [=](velocity_atom) -> float { return velocity(); }, + for (size_t idx = 0; idx < sub_playheads_.size(); ++idx) { - [=](velocity_multiplier_atom) -> float { return velocity_multiplier(); }, + mail(media::source_offset_frames_atom_v) + .request(sub_playheads_[idx].actor(), infinite) + .then( + [=](const int64_t offset) mutable { + auto r = source_alignment_values_->value(); + if (idx < r.size()) + r[idx] = offset; + source_alignment_values_->set_value(r); + (*ct)--; + if (!(*ct)) { + rp.deliver(serialise()); + } + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + (*ct)--; + if (!(*ct)) { + rp.deliver(serialise()); + } + }); + } + return rp; + }, - [=](velocity_multiplier_atom, const float velocity_multiplier) -> unit_t { + [=](module::deserialise_atom, const utility::JsonStore &j) { + // before we de-serialise, we have to be sure that we have been set-up with the + // playback sources provided by the playhead selection actor by waiting for this + // self-message to return + mail( + playlist::selection_actor_atom_v, + caf::actor_cast(playlist_selection_addr_)) + .request(caf::actor_cast(this), infinite) + .then( + [=](bool) { + deserialise(j); + auto layouts_manager = + self()->home_system().registry().template get( + viewport_layouts_manager); + + // following desrialisation, we set-up the compare mode and set the + // frame offsets per sub-playhead + mail(playhead::compare_mode_atom_v, compare_mode_->value()) + .request(layouts_manager, infinite) + .then( + [=](std::pair< + xstudio::playhead::AutoAlignMode, + xstudio::playhead::AssemblyMode> mode) { + set_assembly_mode(mode.second); + source_actors_.clear(); + new_source_list(); + align_clip_frame_numbers(); + align_audio_playhead(); + + auto offset_frames = source_alignment_values_->value(); + int idx = 0; + for (auto &sub_playhead : sub_playheads_) { + if (idx < offset_frames.size()) + anon_mail( + media::source_offset_frames_atom_v, + (int64_t)offset_frames[idx++], + false) + .send(sub_playhead.actor()); + } + notify_loop_end_changed(); + notify_loop_start_changed(); + mail(utility::event_atom_v, loop_atom_v, loop()) + .send(event_group_); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + + // broadcast the compare mode to viewport(s) that are attached to this + // playhead + mail(compare_mode_atom_v, compare_mode_->value()) + .send(viewport_events_group_); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + }, + + [=](velocity_atom) -> float { return velocity(); }, + + [=](velocity_multiplier_atom) -> float { return velocity_multiplier(); }, + + [=](velocity_multiplier_atom, const float velocity_multiplier) -> unit_t { set_velocity_multiplier(velocity_multiplier); - send( - event_group_, - utility::event_atom_v, - velocity_multiplier_atom_v, - velocity_multiplier); - send( - fps_moniotor_group_, - utility::event_atom_v, - velocity_multiplier_atom_v, - velocity_multiplier); + mail(utility::event_atom_v, velocity_multiplier_atom_v, velocity_multiplier) + .send(event_group_); + mail(utility::event_atom_v, velocity_multiplier_atom_v, velocity_multiplier) + .send(fps_moniotor_group_); return unit; }, + [=](full_precache_atom, + const bool all_playheads, + const bool force, + const std::vector sub_playheads) { + if (sub_playheads == sub_playheads_) { + if (all_playheads) { + for (auto &ph : sub_playheads) { + anon_mail(full_precache_atom_v, true, force).send(ph.actor()); + } + } else if (hero_sub_playhead_) { + anon_mail(full_precache_atom_v, true, force) + .send(hero_sub_playhead_.actor()); + } + + if (audio_playhead_) { + anon_mail(full_precache_atom_v, true, force).send(audio_playhead_); + } + } + }, + + [=](clear_precache_queue_atom, + const std::vector &old_sub_playheads, + const utility::UuidActor &video_string_out_actor) { + // this (delayed) message comes from clear_child_playheads method. + mail(child_playheads_deleted_atom_v, to_uuid_vector(old_sub_playheads)) + .send(broadcast_); + for (auto &i : old_sub_playheads) { + unlink_from(i.actor()); + send_exit(i.actor(), caf::exit_reason::user_shutdown); + } + if (video_string_out_actor) { + unlink_from(video_string_out_actor.actor()); + send_exit(video_string_out_actor.actor(), caf::exit_reason::user_shutdown); + } + }, + [=](bool) {}); } -void PlayheadActor::connect_to_playlist_selection_actor(caf::actor playlist_selection) { +void PlayheadActor::connect_to_playlist_selection_actor( + caf::actor playlist_selection, caf::typed_response_promise rp) { if (playlist_selection) { // Here we subscribe to event messages from a PlaylistSelectionActor - // this will push lists of media to the playhead, when media is selected // from a playlist for playback. See 'new_source_list'. + + // Note that we make sure we've joined the event group, before then + // requesting the current selected sources from the PlaylistSelectionActor + // This avoids a race condition where the PlaylistSelectionActor selection + // changes before we have joined the event group but after we make our + // direct request to get the current selection. playlist_selection_addr_ = caf::actor_cast(playlist_selection); - request(playlist_selection, infinite, utility::get_event_group_atom_v) + mail(utility::get_event_group_atom_v) + .request(playlist_selection, caf::infinite) .then( - [=](caf::actor grp) { - request(grp, caf::infinite, broadcast::join_broadcast_atom_v) + [=](caf::actor grp) mutable { + mail(broadcast::join_broadcast_atom_v) + .request(grp, caf::infinite) .then( [=](const bool) mutable { - request( - playlist_selection, - infinite, - playhead::get_selected_sources_atom_v) - .then( - [=](const utility::UuidActorVector &selection) { - std::vector actors; - for (auto &s : selection) - actors.push_back(s.actor()); - new_source_list(actors); - }, - [=](const caf::error &e) { - spdlog::warn( - "{} {}", __PRETTY_FUNCTION__, to_string(e)); - }); + if (!contact_sheet_mode_) { + mail(playhead::get_selected_sources_atom_v) + .request(playlist_selection, infinite) + .then( + [=](const utility::UuidActorVector + &selection) mutable { + dynamic_source_actors_ = selection; + new_source_list(); + rp.deliver(true); + }, + [=](const caf::error &e) mutable { + spdlog::warn( + "{} {}", __PRETTY_FUNCTION__, to_string(e)); + rp.deliver(false); + }); + } else { + rp.deliver(true); + } }, [=](const error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(false); }); }, - [=](const caf::error &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(e)); + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); + } else { + rp.deliver(false); } } void PlayheadActor::clear_all_precache_requests(caf::typed_response_promise rp) { - fan_out_request(sub_playheads_, infinite, uuid_atom_v) + fan_out_request(to_actor_vector(sub_playheads_), infinite, uuid_atom_v) .then( [=](const std::vector uuids) mutable { // stop any read-ahead activity for these playheads - request(pre_reader_, infinite, clear_precache_queue_atom_v, uuids) + mail(clear_precache_queue_atom_v, uuids) + .request(pre_reader_, infinite) .then( [=](bool r) mutable { rp.deliver(r); }, [=](const error &err) mutable { rp.deliver(err); }); @@ -1098,220 +1555,307 @@ void PlayheadActor::clear_all_precache_requests(caf::typed_response_promise(playheads_copy, infinite, uuid_atom_v) - .then( - [=](const std::vector uuids) mutable { - // stop any read-ahead activity for these playheads - anon_send(pre_reader_, clear_precache_queue_atom_v, uuids); - - send(broadcast_, child_playheads_deleted_atom_v, uuids); - for (auto &i : playheads_copy) { - unlink_from(i); - send_exit(i, caf::exit_reason::user_shutdown); - } - for (auto &i : source_wrappers_copy_) { - unlink_from(i); - send_exit(i, caf::exit_reason::user_shutdown); - } - }, - [=](const error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); + // send a message to delete these things in 2 seconds. We want + // a delay as they may be fetching images and providing them to + // the Viewport while we are setting up new sub-playheads (for + // new sources) + anon_mail(clear_precache_queue_atom_v, sub_playheads_, video_string_out_actor_) + .delay(std::chrono::seconds(2)) + .send(this); sub_playheads_.clear(); - source_wrappers_.clear(); - key_playhead_ = caf::actor(); + hero_sub_playhead_ = utility::UuidActor(); } -caf::actor PlayheadActor::make_child_playhead(caf::actor source) { +caf::actor PlayheadActor::make_child_playhead(utility::UuidActor source) { std::stringstream nmstr; nmstr << "ChildPlayhead" << sub_playheads_.size(); - caf::actor sub_playhead = spawn( - nmstr.str(), - source, - actor_cast(this), - loop_start(), - loop_end(), - play_rate_mode(), - playhead_rate(), - media::MediaType::MT_IMAGE); - - link_to(sub_playhead); + const auto uuid = utility::Uuid::generate(); + auto sub_playhead = utility::UuidActor( + uuid, + spawn( + nmstr.str(), + source, + actor_cast(this), + timeline_mode(), + loop_start(), + loop_end(), + play_rate_mode(), + playhead_rate(), + media::MediaType::MT_IMAGE, + uuid)); + + link_to(sub_playhead.actor()); sub_playheads_.push_back(sub_playhead); - join_event_group(this, sub_playhead); + join_event_group(this, sub_playhead.actor()); return sub_playhead; } void PlayheadActor::make_audio_child_playhead(const int source_index) { + if (!audio_output_actor_) + return; + if (source_index >= (int)source_actors_.size()) return; + auto remove_retimer = [=]() { + if (audio_playhead_retimer_) { + unlink_from(audio_playhead_retimer_); + send_exit(audio_playhead_retimer_, caf::exit_reason::user_shutdown); + audio_playhead_retimer_ = caf::actor(); + } + }; + + if (timeline_mode()) { + // Are we already hooked up to the timeline as the audio source? + if (audio_src_ == timeline_actor_) + return; + + audio_src_ = timeline_actor_; + remove_retimer(); + + } else { + + if (assembly_mode() == AM_STRING) { + + // source_actors_ is unchanged since we were last here? + if (string_audio_sources_ == source_actors_) + return; + + remove_retimer(); + + string_audio_sources_ = source_actors_; + audio_playhead_retimer_ = spawn(string_audio_sources_); + link_to(audio_playhead_retimer_); + audio_src_ = utility::UuidActor(utility::Uuid::generate(), audio_playhead_retimer_); + + } else { + + if (audio_src_ == source_actors_[source_index]) + return; + audio_src_ = source_actors_[source_index]; + remove_retimer(); + } + } + if (audio_playhead_) { + // delete the old audio sub-playhead unlink_from(audio_playhead_); send_exit(audio_playhead_, caf::exit_reason::user_shutdown); - if (audio_playhead_retimer_) - unlink_from(audio_playhead_retimer_); - send_exit(audio_playhead_retimer_, caf::exit_reason::user_shutdown); } - // depending on compare mode, audio playhead needs different wrapper for - // sources - audio_playhead_retimer_ = - compare_mode() == CM_OFF ? caf::actor() - : compare_mode() == CM_STRING - ? spawn("EditListActor", source_actors_, media::MT_AUDIO) - : spawn("RetimeActor", source_actors_[source_index], media::MT_AUDIO); - audio_playhead_ = spawn( "AudioPlayhead", - audio_playhead_retimer_ ? audio_playhead_retimer_ : source_actors_[source_index], + audio_src_, actor_cast(this), + timeline_mode(), loop_start(), loop_end(), play_rate_mode(), playhead_rate(), media::MediaType::MT_AUDIO); - link_to(audio_playhead_); - if (audio_playhead_retimer_) - link_to(audio_playhead_retimer_); - - auto ap = audio_playhead_; - request(audio_playhead_, infinite, get_event_group_atom_v) - .then( - [=](caf::actor event_group) mutable { - // need this check, as the audio_playhead might have been destroyed - // and created again (as per first lines of this member function) - // before we got the response here from the original audio playhead! - if (ap == audio_playhead_) { - join_broadcast(this, event_group); - } - }, - [=](const error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); + join_event_group(this, audio_playhead_); } -void PlayheadActor::new_source_list(const std::vector &sl) { +void PlayheadActor::new_source_list() { + + timeline_mode_->set_value(timeline_mode()); + + // if we're not connected to the UI - i.e. if we don't have a viewport + // connected to us, then we don't need to build our subplayheads for + // image delivery + if (!connected_to_ui() && !offscreen_only_) + return; + + const auto &sl = timeline_mode() ? timeline_track_actors_ : dynamic_source_actors_; if (sl == source_actors_) return; - // stop receiving events of old source list - for (auto &old_source : source_actors_) { - request(old_source, infinite, utility::get_event_group_atom_v) - .then( - [=](caf::actor ev_group) mutable { - anon_send(ev_group, broadcast::leave_broadcast_atom_v, this); - }, - [=](caf::error &) { - // don't print errors, as source may just be down (deleted) - }); + source_actors_ = sl; + + if (timeline_mode()) { + rebuild_from_timeline_sources(); + } else { + rebuild_from_dynamic_sources(); } - source_actors_ = sl; - // reset the loop range as we have new sources - set_loop_start(timebase::k_flicks_low); - set_loop_end(timebase::k_flicks_max); - rebuild(); + if (previous_source_actors_ != source_actors_) { + + // One last check ... do we have a completely new set of sources? + // If so, we should turn off any loop region as we are now viewing + // completely different media to what was being viewed before. + bool match = false; + for (const auto &a: previous_source_actors_) { + for (const auto &b: source_actors_) { + if (a == b) { + match = true; + break; + } + } + } + if (!match) { + + set_loop_start(timebase::k_flicks_low); + set_loop_end(timebase::k_flicks_max); + set_use_loop_range(false); + notify_loop_start_changed(); + notify_loop_end_changed(); + } + previous_source_actors_ = source_actors_; + + // finally,. apply auto-alignment + if (!(assembly_mode() == AM_ONE || assembly_mode() == AM_STRING)) { + + // for A/B mode, grid mode etc we need the child playheads to match + // their durations so that they map to a common (shared) timeline + align_clip_frame_numbers(); + } + + align_audio_playhead(); + } } -void PlayheadActor::rebuild() { +void PlayheadActor::rebuild_from_timeline_sources() { clear_child_playheads(); - if (!source_actors_.size()) { + if (!timeline_actor_) { make_child_playhead(empty_clip_); switch_key_playhead(0); set_position(timebase::k_flicks_zero_seconds); // broadcast and empty buffer to clear the viewport - anon_send(this, show_atom_v, key_playhead_uuid_, ImageBufPtr(), true); + anon_mail(show_atom_v, hero_sub_playhead_.uuid(), ImageBufPtr(), true).send(this); + } else if (assembly_mode() == AM_STRING || assembly_mode() == AM_ONE) { + + // if there's just one video track or we're not in a video compare mode + // we just play the timeline + make_child_playhead(timeline_actor_); + switch_key_playhead(0); - } else if (source_actors_.size() == 1 || compare_mode() == CM_OFF) { + } else { + // compare timeline video tracks int count = 1; - for (auto source : source_actors_) { + for (auto source : timeline_track_actors_) { make_child_playhead(source); // in grid, A/B compare modes etc we must limit the number of child playheads // in the case that the user has, say, selected 100 clips as it's too many for // the UI to cope with. - if (compare_mode() != CM_OFF && count++ > max_compare_sources_->value()) { + if (assembly_mode() != AM_ONE && count > max_compare_sources_->value()) { spdlog::warn( "{} {} {}", __PRETTY_FUNCTION__, "Trying to compare too many things, limiting to first ", max_compare_sources_->value()); break; + } else if (assembly_mode() == AM_TEN && count == 10) { + break; } + count++; + } + + // passing a -1 as the index forces a search for a child playhead that + // is showing the current on-screen source + switch_key_playhead(-1); + + match_video_track_durations(); + } + + num_sub_playheads_->set_value(sub_playheads_.size()); + + mail(key_child_playhead_atom_v, to_uuid_vector(sub_playheads_)).send(broadcast_); + + anon_mail(duration_flicks_atom_v).send(this); +} + +void PlayheadActor::rebuild_from_dynamic_sources() { + + clear_child_playheads(); + + if (!source_actors_.size()) { + + make_child_playhead(empty_clip_); + switch_key_playhead(0); + set_position(timebase::k_flicks_zero_seconds); + + // broadcast and empty buffer to clear the viewport + anon_mail(show_atom_v, hero_sub_playhead_.uuid(), ImageBufPtr(), true).send(this); + + + } else if (source_actors_.size() == 1 || assembly_mode() == AM_ONE) { + + int count = 1; + for (auto source : source_actors_) { + make_child_playhead(source); } // passing a -1 as the index forces a search for a child playhead that // is showing the current on-screen source switch_key_playhead(-1); - } else if (compare_mode() == CM_STRING) { + } else if (assembly_mode() == AM_STRING) { - auto foo = spawn("EditListActor", source_actors_, media::MT_IMAGE); - link_to(foo); - make_child_playhead(foo); + video_string_out_actor_ = utility::UuidActor( + utility::Uuid::generate(), spawn(source_actors_)); + link_to(video_string_out_actor_.actor()); + make_child_playhead(video_string_out_actor_); switch_key_playhead(0); - source_wrappers_.push_back(foo); } else { int count = 1; for (auto source : source_actors_) { - auto noo = spawn("RetimeActor", source, media::MT_IMAGE); - link_to(noo); - make_child_playhead(noo); - source_wrappers_.push_back(noo); + make_child_playhead(source); // in grid, A/B compare modes etc we must limit the number of child playheads // in the case that the user has, say, selected 100 clips as it's too many for // the UI to cope with. - if (compare_mode() != CM_OFF && count++ > max_compare_sources_->value()) { + if (assembly_mode() != AM_ONE && count > max_compare_sources_->value()) { spdlog::warn( "{} {} {}", __PRETTY_FUNCTION__, "Trying to compare too many things, limiting to first ", max_compare_sources_->value()); break; + } else if (assembly_mode() == AM_TEN && count == 10) { + break; } + count++; } // passing a -1 as the index forces a search for a child playhead that // is showing the current on-screen source switch_key_playhead(-1); } - if (!(compare_mode() == CM_OFF || compare_mode() == CM_STRING)) { + num_sub_playheads_->set_value(sub_playheads_.size()); - // for A/B mode, grid mode etc we need the child playheads to match - // their durations so that they map to a common (shared) timeline - align_clip_frame_numbers(); - anon_send(this, duration_flicks_atom_v); - } else { - anon_send(this, duration_flicks_atom_v); - } + mail(key_child_playhead_atom_v, to_uuid_vector(sub_playheads_)).send(broadcast_); } void PlayheadActor::switch_key_playhead(int idx) { + if (idx < sub_playheads_.size() && idx >= 0 && hero_sub_playhead_ == sub_playheads_[idx]) + return; + caf::scoped_actor sys(system()); bool force_move = false; + const bool new_sources = idx == -1; if (idx < 0) { // let's see if one of the child playheads is showing a media source that matches // current_media_uuid_, if so we'll pick that one. @@ -1321,10 +1865,11 @@ void PlayheadActor::switch_key_playhead(int idx) { // this ensures that the child playhead is up-to-date and has // got all the data it needs from its source - request_receive(*sys, ph, source_atom_v); + request_receive(*sys, ph.actor(), source_atom_v); - if (to_string(request_receive( - *sys, ph, media_source_atom_v, true)) == current_media_uuid_->value()) { + if (to_string(request_receive( + *sys, ph.actor(), media_source_atom_v, true) + .uuid()) == current_media_source_uuid_->value()) { idx = i; break; } @@ -1335,64 +1880,96 @@ void PlayheadActor::switch_key_playhead(int idx) { else force_move = true; } catch (std::exception &e) { - spdlog::debug("{} {}", __PRETTY_FUNCTION__, e.what()); + spdlog::critical("{} {}", __PRETTY_FUNCTION__, e.what()); idx = 0; } } if (idx >= 0 && idx < (int)sub_playheads_.size()) { - key_playhead_ = sub_playheads_[idx]; - anon_send(key_playhead_, bookmark::get_bookmarks_atom_v); + hero_sub_playhead_ = sub_playheads_[idx]; + key_playhead_index_->set_value(idx); + anon_mail(bookmark::get_bookmarks_atom_v).send(hero_sub_playhead_.actor()); - try { + if (assembly_mode() != AM_STRING && assembly_mode() != AM_ONE && sub_playheads_.size() > 1 && !new_sources) { - // pass the uuid of the new key playhead to the broadcast group - const Uuid uuid = request_receive(*sys, key_playhead_, uuid_atom_v); - key_playhead_uuid_ = uuid; + // If we're in A/B mode, say, with multiple things selected we want to be + // able to rapidly switch the on-screen source via 1,2,3 hotkeys. This + // message will get to the ViewportFrameQueueActor to tell it the 'hero' + // sub playhead has changed. It then does an immediate frame retrieve + // and disaplay to get 'snappy' switching. + mail(key_child_playhead_atom_v, hero_sub_playhead_, utility::clock::now()) + .send(broadcast_); + } - // if 'switch_key_playhead' is called rapidly, the broadcast made below - // can reach the receiver out of order, so we need to give it a timestamp - // so they can know if they have got an out-of-order notification and ignore it - const auto switchpoint = utility::clock::now(); - send(broadcast_, key_child_playhead_atom_v, uuid, switchpoint); + try { - auto source_actor = request_receive(*sys, key_playhead_, source_atom_v); + auto source_actor = + request_receive(*sys, hero_sub_playhead_.actor(), source_atom_v); make_audio_child_playhead(idx); // this should update the 'image_source_' attribute so it shows the correct // list of available sources in the UI - auto media_actor = request_receive(*sys, key_playhead_, media_atom_v); + auto media_actor = + request_receive(*sys, hero_sub_playhead_.actor(), media_atom_v); if (media_actor) current_media_changed(media_actor); // send the change notification - send(event_group_, utility::event_atom_v, utility::change_atom_v); - send(event_group_, utility::event_atom_v, playhead::key_playhead_index_atom_v, idx); + mail(utility::event_atom_v, utility::change_atom_v).send(event_group_); + mail(utility::event_atom_v, playhead::key_playhead_index_atom_v, idx) + .send(event_group_); check_if_loop_range_makes_sense(); // 'jump' to the last viewed frame of the current on-screen source - if (compare_mode() == CM_STRING) { - move_playhead_to_last_viewed_frame_of_given_source( - utility::Uuid(current_media_uuid_->value())); - } else if (compare_mode() == CM_OFF || force_move) { - move_playhead_to_last_viewed_frame_of_current_source(); + if (!timeline_mode()) { + if (assembly_mode() == AM_STRING) { + move_playhead_to_last_viewed_frame_of_given_source( + utility::Uuid(current_media_uuid_->value())); + } else if (assembly_mode() == AM_ONE || force_move) { + move_playhead_to_last_viewed_frame_of_current_source(); + } else { + update_child_playhead_positions(true); + } } else { update_child_playhead_positions(true); } + const auto switchpoint = utility::clock::now(); + // this will trigger an update to the duration, at which point we can // update other dependent data like loop range etc. - request(caf::actor_cast(this), infinite, duration_flicks_atom_v) + mail(duration_flicks_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](timebase::flicks) { // send update events on properties of the playhead that depend on the // child + notify_offset_changed(); update_playback_rate(); rebuild_cached_frames_status(); - restart_readahead_cacheing(compare_mode() != CM_OFF); + restart_readahead_cacheing(assembly_mode() != AM_ONE); + + // if 'switch_key_playhead' is called rapidly, the broadcast made below + // can reach the receiver out of order, so we need to give it a + // timestamp so they can know if they have got an out-of-order + // notification and ignore it + mail(key_child_playhead_atom_v, hero_sub_playhead_, switchpoint) + .send(broadcast_); + + align_audio_playhead(); + }, + [=](const caf::error &err) { + spdlog::warn("{} {} {}", Module::name(), __PRETTY_FUNCTION__, to_string(err)); + }); + + mail(media_frame_ranges_atom_v) + .request(hero_sub_playhead_.actor(), infinite) + .then( + [=](const std::vector &media_frame_ranges) { + media_transition_frames_->set_value(media_frame_ranges); }, [=](const caf::error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -1414,53 +1991,68 @@ void PlayheadActor::switch_key_playhead(int idx) { previous_selected_sources_count_ = source_actors_.size(); } -void PlayheadActor::update_child_playhead_positions( - const bool force_broadcast, const bool playhead_scrubbing) { +void PlayheadActor::update_child_playhead_positions(const bool force_broadcast) { + + if (audio_output_actor_) { + anon_mail( + position_atom_v, + adjusted_position(), + forward(), + velocity(), + playing(), + last_playhead_set_timepoint(), + audio_path_ == playhead::GLOBAL_AUDIO, + uuid()) + .send(audio_output_actor_); + } if (audio_playhead_) { - anon_send( - audio_playhead_, + anon_mail( jump_atom_v, adjusted_position(), forward(), velocity(), playing(), - playhead_scrubbing ? false : force_broadcast, - connected_to_ui()); + force_broadcast, + connected_to_ui(), + user_is_frame_scrubbing_->value()) + .send(audio_playhead_); } - if (compare_mode() == CM_OFF) { + if (assembly_mode() == AM_ONE) { - anon_send( - key_playhead_, + anon_mail( jump_atom_v, position(), forward(), velocity(), playing(), - playhead_scrubbing ? false : force_broadcast, - connected_to_ui()); - - if (playhead_scrubbing) { - anon_send(key_playhead_, full_precache_atom_v, false, false); - } + force_broadcast, + connected_to_ui(), + user_is_frame_scrubbing_->value()) + .send(hero_sub_playhead_.actor()); } else { for (const auto &i : sub_playheads_) { - anon_send( - i, + anon_mail( jump_atom_v, position(), forward(), velocity(), playing(), force_broadcast, - connected_to_ui()); + connected_to_ui(), + user_is_frame_scrubbing_->value()) + .send(i.actor()); + } + } - if (playhead_scrubbing) { - anon_send(key_playhead_, full_precache_atom_v, false, false); - } + if (user_is_frame_scrubbing_->value()) { + // if the user is scrubbing make sure that we aren't trying + // to do lookahead reads for playback + for (const auto &i : sub_playheads_) { + anon_mail(full_precache_atom_v, false, false).send(i.actor()); } } } @@ -1468,20 +2060,22 @@ void PlayheadActor::update_child_playhead_positions( void PlayheadActor::notify_loop_end_changed() { for (const auto &i : sub_playheads_) { - anon_send(i, simple_loop_end_atom_v, loop_end()); + anon_mail(simple_loop_end_atom_v, loop_end()).send(i.actor()); } if (audio_playhead_) { - anon_send(audio_playhead_, simple_loop_end_atom_v, loop_end()); + anon_mail(simple_loop_end_atom_v, loop_end()).send(audio_playhead_); } update_child_playhead_positions(false); - request(key_playhead_, infinite, flicks_to_logical_frame_atom_v, loop_end()) + mail(flicks_to_logical_frame_atom_v, loop_end()) + .request(hero_sub_playhead_.actor(), infinite) .then( [=](const int loop_end) { - loop_end_frame_->set_value(loop_end); - send(event_group_, utility::event_atom_v, simple_loop_end_atom_v, loop_end); + loop_end_frame_->set_value(loop_end, false); + mail(utility::event_atom_v, simple_loop_end_atom_v, loop_end) + .send(event_group_); }, [=](const error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -1491,20 +2085,22 @@ void PlayheadActor::notify_loop_end_changed() { void PlayheadActor::notify_loop_start_changed() { for (const auto &i : sub_playheads_) { - anon_send(i, simple_loop_start_atom_v, loop_start()); + anon_mail(simple_loop_start_atom_v, loop_start()).send(i.actor()); } if (audio_playhead_) { - anon_send(audio_playhead_, simple_loop_start_atom_v, loop_start()); + anon_mail(simple_loop_start_atom_v, loop_start()).send(audio_playhead_); } update_child_playhead_positions(false); - request(key_playhead_, infinite, flicks_to_logical_frame_atom_v, loop_start()) + mail(flicks_to_logical_frame_atom_v, loop_start()) + .request(hero_sub_playhead_.actor(), infinite) .then( [=](const int loop_start) { - loop_start_frame_->set_value(loop_start); - send(event_group_, utility::event_atom_v, simple_loop_start_atom_v, loop_start); + loop_start_frame_->set_value(loop_start, false); + mail(utility::event_atom_v, simple_loop_start_atom_v, loop_start) + .send(event_group_); }, [=](const error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -1513,16 +2109,10 @@ void PlayheadActor::notify_loop_start_changed() { void PlayheadActor::notify_offset_changed() { - request(key_playhead_, infinite, media::source_offset_frames_atom_v) + mail(media::source_offset_frames_atom_v) + .request(hero_sub_playhead_.actor(), infinite) .then( - - [=](const int offset) { - send( - event_group_, - utility::event_atom_v, - media::source_offset_frames_atom_v, - offset); - }, + [=](const int64_t offset) { source_offset_frames_->set_value(offset, false); }, [=](const error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); @@ -1530,7 +2120,8 @@ void PlayheadActor::notify_offset_changed() { void PlayheadActor::update_duration(caf::typed_response_promise rp) { - request(key_playhead_, infinite, duration_flicks_atom_v) + mail(duration_flicks_atom_v) + .request(hero_sub_playhead_.actor(), infinite) .then( [=](const timebase::flicks duration) mutable { if (duration != timebase::k_flicks_zero_seconds) { @@ -1543,15 +2134,13 @@ void PlayheadActor::update_duration(caf::typed_response_promiseset_value(timebase::to_seconds(duration)); rp.deliver(duration); }, [=](const error &err) mutable { @@ -1559,72 +2148,34 @@ void PlayheadActor::update_duration(caf::typed_response_promiseset_value(duration); - send(event_group_, utility::event_atom_v, duration_frames_atom_v, duration); + mail(utility::event_atom_v, duration_frames_atom_v, duration) + .send(event_group_); }, [=](const error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); } -void PlayheadActor::skip_through_sources(const int skip_by) { - - if (compare_mode() != CM_STRING) { - - // Items are multi selected, so we only skip through the items selected - // by switching to the child playhead - int curr_idx = -1; - for (size_t i = 0; i < sub_playheads_.size(); ++i) { - if (sub_playheads_[i] == key_playhead_) { - curr_idx = (int)i; - } - } - if (curr_idx != -1) { - auto prefs = global_store::GlobalStoreHelper(system()); - bool loop = prefs.value("/ui/qml/cycle_through_playlist"); - int skip_idx = curr_idx + skip_by; - - curr_idx = - loop ? skip_idx == -1 ? static_cast(sub_playheads_.size() - 1) - : skip_idx % sub_playheads_.size() - : std::max( - 0, std::min(skip_idx, static_cast(sub_playheads_.size() - 1))); - - switch_key_playhead(curr_idx); - } - - } else { - - request(key_playhead_, infinite, playhead::skip_through_sources_atom_v, skip_by) - .then( - [=](utility::Uuid &new_source_uuid) { - move_playhead_to_last_viewed_frame_of_given_source(new_source_uuid); - }, - [=](const error &err) { - spdlog::warn("B {} {}", __PRETTY_FUNCTION__, to_string(err)); - } - - ); - } -} - void PlayheadActor::jump_to_source(const utility::Uuid &media_uuid) { - if (compare_mode() == CM_STRING) { + if (assembly_mode() == AM_STRING) { move_playhead_to_last_viewed_frame_of_given_source(media_uuid); } else { for (size_t idx = 0; idx < sub_playheads_.size(); ++idx) { - request(sub_playheads_[idx], infinite, playlist::get_media_uuid_atom_v) + mail(playlist::get_media_uuid_atom_v) + .request(sub_playheads_[idx].actor(), infinite) .await( [=](const utility::Uuid &playhead_media_uuid) { if (playhead_media_uuid == media_uuid) { switch_key_playhead(idx); - if (compare_mode() == CM_OFF) { + if (assembly_mode() == AM_ONE) { move_playhead_to_last_viewed_frame_of_given_source(media_uuid); } } @@ -1640,30 +2191,26 @@ void PlayheadActor::update_playback_rate() { // The playback rate is either set by the media being played (by the key_playhead) // when DYNAMIC otherwise the PlayheadBase overrides it. - if (key_playhead_ && play_rate_mode() == utility::TimeSourceMode::DYNAMIC) { - request(key_playhead_, infinite, actual_playback_rate_atom_v) + if (hero_sub_playhead_ && play_rate_mode() == utility::TimeSourceMode::DYNAMIC) { + mail(actual_playback_rate_atom_v) + .request(hero_sub_playhead_.actor(), infinite) .then( [=](const utility::FrameRate &rate) { if (rate != playhead_rate()) { set_playhead_rate(rate); } - send( - fps_moniotor_group_, - utility::event_atom_v, - actual_playback_rate_atom_v, - rate); + mail(utility::event_atom_v, actual_playback_rate_atom_v, rate) + .send(fps_moniotor_group_); }, [=](const error &) { // no media, fallback to 'default' playback rate - send( - event_group_, - utility::event_atom_v, - playhead_rate_atom_v, - playhead_rate()); + mail(utility::event_atom_v, playhead_rate_atom_v, playhead_rate()) + .send(event_group_); }); } } + void PlayheadActor::update_cached_frames_status( const media::MediaKeyVector &new_keys, const media::MediaKeyVector &remove_keys) { @@ -1677,49 +2224,81 @@ void PlayheadActor::update_cached_frames_status( frames_cached_.erase(key); } - cached_frames_ranges_.clear(); - bool in_cache = false; std::pair r; - auto count = static_cast(all_frames_keys_.size()); + auto count = static_cast(all_frame_ids_.size()); auto scale = (count / 2048) + 1; + utility::JsonStore cached_frames = nlohmann::json::array(); + + int prev_frame_status = -1; + + nlohmann::json j; + j["start"] = 0; + j["duration"] = 0; + j["colour_idx"] = 0; + + // this data is used in XsViewerTimeline.qml to draw the cached status. + // We use the status idx as follows: + // 0: frame in cache + // 1: frame is in the cache, but is a held frame + // 2: frame is not in the cache and is not available on disk + // For frames not in the cache but possibly on disk we don't draw anything + for (auto i = 0; i < count; i += scale) { - if (frames_cached_.count(all_frames_keys_[i]) != in_cache) { - if (in_cache == false) { - in_cache = true; - r.first = i; + + int frame_status = -1; + if (all_frame_ids_[i]) { + if (frames_cached_.count(all_frame_ids_[i]->key())) { + // frame is cached - but is it a held frame? + if (all_frame_ids_[i]->frame_status() == media::FS_HELD_FRAME) + frame_status = 1; + else if (all_frame_ids_[i]->frame_status() == media::FS_NOT_ON_DISK) + frame_status = 2; + else + frame_status = 0; + } else if (all_frame_ids_[i]->frame_status() == media::FS_NOT_ON_DISK) { + // not in cache but marked as not on disk + frame_status = 2; } else { - in_cache = false; - r.second = i - 1; - cached_frames_ranges_.push_back(r); + // not cached (yet) + frame_status = -1; + } + } + + if (prev_frame_status != frame_status) { + if (prev_frame_status != -1) { + j["duration"] = i - j["start"].get(); + j["colour_idx"] = prev_frame_status; + cached_frames.push_back(j); } + prev_frame_status = frame_status; + j["start"] = i; } } - if (in_cache) { - r.second = count - 1; - cached_frames_ranges_.push_back(r); + if (prev_frame_status != -1) { + j["duration"] = count - j["start"].get(); + j["colour_idx"] = prev_frame_status; + cached_frames.push_back(j); } - send( - event_group_, - utility::event_atom_v, - media_cache::cached_frames_atom_v, - cached_frames_ranges_); + cached_frames_->set_value(utility::JsonStore(cached_frames)); } void PlayheadActor::rebuild_cached_frames_status() { - if (key_playhead_ && image_cache_) { + if (hero_sub_playhead_ && image_cache_) { // fetch the full list of keys for every frame in the timeline - request(key_playhead_, infinite, media_cache::keys_atom_v) + mail(media_cache::keys_atom_v) + .request(hero_sub_playhead_.actor(), infinite) .then( - [=](const media::MediaKeyVector &keys) mutable { - all_frames_keys_ = keys; + [=](const media::AVFrameIDs &frame_ids) mutable { + all_frame_ids_ = frame_ids; // now fetch the full list of keys for frames that are in the cache - request(image_cache_, infinite, media_cache::keys_atom_v) + mail(media_cache::keys_atom_v) + .request(image_cache_, infinite) .then( [=](const media::MediaKeyVector &cached_frames_keys) mutable { spdlog::stopwatch sw; @@ -1736,143 +2315,263 @@ void PlayheadActor::rebuild_cached_frames_status() { spdlog::warn("B {} {}", __PRETTY_FUNCTION__, to_string(err)); }); } else { - all_frames_keys_.clear(); + all_frame_ids_.clear(); frames_cached_.clear(); update_cached_frames_status(); } } -void PlayheadActor::align_clip_frame_numbers() { +void PlayheadActor::match_video_track_durations() { - try { - if (sub_playheads_.empty()) - return; + if (sub_playheads_.empty()) + return; - scoped_actor sys{system()}; + scoped_actor sys{system()}; - auto timeout = std::chrono::milliseconds(500); + // get the max duration of video tracks + auto max_duration = std::numeric_limits::lowest(); + auto timeout = std::chrono::milliseconds(500); - const bool align = auto_align_mode() != AAM_ALIGN_OFF; - const bool trim = auto_align_mode() == AAM_ALIGN_TRIM; + for (auto &sub_playhead : sub_playheads_) { - // Use timecode to align the sources - if we are trimming, we use trim to the latest - // start frame and the earliest end frame across all sources. If not we do the reverse, - // i.e. extend all sources to start on the first frame of the source with the earliest - // first frame and the last frame of the last source. - // - // Source1: 1001---------------------------1020 - // Source2: 1008-------------------------1030 - // Result with trim: 1008-------------1020 - // Result without trim: 1001---------------------------------------1030 - // - // Or: - // - // Source1: --------------------------------------------- - // Source2: ----------------- - // Result with trim: ----------------- - // Result without trim: --------------------------------------------- - // - // Etc ... + try { + max_duration = std::max( + max_duration, + request_receive_wait( + *sys, + sub_playhead.actor(), + timeout, + playhead::duration_flicks_atom_v, + true // bool here means we get the duration *before* retiming/extension is + // done + )); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + } + // now extend the range of each sub playhead to match the max duration + for (auto &sub_playhead : sub_playheads_) { - int first_frame = - trim ? std::numeric_limits::lowest() : std::numeric_limits::max(); - int last_frame = - trim ? std::numeric_limits::max() : std::numeric_limits::lowest(); - int max_duration = std::numeric_limits::lowest(); + try { + request_receive_wait( + *sys, sub_playhead.actor(), timeout, timeline::duration_atom_v, max_duration); + } catch (...) { + } + } +} +void PlayheadActor::align_audio_playhead() { - for (auto &sub_playhead : sub_playheads_) { + try { - // reset the offset to zero so we get the 'true' media first & last frame - request_receive_wait( - *sys, sub_playhead, timeout, media::source_offset_frames_atom_v, 0); + if (!audio_playhead_) + return; - // reset the duration to cancel any retiming already done on the source + scoped_actor sys{system()}; + + auto timeout = std::chrono::milliseconds(500); + + if (timeline_mode()) { + + // get the offset in frames applied to the hero playhead + int64_t hero_offset = request_receive_wait( + *sys, hero_sub_playhead_.actor(), timeout, media::source_offset_frames_atom_v); + + // apply the offset to line up the audio to the hero source request_receive_wait( *sys, - sub_playhead, + audio_playhead_, timeout, - timeline::duration_atom_v, - timebase::k_flicks_zero_seconds); - - // get the first and last frame of the source - const int source_first_frame = - request_receive_wait( - *sys, sub_playhead, timeout, first_frame_media_pointer_atom_v) - .timecode_.total_frames(); - - const int source_last_frame = - request_receive_wait( - *sys, sub_playhead, timeout, last_frame_media_pointer_atom_v) - .timecode_.total_frames(); - - // get the timecode frames for first and last frame - first_frame = trim ? std::max(first_frame, source_first_frame) - : std::min(first_frame, source_first_frame); - last_frame = trim ? std::min(last_frame, source_last_frame) - : std::max(last_frame, source_last_frame); - max_duration = std::max(max_duration, source_last_frame - source_first_frame); - } + media::source_offset_frames_atom_v, + hero_offset, + true // reset the duration + ); - if (align && first_frame >= last_frame) { - throw std::runtime_error("Attempt to do align + trimmed compare with sources that " - "don't have overlapping frame range!"); - } + // now get duration of hero playhead + auto dur = request_receive_wait( + *sys, hero_sub_playhead_.actor(), timeout, duration_flicks_atom_v); - for (auto sub_playhead : sub_playheads_) { + // set the audio playhead to match + request_receive_wait( + *sys, audio_playhead_, timeout, timeline::duration_atom_v, dur); - auto duration_flicks = request_receive_wait( - *sys, - sub_playhead, - timeout, - logical_frame_to_flicks_atom_v, - (align ? last_frame - first_frame : max_duration) + 1); + } else { - if (position() > duration_flicks) { - // make sure the playhead position is within the new - // duration of aligned sources - set_position(timebase::k_flicks_zero_seconds); - } + // get the first frame of the hero playhead + const auto hero_first_frame = request_receive_wait( + *sys, hero_sub_playhead_.actor(), timeout, first_frame_media_pointer_atom_v); - if (align) { - const auto source_first_frame = request_receive_wait( - *sys, sub_playhead, timeout, first_frame_media_pointer_atom_v); + // get the offset in frames applied to the hero playhead + int64_t hero_offset = request_receive_wait( + *sys, hero_sub_playhead_.actor(), timeout, media::source_offset_frames_atom_v); - int frames_shift = source_first_frame.timecode_.total_frames() - first_frame; + // get the first frame of the hero playhead + const auto audio_first_frame = request_receive_wait( + *sys, audio_playhead_, timeout, first_frame_media_pointer_atom_v); + + // apply the offset to line up the audio to the hero source + request_receive_wait( + *sys, + audio_playhead_, + timeout, + media::source_offset_frames_atom_v, + int(hero_first_frame.timecode().total_frames()) - + int(audio_first_frame.timecode().total_frames()) + hero_offset, + true // reset the duration + ); + + // now get duration of hero playhead + auto dur = request_receive_wait( + *sys, hero_sub_playhead_.actor(), timeout, duration_flicks_atom_v); + + // set the audio playhead to match + request_receive_wait( + *sys, audio_playhead_, timeout, timeline::duration_atom_v, dur); + } + + } catch (std::exception &e) { + spdlog::debug("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + +void PlayheadActor::align_clip_frame_numbers() { + + try { + + if (sub_playheads_.size() < 2) + return; + + scoped_actor sys{system()}; + + auto timeout = std::chrono::milliseconds(500); + + // in timeline_mode we always comparing video tracks from the same timeline. + // We therefore do not need to align or trim - we just extend the duration + // of video tracks to match the longest. + const bool align = timeline_mode() ? false : auto_align_mode() != AAM_ALIGN_OFF; + const bool trim = timeline_mode() ? false : auto_align_mode() == AAM_ALIGN_TRIM; + + // Use timecode to align the sources - if we are trimming, we use trim to the latest + // start frame and the earliest end frame across all sources. If not we do the reverse, + // i.e. extend all sources to start on the first frame of the source with the earliest + // first frame and the last frame of the last source. + // + // Source1: 1001---------------------------1020 + // Source2: 1008-------------------------1030 + // Result with trim: 1008-------------1020 + // Result without trim: 1001---------------------------------------1030 + // + // Or: + // + // Source1: --------------------------------------------- + // Source2: ----------------- + // Result with trim: ----------------- + // Result without trim: --------------------------------------------- + // + // Etc ... + + if (align) { + + int first_frame = + trim ? std::numeric_limits::lowest() : std::numeric_limits::max(); + + for (auto &sub_playhead : sub_playheads_) { + + // get the first frame (timecode frames) of the source + const int source_first_frame = + request_receive_wait( + *sys, sub_playhead.actor(), timeout, first_frame_media_pointer_atom_v) + .timecode() + .total_frames(); + + // evaluate our overall first frame, depending on trim setting + first_frame = trim ? std::max(first_frame, source_first_frame) + : std::min(first_frame, source_first_frame); + } + + for (auto sub_playhead : sub_playheads_) { + + const auto source_first_frame = request_receive_wait( + *sys, sub_playhead.actor(), timeout, first_frame_media_pointer_atom_v); + + int64_t frames_shift = + first_frame - (int)source_first_frame.timecode().total_frames(); // apply the time offset request_receive_wait( *sys, - sub_playhead, + sub_playhead.actor(), timeout, media::source_offset_frames_atom_v, - frames_shift); + frames_shift, + true // reset the any duration override that might have been applied + ); } + } else { + + for (auto sub_playhead : sub_playheads_) { + + // remove time offset + request_receive_wait( + *sys, + sub_playhead.actor(), + timeout, + media::source_offset_frames_atom_v, + int64_t(0), + true // reset the duration + ); + } + } + + // now find the sub-playhead with the longest duration after the offset + timebase::flicks dur = trim ? timebase::k_flicks_max : timebase::flicks(0); + for (auto sub_playhead : sub_playheads_) { + + auto d = request_receive_wait( + *sys, sub_playhead.actor(), timeout, duration_flicks_atom_v); + dur = trim ? std::min(d, dur) : std::max(d, dur); + } + + for (auto sub_playhead : sub_playheads_) { - // and trim the duration request_receive_wait( - *sys, sub_playhead, timeout, timeline::duration_atom_v, duration_flicks); + *sys, sub_playhead.actor(), timeout, timeline::duration_atom_v, dur); + } + + + source_offset_frames_->set_value( + request_receive_wait( + *sys, hero_sub_playhead_.actor(), timeout, media::source_offset_frames_atom_v), + false); + + // the cached frames display might need updating + rebuild_cached_frames_status(); + + if (position() > dur) { + // make sure the playhead position is within the new + // duration of aligned sources + set_position(timebase::k_flicks_zero_seconds); } } catch (std::exception &e) { // supressing errors as 'No Frames' is thrown when the source is empty, // which isn't really an error - spdlog::debug("{} {}", __PRETTY_FUNCTION__, e.what()); + spdlog::critical("{} {}", __PRETTY_FUNCTION__, e.what()); } } void PlayheadActor::move_playhead_to_last_viewed_frame_of_current_source() { - try { scoped_actor sys{system()}; - const auto uuid = - request_receive(*sys, key_playhead_, media_source_atom_v, true); + const auto uuid = request_receive( + *sys, hero_sub_playhead_.actor(), media_source_atom_v, true) + .uuid(); if (uuid) { const auto p = media_frame_per_media_uuid_.find(uuid); @@ -1882,19 +2581,23 @@ void PlayheadActor::move_playhead_to_last_viewed_frame_of_current_source() { } const auto position = request_receive( - *sys, key_playhead_, media_frame_to_flicks_atom_v, uuid, last_viewed_frame); + *sys, + hero_sub_playhead_.actor(), + media_frame_to_flicks_atom_v, + uuid, + last_viewed_frame); set_position(position); } - anon_send(this, jump_atom_v); + anon_mail(jump_atom_v).send(this); } catch (std::exception &e) { - anon_send(this, jump_atom_v); + anon_mail(jump_atom_v).send(this); // supressing errors as 'No Frames' is thrown when the source is empty, // which isn't really an error - spdlog::debug("{} {}", __PRETTY_FUNCTION__, e.what()); + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } @@ -1912,10 +2615,14 @@ void PlayheadActor::move_playhead_to_last_viewed_frame_of_given_source( scoped_actor sys{system()}; const auto position = request_receive( - *sys, key_playhead_, media_frame_to_flicks_atom_v, source_uuid, last_viewed_frame); + *sys, + hero_sub_playhead_.actor(), + media_frame_to_flicks_atom_v, + source_uuid, + last_viewed_frame); set_position(position); - anon_send(this, jump_atom_v); + anon_mail(jump_atom_v).send(this); } catch (std::exception &e) { @@ -1926,24 +2633,42 @@ void PlayheadActor::move_playhead_to_last_viewed_frame_of_given_source( void PlayheadActor::attribute_changed(const utility::Uuid &attr_uuid, const int role) { - if (attr_uuid == compare_mode_->uuid() || attr_uuid == auto_align_mode_->uuid()) { - rebuild(); + if (role != module::Attribute::Value) { + PlayheadBase::attribute_changed(attr_uuid, role); + return; + } + + if (attr_uuid == pinned_source_mode_->uuid()) { + + new_source_list(); + + } else if (attr_uuid == auto_align_mode_->uuid()) { + + if (!(assembly_mode() == AM_ONE || assembly_mode() == AM_STRING)) { + + // for A/B mode, grid mode etc we need the child playheads to match + // their durations so that they map to a common (shared) timeline + align_clip_frame_numbers(); + align_audio_playhead(); + anon_mail(duration_flicks_atom_v).send(this); + } + } else if (attr_uuid == velocity_->uuid()) { - send(fps_moniotor_group_, utility::event_atom_v, velocity_atom_v, velocity()); + mail(utility::event_atom_v, velocity_atom_v, velocity()).send(fps_moniotor_group_); + mail(utility::event_atom_v, velocity_atom_v, velocity()).send(broadcast_); } else if (attr_uuid == velocity_multiplier_->uuid()) { - send( - fps_moniotor_group_, - utility::event_atom_v, - velocity_multiplier_atom_v, - velocity_multiplier()); + mail(utility::event_atom_v, velocity_multiplier_atom_v, velocity_multiplier()) + .send(fps_moniotor_group_); } else if (attr_uuid == forward_->uuid()) { update_child_playhead_positions(true); - send(fps_moniotor_group_, utility::event_atom_v, play_forward_atom_v, forward()); - send(broadcast_, play_forward_atom_v, forward()); + mail(utility::event_atom_v, play_forward_atom_v, forward()).send(fps_moniotor_group_); + mail(play_forward_atom_v, forward()).send(broadcast_); } else if (attr_uuid == image_source_->uuid() && !updating_source_list_) { switch_media_source(image_source_->value(), media::MT_IMAGE); } else if (attr_uuid == audio_source_->uuid() && !updating_source_list_) { switch_media_source(audio_source_->value(), media::MT_AUDIO); + } else if (attr_uuid == image_stream_->uuid() && !updating_source_list_ && media_actor_) { + switch_media_stream(media_actor_, image_stream_->value(), media::MT_IMAGE, true); } else if (attr_uuid == playing_->uuid()) { if (playing()) { @@ -1955,64 +2680,327 @@ void PlayheadActor::attribute_changed(const utility::Uuid &attr_uuid, const int // with 'step' atoms in a ping-pong loop at regular intervals determined // by the PlayheadBase base class. See step_atom message handlers in this file. spawn(this); + + // starts our pre-cache loop + anon_mail(precache_atom_v).send(this); + + } else { + // reset FFWD/FFRV when playback stops + velocity_multiplier_->set_value(1.0, false); } - send(broadcast_, play_atom_v, playing()); - send(playhead_media_events_group_, utility::event_atom_v, play_atom_v, playing()); - anon_send(audio_output_actor_, play_atom_v, playing()); - anon_send( - system().registry().template get(global_registry), - global::busy_atom_v, - playing()); - send(event_group_, utility::event_atom_v, play_atom_v, playing()); - send(fps_moniotor_group_, utility::event_atom_v, play_atom_v, playing()); + + mail(play_atom_v, playing()).send(broadcast_); + mail(utility::event_atom_v, play_atom_v, playing()).send(playhead_media_events_group_); + if (audio_output_actor_) { + anon_mail(play_atom_v, playing(), audio_path_ == playhead::GLOBAL_AUDIO, uuid()) + .send(audio_output_actor_); + } + anon_mail(global::busy_atom_v, playing()) + .send(system().registry().template get(global_registry)); + mail(utility::event_atom_v, play_atom_v, playing()).send(event_group_); + mail(utility::event_atom_v, play_atom_v, playing()).send(fps_moniotor_group_); update_child_playhead_positions(true); } else if (attr_uuid == playhead_logical_frame_->uuid()) { - anon_send( - caf::actor_cast(this), - scrub_frame_atom_v, - playhead_logical_frame_->value()); + + anon_mail(jump_atom_v, playhead_logical_frame_->value()) + .send(caf::actor_cast(this)); + + } else if (attr_uuid == loop_end_frame_->uuid()) { + + try { + + scoped_actor sys{system()}; + // if loop end is set to frame 100, say, we need to include the + // duration of frame 100. So we compute the flicks for frame 101 and + // then subtract playback_step_increment, which ensures the loop + // end is just inside the duration of frame 100. + const timebase::flicks loop_end_flicks = request_receive( + *sys, + hero_sub_playhead_.actor(), + logical_frame_to_flicks_atom_v, + loop_end_frame_->value() + 1); + + if (set_loop_end(loop_end_flicks - PlayheadBase::playback_step_increment)) { + // position or loop end were also changed + notify_loop_start_changed(); + update_child_playhead_positions(false); + } + notify_loop_end_changed(); + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + } else if (attr_uuid == loop_start_frame_->uuid()) { + + try { + + scoped_actor sys{system()}; + + const timebase::flicks loop_start_flicks = request_receive( + *sys, + hero_sub_playhead_.actor(), + logical_frame_to_flicks_atom_v, + loop_start_frame_->value()); + if (set_loop_start(loop_start_flicks)) { + // position or loop end were also changed + notify_loop_end_changed(); + update_child_playhead_positions(false); + } + notify_loop_start_changed(); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + } else if (attr_uuid == loop_mode_->uuid()) { + notify_loop_end_changed(); + notify_loop_start_changed(); + mail(utility::event_atom_v, loop_atom_v, loop()).send(event_group_); + } else if (attr_uuid == key_playhead_index_->uuid()) { + switch_key_playhead(key_playhead_index_->value()); + } else if (attr_uuid == loop_range_enabled_->uuid()) { + notify_loop_start_changed(); + notify_loop_end_changed(); + } else if (attr_uuid == source_offset_frames_->uuid()) { + mail(media::source_offset_frames_atom_v, source_offset_frames_->value(), false) + .request(hero_sub_playhead_.actor(), infinite) + .then( + [=](const bool changed) { + if (changed) { + // update the audio playhead offset + align_audio_playhead(); + // force broacast image buffers so we see the source + // offset change in viewport etc. + anon_mail(jump_atom_v).send(this); + } + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } else if (attr_uuid == compare_mode_->uuid()) { + + auto layouts_manager = + self()->home_system().registry().template get(viewport_layouts_manager); + + // get the assembly mode for the new compare mode + mail(playhead::compare_mode_atom_v, compare_mode_->value()) + .request(layouts_manager, infinite) + .then( + [=](std::pair + mode) { + if (mode.second != assembly_mode()) { + set_assembly_mode(mode.second); + source_actors_.clear(); + new_source_list(); + // get the default align mode + } + // Note: not using the default align mode provided by the + // layout plugin, because we are driving this with a preference + // per container type (Playlsit, Subset, Contact Sheet etc) + + // set_auto_align_mode(mode.first); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + + // broadcast the compare mode to viewport(s) that are attached to this + // playhead + mail(compare_mode_atom_v, compare_mode_->value()).send(viewport_events_group_); + + } else if (attr_uuid == connect_to_ui_attr_->uuid()) { + if (!connected_to_ui()) + connect_to_ui(); } else { PlayheadBase::attribute_changed(attr_uuid, role); } } +void PlayheadActor::hotkey_pressed( + const utility::Uuid &hotkey_uuid, const std::string &context, const std::string &window) { + + // If the context starts with 'viewport' the hotkey was hit while a viewport + // had mouse focus. If that viewport is NOT attached to this playhead we + // ignore the hotkey + if (context.find("viewport") == 0 && + active_viewports_.find(context) == active_viewports_.end()) { + return; + } + + if (hotkey_uuid == move_selection_down_hotkey_) { + + step_to_next_media(true); + + } else if (hotkey_uuid == move_selection_up_hotkey_) { + + step_to_next_media(false); + + } else if (hotkey_uuid == jump_to_previous_note_hotkey_) { + + int cframe = playhead_logical_frame_->value(); + int d = std::numeric_limits::max(); + int r = -1; + for (const auto &bm : bookmark_frames_ranges_) { + int delta = cframe - std::get<2>(bm); + if (delta > 0 && delta < d) { + r = std::get<2>(bm); + d = delta; + } + } + if (r != -1) { + anon_mail(jump_atom_v, r).send(caf::actor_cast(this)); + } + + } else if (hotkey_uuid == jump_to_next_note_hotkey_) { + + int cframe = playhead_logical_frame_->value(); + int d = std::numeric_limits::max(); + int r = -1; + for (const auto &bm : bookmark_frames_ranges_) { + int delta = std::get<2>(bm) - cframe; + if (delta > 0 && delta < d) { + r = std::get<2>(bm); + d = delta; + } + } + if (r != -1) { + anon_mail(jump_atom_v, r).send(caf::actor_cast(this)); + } + + } else if ( + switch_key_playhead_hotkeys_.find(hotkey_uuid) != switch_key_playhead_hotkeys_.end()) { + switch_key_playhead(switch_key_playhead_hotkeys_[hotkey_uuid]); + } else if (hotkey_uuid == set_loop_in_) { + anon_mail(simple_loop_start_atom_v, position()).send(caf::actor_cast(this)); + } else if (hotkey_uuid == set_loop_out_) { + + mail(flicks_to_logical_frame_atom_v, position()) + .request(hero_sub_playhead_.actor(), infinite) + .then( + [=](int logical) { loop_end_frame_->set_value(logical); }, + [=](caf::error &err) { + + }); + + } else if (hotkey_uuid == step_forward_) { + + set_playing(false); + anon_mail(step_atom_v, 1).send(caf::actor_cast(this)); + + // make some random number as in 'ID' for the key press. If the key + // is STILL being pressed 500ms later (we know this if step_keypress_event_id_ + // matches the value in the delayed message), we start playback + step_keypress_event_id_ = (double)rand() / RAND_MAX; + anon_mail(play_atom_v, step_keypress_event_id_) + .delay(std::chrono::milliseconds(500)) + .send(this); + } else if (hotkey_uuid == step_backward_) { + set_playing(false); + anon_mail(step_atom_v, -1).send(caf::actor_cast(this)); + step_keypress_event_id_ = -(double)rand() / RAND_MAX; + anon_mail(play_atom_v, step_keypress_event_id_) + .delay(std::chrono::milliseconds(500)) + .send(this); + } else if (hotkey_uuid == jump_to_first_frame_) { + + if (loop_start_frame_->value() > 0 && loop_range_enabled_->value()) { + anon_mail(jump_atom_v, loop_start_frame_->value()) + .send(caf::actor_cast(this)); + } else { + anon_mail(jump_atom_v, 0).send(caf::actor_cast(this)); + } + + } else if (hotkey_uuid == jump_to_last_frame_) { + + if (loop_end_frame_->value() > 0 && loop_range_enabled_->value()) { + anon_mail(jump_atom_v, loop_end_frame_->value()) + .send(caf::actor_cast(this)); + } else { + anon_mail(jump_atom_v, std::numeric_limits::max()) + .send(caf::actor_cast(this)); + } + + } else if (hotkey_uuid == cycle_image_layer_up_) { + + auto stream_names = image_stream_->get_role_data>( + module::Attribute::StringChoices); + auto curr_stream = image_stream_->value(); + auto p = std::find(stream_names.begin(), stream_names.end(), curr_stream); + if (p != stream_names.end()) { + size_t idx = std::distance(stream_names.begin(), p); + if (idx > 0) { + image_stream_->set_value(stream_names[idx - 1]); + } + } + + } else if (hotkey_uuid == cycle_image_layer_down_) { + + auto stream_names = image_stream_->get_role_data>( + module::Attribute::StringChoices); + auto curr_stream = image_stream_->value(); + auto p = std::find(stream_names.begin(), stream_names.end(), curr_stream); + if (p != stream_names.end()) { + size_t idx = std::distance(stream_names.begin(), p); + if (idx < (stream_names.size() - 1)) { + image_stream_->set_value(stream_names[idx + 1]); + } + } + + } else { + PlayheadBase::hotkey_pressed(hotkey_uuid, context, window); + } +} + +void PlayheadActor::hotkey_released( + const utility::Uuid &hotkey_uuid, const std::string &context) { + + if (hotkey_uuid == step_forward_ || hotkey_uuid == step_backward_) { + set_playing(false); + step_keypress_event_id_ = 0.0f; + } +} + void PlayheadActor::connected_to_ui_changed() { if (connected_to_ui()) { - restart_readahead_cacheing(true); + new_source_list(); + + restart_readahead_cacheing(assembly_mode() != AM_ONE); update_playback_rate(); - send(fps_moniotor_group_, utility::event_atom_v, velocity_atom_v, velocity()); - send( - fps_moniotor_group_, - utility::event_atom_v, - velocity_multiplier_atom_v, - velocity_multiplier()); - send(fps_moniotor_group_, utility::event_atom_v, play_forward_atom_v, forward()); - - request(key_playhead_, infinite, media_source_atom_v) + mail(utility::event_atom_v, velocity_atom_v, velocity()).send(fps_moniotor_group_); + mail(utility::event_atom_v, velocity_multiplier_atom_v, velocity_multiplier()) + .send(fps_moniotor_group_); + mail(utility::event_atom_v, play_forward_atom_v, forward()).send(fps_moniotor_group_); + + mail(media_source_atom_v) + .request(hero_sub_playhead_.actor(), infinite) .then( [=](caf::actor media_actor) { - send( - broadcast_, + mail( utility::event_atom_v, media_source_atom_v, media_actor, - utility::Uuid(current_media_source_uuid_->value())); + utility::Uuid(current_media_source_uuid_->value())) + .send(broadcast_); }, [=](caf::error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); } else { + + connect_to_ui_attr_->set_value(false, false); // stop all cacheing for (auto ph : sub_playheads_) { - anon_send(ph, clear_precache_queue_atom_v); + anon_mail(clear_precache_queue_atom_v).send(ph.actor()); } if (audio_playhead_) { - anon_send(audio_playhead_, clear_precache_queue_atom_v); + anon_mail(clear_precache_queue_atom_v).send(audio_playhead_); } + clear_child_playheads(); + source_actors_.clear(); } } @@ -2021,66 +3009,75 @@ void PlayheadActor::current_media_changed(caf::actor media_actor, const bool for if (not force and media_actor_ == media_actor) return; media_actor_ = media_actor; - send( - playhead_media_events_group_, - utility::event_atom_v, - playhead::media_atom_v, - media_actor); + mail(utility::event_atom_v, playhead::media_atom_v, media_actor) + .send(playhead_media_events_group_); update_source_multichoice(image_source_, media::MT_IMAGE); update_source_multichoice(audio_source_, media::MT_AUDIO); + update_stream_multichoice(image_stream_, media::MT_IMAGE); } void PlayheadActor::update_source_multichoice( module::StringChoiceAttribute *attr, const media::MediaType mt) { // get the MediaSource items in the MediaActor - request(media_actor_, infinite, media::current_media_source_atom_v, mt) + mail(media::current_media_source_atom_v, mt) + .request(media_actor_, infinite) .then( [=](const UuidActor ¤t_source) mutable { if (playhead_events_actor_ && mt == media::MT_IMAGE) { // send new on screen media to global playhead events actor - send( - playhead_events_actor_, - show_atom_v, - media_actor_, - current_source.actor()); + mail(show_atom_v, media_actor_, current_source.actor()) + .send(playhead_events_actor_); } - request(current_source.actor(), infinite, utility::name_atom_v) + using uuid_name = std::pair; + + mail(utility::name_atom_v) + .request(current_source.actor(), infinite) .then( [=](const std::string ¤t_source_name) mutable { - request( - media_actor_, - infinite, - media::get_media_source_names_atom_v, - mt) + mail(media::get_media_source_names_atom_v, mt) + .request(media_actor_, infinite) .then( - [=](const std::vector> - &source_uuid_names) mutable { + [=](std::vector source_uuid_names) mutable { updating_source_list_ = true; - attr->set_value(current_source_name); + attr->set_value(to_string(current_source.uuid())); + if (attr == image_source_) { + image_source_name_->set_value(current_source_name); + } + + std::sort( + source_uuid_names.begin(), + source_uuid_names.end(), + [](const uuid_name &a, const uuid_name &b) -> bool { + return a.second < b.second; + }); - auto source_names = + std::vector source_names = vpair_second_to_v(source_uuid_names); - // sort names.. - std::sort(source_names.begin(), source_names.end()); + std::vector source_uuids = + vpair_first_to_v(source_uuid_names); if (source_names.empty()) { source_names.emplace_back( mt == media::MT_IMAGE ? "No Video" : "No Audio"); + source_uuids.emplace_back(utility::Uuid()); } attr->set_role_data( module::Attribute::StringChoices, source_names); attr->set_role_data( module::Attribute::AbbrStringChoices, source_names); + attr->set_role_data( + module::Attribute::StringChoicesIds, source_uuids); updating_source_list_ = false; }, [=](error &err) mutable { spdlog::warn( "{} {}", __PRETTY_FUNCTION__, to_string(err)); + updating_source_list_ = false; }); }, [=](error &err) mutable { @@ -2101,26 +3098,92 @@ void PlayheadActor::update_source_multichoice( }); } +void PlayheadActor::update_stream_multichoice( + module::StringChoiceAttribute *streams_attr, const media::MediaType mt) { + + auto clear_attr = [=]() { + streams_attr->set_role_data( + module::Attribute::StringChoices, std::vector(), false); + streams_attr->set_value("", false); + }; + + auto receive_error = [=](error &err) mutable { + spdlog::debug("{} {}", __PRETTY_FUNCTION__, to_string(err)); + clear_attr(); + }; + + // get the MediaSource items in the MediaActor + mail(media::get_media_stream_atom_v, mt) + .request(media_actor_, infinite) + .then( + [=](const std::vector &streams) mutable { + if (streams.size() <= 1) { + // There is only one stream. We can blank the attr as + // the streams menu will be hidden in this case + clear_attr(); + } + + // get the name of the current source + mail(media::current_media_stream_atom_v, mt) + .request(media_actor_, infinite) + .then( + [=](const UuidActor currentStream) mutable { + std::vector stream_actors; + for (const auto &a : streams) { + stream_actors.push_back(a.actor()); + } + + // get the container details of the streams + fan_out_request( + stream_actors, infinite, detail_atom_v) + .then( + [=](std::vector streams_details) mutable { + std::string current_stream; + std::vector stream_names; + for (const auto &stream_detail : streams_details) { + stream_names.push_back(stream_detail.name_); + if (stream_detail.uuid_ == currentStream) { + current_stream = stream_detail.name_; + } + } + std::sort(stream_names.begin(), stream_names.end()); + + streams_attr->set_value(current_stream, false); + streams_attr->set_role_data( + module::Attribute::StringChoices, + stream_names, + false); + streams_attr->set_role_data( + module::Attribute::AbbrStringChoices, + stream_names, + false); + }, + receive_error); + }, + receive_error); + }, + receive_error); +} + void PlayheadActor::restart_readahead_cacheing( const bool all_child_playheads, const bool force) { if (!playing() && connected_to_ui()) { // first make sure we've wiped all outstanding precache requests... - request(caf::actor_cast(this), infinite, clear_precache_requests_atom_v) + mail(clear_precache_requests_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](bool r) { - if (all_child_playheads) { - for (auto ph : sub_playheads_) { - anon_send(ph, full_precache_atom_v, true, force); - } - - } else { - anon_send(key_playhead_, full_precache_atom_v, true, force); - } - if (audio_playhead_) { - anon_send(audio_playhead_, full_precache_atom_v, true, force); - } + // wait one second, and then tell sub-playheads that they can start cacheing + // frames ready for playback. We wait one second because the user might be + // scrolling the selection up or down the media list rapidly, meaning + // subplayheads are being made and destroyed rapidly. We don't want them + // swamping the media readers with pre-cache requests until we're 'steady' + + anon_mail(full_precache_atom_v, all_child_playheads, force, sub_playheads_) + .delay(std::chrono::seconds(1)) + .send(this); }, [=](caf::error &err) { @@ -2129,41 +3192,48 @@ void PlayheadActor::restart_readahead_cacheing( } void PlayheadActor::switch_media_source( - const std::string new_source_name, const media::MediaType mt) { + const std::string new_source_uuid, const media::MediaType mt) { + + auto siwtch_all_selected = [=](const std::string source_name) { + // now, if we have a selection actor we want to try and switch all the + // media sources in the selected media too: + auto playlist_selection = caf::actor_cast(playlist_selection_addr_); + if (playlist_selection) { + + mail(get_selection_atom_v, true) + .request(playlist_selection, infinite) + .then( + [=](std::vector selected_sources) mutable { + for (auto &media_src : selected_sources) { + anon_mail(media_source_atom_v, source_name, mt).send(media_src); + } + }, + [=](error &err) mutable { + if (to_string(err) != "No frames") + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } + }; - if (!key_playhead_) + if (!hero_sub_playhead_ || new_source_uuid == "N/A" || new_source_uuid == "-") return; + utility::Uuid source_uuid(new_source_uuid); + // going via the sub-playhead (which resolves which actual MediaActor is // on screen now), we make the request to change the active MediaSource // for the MediaActor - request(key_playhead_, infinite, media_source_atom_v, new_source_name, mt) + mail(media_source_atom_v, source_uuid, mt) + .request(hero_sub_playhead_.actor(), infinite) .then( - [=](bool switched) mutable { + [=](const std::string &new_source_name) mutable { // re-kick cacheing following switch - if (switched && connected_to_ui()) { + if (connected_to_ui()) { restart_readahead_cacheing(false, true); } - // now, if we have a selection actor we want to try and switch all the - // media sources in the selected media too: - auto playlist_selection = caf::actor_cast(playlist_selection_addr_); - if (playlist_selection) { - - request(playlist_selection, infinite, get_selection_atom_v, true) - .then( - [=](std::vector selected_sources) mutable { - for (auto &media_src : selected_sources) { - anon_send( - media_src, media_source_atom_v, new_source_name, mt); - } - }, - [=](error &err) mutable { - if (to_string(err) != "No frames") - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - } + siwtch_all_selected(new_source_name); }, [=](error &err) mutable { if (to_string(err) != "No frames") @@ -2171,27 +3241,273 @@ void PlayheadActor::switch_media_source( }); } +void PlayheadActor::switch_media_stream( + caf::actor media_actor, + const std::string new_stream_name, + const media::MediaType mt, + bool apply_to_all_selected) { + + auto error_handler = [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }; + + auto apply_to_all = [=]() { + // now, if we have a selection actor we want to try and switch all the + // media sources in the selected media too: + auto playlist_selection = caf::actor_cast(playlist_selection_addr_); + if (playlist_selection) { + + mail(get_selection_atom_v, true) + .request(playlist_selection, infinite) + .then( + [=](std::vector selected_sources) mutable { + for (auto &media : selected_sources) { + switch_media_stream(media, new_stream_name, mt, false); + } + }, + [=](error &err) mutable { + if (to_string(err) != "No frames") + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } + }; + + // get the MediaSource items in the MediaActor + mail(media::get_media_stream_atom_v, mt) + .request(media_actor, infinite) + .then( + [=](const std::vector &streams) mutable { + std::vector stream_actors; + for (const auto &a : streams) { + stream_actors.push_back(a.actor()); + } + + // get the container details of the streams + fan_out_request(stream_actors, infinite, detail_atom_v) + .then( + [=](std::vector streams_details) mutable { + for (const auto &detail : streams_details) { + if (detail.name_ != new_stream_name) + continue; + mail(media::current_media_stream_atom_v, mt, detail.uuid_) + .request(media_actor, infinite) + .then( + [=](bool changed) { + if (changed) { + restart_readahead_cacheing(false, true); + if (apply_to_all_selected) { + apply_to_all(); + } + } + }, + error_handler); + } + }, + error_handler); + }, + error_handler); +} + void PlayheadActor::check_if_loop_range_makes_sense() { - // current behaviour is to turn off the loop range whenever the source(s) - // changes, and when switching to String compare mode, in which case we sanity check the - // range and turn off looping if it's outside the source range (see - // notify_duration_changed) + // current behaviour is to turn off the loop range in string mode when + // we have a new selection etc. if (use_loop_range()) { - if (has_selection_changed() || compare_mode() == CM_STRING) { + if (assembly_mode() == AM_STRING) { set_use_loop_range(false); notify_loop_start_changed(); notify_loop_end_changed(); - send(event_group_, utility::event_atom_v, use_loop_range_atom_v, use_loop_range()); + mail(utility::event_atom_v, use_loop_range_atom_v, use_loop_range()) + .send(event_group_); + } + } +} + +void PlayheadActor::make_source_menu_model() { + + // Make hotkeys for switching the on-screen (primary) source (e.g. for A/B compare) + for (int i = 0; i < 9; ++i) { + + // numeric keys on a querty keyboard start with key '1' having a keyboard + // key id of 0x31 + auto hotkey_id = register_hotkey( + 0x31 + i, + ui::NoModifier, + fmt::format("View source {}", i + 1), + "When comparing multiple selected items hit the hotkey to switch the corresponding " + "item to show in the viewport.", + false, + "Playback"); + switch_key_playhead_hotkeys_[hotkey_id] = i; + } + + move_selection_up_hotkey_ = register_hotkey( + "Up", + "Move backwards through media/clips", + "When comparing multiple selected items hit the hotkey to cycle back through the " + "selection. If only one item is selected then select the previous item in the " + "playlist.", + true, + "Playback"); + + move_selection_down_hotkey_ = register_hotkey( + "Down", + "Move Forwards through media/clips", + "When comparing multiple selected items hit the hotkey to cycle forward through the " + "selection. If only one item is selected then select the next item in the playlist.", + true, + "Playback"); + + jump_to_previous_note_hotkey_ = register_hotkey( + ",", + "Move backwards to previous note", + "Jump the playhead backwards to the next note.", + true, + "Playback"); + + jump_to_next_note_hotkey_ = register_hotkey( + ".", + "Move forwards to next note", + "Jump the playhead forwards to the next note ", + true, + "Playback"); + + // here we make a menu model unique to each instance of the playheadactor. + // We use the uuid of the playhead to construct the unique menu model name. + // This is picked up in the XsViewerSourceSelectorButton.qml script that + // adds the source selector button to the viewport toolbar + + // set the image source and audio source attrs to be 'radiogroup' type + image_source_->set_role_data(module::Attribute::Type, "RadioGroup"); + audio_source_->set_role_data(module::Attribute::Type, "RadioGroup"); + // image_stream_->set_role_data(module::Attribute::Type, "RadioGroup"); + + const std::string source_menu_model_name = + "{" + to_string(Module::uuid()) + "}" + "source_menu"; + + // add a labelled divider saying 'Video Source' + insert_menu_item(source_menu_model_name, "Video Source", "", 1.0f, nullptr, true); + + insert_menu_item(source_menu_model_name, "", "", 2.0f, image_source_); + + insert_menu_item(source_menu_model_name, "Image Layer", "", 2.25f, nullptr, true); + + insert_menu_item(source_menu_model_name, "USE_ATTR_VALUE", "", 2.5f, image_stream_); + + // add a labelled divider saying 'Audio Source' + insert_menu_item(source_menu_model_name, "Audio Source", "", 3.0f, nullptr, true); + + insert_menu_item(source_menu_model_name, "", "", 4.0f, audio_source_); +} + +utility::UuidActorVector +PlayheadActor::to_uuid_actor_vec(const std::vector &actors) { + caf::scoped_actor sys(system()); + + utility::UuidActorVector r; + r.resize(actors.size()); + + int idx = 0; + for (auto &a : actors) { + try { + auto uuid = request_receive(*sys, a, utility::uuid_atom_v); + r[idx] = utility::UuidActor(uuid, a); + } catch (...) { } + idx++; } + return r; } -bool PlayheadActor::has_selection_changed() { - if (source_actors_.size() == 1) { - return to_string(previous_source_uuid_) != current_media_source_uuid_->value(); +void PlayheadActor::apply_compare_prefs() { + + auto owner = caf::actor_cast(parent_playlist_); + if (owner) { + mail(utility::detail_atom_v) + .request(owner, infinite) + .then( + [=](utility::ContainerDetail &detail) { + // this playhead has been set-up from serialisation data, so + // we don't apply the default compare prefs + if (deserialised_) + return; + + utility::JsonStore j; + try { + auto prefs = GlobalStoreHelper(system()); + if (detail.type_ == "Playlist") { + j = prefs.value( + "/core/playhead/playlist_default_compare"); + } else if (detail.type_ == "Subset") { + j = prefs.value( + "/core/playhead/subset_default_compare"); + } else if (detail.type_ == "Contact Sheet") { + j = prefs.value( + "/core/playhead/contact_sheet_default_compare"); + } else if (detail.type_ == "Timeline") { + j = prefs.value( + "/core/playhead/timeline_default_compare"); + } + + if (j.is_array() && j.size() == 2 && j[0].is_string() && + j[1].is_string()) { + std::string compare_mode = j[0].get(); + std::string align = j[1].get(); + compare_mode_->set_value(compare_mode); + auto_align_mode_->set_value(align); + } + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + }, + [=](caf::error &err) {}); } +} + +void PlayheadActor::step_to_next_media(const bool forwards) { + + if (timeline_mode() || (assembly_mode() == AM_STRING && source_actors_.size() > 1)) { + + anon_mail(skip_to_clip_atom_v, forwards).send(caf::actor_cast(this)); + + } else { - return static_cast(source_actors_.size()) != previous_selected_sources_count_; + if (sub_playheads_.size() <= 1) { + + auto playlist_selection = caf::actor_cast(playlist_selection_addr_); + if (playlist_selection) { + anon_mail(playhead::select_next_media_atom_v, forwards ? 1 : -1) + .send(playlist_selection); + } + + } else { + for (size_t i = 0; i < sub_playheads_.size(); ++i) { + if (sub_playheads_[i] == hero_sub_playhead_) { + if (forwards) { + if (i == (sub_playheads_.size() - 1)) { + switch_key_playhead(0); + } else { + switch_key_playhead(i + 1); + } + } else if (!i) { + switch_key_playhead(sub_playheads_.size() - 1); + } else { + switch_key_playhead(i - 1); + } + break; + } + } + } + } +} + +void PlayheadActor::connected_viewports_changed(std::set &connected_viewports) { + + if (connected_viewports.empty()) { + disconnect_from_ui(); + } else if (!connected_to_ui()) { + connect_to_ui(); + } } diff --git a/src/playhead/src/playhead_global_events_actor.cpp b/src/playhead/src/playhead_global_events_actor.cpp index 955cbf4b2..c6f2326b2 100644 --- a/src/playhead/src/playhead_global_events_actor.cpp +++ b/src/playhead/src/playhead_global_events_actor.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include "xstudio/atoms.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" @@ -21,6 +22,45 @@ PlayheadGlobalEventsActor::PlayheadGlobalEventsActor(caf::actor_config &cfg) init(); } +void PlayheadGlobalEventsActor::on_exit() { + global_active_playhead_ = caf::actor(); + viewports_.clear(); + system().registry().erase(global_playhead_events_actor); +} + +void PlayheadGlobalEventsActor::monitor_it(const caf::actor &actor) { + auto act_addr = caf::actor_cast(actor); + + if (auto sit = monitor_.find(act_addr); sit == std::end(monitor_)) { + monitor_[act_addr] = monitor(actor, [this, addr = actor.address()](const error &) { + if (auto mit = monitor_.find(caf::actor_cast(addr)); + mit != std::end(monitor_)) + monitor_.erase(mit); + + auto q = viewports_.begin(); + if (addr == global_active_playhead_) { + global_active_playhead_ = caf::actor(); + } + while (q != viewports_.end()) { + if (addr == q->second.viewport) { + q = viewports_.erase(q); + } else { + if (addr == q->second.playhead) { + anon_mail(ui::viewport::viewport_playhead_atom_v, caf::actor()) + .send(q->second.viewport); + q->second.playhead = caf::actor(); + } + if (addr == global_active_playhead_) { + global_active_playhead_ = caf::actor(); + } + q++; + } + } + }); + } +} + + void PlayheadGlobalEventsActor::init() { spdlog::debug("Created PlayheadGlobalEventsActor {}", name()); @@ -28,111 +68,247 @@ void PlayheadGlobalEventsActor::init() { system().registry().put(global_playhead_events_actor, this); - event_group_ = spawn(this); + event_group_ = spawn(this); + fine_grain_events_group_ = spawn(this); link_to(event_group_); - set_down_handler([=](down_msg &msg) { - // find in playhead list.. - if (msg.source == on_screen_playhead_) { - demonitor(on_screen_playhead_); - on_screen_playhead_ = caf::actor(); - send( - event_group_, - utility::event_atom_v, - ui::viewport::viewport_playhead_atom_v, - on_screen_playhead_); - } - }); + // set_default_handler( + // [this](caf::scheduled_actor *, caf::message &msg) -> caf::skippable_result { + // // UNCOMMENT TO DEBUG UNEXPECT MESSAGES + + // spdlog::warn( + // "Got broadcast from {} {}", to_string(current_sender()), to_string(msg)); + + // return message{}; + // }); behavior_.assign( [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) {}, [=](utility::get_event_group_atom) -> caf::actor { return event_group_; }, [=](broadcast::join_broadcast_atom, caf::actor joiner) { - delegate(event_group_, broadcast::join_broadcast_atom_v, joiner); - if (on_screen_playhead_) { - // re-send events so new joiner has current on-screen buffer etc. - send( - event_group_, - utility::event_atom_v, - ui::viewport::viewport_playhead_atom_v, - on_screen_playhead_); - request(on_screen_playhead_, infinite, buffer_atom_v) - .then( - [=](const media_reader::ImageBufPtr &buf) { - /*send( - event_group_, - utility::event_atom_v, - show_atom_v, - buf, - "viewport0");*/ - }, - [=](caf::error &) {}); - } + return mail(broadcast::join_broadcast_atom_v, joiner).delegate(event_group_); }, [=](broadcast::leave_broadcast_atom, caf::actor joiner) { - delegate(event_group_, broadcast::leave_broadcast_atom_v, joiner); + return mail(broadcast::leave_broadcast_atom_v, joiner).delegate(event_group_); + }, + [=](ui::viewport::viewport_playhead_atom) -> caf::actor { + return global_active_playhead_; + }, + [=](ui::viewport::viewport_playhead_atom, + caf::actor playhead, + bool request) -> std::vector { + std::vector r; + // return viewports that are connected to the given playhead + for (auto &p : viewports_) { + if (p.second.playhead == playhead) { + r.push_back(p.second.viewport); + } + } + return r; + }, + [=](ui::viewport::viewport_cursor_atom, const std::string &cursor_name) { + for (auto &p : viewports_) { + anon_mail(ui::viewport::viewport_cursor_atom_v, cursor_name) + .send(p.second.viewport); + } }, [=](ui::viewport::viewport_playhead_atom, caf::actor playhead) { - if (on_screen_playhead_ != playhead) { - send( - event_group_, - utility::event_atom_v, - ui::viewport::viewport_playhead_atom_v, - playhead); - on_screen_playhead_ = playhead; - if (playhead) { - // force an event broadcast for the on-screen media and - // media source (useful for plugins or anything else who - // has joined our event group) - request(playhead, infinite, playhead::media_atom_v) - .then( - [=](caf::actor media) { - request(playhead, infinite, playhead::media_source_atom_v) - .then( - [=](caf::actor media_source) { - send( - event_group_, - utility::event_atom_v, - show_atom_v, - media, - media_source); - }, - [=](caf::error &) {}); - }, - [=](caf::error &) {}); + // something can send us this message in order to set the 'global' + // playhead - i.e. the playhead that is being viewed by non-quickview + // viewports. SessionModel::setCurrentPlayheadFromPlaylist does this for example. + for (auto &p : viewports_) { + anon_mail(ui::viewport::viewport_playhead_atom_v, playhead) + .send(p.second.viewport); + } + global_active_playhead_ = playhead; + mail( + utility::event_atom_v, + ui::viewport::viewport_playhead_atom_v, + global_active_playhead_) + .send(event_group_); + }, + [=](ui::viewport::viewport_playhead_atom, + const std::string viewport_name, + caf::actor playhead) { + if (viewports_[viewport_name].playhead == playhead) + return; + + // a viewport named 'viewport_name' is connecting to a playhead + mail( + utility::event_atom_v, + ui::viewport::viewport_playhead_atom_v, + viewport_name, + playhead) + .send(event_group_); + + // what's the playhead that is currently attached to the viewport + // (if any) + auto playhead_to_be_disconnected = viewports_[viewport_name].playhead; + + viewports_[viewport_name].playhead = playhead; + + // is this old playhead connected to another viewport? + for (auto &p : viewports_) { + if (p.second.playhead == playhead_to_be_disconnected) { + playhead_to_be_disconnected = caf::actor(); + break; + } + } + + if (playhead_to_be_disconnected) { + + // No, no other viewports are using the playhead that is to + // be disconnected from the viewport. Therefore we tell the + // playhead to stop playing (if it is playing). + anon_mail(playhead::play_atom_v, false).send(playhead_to_be_disconnected); + anon_mail(module::disconnect_from_ui_atom_v).send(playhead_to_be_disconnected); + // we can stop monitoring it as we don't care if it exits or + // not - we're only keeping track of playheads that are + // connected to viewports + + if (auto it = monitor_.find( + caf::actor_cast(playhead_to_be_disconnected)); + it != std::end(monitor_)) { + it->second.dispose(); + monitor_.erase(it); } - monitor(playhead); + } + + if (playhead) { + monitor_it(playhead); + // since the playhead has changed we want to tell subscribers + // the new media/media_source + mail(playhead::media_atom_v) + .request(playhead, infinite) + .then( + [=](caf::actor media) { + mail(playhead::media_source_atom_v) + .request(playhead, infinite) + .then( + [=](caf::actor media_source) { + mail( + utility::event_atom_v, + show_atom_v, + media, + media_source, + viewport_name) + .send(event_group_); + }, + [=](caf::error &err) { + + }); + }, + [=](caf::error &err) { + + }); } }, + [=](show_atom, const media_reader::ImageBufPtr &buf) { - // TODO: cleanup this stuff? - /*if (caf::actor_cast(current_sender()) == on_screen_playhead_) { - send(event_group_, utility::event_atom_v, show_atom_v, buf, "viewport0"); - }*/ + // a playhead is telling us a new frame is being shown. + + // Forward the info to our 'fine grain' message group with details + // of which viewport the frame is being shown on + auto playhead = caf::actor_cast(current_sender()); + for (auto &p : viewports_) { + if (p.second.playhead == playhead) { + } + } }, [=](show_atom, caf::actor media, caf::actor media_source) { - // TODO: cleanup this stuff? - if (caf::actor_cast(current_sender()) == on_screen_playhead_) { - send(event_group_, utility::event_atom_v, show_atom_v, media, media_source); + // a playhead is telling us the on-screen media has changed + auto playhead = caf::actor_cast(current_sender()); + for (auto &p : viewports_) { + if (p.second.playhead == playhead) { + // forward the event, including the name of the viewport(s) + // that are attached to the playhead + mail(utility::event_atom_v, show_atom_v, media, media_source, p.first) + .send(event_group_); + } + } + }, + [=](ui::viewport::viewport_playhead_atom, + const std::string viewport_name) -> result { + if (viewport_name.empty()) + return global_active_playhead_; + if (viewports_.find(viewport_name) != viewports_.end()) { + return viewports_[viewport_name].playhead; + } + return make_error( + xstudio_error::error, fmt::format("No viewport named {}", viewport_name)); + }, + [=](ui::viewport::viewport_atom) -> std::vector { + std::vector result; + for (const auto &p : viewports_) { + result.push_back(p.second.viewport); } + return result; }, - [=](ui::viewport::viewport_playhead_atom) -> caf::actor { return on_screen_playhead_; }, [=](ui::viewport::viewport_atom, const std::string viewport_name, caf::actor viewport) { - viewports_[viewport_name] = caf::actor_cast(viewport); + monitor_it(viewport); + // viewports register themselves by sending us this message + viewports_[viewport_name] = ViewportAndPlayhead({viewport, caf::actor()}); + mail(utility::event_atom_v, ui::viewport::viewport_atom_v, viewport_name, viewport) + .send(event_group_); + }, + [=](playhead::redraw_viewport_atom) { + // force all viewport to do a redraw + for (const auto &p : viewports_) { + anon_mail(playhead::redraw_viewport_atom_v).send(p.second.viewport); + } }, [=](ui::viewport::viewport_atom, const std::string viewport_name) -> result { + // Here we can request a named viewport caf::actor r; auto p = viewports_.find(viewport_name); if (p != viewports_.end()) { - r = caf::actor_cast(p->second); + r = p->second.viewport; } if (!r) return make_error( xstudio_error::error, fmt::format("No viewport named {}", viewport_name)); return r; + }, + [=](ui::viewport::fit_mode_atom atom, + const ui::viewport::FitMode mode, + const ui::viewport::MirrorMode mirror_mode, + const float scale, + const Imath::V2f pan, + const std::string &viewport_name, + const std::string &window_id) { + // viewports tell us when they are zooming/panning, setting fit or mirror mode. + // We broadcast to all other viewports so that they can track (if they want to) + for (const auto &p : viewports_) { + if (p.first != viewport_name) { + mail(atom, mode, mirror_mode, scale, pan, viewport_name, window_id) + .send(p.second.viewport); + } + } + }, + [=](ui::viewport::active_viewport_atom) -> caf::actor { + // find a viewport that lives in the xstudio_main_window + // and is visible + caf::scoped_actor sys(system()); + try { + + for (const auto &p : viewports_) { + auto window_name = utility::request_receive( + *sys, p.second.viewport, utility::name_atom_v, true); + if (window_name == "xstudio_main_window") { + if (utility::request_receive( + *sys, + p.second.viewport, + ui::viewport::viewport_visibility_atom_v)) { + return p.second.viewport; + } + } + } + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + return caf::actor(); }); } \ No newline at end of file diff --git a/src/playhead/src/playhead_selection_actor.cpp b/src/playhead/src/playhead_selection_actor.cpp index e1deee3f5..1510b73fd 100644 --- a/src/playhead/src/playhead_selection_actor.cpp +++ b/src/playhead/src/playhead_selection_actor.cpp @@ -21,35 +21,13 @@ PlayheadSelectionActor::PlayheadSelectionActor( base_(static_cast(jsn["base"])), playlist_(std::move(monitored_playlist)) { - - // how do we restore.. ? - - // // parse and generate tracks/stacks. - // for (const auto& [key, value] : jsn["actors"].items()) { - // if(value["base"]["container"]["type"] == "TrackActor"){ - // try { - // actors_[key] = spawn(playlist, - // static_cast(value)); link_to(actors_[key]); - // } catch (const std::exception &e) { spdlog::error("{}", e.what()); - // } - // } - // else if(value["base"]["container"]["type"] == "ClipActor"){ - // try { - // actors_[key] = spawn(playlist, - // static_cast(value)); link_to(actors_[key]); - // } catch (const std::exception &e) { spdlog::error("{}", e.what()); - // } - // } - // else if(value["base"]["container"]["type"] == "StackActor"){ - // try { - // actors_[key] = spawn(playlist, - // static_cast(value)); link_to(actors_[key]); - // } catch (const std::exception &e) { spdlog::error("{}", e.what()); - // } - // } - // } - init(); + + // a bit clunky, but to finish deserialisation we need to re-select the + // media, thus: + UuidVector serialised_selection = base_.items_vec(); + base_.clear(); + select_media(serialised_selection); } PlayheadSelectionActor::PlayheadSelectionActor( @@ -60,41 +38,11 @@ PlayheadSelectionActor::PlayheadSelectionActor( init(); } +void PlayheadSelectionActor::on_exit() { playlist_ = caf::actor(); } -void PlayheadSelectionActor::init() { - - spdlog::debug("Created PlayheadSelectionActor {}", base_.name()); - print_on_exit(this, "PlayheadSelectionActor"); - event_group_ = spawn(this); - link_to(event_group_); - - caf::scoped_actor sys(system()); - link_to(playlist_); - request(playlist_, infinite, uuid_atom_v) - .then( - [=](const utility::Uuid &uuid) mutable { base_.set_monitored(uuid); }, - [=](error &) {}); - request(playlist_, infinite, playlist::get_change_event_group_atom_v) - .then([=](caf::actor grp) mutable { join_broadcast(this, grp); }, [=](error &) {}); - - set_down_handler([=](down_msg &msg) { - // find removed sources.. - remove_dead_actor(msg.source); - }); - - behavior_.assign( - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), - +caf::message_handler PlayheadSelectionActor::message_handler() { + return caf::message_handler{ [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) {}, [=](playhead::delete_selection_from_playlist_atom) { if (base_.items().empty()) @@ -102,20 +50,16 @@ void PlayheadSelectionActor::init() { utility::Uuid deleted_media_source_uuid = base_.items().front().uuid_; - request( - playlist_, - infinite, - playlist::get_next_media_atom_v, - deleted_media_source_uuid, - -1) + mail(playlist::get_next_media_atom_v, deleted_media_source_uuid, -1) + .request(playlist_, infinite) .then( [=](UuidActor media_actor) mutable { utility::Uuid new_selection_uuid = media_actor.uuid(); - request( - playlist_, infinite, playlist::remove_media_atom_v, base_.items()) + mail(playlist::remove_media_atom_v, base_.items()) + .request(playlist_, infinite) .await( [=](const bool) { - select_media(UuidList({new_selection_uuid})); + select_media(UuidVector({new_selection_uuid})); }, [=](error &err) { spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -127,7 +71,7 @@ void PlayheadSelectionActor::init() { }, [=](media_hook::gather_media_sources_atom) { - anon_send(playlist_, media_hook::gather_media_sources_atom_v, base_.items()); + anon_mail(media_hook::gather_media_sources_atom_v, base_.items()).send(playlist_); }, [=](playhead::evict_selection_from_cache_atom) -> result { @@ -162,11 +106,8 @@ void PlayheadSelectionActor::init() { }, [=](playhead::get_selection_atom, caf::actor requester) { - anon_send( - requester, - utility::event_atom_v, - playhead::selection_changed_atom_v, - base_.items()); + anon_mail(utility::event_atom_v, playhead::selection_changed_atom_v, base_.items()) + .send(requester); }, [=](playhead::select_next_media_atom, const int skip_by) { @@ -175,33 +116,90 @@ void PlayheadSelectionActor::init() { [=](playlist::select_all_media_atom) { select_all(); }, - [=](playlist::select_media_atom, const UuidList &media_uuids) -> bool { + [=](playlist::select_media_atom, const UuidList &media_uuids) { + UuidVector v; + for (auto &i : media_uuids) + v.push_back(i); + + return mail(playlist::select_media_atom_v, v) + .delegate(caf::actor_cast(this)); + }, + + [=](playlist::select_media_atom, const UuidVector &media_uuids, bool retry) -> bool { if (media_uuids.empty()) { - select_one(); + anon_mail(playlist::select_media_atom_v, utility::UuidVector()).send(this); + } else { + select_media(media_uuids, false); + } + return true; + }, + + [=](playlist::select_media_atom, const UuidVector &media_uuids) -> bool { + if (media_uuids.empty()) { + anon_mail(playlist::select_media_atom_v, true) + .delay(std::chrono::milliseconds(500)) + .send(this); } else { select_media(media_uuids); } return true; }, + [=](playlist::select_media_atom, + const UuidVector &media_uuids, + const bool retry, + const playhead::SelectionMode mode) -> bool { + if (media_uuids.empty()) { + anon_mail(playlist::select_media_atom_v, true) + .delay(std::chrono::milliseconds(500)) + .send(this); + } else { + select_media(media_uuids, retry, mode); + } + return true; + }, + + [=](playlist::select_media_atom, + const UuidVector &media_uuids, + const playhead::SelectionMode mode) -> bool { + if (media_uuids.empty()) + anon_mail(playlist::select_media_atom_v, true) + .delay(std::chrono::milliseconds(500)) + .send(this); + else + select_media(media_uuids, true, mode); + + return true; + }, + + [=](playlist::select_media_atom, const bool one) { + if (base_.empty()) + select_one(); + }, + [=](playlist::select_media_atom) -> bool { // clears the selection - select_media(UuidList()); + select_media(UuidVector(), true, SM_CLEAR); return true; }, [=](playlist::select_media_atom, utility::Uuid media_uuid) { - auto modified_selection = base_.items(); - modified_selection.push_back(media_uuid); - select_media(modified_selection); + select_media(UuidVector({media_uuid}), true, SM_SELECT); + }, + + [=](playlist::media_filter_string, const std::string &filter_string) { + filter_string_ = filter_string; }, + [=](playlist::media_filter_string) -> std::string { return filter_string_; }, + + [=](utility::event_atom, utility::change_atom) { if (current_sender() == playlist_) { // the playlist has changed - if it was previously empty but now // (perhaps) has media content then try selecting the first item if (base_.empty()) - anon_send(this, playlist::select_media_atom_v, UuidList()); + anon_mail(playlist::select_media_atom_v, utility::UuidVector()).send(this); } }, @@ -224,86 +222,176 @@ void PlayheadSelectionActor::init() { [=](utility::event_atom, playlist::move_media_atom, const UuidVector &, const Uuid &) { }, [=](utility::event_atom, playlist::remove_media_atom, const UuidVector &) {}, - [=](utility::event_atom, playlist::add_media_atom, utility::UuidActor media) { - if (base_.items().empty()) { - select_one(); - } - } + [=](utility::event_atom, playlist::add_media_atom, const utility::UuidActorVector &) { + if (base_.empty()) + anon_mail(playlist::select_media_atom_v, true) + .delay(std::chrono::milliseconds(500)) + .send(this); + }}; +} + - ); +void PlayheadSelectionActor::init() { + + spdlog::debug("Created PlayheadSelectionActor {}", base_.name()); + print_on_exit(this, "PlayheadSelectionActor"); + + caf::scoped_actor sys(system()); + link_to(playlist_); + mail(uuid_atom_v) + .request(playlist_, infinite) + .then( + [=](const utility::Uuid &uuid) mutable { base_.set_monitored(uuid); }, + [=](error &) {}); + mail(playlist::get_change_event_group_atom_v) + .request(playlist_, infinite) + .then([=](caf::actor grp) mutable { join_broadcast(this, grp); }, [=](error &) {}); } -void PlayheadSelectionActor::select_media(const UuidList &media_uuids) { - if (base_.items_vec() == std::vector(media_uuids.begin(), media_uuids.end())) { +void PlayheadSelectionActor::select_media( + const UuidVector &media_uuids, const bool retry, const SelectionMode mode) { + // this depends on mode... + + // build list of selected / deselected + + // spdlog::warn("select_media {} {}", retry, mode); + // for(const auto &i: media_uuids) + // spdlog::warn("{}", to_string(i)); + + if (mode == SM_NO_UPDATE) return; - } - if (media_uuids.empty()) { - for (const auto &i : source_actors_) - demonitor(i.second); - source_actors_.clear(); - base_.clear(); - send( - event_group_, - utility::event_atom_v, - playhead::source_atom_v, - std::vector()); - } else { + auto selected = UuidVector(); + auto deselected = UuidVector(); + + if (mode == SM_CLEAR) + deselected = base_.items_vec(); + else if (mode == SM_DESELECT) + deselected = media_uuids; + else if (mode == SM_SELECT) + selected = media_uuids; + else if (mode == SM_TOGGLE) { + auto current_selection = base_.items_vec(); + auto current_selection_set = + UuidSet(current_selection.begin(), current_selection.end()); + for (const auto &i : media_uuids) { + if (current_selection_set.count(i)) + deselected.push_back(i); + else + selected.push_back(i); + } + } else if (mode == SM_CLEAR_AND_SELECT) { + deselected = base_.items_vec(); + selected = media_uuids; + } - // fetch all the media in the playlist - request(playlist_, infinite, playlist::get_media_atom_v) + if (not selected.empty()) { + // grab media from playlist.. + mail(playlist::get_media_atom_v) + .request(playlist_, infinite) .then( [=](const std::vector &media_actors) mutable { - // re can have nasty races.. - // we should try and give a grace period + // It's possible that a client has told us to select a piece + // of media that the playlist hasn't quite got around to + // adding yet. e.g. client creates media, adds to playlist + // and then tells PlayheadSelectionActor to select it. + // Due to async actors the Playlist might not have the new + // media quite yet.. in this case order a retry + + // check to retry auto media_uas = uuidactor_vect_to_map(media_actors); - for (const auto &i : media_uuids) { - if (not media_uas.count(i)) - return delayed_anon_send( - caf::actor_cast(this), - std::chrono::milliseconds(50), - playlist::select_media_atom_v, - media_uuids); + if (retry) { + for (const auto &i : selected) { + if (not media_uas.count(i) || !media_uas[i]) { + anon_mail( + playlist::select_media_atom_v, media_uuids, false, mode) + .delay(std::chrono::milliseconds(500)) + .send(caf::actor_cast(this)); + + return; + } + } } - for (const auto &i : source_actors_) - demonitor(i.second); - source_actors_.clear(); - base_.clear(); - - std::vector selected_media_actors; - - for (auto p : media_uuids) { - if (media_uas.count(p)) { - insert_actor(media_uas[p], p, Uuid()); - selected_media_actors.push_back(media_uas[p]); + for (const auto &i : deselected) { + if (auto currently_selected = source_actors_.find(i); + currently_selected != source_actors_.end()) { + // part of current selection.. + + if (auto mit = monitor_.find(caf::actor_cast( + currently_selected->second)); + mit != std::end(monitor_)) { + mit->second.dispose(); + monitor_.erase(mit); + } + source_actors_.erase(currently_selected); + base_.remove_item(i); } } + // some uuids might be bogus.. + // we ignore them.. + for (const auto &i : selected) { + // make sure they not already active.. + if (not media_uas.count(i)) { + spdlog::debug("Invalid media uuid in selection {}", to_string(i)); + } else if (media_uas.at(i)) { + if (not source_actors_.count(i)) { + // add to selection.. + insert_actor(media_uas.at(i), i, Uuid()); + } + } + } + + auto tmp = std::vector(); + for (const auto &i : base_.items_vec()) + tmp.push_back(source_actors_[i]); - send( - event_group_, - utility::event_atom_v, - playhead::source_atom_v, - selected_media_actors); + mail(utility::event_atom_v, playhead::source_atom_v, tmp) + .send(base_.event_group()); }, [=](error &err) mutable { spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); + } else { + // deselect first + // then select as order matters. + for (const auto &i : deselected) { + if (auto currently_selected = source_actors_.find(i); + currently_selected != source_actors_.end()) { + // part of current selection.. + if (auto mit = monitor_.find( + caf::actor_cast(currently_selected->second)); + mit != std::end(monitor_)) { + mit->second.dispose(); + monitor_.erase(mit); + } + + source_actors_.erase(currently_selected); + base_.remove_item(i); + } + } + + // we're done flush changes. + // order matters.. + auto tmp = std::vector(); + for (const auto &i : base_.items_vec()) + tmp.push_back(source_actors_[i]); + mail(utility::event_atom_v, playhead::source_atom_v, tmp).send(base_.event_group()); } } void PlayheadSelectionActor::select_one() { - // fetch all the media in the playlist - request(playlist_, infinite, playlist::get_media_atom_v) + mail(playlist::get_media_atom_v) + .request(playlist_, infinite) .then( [=](std::vector media_actors) mutable { - if (!media_actors.empty()) { + if (not media_actors.empty()) { // select only the first item in the playlist - select_media(UuidList({media_actors[0].uuid()})); + select_media(UuidVector({media_actors[0].uuid()})); } else { - select_media(UuidList()); + select_media(UuidVector(), true, SM_CLEAR); } }, [=](error &err) mutable { @@ -314,11 +402,12 @@ void PlayheadSelectionActor::select_one() { void PlayheadSelectionActor::select_all() { // fetch all the media in the playlist - request(playlist_, infinite, playlist::get_media_atom_v) + mail(playlist::get_media_atom_v) + .request(playlist_, infinite) .then( [=](std::vector media_actors) mutable { // make a list of all the media item uuids in the plauylist - UuidList l; + UuidVector l; for (auto i : media_actors) { l.push_back(i.uuid()); } @@ -341,16 +430,11 @@ void PlayheadSelectionActor::select_next_media_item(const int skip_by) { // get the next item in the playlist using the uuid of the current item // in the selection - request( - playlist_, - infinite, - playlist::get_next_media_atom_v, - current_media_source_uuid, - skip_by) + mail(playlist::get_next_media_atom_v, current_media_source_uuid, skip_by, filter_string_) + .request(playlist_, infinite) .then( [=](UuidActor media_actor) mutable { - UuidList l({media_actor.uuid()}); - select_media(l); + select_media(UuidVector({media_actor.uuid()})); }, [=](error &err) mutable { spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -364,19 +448,21 @@ void PlayheadSelectionActor::insert_actor( // we wrap sources in selection_clip.. base_.insert_item(media_uuid, before_uuid); source_actors_[media_uuid] = actor; - monitor(actor); -} -void PlayheadSelectionActor::remove_dead_actor(caf::actor_addr actor) { - utility::Uuid media_uuid; - for (const auto &i : source_actors_) { - if (caf::actor_cast(i.second) == actor) - media_uuid = i.first; - } + auto act_addr = caf::actor_cast(actor); + if (auto sit = monitor_.find(act_addr); sit == std::end(monitor_)) { + monitor_[act_addr] = monitor(actor, [this, addr = actor.address()](const error &) { + if (auto mit = monitor_.find(caf::actor_cast(addr)); + mit != std::end(monitor_)) + monitor_.erase(mit); - if (not media_uuid.is_null()) { - demonitor(source_actors_[media_uuid]); - base_.remove_item(media_uuid); - source_actors_.erase(media_uuid); + for (const auto &i : source_actors_) { + if (i.second == addr and not i.first.is_null()) { + base_.remove_item(i.first); + source_actors_.erase(i.first); + break; + } + } + }); } } diff --git a/src/playhead/src/retime_actor.cpp b/src/playhead/src/retime_actor.cpp deleted file mode 100644 index 9821745f4..000000000 --- a/src/playhead/src/retime_actor.cpp +++ /dev/null @@ -1,467 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/broadcast/broadcast_actor.hpp" -#include "xstudio/playhead/retime_actor.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" - -using namespace xstudio; -using namespace xstudio::playhead; -using namespace xstudio::utility; -using namespace xstudio::timeline; -using namespace caf; - -RetimeActor::RetimeActor( - caf::actor_config &cfg, - const std::string &name, - caf::actor &source, - const media::MediaType mt) - : caf::event_based_actor(cfg), source_(source), frames_offset_(0) { - - spdlog::debug("Created RetimeActor {}", name); - print_on_exit(this, "RetimeActor"); - - event_group_ = spawn(this); - link_to(event_group_); - - // N.B. this is blocking - join_event_group(this, source); - - get_source_edit_list(mt); - - behavior_.assign( - [=](utility::event_atom, utility::change_atom) { - // my sources have changed.. - caf::scoped_actor sys(system()); - - get_source_edit_list(mt); - - // if duration was previously set externally, we want to retain that - set_duration(forced_duration_); - send(event_group_, utility::event_atom_v, utility::change_atom_v); - }, - [=](utility::event_atom, utility::last_changed_atom, const time_point &) { - send(event_group_, utility::event_atom_v, utility::change_atom_v); - }, - - [=](utility::event_atom, - media::add_media_source_atom, - const utility::UuidActorVector &uav) { - send(event_group_, utility::event_atom_v, media::add_media_source_atom_v, uav); - }, - - [=](utility::event_atom, - playlist::reflag_container_atom, - const utility::Uuid &, - const std::tuple &) {}, - [=](utility::event_atom, - bookmark::bookmark_change_atom, - const utility::Uuid &bookmark_uuid) { - send( - event_group_, - utility::event_atom_v, - bookmark::bookmark_change_atom_v, - bookmark_uuid); - }, - - [=](utility::event_atom, media::media_status_atom, const media::MediaStatus ms) {}, - [=](utility::event_atom, - media::current_media_source_atom, - UuidActor &, - const media::MediaType) { - caf::scoped_actor sys(system()); - - get_source_edit_list(mt); - // if duration was previously set externally, we want to retain that - set_duration(forced_duration_); - send(event_group_, utility::event_atom_v, utility::change_atom_v); - }, - - [=](const error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }, - - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) {}, - - [=](utility::uuid_atom) { delegate(source_, utility::uuid_atom_v); }, - - [=](timeline::duration_atom, const timebase::flicks &new_duration) -> bool { - set_duration(new_duration); - return true; - }, - - [=](duration_flicks_atom, - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate) -> result { - return retime_edit_list_.duration_flicks(tsm, override_rate); - }, - - [=](duration_frames_atom, - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate) -> result { - // spdlog::warn("retime duration_frames_atom {}", - // (int)retime_edit_list_.duration_frames()); spdlog::warn("retime - // duration_frames_atom {}", (int)retime_edit_list_.duration_frames(tsm, - // override_rate)); - return (int)retime_edit_list_.duration_frames(tsm, override_rate); - }, - - [=](playhead::overflow_mode_atom) -> int { return static_cast(overflow_mode_); }, - - [=](media::get_edit_list_atom, const Uuid & /*override_uuid*/) -> utility::EditList { - // spdlog::warn("get_edit_list_atom {}", __PRETTY_FUNCTION__); - // we don't use the override uuid, instead the uuid in the edit list correspond to - // the source - return retime_edit_list_; - }, - - [=](media::get_media_pointer_atom atom, const int logical_frame) { - delegate(caf::actor_cast(this), atom, media::MT_IMAGE, logical_frame); - }, - - [=](media::get_media_pointer_atom, - const media::MediaType media_type, - const int logical_frame) -> result { - auto rp = make_response_promise(); - - RetimeFrameResult retime_result = IN_RANGE; - int retimed_logical_frame = apply_retime(logical_frame, retime_result); - - if (retime_result == FAIL) { - rp.deliver(make_error(xstudio_error::error, "No frames left")); - } else if (retime_result == OUT_OF_RANGE) { - rp.deliver(*(media::make_blank_frame(media_type))); - } else { - - request( - source_, - infinite, - media::get_media_pointer_atom_v, - media_type, - retimed_logical_frame) - .then( - [=](media::AVFrameID &mp) mutable { - if (retime_result == HELD_FRAME) { - mp.params_["HELD_FRAME"] = true; - } - rp.deliver(mp); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - } - return rp; - }, - - [=](media::get_media_pointers_atom, - const media::MediaType media_type, - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate) -> result { - auto rp = make_response_promise(); - auto result = std::make_shared(); - recursive_deliver_all_media_pointers( - tsm, override_rate, media_type, 0, timebase::flicks(0), result, rp); - return rp; - }, - - [=](media::source_offset_frames_atom) -> int { return frames_offset_; }, - - [=](media::source_offset_frames_atom, const int offset) -> bool { - frames_offset_ = offset; - send( - event_group_, - utility::event_atom_v, - media::source_offset_frames_atom_v, - frames_offset_); - send(event_group_, utility::event_atom_v, utility::change_atom_v); - return true; - }, - - [=](playhead::media_atom, const int logical_frame) -> result { - auto rp = make_response_promise(); - - RetimeFrameResult retime_result = IN_RANGE; - apply_retime(logical_frame, retime_result); - - if (retime_result == FAIL) { - rp.deliver(make_error(xstudio_error::error, "No frames left")); - } else if (retime_result == OUT_OF_RANGE) { - rp.deliver(make_error(xstudio_error::error, "Out of range")); - } else { - rp.deliver(source_); - } - return rp; - }, - - [=](playhead::flicks_to_logical_frame_atom, - const timebase::flicks flicks, - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate) -> result { - // convert between type limits - if (flicks == - timebase::flicks(std::numeric_limits::lowest())) - return std::numeric_limits::lowest(); - if (flicks == timebase::flicks(std::numeric_limits::max())) - return std::numeric_limits::max(); - - try { - return retime_edit_list_.logical_frame(tsm, flicks, override_rate); - } catch (std::exception &e) { - return make_error(xstudio_error::error, e.what()); - } - }, - - [=](playlist::get_media_uuid_atom) -> result { - auto rp = make_response_promise(); - request(source_, infinite, utility::uuid_atom_v) - .then( - [=](const utility::Uuid &uuid) mutable { rp.deliver(uuid); }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - return rp; - }, - - [=](playhead::logical_frame_to_flicks_atom, - const int logical_frame, - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate) -> result { - // convert between type limits - if (logical_frame == std::numeric_limits::lowest()) - return timebase::flicks(std::numeric_limits::lowest()); - if (logical_frame == std::numeric_limits::max()) - return timebase::flicks(std::numeric_limits::max()); - - try { - return retime_edit_list_.flicks_from_frame(tsm, logical_frame, override_rate); - } catch (std::exception &e) { - return make_error(xstudio_error::error, e.what()); - } - }, - - [=](rate_atom, const int logical_frame) -> result { - try { - return retime_edit_list_.frame_rate_at_frame(logical_frame - frames_offset_); - } catch (std::exception &e) { - return make_error(xstudio_error::error, e.what()); - } - }, - - [=](utility::event_atom, timeline::item_atom, const utility::JsonStore &changes, bool) { - // ignoring timeline events - }, - - [=](utility::get_event_group_atom) -> caf::actor { return event_group_; }); -} - -void RetimeActor::set_duration(const timebase::flicks &new_duration) { - - forced_duration_ = new_duration; - if (forced_duration_.count()) { - retime_edit_list_.clear(); - auto clip = source_edit_list_.section_list()[0]; - clip.frame_rate_and_duration_.set_duration(new_duration); - retime_edit_list_.append(clip); - } else { - retime_edit_list_ = source_edit_list_; - } - send(event_group_, utility::event_atom_v, utility::change_atom_v); -} - -int RetimeActor::apply_retime(const int logical_frame, RetimeFrameResult &retime_result) { - int retimed_frame = logical_frame - frames_offset_; - const int duration_frames = source_edit_list_.duration_frames(TimeSourceMode::DYNAMIC); - if (retimed_frame < 0) { - if (overflow_mode_ == OM_FAIL) { - retime_result = FAIL; - } else if (overflow_mode_ == OM_NULL) { - retime_result = OUT_OF_RANGE; - } else if (overflow_mode_ == OM_HOLD) { - retimed_frame = 0; - retime_result = HELD_FRAME; - } else if (overflow_mode_ == OM_LOOP) { - while (retimed_frame < 0) { - retimed_frame += duration_frames; - } - retime_result = LOOPED_RANGE; - } - } else if (retimed_frame >= duration_frames) { - if (overflow_mode_ == OM_FAIL) { - retime_result = FAIL; - } else if (overflow_mode_ == OM_NULL) { - retime_result = OUT_OF_RANGE; - } else if (overflow_mode_ == OM_HOLD) { - retimed_frame = duration_frames - 1; - retime_result = HELD_FRAME; - } else if (overflow_mode_ == OM_LOOP) { - retimed_frame %= duration_frames; - retime_result = LOOPED_RANGE; - } - } else { - retime_result = IN_RANGE; - } - return retimed_frame; -} - -void RetimeActor::recursive_deliver_all_media_pointers( - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate, - const media::MediaType media_type, - const int clip_index, - const timebase::flicks clip_start_time_point, - std::shared_ptr result, - caf::typed_response_promise rp) { - - // this function is crucial. It works by recursively self calling until the - // 'clip_index' is greater than the number of clips (incrementind clip_index) - // in this edit list. - // We use it to get a full list of media pointers for this edit list from - // start to finish. A 'AVFrameID' is a struct that contains all the - // information we need to read/retrieve a frame of video/audio data. Along - // with the media pointers we also get the associated timepoint for each - // media pointer, that is where it lies on the timeline (or when they should - // be displayed if we start playback from time=0) - - if (clip_index >= (int)source_edit_list_.size()) { - // we're done, we've exhausted the number of clips. Deliver the result - // and return - // Now we need something funky ... - // we meed to add a timepoint at the end so we know when the - // last frame's duration runs out. To understand this, imagine we have - // a source with a single frame. It shows at time=0 but it should be - // on screen for 1/fps seconds ... so we add a timepoint with a null - // media pointer at this time. Note that we increment time_point for - // every frame that's been added to result, so it time_point is already - // where we need it - (*result)[clip_start_time_point].reset(); - rp.deliver(*result); - return; - } - - // the TP of the first frame in the current clip - timebase::flicks time_point = clip_start_time_point; - - // get a copy of the source clip - const auto clip = source_edit_list_.section_list()[clip_index]; - - const utility::Timecode tc = clip.timecode_; - - // number of logical frames in the source clip - const int num_clip_frames = clip.frame_rate_and_duration_.frames( - tsm == TimeSourceMode::FIXED ? override_rate : FrameRate()); - - // get the retimed duration - int retimed_duration = (int)retime_edit_list_.duration_frames(tsm, override_rate); - - // now we get media all pointers for the source clip ... we then apply - // the retime to those pointers - - request( - source_, - infinite, - media::get_media_pointers_atom_v, - media_type, - // media::LogicalFrameRanges({{0, std::max(0,num_clip_frames-1)}}), - media::LogicalFrameRanges({{0, num_clip_frames - 1}}), - override_rate) - .await( - [=](const media::AVFrameIDs &mps) mutable { - if ((int)mps.size() != num_clip_frames) { - rp.deliver(make_error( - xstudio_error::error, - "RetimeActor::recursive_deliver_all_media_pointers media pointers " - "returned by media source not matching requested number of frames.")); - return; - } - - for (int f = 0; f < retimed_duration; f++) { - - RetimeFrameResult r; - int retime_frame = apply_retime(f, r); - const int playhead_logical_frame = result->size(); - - if (r == FAIL) { - rp.deliver(make_error(xstudio_error::error, "No frames left")); - return; - } else if (retime_frame >= 0 && retime_frame < (int)mps.size()) { - if (r == IN_RANGE) { - // re-use the pointer we were given - (*result)[time_point] = mps[retime_frame]; - } else { - // dupliacte frame, need to duplicate the data - media::AVFrameID mptr(*mps[retime_frame]); - if (r == HELD_FRAME) { - mptr.params_["HELD_FRAME"] = true; - } - (*result)[time_point] = - std::make_shared(mptr); - } - const_cast((*result)[time_point].get())->timecode_ = - tc + f - frames_offset_; - - } else { // OUT_OF_RANGE - (*result)[time_point] = media::make_blank_frame(media_type); - } - - // set the logical frame - const_cast((*result)[time_point].get()) - ->playhead_logical_frame_ = playhead_logical_frame; - - time_point += tsm == TimeSourceMode::FIXED - ? override_rate - : clip.frame_rate_and_duration_.rate(); - } - - recursive_deliver_all_media_pointers( - tsm, override_rate, media_type, clip_index + 1, time_point, result, rp); - }, - [=](error &err) mutable { - // Something is wrong with the media source (e.g. file is not - // on the file system). Make blank frames instead and add the - // error message - - auto blank_frame = media::make_blank_frame(media_type); - auto *m_ptr = const_cast(blank_frame.get()); - m_ptr->error_ = to_string(err); - - for (int f = 0; f < num_clip_frames; f++) { - (*result)[time_point] = blank_frame; - time_point += tsm == TimeSourceMode::FIXED - ? override_rate - : clip.frame_rate_and_duration_.rate(); - } - - recursive_deliver_all_media_pointers( - tsm, override_rate, media_type, clip_index + 1, time_point, result, rp); - }); -} - -void RetimeActor::get_source_edit_list(const media::MediaType mt) { - - caf::scoped_actor sys(system()); - try { - - // we try and get the edit list for the desired media type ... however, if - // we can't provide one (say we want MT_IMAGE and the media_source only - // provides MT_AUDIO sources) we retry and continue. This means that the - // playhead can 'play' a source that has no video and audio only - the audio - // only source will provide empty video frames so playback can still happen. - - source_edit_list_ = - request_receive(*sys, source_, media::get_edit_list_atom_v, mt, Uuid()); - retime_edit_list_ = source_edit_list_; - - } catch (std::exception & /*e*/) { - - try { - source_edit_list_ = request_receive( - *sys, - source_, - media::get_edit_list_atom_v, - mt == media::MT_AUDIO ? media::MT_IMAGE : media::MT_AUDIO, - Uuid()); - retime_edit_list_ = source_edit_list_; - } catch (std::exception & /*e*/) { - } - - // suppressing 'no streams' warning for dud media - // spdlog::critical("{} {}", __PRETTY_FUNCTION__, e.what()); - } -} \ No newline at end of file diff --git a/src/playhead/src/string_out_actor.cpp b/src/playhead/src/string_out_actor.cpp new file mode 100644 index 000000000..c8437ccc5 --- /dev/null +++ b/src/playhead/src/string_out_actor.cpp @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include + +#include "xstudio/atoms.hpp" +#include "xstudio/broadcast/broadcast_actor.hpp" +#include "xstudio/playhead/string_out_actor.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/logging.hpp" +#include "xstudio/utility/media_reference.hpp" +#include "xstudio/thumbnail/thumbnail.hpp" + +using namespace xstudio; +using namespace xstudio::playhead; +using namespace xstudio::media_reader; + +StringOutActor::StringOutActor(caf::actor_config &cfg, const utility::UuidActorVector &sources) + : caf::event_based_actor(cfg), source_actors_(sources) { + + event_group_ = spawn(this); + link_to(event_group_); + + for (auto &source : source_actors_) { + monitor(source.actor(), [this, addr = source.actor().address()](const error &) { + auto p = source_actors_.begin(); + bool change = false; + while (p != source_actors_.end()) { + if ((*p).actor() == addr) { + p = source_actors_.erase(p); + change = true; + } else { + p++; + } + } + if (change) { + mail(utility::event_atom_v, utility::change_atom_v).send(event_group_); + } + }); + + utility::join_event_group(this, source.actor()); + } + + // Note - out source_actors_ will usually be MediaActors, but might be + // A TimelineActor or a ClipActor. The event_atom handlers below do nothing + // as we're not interested in the messages except for an event_atom, change_atom + // message. If we get one of these we re-emit it to our event_group_. The + // SubPlayhead that is playing us will then respond by fetching our frames + // from us. + + behavior_.assign( + + [=](utility::get_event_group_atom) -> caf::actor { return event_group_; }, + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + + [=](media::get_media_pointers_atom atom, + const media::MediaType media_type, + const utility::TimeSourceMode tsm, + const utility::FrameRate &override_rate) -> caf::result { + auto rp = make_response_promise(); + build_frame_map(media_type, tsm, override_rate, rp); + return rp; + }, + + [=](utility::event_atom, playlist::add_media_atom, const utility::UuidActorVector &) {}, + + [=](utility::event_atom, playlist::remove_media_atom, const utility::UuidVector &) {}, + + [=](utility::event_atom, + media_reader::get_thumbnail_atom, + const thumbnail::ThumbnailBufferPtr &buf) {}, + + [=](utility::event_atom, + media::add_media_source_atom, + const utility::UuidActorVector &uav) { + mail(utility::event_atom_v, media::add_media_source_atom_v, uav).send(event_group_); + }, + + [=](utility::event_atom, timeline::item_atom, const utility::JsonStore &changes, bool) { + }, + + [=](utility::event_atom, media::media_status_atom, const media::MediaStatus) {}, + + [=](utility::event_atom, + media::media_display_info_atom, + const utility::JsonStore &, + caf::actor_addr &) {}, + + [=](utility::event_atom, media::media_display_info_atom, const utility::JsonStore &) {}, + + [=](utility::event_atom, + media::current_media_source_atom, + utility::UuidActor &a, + const media::MediaType mt) { + mail(utility::event_atom_v, media::current_media_source_atom_v, a, mt) + .send(event_group_); + }, + + [=](utility::event_atom, utility::change_atom) { + // one of the sources has changed - this might affect its actual + // frames for display. So we clear the entry in our cache, and + // re-emit the change event to SubPlayhead + auto sender = caf::actor_cast(current_sender()); + for (auto &s : source_actors_) { + if (s.actor() == sender) { + source_frames_[s.uuid()].reset(); + } + } + mail( + utility::event_atom_v, + utility::change_atom_v) + .send(event_group_); // triggers refresh of frames_time_list_ + }, + + [=](utility::event_atom, utility::last_changed_atom, const utility::time_point &) { + return mail(utility::event_atom_v, utility::change_atom_v) + .delegate(caf::actor_cast(this)); + }, + + [=](utility::rate_atom atom, const media::MediaType media_type) -> utility::FrameRate { + return utility::FrameRate(timebase::k_flicks_24fps); + }, + + [=](utility::event_atom, utility::name_atom, const std::string & /*name*/) {}, + + [=](utility::event_atom, + bookmark::bookmark_change_atom, + const utility::Uuid &bookmark_uuid) {}, + + [=](utility::event_atom, + bookmark::bookmark_change_atom, + const utility::Uuid &, + const utility::UuidList &) {}, + + [=](utility::event_atom, + playlist::reflag_container_atom, + const utility::Uuid &, + const std::tuple &) {}); +} + +void StringOutActor::build_frame_map( + const media::MediaType media_type, + const utility::TimeSourceMode tsm, + const utility::FrameRate &override_rate, + caf::typed_response_promise rp) { + + + auto count = std::make_shared(0); + + for (auto &c : source_actors_) { + // we already have frames for this source + if (source_frames_[c.uuid()]) + continue; + + (*count)++; + + auto source_uuid = c.uuid(); + mail(media::get_media_pointers_atom_v, media_type, tsm, override_rate) + .request(c.actor(), infinite) + .then( + [=](const media::FrameTimeMapPtr &mpts) mutable { + source_frames_[source_uuid] = mpts; + (*count)--; + if (!(*count)) { + finalise_frame_map(rp); + } + }, + [=](caf::error &err) mutable { + (*count)--; + if (!(*count)) { + finalise_frame_map(rp); + } + }); + } + + if (!(*count)) { + finalise_frame_map(rp); + } +} + +void StringOutActor::finalise_frame_map( + caf::typed_response_promise rp) { + + media::FrameTimeMap *result = new media::FrameTimeMap; + timebase::flicks d(0); + for (auto &c : source_actors_) { + + if (!source_frames_[c.uuid()]) + continue; + const media::FrameTimeMap &map = *(source_frames_[c.uuid()]); + timebase::flicks prev(0); + for (const auto &p : map) { + d += p.first - prev; + (*result)[d] = p.second; + prev = p.first; + } + } + rp.deliver(media::FrameTimeMapPtr(result)); +} diff --git a/src/playhead/src/sub_playhead.cpp b/src/playhead/src/sub_playhead.cpp index 73d7d7f38..0083b24ff 100644 --- a/src/playhead/src/sub_playhead.cpp +++ b/src/playhead/src/sub_playhead.cpp @@ -3,6 +3,7 @@ #include #include +#include #include "xstudio/atoms.hpp" #include "xstudio/bookmark/bookmark.hpp" @@ -10,10 +11,10 @@ #include "xstudio/global_store/global_store.hpp" #include "xstudio/media_reader/media_reader_actor.hpp" #include "xstudio/playhead/sub_playhead.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/media_reference.hpp" +#include "xstudio/thumbnail/thumbnail.hpp" using namespace xstudio; using namespace xstudio::utility; @@ -26,39 +27,55 @@ using namespace caf; SubPlayhead::SubPlayhead( caf::actor_config &cfg, const std::string &name, - caf::actor source, + utility::UuidActor source, caf::actor parent, + const bool source_is_timeline, const timebase::flicks loop_in_point, const timebase::flicks loop_out_point, const utility::TimeSourceMode time_source_mode, const utility::FrameRate override_frame_rate, - const media::MediaType media_type) + const media::MediaType media_type, + const utility::Uuid &uuid) : caf::event_based_actor(cfg), - base_(name, "ChildPlayhead"), + name_(name), source_(std::move(source)), parent_(std::move(parent)), + source_is_timeline_(source_is_timeline), loop_in_point_(loop_in_point), loop_out_point_(loop_out_point), time_source_mode_(time_source_mode), override_frame_rate_(override_frame_rate), - media_type_(media_type) { + media_type_(media_type), + uuid_(uuid) { init(); } +SubPlayhead::~SubPlayhead() {} + +void SubPlayhead::on_exit() { + parent_ = caf::actor(); + source_ = utility::UuidActor(); + current_media_actor_ = caf::actor(); +} + void SubPlayhead::init() { // if (parent_) // link_to(parent_); // get global reader and steal mrm.. - spdlog::debug("Created SubPlayhead {}", base_.name()); + spdlog::debug("Created SubPlayhead {}", name_); // print_on_exit(this, "SubPlayhead"); + auto global_prefs_actor = caf::actor(); + try { auto prefs = GlobalStoreHelper(system()); JsonStore j; + + global_prefs_actor = prefs.get_jsonactor(); join_broadcast(this, prefs.get_group(j)); pre_cache_read_ahead_frames_ = preference_value(j, "/core/playhead/read_ahead"); static_cache_delay_milliseconds_ = std::chrono::milliseconds( @@ -75,84 +92,83 @@ void SubPlayhead::init() { link_to(event_group_); // subscribe to source.. - if (!source_) { - throw std::runtime_error("Creating child playhead actor without a source."); + if (source_) { + + monitor(source_.actor(), [this, addr = source_.actor().address()](const error &err) { + if (addr == source_.actor()) { + spdlog::debug("ChildPlayhead source down: {}", to_string(err)); + source_ = utility::UuidActor(); + } + }); + + utility::join_event_group(this, source_); + // we need a snwsible default rate to pad out frames if we have to extend + // our duration + mail(utility::rate_atom_v, media_type_) + .request(source_.actor(), infinite) + .then( + [=](const utility::FrameRate &r) mutable { default_rate_ = r; }, + [=](const caf::error &err) mutable {}); } - monitor(source_); + // this triggers us to fetch the frames information from the source + anon_mail(source_atom_v).send(this); - set_down_handler([=](down_msg &msg) { - // is source down? - if (msg.source == source_) { - spdlog::debug("ChildPlayhead source down: {}", to_string(msg.reason)); - source_ = caf::actor(); - } - }); + behavior_.assign( - request(source_, std::chrono::milliseconds(240), utility::get_event_group_atom_v) - .then( - [=](caf::actor grp) mutable { - request(grp, caf::infinite, broadcast::join_broadcast_atom_v) - .then( - [=](const bool) mutable {}, - [=](const error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - }, - [=](const caf::error &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(e)); - }); + // [=](caf::message &msg) -> caf::skippable_result { + // // UNCOMMENT TO DEBUG UNEXPECT MESSAGES - // this triggers us to fetch the frames information from the source - anon_send(this, source_atom_v); + // spdlog::warn( + // "Got unwanted messate from {} {}", to_string(current_sender()), + // to_string(msg)); - // this ensures that all pre-cache requests are removed - set_exit_handler([=](scheduled_actor *a, caf::exit_msg &m) { - anon_send(pre_reader_, clear_precache_queue_atom_v, base_.uuid()); - default_exit_handler(a, m); - }); + // return message{}; + // }, - set_default_handler( - [this](caf::scheduled_actor *, caf::message &msg) -> caf::skippable_result { - // UNCOMMENT TO DEBUG UNEXPECT MESSAGES + [=](caf::exit_msg &msg) { + anon_mail(clear_precache_queue_atom_v, uuid_).send(pre_reader_); + quit(msg.reason); + }, - spdlog::warn( - "Got unwanted messate from {} {}", to_string(current_sender()), to_string(msg)); + [=](utility::event_atom, utility::notification_atom, const utility::JsonStore &) {}, - return message{}; - }); + [=](utility::uuid_atom) -> utility::Uuid { return uuid_; }, - behavior_.assign( - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), + [=](get_event_group_atom) -> caf::actor { return event_group_; }, [=](actual_playback_rate_atom) -> result { auto rp = make_response_promise(); - request( - caf::actor_cast(this), infinite, media::get_media_pointer_atom_v) + mail(media::get_media_pointer_atom_v) + .request(caf::actor_cast(this), infinite) .then( - [=](const media::AVFrameID &id) mutable { rp.deliver(id.rate_); }, + [=](const media::AVFrameID &id) mutable { rp.deliver(id.rate()); }, [=](const caf::error &err) mutable { rp.deliver(err); }); return rp; }, [=](clear_precache_queue_atom) { - delegate(pre_reader_, clear_precache_queue_atom_v, base_.uuid()); + return mail(clear_precache_queue_atom_v, uuid_).delegate(pre_reader_); }, [=](const error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }, [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) { - // if(msg.source == store_events) - // unsubscribe(); + + [=](source_atom, utility::time_point update_tp) -> result { + // the message will be sent with a delay - the time it was sent + // is update_tp, which, if it matches last_change_timepoint_, then + // no other updates have been requested since so we continue with + // the develop + auto rp = make_response_promise(); + if (last_change_timepoint_ == update_tp) { + get_full_timeline_frame_list(rp); + } else { + // a new update request has been issued since the message we're + // processing now was sent so we can skip. + rp.deliver(caf::actor()); + } + return rp; }, [=](source_atom) -> result { @@ -161,69 +177,107 @@ void SubPlayhead::init() { return rp; }, + [=](source_atom, bool /*retry*/) -> result { + auto rp = make_response_promise(); + get_full_timeline_frame_list(rp); + return rp; + }, + + [=](utility::event_atom, playlist::add_media_atom, const utility::UuidActorVector &) {}, + + [=](utility::event_atom, playlist::remove_media_atom, const utility::UuidVector &) {}, + + [=](utility::event_atom, + playlist::move_media_atom, + const utility::UuidVector &, + const utility::Uuid &) {}, + + [=](utility::event_atom, + media_reader::get_thumbnail_atom, + const thumbnail::ThumbnailBufferPtr &buf) {}, + [=](utility::event_atom, media::current_media_source_atom, - UuidActor &, + UuidActor &a, const media::MediaType) { - anon_send(this, source_atom_v); // triggers refresh of frames_time_list_ + up_to_date_ = false; + anon_mail(source_atom_v).send(this); // triggers refresh of frames_time_list_ }, [=](timeline::duration_atom, const timebase::flicks &new_duration) -> result { - // request to force a new duration on the source, need to update - // our full_timeline_frames_ afterwards - auto rp = make_response_promise(); - request(source_, infinite, timeline::duration_atom_v, new_duration) + forced_duration_ = new_duration; + update_retiming(); + anon_mail(bookmark::get_bookmarks_atom_v, true).send(this); + return true; + }, + + [=](duration_flicks_atom atom, + bool /*before retiming, extension*/) -> result { + if (up_to_date_) { + if (full_timeline_frames_.size() < 2) { + return timebase::flicks(0); + } + return full_timeline_frames_.rbegin()->first; + } + // not up to date, we need to get the timeline frames list from + // the source + auto rp = make_response_promise(); + mail(source_atom_v) + .request(caf::actor_cast(this), infinite) .then( - [=](bool) mutable { - request(caf::actor_cast(this), infinite, source_atom_v) - .then( - [=](caf::actor) mutable { rp.deliver(true); }, - [=](const error &err) mutable { rp.deliver(err); }); + [=](caf::actor) mutable { + if (full_timeline_frames_.size() < 2) { + rp.deliver(timebase::flicks(0)); + } else { + rp.deliver(full_timeline_frames_.rbegin()->first); + } }, - [=](caf::error &err) mutable { rp.deliver(err); }); + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(timebase::flicks(0)); + }); return rp; }, [=](duration_flicks_atom atom) -> result { if (up_to_date_) { - if (full_timeline_frames_.size() < 2) { + if (retimed_frames_.size() < 2) { return timebase::flicks(0); } - return std::chrono::duration_cast( - full_timeline_frames_.rbegin()->first - - full_timeline_frames_.begin()->first); + return retimed_frames_.rbegin()->first; } // not up to date, we need to get the timeline frames list from // the source auto rp = make_response_promise(); - request(caf::actor_cast(this), infinite, source_atom_v) + mail(source_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](caf::actor) mutable { - if (full_timeline_frames_.size() < 2) { + if (retimed_frames_.size() < 2) { rp.deliver(timebase::flicks(0)); } else { - rp.deliver(std::chrono::duration_cast( - full_timeline_frames_.rbegin()->first - - full_timeline_frames_.begin()->first)); + rp.deliver(retimed_frames_.rbegin()->first); } }, - [=](const error &err) mutable { rp.deliver(err); }); + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(timebase::flicks(0)); + }); return rp; }, [=](duration_frames_atom atom) -> result { if (up_to_date_) { - return full_timeline_frames_.size() ? full_timeline_frames_.size() - 1 : 0; + return retimed_frames_.size() ? retimed_frames_.size() - 1 : 0; } // not up to date, we need to get the timeline frames list from // the source auto rp = make_response_promise(); - request(caf::actor_cast(this), infinite, source_atom_v) + mail(source_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](caf::actor) mutable { - rp.deliver( - full_timeline_frames_.size() ? full_timeline_frames_.size() - 1 - : 0); + rp.deliver(retimed_frames_.size() ? retimed_frames_.size() - 1 : 0); }, [=](const error &err) mutable { rp.deliver(err); }); return rp; @@ -233,28 +287,38 @@ void SubPlayhead::init() { timebase::flicks frame_period, timeline_pts; std::shared_ptr frame = get_frame(flicks, frame_period, timeline_pts); - return frame ? frame->playhead_logical_frame_ : 0; + return logical_frame_from_pts(timeline_pts); }, [=](json_store::update_atom, const JsonStore & /*change*/, const std::string & /*path*/, const JsonStore &full) mutable { - try { - - pre_cache_read_ahead_frames_ = - preference_value(full, "/core/playhead/read_ahead"); - static_cache_delay_milliseconds_ = - std::chrono::milliseconds(preference_value( - full, "/core/playhead/static_cache_delay_milliseconds")); - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + if (current_sender() == global_prefs_actor) { + try { + pre_cache_read_ahead_frames_ = + preference_value(full, "/core/playhead/read_ahead"); + static_cache_delay_milliseconds_ = + std::chrono::milliseconds(preference_value( + full, "/core/playhead/static_cache_delay_milliseconds")); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } } }, - [=](json_store::update_atom, const JsonStore &js) { - delegate(actor_cast(this), json_store::update_atom_v, js, "", js); + [=](json_store::update_atom, const JsonStore &full) { + if (current_sender() == global_prefs_actor) { + try { + pre_cache_read_ahead_frames_ = + preference_value(full, "/core/playhead/read_ahead"); + static_cache_delay_milliseconds_ = + std::chrono::milliseconds(preference_value( + full, "/core/playhead/static_cache_delay_milliseconds")); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + } }, [=](jump_atom, @@ -263,38 +327,54 @@ void SubPlayhead::init() { const float velocity, const bool playing, const bool force_updates, - const bool active_in_ui) -> unit_t { + const bool active_in_ui, + const bool scrubbing) -> unit_t { // if the playhead has jumped, we set read_ahead_frames_ to zero so // that we make a fresh request for the readahead frames that we // need - set_position(flicks, forwards, playing, velocity, force_updates, active_in_ui); + set_position( + flicks, forwards, playing, velocity, force_updates, active_in_ui, scrubbing); return unit; }, [=](logical_frame_atom) -> int { return logical_frame_; }, - [=](logical_frame_to_flicks_atom, int logical_frame) -> result { - if (full_timeline_frames_.size() < 2) { + [=](logical_frame_to_flicks_atom, int64_t logical_frame) { + return mail(logical_frame_to_flicks_atom_v, int(logical_frame)) + .delegate(caf::actor_cast(this)); + }, + + [=](logical_frame_to_flicks_atom, int logical_frame) { + return mail(logical_frame_to_flicks_atom_v, logical_frame, false) + .delegate(caf::actor_cast(this)); + }, + + [=](logical_frame_to_flicks_atom, + int logical_frame, + const bool clamp_to_range) -> result { + if (retimed_frames_.size() < 2) { return make_error(xstudio_error::error, "No Frames"); } // to get to the last frame, due to the last 'dummy' frame that is appended // to account for the duration of the last frame, step back twice from end - auto last_frame = full_timeline_frames_.end(); + auto last_frame = retimed_frames_.end(); last_frame--; last_frame--; - auto frame = full_timeline_frames_.begin(); + // woopsie - this loop is not an efficient way to work out if + // we will hit the end frame!! + auto frame = retimed_frames_.begin(); while (logical_frame > 0 && frame != last_frame) { frame++; logical_frame--; } auto tp = frame->first; - if (logical_frame) { + if (logical_frame && !clamp_to_range) { // if logical_frame goes beyond our last frame then use the // duration of the final last frame to extend the result - auto dummy_last = full_timeline_frames_.end(); + auto dummy_last = retimed_frames_.end(); dummy_last--; auto last_frame_duration = dummy_last->first - last_frame->first; tp += logical_frame * last_frame_duration; @@ -304,19 +384,19 @@ void SubPlayhead::init() { }, [=](media_frame_to_flicks_atom, - const utility::Uuid &media_uuid, + const utility::Uuid &media_source_uuid, int logical_media_frame) -> result { if (logical_media_frame < 0) return make_error(xstudio_error::error, "Out of range"); // loop over frames until we hit the media item - auto frame = full_timeline_frames_.begin(); - while (frame != full_timeline_frames_.end()) { - if (frame->second && frame->second->media_uuid_ == media_uuid) { + auto frame = retimed_frames_.begin(); + while (frame != retimed_frames_.end()) { + if (frame->second && frame->second->source_uuid() == media_source_uuid) { break; } frame++; } - if (frame == full_timeline_frames_.end()) { + if (frame == retimed_frames_.end()) { return make_error(xstudio_error::error, "Media Not Found"); } @@ -325,10 +405,10 @@ void SubPlayhead::init() { timebase::flicks result = frame->first; // now loop over frames for the media item until we match to the logical_media_frame - while (frame != full_timeline_frames_.end()) { - if (frame->second && frame->second->media_uuid_ != media_uuid) { + while (frame != retimed_frames_.end()) { + if (frame->second && frame->second->source_uuid() != media_source_uuid) { return make_error(xstudio_error::error, "Out of range"); - } else if (frame->second && frame->second->frame_ >= logical_media_frame) { + } else if (frame->second && frame->second->frame() >= logical_media_frame) { // note the >= .... if logical_media_frame is *less* than // the frame's media frame, we return the timestamp for // 'frame'. The reason is that if we've been asked to get @@ -342,57 +422,77 @@ void SubPlayhead::init() { return result; }, - [=](media::get_edit_list_atom _get_edit_list_atom, const Uuid &uuid) { - delegate(source_, _get_edit_list_atom, media_type_, uuid); - }, - - [=](media::source_offset_frames_atom atom) { delegate(source_, atom); }, - - [=](media::source_offset_frames_atom atom, const int offset) -> result { - // change the time offset on the source ... this means we have - // to rebuild full_timeline_frames_ too - so we don't respond until - // that has been done - auto rp = make_response_promise(); - request(source_, infinite, atom, offset) - .then( - [=](bool) mutable { - request(caf::actor_cast(this), infinite, source_atom_v) - .then( - [=](caf::actor) mutable { rp.deliver(true); }, - [=](const error &err) mutable { rp.deliver(err); }); - }, - [=](caf::error &err) mutable { rp.deliver(err); }); - return rp; + [=](media::source_offset_frames_atom atom) -> result { return frame_offset_; }, + + [=](media::source_offset_frames_atom atom, + const int64_t offset, + const bool reset_duration) -> result { + if (offset != frame_offset_) { + if (reset_duration) + forced_duration_ = timebase::k_flicks_zero_seconds; + frame_offset_ = offset; + update_retiming(); + anon_mail(bookmark::get_bookmarks_atom_v, true).send(this); + return true; + } else if (reset_duration && forced_duration_ != timebase::k_flicks_zero_seconds) { + forced_duration_ = timebase::k_flicks_zero_seconds; + update_retiming(); + anon_mail(bookmark::get_bookmarks_atom_v, true).send(this); + return true; + } + return false; }, [=](first_frame_media_pointer_atom) -> result { - if (full_timeline_frames_.size()) { - if (!full_timeline_frames_.begin()->second) { - return make_error(xstudio_error::error, "Empty frame"); + if (up_to_date_) { + if (full_timeline_frames_.size()) { + if (!full_timeline_frames_.begin()->second) { + return make_error(xstudio_error::error, "Empty frame"); + } + return *(full_timeline_frames_.begin()->second); + } else { + return make_error(xstudio_error::error, "No frames"); } - return *(full_timeline_frames_.begin()->second); } - return make_error(xstudio_error::error, "No frames"); + // not up to date, we need to get the timeline frames list from + // the source + auto rp = make_response_promise(); + mail(source_atom_v) + .request(caf::actor_cast(this), infinite) + .then( + [=](caf::actor) mutable { + if (full_timeline_frames_.size()) { + if (!full_timeline_frames_.begin()->second) { + rp.deliver(make_error(xstudio_error::error, "Empty frame")); + } else { + rp.deliver(*(full_timeline_frames_.begin()->second)); + } + } else { + rp.deliver(make_error(xstudio_error::error, "No frames")); + } + }, + [=](const error &err) mutable { rp.deliver(err); }); + return rp; }, [=](last_frame_media_pointer_atom) -> result { - if (full_timeline_frames_.size() > 1) { - // remember the last entry in full_timeline_frames_ is + if (retimed_frames_.size() > 1) { + // remember the last entry in retimed_frames_ is // a dummy frame marking the end of the last frame - auto p = full_timeline_frames_.rbegin(); + auto p = retimed_frames_.rbegin(); p++; if (!p->second) { return make_error(xstudio_error::error, "Empty frame"); } return *(p->second); } - return make_error(xstudio_error::error, "No frames"); + return make_error(xstudio_error::error, "No Frames"); }, [=](media::get_media_pointer_atom) -> result { if (up_to_date_) { - auto frame = full_timeline_frames_.lower_bound(position_flicks_); - if (full_timeline_frames_.size() && frame != full_timeline_frames_.end()) { + auto frame = retimed_frames_.lower_bound(position_flicks_); + if (retimed_frames_.size() && frame != retimed_frames_.end()) { if (frame->second) { return *(frame->second); } else { @@ -405,12 +505,12 @@ void SubPlayhead::init() { // not up to date, we need to get the timeline frames list from // the source auto rp = make_response_promise(); - request(caf::actor_cast(this), infinite, source_atom_v) + mail(source_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](caf::actor) mutable { - auto frame = full_timeline_frames_.lower_bound(position_flicks_); - if (full_timeline_frames_.size() && - frame != full_timeline_frames_.end()) { + auto frame = retimed_frames_.lower_bound(position_flicks_); + if (retimed_frames_.size() && frame != retimed_frames_.end()) { rp.deliver(*(frame->second)); } else { rp.deliver(make_error(xstudio_error::error, "No Frame")); @@ -425,15 +525,17 @@ void SubPlayhead::init() { auto rp = make_response_promise(); // we have to have run the 'source_atom' handler first (to have - // built full_timeline_frames_) before we can fetch the media on + // built retimed_frames_) before we can fetch the media on // the current frame - request(caf::actor_cast(this), infinite, source_atom_v) + mail(source_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](caf::actor) mutable { - auto frame = full_timeline_frames_.lower_bound(position_flicks_); + auto frame = retimed_frames_.lower_bound(position_flicks_); caf::actor result; - if (frame != full_timeline_frames_.end() && frame->second) { - result = caf::actor_cast(frame->second->actor_addr_); + if (frame != retimed_frames_.end() && frame->second) { + result = + caf::actor_cast(frame->second->media_source_addr()); } rp.deliver(result); }, @@ -441,12 +543,50 @@ void SubPlayhead::init() { return rp; }, + [=](media_source_atom, + utility::Uuid source_uuid, + const media::MediaType mt) -> result { + // switch the media source and return the name of the new source + + auto rp = make_response_promise(); + // get the media actor on the current frame + mail(media_atom_v) + .request(caf::actor_cast(this), infinite) + .then( + [=](caf::actor media_actor) mutable { + // no media ? + if (!media_actor) { + rp.deliver(""); + return; + } + // switch the source + mail(media::current_media_source_atom_v, source_uuid, mt) + .request(media_actor, infinite) + .then( + [=](const bool) mutable { + // now get the name of the new source + mail(media::current_media_source_atom_v, mt) + .request(media_actor, infinite) + .then( + [=](UuidActor media_source) mutable { + rp.delegate( + media_source.actor(), utility::name_atom_v); + }, + [=](const error &err) mutable { rp.deliver(err); }); + }, + [=](const error &err) mutable { rp.deliver(err); }); + }, + [=](const error &err) mutable { rp.deliver(err); }); + return rp; + }, + [=](media_source_atom, std::string source_name, const media::MediaType mt) -> result { auto rp = make_response_promise(); // get the media actor on the current frame - request(caf::actor_cast(this), infinite, media_atom_v) + mail(media_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](caf::actor media_actor) mutable { // no media ? @@ -456,17 +596,16 @@ void SubPlayhead::init() { } // now get it to switched to the named MediaSource - request(media_actor, infinite, media_source_atom_v, source_name, mt) + mail(media_source_atom_v, source_name, mt) + .request(media_actor, infinite) .then( [=](bool) mutable { up_to_date_ = false; // now ensure we have rebuilt ourselves to reflect the new // source i.e. we have checked out the new frames_time_list_ - request( - caf::actor_cast(this), - infinite, - source_atom_v) + mail(source_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](caf::actor) mutable { rp.deliver(true); }, [=](const error &err) mutable { rp.deliver(err); }); @@ -479,31 +618,25 @@ void SubPlayhead::init() { [=](media_atom) -> result { // MediaActor at current playhead position - - auto rp = make_response_promise(); - request(caf::actor_cast(this), infinite, media_source_atom_v) - .then( - [=](caf::actor media_source) mutable { - if (!media_source) - rp.deliver(caf::actor()); - else { - request(media_source, infinite, utility::parent_atom_v) - .then( - [=](caf::actor media_actor) mutable { - rp.deliver(media_actor); - }, - [=](const error &err) mutable { rp.deliver(err); }); - } - }, - [=](const error &err) mutable { rp.deliver(err); }); - return rp; + auto frame = retimed_frames_.upper_bound(position_flicks_); + if (frame != retimed_frames_.begin()) + frame--; + utility::UuidActor result; + if (frame != retimed_frames_.end() && frame->second) { + auto m = caf::actor_cast(frame->second->media_addr()); + result = utility::UuidActor(frame->second->media_uuid(), m); + } + return result; }, - [=](media_source_atom, bool) -> utility::Uuid { - auto frame = full_timeline_frames_.lower_bound(position_flicks_); - utility::Uuid result; - if (frame != full_timeline_frames_.end() && frame->second) { - result = frame->second->media_uuid_; + [=](media_source_atom, bool) -> utility::UuidActor { + auto frame = retimed_frames_.upper_bound(position_flicks_); + if (frame != retimed_frames_.begin()) + frame--; + utility::UuidActor result; + if (frame != retimed_frames_.end() && frame->second) { + auto m = caf::actor_cast(frame->second->media_source_addr()); + result = utility::UuidActor(frame->second->source_uuid(), m); } return result; }, @@ -511,31 +644,64 @@ void SubPlayhead::init() { [=](utility::event_atom, media::add_media_source_atom, const utility::UuidActorVector &uav) { - send(parent_, utility::event_atom_v, media::add_media_source_atom_v, uav); + mail(utility::event_atom_v, media::add_media_source_atom_v, uav).send(parent_); }, [=](utility::event_atom, timeline::item_atom, const utility::JsonStore &changes, bool) { - up_to_date_ = false; - anon_send(this, source_atom_v); // triggers refresh of frames_time_list_ + // when a timeline autoconform is happening it triggers a flood of + // these item_atom events as the timeline is rebuilt. We don't want + // to keep requesting frames from the timeline while its still busy + // doing the autoconform. By delaying the update request, and + // checking if another update has come in since this request was + // made, we can skip excessive updates + up_to_date_ = false; + last_change_timepoint_ = utility::clock::now(); + + anon_mail(source_atom_v, last_change_timepoint_) + .delay(std::chrono::milliseconds(50)) + .send(this); }, - [=](media_cache::keys_atom) -> media::MediaKeyVector { - media::MediaKeyVector result; - result.reserve(full_timeline_frames_.size()); - for (const auto &pp : full_timeline_frames_) { - if (pp.second) { - result.push_back(pp.second->key_); - } + [=](utility::event_atom, media::media_status_atom, const media::MediaStatus) { + // this can come from a MediActor that is our source_ + }, + + [=](utility::event_atom, + media::media_display_info_atom, + const utility::JsonStore &, + caf::actor_addr &) {}, + + [=](utility::event_atom, media::media_display_info_atom, const utility::JsonStore &) {}, + + [=](media_cache::keys_atom) -> media::AVFrameIDs { + media::AVFrameIDs result; + result.reserve(retimed_frames_.size()); + for (const auto &pp : retimed_frames_) { + result.push_back(pp.second); } return result; }, [=](bookmark::get_bookmarks_atom) { - send( - parent_, - utility::event_atom_v, - bookmark::get_bookmarks_atom_v, - bookmark_ranges_); + mail(bookmark::get_bookmarks_atom_v, true) + .request(caf::actor_cast(this), infinite) + .then( + [=](bool) { + mail( + utility::event_atom_v, + bookmark::get_bookmarks_atom_v, + bookmark_ranges_) + .send(parent_); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + }, + + [=](bookmark::get_bookmarks_atom, bool wait) -> result { + auto rp = make_response_promise(); + full_bookmarks_update(rp); + return rp; }, [=](buffer_atom) -> result { @@ -545,52 +711,49 @@ void SubPlayhead::init() { get_frame(position_flicks_, frame_period, timeline_pts); if (!frame) { + rp.deliver(ImageBufPtr()); return rp; } - request( - pre_reader_, - std::chrono::milliseconds(5000), - media_reader::get_image_atom_v, - *(frame.get()), - false, - base_.uuid()) + + mail(media_reader::get_image_atom_v, *(frame.get()), false, uuid_, timeline_pts) + .request(pre_reader_, std::chrono::seconds(20)) .then( [=](ImageBufPtr image_buffer) mutable { image_buffer.when_to_display_ = utility::clock::now(); image_buffer.set_timline_timestamp(timeline_pts); image_buffer.set_frame_id(*(frame.get())); + image_buffer.set_playhead_logical_frame( + logical_frame_from_pts(timeline_pts)); add_annotations_data_to_frame(image_buffer); - - if (image_buffer) { - image_buffer->params()["playhead_frame"] = - frame->playhead_logical_frame_; - if (frame->params_.find("HELD_FRAME") != frame->params_.end()) { - image_buffer->params()["HELD_FRAME"] = true; - } else { - image_buffer->params()["HELD_FRAME"] = false; - } - // image_buffer.colour_pipe_data_ = colour_pipe_data; - } rp.deliver(image_buffer); }, - [=](const error &err) mutable { rp.deliver(err); }); + [=](const error &err) mutable { + spdlog::critical( + "SubPlayhead buffer read timeout for frame {}", + to_string(frame->key())); + rp.deliver(err); + }); + return rp; }, [=](media_reader::push_image_atom, ImageBufPtr image_buffer, const media::AVFrameID &mptr, - const time_point &tp) { receive_image_from_cache(image_buffer, mptr, tp); }, + const time_point &tp, + const timebase::flicks timeline_pts) { + receive_image_from_cache(image_buffer, mptr, tp, timeline_pts); + }, [=](playlist::get_media_uuid_atom) -> result { auto rp = make_response_promise(); - request( - caf::actor_cast(this), infinite, media::get_media_pointer_atom_v) + mail(media::get_media_pointer_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](const media::AVFrameID &frameid) mutable { - rp.deliver(frameid.media_uuid_); + rp.deliver(frameid.media_uuid()); }, [=](const caf::error &err) mutable { rp.deliver(err); }); return rp; @@ -598,14 +761,29 @@ void SubPlayhead::init() { [=](rate_atom) -> result { auto rp = make_response_promise(); - request( - caf::actor_cast(this), infinite, media::get_media_pointer_atom_v) + mail(media::get_media_pointer_atom_v) + .request(caf::actor_cast(this), infinite) .then( - [=](const media::AVFrameID &frameid) mutable { rp.deliver(frameid.rate_); }, + [=](const media::AVFrameID &frameid) mutable { + rp.deliver(frameid.rate()); + }, [=](const caf::error &err) mutable { rp.deliver(err); }); return rp; }, + [=](playhead::media_frame_ranges_atom) -> result> { + auto rp = make_response_promise>(); + // we have to have run the 'source_atom' handler first (to have + // built retimed_frames_) before we know the list of frames + // where media changes in the timeline + mail(source_atom_v) + .request(caf::actor_cast(this), infinite) + .then( + [=](caf::actor) mutable { rp.deliver(media_ranges_); }, + [=](const error &err) mutable { rp.deliver(err); }); + return rp; + }, + [=](simple_loop_end_atom, const timebase::flicks flicks) { loop_out_point_ = flicks; set_in_and_out_frames(); @@ -616,12 +794,6 @@ void SubPlayhead::init() { set_in_and_out_frames(); }, - [=](skip_through_sources_atom, const int skip_by) { - // if logical_frame_ sits on source N in an edit list, then this returns - // the uuid of the source (N+skip_by) in the edit list - delegate(source_, skip_through_sources_atom_v, skip_by, logical_frame_); - }, - [=](step_atom, timebase::flicks current_playhead_position, const int step_frames, @@ -631,43 +803,28 @@ void SubPlayhead::init() { return rp; }, - [=](utility::event_atom, media::source_offset_frames_atom atom, const int offset) { - // pass up to the main playhead that the offset has changed - if (parent_) - anon_send(parent_, atom, this, offset); - }, - - [=](utility::event_atom, utility::change_atom, const bool) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, - // to_string(caf::actor_cast(this))); - content_changed_ = false; - up_to_date_ = false; - anon_send(this, source_atom_v); // triggers refresh of frames_time_list_ + [=](skip_to_clip_atom, + timebase::flicks current_playhead_position, + const bool next_clip) -> timebase::flicks { + // get the position corresponding to the first frame of the next + // clip of the previous (current) clip + return get_next_or_previous_clip_start_position( + current_playhead_position, next_clip); }, [=](utility::event_atom, utility::change_atom) { - if (not content_changed_) { - content_changed_ = true; - delayed_send( - this, - std::chrono::milliseconds(250), - utility::event_atom_v, - change_atom_v, - true); - } + up_to_date_ = false; + last_change_timepoint_ = utility::clock::now(); + anon_mail(source_atom_v, last_change_timepoint_) + .delay(std::chrono::milliseconds(50)) + .send(this); }, [=](utility::event_atom, utility::last_changed_atom, const time_point &) { - if (not content_changed_) { - content_changed_ = true; - delayed_send( - this, - std::chrono::milliseconds(250), - utility::event_atom_v, - change_atom_v, - true); - } + return mail(utility::event_atom_v, utility::change_atom_v) + .delegate(caf::actor_cast(this)); }, + [=](utility::event_atom, utility::name_atom, const std::string & /*name*/) {}, [=](utility::event_atom, @@ -678,27 +835,24 @@ void SubPlayhead::init() { // main BookmarkManager }, + [=](utility::event_atom, + bookmark::bookmark_change_atom, + const utility::Uuid &, + const utility::UuidList &) {}, + [=](utility::event_atom, playlist::reflag_container_atom, const utility::Uuid &, const std::tuple &) {}, - [=](utility::event_atom, media::media_status_atom, const media::MediaStatus ms) { - // this can come from a MediaActor source, for example - }, - - [=](utility::serialise_atom) -> result { - JsonStore jsn; - jsn["base"] = base_.serialise(); - return jsn; - }, [=](precache_atom) -> result { auto rp = make_response_promise(); update_playback_precache_requests(rp); return rp; }, [=](full_precache_atom, bool start_precache, const bool force) -> result { - auto rp = make_response_promise(); + full_precache_activated_ = true; + auto rp = make_response_promise(); if (start_precache && (precache_start_frame_ != logical_frame_ || force)) { // we've been asked to start precaching, and the previous // sarting point is different to current position, so go ahead @@ -720,7 +874,7 @@ void SubPlayhead::init() { [=](check_logical_frame_changing_atom, const int logical_frame) { if (logical_frame == logical_frame_ && logical_frame != precache_start_frame_) { // logical frame is not changing! Kick off a full precache - anon_send(this, full_precache_atom_v, true, false); + anon_mail(full_precache_atom_v, true, false).send(this); } else if (logical_frame != logical_frame_) { // otherwise stop any pre cacheing precache_start_frame_ = std::numeric_limits::lowest(); @@ -730,27 +884,28 @@ void SubPlayhead::init() { bookmark::remove_bookmark_atom, const utility::Uuid &bookmark_uuid) { bookmark_deleted(bookmark_uuid); }, [=](utility::event_atom, bookmark::add_bookmark_atom, const utility::UuidActor &n) { - full_bookmarks_update(); + anon_mail(bookmark::get_bookmarks_atom_v, true).send(this); }, [=](utility::event_atom, bookmark::bookmark_change_atom, const utility::UuidActor &a) { bookmark_changed(a); }); - scoped_actor sys{system()}; - try { - auto session = utility::request_receive( - *sys, - system().registry().template get(studio_registry), - session::session_atom_v); - auto bookmark_manager = - utility::request_receive(*sys, session, bookmark::get_bookmark_atom_v); - - utility::join_event_group(this, bookmark_manager); - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } + // slightly awkward...we need to join event group of bookmark manager + mail(session::session_atom_v) + .request(system().registry().template get(studio_registry), infinite) + .then( + [=](caf::actor session) { + mail(bookmark::get_bookmark_atom_v) + .request(session, infinite) + .then( + [=](caf::actor bookmark_manager) { + utility::join_event_group(this, bookmark_manager); + }, + [=](caf::error &err) {}); + }, + [=](caf::error &err) {}); } + // move playhead to position void SubPlayhead::set_position( const timebase::flicks time, @@ -758,7 +913,8 @@ void SubPlayhead::set_position( const bool playing, const float velocity, const bool force_updates, - const bool active_in_ui) { + const bool active_in_ui, + const bool scrubbing) { position_flicks_ = time; playing_forwards_ = forwards; @@ -766,25 +922,39 @@ void SubPlayhead::set_position( timebase::flicks frame_period, timeline_pts; std::shared_ptr frame = get_frame(time, frame_period, timeline_pts); - int logical_frame = frame ? frame->playhead_logical_frame_ : 0; + int logical_frame = logical_frame_from_pts(timeline_pts); + + if (playing && precache_start_frame_ != std::numeric_limits::lowest()) { + // a big stream of static pre-cache frame requests is probably sitting + // with the reader. But we are playing back now so we need to cancel + // the pre-cache requests + precache_start_frame_ = std::numeric_limits::lowest(); + anon_mail(media_reader::static_precache_atom_v, media::AVFrameIDsAndTimePoints(), uuid_) + .send(pre_reader_); + } - if (logical_frame_ != logical_frame || force_updates) { + if (logical_frame_ != logical_frame || force_updates || scrubbing) { - logical_frame_ = logical_frame; + const bool logical_changed = logical_frame_ != logical_frame; + logical_frame_ = logical_frame; auto now = utility::clock::now(); // get the image from the image readers or cache and also request the // next frame so we can do async texture uploads in the viewer - if (playing || (force_updates && active_in_ui)) { + if (playing || ((force_updates || scrubbing) && active_in_ui)) { // make a blocking request to retrieve the image if (media_type_ == media::MediaType::MT_IMAGE) { broadcast_image_frame(now, frame, playing, timeline_pts); - } else if (media_type_ == media::MediaType::MT_AUDIO && playing) { - broadcast_audio_frame(now, frame, false); + } else if ( + media_type_ == media::MediaType::MT_AUDIO && + (playing || (scrubbing && logical_changed)) && logical_changed) { + // only broadcast audio when playing OR scrubbing, not doing + // a 'force_update' + broadcast_audio_frame(now, frame, scrubbing); } } else if (frame && (active_in_ui || force_updates)) { @@ -796,57 +966,62 @@ void SubPlayhead::set_position( // will override this one if the reader isn't able to keep up. if (media_type_ == media::MediaType::MT_IMAGE) { media::AVFrameID mptr(*(frame.get())); - mptr.playhead_logical_frame_ = logical_frame_; - anon_send( - pre_reader_, + anon_mail( media_reader::get_image_atom_v, mptr, actor_cast(this), - base_.uuid(), + uuid_, now, - logical_frame_); + timeline_pts) + .send(pre_reader_); + } else if (media_type_ == media::MediaType::MT_AUDIO) { + // don't sound audio if not playing or scrubbing } } + if (media_type_ == media::MediaType::MT_AUDIO) { + // we always broadcast the current audio frames - this is for + // visualisation only not sounding. + broadcast_audio_samples(); + } + // update the parent playhead with our position - if (frame && (previous_frame_ != frame || force_updates)) { - anon_send( - parent_, + if (frame && (previous_frame_ != frame || force_updates || logical_changed)) { + anon_mail( position_atom_v, this, logical_frame_, - frame->media_uuid_, - frame->frame_ - frame->first_frame_, - frame->frame_, - frame->rate_, - frame->timecode_); + frame->source_uuid(), + frame->frame() - frame->first_frame(), + frame->frame(), + frame->rate(), + frame->timecode()) + .send(parent_); } - if (!playing && active_in_ui) { + if (!playing && active_in_ui && full_precache_activated_) { // this delayed message allows us to kick-off the // pre-cache operation *if* the playhead has stopped // moving for static_cache_delay_milliseconds_, as we // will check in the message handler if the playhead // has indeed changed position and if not start the // pre-cache - delayed_anon_send( - this, - static_cache_delay_milliseconds_, - check_logical_frame_changing_atom_v, - logical_frame_); + anon_mail(check_logical_frame_changing_atom_v, logical_frame_) + .delay(static_cache_delay_milliseconds_) + .send(this); } - if (playing) { + /*if (playing) { if (read_ahead_frames_ < 1 || force_updates) { auto rp = make_response_promise(); update_playback_precache_requests(rp); - read_ahead_frames_ = 8; + read_ahead_frames_ = pre_cache_read_ahead_frames_/4; } else { read_ahead_frames_--; } } else { - read_ahead_frames_ = 0; - } + read_ahead_frames_ = int(drand48()*double(pre_cache_read_ahead_frames_)/4.0); + }*/ previous_frame_ = frame; } @@ -866,20 +1041,14 @@ void SubPlayhead::broadcast_image_frame( // .. we are in playback, assumption is that the readers/cache // can't decode frames fast enough - so we have to tell the parent // that we are dropping frames - send(parent_, dropped_frame_atom_v); + mail(dropped_frame_atom_v).send(parent_); return; } else { - // we're not playing, assumption is that the timeline is being scrubbed - // and the reader(s) can't keep up. We can't request more frames until - // the reader has responded or we could build up a big list of pending frame - // requests. Instead, we'll send a delayed message to the parent playhead - // to re-broadcast its position to us so that (when the reader is less busy) - // we can try again to fetch the current frame and broadcast to the viewer - delayed_anon_send( - parent_, - std::chrono::milliseconds(10), - jump_atom_v, - caf::actor_cast(this)); + // we still haven't received the last frame we asked for a previous + // time we entered this method. Instead of flooding the reader with + // more frame read requests, we just return here. When we do get + // the frame we asked for, we then force another update so that we + // do request the latest frame based on the parent playhead position return; } } @@ -887,68 +1056,56 @@ void SubPlayhead::broadcast_image_frame( if (!frame_media_pointer || frame_media_pointer->is_nil()) { // If there is no media pointer, tell the parent playhead that we have // no media to show - send( - parent_, + mail( event_atom_v, media_source_atom_v, caf::actor(), caf::actor_cast(this), Uuid(), Uuid(), - 0); + 0) + .send(parent_); waiting_for_next_frame_ = false; return; } waiting_for_next_frame_ = true; - request( - pre_reader_, - infinite, + const int broadcast_logical = logical_frame_; + + mail( media_reader::get_image_atom_v, *(frame_media_pointer.get()), false, - base_.uuid()) + uuid_, + timeline_pts) + .request(pre_reader_, infinite) .then( [=](ImageBufPtr image_buffer) mutable { image_buffer.when_to_display_ = when_to_show_frame; image_buffer.set_timline_timestamp(timeline_pts); image_buffer.set_frame_id(*(frame_media_pointer.get())); + image_buffer.set_playhead_logical_frame(logical_frame_from_pts(timeline_pts)); add_annotations_data_to_frame(image_buffer); - if (image_buffer) { - image_buffer->params()["playhead_frame"] = - frame_media_pointer->playhead_logical_frame_; - if (frame_media_pointer->params_.find("HELD_FRAME") != - frame_media_pointer->params_.end()) { - image_buffer->params()["HELD_FRAME"] = true; - } else { - image_buffer->params()["HELD_FRAME"] = false; - } - // image_buffer.colour_pipe_data_ = colour_pipe_data; - } - - send( - parent_, + mail( show_atom_v, - base_.uuid(), // the uuid of this playhead + uuid_, // the uuid of this playhead image_buffer, // the image true // is this the frame that should be on-screen now? - ); - - auto m = caf::actor_cast(frame_media_pointer->actor_addr_); - if (m) { - send( - parent_, - event_atom_v, - media_source_atom_v, - m, - actor_cast(this), - frame_media_pointer->media_uuid_, - frame_media_pointer->source_uuid_, - frame_media_pointer->frame_); - } + ) + .send(parent_); + + mail( + event_atom_v, + media_source_atom_v, + caf::actor_cast(frame_media_pointer->media_source_addr()), + actor_cast(this), + frame_media_pointer->media_uuid(), + frame_media_pointer->source_uuid(), + frame_media_pointer->frame()) + .send(parent_); waiting_for_next_frame_ = false; @@ -958,37 +1115,151 @@ void SubPlayhead::broadcast_image_frame( // re-draws during playback if (playing) request_future_frames(); + else if (broadcast_logical != logical_frame_) { + // This is crucial ... what if the playhead has moved since we + // requested the frame to broadcast? This could happen if the user + // is scrubbing frames and the reader can't keep up. If that is the + // case, we send a jump_atom message to the parent playhead, which + // in turn sends us back a jump_atom with its state and we request + // another broadcast frame + anon_mail(jump_atom_v, caf::actor_cast(this)) + .send(parent_); + } }, [=](const caf::error &err) mutable { waiting_for_next_frame_ = false; + if (broadcast_logical != logical_frame_) { + // see above + anon_mail(jump_atom_v, caf::actor_cast(this)) + .send(parent_); + } spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); } void SubPlayhead::broadcast_audio_frame( const utility::time_point /*when_to_show_frame*/, - std::shared_ptr /*frame_media_pointer*/, - const bool /*is_future_frame*/) { + std::shared_ptr frame_media_pointer, + const bool scrubbing) { + // This function is called every time we play a new frame of audio .. + // We actually retrieve 20 whole frames of audio ahead of where the playhead + // is right now. The reason is to allow a decent buffer for 'pre-roll' as + // some audio output might need a substantial number of samples to be + // buffered up. media::AVFrameIDsAndTimePoints future_frames; - get_lookahead_frame_pointers(future_frames, 50); + + std::vector tps; + + if (scrubbing) { + + // pick the next 5 frames in the timeline to send audio + // samples for sounding when we scrub + auto frame = current_frame_iterator(); + if (frame == retimed_frames_.end() || !frame->second) { + return; + } + if (frame != first_frame_) + frame--; + auto tt = utility::clock::now(); + for (int i = 0; i < 5; ++i) { + future_frames.emplace_back(tt, frame->second); + tps.emplace_back(frame->first); + auto a = frame->first; + frame++; + if (frame == retimed_frames_.end() || !frame->second) + break; + auto b = frame->first; + tt = tt + std::chrono::duration_cast(b - a); + } + + } else { + tps = get_lookahead_frame_pointers(future_frames, 20); + } // now fetch audio samples for playback - request( - pre_reader_, - std::chrono::milliseconds(5000), - media_reader::get_audio_atom_v, - future_frames, - base_.uuid()) + mail(media_reader::get_audio_atom_v, future_frames, uuid_) + .request(pre_reader_, std::chrono::milliseconds(5000)) .then( - [=](const std::vector &audio_buffers) mutable { - send( - parent_, + [=](std::vector &audio_buffers) mutable { + auto ab = audio_buffers.begin(); + auto fp = future_frames.begin(); + auto tt = tps.begin(); + while (ab != audio_buffers.end() && fp != future_frames.end()) { + ab->when_to_display_ = (*fp).first; + ab->set_timline_timestamp(*(tt++)); + ab++; + fp++; + } + + mail( sound_audio_atom_v, - base_.uuid(), // the uuid of this playhead - audio_buffers); + uuid_, // the uuid of this playhead + audio_buffers, + scrubbing) + .send(parent_); + }, + [=](const caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); +} + +void SubPlayhead::broadcast_audio_samples() { + + // this is a lot like broadcast_audio_frame but we broadcast audio frames + // either side of and including the current audio frame - this is used + // for audio waveform visualisation on the viewport, for example + media::AVFrameIDsAndTimePoints future_frames; + std::vector tps; + + // pick the next 1 or two frames in the timeline to send audio + // samples for sounding + auto frame = current_frame_iterator(); + if (frame == retimed_frames_.end()) { + return; + } + + // step backwards two frames + if (frame != retimed_frames_.begin()) + frame--; + if (frame != retimed_frames_.begin()) + frame--; + + int i = 0; + auto tt = utility::clock::now(); + while (frame != retimed_frames_.end() && i < 5) { + if (frame->second) { + future_frames.emplace_back(tt, frame->second); + tps.emplace_back(frame->first); + } + i++; + frame++; + } + + const auto playhead_position = position_flicks_; + + // now fetch audio samples for playback + mail(media_reader::get_audio_atom_v, future_frames, uuid_) + .request(pre_reader_, std::chrono::milliseconds(5000)) + .then( + + [=](std::vector &audio_buffers) mutable { + auto ab = audio_buffers.begin(); + auto fp = future_frames.begin(); + auto tt = tps.begin(); + while (ab != audio_buffers.end() && fp != future_frames.end()) { + ab->when_to_display_ = (*fp).first; + ab->set_timline_timestamp(*(tt++)); + ab++; + fp++; + } + mail( + audio::audio_samples_atom_v, + audio_buffers, // the uuid of this playhead + playhead_position) + .send(parent_); }, [=](const caf::error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -999,16 +1270,14 @@ std::vector SubPlayhead::get_lookahead_frame_pointers( media::AVFrameIDsAndTimePoints &result, const int max_num_frames) { std::vector tps; - if (full_timeline_frames_.size() < 2) { + if (retimed_frames_.size() < 2) { return tps; } timebase::flicks current_frame_tp = std::min(out_frame_->first, std::max(in_frame_->first, position_flicks_)); - auto frame = full_timeline_frames_.upper_bound(current_frame_tp); - if (frame != full_timeline_frames_.begin()) - frame--; + auto frame = current_frame_iterator(current_frame_tp); const auto start_point = frame; @@ -1034,7 +1303,9 @@ std::vector SubPlayhead::get_lookahead_frame_pointers( tt += std::chrono::duration_cast( frame_duration / playback_velocity_); - if (frame->second && !frame->second->source_uuid_.is_null()) { + bool repeat_frame = result.size() && result.back().second == frame->second; + + if (frame->second && !frame->second->source_uuid().is_null() && !repeat_frame) { // we don't send pre-read requests for 'blank' frames where // source_uuid is null result.emplace_back(tt, frame->second); @@ -1056,30 +1327,28 @@ void SubPlayhead::request_future_frames() { media::AVFrameIDsAndTimePoints future_frames; auto timeline_pts_vec = get_lookahead_frame_pointers(future_frames, 4); - request( - pre_reader_, - std::chrono::milliseconds(5000), - media_reader::get_future_frames_atom_v, - future_frames, - base_.uuid()) + mail(media_reader::get_future_frames_atom_v, future_frames, uuid_) + .request(pre_reader_, std::chrono::milliseconds(5000)) .then( [=](std::vector image_buffers) mutable { auto tp = timeline_pts_vec.begin(); auto idsp = future_frames.begin(); for (auto &imbuf : image_buffers) { + imbuf.set_playhead_logical_frame(logical_frame_from_pts(*(tp))); imbuf.set_timline_timestamp(*(tp++)); std::shared_ptr av_idx = (idsp++)->second; if (av_idx) { imbuf.set_frame_id(*(av_idx.get())); + add_annotations_data_to_frame(imbuf); } } - send( - parent_, + mail( show_atom_v, - base_.uuid(), // the uuid of this playhead - image_buffers); + uuid_, // the uuid of this playhead + image_buffers) + .send(parent_); }, [=](const caf::error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -1088,16 +1357,14 @@ void SubPlayhead::request_future_frames() { void SubPlayhead::update_playback_precache_requests(caf::typed_response_promise &rp) { - auto tt = utility::clock::now(); - // get the AVFrameID iterator for the current frame media::AVFrameIDsAndTimePoints requests; get_lookahead_frame_pointers(requests, pre_cache_read_ahead_frames_); make_prefetch_requests_for_colour_pipeline(requests); - request( - pre_reader_, infinite, media_reader::playback_precache_atom_v, requests, base_.uuid()) + mail(media_reader::playback_precache_atom_v, requests, uuid_, media_type_) + .request(pre_reader_, infinite) .then( [=](const bool requests_processed) mutable { rp.deliver(requests_processed); }, [=](const error &err) mutable { rp.deliver(err); }); @@ -1121,19 +1388,15 @@ void SubPlayhead::make_static_precache_request( make_prefetch_requests_for_colour_pipeline(requests); - request( - pre_reader_, infinite, media_reader::static_precache_atom_v, requests, base_.uuid()) + mail(media_reader::static_precache_atom_v, requests, uuid_) + .request(pre_reader_, infinite) .await( [=](bool requests_processed) mutable { rp.deliver(requests_processed); }, [=](const error &err) mutable { rp.deliver(err); }); } else { - request( - pre_reader_, - infinite, - media_reader::static_precache_atom_v, - media::AVFrameIDsAndTimePoints(), - base_.uuid()) + mail(media_reader::static_precache_atom_v, media::AVFrameIDsAndTimePoints(), uuid_) + .request(pre_reader_, infinite) .await( [=](bool requests_processed) mutable { rp.deliver(requests_processed); }, [=](const error &err) mutable { rp.deliver(err); }); @@ -1150,18 +1413,22 @@ void SubPlayhead::make_prefetch_requests_for_colour_pipeline( media::AVFrameIDsAndTimePoints frame_ids_for_colour_precompute_frame_ids; utility::Uuid curr_uuid; for (const auto &r : lookeahead_frames) { - if (r.second->source_uuid_ != curr_uuid) { + if (r.second->source_uuid() != curr_uuid) { frame_ids_for_colour_precompute_frame_ids.push_back(r); - curr_uuid = r.second->source_uuid_; + curr_uuid = r.second->source_uuid(); } } - send(parent_, colour_pipeline_lookahead_atom_v, frame_ids_for_colour_precompute_frame_ids); + mail(colour_pipeline_lookahead_atom_v, frame_ids_for_colour_precompute_frame_ids) + .send(parent_); } void SubPlayhead::receive_image_from_cache( - ImageBufPtr image_buffer, const media::AVFrameID mptr, const time_point tp) { + ImageBufPtr image_buffer, + const media::AVFrameID mptr, + const time_point tp, + const timebase::flicks timeline_pts) { // are we receiving an image from the cache that was requested *before* the previous // image we received, i.e. it's being sent from the readers out of order? @@ -1170,115 +1437,204 @@ void SubPlayhead::receive_image_from_cache( return; last_image_timepoint_ = tp; - if (image_buffer) { - image_buffer->params()["playhead_frame"] = mptr.playhead_logical_frame_; - if (mptr.params_.find("HELD_FRAME") != mptr.params_.end()) { - image_buffer->params()["HELD_FRAME"] = true; - } else { - image_buffer->params()["HELD_FRAME"] = false; - } - } - image_buffer.when_to_display_ = utility::clock::now(); - if (mptr.playhead_logical_frame_ < (int)full_timeline_frames_.size()) { - auto p = full_timeline_frames_.begin(); - std::advance(p, mptr.playhead_logical_frame_); - image_buffer.set_timline_timestamp(p->first); - } else { - image_buffer.set_timline_timestamp(position_flicks_); - } + image_buffer.set_timline_timestamp(timeline_pts); + image_buffer.set_playhead_logical_frame(logical_frame_from_pts(timeline_pts)); image_buffer.set_frame_id(mptr); add_annotations_data_to_frame(image_buffer); - send( - parent_, + mail( show_atom_v, - base_.uuid(), // the uuid of this playhead + uuid_, // the uuid of this playhead image_buffer, // the image true // this image supposed to be shown on-screen NOW - ); + ) + .send(parent_); - if (auto m = caf::actor_cast(mptr.actor_addr_)) { - send( - parent_, + if (auto m = caf::actor_cast(mptr.media_source_addr())) { + mail( event_atom_v, media_source_atom_v, m, actor_cast(this), - mptr.media_uuid_, - mptr.source_uuid_, - mptr.frame_); + mptr.media_uuid(), + mptr.source_uuid(), + mptr.frame()) + .send(parent_); } } void SubPlayhead::get_full_timeline_frame_list(caf::typed_response_promise rp) { if (up_to_date_) { - rp.deliver(source_); + rp.deliver(source_.actor()); return; } - // Note that the 'await' is important here ... there are a number of - // message handlers that require full_timeline_frames_ to be up-to-date - // so we don't want those to be available until this has completed - request( - source_, - infinite, - media::get_media_pointers_atom_v, - media_type_, - time_source_mode_, - override_frame_rate_) - .await( - [=](const media::FrameTimeMap &mpts) mutable { - full_timeline_frames_ = mpts; - - if (full_timeline_frames_.size() && full_timeline_frames_.rbegin()->second) { - // the logic here is crucial ... full_timeline_frames_ is used to - // evaluate the full duration of what's being played. We need to drop - // in an empty frame at the end, with a timestamp that matches the - // point just *after* the last frame's timestamp plus its duration. - // Thus, for a single frame sourc that is 24pfs, say, we will have - // two entries in full_timeline_frames_ ... one entry a t=0that is - // the frame. The second is a nullptr at t = 1.0/24.0s. - // - // We test if the last frame is empty in case our source has already - // taken care of this for us. - auto last_frame_timepoint = full_timeline_frames_.rbegin()->first; - last_frame_timepoint += time_source_mode_ == TimeSourceMode::FIXED - ? override_frame_rate_ - : full_timeline_frames_.rbegin()->second->rate_; - full_timeline_frames_[last_frame_timepoint].reset(); - } - // int logical_frame = 0; - all_media_uuids_.clear(); - utility::Uuid media_uuid; - for (const auto &f : full_timeline_frames_) { - // f.second->playhead_logical_frame_ = logical_frame++; - if (f.second && f.second->media_uuid_ != media_uuid) { - media_uuid = f.second->media_uuid_; - all_media_uuids_.insert(media_uuid); - } else if (!f.second) - media_uuid = utility::Uuid(); - } + inflight_update_requests_.push_back(rp); + + if (!source_.actor()) { + full_timeline_frames_.clear(); + update_retiming(); + for (auto &rprm : inflight_update_requests_) { + rprm.deliver(source_.actor()); + } + inflight_update_requests_.clear(); + up_to_date_ = true; + return; + } + + // check if we've already requested an update (in the request immediately + // below here) that hasn't come baack yet. If so, don't make the request + // again + if (inflight_update_requests_.size() > 1) + return; - set_in_and_out_frames(); + const auto request_update_timepoint = utility::clock::now(); + + auto ttt = utility::clock::now(); + + mail(media::get_media_pointers_atom_v, media_type_, time_source_mode_, override_frame_rate_) + .request(source_.actor(), infinite) + .then( + [=](const media::FrameTimeMapPtr &mpts) mutable { + if (mpts) { + full_timeline_frames_ = *mpts; + } else { + full_timeline_frames_.clear(); + } - full_bookmarks_update(); + update_retiming(); - // our data has changed (full_timeline_frames_ describes most) - // things that are important about the timeline, so send change - // notification - send( - event_group_, - utility::event_atom_v, - utility::change_atom_v, - actor_cast(this)); + /*auto tp = utility::clock::now(); + if (media_type_ == media::MT_IMAGE) { + std::cerr << "VID FRAMES GEN DELAY " << to_string(uuid_) << " " << + std::chrono::duration_cast(tp-request_update_timepoint).count() + << "\n"; + }*/ - rp.deliver(source_); - up_to_date_ = true; + // last step is to get all info on bookmarks for the media that + // we might be playing + mail(bookmark::get_bookmarks_atom_v, true) + .request(caf::actor_cast(this), infinite) + .then( + [=](bool) mutable { + // our data has changed (retimed_frames_ describes most) + // things that are important about the timeline, so send change + // notification + mail( + utility::event_atom_v, + utility::change_atom_v, + actor_cast(this)) + .send(event_group_); + + // rp.deliver(source_); + for (auto &rprm : inflight_update_requests_) { + rprm.deliver(source_.actor()); + } + inflight_update_requests_.clear(); + + mail( + utility::event_atom_v, + playhead::media_frame_ranges_atom_v, + media_ranges_) + .send(parent_); + + + if (request_update_timepoint < last_change_timepoint_) { + // One last check: + // we've been told by the source_ that it has changed + // at some point after we did the get_media_pointers_atom + // request here... therefore, we must request an + // update again to make sure we are fully up-to-date + // with the source + anon_mail(source_atom_v).send(this); + up_to_date_ = false; + } else { + up_to_date_ = true; + } + }, + [=](const error &err) mutable { + for (auto &rprm : inflight_update_requests_) { + rprm.deliver(err); + } + inflight_update_requests_.clear(); + }); }, - [=](const error &err) mutable { rp.deliver(err); }); + [=](const error &err) mutable { + if (to_string(err) == "error(\"No streams\")" || + to_string(err) == "error(\"No MediaSources\")") { + + // We're here because the source has no streams, or it has no media + + if (media_type_ == media::MT_IMAGE) { + // we have no image streams/sources. However, there might be a + // valid AUDIO source. In this case, we want to build a blank + // image frames to align with audio frames, as the main + // PlayheadActor requires a valid IMAGE source to drive frame-rate, + // duration etc. + mail( + media::get_media_pointers_atom_v, + media::MT_AUDIO, + time_source_mode_, + override_frame_rate_) + .request(source_.actor(), infinite) + .then( + [=](const media::FrameTimeMapPtr &mpts) mutable { + if (mpts) { + // replace the Audio frame ptrs with + // blank video frames + full_timeline_frames_ = *mpts; + auto p = full_timeline_frames_.begin(); + while (p != full_timeline_frames_.end()) { + p->second = media::make_blank_frame( + p->second->rate(), media_type_); + p++; + } + + } else { + full_timeline_frames_.clear(); + } + update_retiming(); + for (auto &rprm : inflight_update_requests_) { + rprm.deliver(source_.actor()); + } + inflight_update_requests_.clear(); + up_to_date_ = true; + }, + [=](const error &err) mutable { + // audio frames fetch aslo failing - fallback to empty + // frame map. + full_timeline_frames_.clear(); + update_retiming(); + for (auto &rprm : inflight_update_requests_) { + rprm.deliver(source_.actor()); + } + inflight_update_requests_.clear(); + up_to_date_ = true; + }); + } else { + + // still no media stream/source + full_timeline_frames_.clear(); + update_retiming(); + for (auto &rprm : inflight_update_requests_) { + rprm.deliver(source_.actor()); + } + inflight_update_requests_.clear(); + up_to_date_ = true; + } + + } else { + // rp.deliver(err); + for (auto &rprm : inflight_update_requests_) { + rprm.deliver(err); + } + inflight_update_requests_.clear(); + up_to_date_ = true; + } + }); } std::shared_ptr SubPlayhead::get_frame( @@ -1291,14 +1647,14 @@ std::shared_ptr SubPlayhead::get_frame( // Therefore if time = 39ms then we return first frame // // Also, note there will be an extra null pointer at the end of - // full_timeline_frames_ corresponding to last_frame + duration of last frame. - // So if we have a source with only one frame full_timeline_frames_ + // retimed_frames_ corresponding to last_frame + duration of last frame. + // So if we have a source with only one frame retimed_frames_ // would look like this: // { // {0ms, media::AVFrameID(first frame)}, // {40ms, media::AVFrameID(null frame)} // } - if (full_timeline_frames_.size() < 2) { + if (retimed_frames_.size() < 2) { // and give the others values something valid ??? frame_period = timebase::k_flicks_zero_seconds; timeline_pts = timebase::k_flicks_zero_seconds; @@ -1310,11 +1666,9 @@ std::shared_ptr SubPlayhead::get_frame( // get the frame to be show *after* time point t and decrement to get our // frame. auto tt = utility::clock::now(); - auto frame = full_timeline_frames_.upper_bound(t); - if (frame != full_timeline_frames_.begin()) - frame--; + auto frame = current_frame_iterator(t); - // see above, there is a dummy frame at the end of full_timeline_frames_ so + // see above, there is a dummy frame at the end of retimed_frames_ so // this is always valid: auto next_frame = frame; next_frame++; @@ -1325,22 +1679,19 @@ std::shared_ptr SubPlayhead::get_frame( } void SubPlayhead::get_position_after_step_by_frames( - const timebase::flicks start_position, + const timebase::flicks ref_position, caf::typed_response_promise &rp, int step_frames, const bool loop) { - if (full_timeline_frames_.size() < 2) { + if (retimed_frames_.size() < 2) { rp.deliver(make_error(xstudio_error::error, "No Frames")); return; } - timebase::flicks t = - std::min(out_frame_->first, std::max(in_frame_->first, start_position)); + timebase::flicks t = std::min(out_frame_->first, std::max(in_frame_->first, ref_position)); - auto frame = full_timeline_frames_.upper_bound(t); - if (frame != full_timeline_frames_.begin()) - frame--; + auto frame = current_frame_iterator(t); const bool forwards = step_frames >= 0; @@ -1369,27 +1720,222 @@ void SubPlayhead::get_position_after_step_by_frames( rp.deliver(frame->first); } +timebase::flicks SubPlayhead::get_next_or_previous_clip_start_position( + const timebase::flicks ref_position, const bool next_clip) { + + auto result = ref_position; + + if (retimed_frames_.size() < 2) { + return result; + } + + timebase::flicks frame_period, timeline_pts; + std::shared_ptr frame = + get_frame(ref_position, frame_period, timeline_pts); + int logical = logical_frame_from_pts(timeline_pts); + + if (next_clip) { + for (int i = 0; i < (int)media_ranges_.size(); ++i) { + if (media_ranges_[i] > logical) { + logical = media_ranges_[i]; + break; + } + } + } else { + for (int i = (int(media_ranges_.size()) - 1); i >= 0; --i) { + if (media_ranges_[i] < logical) { + logical = media_ranges_[i]; + break; + } + } + } + + // woopsie - this loop is not an efficient way to work out if + // we will hit the end frame!! + auto f = retimed_frames_.begin(); + while (logical && f != retimed_frames_.end()) { + f++; + logical--; + } + if (f == retimed_frames_.end()) + f--; + result = f->first; + return result; +} + +void SubPlayhead::update_retiming() { + + // to do retiming we insert held frames at the start or end of + // full_timeline_frames_, or conversely we trim frames off the start or + // end. + + + auto retimed_frames = full_timeline_frames_; + + if (!retimed_frames.size()) { + // provide a single blank frame, which can then be extended in the loop + // below to fill the forced duration. + retimed_frames[timebase::flicks(0)] = + media::make_blank_frame(default_rate_, media_type_); + } + + if (frame_offset_ > 0) { + + // if offset is forward, we remove frames at the front + auto p = retimed_frames.begin(); + std::advance(p, std::min(int64_t(retimed_frames.size()) - 1, frame_offset_)); + retimed_frames.erase(retimed_frames.begin(), p); + + } else if (frame_offset_ < 0) { + + // negative offset means we extend the beginning with held frames, + // i.e. duplicate the first frame - unless we are timeline (where we + // want a blank frame, not held frame, or audio where we want silence) + auto first_frame = + (source_is_timeline_ || media_type_ == media::MT_AUDIO) + ? media::make_blank_frame(retimed_frames.begin()->second->rate(), media_type_) + : retimed_frames.begin()->second; + + timebase::flicks frame_duration = override_frame_rate_; + if (time_source_mode_ != TimeSourceMode::FIXED && first_frame) { + frame_duration = first_frame->rate(); + } + // now mark as a 'held frame' + if (first_frame) { + media::AVFrameID ff = *first_frame; + ff.set_frame_status(media::FS_HELD_FRAME); + first_frame = std::make_shared(ff); + } + int64_t off = frame_offset_; + while (off < 0) { + retimed_frames[retimed_frames.begin()->first - frame_duration] = first_frame; + off++; + } + } + + // now we need to rebase retimed_frames so that first frame is at t=0 + retimed_frames_.clear(); + if (retimed_frames.size()) { + auto t0 = retimed_frames.begin()->first; + for (auto &p : retimed_frames) { + retimed_frames_[p.first - t0] = p.second; + } + } + + if (forced_duration_ != timebase::k_flicks_zero_seconds) { + + // trim end frame off until our duration is leq than forced_duration_ + while (retimed_frames_.size() > 1 && + retimed_frames_.rbegin()->first >= forced_duration_) { + retimed_frames_.erase(std::prev(retimed_frames_.end())); + } + + // if forced_duration_ extends beyond the last entry in retimed_frames_, + // we duplicate the last frame until this is no longer the case, + // i.e. held frame behaviour. UNLESS the source is a timeline, in whcih + // case we want blank frames, or if we're audio playhead in which case + // we want silence + auto last_frame = + (source_is_timeline_ || media_type_ == media::MT_AUDIO) + ? media::make_blank_frame(retimed_frames_.rbegin()->second->rate(), media_type_) + : retimed_frames_.rbegin()->second; + timebase::flicks frame_duration = override_frame_rate_; + if (time_source_mode_ != TimeSourceMode::FIXED && last_frame) { + frame_duration = last_frame->rate(); + } + // now mark as a 'held frame' + if (last_frame) { + media::AVFrameID ff = *last_frame; + ff.set_frame_status(media::FS_HELD_FRAME); + last_frame = std::make_shared(ff); + } + while ((retimed_frames_.rbegin()->first + frame_duration) < forced_duration_) { + retimed_frames_[retimed_frames_.rbegin()->first + frame_duration] = last_frame; + } + } + + + if (retimed_frames_.size() && retimed_frames_.rbegin()->second) { + // the logic here is crucial ... retimed_frames_ is used to + // evaluate the full duration of what's being played. We need to drop + // in an empty frame at the end, with a timestamp that matches the + // point just *after* the last frame's timestamp plus its duration. + // Thus, for a single frame sourc that is 24pfs, say, we will have + // two entries in retimed_frames_ ... one entry a t=0that is + // the frame. The second is a nullptr at t = 1.0/24.0s. + // + // We test if the last frame is empty in case our source has already + // taken care of this for us. + auto last_frame_timepoint = retimed_frames_.rbegin()->first; + last_frame_timepoint += time_source_mode_ == TimeSourceMode::FIXED + ? override_frame_rate_ + : retimed_frames_.rbegin()->second->rate(); + retimed_frames_[last_frame_timepoint].reset(); + } + + store_media_frame_ranges(); + + set_in_and_out_frames(); +} + +void SubPlayhead::store_media_frame_ranges() { + + all_media_uuids_.clear(); + media_ranges_.clear(); + logical_frames_.clear(); + + utility::Uuid media_uuid; + utility::Uuid clip_uuid; + int logical_frame = 0; + for (const auto &f : retimed_frames_) { + if (f.second && f.second->media_uuid() != media_uuid) { + media_uuid = f.second->media_uuid(); + all_media_uuids_.insert(media_uuid); + media_ranges_.push_back(logical_frame); + } else if (!f.second) + media_uuid = utility::Uuid(); + + // we want the media_ranges_ to give frames where a new clip + // OR new media starts. We only get clip IDs with Timelines. + if (f.second && f.second->clip_uuid() != clip_uuid) { + clip_uuid = f.second->clip_uuid(); + if (media_ranges_.size() && media_ranges_.back() != logical_frame) { + media_ranges_.push_back(logical_frame); + } + } else if (!f.second) + clip_uuid = utility::Uuid(); + logical_frames_[f.first] = logical_frame++; + } + + media_ranges_.push_back(logical_frame); +} + void SubPlayhead::set_in_and_out_frames() { - if (full_timeline_frames_.size() < 2) { - out_frame_ = full_timeline_frames_.begin(); - in_frame_ = full_timeline_frames_.begin(); - last_frame_ = full_timeline_frames_.begin(); - first_frame_ = full_timeline_frames_.begin(); + if (retimed_frames_.size() < 2) { + out_frame_ = retimed_frames_.begin(); + in_frame_ = retimed_frames_.begin(); + last_frame_ = retimed_frames_.begin(); + first_frame_ = retimed_frames_.begin(); return; } // to get to the last frame, due to the last 'dummy' frame that is appended // to account for the duration of the last frame, step back twice from end - last_frame_ = full_timeline_frames_.end(); + last_frame_ = retimed_frames_.end(); last_frame_--; last_frame_--; - first_frame_ = full_timeline_frames_.begin(); + first_frame_ = retimed_frames_.begin(); if (loop_out_point_ > last_frame_->first) { out_frame_ = last_frame_; } else { - out_frame_ = full_timeline_frames_.upper_bound(loop_out_point_); + // loop out point includes frame duration of last frame in the loop + // range. + // So if frame rate is @25hz, frame duration is 40ms + // If loop in point is 0ms and loop out_point is 80ms, we will loop + // over frames 0 & 1 only. + out_frame_ = retimed_frames_.upper_bound(loop_out_point_); if (out_frame_ != first_frame_) out_frame_--; } @@ -1397,36 +1943,42 @@ void SubPlayhead::set_in_and_out_frames() { if (loop_in_point_ > out_frame_->first) { in_frame_ = out_frame_; } else { - in_frame_ = full_timeline_frames_.upper_bound(loop_in_point_); + in_frame_ = retimed_frames_.upper_bound(loop_in_point_); if (in_frame_ != first_frame_) in_frame_--; } } -void SubPlayhead::full_bookmarks_update() { +void SubPlayhead::full_bookmarks_update(caf::typed_response_promise done) { // the goal here is to work out which frames are bookmarked and make // a list of each bookmark and its frame range (in the playhead timeline). // Note that the same bookmark can appear twice in the case where the same // piece of media appears twice in a timeline, say + // TODO: This code works but it is really hard to understand. This is a + // downside of our async requests to bookmark and building the bookmarks + // in the context of the playback timeline. We can massively simplify it + // by getting the MediaActors to deliver their bookmark ranges via the + // AVFrameID struct instead. + if (all_media_uuids_.empty()) { - fetch_bookmark_annotations(BookmarkRanges()); + fetch_bookmark_annotations(BookmarkRanges(), done); + return; } auto global = system().registry().template get(global_registry); - request(global, infinite, bookmark::get_bookmark_atom_v) + mail(bookmark::get_bookmark_atom_v) + .request(global, infinite) .then( - [=](caf::actor bookmarks_manager) { + [=](caf::actor bookmarks_manager) mutable { // here we get all bookmarks that match any and all of the media // that appear in our timline - request( - bookmarks_manager, - infinite, - bookmark::bookmark_detail_atom_v, - all_media_uuids_) + mail(bookmark::bookmark_detail_atom_v, all_media_uuids_) + .request(bookmarks_manager, std::chrono::seconds(5)) .then( - [=](const std::vector &bookmark_details) { + [=](const std::vector + &bookmark_details) mutable { // make a map of the bookmarks against the uuid of the media // that owns the bookmark std::map> @@ -1435,8 +1987,9 @@ void SubPlayhead::full_bookmarks_update() { BookmarkRanges result; if (bookmark_details.empty()) { - fetch_bookmark_annotations(result); - }; + fetch_bookmark_annotations(result, done); + return; + } for (const auto &i : bookmark_details) { if (i.owner_ and i.media_reference_) { @@ -1454,11 +2007,11 @@ void SubPlayhead::full_bookmarks_update() { // loop over the timeline frames, kkep track of current // media (for efficiency) and check against the bookmarks int logical_playhead_frame = 0; - for (const auto &f : full_timeline_frames_) { + for (const auto &f : retimed_frames_) { // check if media changed and if so are there bookmarks? - if (f.second && f.second->media_uuid_ != curr_media_uuid) { - curr_media_uuid = f.second->media_uuid_; + if (f.second && f.second->media_uuid() != curr_media_uuid) { + curr_media_uuid = f.second->media_uuid(); if (bookmarks.count(curr_media_uuid)) { curr_media_bookmarks = &(bookmarks[curr_media_uuid]); } else { @@ -1473,7 +2026,7 @@ void SubPlayhead::full_bookmarks_update() { if (curr_media_bookmarks) { auto media_frame = - f.second->frame_ - f.second->first_frame_; + f.second->frame() - f.second->first_frame(); for (const auto &bookmark : *curr_media_bookmarks) { if (bookmark.start_frame() <= media_frame && bookmark.end_frame() >= media_frame) { @@ -1485,14 +2038,16 @@ void SubPlayhead::full_bookmarks_update() { logical_playhead_frame++; } - fetch_bookmark_annotations(result); + fetch_bookmark_annotations(result, done); }, [=](const error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + done.deliver(false); + spdlog::warn("A {} {}", __PRETTY_FUNCTION__, to_string(err)); }); }, [=](const error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + done.deliver(false); + spdlog::warn("B {} {}", __PRETTY_FUNCTION__, to_string(err)); }); } @@ -1516,11 +2071,17 @@ void SubPlayhead::extend_bookmark_frame( } } -void SubPlayhead::fetch_bookmark_annotations(BookmarkRanges bookmark_ranges) { +void SubPlayhead::fetch_bookmark_annotations( + BookmarkRanges bookmark_ranges, caf::typed_response_promise rp) { + + // TODO: this bookmark evaluation code is horrible! refactor. + if (!bookmark_ranges.size()) { bookmark_ranges_.clear(); bookmarks_.clear(); - send(parent_, utility::event_atom_v, bookmark::get_bookmarks_atom_v, bookmark_ranges_); + mail(utility::event_atom_v, bookmark::get_bookmarks_atom_v, bookmark_ranges_) + .send(parent_); + rp.deliver(true); return; } utility::UuidList bookmark_ids; @@ -1530,30 +2091,31 @@ void SubPlayhead::fetch_bookmark_annotations(BookmarkRanges bookmark_ranges) { // first we need to get to the 'bookmarks_manager' auto global = system().registry().template get(global_registry); - request(global, infinite, bookmark::get_bookmark_atom_v) + mail(bookmark::get_bookmark_atom_v) + .request(global, infinite) .then( - [=](caf::actor bookmarks_manager) { + [=](caf::actor bookmarks_manager) mutable { // get the bookmark actors for bookmarks that are in our timline - request( - bookmarks_manager, infinite, bookmark::get_bookmark_atom_v, bookmark_ids) + mail(bookmark::get_bookmark_atom_v, bookmark_ids) + .request(bookmarks_manager, infinite) .then( - [=](const std::vector &bookmarks) { + [=](const std::vector &bookmarks) mutable { // now we are ready to build our vector of bookmark, annotations and // associated logical frame ranges auto result = std::shared_ptr( new xstudio::bookmark::BookmarkAndAnnotations); auto count = std::make_shared(bookmarks.size()); - + if (bookmarks.empty()) + rp.deliver(true); for (auto bookmark : bookmarks) { // now ask the bookmark actor for its detail and // annotation data (if any) - request( - bookmark.actor(), - infinite, + mail( bookmark::bookmark_detail_atom_v, bookmark::get_annotation_atom_v) + .request(bookmark.actor(), infinite) .then( [=](bookmark::BookmarkAndAnnotationPtr data) mutable { for (const auto &p : bookmark_ranges) { @@ -1611,24 +2173,34 @@ void SubPlayhead::fetch_bookmark_annotations(BookmarkRanges bookmark_ranges) { // we've finished, ping the parent PlayheadActor // with our new bookmark ranges - send( - parent_, + mail( utility::event_atom_v, bookmark::get_bookmarks_atom_v, - bookmark_ranges_); + bookmark_ranges_) + .send(parent_); + + anon_mail( + jump_atom_v, + caf::actor_cast(this)) + .send(parent_); + + rp.deliver(true); } }, [=](const error &err) mutable { + rp.deliver(false); spdlog::warn( "{} {}", __PRETTY_FUNCTION__, to_string(err)); }); } }, [=](const error &err) mutable { + rp.deliver(false); spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); }, [=](const error &err) mutable { + rp.deliver(false); spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); } @@ -1636,7 +2208,7 @@ void SubPlayhead::fetch_bookmark_annotations(BookmarkRanges bookmark_ranges) { void SubPlayhead::add_annotations_data_to_frame(ImageBufPtr &frame) { xstudio::bookmark::BookmarkAndAnnotations bookmarks; - int logical_frame = frame.frame_id().playhead_logical_frame_; + int logical_frame = logical_frame_from_pts(frame.timeline_timestamp()); for (auto &p : bookmarks_) { if (p->start_frame_ <= logical_frame && p->end_frame_ >= logical_frame) { bookmarks.push_back(p); @@ -1651,7 +2223,8 @@ void SubPlayhead::add_annotations_data_to_frame(ImageBufPtr &frame) { void SubPlayhead::bookmark_deleted(const utility::Uuid &bookmark_uuid) { // update bookmark only if the removed bookmark is in our list... - auto p = bookmarks_.begin(); + const size_t b = bookmarks_.size(); + auto p = bookmarks_.begin(); while (p != bookmarks_.end()) { if ((*p)->detail_.uuid_ == bookmark_uuid) { p = bookmarks_.erase(p); @@ -1669,8 +2242,9 @@ void SubPlayhead::bookmark_deleted(const utility::Uuid &bookmark_uuid) { } } - if (n != bookmark_ranges_.size()) { - send(parent_, utility::event_atom_v, bookmark::get_bookmarks_atom_v, bookmark_ranges_); + if (n != bookmark_ranges_.size() || b != bookmarks_.size()) { + mail(utility::event_atom_v, bookmark::get_bookmarks_atom_v, bookmark_ranges_) + .send(parent_); } } @@ -1680,15 +2254,20 @@ void SubPlayhead::bookmark_changed(const utility::UuidActor bookmark) { // do a full rebuild to make sure we're fully up to date. for (auto &p : bookmarks_) { if (p->detail_.uuid_ == bookmark.uuid()) { - full_bookmarks_update(); + anon_mail(bookmark::get_bookmarks_atom_v, true).send(this); return; } } + // null bookmark actor means a bookmark has been deleted + if (!bookmark.actor()) + return; + // even though this doesn't look like our bookmark, the change that has // happened to it might have been associating it with media that IS in // our timeline, in which case we need to rebuild our bookmarks data - request(bookmark.actor(), infinite, bookmark::bookmark_detail_atom_v) + mail(bookmark::bookmark_detail_atom_v) + .request(bookmark.actor(), infinite) .then( [=](const bookmark::BookmarkDetail &detail) { if (detail.owner_) { @@ -1697,11 +2276,31 @@ void SubPlayhead::bookmark_changed(const utility::UuidActor bookmark) { all_media_uuids_.end(), (*(detail.owner_)).uuid()); if (p != all_media_uuids_.end()) { - full_bookmarks_update(); + anon_mail(bookmark::get_bookmarks_atom_v, true).send(this); } } }, [=](const caf::error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + spdlog::warn( + "{} {} {}", + __PRETTY_FUNCTION__, + to_string(err), + to_string(bookmark.uuid())); }); } + +media::FrameTimeMap::iterator SubPlayhead::current_frame_iterator(const timebase::flicks t) { + auto frame = retimed_frames_.upper_bound(t); + if (frame != retimed_frames_.begin()) { + frame--; + } + return frame; +} + +media::FrameTimeMap::iterator SubPlayhead::current_frame_iterator() { + auto frame = retimed_frames_.upper_bound(position_flicks_); + if (frame != retimed_frames_.begin()) { + frame--; + } + return frame; +} diff --git a/src/playhead/test/playhead_actor_test.cpp b/src/playhead/test/playhead_actor_test.cpp index a75f86552..c6682315d 100644 --- a/src/playhead/test/playhead_actor_test.cpp +++ b/src/playhead/test/playhead_actor_test.cpp @@ -10,7 +10,6 @@ #include "xstudio/media_cache/media_cache_actor.hpp" #include "xstudio/media_reader/media_reader_actor.hpp" #include "xstudio/playlist/playlist_actor.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" @@ -97,7 +96,7 @@ TEST(PlayheadActorTest, Test) { using namespace std::chrono_literals; - UuidList uuids = {media1_uuid, media2_uuid, media3_uuid}; + utility::UuidVector uuids = {media1_uuid, media2_uuid, media3_uuid}; std::this_thread::sleep_for(100ms); @@ -116,14 +115,6 @@ TEST(PlayheadActorTest, Test) { try { - // EXPECT_EQ( - // request_receive_wait( - // *(f.self), - // playhead, - // std::chrono::milliseconds(1000), - // playhead::compare_mode_atom_v), - // playhead::CM_OFF); - request_receive_wait( *(f.self), playhead_selection, diff --git a/src/playlist/src/CMakeLists.txt b/src/playlist/src/CMakeLists.txt index 919fc2826..ddb43276f 100644 --- a/src/playlist/src/CMakeLists.txt +++ b/src/playlist/src/CMakeLists.txt @@ -1,7 +1,6 @@ SET(LINK_DEPS - caf::core + CAF::core xstudio::audio_output - xstudio::broadcast xstudio::contact_sheet xstudio::media xstudio::playhead @@ -10,4 +9,4 @@ SET(LINK_DEPS xstudio::utility ) -create_component(playlist 0.1.0 "${LINK_DEPS}") +create_component(playlist ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/playlist/src/playlist.cpp b/src/playlist/src/playlist.cpp index 1c1ba80fb..00715f8eb 100644 --- a/src/playlist/src/playlist.cpp +++ b/src/playlist/src/playlist.cpp @@ -23,6 +23,7 @@ Playlist::Playlist(const JsonStore &jsn) media_rate_ = jsn["media_rate"]; playhead_rate_ = jsn["playhead_rate"]; container_tree_ = PlaylistTree(static_cast(jsn["container_tree"])); + expanded_ = jsn.value("expanded", false); set_version(PROJECT_VERSION); set_file_version(PLAYLIST_FILE_VERSION, true); @@ -38,6 +39,7 @@ JsonStore Playlist::serialise() const { // identify actors that are media.. jsn["media"] = media_list_.serialise(); jsn["container_tree"] = container_tree_.serialise(); + jsn["expanded"] = expanded_; return jsn; } diff --git a/src/playlist/src/playlist_actor.cpp b/src/playlist/src/playlist_actor.cpp index 6e5634b40..413b015cb 100644 --- a/src/playlist/src/playlist_actor.cpp +++ b/src/playlist/src/playlist_actor.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include #include @@ -8,7 +9,6 @@ #include "xstudio/atoms.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" #include "xstudio/contact_sheet/contact_sheet_actor.hpp" -#include "xstudio/event/event.hpp" #include "xstudio/global_store/global_store.hpp" #include "xstudio/json_store/json_store_actor.hpp" #include "xstudio/media/media_actor.hpp" @@ -30,12 +30,14 @@ using namespace xstudio::utility; namespace fs = std::filesystem; +const auto MEDIA_NOTIFY_DELAY_SLOW = std::chrono::seconds(2); +const auto MEDIA_NOTIFY_DELAY_FAST = std::chrono::milliseconds(100); using namespace nlohmann; void blocking_loader( blocking_actor *self, - caf::response_promise rp, + caf::typed_response_promise> rp, UuidActor dst, const caf::uri &path, const bool recursive, @@ -45,16 +47,19 @@ void blocking_loader( const utility::Uuid &before) { std::vector result; std::vector batched_media_to_add; + // spdlog::error("blocking_loader uri {}", to_string(path)); // spdlog::error("blocking_loader posix {}", uri_to_posix_path(path)); - self->anon_send(dst.actor(), playlist::loading_media_atom_v, true); + anon_mail(playlist::loading_media_atom_v, true).send(dst.actor()); - auto event_msg = event::Event("Loading Playlist Media {}", 0, 0, 1, {dst.uuid()}); + // auto event_msg = event::Event("Loading Playlist Media {}", 0, 0, 1, {dst.uuid()}); - event::send_event(self, event_msg); + // event::send_event(self, event_msg); - auto items = utility::scan_posix_path(uri_to_posix_path(path), recursive ? -1 : 0); + auto items = utility::scan_posix_path( + to_string(path).find("http") == 0 ? to_string(path) : uri_to_posix_path(path), + recursive ? -1 : 0); std::sort( std::begin(items), std::end(items), @@ -62,12 +67,12 @@ void blocking_loader( return a.first < b.first; }); - event_msg.set_progress_maximum(items.size()); - event::send_event(self, event_msg); + // event_msg.set_progress_maximum(items.size()); + // event::send_event(self, event_msg); for (const auto &i : items) { - event_msg.set_progress(event_msg.progress() + 1); - event::send_event(self, event_msg); + // event_msg.set_progress(event_msg.progress() + 1); + // event::send_event(self, event_msg); try { if (is_file_supported(i.first)) { @@ -76,7 +81,6 @@ void blocking_loader( const FrameList &frame_list = i.second; const auto uuid = Uuid::generate(); - #ifdef _WIN32 std::string ext = ltrim_char( get_path_extension(to_upper_path(fs::path(uri_to_posix_path(uri)))), '.'); @@ -97,12 +101,23 @@ void blocking_loader( default_rate, source_uuid); - auto media = self->spawn( - "New Media", uuid, UuidActorVector({UuidActor(source_uuid, source)})); + auto media = + self->spawn("New Media", uuid, UuidActorVector()); + + self->mail( + + media::add_media_source_atom_v, + UuidActorVector({UuidActor(source_uuid, source)})) + .request(media, infinite) + .receive( + [=](bool) {}, + [=](error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); if (auto_gather) - anon_send( - session, media_hook::gather_media_sources_atom_v, media, default_rate); + anon_mail(media_hook::gather_media_sources_atom_v, media, default_rate) + .send(session); UuidActor ua(uuid, media); @@ -116,19 +131,15 @@ void blocking_loader( // [=](error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, // to_string(err)); } // ); - // self->anon_send(media, media_metadata::get_metadata_atom_v); + // anon_mail(media_metadata::get_metadata_atom_v).send(media); batched_media_to_add.emplace_back(ua); // we batch to try and cut down thrashing.. if (batched_media_to_add.size() == 1) { - self->anon_send(dst.actor(), playlist::loading_media_atom_v, true); - self->request( - dst.actor(), - infinite, - playlist::add_media_atom_v, - batched_media_to_add, - before) + anon_mail(playlist::loading_media_atom_v, true).send(dst.actor()); + self->mail(playlist::add_media_atom_v, batched_media_to_add, before) + .request(dst.actor(), infinite) .receive( [=](const bool) mutable {}, [=](error &err) { @@ -146,21 +157,21 @@ void blocking_loader( } if (not batched_media_to_add.empty()) { - self->request( - dst.actor(), infinite, playlist::add_media_atom_v, batched_media_to_add, Uuid()) + self->mail(playlist::add_media_atom_v, batched_media_to_add, Uuid()) + .request(dst.actor(), infinite) .receive( [=](const bool) mutable {}, [=](error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); } - event_msg.set_complete(); - event::send_event(self, event_msg); + // event_msg.set_complete(); + // event::send_event(self, event_msg); // this message will update the 'loading_media_atom' status of the playlist in the UI - self->anon_send(dst.actor(), playlist::loading_media_atom_v, false); - // self->delayed_anon_send(dst.actor(), std::chrono::seconds(1), - // media_content_changed_atom_v); + anon_mail(playlist::loading_media_atom_v, false).send(dst.actor()); + // self->anon_mail(// + // media_content_changed_atom_v).delay(std::chrono::seconds(1)).send(dst.actor()); rp.deliver(result); } @@ -183,10 +194,14 @@ PlaylistActor::PlaylistActor( std::chrono::milliseconds(50)); } - link_to(json_store_); + if (jsn.contains("playhead")) { + playhead_serialisation_ = jsn["playhead"]; + } + link_to(json_store_); + join_event_group(this, json_store_); // media needs to exist before we can deserialise containers. - // spdlog::stopwatch sw; + spdlog::stopwatch sw; for (const auto &[key, value] : jsn.at("actors").items()) { if (value.at("base").at("container").at("type") == "Media") { @@ -201,7 +216,7 @@ PlaylistActor::PlaylistActor( } } } - // spdlog::info("media loaded in {:.3} seconds.", sw); + spdlog::debug("media loaded in {:.3} seconds.", sw); // deserialise containers for (const auto &[key, value] : jsn.at("actors").items()) { if (value.at("base").at("container").at("type") == "Subset") { @@ -233,17 +248,23 @@ PlaylistActor::PlaylistActor( link_to(actor); join_event_group(this, actor); // link media to clips. - anon_send(actor, timeline::link_media_atom_v, media_); + anon_mail(timeline::link_media_atom_v, media_, false).send(actor); } catch (const std::exception &e) { spdlog::error("{}", e.what()); } - } - } + } else if (value.at("base").at("container").at("type") == "PlayheadSelection") { - selection_actor_ = spawn( - "PlaylistPlayheadSelectionActor", caf::actor_cast(this)); - link_to(selection_actor_); + try { + + selection_actor_ = system().spawn( + static_cast(value), caf::actor_cast(this)); + link_to(selection_actor_); + } catch (const std::exception &e) { + spdlog::error("{}", e.what()); + } + } + } init(); } @@ -262,96 +283,70 @@ PlaylistActor::PlaylistActor( json_store_ = spawn( utility::Uuid::generate(), utility::JsonStore(), std::chrono::milliseconds(50)); link_to(json_store_); - + join_event_group(this, json_store_); init(); } +PlaylistActor::~PlaylistActor() {} caf::message_handler PlaylistActor::default_event_handler() { - return { - [=](utility::event_atom, change_atom) {}, - [=](utility::event_atom, add_media_atom, const utility::UuidActor &) {}, - [=](utility::event_atom, loading_media_atom, const bool) {}, - [=](utility::event_atom, create_divider_atom, const utility::Uuid &) {}, - [=](utility::event_atom, create_group_atom, const utility::Uuid &) {}, - [=](utility::event_atom, - reflag_container_atom, - const utility::Uuid &, - const std::string &) {}, - [=](utility::event_atom, remove_container_atom, const utility::Uuid &) {}, - [=](utility::event_atom, - rename_container_atom, - const utility::Uuid &, - const std::string &) {}, - [=](utility::event_atom, create_subset_atom, const utility::UuidActor &) {}, - [=](utility::event_atom, create_contact_sheet_atom, const utility::UuidActor &) {}, - [=](utility::event_atom, create_timeline_atom, const utility::UuidActor &) {}, - [=](utility::event_atom, media_content_changed_atom, const utility::UuidActorVector &) { - }, - [=](utility::event_atom, move_container_atom, const Uuid &, const Uuid &, const bool) { - }}; + return caf::message_handler( + {[=](utility::event_atom, change_atom) {}, + [=](utility::event_atom, add_media_atom, const utility::UuidActorVector &) {}, + [=](utility::event_atom, loading_media_atom, const bool) {}, + [=](utility::event_atom, create_divider_atom, const utility::Uuid &) {}, + [=](utility::event_atom, create_group_atom, const utility::Uuid &) {}, + [=](utility::event_atom, + reflag_container_atom, + const utility::Uuid &, + const std::string &) {}, + [=](utility::event_atom, remove_container_atom, const utility::Uuid &) {}, + [=](utility::event_atom, + rename_container_atom, + const utility::Uuid &, + const std::string &) {}, + [=](json_store::update_atom, + const JsonStore &, + const std::string &, + const JsonStore &) {}, + [=](json_store::update_atom, const JsonStore &) {}, + [=](utility::event_atom, create_subset_atom, const utility::UuidActor &) {}, + [=](utility::event_atom, + create_contact_sheet_atom, + const utility::UuidActor &) {}, + [=](utility::event_atom, create_timeline_atom, const utility::UuidActor &) {}, + [=](utility::event_atom, + media_content_changed_atom, + const utility::UuidActorVector &) {}, + [=](utility::event_atom, + move_container_atom, + const Uuid &, + const Uuid &, + const bool) {}}) + .or_else(NotificationHandler::default_event_handler()) + .or_else(Container::default_event_handler()); } - -void PlaylistActor::init() { - print_on_create(this, base_); - print_on_exit(this, base_); - - // event_group_ = - // system().groups().get_local(to_string(actor_cast(this)));//system().groups().anonymous(); - event_group_ = spawn(this); - change_event_group_ = spawn(this); - link_to(event_group_); - link_to(change_event_group_); - - playlist_broadcast_ = spawn(this); - - if (!selection_actor_) { - selection_actor_ = spawn( - "PlaylistPlayheadSelectionActor", caf::actor_cast(this)); - link_to(selection_actor_); - } - - join_broadcast( - caf::actor_cast(selection_actor_), playlist_broadcast_); - - try { - auto prefs = GlobalStoreHelper(system()); - JsonStore j; - join_broadcast(this, prefs.get_group(j)); - auto_gather_sources_ = - preference_value(j, "/core/media_reader/auto_gather_sources"); - } catch (...) { - } - - // side step problem with anon_send.. - // set_default_handler(skip); - - set_down_handler([=](down_msg &msg) { - // find in playhead list.. - if (msg.source == playhead_.actor()) { - demonitor(playhead_.actor()); - send(event_group_, utility::event_atom_v, change_atom_v); - base_.send_changed(event_group_, this); - } - }); - - behavior_.assign( +caf::message_handler PlaylistActor::message_handler() { + return caf::message_handler{ [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - base_.make_ignore_error_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), - base_.make_last_changed_event_handler(event_group_, this), + + make_ignore_error_handler(), [=](broadcast::join_broadcast_atom) -> caf::actor { return playlist_broadcast_; }, + [=](playlist::add_media_atom, utility::event_atom) { + // delayed add media event.. + if (not delayed_add_media_.empty()) { + mail(utility::event_atom_v, add_media_atom_v, delayed_add_media_) + .send(base_.event_group()); + mail(utility::event_atom_v, add_media_atom_v, delayed_add_media_) + .send(playlist_broadcast_); + // spdlog::warn("delayed update {}", delayed_add_media_.size()); + delayed_add_media_.clear(); + } + }, + [=](utility::event_atom, timeline::item_atom, const utility::JsonStore &update, @@ -384,33 +379,21 @@ void PlaylistActor::init() { name, uuid, UuidActorVector({UuidActor(source_uuid, source)})); if (auto_gather_sources_) - anon_send( - actor_cast(session_), - media_hook::gather_media_sources_atom_v, - media, - rate); + anon_mail(media_hook::gather_media_sources_atom_v, media, rate) + .send(actor_cast(session_)); - delegate( - actor_cast(this), - add_media_atom_v, - UuidActor(uuid, media), - uuid_before); + return mail(add_media_atom_v, UuidActor(uuid, media), uuid_before) + .delegate(actor_cast(this)); }, [=](add_media_atom, const std::string &name, const caf::uri &uri, const utility::Uuid &uuid_before) { - delegate( - actor_cast(this), - add_media_atom_v, - name, - uri, - FrameList(), - uuid_before); + return mail(add_media_atom_v, name, uri, FrameList(), uuid_before) + .delegate(actor_cast(this)); }, - // this should not be required... we should be able to delegate, but it doesn't work.. [=](add_media_atom atom, const std::string &name, @@ -427,13 +410,8 @@ void PlaylistActor::init() { // uuid_before); const auto uuid = Uuid::generate(); -#ifdef _WIN32 - std::string ext = - ltrim_char(to_upper_path(fs::path(uri_to_posix_path(uri)).extension()), '.'); -#else - std::string ext = - ltrim_char(to_upper(fs::path(uri_to_posix_path(uri)).extension()), '.'); -#endif + std::string ext = ltrim_char( + to_upper(fs::path(uri_to_posix_path(uri)).extension().string()), '.'); const auto source_uuid = Uuid::generate(); auto source = @@ -451,30 +429,27 @@ void PlaylistActor::init() { name, uuid, UuidActorVector({UuidActor(source_uuid, source)})); if (auto_gather_sources_) - anon_send( - actor_cast(session_), - media_hook::gather_media_sources_atom_v, - media, - base_.media_rate()); + anon_mail(media_hook::gather_media_sources_atom_v, media, base_.media_rate()) + .send(actor_cast(session_)); - delegate( - actor_cast(this), - add_media_atom_v, - UuidActor(uuid, media), - uuid_before); + return mail(add_media_atom_v, UuidActor(uuid, media), uuid_before) + .delegate(actor_cast(this)); }, [=](add_media_atom atom, const caf::uri &path, const bool recursive, const utility::Uuid &uuid_before) { - delegate( - actor_cast(this), - atom, - path, - recursive, - base_.media_rate(), - uuid_before); + return mail(atom, path, recursive, base_.media_rate(), uuid_before) + .delegate(actor_cast(this)); + }, + + [=](add_media_with_subsets_atom, + const caf::uri &path, + const utility::Uuid &uuid_before) -> result> { + auto rp = make_response_promise>(); + recursive_add_media_with_subsets(rp, path, uuid_before); + return rp; }, [=](add_media_atom atom, @@ -487,10 +462,17 @@ void PlaylistActor::init() { join_event_group(this, media); link_to(media); base_.insert_media(uuid, uuid_before); - send(event_group_, utility::event_atom_v, add_media_atom_v, ua); - send(playlist_broadcast_, utility::event_atom_v, add_media_atom_v, ua); - send_content_changed_event(); - base_.send_changed(event_group_, this); + + delayed_add_media_.push_back(ua); + mail(playlist::add_media_atom_v, utility::event_atom_v) + .delay(MEDIA_NOTIFY_DELAY_FAST) + .send(this); + + // mail(utility::event_atom_v, add_media_atom_v, + // UuidActorVector({ua})).send(base_.event_group()); mail(utility::event_atom_v, + // add_media_atom_v, UuidActorVector({ua})).send(playlist_broadcast_); + // send_content_changed_event(); + base_.send_changed(); return ua; }, @@ -508,11 +490,19 @@ void PlaylistActor::init() { join_event_group(this, media); link_to(media); base_.insert_media(uuid, uuid_before); - send(event_group_, utility::event_atom_v, add_media_atom_v, ua); - send(playlist_broadcast_, utility::event_atom_v, add_media_atom_v, ua); } + + delayed_add_media_.insert(delayed_add_media_.end(), result.begin(), result.end()); + + mail(playlist::add_media_atom_v, utility::event_atom_v) + .delay(MEDIA_NOTIFY_DELAY_FAST) + .send(this); + + // mail(utility::event_atom_v, add_media_atom_v, result).send(base_.event_group()); + // mail(utility::event_atom_v, add_media_atom_v, result).send(playlist_broadcast_); + send_content_changed_event(); - base_.send_changed(event_group_, this); + base_.send_changed(); return result; }, @@ -556,7 +546,8 @@ void PlaylistActor::init() { // to enact the adding, because it might happen *after* we get // another of these add_media messages which would then mess up the // ordering algorithm - add_media(ua, before, rp); + // we batch notifying of media if size if greater than 50 + add_media(ua, before, final_ordered_uuid_list.size() > 50, rp); return rp; }, @@ -565,7 +556,7 @@ void PlaylistActor::init() { UuidActor ua, const utility::Uuid &uuid_before) -> result { auto rp = make_response_promise(); - add_media(ua, uuid_before, rp); + add_media(ua, uuid_before, false, rp); return rp; }, @@ -574,16 +565,13 @@ void PlaylistActor::init() { const utility::Uuid &uuid_before) -> result { auto rp = make_response_promise(); // we block other requests, as we don't wat the order beening messed up. - request(actor, infinite, uuid_atom_v) + mail(uuid_atom_v) + .request(actor, infinite) .await( [=](const utility::Uuid &uuid) mutable { // hmm this is going to knacker ordering.. - request( - actor_cast(this), - infinite, - add_media_atom_v, - UuidActor(uuid, actor), - uuid_before) + mail(add_media_atom_v, UuidActor(uuid, actor), uuid_before) + .request(actor_cast(this), infinite) .then( [=](const utility::UuidActor &ua) mutable { rp.deliver(ua); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); @@ -610,22 +598,26 @@ void PlaylistActor::init() { actor_cast(session_), uuid_before); - send(event_group_, utility::event_atom_v, loading_media_atom_v, true); + mail(utility::event_atom_v, loading_media_atom_v, true).send(base_.event_group()); return rp; }, [=](add_media_atom, - std::vector ma, + const std::vector &ma, const utility::Uuid &uuid_before) -> result { // before we can add media actors, we have to make sure the detail has been acquired // so that the duration of the media is known. This is because the playhead will // update and build a timeline as soon as the playlist notifies of change, so the // duration and frame rate must be known up-front - - std::vector media_actors = ma; - auto source_count = std::make_shared(); - (*source_count) = media_actors.size(); - auto rp = make_response_promise(); + std::vector media_actors; + for (const auto &media : ma) { + if (!media_.count(media.uuid())) { + media_actors.push_back(media); + } + } + auto source_count = std::make_shared(); + (*source_count) = media_actors.size(); + auto rp = make_response_promise(); // add to lis first, then lazy update.. @@ -640,20 +632,19 @@ void PlaylistActor::init() { // that we are guaranteed to add them (in the innermost request response) in the // correct order for (auto i : media_actors) { - request( - i.actor(), - infinite, - media::acquire_media_detail_atom_v, - base_.playhead_rate()) + mail(media::acquire_media_detail_atom_v, base_.playhead_rate()) + .request(i.actor(), infinite) .then( [=](const bool) mutable { - request(i.actor(), infinite, media_hook::get_media_hook_atom_v) + mail(media_hook::get_media_hook_atom_v) + .request(i.actor(), infinite) .then( [=](bool) mutable { // media_[media_actor.first] = media_actor.second; // link_to(media_actor.second); // base_.insert_media(media_actor.first, uuid_before); + (*source_count)--; if (!(*source_count)) { // we're done! @@ -661,14 +652,24 @@ void PlaylistActor::init() { if (is_in_viewer_) open_media_reader(media_actors[0].actor()); rp.deliver(true); - for (auto i : media_actors) { - send( - playlist_broadcast_, - utility::event_atom_v, - add_media_atom_v, - i); - } - send_content_changed_event(); + + delayed_add_media_.insert( + delayed_add_media_.end(), + media_actors.begin(), + media_actors.end()); + + mail( + playlist::add_media_atom_v, + utility::event_atom_v) + .delay(MEDIA_NOTIFY_DELAY_FAST) + .send(this); + + // mail(// utility::event_atom_v, + // add_media_atom_v, + // media_actors).send(// base_.event_group()); + // mail(// utility::event_atom_v, + // add_media_atom_v, + // media_actors).send(// playlist_broadcast_); } }, [=](error &err) mutable { @@ -677,7 +678,7 @@ void PlaylistActor::init() { (*source_count)--; if (!(*source_count)) { // we're done! - send_content_changed_event(); + // send_content_changed_event(); if (is_in_viewer_) open_media_reader(media_actors[0].actor()); rp.deliver(true); @@ -710,13 +711,8 @@ void PlaylistActor::init() { const utility::Uuid &uuid_before) -> result { auto rp = make_response_promise(); - request( - actor_cast(this), - infinite, - create_contact_sheet_atom_v, - name, - uuid_before, - false) + mail(create_contact_sheet_atom_v, name, uuid_before, false) + .request(actor_cast(this), infinite) .then( [=](const utility::UuidUuidActor &result) mutable { rp.deliver(result); @@ -724,22 +720,17 @@ void PlaylistActor::init() { auto src_container = base_.containers().cfind_any(uuid); if (src_container) { - request( - container_[(*src_container)->value().uuid()], - infinite, - get_media_atom_v) + mail(get_media_atom_v) + .request(container_[(*src_container)->value().uuid()], infinite) .then( [=](const std::vector &media) mutable { - anon_send( - result.second.actor(), - add_media_atom_v, - media, - Uuid()); - anon_send( - actor_cast(this), + anon_mail(add_media_atom_v, media, Uuid()) + .send(result.second.actor()); + anon_mail( reflag_container_atom_v, (*src_container)->value().flag(), - result.first); + result.first) + .send(actor_cast(this)); }, [=](error &err) mutable { spdlog::warn( @@ -759,13 +750,8 @@ void PlaylistActor::init() { const utility::Uuid &uuid_before) -> result { auto rp = make_response_promise(); - request( - actor_cast(this), - infinite, - create_timeline_atom_v, - name, - uuid_before, - false) + mail(create_timeline_atom_v, name, uuid_before, false, true) + .request(actor_cast(this), infinite) .then( [=](const utility::UuidUuidActor &result) mutable { rp.deliver(result); @@ -773,22 +759,17 @@ void PlaylistActor::init() { auto src_container = base_.containers().cfind_any(uuid); if (src_container) { - request( - container_[(*src_container)->value().uuid()], - infinite, - get_media_atom_v) + mail(get_media_atom_v) + .request(container_[(*src_container)->value().uuid()], infinite) .then( [=](const std::vector &media) mutable { - anon_send( - result.second.actor(), - add_media_atom_v, - media, - Uuid()); - anon_send( - actor_cast(this), + anon_mail(add_media_atom_v, media, Uuid()) + .send(result.second.actor()); + anon_mail( reflag_container_atom_v, (*src_container)->value().flag(), - result.first); + result.first) + .send(actor_cast(this)); }, [=](error &err) mutable { spdlog::warn( @@ -808,13 +789,8 @@ void PlaylistActor::init() { const utility::Uuid &uuid_before) -> result { auto rp = make_response_promise(); - request( - actor_cast(this), - infinite, - create_subset_atom_v, - name, - uuid_before, - false) + mail(create_subset_atom_v, name, uuid_before, false) + .request(actor_cast(this), infinite) .then( [=](const utility::UuidUuidActor &result) mutable { rp.deliver(result); @@ -822,22 +798,17 @@ void PlaylistActor::init() { auto src_container = base_.containers().cfind_any(uuid); if (src_container) { - request( - container_[(*src_container)->value().uuid()], - infinite, - get_media_atom_v) + mail(get_media_atom_v) + .request(container_[(*src_container)->value().uuid()], infinite) .then( [=](const std::vector &media) mutable { - anon_send( - result.second.actor(), - add_media_atom_v, - media, - Uuid()); - anon_send( - actor_cast(this), + anon_mail(add_media_atom_v, media, Uuid()) + .send(result.second.actor()); + anon_mail( reflag_container_atom_v, (*src_container)->value().flag(), - result.first); + result.first) + .send(actor_cast(this)); }, [=](error &err) mutable { spdlog::warn( @@ -851,9 +822,11 @@ void PlaylistActor::init() { return rp; }, - [=](copy_container_atom atom, caf::actor bookmarks) { - delegate( - actor_cast(this), atom, std::vector(), bookmarks); + [=](copy_container_atom atom, caf::actor bookmarks) -> result { + auto rp = make_response_promise(); + + copy_container(std::vector(), bookmarks, rp); + return rp; }, [=](copy_container_atom, @@ -873,7 +846,7 @@ void PlaylistActor::init() { // try insert as requested, but add to end if it fails. auto rp = make_response_promise(); auto actor = spawn(this, name); - anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); + anon_mail(playhead::playhead_rate_atom_v, base_.playhead_rate()).send(actor); create_container(actor, rp, uuid_before, into); return rp; }, @@ -894,8 +867,9 @@ void PlaylistActor::init() { const bool into) -> result { auto i = base_.insert_divider(name, uuid_before, into); if (i) { - send(event_group_, utility::event_atom_v, create_divider_atom_v, *i); - base_.send_changed(event_group_, this); + mail(utility::event_atom_v, create_divider_atom_v, *i) + .send(base_.event_group()); + base_.send_changed(); return *i; } @@ -907,8 +881,8 @@ void PlaylistActor::init() { const utility::Uuid &uuid_before) -> result { auto i = base_.insert_group(name, uuid_before); if (i) { - send(event_group_, utility::event_atom_v, create_group_atom_v, *i); - base_.send_changed(event_group_, this); + mail(utility::event_atom_v, create_group_atom_v, *i).send(base_.event_group()); + base_.send_changed(); return *i; } @@ -918,12 +892,17 @@ void PlaylistActor::init() { [=](create_timeline_atom, const std::string &name, const utility::Uuid &uuid_before, - const bool into) -> result { + const bool into, + const bool with_tracks) -> result { // try insert as requested, but add to end if it fails. auto rp = make_response_promise(); auto actor = spawn( - name, utility::Uuid::generate(), actor_cast(this)); - // anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); + name, + base_.media_rate(), + utility::Uuid::generate(), + actor_cast(this), + with_tracks); + // anon_mail(playhead::playhead_rate_atom_v, base_.playhead_rate()).send(actor); create_container(actor, rp, uuid_before, into); return rp; }, @@ -935,122 +914,29 @@ void PlaylistActor::init() { // try insert as requested, but add to end if it fails. auto rp = make_response_promise(); auto actor = spawn(this, name); - anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); + anon_mail(playhead::playhead_rate_atom_v, base_.playhead_rate()).send(actor); create_container(actor, rp, uuid_before, into); return rp; }, [=](json_store::get_json_atom atom, const std::string &path) { - delegate(json_store_, atom, path); + return mail(atom, path).delegate(json_store_); }, + [=](json_store::get_json_atom atom) { return mail(atom).delegate(json_store_); }, + [=](json_store::set_json_atom atom, const JsonStore &json, const std::string &path) { - delegate(json_store_, atom, json, path); + return mail(atom, json, path).delegate(json_store_); }, [=](duplicate_atom, caf::actor src_bookmarks, caf::actor dst_bookmarks) -> result { - auto uuid = utility::Uuid::generate(); - auto actor = - spawn(base_.name(), uuid, caf::actor_cast(session_)); - anon_send(actor, session::media_rate_atom_v, base_.media_rate()); - anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); - - caf::scoped_actor sys(system()); - - auto json = - request_receive(*sys, json_store_, json_store::get_json_atom_v); - anon_send(actor, json_store::set_json_atom_v, json, ""); - - // clone media into new playlist - // store mapping from old to new uuids - UuidUuidMap media_map; - for (const auto &i : base_.media()) { - try { - // duplicate media - auto ua = request_receive( - *sys, - media_.at(i), - utility::duplicate_atom_v, - src_bookmarks, - dst_bookmarks); - - // store new media uuid - media_map[i] = ua.second.uuid(); - - // add new media to new playlist - auto mua = request_receive( - *sys, actor, add_media_atom_v, ua.second.actor(), Uuid()); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - // clone subgroups / dividers - - try { - // need to clone container tree.. - // if we process in order.... - for (const auto &i : base_.containers().uuids(true)) { - auto ii = base_.containers().cfind(i); - if (ii) { - // create in new playlist.. - // is actor ? - if (container_.count((*ii)->value().uuid())) { - auto ua = request_receive( - *sys, - container_[(*ii)->value().uuid()], - utility::duplicate_atom_v); - - // add to new playlist - auto nua = request_receive( - *sys, - actor, - create_container_atom_v, - ua.actor(), - Uuid(), - false); - - // reassign media / reparent - - request_receive( - *sys, - nua.second.actor(), - playhead::source_atom_v, - actor, - media_map); - - request_receive( - *sys, - actor, - reflag_container_atom_v, - (*ii)->value().flag(), - nua.first); - } else { - // add to new playlist - auto u = request_receive( - *sys, - actor, - create_divider_atom_v, - (*ii)->value().name(), - Uuid(), - false); - request_receive( - *sys, actor, reflag_container_atom_v, (*ii)->value().flag(), u); - } - } - } - - // have we really changed ? - base_.send_changed(event_group_, this); - return utility::UuidActor(uuid, actor); + auto rp = make_response_promise(); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } + duplicate(rp, src_bookmarks, dst_bookmarks); - return make_error(xstudio_error::error, "Invalid uuid"); + return rp; }, // is this actually used ? @@ -1059,12 +945,8 @@ void PlaylistActor::init() { caf::actor dst_bookmarks, const bool /*include_self*/) -> result> { auto rp = make_response_promise>(); - request( - actor_cast(this), - infinite, - duplicate_atom_v, - src_bookmarks, - dst_bookmarks) + mail(duplicate_atom_v, src_bookmarks, dst_bookmarks) + .request(actor_cast(this), infinite) .then( [=](const UuidActor &result) mutable { rp.deliver(std::make_pair( @@ -1097,14 +979,32 @@ void PlaylistActor::init() { [=](utility::event_atom, bookmark::bookmark_change_atom, const utility::Uuid & /*bookmark_uuid*/) {}, + [=](utility::event_atom, + bookmark::bookmark_change_atom, + const utility::Uuid &, + const utility::UuidList &) {}, [=](utility::event_atom, media::media_status_atom, const media::MediaStatus ms) {}, + [=](utility::event_atom, + media::media_display_info_atom, + const utility::JsonStore &, + caf::actor_addr &) {}, + + [=](utility::event_atom, media::media_display_info_atom, const utility::JsonStore &a) { + mail( + utility::event_atom_v, + media::media_display_info_atom_v, + a, + caf::actor_cast(current_sender())) + .send(base_.event_group()); + }, + [=](utility::event_atom, media::current_media_source_atom, UuidActor &, const media::MediaType) { - base_.send_changed(event_group_, this); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); + base_.send_changed(); + mail(utility::event_atom_v, utility::change_atom_v).send(change_event_group_); }, [=](get_change_event_group_atom) -> caf::actor { return change_event_group_; }, @@ -1112,7 +1012,7 @@ void PlaylistActor::init() { [=](get_container_atom) -> PlaylistTree { return base_.containers(); }, // unused..// [=](get_container_atom, const utility::Uuid &uuid) -> result - // {// if(container_.count(uuid))// return container_[uuid];// return + // {// if(container_.count(uuid))// return container_[uuid];// return // make_error(xstudio_error::error, "Invalid uuid");// }, [=](get_container_atom, const Uuid &uuid) -> caf::result { auto found = base_.containers().cfind(uuid); @@ -1137,6 +1037,25 @@ void PlaylistActor::init() { return actors; }, + [=](media::current_media_source_atom) + -> caf::result>>> { + auto rp = make_response_promise< + std::vector>>>(); + if (not media_.empty()) { + fan_out_request( + map_value_to_vec(media_), infinite, media::current_media_source_atom_v) + .then( + [=](const std::vector< + std::pair>> + details) mutable { rp.deliver(details); }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + } else { + rp.deliver( + std::vector>>()); + } + return rp; + }, + [=](get_media_atom, const utility::UuidList &selection) -> result> { std::vector actors; @@ -1155,11 +1074,8 @@ void PlaylistActor::init() { if (not media_.empty()) { auto rp = make_response_promise>(); // collect media data.. - std::vector actors; - for (const auto &i : media_) - actors.push_back(i.second); - - fan_out_request(actors, infinite, utility::detail_atom_v) + fan_out_request( + map_value_to_vec(media_), infinite, utility::detail_atom_v) .then( [=](const std::vector details) mutable { std::vector reordered_details; @@ -1195,54 +1111,161 @@ void PlaylistActor::init() { return caf::actor(); }, - [=](get_next_media_atom, - const utility::Uuid &after_this_uuid, - int skip_by) -> result { - const utility::UuidList media = base_.media(); - if (skip_by > 0) { - auto i = std::find(media.begin(), media.end(), after_this_uuid); - if (i == media.end()) { - // not found! - return make_error( - xstudio_error::error, - "playlist::get_next_media_atom called with uuid that is not in " - "playlist"); - } - while (skip_by--) { - i++; - if (i == media.end()) { - i--; - break; - } - } - if (media_.count(*i)) - return UuidActor(*i, media_.at(*i)); + [=](media::get_media_source_atom, + const utility::Uuid &uuid, + bool return_null) -> result { + auto rp = make_response_promise(); + // collect media data.. + std::vector actors; + for (const auto &i : media_) + actors.push_back(i.second); - } else { - auto i = std::find(media.rbegin(), media.rend(), after_this_uuid); - if (i == media.rend()) { - // not found! - return make_error( - xstudio_error::error, - "playlist::get_next_media_atom called with uuid that is not in " - "playlist"); - } - while (skip_by++) { - i++; - if (i == media.rend()) { - i--; - break; - } + if (actors.empty()) { + if (return_null) { + rp.deliver(caf::actor()); + } else { + rp.deliver(make_error(xstudio_error::error, "No matching media source")); } + return rp; + } + + fan_out_request( + actors, infinite, media::get_media_source_atom_v, uuid, true) + .then( + [=](const std::vector matching_sources) mutable { + for (auto &a : matching_sources) { + if (a) { + rp.deliver(a); + return; + } + } + if (return_null) { + rp.deliver(caf::actor()); + } else { + rp.deliver( + make_error(xstudio_error::error, "No matching media source")); + } + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + + return rp; + }, - if (media_.count(*i)) - return UuidActor(*i, media_.at(*i)); + [=](filter_media_atom, const std::string &filter_string) -> result { + // for each media item in the playlist, check if any fields in the + // 'media display info' for the media matches filter_string. If + // it does, include it in the result. We have to do some awkward + // shenanegans thanks to async requests ... the usual stuff!! + + if (filter_string.empty()) + return base_.media(); + + auto rp = make_response_promise(); + // share ptr to store result for each media piece (in order) + auto vv = std::make_shared>(base_.media().size()); + // keep count of responses with this + auto ct = std::make_shared(base_.media().size()); + const auto filter_string_lower = utility::to_lower(filter_string); + + int idx = 0; + for (const auto &uuid : base_.media()) { + UuidActor media(uuid, media_.at(uuid)); + mail(media::media_display_info_atom_v) + .request(media.actor(), infinite) + .then( + [=](const utility::JsonStore &media_display_info) mutable { + if (media_display_info.is_array()) { + for (int i = 0; i < media_display_info.size(); ++i) { + std::string data = media_display_info[i].dump(); + if (utility::to_lower(data).find(filter_string_lower) != + std::string::npos) { + (*vv)[idx] = media.uuid(); + break; + } + } + } + (*ct)--; + if (*ct == 0) { + utility::UuidList r; + for (const auto &v : *vv) { + if (!v.is_null()) { + r.push_back(v); + } + } + rp.deliver(r); + } + }, + [=](caf::error &err) mutable { + (*ct)--; + if (*ct == 0) { + utility::UuidList r; + for (const auto &v : *vv) { + if (!v.is_null()) { + r.push_back(v); + } + } + rp.deliver(r); + } + }); + idx++; } + return rp; + }, + + [=](get_next_media_atom, + const utility::Uuid &after_this_uuid, + int skip_by, + const std::string &filter_string) -> result { + auto rp = make_response_promise(); + + mail(filter_media_atom_v, filter_string) + .request(caf::actor_cast(this), infinite) + .then( + [=](const utility::UuidList &media) mutable { + if (skip_by > 0) { + auto i = std::find(media.begin(), media.end(), after_this_uuid); + if (i == media.end()) { + // not found! + rp.deliver(make_error( + xstudio_error::error, + "playlist::get_next_media_atom called with uuid that is " + "not in " + "playlist")); + } + while (skip_by--) { + i++; + if (i == media.end()) { + i--; + break; + } + } + if (media_.count(*i)) + rp.deliver(UuidActor(*i, media_.at(*i))); + + } else { + auto i = std::find(media.rbegin(), media.rend(), after_this_uuid); + if (i == media.rend()) { + // not found! + rp.deliver(make_error( + xstudio_error::error, + "playlist::get_next_media_atom called with uuid that is " + "not in " + "playlist")); + } + while (skip_by++) { + i++; + if (i == media.rend()) { + i--; + break; + } + } - return make_error( - xstudio_error::error, - "playlist::get_next_media_atom called with uuid for which no media actor " - "exists"); + if (media_.count(*i)) + rp.deliver(UuidActor(*i, media_.at(*i))); + } + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + return rp; }, [=](insert_container_atom, @@ -1256,12 +1279,8 @@ void PlaylistActor::init() { // for(const auto &m : cr.media_) // spdlog::warn("add new media {} {}", to_string(m.first), to_string(m.second)); - request( - actor_cast(this), - infinite, - playlist::add_media_atom_v, - cr.media_, - utility::Uuid()) + mail(playlist::add_media_atom_v, cr.media_, utility::Uuid()) + .request(actor_cast(this), infinite) .then( [=](const bool) mutable { // now add containers actors and remap sources. @@ -1269,14 +1288,14 @@ void PlaylistActor::init() { container_[i.uuid()] = i.actor(); link_to(i.actor()); join_event_group(this, i.actor()); - anon_send( - i.actor(), + anon_mail( playhead::source_atom_v, actor_cast(this), - cr.media_map_); + cr.media_map_) + .send(i.actor()); } notify_tree(cr.tree_); - base_.send_changed(event_group_, this); + base_.send_changed(); rp.deliver(cr.tree_.children_uuid(true)); }, [=](error &err) mutable { @@ -1286,42 +1305,7 @@ void PlaylistActor::init() { }, [=](loading_media_atom atom, bool is_loading) { - send(event_group_, utility::event_atom_v, atom, is_loading); - }, - - [=](media::get_edit_list_atom, const utility::Uuid &uuid) -> result { - std::vector actors; - for (const auto &i : base_.media()) - actors.push_back(media_.at(i)); - - if (not actors.empty()) { - auto rp = make_response_promise(); - - fan_out_request( - actors, infinite, media::get_edit_list_atom_v, Uuid()) - .then( - [=](std::vector sections) mutable { - utility::EditList ordered_sections; - for (const auto &i : base_.media()) { - for (const auto &ii : sections) { - const auto &[ud, rt, tc] = ii.section_list()[0]; - if (ud == i) { - if (uuid.is_null()) - ordered_sections.push_back(ii.section_list()[0]); - else - ordered_sections.push_back({uuid, rt, tc}); - break; - } - } - } - rp.deliver(ordered_sections); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - - return rp; - } - - return result(utility::EditList()); + mail(utility::event_atom_v, atom, is_loading).send(base_.event_group()); }, [=](media::media_reference_atom) -> result> { @@ -1359,31 +1343,26 @@ void PlaylistActor::init() { const bool into) -> bool { bool result = base_.move_container(uuid, uuid_before, into); if (result) { - send( - event_group_, - utility::event_atom_v, - move_container_atom_v, - uuid, - uuid_before, - into); - base_.send_changed(event_group_, this); + mail(utility::event_atom_v, move_container_atom_v, uuid, uuid_before, into) + .send(base_.event_group()); + base_.send_changed(); } return result; }, [=](playlist::move_media_atom atom, const Uuid &uuid, const Uuid &uuid_before) { - delegate( - actor_cast(this), atom, utility::UuidVector({uuid}), uuid_before); + return mail(atom, utility::UuidVector({uuid}), uuid_before) + .delegate(actor_cast(this)); }, [=](playlist::move_media_atom atom, const UuidList &media_uuids, const Uuid &uuid_before) { - delegate( - actor_cast(this), - atom, - utility::UuidVector(media_uuids.begin(), media_uuids.end()), - uuid_before); + return mail( + atom, + utility::UuidVector(media_uuids.begin(), media_uuids.end()), + uuid_before) + .delegate(actor_cast(this)); }, [=](playlist::move_media_atom, @@ -1394,7 +1373,7 @@ void PlaylistActor::init() { result |= base_.move_media(uuid, uuid_before); } if (result) { - base_.send_changed(event_group_, this); + base_.send_changed(); send_content_changed_event(); } return result; @@ -1404,7 +1383,7 @@ void PlaylistActor::init() { [=](playhead::playhead_rate_atom, const FrameRate &rate) { base_.set_playhead_rate(rate); - base_.send_changed(event_group_, this); + base_.send_changed(); }, // create a new timeline, attach it to new playhead. @@ -1415,16 +1394,52 @@ void PlaylistActor::init() { std::stringstream ss; ss << base_.name() << " Playhead"; auto uuid = utility::Uuid::generate(); - auto actor = spawn(ss.str(), selection_actor_, uuid); - anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); + auto actor = spawn( + ss.str(), + playhead::GLOBAL_AUDIO, + selection_actor_, + uuid, + caf::actor_cast(this)); + + anon_mail(playhead::playhead_rate_atom_v, base_.playhead_rate()).send(actor); playhead_ = UuidActor(uuid, actor); - monitor(actor); - base_.send_changed(event_group_, this); + link_to(playhead_.actor()); + + monitor(actor, [this, addr = actor.address()](const error &) { + if (addr == playhead_.actor()) { + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + base_.send_changed(); + } + }); + + base_.send_changed(); + + // have we actually selected anything for initial view/playback? + // If not, make the selection actor select something + mail(playhead::get_selection_atom_v) + .request(selection_actor_, infinite) + .then( + [=](const UuidList &selection) { + if (!selection.size()) { + // by sending an empty list this will force the + // selection actor to select the first item in this playlist + anon_mail(playlist::select_media_atom_v, utility::UuidVector()) + .send(selection_actor_); + } + if (!playhead_serialisation_.is_null()) { + anon_mail(module::deserialise_atom_v, playhead_serialisation_) + .send(playhead_.actor()); + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); return playhead_; }, [=](playlist::get_playhead_atom) { - delegate(caf::actor_cast(this), playlist::create_playhead_atom_v); + return mail(playlist::create_playhead_atom_v) + .delegate(caf::actor_cast(this)); }, [=](playlist::selection_actor_atom) -> caf::actor { return selection_actor_; }, @@ -1440,14 +1455,15 @@ void PlaylistActor::init() { [=](reflag_container_atom, const std::string &flag, const utility::Uuid &uuid) -> bool { bool result = base_.reflag_container(flag, uuid); if (result) { - send(event_group_, utility::event_atom_v, reflag_container_atom_v, uuid, flag); - base_.send_changed(event_group_, this); + mail(utility::event_atom_v, reflag_container_atom_v, uuid, flag) + .send(base_.event_group()); + base_.send_changed(); } return result; }, [=](remove_container_atom atom, const utility::Uuid &uuid) { - delegate(actor_cast(this), atom, uuid, false); + return mail(atom, uuid, false).delegate(actor_cast(this)); }, [=](remove_container_atom, @@ -1458,11 +1474,8 @@ void PlaylistActor::init() { for (const auto &i : cuuids) { if (base_.cfind(i)) { something_removed = true; - anon_send( - actor_cast(this), - remove_container_atom_v, - i, - remove_orphans); + anon_mail(remove_container_atom_v, i, remove_orphans) + .send(actor_cast(this)); } } @@ -1525,13 +1538,15 @@ void PlaylistActor::init() { } if (result) { - if (remove_orphans && !check_media.empty()) { + if (remove_orphans && not check_media.empty()) { // for (const auto &i : check_media) // spdlog::info("candiate {}", to_string(i)); - anon_send(actor_cast(this), remove_orphans_atom_v, check_media); + anon_mail(remove_orphans_atom_v, check_media) + .send(actor_cast(this)); } - send(event_group_, utility::event_atom_v, remove_container_atom_v, cuuid); - base_.send_changed(event_group_, this); + mail(utility::event_atom_v, remove_container_atom_v, cuuid) + .send(base_.event_group()); + base_.send_changed(); } return result; @@ -1540,11 +1555,11 @@ void PlaylistActor::init() { [=](media_hook::gather_media_sources_atom, const utility::UuidVector &uuids) { for (const auto &uuid : uuids) { if (media_.count(uuid)) { - anon_send( - caf::actor_cast(session_), + anon_mail( media_hook::gather_media_sources_atom_v, media_.at(uuid), - base_.media_rate()); + base_.media_rate()) + .send(caf::actor_cast(session_)); } } }, @@ -1552,52 +1567,55 @@ void PlaylistActor::init() { [=](media_hook::gather_media_sources_atom, const utility::UuidList &uuids) { for (const auto &uuid : uuids) { if (media_.count(uuid)) { - anon_send( - caf::actor_cast(session_), + anon_mail( media_hook::gather_media_sources_atom_v, media_.at(uuid), - base_.media_rate()); + base_.media_rate()) + .send(caf::actor_cast(session_)); } } }, - [=](remove_media_atom, const utility::Uuid &uuid) -> bool { + [=](remove_media_atom, const utility::Uuid &uuid) -> caf::result { // this needs to propergate to children somehow.. + auto rp = make_response_promise(); + if (base_.remove_media(uuid)) { - auto a = media_.at(uuid); + auto removed_media = media_.at(uuid); media_.erase(media_.find(uuid)); - unlink_from(a); - send_exit(a, caf::exit_reason::user_shutdown); - send_content_changed_event(); - base_.send_changed(event_group_, this); - - // send remove to all children ? - // std::vector actors; - // for(const auto &i : container_) - // actors.push_back(i.second); - - // if(actors.empty()) { - // send_exit(a, caf::exit_reason::user_shutdown); - // return result(true); - // } - // auto rp = make_response_promise(); - - // fan_out_request(actors, infinite, remove_media_atom_v, - // uuid).await( - // [=](std::vector) mutable { - // send_exit(a, caf::exit_reason::user_shutdown); - // rp.deliver(true); - // }, - // [=](error&) mutable { - // send_exit(a, caf::exit_reason::user_shutdown); - // rp.deliver(true); - // } - // ); + unlink_from(removed_media); - return true; + std::vector actors; + for (const auto &i : container_) + actors.push_back(i.second); + + if (actors.empty()) { + send_exit(removed_media, caf::exit_reason::user_shutdown); + rp.deliver(true); + send_content_changed_event(); + base_.send_changed(); + } else { + fan_out_request( + actors, infinite, remove_media_atom_v, uuid) + .await( + [=](std::vector) mutable { + send_exit(removed_media, caf::exit_reason::user_shutdown); + rp.deliver(true); + send_content_changed_event(); + base_.send_changed(); + }, + [=](error &) mutable { + send_exit(removed_media, caf::exit_reason::user_shutdown); + rp.deliver(true); + send_content_changed_event(); + base_.send_changed(); + }); + } + } else { + rp.deliver(false); } - return false; + return rp; }, [=](media::rescan_atom atom, @@ -1614,15 +1632,13 @@ void PlaylistActor::init() { return make_error(xstudio_error::error, "Invalid Uuid."); auto rp = make_response_promise(); - request(media_.at(media_uuid), infinite, media::decompose_atom_v) + mail(media::decompose_atom_v) + .request(media_.at(media_uuid), infinite) .then( [=](const UuidActorVector &result) mutable { if (not result.empty()) - anon_send( - caf::actor_cast(this), - add_media_atom_v, - result, - base_.next_media(media_uuid)); + anon_mail(add_media_atom_v, result, base_.next_media(media_uuid)) + .send(caf::actor_cast(this)); rp.deliver(result); }, [=](const caf::error &err) mutable { rp.deliver(err); }); @@ -1631,10 +1647,8 @@ void PlaylistActor::init() { }, [=](remove_media_atom atom, const utility::UuidList &uuids) { - delegate( - actor_cast(this), - atom, - utility::UuidVector(uuids.begin(), uuids.end())); + return mail(atom, utility::UuidVector(uuids.begin(), uuids.end())) + .delegate(actor_cast(this)); }, [=](remove_media_atom, const utility::UuidVector &uuids) -> bool { @@ -1651,7 +1665,7 @@ void PlaylistActor::init() { } if (changed) { send_content_changed_event(); - base_.send_changed(event_group_, this); + base_.send_changed(); } return changed; }, @@ -1664,8 +1678,17 @@ void PlaylistActor::init() { if (!actors.empty()) { // turn into set.. std::set candidates; - std::copy( - uuids.begin(), uuids.end(), std::inserter(candidates, candidates.end())); + if (uuids.empty()) { + auto media_uuids = map_key_to_vec(media_); + std::copy( + media_uuids.begin(), + media_uuids.end(), + std::inserter(candidates, candidates.end())); + } else + std::copy( + uuids.begin(), + uuids.end(), + std::inserter(candidates, candidates.end())); // for (const auto &i : candidates) // spdlog::info("candidates {}", to_string(i)); @@ -1697,22 +1720,24 @@ void PlaylistActor::init() { // list of media to remove for (const auto &i : result) - anon_send(actor_cast(this), remove_media_atom_v, i); + anon_mail(remove_media_atom_v, i) + .send(actor_cast(this)); }, [=](error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); } else { for (const auto &i : uuids) - anon_send(actor_cast(this), remove_media_atom_v, i); + anon_mail(remove_media_atom_v, i).send(actor_cast(this)); } }, [=](rename_container_atom, const std::string &name, const utility::Uuid &uuid) -> bool { bool result = base_.rename_container(name, uuid); if (result) { - send(event_group_, utility::event_atom_v, rename_container_atom_v, uuid, name); - base_.send_changed(event_group_, this); + mail(utility::event_atom_v, rename_container_atom_v, uuid, name) + .send(base_.event_group()); + base_.send_changed(); } return result; }, @@ -1725,73 +1750,111 @@ void PlaylistActor::init() { // ignore child events.. [=](session::media_rate_atom, const FrameRate &rate) { base_.set_media_rate(rate); - base_.send_changed(event_group_, this); + base_.send_changed(); }, - [=](sort_alphabetically_atom) { sort_alphabetically(); }, + [=](sort_by_media_display_info_atom, + const int sort_column_index, + const bool ascending) { sort_by_media_display_info(sort_column_index, ascending); }, [=](utility::event_atom, playlist::remove_media_atom, const UuidVector &) {}, - [=](utility::event_atom, playlist::add_media_atom, const utility::UuidActor &ua) { - send(event_group_, utility::event_atom_v, add_media_atom_v, ua); + + [=](utility::event_atom, + playlist::add_media_atom, + const utility::UuidActorVector &uav) { + mail(utility::event_atom_v, add_media_atom_v, uav).send(base_.event_group()); }, + [=](utility::event_atom, playlist::move_media_atom, const UuidVector &, const Uuid &) { }, + [=](utility::event_atom, + media_reader::get_thumbnail_atom, + const thumbnail::ThumbnailBufferPtr &buf) {}, + [=](utility::event_atom, utility::change_atom) { // one of our media changed.. // this gets spammy... // buffer it ? - send_content_changed_event(); + // send_content_changed_event(); }, // watch for events... [=](json_store::update_atom, - const JsonStore & /*change*/, - const std::string & /*path*/, + const JsonStore &change, + const std::string &path, const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + if (current_sender() == global_prefs_actor_) { + try { + auto_gather_sources_ = + preference_value(full, "/core/media_reader/auto_gather_sources"); + } catch (...) { + } + } else if (current_sender() == json_store_) + mail(json_store::update_atom_v, change, path, full).send(base_.event_group()); }, - [=](json_store::update_atom, const JsonStore &j) mutable { - try { - auto_gather_sources_ = - preference_value(j, "/core/media_reader/auto_gather_sources"); - } catch (...) { - } + [=](json_store::update_atom, const JsonStore &full) mutable { + if (current_sender() == global_prefs_actor_) { + try { + auto_gather_sources_ = + preference_value(full, "/core/media_reader/auto_gather_sources"); + } catch (...) { + } + } else if (current_sender() == json_store_) + mail(json_store::update_atom_v, full).send(base_.event_group()); }, [=](session::import_atom, const caf::uri &path, - const Uuid &uuid_before) -> result { + const Uuid &uuid_before, + const bool wait) -> result { auto rp = make_response_promise(); try { caf::scoped_actor sys(system()); auto global = system().registry().template get(global_registry); auto epa = request_receive(*sys, global, global::get_python_atom_v); - + // spdlog::warn("Load from python"); // request otio xml from embedded_python. - request(epa, infinite, session::import_atom_v, path) + mail(session::import_atom_v, path) + .request(epa, infinite) .then( [=](const std::string &data) mutable { // got data create timeline and load it.. const auto name = fs::path(uri_to_posix_path(path)).stem().string(); - - request( - actor_cast(this), - infinite, - create_timeline_atom_v, - name, - uuid_before, - false) + // spdlog::warn("Loaded from python {}", name); + mail(create_timeline_atom_v, name, uuid_before, false, false) + .request(actor_cast(this), infinite) .then( [=](const utility::UuidUuidActor &uua) mutable { // request loading of timeline - anon_send( - uua.second.actor(), session::import_atom_v, data); - rp.deliver(uua.second); + if (not wait) { + anon_mail(session::import_atom_v, path, data) + .send(uua.second.actor()); + rp.deliver(uua.second); + } else { + mail(session::import_atom_v, path, data) + .request(uua.second.actor(), infinite) + .then( + [=](const bool) mutable { + rp.deliver(uua.second); + }, + + [=](error &err) mutable { + spdlog::warn( + "{} {}", + __PRETTY_FUNCTION__, + to_string(err)); + rp.deliver(std::move(err)); + }); + } }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + [=](error &err) mutable { + spdlog::warn( + "{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(std::move(err)); + }); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); } catch (const std::exception &err) { @@ -1801,13 +1864,18 @@ void PlaylistActor::init() { return rp; }, + [=](expanded_atom) -> bool { return base_.expanded(); }, + + [=](expanded_atom, const bool expanded) { base_.set_expanded(expanded); }, + // handle child change events. [=](utility::event_atom, utility::name_atom, const std::string & /*name*/) {}, [=](utility::serialise_atom) -> result { auto rp = make_response_promise(); - request(json_store_, infinite, json_store::get_json_atom_v, "") + mail(json_store::get_json_atom_v, "") + .request(json_store_, infinite) .then( [=](const JsonStore &meta) mutable { std::vector clients; @@ -1815,8 +1883,7 @@ void PlaylistActor::init() { clients.push_back(i.second); for (const auto &i : container_) clients.push_back(i.second); - if (playhead_) - clients.push_back(playhead_); + clients.push_back(selection_actor_); if (not clients.empty()) { @@ -1825,22 +1892,38 @@ void PlaylistActor::init() { .then( [=](std::vector json) mutable { JsonStore jsn; - jsn["store"] = meta; - jsn["base"] = base_.serialise(); - jsn["playheads"] = {}; - jsn["actors"] = {}; + jsn["store"] = meta; + jsn["base"] = base_.serialise(); + jsn["actors"] = {}; for (const auto &j : json) { - if (j["base"]["container"]["type"] - .get() == "Playhead") { - jsn["playheads"][static_cast( - j["base"]["container"]["uuid"])] = j; - - } else { - jsn["actors"][static_cast( - j["base"]["container"]["uuid"])] = j; + jsn["actors"][static_cast( + j["base"]["container"]["uuid"])] = j; + } + if (playhead_) { + mail(utility::serialise_atom_v) + .request(playhead_.actor(), infinite) + .then( + [=](const utility::JsonStore + &playhead_state) mutable { + playhead_serialisation_ = + playhead_state; + jsn["playhead"] = playhead_state; + rp.deliver(jsn); + }, + [=](caf::error &err) mutable { + spdlog::warn( + "{} {}", + __PRETTY_FUNCTION__, + to_string(err)); + rp.deliver(jsn); + }); + + } else { + if (!playhead_serialisation_.is_null()) { + jsn["playhead"] = playhead_serialisation_; } + rp.deliver(jsn); } - rp.deliver(jsn); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); @@ -1855,9 +1938,44 @@ void PlaylistActor::init() { [=](error &err) mutable { rp.deliver(std::move(err)); }); return rp; - } + }}; +} - ); + +void PlaylistActor::init() { + print_on_create(this, base_); + print_on_exit(this, base_); + + // base_.event_group() = + // system().groups().get_local(to_string(actor_cast(this)));//system().groups().anonymous(); + change_event_group_ = spawn(this); + link_to(change_event_group_); + + playlist_broadcast_ = spawn(this); + + if (!selection_actor_) { + selection_actor_ = spawn( + base_.name() + " SelectionActor", caf::actor_cast(this)); + link_to(selection_actor_); + } + + join_broadcast( + caf::actor_cast(selection_actor_), playlist_broadcast_); + + global_prefs_actor_ = caf::actor(); + + try { + auto prefs = GlobalStoreHelper(system()); + JsonStore j; + global_prefs_actor_ = prefs.get_jsonactor(); + join_broadcast(this, prefs.get_group(j)); + auto_gather_sources_ = + preference_value(j, "/core/media_reader/auto_gather_sources"); + } catch (...) { + } + + // side step problem with anon_send.. + // set_default_handler(skip); } void PlaylistActor::on_exit() { @@ -1870,7 +1988,8 @@ void PlaylistActor::create_container( caf::typed_response_promise rp, const utility::Uuid &uuid_before, const bool into) { - request(actor, infinite, detail_atom_v) + mail(detail_atom_v) + .request(actor, infinite) .await( [=](const ContainerDetail &detail) mutable { container_[detail.uuid_] = actor; @@ -1884,25 +2003,25 @@ void PlaylistActor::create_container( } if (detail.type_ == "Subset") - send( - event_group_, + mail( utility::event_atom_v, create_subset_atom_v, - UuidActor(detail.uuid_, actor)); + UuidActor(detail.uuid_, actor)) + .send(base_.event_group()); else if (detail.type_ == "ContactSheet") - send( - event_group_, + mail( utility::event_atom_v, create_contact_sheet_atom_v, - UuidActor(detail.uuid_, actor)); + UuidActor(detail.uuid_, actor)) + .send(base_.event_group()); else if (detail.type_ == "Timeline") - send( - event_group_, + mail( utility::event_atom_v, create_timeline_atom_v, - UuidActor(detail.uuid_, actor)); + UuidActor(detail.uuid_, actor)) + .send(base_.event_group()); - base_.send_changed(event_group_, this); + base_.send_changed(); rp.deliver(std::make_pair(*cuuid, UuidActor(detail.uuid_, actor))); }, [=](error &err) mutable { rp.deliver(err); }); @@ -1911,28 +2030,44 @@ void PlaylistActor::create_container( void PlaylistActor::add_media( UuidActor &ua, const utility::Uuid &uuid_before, + const bool delayed, caf::typed_response_promise rp) { media_[ua.uuid()] = ua.actor(); join_event_group(this, ua.actor()); link_to(ua.actor()); base_.insert_media(ua.uuid(), uuid_before); - send(event_group_, utility::event_atom_v, add_media_atom_v, ua); - send(playlist_broadcast_, utility::event_atom_v, add_media_atom_v, ua); - request(ua.actor(), infinite, media::acquire_media_detail_atom_v, base_.playhead_rate()) + if (delayed and delayed_add_media_.empty()) { + delayed_add_media_.push_back(ua); + + mail(playlist::add_media_atom_v, utility::event_atom_v) + .delay(MEDIA_NOTIFY_DELAY_SLOW) + .send(this); + + } else { + delayed_add_media_.push_back(ua); + mail(playlist::add_media_atom_v, utility::event_atom_v) + .delay(MEDIA_NOTIFY_DELAY_FAST) + .send(this); + } + + mail(media::acquire_media_detail_atom_v, base_.playhead_rate()) + .request(ua.actor(), infinite) .then( [=](const bool) mutable { - request(ua.actor(), infinite, media_hook::get_media_hook_atom_v) + mail(media_hook::get_media_hook_atom_v) + .request(ua.actor(), infinite) .then( [=](bool) {}, [=](error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); - // send(event_group_, utility::event_atom_v, add_media_atom_v, ua); + // mail(utility::event_atom_v, add_media_atom_v, + // UuidActorVector({ua})).send(base_.event_group()); send_content_changed_event(); - base_.send_changed(event_group_, this); + base_.send_changed(); rp.deliver(ua); if (is_in_viewer_) open_media_reader(ua.actor()); @@ -1941,7 +2076,7 @@ void PlaylistActor::add_media( spdlog::warn( "{} {} {}", __PRETTY_FUNCTION__, to_string(err), to_string(ua.actor())); send_content_changed_event(); - base_.send_changed(event_group_, this); + base_.send_changed(); rp.deliver(ua); }); } @@ -1972,7 +2107,7 @@ void PlaylistActor::duplicate_container( notify_tree(new_tree); base_.insert_container(new_tree, uuid_before, into); - base_.send_changed(event_group_, this); + base_.send_changed(); std::function &)> flatten_tree; flatten_tree = [&flatten_tree](const PlaylistTree &tree, std::vector &rt) { rt.push_back(tree.uuid()); @@ -1990,9 +2125,11 @@ void PlaylistActor::duplicate_container( // session actors container groups/dividers and playlists.. void PlaylistActor::notify_tree(const utility::UuidTree &tree) { if (tree.value().type() == "ContainerDivider") { - send(event_group_, utility::event_atom_v, playlist::create_divider_atom_v, tree.uuid()); + mail(utility::event_atom_v, playlist::create_divider_atom_v, tree.uuid()) + .send(base_.event_group()); } else if (tree.value().type() == "ContainerGroup") { - send(event_group_, utility::event_atom_v, playlist::create_group_atom_v, tree.uuid()); + mail(utility::event_atom_v, playlist::create_group_atom_v, tree.uuid()) + .send(base_.event_group()); } else if ( tree.value().type() == "Subset" || tree.value().type() == "ContactSheet" || tree.value().type() == "Timeline") { @@ -2002,11 +2139,12 @@ void PlaylistActor::notify_tree(const utility::UuidTree & auto ua = UuidActor(tree.value().uuid(), container_[tree.value().uuid()]); if (tree.value().type() == "Subset") { - send(event_group_, utility::event_atom_v, create_subset_atom_v, ua); + mail(utility::event_atom_v, create_subset_atom_v, ua).send(base_.event_group()); } else if (tree.value().type() == "ContactSheet") { - send(event_group_, utility::event_atom_v, create_contact_sheet_atom_v, ua); + mail(utility::event_atom_v, create_contact_sheet_atom_v, ua) + .send(base_.event_group()); } else if (tree.value().type() == "Timeline") { - send(event_group_, utility::event_atom_v, create_timeline_atom_v, ua); + mail(utility::event_atom_v, create_timeline_atom_v, ua).send(base_.event_group()); } } @@ -2035,12 +2173,11 @@ void PlaylistActor::duplicate_tree(utility::UuidTree &tre auto result = request_receive( *sys, container_[tree.value().uuid()], duplicate_atom_v); + // need a list of source/dest media uuids. if (type == "Timeline") - anon_send( - result.actor(), - playhead::source_atom_v, - caf::actor_cast(this), - UuidUuidMap()); + anon_mail( + playhead::source_atom_v, caf::actor_cast(this), UuidUuidMap()) + .send(result.actor()); tree.value().set_uuid(result.uuid()); container_[result.uuid()] = result.actor(); @@ -2048,8 +2185,9 @@ void PlaylistActor::duplicate_tree(utility::UuidTree &tre // update name auto name = request_receive(*sys, result.actor(), name_atom_v); tree.value().set_name(name + " - copy"); - send(result.actor(), name_atom_v, tree.value().name()); - // send(event_group_, utility::event_atom_v, add_playlist_atom_v, result); + mail(name_atom_v, tree.value().name()).send(result.actor()); + // mail(utility::event_atom_v, add_playlist_atom_v, + // result).send(base_.event_group()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); // set to invalid uuid ? @@ -2074,13 +2212,11 @@ void PlaylistActor::duplicate_tree(utility::UuidTree &tre // for(const auto &i : items) { // try { // if(is_file_supported(i.first)){ -// anon_send( -// actor_cast(this), -// playlist::add_media_atom_v, +// anon_mail(// playlist::add_media_atom_v, // "New media", // i.first, // i.second -// ); +// ).send(// actor_cast(this)); // } else { // spdlog::warn("Unsupported file type {}.", to_string(i.first)); // } @@ -2102,8 +2238,8 @@ void PlaylistActor::open_media_readers() { void PlaylistActor::open_media_reader(caf::actor media_actor) { -#pragma message \ - "Disabling auto initialising of readers, it hits IO too hard when playlist is large. Needs a rethink." + // #pragma message \ +// "Disabling auto initialising of readers, it hits IO too hard when playlist is large. Needs a rethink." return; auto global_reader = system().registry().template get(media_reader_registry); @@ -2118,40 +2254,38 @@ void PlaylistActor::open_media_reader(caf::actor media_actor) { // the 'urgent' media reader for the media source ready for switching the source // in the UI - request(media_actor, infinite, media::get_media_pointer_atom_v, 0).then( + mail(media::get_media_pointer_atom_v, 0).request(media_actor, infinite).then( [=](const media::AVFrameID &media) mutable { if (!media.is_nil()) { - anon_send(global_reader, media_reader::get_image_atom_v, media, - true); std::string path = uri_to_posix_path(media.uri_); + anon_mail(media_reader::get_image_atom_v, media, + true).send(global_reader); std::string path = uri_to_posix_path(media.uri_); } }, [=](error&) mutable { } - );*/ + ); // force cache the first frame of every source in the playlist. This will open // the precache media reader for the media source ready for playback - request(media_actor, infinite, media::get_media_pointer_atom_v, 0) - .then( + mail(media::get_media_pointer_atom_v, 0) + .request(media_actor, infinite).then( [=](const media::AVFrameID &media) mutable { if (!media.is_nil()) { - anon_send( - global_reader, - media_reader::playback_precache_atom_v, + anon_mail(media_reader::playback_precache_atom_v, media, utility::clock::now() + std::chrono::seconds( 60), // saying we need this in 1 minute hence low priority - utility::Uuid()); + utility::Uuid()).send(global_reader); } }, - [=](error &) mutable {}); + [=](error &) mutable {});*/ } void PlaylistActor::send_content_changed_event(const bool queue) { @@ -2160,8 +2294,9 @@ void PlaylistActor::send_content_changed_event(const bool queue) { for (const auto &i : base_.media()) actors.emplace_back(i, media_.at(i)); - send(event_group_, utility::event_atom_v, media_content_changed_atom_v, actors); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); + mail(utility::event_atom_v, media_content_changed_atom_v, actors) + .send(base_.event_group()); + mail(utility::event_atom_v, utility::change_atom_v).send(change_event_group_); content_changed_ = false; // spdlog::warn("{} {}", __PRETTY_FUNCTION__, // to_string(caf::actor_cast(this))); @@ -2169,8 +2304,9 @@ void PlaylistActor::send_content_changed_event(const bool queue) { } else { if (not content_changed_) { content_changed_ = true; - delayed_anon_send( - this, std::chrono::milliseconds(100), media_content_changed_atom_v); + anon_mail(media_content_changed_atom_v) + .delay(std::chrono::milliseconds(100)) + .send(this); } } } @@ -2306,48 +2442,61 @@ PlaylistActor::get_containers(utility::UuidTree &tree) co return uav; } -void PlaylistActor::sort_alphabetically() { +void PlaylistActor::sort_by_media_display_info( + const int sort_column_index, const bool ascending) { using SourceAndUuid = std::pair; - auto media_names_vs_uuids = + auto sort_keys_vs_uuids = std::make_shared>>(); + int idx = 0; for (const auto &i : media_) { // Pro tip: because i is a reference, it's the reference that is captured in our lambda // below and therefore it is 'unstable' so we make a copy here and use that in the // lambda as this is object-copied in the capture instead. UuidActor media_actor(i.first, i.second); - - request(media_actor.actor(), infinite, media::media_reference_atom_v, utility::Uuid()) + idx++; + mail(media::media_display_info_atom_v) + .request(media_actor.actor(), infinite) .await( - [=](const std::pair &m_ref) mutable { - std::string path = uri_to_posix_path(m_ref.second.uri()); - path = std::string(path, path.rfind("/") + 1); - path = to_lower(path); + [=](const utility::JsonStore &media_display_info) mutable { + // media_display_info should be an array of arrays. Each + // array is the data shown in the media list columns. + // So info_set_idx corresponds to the media list (in the UI) + // from which the sort was requested. And the info_item_idx + // corresponds to the column of that media list. - (*media_names_vs_uuids).push_back(std::make_pair(path, media_actor.uuid())); + // default sort key keeps current sorting but should always + // put it after the last element that did have a sort key + std::string sort_key = fmt::format("ZZZZZZ{}", idx); - if (media_names_vs_uuids->size() == media_.size()) { + if (media_display_info.is_array() && + sort_column_index < media_display_info.size()) { + sort_key = media_display_info[sort_column_index].dump(); + } + + (*sort_keys_vs_uuids) + .push_back(std::make_pair(sort_key, media_actor.uuid())); + + if (sort_keys_vs_uuids->size() == media_.size()) { std::sort( - media_names_vs_uuids->begin(), - media_names_vs_uuids->end(), - [](const SourceAndUuid &a, const SourceAndUuid &b) -> bool { - return a.first < b.first; + sort_keys_vs_uuids->begin(), + sort_keys_vs_uuids->end(), + [ascending]( + const SourceAndUuid &a, const SourceAndUuid &b) -> bool { + return ascending ? a.first < b.first : a.first > b.first; }); utility::UuidList ordered_uuids; - for (const auto &p : (*media_names_vs_uuids)) { + for (const auto &p : (*sort_keys_vs_uuids)) { ordered_uuids.push_back(p.second); } - anon_send( - caf::actor_cast(this), - move_media_atom_v, - ordered_uuids, - utility::Uuid()); + anon_mail(move_media_atom_v, ordered_uuids, utility::Uuid()) + .send(caf::actor_cast(this)); } }, [=](error &err) mutable { @@ -2355,3 +2504,240 @@ void PlaylistActor::sort_alphabetically() { }); } } + + +void PlaylistActor::duplicate( + caf::typed_response_promise rp, + caf::actor src_bookmarks, + caf::actor dst_bookmarks) { + // spdlog::stopwatch sw; + + auto uuid = utility::Uuid::generate(); + auto actor = + spawn(base_.name(), uuid, caf::actor_cast(session_)); + + anon_mail(session::media_rate_atom_v, base_.media_rate()).send(actor); + anon_mail(playhead::playhead_rate_atom_v, base_.playhead_rate()).send(actor); + + { + caf::scoped_actor sys(system()); + auto json = request_receive(*sys, json_store_, json_store::get_json_atom_v); + anon_mail(json_store::set_json_atom_v, json, "").send(actor); + } + + // clone media into new playlist + // store mapping from old to new uuids + + // spdlog::info("duplicate {:.3} seconds.", sw); + + if (not media_.empty()) { + fan_out_request( + map_value_to_vec(media_), + infinite, + utility::duplicate_atom_v, + src_bookmarks, + dst_bookmarks) + .then( + [=](const std::vector dmedia) mutable { + std::map dmedia_map; + + // spdlog::info("cloned media {:.3} seconds.", sw); + + for (const auto &i : dmedia) + dmedia_map[i.first] = i.second; + + UuidUuidMap media_map; + UuidActorVector new_media; + + for (const auto &i : base_.media()) { + media_map[i] = dmedia_map[i].uuid(); + new_media.push_back(dmedia_map[i]); + } + + { + caf::scoped_actor sys(system()); + request_receive(*sys, actor, add_media_atom_v, new_media, Uuid()); + } + + // spdlog::info("added media {:.3} seconds.", sw); + + // handle containers. + duplicate_containers(rp, UuidActor(uuid, actor), media_map); + // spdlog::info("duplicate_containers {:.3} seconds.", sw); + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } else { + duplicate_containers(rp, UuidActor(uuid, actor), UuidUuidMap()); + } +} + +void PlaylistActor::duplicate_containers( + caf::typed_response_promise rp, + const utility::UuidActor &new_playlist, + const utility::UuidUuidMap &media_map) { + + try { + caf::scoped_actor sys(system()); + + // need to clone container tree.. + // if we process in order.... + for (const auto &i : base_.containers().uuids(true)) { + auto ii = base_.containers().cfind(i); + if (ii) { + // create in new playlist.. + // is actor ? + if (container_.count((*ii)->value().uuid())) { + auto ua = request_receive( + *sys, container_[(*ii)->value().uuid()], utility::duplicate_atom_v); + + // add to new playlist + auto nua = request_receive( + *sys, + new_playlist.actor(), + create_container_atom_v, + ua.actor(), + Uuid(), + false); + + // reassign media / reparent + + request_receive( + *sys, + nua.second.actor(), + playhead::source_atom_v, + new_playlist.actor(), + media_map); + + request_receive( + *sys, + new_playlist.actor(), + reflag_container_atom_v, + (*ii)->value().flag(), + nua.first); + } else { + // add to new playlist + auto u = request_receive( + *sys, + new_playlist.actor(), + create_divider_atom_v, + (*ii)->value().name(), + Uuid(), + false); + request_receive( + *sys, + new_playlist.actor(), + reflag_container_atom_v, + (*ii)->value().flag(), + u); + } + } + } + + // have we really changed ? + base_.send_changed(); + rp.deliver(new_playlist); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +void PlaylistActor::recursive_add_media_with_subsets( + caf::typed_response_promise> rp, + const caf::uri &path, + const utility::Uuid &uuid_before) { + + // 'path' should be a directory. Media directly within the directory + // will be added to this playlsit only. + // Any subdirectories in path will create a named subset, and the media + // within that subdirectory will be added to this subset + + auto result = std::make_shared>(); + + // First add media items that are directly inside 'path' + mail( + add_media_atom_v, + path, + false, // not recursive + uuid_before) + .request(caf::actor_cast(this), infinite) + .then( + [=](const std::vector &media) mutable { + result->insert(result->end(), media.begin(), media.end()); + + // find child folders + std::vector subfolders = utility::uri_subfolders(path); + if (!subfolders.size()) { + rp.deliver(*result); + } + + auto result_counter = std::make_shared(subfolders.size()); + + // func to decrement result_counter and deliver on the RP when + // we've finished + auto check_deliver = [result_counter, result, rp]() mutable { + (*result_counter)--; + if (!(*result_counter)) { + rp.deliver(*result); + } + }; + + for (auto subfolder : subfolders) { + + // now add the media from the subfolders + mail( + add_media_atom_v, + subfolder, + true, // recursive yes! + uuid_before) + .request(caf::actor_cast(this), infinite) + .then( + [=](const std::vector &media) mutable { + result->insert(result->end(), media.begin(), media.end()); + if (media.size()) { + + // make the subset and add the media into that. + fs::path p(uri_to_posix_path(subfolder)); + const auto subset_name = std::string(p.filename().string()); + mail( + create_subset_atom_v, + subset_name, + utility::Uuid(), + false) + .request(caf::actor_cast(this), infinite) + .then( + [=](utility::UuidUuidActor subset) mutable { + mail(add_media_atom_v, media, utility::Uuid()) + .request(subset.second.actor(), infinite) + .then( + [=](bool) mutable { check_deliver(); }, + [=](caf::error &err) mutable { + spdlog::warn( + "{} {}", + __PRETTY_FUNCTION__, + to_string(err)); + check_deliver(); + }); + }, + [=](caf::error &err) mutable { + spdlog::warn( + "{} {}", + __PRETTY_FUNCTION__, + to_string(err)); + check_deliver(); + }); + } + }, + [=](caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + check_deliver(); + }); + } + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + + mail(utility::event_atom_v, loading_media_atom_v, true).send(base_.event_group()); +} diff --git a/src/playlist/test/CMakeLists.txt b/src/playlist/test/CMakeLists.txt index c94f9dbaf..9268ac0b0 100644 --- a/src/playlist/test/CMakeLists.txt +++ b/src/playlist/test/CMakeLists.txt @@ -3,7 +3,7 @@ include(CTest) SET(LINK_DEPS xstudio::playlist xstudio::global - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/playlist/test/playlist_actor_test.cpp b/src/playlist/test/playlist_actor_test.cpp index 3e32e82c5..7fc1ec4aa 100644 --- a/src/playlist/test/playlist_actor_test.cpp +++ b/src/playlist/test/playlist_actor_test.cpp @@ -11,7 +11,6 @@ #include "xstudio/playhead/sub_playhead.hpp" #include "xstudio/playhead/playhead_actor.hpp" #include "xstudio/playlist/playlist_actor.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/helpers.hpp" using namespace xstudio::utility; @@ -43,46 +42,48 @@ TEST(PlaylistActorTest, Test) { [&](const std::string &name) { EXPECT_EQ(name, "Test"); }, [&](const caf::error &err) { EXPECT_TRUE(false) << to_string(err); }); - f.self->anon_send(tmp, name_atom_v, "Test2"); + anon_mail(name_atom_v, "Test2").send(tmp); f.self->request(tmp, std::chrono::seconds(10), name_atom_v) .receive( [&](const std::string &name) { EXPECT_EQ(name, "Test2"); }, [&](const caf::error &err) { EXPECT_TRUE(false) << to_string(err); }); auto su1 = Uuid::generate(); - f.self->anon_send( - tmp, - add_media_atom_v, - f.self->spawn( - "test", - Uuid(), - UuidActorVector({UuidActor( - su1, - f.self->spawn( - "test", - posix_path_to_uri(TEST_RESOURCE "/media/test.{:04d}.ppm"), - FrameList(1, 10), - utility::FrameRate(timebase::k_flicks_24fps), - su1))})), - - Uuid()); + f.self + ->mail( + add_media_atom_v, + f.self->spawn( + "test", + Uuid(), + UuidActorVector({UuidActor( + su1, + f.self->spawn( + "test", + posix_path_to_uri(TEST_RESOURCE "/media/test.{:04d}.ppm"), + FrameList(1, 10), + utility::FrameRate(timebase::k_flicks_24fps), + su1))})), + + Uuid()) + .send(tmp); auto su2 = Uuid::generate(); - f.self->anon_send( - tmp, - add_media_atom_v, - f.self->spawn( - "test", - Uuid(), - UuidActorVector({UuidActor( - su1, - f.self->spawn( - "test", - posix_path_to_uri(TEST_RESOURCE "/media/test.mov"), - utility::FrameRate(timebase::k_flicks_24fps), - su2))})), - Uuid()); + f.self + ->mail( + add_media_atom_v, + f.self->spawn( + "test", + Uuid(), + UuidActorVector({UuidActor( + su1, + f.self->spawn( + "test", + posix_path_to_uri(TEST_RESOURCE "/media/test.mov"), + utility::FrameRate(timebase::k_flicks_24fps), + su2))})), + Uuid()) + .send(tmp); JsonStore serial; f.self->request(tmp, infinite, serialise_atom_v) @@ -127,7 +128,7 @@ TEST(PlaylistActorTest, Test) { // Uuid() // ); -// f.self->anon_send(tmp, add_media_atom_v, +// f.self->anon_mail(add_media_atom_v, // f.self->spawn( // "Media3", // Uuid(), @@ -136,7 +137,7 @@ TEST(PlaylistActorTest, Test) { // "/media/test.mov")) // ), // Uuid() -// ); +// ).send(tmp); // // check our media refs work.. // f.self->request(tmp, std::chrono::seconds(10), get_media_pointer_atom_v, media::MT_IMAGE, diff --git a/src/plugin/colour_op/grading/src/CMakeLists.txt b/src/plugin/colour_op/grading/src/CMakeLists.txt index d739d8fef..34d553586 100644 --- a/src/plugin/colour_op/grading/src/CMakeLists.txt +++ b/src/plugin/colour_op/grading/src/CMakeLists.txt @@ -1,17 +1,21 @@ -project(grading VERSION 0.1.0 LANGUAGES CXX) +project(grading VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) find_package(Imath) find_package(OpenColorIO CONFIG) +find_package(Qt6 COMPONENTS Concurrent Core Quick Gui Qml Widgets OpenGL REQUIRED) set(SOURCES grading_data.cpp grading_data_serialiser.cpp grading_colour_op.cpp + grading_common.cpp grading.cpp grading_mask_gl_renderer.cpp serialisers/1.0/serialiser_1_pt_0.cpp ) +qt6_add_resources(SOURCES qml/Grading.2/grading.qrc) + set(CMAKE_INCLUDE_CURRENT_DIR ON) add_library(${PROJECT_NAME} SHARED ${SOURCES}) @@ -26,8 +30,9 @@ target_link_libraries(${PROJECT_NAME} xstudio::ui::opengl::viewport Imath::Imath OpenColorIO::OpenColorIO + Qt6::Core ) set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) -add_subdirectory(qml) \ No newline at end of file +add_plugin_qml(${PROJECT_NAME} qml) diff --git a/src/plugin/colour_op/grading/src/grading.cpp b/src/plugin/colour_op/grading/src/grading.cpp index e370812e7..93297bc88 100644 --- a/src/plugin/colour_op/grading/src/grading.cpp +++ b/src/plugin/colour_op/grading/src/grading.cpp @@ -5,6 +5,7 @@ #include "xstudio/ui/opengl/shader_program_base.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/string_helpers.hpp" +#include "xstudio/media_reader/image_buffer_set.hpp" #include "grading.h" #include "grading_mask_render_data.h" @@ -17,222 +18,128 @@ using namespace xstudio::colour_pipeline; using namespace xstudio::ui::viewport; -GradingTool::GradingTool(caf::actor_config &cfg, const utility::JsonStore &init_settings) - : plugin::StandardPlugin(cfg, "GradingTool", init_settings) { +namespace { - module::QmlCodeAttribute *button = add_qml_code_attribute( - "MyCode", - R"( - import Grading 1.0 - GradingButton { - anchors.fill: parent - } - )"); +std::vector array4d_to_vector4f(const std::array &arr) { + return std::vector{ + static_cast(arr[0]), + static_cast(arr[1]), + static_cast(arr[2]), + static_cast(arr[3])}; +} - button->expose_in_ui_attrs_group("media_tools_buttons_0"); - button->set_role_data(module::Attribute::ToolbarPosition, 500.0); +std::array vector4f_to_array4d(const std::vector &vec) { + return std::array{vec[0], vec[1], vec[2], vec[3]}; +} - // General +} // anonymous namespace - tool_is_active_ = - add_boolean_attribute("grading_tool_active", "grading_tool_active", false); - tool_is_active_->expose_in_ui_attrs_group("grading_settings"); - tool_is_active_->set_role_data( - module::Attribute::MenuPaths, - std::vector({"panels_main_menu_items|Grading Tool"})); - mask_is_active_ = add_boolean_attribute("mask_tool_active", "mask_tool_active", false); - mask_is_active_->expose_in_ui_attrs_group("grading_settings"); +GradingTool::GradingTool(caf::actor_config &cfg, const utility::JsonStore &init_settings) + : plugin::StandardPlugin(cfg, "GradingTool", init_settings) { + + // General + + tool_opened_count_ = add_integer_attribute("tool_opened_count", "tool_opened_count", 0); + tool_opened_count_->expose_in_ui_attrs_group("grading_settings"); grading_action_ = add_string_attribute("grading_action", "grading_action", ""); grading_action_->expose_in_ui_attrs_group("grading_settings"); - drawing_action_ = add_string_attribute("drawing_action", "drawing_action", ""); - drawing_action_->expose_in_ui_attrs_group("grading_settings"); + grading_bypass_ = add_boolean_attribute("grading_bypass", "grading_bypass", false); + grading_bypass_->expose_in_ui_attrs_group("grading_settings"); - // Grading elements + media_colour_managed_ = + add_boolean_attribute("media_colour_managed", "media_colour_managed", false); + media_colour_managed_->expose_in_ui_attrs_group("grading_settings"); - grading_panel_ = add_string_choice_attribute( - "grading_panel", - "grading_panel", - utility::map_value_to_vec(grading_panel_names_).front(), - utility::map_value_to_vec(grading_panel_names_)); - grading_panel_->expose_in_ui_attrs_group("grading_settings"); - grading_panel_->set_preference_path("/plugin/grading/grading_panel"); + // Grading elements - grading_layer_ = add_string_choice_attribute("grading_layer", "grading_layer"); - grading_layer_->expose_in_ui_attrs_group("grading_settings"); - grading_layer_->expose_in_ui_attrs_group("grading_layers"); + grading_bookmark_ = add_string_attribute("grading_bookmark", "grading_bookmark", ""); + grading_bookmark_->expose_in_ui_attrs_group("grading_settings"); - grading_bypass_ = add_boolean_attribute("drawing_bypass", "drawing_bypass", false); - grading_bypass_->expose_in_ui_attrs_group("grading_settings"); + colour_space_ = add_string_choice_attribute( + "colour_space", "colour_space", "scene_linear", {"scene_linear", "compositing_log"}); + colour_space_->expose_in_ui_attrs_group("grading_settings"); - grading_buffer_ = add_string_choice_attribute("grading_buffer", "grading_buffer"); - grading_buffer_->expose_in_ui_attrs_group("grading_settings"); + working_space_ = add_string_attribute("working_space", "working_space", ""); + working_space_->expose_in_ui_attrs_group("grading_settings"); // Slope - slope_red_ = add_float_attribute("Red Slope", "Red", 1.0f, 0.0f, 4.0f, 0.005f); - slope_red_->set_redraw_viewport_on_change(true); - slope_red_->set_role_data(module::Attribute::DefaultValue, 1.0f); - slope_red_->set_role_data(module::Attribute::ToolTip, "Red slope"); - slope_red_->expose_in_ui_attrs_group("grading_settings"); - slope_red_->expose_in_ui_attrs_group("grading_slope"); - slope_red_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(1.0f, 0.0f, 0.0f)); - - slope_green_ = add_float_attribute("Green Slope", "Green", 1.0f, 0.0f, 4.0f, 0.005f); - slope_green_->set_redraw_viewport_on_change(true); - slope_green_->set_role_data(module::Attribute::DefaultValue, 1.0f); - slope_green_->set_role_data(module::Attribute::ToolTip, "Green slope"); - slope_green_->expose_in_ui_attrs_group("grading_settings"); - slope_green_->expose_in_ui_attrs_group("grading_slope"); - slope_green_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(0.0f, 1.0f, 0.0f)); - - slope_blue_ = add_float_attribute("Blue Slope", "Blue", 1.0f, 0.0f, 4.0f, 0.005f); - slope_blue_->set_redraw_viewport_on_change(true); - slope_blue_->set_role_data(module::Attribute::DefaultValue, 1.0f); - slope_blue_->set_role_data(module::Attribute::ToolTip, "Blue slope"); - slope_blue_->expose_in_ui_attrs_group("grading_settings"); - slope_blue_->expose_in_ui_attrs_group("grading_slope"); - slope_blue_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(0.0f, 0.0f, 1.0f)); - - slope_master_ = add_float_attribute( - "Master Slope", "Master", 1.0f, std::pow(2.0, -6.0), std::pow(2.0, 6.0), 0.005f); - slope_master_->set_redraw_viewport_on_change(true); - slope_master_->set_role_data(module::Attribute::DefaultValue, 1.0f); - slope_master_->set_role_data(module::Attribute::ToolTip, "Master slope"); - slope_master_->expose_in_ui_attrs_group("grading_settings"); - slope_master_->expose_in_ui_attrs_group("grading_slope"); - slope_master_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(1.0f, 1.0f, 1.0f)); + slope_ = add_float_vector_attribute( + "Slope", + "Slope", + std::vector({1.0, 1.0, 1.0, 1.0}), // initial value + std::vector({0.0, 0.0, 0.0, 0.0}), // min + std::vector({4.0, 4.0, 4.0, 4.0}), // max + std::vector({0.005, 0.005, 0.005, 0.005}) // step + ); + slope_->expose_in_ui_attrs_group("grading_settings"); + slope_->expose_in_ui_attrs_group("grading_wheels"); // Offset - offset_red_ = add_float_attribute("Red Offset", "Red", 0.0f, -0.2f, 0.2f, 0.005f); - offset_red_->set_redraw_viewport_on_change(true); - offset_red_->set_role_data(module::Attribute::DefaultValue, 0.0f); - offset_red_->set_role_data(module::Attribute::ToolTip, "Red offset"); - offset_red_->expose_in_ui_attrs_group("grading_settings"); - offset_red_->expose_in_ui_attrs_group("grading_offset"); - offset_red_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(1.0f, 0.0f, 0.0f)); - - offset_green_ = add_float_attribute("Green Offset", "Green", 0.0f, -0.2f, 0.2f, 0.005f); - offset_green_->set_redraw_viewport_on_change(true); - offset_green_->set_role_data(module::Attribute::DefaultValue, 0.0f); - offset_green_->set_role_data(module::Attribute::ToolTip, "Green offset"); - offset_green_->expose_in_ui_attrs_group("grading_settings"); - offset_green_->expose_in_ui_attrs_group("grading_offset"); - offset_green_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(0.0f, 1.0f, 0.0f)); - - offset_blue_ = add_float_attribute("Blue Offset", "Blue", 0.0f, -0.2f, 0.2f, 0.005f); - offset_blue_->set_redraw_viewport_on_change(true); - offset_blue_->set_role_data(module::Attribute::DefaultValue, 0.0f); - offset_blue_->set_role_data(module::Attribute::ToolTip, "Blue offset"); - offset_blue_->expose_in_ui_attrs_group("grading_settings"); - offset_blue_->expose_in_ui_attrs_group("grading_offset"); - offset_blue_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(0.0f, 0.0f, 1.0f)); - - offset_master_ = add_float_attribute("Master Offset", "Master", 0.0f, -0.2f, 0.2f, 0.005f); - offset_master_->set_redraw_viewport_on_change(true); - offset_master_->set_role_data(module::Attribute::DefaultValue, 0.0f); - offset_master_->set_role_data(module::Attribute::ToolTip, "Master offset"); - offset_master_->expose_in_ui_attrs_group("grading_settings"); - offset_master_->expose_in_ui_attrs_group("grading_offset"); - offset_master_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(1.0f, 1.0f, 1.0f)); + offset_ = add_float_vector_attribute( + "Offset", + "Offset", + std::vector({0.0, 0.0, 0.0, 0.0}), // initial value + std::vector({-0.2, -0.2, -0.2, -0.2}), // min + std::vector({0.2, 0.2, 0.2, 0.2}), // max + std::vector({0.005, 0.005, 0.005, 0.005}) // step + ); + offset_->expose_in_ui_attrs_group("grading_settings"); + offset_->expose_in_ui_attrs_group("grading_wheels"); // Power - power_red_ = add_float_attribute("Red Power", "Red", 1.0f, 0.2f, 4.0f, 0.005f); - power_red_->set_redraw_viewport_on_change(true); - power_red_->set_role_data(module::Attribute::DefaultValue, 1.0f); - power_red_->set_role_data(module::Attribute::ToolTip, "Red power"); - power_red_->expose_in_ui_attrs_group("grading_settings"); - power_red_->expose_in_ui_attrs_group("grading_power"); - power_red_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(1.0f, 0.0f, 0.0f)); - - power_green_ = add_float_attribute("Green Power", "Green", 1.0f, 0.2f, 4.0f, 0.005f); - power_green_->set_redraw_viewport_on_change(true); - power_green_->set_role_data(module::Attribute::DefaultValue, 1.0f); - power_green_->set_role_data(module::Attribute::ToolTip, "Green power"); - power_green_->expose_in_ui_attrs_group("grading_settings"); - power_green_->expose_in_ui_attrs_group("grading_power"); - power_green_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(0.0f, 1.0f, 0.0f)); - - power_blue_ = add_float_attribute("Blue Power", "Blue", 1.0f, 0.2f, 4.0f, 0.005f); - power_blue_->set_redraw_viewport_on_change(true); - power_blue_->set_role_data(module::Attribute::DefaultValue, 1.0f); - power_blue_->set_role_data(module::Attribute::ToolTip, "Blue power"); - power_blue_->expose_in_ui_attrs_group("grading_settings"); - power_blue_->expose_in_ui_attrs_group("grading_power"); - power_blue_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(0.0f, 0.0f, 1.0f)); - - power_master_ = add_float_attribute("Master Power", "Master", 1.0f, 0.2f, 4.0f, 0.005f); - power_master_->set_redraw_viewport_on_change(true); - power_master_->set_role_data(module::Attribute::DefaultValue, 1.0f); - power_master_->set_role_data(module::Attribute::ToolTip, "Master power"); - power_master_->expose_in_ui_attrs_group("grading_settings"); - power_master_->expose_in_ui_attrs_group("grading_power"); - power_master_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(1.0f, 1.0f, 1.0f)); - - // Basic controls - // These directly maps to the CDL parameters above - - basic_exposure_ = - add_float_attribute("Basic Exposure", "Exposure", 0.0f, -6.0f, 6.0f, 0.1f); - basic_exposure_->set_redraw_viewport_on_change(true); - basic_exposure_->set_role_data(module::Attribute::DefaultValue, 0.0f); - basic_exposure_->set_role_data(module::Attribute::ToolTip, "Exposure"); - basic_exposure_->expose_in_ui_attrs_group("grading_settings"); - basic_exposure_->expose_in_ui_attrs_group("grading_simple"); - basic_exposure_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(1.0f, 1.0f, 1.0f)); - - basic_offset_ = add_float_attribute("Basic Offset", "Offset", 0.0f, -0.2f, 0.2f, 0.005f); - basic_offset_->set_redraw_viewport_on_change(true); - basic_offset_->set_role_data(module::Attribute::DefaultValue, 0.0f); - basic_offset_->set_role_data(module::Attribute::ToolTip, "Offset"); - basic_offset_->expose_in_ui_attrs_group("grading_settings"); - basic_offset_->expose_in_ui_attrs_group("grading_simple"); - basic_offset_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(1.0f, 1.0f, 1.0f)); - - basic_power_ = add_float_attribute("Basic Power", "Gamma", 1.0f, 0.2f, 4.0f, 0.005f); - basic_power_->set_redraw_viewport_on_change(true); - basic_power_->set_role_data(module::Attribute::DefaultValue, 1.0f); - basic_power_->set_role_data(module::Attribute::ToolTip, "Gamma"); - basic_power_->expose_in_ui_attrs_group("grading_settings"); - basic_power_->expose_in_ui_attrs_group("grading_simple"); - basic_power_->set_role_data( - module::Attribute::Colour, utility::ColourTriplet(1.0f, 1.0f, 1.0f)); + power_ = add_float_vector_attribute( + "Power", + "Power", + std::vector({1.0, 1.0, 1.0, 1.0}), // initial value + std::vector({0.2, 0.2, 0.2, 0.2}), // min + std::vector({4.0, 4.0, 4.0, 4.0}), // max + std::vector({0.005, 0.005, 0.005, 0.005}) // step + ); + power_->expose_in_ui_attrs_group("grading_settings"); + power_->expose_in_ui_attrs_group("grading_wheels"); // Sat - sat_ = add_float_attribute("Saturation", "Sat", 1.0f, 0.0f, 4.0f, 0.005f); - sat_->set_redraw_viewport_on_change(true); - sat_->set_role_data(module::Attribute::DefaultValue, 1.0f); - sat_->set_role_data(module::Attribute::ToolTip, "Saturation"); - sat_->expose_in_ui_attrs_group("grading_settings"); - sat_->expose_in_ui_attrs_group("grading_saturation"); - sat_->expose_in_ui_attrs_group("grading_simple"); - sat_->set_role_data(module::Attribute::Colour, utility::ColourTriplet(1.0f, 1.0f, 1.0f)); + saturation_ = add_float_attribute("Saturation", "Sat.", 1.0f, 0.0f, 4.0f, 0.005f); + saturation_->set_redraw_viewport_on_change(true); + saturation_->set_role_data(module::Attribute::DefaultValue, 1.0f); + saturation_->expose_in_ui_attrs_group("grading_settings"); + saturation_->expose_in_ui_attrs_group("grading_sliders"); + + // Exposure + exposure_ = add_float_attribute("Exposure", "Exp.", 0.0f, -8.0f, 8.0f, 0.1f); + exposure_->set_redraw_viewport_on_change(true); + exposure_->set_role_data(module::Attribute::DefaultValue, 0.0f); + exposure_->expose_in_ui_attrs_group("grading_settings"); + exposure_->expose_in_ui_attrs_group("grading_sliders"); + + // Contrast + contrast_ = add_float_attribute("Contrast", "Cont.", 1.0f, 0.001f, 4.0f, 0.005f); + contrast_->set_redraw_viewport_on_change(true); + contrast_->set_role_data(module::Attribute::DefaultValue, 1.0f); + contrast_->expose_in_ui_attrs_group("grading_settings"); + contrast_->expose_in_ui_attrs_group("grading_sliders"); // Masking elements drawing_tool_ = add_string_choice_attribute( "drawing_tool", "drawing_tool", - utility::map_value_to_vec(drawing_tool_names_).front(), + "Shape", utility::map_value_to_vec(drawing_tool_names_)); drawing_tool_->expose_in_ui_attrs_group("mask_tool_settings"); drawing_tool_->expose_in_ui_attrs_group("mask_tool_types"); + // Not using preference for drawing_tool attr for now, as it should + // be set to 'Shape' always as mask painting is disabled + // drawing_tool_->set_preference_path("/plugin/grading/drawing_tool"); + + drawing_action_ = add_string_attribute("drawing_action", "drawing_action", ""); + drawing_action_->expose_in_ui_attrs_group("grading_settings"); + drawing_action_->expose_in_ui_attrs_group("mask_tool_settings"); + draw_pen_size_ = add_integer_attribute("Draw Pen Size", "Draw Pen Size", 10, 1, 300); draw_pen_size_->expose_in_ui_attrs_group("mask_tool_settings"); draw_pen_size_->set_preference_path("/plugin/grading/draw_pen_size"); @@ -250,10 +157,31 @@ GradingTool::GradingTool(caf::actor_config &cfg, const utility::JsonStore &init_ pen_opacity_->expose_in_ui_attrs_group("mask_tool_settings"); pen_opacity_->set_preference_path("/plugin/grading/pen_opacity"); - pen_softness_ = add_integer_attribute("Pen Softness", "Pen Softness", 0, 0, 100); + pen_softness_ = add_integer_attribute("Pen Softness", "Pen Softness", 100, 0, 500); pen_softness_->expose_in_ui_attrs_group("mask_tool_settings"); pen_softness_->set_preference_path("/plugin/grading/pen_softness"); + shape_invert_ = add_boolean_attribute("shape_invert", "shape_invert", false); + shape_invert_->set_redraw_viewport_on_change(true); + shape_invert_->expose_in_ui_attrs_group("mask_tool_settings"); + + polygon_init_ = add_boolean_attribute("polygon_init", "polygon_init", false); + polygon_init_->set_redraw_viewport_on_change(true); + polygon_init_->expose_in_ui_attrs_group("mask_tool_settings"); + + interacting_ = add_boolean_attribute("interacting", "interacting", false); + interacting_->set_redraw_viewport_on_change(true); + interacting_->expose_in_ui_attrs_group("mask_tool_settings"); + + mask_selected_shape_ = + add_integer_attribute("mask_selected_shape", "mask_selected_shape", -1); + mask_selected_shape_->expose_in_ui_attrs_group("mask_tool_settings"); + + mask_shapes_visible_ = + add_boolean_attribute("mask_shapes_visible", "mask_shapes_visible", true); + mask_shapes_visible_->set_redraw_viewport_on_change(true); + mask_shapes_visible_->expose_in_ui_attrs_group("mask_tool_settings"); + display_mode_attribute_ = add_string_choice_attribute( "display_mode", "display_mode", @@ -262,32 +190,55 @@ GradingTool::GradingTool(caf::actor_config &cfg, const utility::JsonStore &init_ display_mode_attribute_->expose_in_ui_attrs_group("mask_tool_settings"); display_mode_attribute_->set_preference_path("/plugin/grading/display_mode"); - // This allows for quick toggle between masking & layering options enabled or disabled - mvp_1_release_ = add_boolean_attribute("mvp_1_release", "mvp_1_release", true); - mvp_1_release_->expose_in_ui_attrs_group("grading_settings"); - make_behavior(); listen_to_playhead_events(true); - reset_grade_layers(); + connect_to_ui(); - // we have to maintain a list of GradingColourOperator instances that are - // alive to send them messages about our state (currently only the state - // of the bypass attr) - set_down_handler([=](down_msg &msg) { - auto it = grading_colour_op_actors_.begin(); - while (it != grading_colour_op_actors_.end()) { - if (msg.source == *it) { - it = grading_colour_op_actors_.erase(it); - } else { - it++; + // Register the QML code that instances the grading tool UI in an + // xstudio 'panel'. It will show as 'Grading Tools' under the tabs + register_ui_panel_qml( + "Grading Tools", + R"( + + import QtQuick + import Grading 2.0 + + import xStudio 1.0 + + Item { + anchors.fill: parent + + XsGradientRectangle{ + anchors.fill: parent + } + + GradingTools { + anchors.fill: parent + } } - } - }); + )", + 6.0f, + "qrc:/icons/instant_mix.svg", + 2.0f); + + // here is where we declare the singleton overlay item that will draw + // our graphics and handle mouse interaction + qml_viewport_overlay_code( + R"( + import Grading 2.0 + + GradingOverlay { + anchors.fill: parent + } + )"); } -utility::BlindDataObjectPtr GradingTool::prepare_overlay_data( - const media_reader::ImageBufPtr &image, const bool offscreen) const { +utility::BlindDataObjectPtr GradingTool::onscreen_render_data( + const media_reader::ImageBufPtr &image, + const std::string & /*viewport_name*/, + const utility::Uuid & /*playhead_uuid*/, + const bool is_hero_image) const { // This callback is made just before viewport redraw. We want to check // if the image to be drawn is from the same media to which a grade is @@ -303,6 +254,7 @@ utility::BlindDataObjectPtr GradingTool::prepare_overlay_data( break; } } + if (we_are_editing_grade_on_this_image) { auto render_data = std::make_shared(); @@ -314,6 +266,7 @@ utility::BlindDataObjectPtr GradingTool::prepare_overlay_data( // reference/pointer to grading_data_ here so when drawing happens we're // using the interaction member data of this class. render_data->interaction_grading_data_ = grading_data_; + return render_data; } } @@ -326,93 +279,100 @@ AnnotationBasePtr GradingTool::build_annotation(const utility::JsonStore &data) } void GradingTool::images_going_on_screen( - const std::vector &images, + const media_reader::ImageBufDisplaySetPtr &images, const std::string viewport_name, const bool playhead_playing) { - // this callback happens just before every viewport refresh - - // for now, we only care about monitoring what's going on - // in the main viewport - if (viewport_name == "viewport0") { - if (images.size()) { - - current_on_screen_frame_ = images[0]; - int n = 0; - GradingData *grading_data = nullptr; - for (auto &bookmark : images[0].bookmarks()) { + // Only care about the main viewport(s), lightweight viewport will + // be named like quick_viewport_n. + if (!utility::starts_with(viewport_name, "viewport")) { + return; + } - auto data = dynamic_cast(bookmark->annotation_.get()); - if (data && !grading_data) { - grading_data = data; - n++; - } else if (data) { - n++; + // It's useful to keep a hold of the images that are on-screen so if the + // user starts drawing when there is a bookmark on screen then we can + // add the strokes to that existing bookmark instead of making a brand + // new note + if (images && current_frame_id_ != images->hero_image().frame_id()) { + + current_viewport_ = viewport_name; + viewport_current_images_ = images; + playhead_media_frame_ = images->hero_image().frame_id().frame() - + images->hero_image().frame_id().first_frame(); + current_frame_id_ = images->hero_image().frame_id(); + + if (!grading_data_.bookmark_uuid_.is_null()) { + // here we check if the current edited bookmark is still on-screen, + // i.e. has the user scrubbed away from the frame with a grade on + // that they were editing? + bool current_grade_is_onscreen = false; + for (const auto &bm : images->hero_image().bookmarks()) { + if (bm->detail_.uuid_ == grading_data_.bookmark_uuid_) { + current_grade_is_onscreen = true; + break; } } - - if (n > 1) { - spdlog::warn("Only one grading bookmark can be active at once, found {}", n); - } - - if (grading_data && grading_data->bookmark_uuid_ != grading_data_.bookmark_uuid_) { - - // there is a grade attached to the image but its not the one - // that we have been editing. Load the data for the new incoming - // grade ready for us to edit it. - load_grade_layers(grading_data); - - } else if ( - !grading_data && !grading_data_.identity() && - current_on_screen_frame_ != grading_data_creation_frame_) { - - // we have been editing a grade but there is no grading data for - // the on screen frame and the frame has changed since we - // created the edited grade. Thus we clear the edited grade as - // the playhead must have moved off the media that we had been - // grading - reset_grade_layers(); + if (!current_grade_is_onscreen) { + select_bookmark(utility::Uuid()); } } + + } else if (!images) { + viewport_current_images_.reset(); + select_bookmark(utility::Uuid()); + current_frame_id_ = media::AVFrameID(); } } +void GradingTool::on_screen_media_changed( + caf::actor media_actor, + const utility::MediaReference &media_ref, + const utility::JsonStore &colour_params) { + + const std::string config_name = colour_params.get_or("ocio_config", std::string("")); + const std::string working_space = + colour_params.get_or("working_space", std::string("scene_linear")); + // Only medias that are fully inverted to scene_linear currently support custom colour space + // This exclude medias that are only inverted to display_linear. + // TODO: Detect when display_linear input space and un-tone-mapped view are used + const bool is_unmanaged = + config_name == "" || config_name == "__raw__" || working_space != "scene_linear"; + + working_space_->set_value(working_space); + media_colour_managed_->set_value(!is_unmanaged); +} + void GradingTool::attribute_changed(const utility::Uuid &attribute_uuid, const int role) { - if (attribute_uuid == tool_is_active_->uuid()) { + // Only care about attribute value update. + if (role != module::Attribute::Value) + return; - if (tool_is_active_->value()) { - if (drawing_tool_->value() == "None") - drawing_tool_->set_value("Draw"); - grab_mouse_focus(); - } else { - release_mouse_focus(); - release_keyboard_focus(); - end_drawing(); - } + if (attribute_uuid == tool_opened_count_->uuid()) { - } else if (attribute_uuid == mask_is_active_->uuid()) { - if (mask_is_active_->value()) { - if (drawing_tool_->value() == "None") { - drawing_tool_->set_value("Draw"); - } - grab_mouse_focus(); + if (tool_opened_count_->value() < 0) { + tool_opened_count_->set_value(0); + } + + if (!grading_tools_active()) { - } else { release_mouse_focus(); release_keyboard_focus(); end_drawing(); } - refresh_current_layer_from_ui(); + } else if (attribute_uuid == grading_bookmark_->uuid()) { + + utility::Uuid bookmark_uuid(grading_bookmark_->value()); + select_bookmark(bookmark_uuid); } else if (attribute_uuid == grading_action_->uuid() && grading_action_->value() != "") { if (grading_action_->value() == "Clear") { - clear_cdl(); - refresh_current_layer_from_ui(); + clear_grade(); + refresh_current_grade_from_ui(); } else if (utility::starts_with(grading_action_->value(), "Save CDL ")) { @@ -421,28 +381,53 @@ void GradingTool::attribute_changed(const utility::Uuid &attribute_uuid, const i prefix_length, grading_action_->value().size() - prefix_length); save_cdl(filepath); - } else if (grading_action_->value() == "Prev Layer") { + } else if (grading_action_->value() == "Add CC") { - toggle_grade_layer(active_layer_ - 1); + save_bookmark(); + grading_data_ = GradingData(); + create_bookmark(); + save_bookmark(); - } else if (grading_action_->value() == "Next Layer") { + } else if (grading_action_->value() == "Remove CC") { - toggle_grade_layer(active_layer_ + 1); + remove_bookmark(); - } else if (grading_action_->value() == "Add Layer") { + } else if (utility::starts_with(grading_action_->value(), "Set Bookmark Full Range")) { - add_grade_layer(); + auto d = utility::split(grading_action_->value(), '|'); + if (d.size() != 2) + return; + utility::Uuid bm_uuid(d[1]); + auto bmd = get_bookmark_detail(bm_uuid); + if (bmd.media_reference_) { - } else if (grading_action_->value() == "Remove Layer") { + bmd.start_ = timebase::k_flicks_low; + bmd.duration_ = timebase::k_flicks_max; + update_bookmark_detail(bm_uuid, bmd); + } + select_bookmark(bm_uuid); + + } else if (utility::starts_with(grading_action_->value(), "Set Bookmark One Frame")) { + + auto d = utility::split(grading_action_->value(), '|'); + if (d.size() != 3) + return; + utility::Uuid bm_uuid(d[1]); + auto bmd = get_bookmark_detail(bm_uuid); + int media_frame = std::atoi(d[2].c_str()); + if (bmd.media_reference_) { + auto &media = bmd.media_reference_.value(); + bmd.start_ = media_frame * media.rate().to_flicks(); + bmd.duration_ = timebase::k_flicks_zero_seconds; + update_bookmark_detail(bm_uuid, bmd); + } - delete_grade_layer(); + select_bookmark(bm_uuid); } grading_action_->set_value(""); - } else if ( - - attribute_uuid == drawing_action_->uuid() && drawing_action_->value() != "") { + } else if (attribute_uuid == drawing_action_->uuid() && drawing_action_->value() != "") { if (drawing_action_->value() == "Clear") { clear_mask(); @@ -450,12 +435,41 @@ void GradingTool::attribute_changed(const utility::Uuid &attribute_uuid, const i undo(); } else if (drawing_action_->value() == "Redo") { redo(); + } else if (drawing_action_->value() == "Add quad") { + create_bookmark_if_empty(); + start_quad( + {Imath::V2f(-0.5, -0.5), + Imath::V2f(-0.5, 0.5), + Imath::V2f(0.5, 0.5), + Imath::V2f(0.5, -0.5)}); + update_boomark_shape(current_bookmark()); + } else if (utility::starts_with(drawing_action_->value(), "Add Polygon ")) { + create_bookmark_if_empty(); + const auto points = utility::split(drawing_action_->value().substr(12), ','); + if (points.size() % 2 == 0) { + std::vector poly_points; + for (size_t i = 0; i < points.size(); i += 2) { + poly_points.push_back( + Imath::V2f(std::stof(points[i]), std::stof(points[i + 1]))); + } + start_polygon(poly_points); + } + update_boomark_shape(current_bookmark()); + } else if (drawing_action_->value() == "Add ellipse") { + cancel_other_drawing_tools(); + create_bookmark_if_empty(); + start_ellipse(Imath::V2f(0.0, 0.0), Imath::V2f(0.35, 0.25), 90.0); + update_boomark_shape(current_bookmark()); + } else if (drawing_action_->value() == "Remove shape") { + create_bookmark_if_empty(); + remove_shape(mask_selected_shape_->value()); + update_boomark_shape(current_bookmark()); } drawing_action_->set_value(""); } else if (attribute_uuid == drawing_tool_->uuid()) { - if (tool_is_active_->value()) { + if (grading_tools_active()) { if (drawing_tool_->value() == "None") { release_mouse_focus(); @@ -469,49 +483,139 @@ void GradingTool::attribute_changed(const utility::Uuid &attribute_uuid, const i } else if (attribute_uuid == display_mode_attribute_->uuid()) { - refresh_current_layer_from_ui(); + refresh_current_grade_from_ui(); + + } else if (colour_space_ && attribute_uuid == colour_space_->uuid()) { + + refresh_current_grade_from_ui(); + save_bookmark(); } else if (attribute_uuid == grading_bypass_->uuid()) { for (auto &a : grading_colour_op_actors_) { - send(a, utility::event_atom_v, "bypass", grading_bypass_->value()); + mail(utility::event_atom_v, "bypass", grading_bypass_->value()).send(a); } } else if ( - (slope_red_ && slope_green_ && slope_blue_ && slope_master_) && - (offset_red_ && offset_green_ && offset_blue_ && offset_master_) && - (power_red_ && power_green_ && power_blue_ && power_master_) && - (basic_power_ && basic_offset_ && basic_exposure_) && (sat_) && - (attribute_uuid == slope_red_->uuid() || attribute_uuid == slope_green_->uuid() || - attribute_uuid == slope_blue_->uuid() || attribute_uuid == slope_master_->uuid() || - attribute_uuid == offset_red_->uuid() || attribute_uuid == offset_green_->uuid() || - attribute_uuid == offset_blue_->uuid() || attribute_uuid == offset_master_->uuid() || - attribute_uuid == power_red_->uuid() || attribute_uuid == power_green_->uuid() || - attribute_uuid == power_blue_->uuid() || attribute_uuid == power_master_->uuid() || - attribute_uuid == basic_power_->uuid() || attribute_uuid == basic_offset_->uuid() || - attribute_uuid == basic_exposure_->uuid() || attribute_uuid == sat_->uuid())) { - - // Make sure basic controls are in sync - if (attribute_uuid == basic_power_->uuid() || attribute_uuid == basic_offset_->uuid() || - attribute_uuid == basic_exposure_->uuid()) { - - slope_master_->set_value(std::pow(2.0, basic_exposure_->value()), false); - offset_master_->set_value(basic_offset_->value(), false); - power_master_->set_value(basic_power_->value(), false); + attribute_uuid == slope_->uuid() || attribute_uuid == offset_->uuid() || + attribute_uuid == power_->uuid() || attribute_uuid == saturation_->uuid() || + attribute_uuid == exposure_->uuid() || attribute_uuid == contrast_->uuid()) { - } else if ( - attribute_uuid == slope_master_->uuid() || - attribute_uuid == offset_master_->uuid() || - attribute_uuid == power_master_->uuid()) { + refresh_current_grade_from_ui(); + create_bookmark_if_empty(); + save_bookmark(); - basic_exposure_->set_value(std::log2(slope_master_->value()), false); - basic_offset_->set_value(offset_master_->value(), false); - basic_power_->set_value(power_master_->value(), false); + } else if (attribute_uuid == mask_selected_shape_->uuid()) { + + // Refresh UI settings according to selected shape + const int id = mask_selected_shape_->value(); + if (id >= 0 && id < mask_shapes_.size()) { + auto value = mask_shapes_[id]->role_data_as_json(module::Attribute::Value); + pen_opacity_->set_value( + value["opacity"], false); // 2nd flag means we don't get notified + pen_softness_->set_value(value["softness"], false); + // TODO: Fix colour json <-> qvariant conversion + // pen_colour_->set_value(value["colour"]); + shape_invert_->set_value(value["invert"], false); } - refresh_current_layer_from_ui(); - create_bookmark(); - save_bookmark(); + } else if (attribute_uuid == pen_softness_->uuid()) { + + // set softness on current shape + const int id = mask_selected_shape_->value(); + if (id >= 0 && id < mask_shapes_.size()) { + auto user_data = mask_shapes_[id]->role_data_as_json(module::Attribute::Value); + user_data["softness"] = pen_softness_->value(); + mask_shapes_[id]->set_role_data(module::Attribute::Value, user_data); + } + + } else if (attribute_uuid == pen_opacity_->uuid()) { + + // set opacity on current shape + const int id = mask_selected_shape_->value(); + if (id >= 0 && id < mask_shapes_.size()) { + auto user_data = mask_shapes_[id]->role_data_as_json(module::Attribute::Value); + user_data["opacity"] = pen_opacity_->value(); + mask_shapes_[id]->set_role_data(module::Attribute::Value, user_data); + } + + } else if (attribute_uuid == shape_invert_->uuid()) { + + // set invert on current shape + const int id = mask_selected_shape_->value(); + if (id >= 0 && id < mask_shapes_.size()) { + auto user_data = mask_shapes_[id]->role_data_as_json(module::Attribute::Value); + user_data["invert"] = shape_invert_->value(); + mask_shapes_[id]->set_role_data(module::Attribute::Value, user_data); + } + + } else if (attribute_uuid == interacting_->uuid()) { + + // this toggle allows us to 'grab' all mouse events, so that they arent' + // passwed on to the playhead, for example, which would otherwise mean + // the playhead would scrub frames when we drag a grading shape point to + // move it in the viewport + if (interacting_->value()) { + grab_mouse_focus(); + } else { + release_mouse_focus(); + } + + } else if (attribute_uuid == polygon_init_->uuid()) { + + if (polygon_init_->value()) { + cancel_other_drawing_tools(); + grab_mouse_focus(); + } else { + release_mouse_focus(); + } + + } else { + + for (auto &attr : mask_shapes_) { + if (attribute_uuid == attr->uuid()) { + auto value = attr->role_data_as_json(module::Attribute::Value); + auto user_data = attr->role_data_as_json(module::Attribute::UserData); + + if (value["type"] == "quad") { + grading_data_.mask().update_quad( + user_data, + // TODO: Fix colour json <-> qvariant conversion + // value["colour"], + pen_colour_->value(), + {value["bl"], value["tl"], value["tr"], value["br"]}, + value["softness"], + value["opacity"], + value["invert"]); + } else if (value["type"] == "polygon") { + grading_data_.mask().update_polygon( + user_data, + // TODO: Fix colour json <-> qvariant conversion + // value["colour"], + pen_colour_->value(), + value["points"], + value["softness"], + value["opacity"], + value["invert"]); + } else if (value["type"] == "ellipse") { + grading_data_.mask().update_ellipse( + user_data, + // TODO: Fix colour json <-> qvariant conversion + // value["colour"], + pen_colour_->value(), + value["center"], + value["radius"], + value["angle"], + value["softness"], + value["opacity"], + value["invert"]); + } + + save_bookmark(); + + break; + } + } } redraw_viewport(); @@ -519,17 +623,11 @@ void GradingTool::attribute_changed(const utility::Uuid &attribute_uuid, const i void GradingTool::register_hotkeys() { - toggle_active_hotkey_ = register_hotkey( - int('G'), + bypass_hotkey_ = register_hotkey( + int('B'), ui::ControlModifier, - "Toggle Grading Tool", - "Show or hide the grading toolbox"); - - toggle_mask_hotkey_ = register_hotkey( - int('M'), - ui::NoModifier, - "Toggle masking", - "Use drawing tools to apply a matte or apply grading to whole frame"); + "Bypass all grades", + "Bypass all grades applied on all media"); undo_hotkey_ = register_hotkey( int('Z'), @@ -545,22 +643,22 @@ void GradingTool::register_hotkeys() { } void GradingTool::hotkey_pressed( - const utility::Uuid &hotkey_uuid, const std::string & /*context*/) { - - if (hotkey_uuid == toggle_active_hotkey_) { - - tool_is_active_->set_value(!tool_is_active_->value()); + const utility::Uuid &hotkey_uuid, + const std::string & /*context*/, + const std::string & /*window*/) { - } else if (hotkey_uuid == toggle_mask_hotkey_ && tool_is_active_->value()) { + // This shortcut should be active even when no grading panel is visible + if (hotkey_uuid == bypass_hotkey_) { - mask_is_active_->set_value(!mask_is_active_->value()); + grading_bypass_->set_value(!grading_bypass_->value()); + redraw_viewport(); - } else if (hotkey_uuid == undo_hotkey_ && tool_is_active_->value()) { + } else if (hotkey_uuid == undo_hotkey_ && grading_tools_active()) { undo(); redraw_viewport(); - } else if (hotkey_uuid == redo_hotkey_ && tool_is_active_->value()) { + } else if (hotkey_uuid == redo_hotkey_ && grading_tools_active()) { redo(); redraw_viewport(); @@ -569,7 +667,7 @@ void GradingTool::hotkey_pressed( bool GradingTool::pointer_event(const ui::PointerEvent &e) { - if (!tool_is_active_->value() || !mask_is_active_->value()) + if (!grading_tools_active()) return false; bool redraw = true; @@ -578,14 +676,13 @@ bool GradingTool::pointer_event(const ui::PointerEvent &e) { if (drawing_tool_->value() == "Draw" || drawing_tool_->value() == "Erase") { - if (e.type() == ui::Signature::EventType::ButtonDown && + if (e.type() == ui::EventType::ButtonDown && e.buttons() == ui::Signature::Button::Left) { start_stroke(pointer_pos); } else if ( - e.type() == ui::Signature::EventType::Drag && - e.buttons() == ui::Signature::Button::Left) { + e.type() == ui::EventType::Drag && e.buttons() == ui::Signature::Button::Left) { update_stroke(pointer_pos); - } else if (e.type() == ui::Signature::EventType::ButtonRelease) { + } else if (e.type() == ui::EventType::ButtonRelease) { end_drawing(); } } else { @@ -598,119 +695,174 @@ bool GradingTool::pointer_event(const ui::PointerEvent &e) { return false; } -void GradingTool::start_stroke(const Imath::V2f &point) { +void GradingTool::turn_off_overlay_interaction() { + polygon_init_->set_value(false); + release_mouse_focus(); + release_keyboard_focus(); + end_drawing(); +} - LayerData *layer = current_layer(); - if (!layer) { - return; - } +bool GradingTool::grading_tools_active() const { return tool_opened_count_->value() > 0; } + +void GradingTool::start_stroke(const Imath::V2f &point) { if (drawing_tool_->value() == "Draw") { - layer->mask().start_stroke( + grading_data_.mask().start_stroke( pen_colour_->value(), draw_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE, pen_softness_->value() / 100.0, pen_opacity_->value() / 100.0); } else if (drawing_tool_->value() == "Erase") { - layer->mask().start_erase_stroke(erase_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE); + grading_data_.mask().start_erase_stroke( + erase_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE); } update_stroke(point); + + create_bookmark_if_empty(); } void GradingTool::update_stroke(const Imath::V2f &point) { - LayerData *layer = current_layer(); - if (!layer) { - return; - } - - layer->mask().update_stroke(point); + grading_data_.mask().update_stroke(point); } -void GradingTool::end_drawing() { +void GradingTool::start_quad(const std::vector &corners) { - LayerData *layer = current_layer(); - if (!layer) { - return; - } + uint32_t id = grading_data_.mask().start_quad(pen_colour_->value(), corners); - layer->mask().end_draw(); - save_bookmark(); + utility::JsonStore shape_data; + shape_data["type"] = "quad"; + shape_data["bl"] = corners[0]; + shape_data["tl"] = corners[1]; + shape_data["tr"] = corners[2]; + shape_data["br"] = corners[3]; + shape_data["colour"] = pen_colour_->value(); + shape_data["softness"] = 0.f; + shape_data["opacity"] = 100.f; + shape_data["invert"] = false; + + utility::JsonStore additional_roles; + additional_roles["user_data"] = id; + + const std::string name = fmt::format("Quad{}", id); + mask_shapes_.push_back(add_attribute(name, shape_data, additional_roles)); + expose_attribute_in_model_data(mask_shapes_.back(), "grading_tool_overlay_shapes"); + + end_drawing(); } -void GradingTool::undo() { +void GradingTool::start_polygon(const std::vector &points) { - LayerData *layer = current_layer(); - if (!layer) { - return; - } + uint32_t id = grading_data_.mask().start_polygon(pen_colour_->value(), points); - if (mask_is_active_->value()) { + utility::JsonStore shape_data; + shape_data["type"] = "polygon"; + shape_data["points"] = points; + shape_data["count"] = points.size(); + shape_data["colour"] = pen_colour_->value(); + shape_data["softness"] = 0.f; + shape_data["opacity"] = 100.f; + shape_data["invert"] = false; - layer->mask().undo(); - } + utility::JsonStore additional_roles; + additional_roles["user_data"] = id; - // TODO: Support undo / redo for grading + const std::string name = fmt::format("Polygon{}", id); + mask_shapes_.push_back(add_attribute(name, shape_data, additional_roles)); + expose_attribute_in_model_data(mask_shapes_.back(), "grading_tool_overlay_shapes"); + end_drawing(); + // calling this runs our code to update the mask data on the grading_data_ object + attribute_changed(mask_shapes_.back()->uuid(), module::Attribute::Value); } -void GradingTool::redo() { +void GradingTool::start_ellipse( + const Imath::V2f ¢er, const Imath::V2f &radius, float angle) { + + uint32_t id = + grading_data_.mask().start_ellipse(pen_colour_->value(), center, radius, angle); + + utility::JsonStore shape_data; + shape_data["type"] = "ellipse"; + shape_data["center"] = center; + shape_data["radius"] = radius; + shape_data["angle"] = angle; + shape_data["colour"] = pen_colour_->value(); + shape_data["softness"] = 0.f; + shape_data["opacity"] = 100.f; + shape_data["invert"] = false; + + utility::JsonStore additional_roles; + additional_roles["user_data"] = id; + + std::string name = fmt::format("Ellipse{}", id); + mask_shapes_.push_back(add_attribute(name, shape_data, additional_roles)); + expose_attribute_in_model_data(mask_shapes_.back(), "grading_tool_overlay_shapes"); + end_drawing(); + // calling this runs our code to update the mask data on the grading_data_ object + attribute_changed(mask_shapes_.back()->uuid(), module::Attribute::Value); +} - LayerData *layer = current_layer(); - if (!layer) { - return; - } +void GradingTool::remove_shape(int id) { + + if (id >= 0 && id < mask_shapes_.size()) { + auto canvas_id = mask_shapes_[id]->role_data_as_json(module::Attribute::UserData); + grading_data_.mask().remove_shape(canvas_id); - if (mask_is_active_->value()) { + remove_attribute(mask_shapes_[id]->uuid()); + mask_shapes_.erase(mask_shapes_.begin() + id); - layer->mask().redo(); + save_bookmark(); } +} + +void GradingTool::end_drawing() { - // TODO: Support undo / redo for grading + grading_data_.mask().end_draw(); + save_bookmark(); } -void GradingTool::clear_mask() { +void GradingTool::undo() { - LayerData *layer = current_layer(); - if (!layer) { - return; - } + grading_data_.mask().undo(); + save_bookmark(); +} + +void GradingTool::redo() { - layer->mask().clear(); + grading_data_.mask().redo(); + save_bookmark(); } -void GradingTool::clear_cdl() { +void GradingTool::clear_mask() { - slope_red_->set_value(slope_red_->get_role_data(module::Attribute::DefaultValue)); - slope_green_->set_value( - slope_green_->get_role_data(module::Attribute::DefaultValue)); - slope_blue_->set_value(slope_blue_->get_role_data(module::Attribute::DefaultValue)); - slope_master_->set_value( - slope_master_->get_role_data(module::Attribute::DefaultValue)); + grading_data_.mask().clear(); - offset_red_->set_value(offset_red_->get_role_data(module::Attribute::DefaultValue)); - offset_green_->set_value( - offset_green_->get_role_data(module::Attribute::DefaultValue)); - offset_blue_->set_value( - offset_blue_->get_role_data(module::Attribute::DefaultValue)); - offset_master_->set_value( - offset_master_->get_role_data(module::Attribute::DefaultValue)); + clear_shapes(); - power_red_->set_value(power_red_->get_role_data(module::Attribute::DefaultValue)); - power_green_->set_value( - power_green_->get_role_data(module::Attribute::DefaultValue)); - power_blue_->set_value(power_blue_->get_role_data(module::Attribute::DefaultValue)); - power_master_->set_value( - power_master_->get_role_data(module::Attribute::DefaultValue)); + save_bookmark(); +} - basic_exposure_->set_value( - basic_exposure_->get_role_data(module::Attribute::DefaultValue)); - basic_offset_->set_value( - basic_offset_->get_role_data(module::Attribute::DefaultValue)); - basic_power_->set_value( - basic_power_->get_role_data(module::Attribute::DefaultValue)); +void GradingTool::clear_shapes() { - sat_->set_value(sat_->get_role_data(module::Attribute::DefaultValue)); + for (size_t i = 0; i < mask_shapes_.size(); ++i) { + expose_attribute_in_model_data(mask_shapes_[i], "grading_tool_overlay_shapes", false); + remove_attribute(mask_shapes_[i]->uuid()); + } + mask_shapes_.clear(); +} + +void GradingTool::clear_grade() { + + slope_->set_value( + slope_->get_role_data>(module::Attribute::DefaultValue)); + offset_->set_value( + offset_->get_role_data>(module::Attribute::DefaultValue)); + power_->set_value( + power_->get_role_data>(module::Attribute::DefaultValue)); + saturation_->set_value(saturation_->get_role_data(module::Attribute::DefaultValue)); + exposure_->set_value(exposure_->get_role_data(module::Attribute::DefaultValue)); + contrast_->set_value(contrast_->get_role_data(module::Attribute::DefaultValue)); } void GradingTool::save_cdl(const std::string &filepath) const { @@ -718,22 +870,24 @@ void GradingTool::save_cdl(const std::string &filepath) const { OCIO::CDLTransformRcPtr cdl = OCIO::CDLTransform::Create(); std::array slope{ - slope_red_->value() * slope_master_->value(), - slope_green_->value() * slope_master_->value(), - slope_blue_->value() * slope_master_->value()}; + slope_->value()[0] * slope_->value()[3] * std::pow(2.f, exposure_->value()), + slope_->value()[1] * slope_->value()[3] * std::pow(2.f, exposure_->value()), + slope_->value()[2] * slope_->value()[3] * std::pow(2.f, exposure_->value())}; + std::array offset{ - offset_red_->value() + offset_master_->value(), - offset_green_->value() + offset_master_->value(), - offset_blue_->value() + offset_master_->value()}; + offset_->value()[0] + offset_->value()[3], + offset_->value()[1] + offset_->value()[3], + offset_->value()[2] + offset_->value()[3]}; + std::array power{ - power_red_->value() * power_master_->value(), - power_green_->value() * power_master_->value(), - power_blue_->value() * power_master_->value()}; + power_->value()[0] * power_->value()[3], + power_->value()[1] * power_->value()[3], + power_->value()[2] * power_->value()[3]}; cdl->setSlope(slope.data()); cdl->setOffset(offset.data()); cdl->setPower(power.data()); - cdl->setSat(sat_->value()); + cdl->setSat(saturation_->value()); OCIO::FormatMetadata &metadata = cdl->getFormatMetadata(); metadata.setID("0"); @@ -763,195 +917,237 @@ void GradingTool::save_cdl(const std::string &filepath) const { } } -void GradingTool::load_grade_layers(GradingData *grading_data) { +void GradingTool::refresh_current_grade_from_ui() { + + auto &grade = grading_data_.grade(); + + grade.slope = vector4f_to_array4d(slope_->value()); + grade.offset = vector4f_to_array4d(offset_->value()); + grade.power = vector4f_to_array4d(power_->value()); + grade.sat = saturation_->value(); + grade.exposure = exposure_->value(); + grade.contrast = contrast_->value(); + + grading_data_.set_colour_space(colour_space_->value()); + grading_data_.set_mask_editing(display_mode_attribute_->value() == "Mask"); +} + +void GradingTool::refresh_ui_from_current_grade() { - // Load layer(s) + auto &grade = grading_data_.grade(); - grading_data_ = *grading_data; - active_layer_ = grading_data_.size() - 1; + slope_->set_value(array4d_to_vector4f(grade.slope), false); + offset_->set_value(array4d_to_vector4f(grade.offset), false); + power_->set_value(array4d_to_vector4f(grade.power), false); + saturation_->set_value(float(grade.sat), false); + exposure_->set_value(float(grade.exposure), false); + contrast_->set_value(float(grade.contrast), false); - // Shader (re) construction + colour_space_->set_value(grading_data_.colour_space(), false); + display_mode_attribute_->set_value(grading_data_.mask_editing() ? "Mask" : "Grade", false); + // Initialize shapes overlay + using Quad = xstudio::ui::canvas::Quad; + using Polygon = xstudio::ui::canvas::Polygon; + using Ellipse = xstudio::ui::canvas::Ellipse; - // Update UI + clear_shapes(); + mask_selected_shape_->set_value(-1, false); - std::vector layer_choices; - for (int i = 0; i < grading_data_.size(); ++i) { - layer_choices.push_back(fmt::format("Layer {}", i + 1)); - } - grading_layer_->set_role_data(module::Attribute::StringChoices, layer_choices, false); - grading_layer_->set_value(layer_choices.back(), false); + // Please make sure that attribute_changed don't listen to + // Group change notification to avoid dealock acquiring the + // Canvas mutex when processing shapes event. + grading_data_.mask().read_lock(); - refresh_ui_from_current_layer(); -} + for (const auto &item : grading_data_.mask()) { + if (std::holds_alternative(item)) { -void GradingTool::reset_grade_layers() { + const Quad &quad = std::get(item); - grading_data_ = GradingData(); - grading_layer_->set_role_data( - module::Attribute::StringChoices, std::vector(), false); - grading_layer_->set_value("", false); - grading_data_creation_frame_ = media_reader::ImageBufPtr(); - add_grade_layer(); -} + utility::JsonStore shape_data; + shape_data["type"] = "quad"; + shape_data["bl"] = quad.bl; + shape_data["tl"] = quad.tl; + shape_data["tr"] = quad.tr; + shape_data["br"] = quad.br; + shape_data["colour"] = quad.colour; + shape_data["softness"] = quad.softness; + shape_data["opacity"] = quad.opacity; + shape_data["invert"] = quad.invert; -void GradingTool::add_grade_layer() { + utility::JsonStore additional_roles; + additional_roles["user_data"] = quad._id; - if (grading_data_.size() >= maximum_layers_) { - spdlog::warn("Maximum number of layers reached ({})", maximum_layers_); - return; - } + const std::string name = fmt::format("Quad{}", quad._id); + mask_shapes_.push_back(add_attribute(name, shape_data, additional_roles)); + expose_attribute_in_model_data(mask_shapes_.back(), "grading_tool_overlay_shapes"); - // Add layer on top + } else if (std::holds_alternative(item)) { - active_layer_ = grading_data_.size(); - grading_data_.push_layer(); + const Polygon &polygon = std::get(item); - // Update UI + utility::JsonStore shape_data; + shape_data["type"] = "polygon"; + shape_data["points"] = polygon.points; + shape_data["count"] = polygon.points.size(); + shape_data["colour"] = polygon.colour; + shape_data["softness"] = polygon.softness; + shape_data["opacity"] = polygon.opacity; + shape_data["invert"] = polygon.invert; - auto layer_name = std::string(fmt::format("Layer {}", active_layer_ + 1)); + utility::JsonStore additional_roles; + additional_roles["user_data"] = polygon._id; - auto layer_choices = grading_layer_->get_role_data>( - module::Attribute::StringChoices); - layer_choices.push_back(layer_name); - grading_layer_->set_role_data(module::Attribute::StringChoices, layer_choices, false); - grading_layer_->set_value(layer_choices.back(), false); + const std::string name = fmt::format("Polygon{}", polygon._id); + mask_shapes_.push_back(add_attribute(name, shape_data, additional_roles)); + expose_attribute_in_model_data(mask_shapes_.back(), "grading_tool_overlay_shapes"); - refresh_ui_from_current_layer(); -} + } else if (std::holds_alternative(item)) { -void GradingTool::toggle_grade_layer(size_t layer) { + const Ellipse &ellipse = std::get(item); - if (layer >= grading_data_.size() || layer < 0) { - spdlog::warn("Trying to toggle to non-existing layer {}", layer); - return; + utility::JsonStore shape_data; + shape_data["type"] = "ellipse"; + shape_data["center"] = ellipse.center; + shape_data["radius"] = ellipse.radius; + shape_data["angle"] = ellipse.angle; + shape_data["colour"] = ellipse.colour; + shape_data["softness"] = ellipse.softness; + shape_data["opacity"] = ellipse.opacity; + shape_data["invert"] = ellipse.invert; + + utility::JsonStore additional_roles; + additional_roles["user_data"] = ellipse._id; + + std::string name = fmt::format("Ellipse{}", ellipse._id); + mask_shapes_.push_back(add_attribute(name, shape_data, additional_roles)); + expose_attribute_in_model_data(mask_shapes_.back(), "grading_tool_overlay_shapes"); + } } + grading_data_.mask().read_unlock(); + + // Auto select the last shape, this will force shape controls + // (softness, opacity, etc) to refresh when changing selected layer. + if (!mask_shapes_.empty()) + mask_selected_shape_->set_value(mask_shapes_.size()); + else + mask_selected_shape_->set_value(-1); +} + +utility::Uuid GradingTool::current_bookmark() const { + + return utility::Uuid(grading_bookmark_->value()); +} - active_layer_ = layer; +utility::UuidList GradingTool::current_clip_bookmarks() { - // Update UI + // const utility::UuidList bookmarks_list = + utility::UuidList d = get_bookmarks_on_current_media(current_viewport_); - auto layer_name = std::string(fmt::format("Layer {}", active_layer_ + 1)); - grading_layer_->set_value(layer_name, false); + utility::UuidList filtered_list; + if (viewport_current_images_ && viewport_current_images_->hero_image()) { - refresh_ui_from_current_layer(); + for (auto &bookmark : viewport_current_images_->hero_image().bookmarks()) { + + if (bookmark->detail_.user_type_.value_or("") == "Grading") { + filtered_list.push_back(bookmark->detail_.uuid_); + } + } + } + return filtered_list; } -void GradingTool::delete_grade_layer() { +void GradingTool::create_bookmark_if_empty() { - if (grading_data_.size() < 2) { - spdlog::warn("Can't delete base grade layer"); - return; + if (!current_bookmark()) { + create_bookmark(); } +} - // Delete top layer +void GradingTool::create_bookmark() { - grading_data_.pop_layer(); - active_layer_ = grading_data_.size() - 1; + if (viewport_current_images_ && viewport_current_images_->hero_image()) { - // Update UI + bookmark::BookmarkDetail bmd; + // Hides bookmark from timeline + bmd.colour_ = "transparent"; + bmd.visible_ = false; + bmd.user_type_ = "Grading"; - auto layer_choices = grading_layer_->get_role_data>( - module::Attribute::StringChoices); - layer_choices.pop_back(); - grading_layer_->set_role_data(module::Attribute::StringChoices, layer_choices, false); - grading_layer_->set_value(layer_choices.back(), false); + utility::JsonStore user_data; + user_data["grade_active"] = true; + user_data["mask_active"] = false; - refresh_ui_from_current_layer(); -} + auto clip_layers = current_clip_bookmarks(); + user_data["layer_name"] = "Grade Layer " + std::to_string(clip_layers.size() + 1); -ui::viewport::LayerData *GradingTool::current_layer() { + bmd.user_data_ = user_data; - return grading_data_.layer(active_layer_); -} + auto uuid = StandardPlugin::create_bookmark_on_frame( + viewport_current_images_->hero_image().frame_id(), + "Grading Note", // bookmark_subject + bmd, // detail + true // bookmark_entire_duration + ); -void GradingTool::refresh_current_layer_from_ui() { + grading_data_.bookmark_uuid_ = uuid; + grading_data_.set_colour_space(working_space_->value()); + grading_bookmark_->set_value(utility::to_string(uuid), false); - LayerData *layer = current_layer(); - if (!layer) { - return; + refresh_ui_from_current_grade(); } - auto &grade = layer->grade(); - - grade.slope = { - slope_red_->value(), - slope_green_->value(), - slope_blue_->value(), - basic_exposure_->value()}; - grade.offset = { - offset_red_->value(), - offset_green_->value(), - offset_blue_->value(), - offset_master_->value()}; - grade.power = { - power_red_->value(), - power_green_->value(), - power_blue_->value(), - power_master_->value()}; - grade.sat = sat_->value(); - - layer->set_mask_active(mask_is_active_->value()); - layer->set_mask_editing(display_mode_attribute_->value() == "Mask"); -} - -void GradingTool::refresh_ui_from_current_layer() { - - LayerData *layer = current_layer(); - if (!layer) { - return; - } + spdlog::debug("Created bookmark {}", utility::to_string(grading_data_.bookmark_uuid_)); +} - auto &grade = layer->grade(); +void GradingTool::select_bookmark(const utility::Uuid &uuid) { - slope_red_->set_value(grade.slope[0], false); - slope_green_->set_value(grade.slope[1], false); - slope_blue_->set_value(grade.slope[2], false); - slope_master_->set_value(std::pow(2.0, grade.slope[3]), false); - basic_exposure_->set_value(grade.slope[3], false); + if (grading_data_.bookmark_uuid_ == uuid) + return; - offset_red_->set_value(grade.offset[0], false); - offset_green_->set_value(grade.offset[1], false); - offset_blue_->set_value(grade.offset[2], false); - offset_master_->set_value(grade.offset[3], false); - basic_offset_->set_value(grade.offset[3], false); + GradingData *grading_data_ptr = nullptr; + if (uuid) { + auto base_ptr = get_bookmark_annotation(uuid); + grading_data_ptr = dynamic_cast(base_ptr.get()); + } - power_red_->set_value(grade.power[0], false); - power_green_->set_value(grade.power[1], false); - power_blue_->set_value(grade.power[2], false); - power_master_->set_value(grade.power[3], false); - basic_power_->set_value(grade.power[3], false); + if (grading_data_ptr) { + grading_data_ = *grading_data_ptr; + } else { + grading_data_ = GradingData(); + grading_data_.set_colour_space(working_space_->value()); + } - sat_->set_value(grade.sat, false); + grading_data_.bookmark_uuid_ = uuid; + grading_bookmark_->set_value(utility::to_string(uuid), false); - // mask_is_active_->set_value(layer->mask_active()); - display_mode_attribute_->set_value(layer->mask_editing() ? "Mask" : "Grade"); + refresh_ui_from_current_grade(); } -utility::Uuid GradingTool::current_bookmark() const { return grading_data_.bookmark_uuid_; } +void GradingTool::update_boomark_shape(const utility::Uuid &uuid) { -void GradingTool::create_bookmark() { + auto bmd = get_bookmark_detail(uuid); + bool update = false; + if (bmd.user_data_) { - if (current_bookmark().is_null()) { + (*bmd.user_data_)["mask_active"] = !mask_shapes_.empty(); + update = true; + } - bookmark::BookmarkDetail bmd; - /*std::string name = on_screen_media_name_; - if (name.rfind("/") != std::string::npos) { - name = std::string(name, name.rfind("/") + 1); - } - std::ostringstream oss; - oss << name << " grading @ " << media_logical_frame_; - bmd.subject_ = oss.str();*/ + // First shape added on the layer, switch to Single frame mode + if (mask_shapes_.size() == 1) { - // Hides bookmark from timeline - bmd.colour_ = "transparent"; - bmd.visible_ = false; + if (bmd.media_reference_) { - grading_data_.bookmark_uuid_ = StandardPlugin::create_bookmark_on_current_media( - "viewport0", "Grading Note", bmd, true); - grading_data_creation_frame_ = current_on_screen_frame_; + auto &media = bmd.media_reference_.value(); + bmd.start_ = playhead_media_frame_ * media.rate().to_flicks(); + bmd.duration_ = timebase::k_flicks_zero_seconds; + update = true; + } + } - // StandardPlugin::update_bookmark_detail(grading_data_.bookmark_uuid_, bmd); + if (update) { + update_bookmark_detail(uuid, bmd); } } @@ -960,13 +1156,19 @@ void GradingTool::save_bookmark() { if (current_bookmark()) { StandardPlugin::update_bookmark_annotation( - current_bookmark(), - std::make_shared(grading_data_), - grading_data_.identity() // this will delete the bookmark if true - ); - if (grading_data_.identity()) { - reset_grade_layers(); - } + current_bookmark(), std::make_shared(grading_data_), false); + // spdlog::warn("Saved bookmark {}", utility::to_string(current_bookmark())); + } +} + +void GradingTool::remove_bookmark() { + + if (current_bookmark()) { + + // spdlog::warn("Removing bookmark {}", utility::to_string(current_bookmark())); + auto bm = current_bookmark(); + select_bookmark(utility::Uuid()); + StandardPlugin::remove_bookmark(bm); } } @@ -974,12 +1176,25 @@ caf::message_handler GradingTool::message_handler_extensions() { return caf::message_handler({[=](const std::string &desc, caf::actor grading_colour_op) { if (desc == "follow_bypass") { grading_colour_op_actors_.push_back(grading_colour_op); - monitor(grading_colour_op); - send( + + // we have to maintain a list of GradingColourOperator instances that are + // alive to send them messages about our state (currently only the state + // of the bypass attr) + monitor( grading_colour_op, - utility::event_atom_v, - "bypass", - grading_bypass_->value()); + [this, addr = grading_colour_op.address()](const error &) { + auto it = grading_colour_op_actors_.begin(); + while (it != grading_colour_op_actors_.end()) { + if (addr == *it) { + it = grading_colour_op_actors_.erase(it); + } else { + it++; + } + } + }); + + mail(utility::event_atom_v, "bypass", grading_bypass_->value()) + .send(grading_colour_op); } }}) .or_else(StandardPlugin::message_handler_extensions()); diff --git a/src/plugin/colour_op/grading/src/grading.h b/src/plugin/colour_op/grading/src/grading.h index 6704d4d36..450f9e1ab 100644 --- a/src/plugin/colour_op/grading/src/grading.h +++ b/src/plugin/colour_op/grading/src/grading.h @@ -2,6 +2,8 @@ #pragma once +#include + #include //NOLINT #include "xstudio/colour_pipeline/colour_operation.hpp" @@ -22,114 +24,118 @@ class GradingTool : public plugin::StandardPlugin { GradingTool(caf::actor_config &cfg, const utility::JsonStore &init_settings); ~GradingTool() override = default; - utility::BlindDataObjectPtr prepare_overlay_data( - const media_reader::ImageBufPtr &, const bool offscreen) const override; + utility::BlindDataObjectPtr onscreen_render_data( + const media_reader::ImageBufPtr &, const std::string & /*viewport_name*/, + const utility::Uuid &/*playhead_uuid*/, + const bool is_hero_image) const override; // Annotations (grading) bookmark::AnnotationBasePtr build_annotation(const utility::JsonStore &data) override; void images_going_on_screen( - const std::vector & images, + const media_reader::ImageBufDisplaySetPtr &images, const std::string viewport_name, const bool playhead_playing ) override; + void on_screen_media_changed( + caf::actor, + const utility::MediaReference &, + const utility::JsonStore & + ) override; + // Interactions void attribute_changed( const utility::Uuid &attribute_uuid, const int role) override; void register_hotkeys() override; - void hotkey_pressed(const utility::Uuid &hotkey_uuid, const std::string &context) override; + void hotkey_pressed(const utility::Uuid &hotkey_uuid, const std::string &context, const std::string &window) override; bool pointer_event(const ui::PointerEvent &e) override; - // Playhead events + void turn_off_overlay_interaction() override; protected: - caf::message_handler message_handler_extensions() override; private: + bool grading_tools_active() const; + void start_stroke(const Imath::V2f &point); void update_stroke(const Imath::V2f &point); + + void start_quad(const std::vector &corners); + void start_polygon(const std::vector &points); + void start_ellipse(const Imath::V2f ¢er, const Imath::V2f &radius, float angle); + + void remove_shape(int id); + void end_drawing(); void undo(); void redo(); void clear_mask(); - void clear_cdl(); + void clear_shapes(); + void clear_grade(); void save_cdl(const std::string &filepath) const; - void load_grade_layers(ui::viewport::GradingData* grading_data); - void reset_grade_layers(); - void add_grade_layer(); - void toggle_grade_layer(size_t layer); - void delete_grade_layer(); - - ui::viewport::LayerData* current_layer(); - void refresh_current_layer_from_ui(); - void refresh_ui_from_current_layer(); - utility::Uuid current_bookmark() const; + utility::UuidList current_clip_bookmarks(); + void create_bookmark_if_empty(); void create_bookmark(); + void select_bookmark(const utility::Uuid &uuid); + void update_boomark_shape(const utility::Uuid &uuid); void save_bookmark(); + void remove_bookmark(); + void refresh_current_grade_from_ui(); + void refresh_ui_from_current_grade(); private: // General - module::BooleanAttribute *tool_is_active_ {nullptr}; - module::BooleanAttribute *mask_is_active_ {nullptr}; - module::StringAttribute *grading_action_ {nullptr}; - module::StringAttribute *drawing_action_ {nullptr}; + module::IntegerAttribute *tool_opened_count_ {nullptr}; + module::StringAttribute *grading_action_ {nullptr}; + module::BooleanAttribute *grading_bypass_ {nullptr}; + module::StringAttribute *drawing_action_ {nullptr}; + module::BooleanAttribute *media_colour_managed_ {nullptr}; // Grading - enum class GradingPanel { Basic, CDLSliders, CDLWheels }; - const std::map grading_panel_names_ = { - {GradingPanel::Basic, "Basic"}, - {GradingPanel::CDLSliders, "Sliders"}, - {GradingPanel::CDLWheels, "Wheels"} - }; + module::StringAttribute *grading_bookmark_ {nullptr}; + module::StringAttribute *working_space_ {nullptr}; + module::StringChoiceAttribute *colour_space_ {nullptr}; - module::StringChoiceAttribute *grading_panel_ {nullptr}; - module::StringChoiceAttribute *grading_layer_ {nullptr}; - module::BooleanAttribute *grading_bypass_ {nullptr}; - module::StringChoiceAttribute *grading_buffer_ {nullptr}; - - module::FloatAttribute *slope_red_ {nullptr}; - module::FloatAttribute *slope_green_ {nullptr}; - module::FloatAttribute *slope_blue_ {nullptr}; - module::FloatAttribute *slope_master_ {nullptr}; - module::FloatAttribute *offset_red_ {nullptr}; - module::FloatAttribute *offset_green_ {nullptr}; - module::FloatAttribute *offset_blue_ {nullptr}; - module::FloatAttribute *offset_master_ {nullptr}; - module::FloatAttribute *power_red_ {nullptr}; - module::FloatAttribute *power_green_ {nullptr}; - module::FloatAttribute *power_blue_ {nullptr}; - module::FloatAttribute *power_master_ {nullptr}; - - module::FloatAttribute *basic_exposure_ {nullptr}; - module::FloatAttribute *basic_offset_ {nullptr}; - module::FloatAttribute *basic_power_ {nullptr}; - - module::FloatAttribute *sat_ {nullptr}; + module::FloatVectorAttribute *slope_ {nullptr}; + module::FloatVectorAttribute *offset_ {nullptr}; + module::FloatVectorAttribute *power_ {nullptr}; + module::FloatAttribute *saturation_ {nullptr}; + module::FloatAttribute *exposure_ {nullptr}; + module::FloatAttribute *contrast_ {nullptr}; // Drawing Mask - enum class DrawingTool { Draw, Erase, None }; + enum class DrawingTool { Draw, Erase, Shape, None }; const std::map drawing_tool_names_ = { + {DrawingTool::Shape, "Shape"}, + {DrawingTool::None, "None"}, {DrawingTool::Draw, "Draw"}, {DrawingTool::Erase, "Erase"} }; - module::StringChoiceAttribute *drawing_tool_ {nullptr}; - module::IntegerAttribute *draw_pen_size_ {nullptr}; - module::IntegerAttribute *erase_pen_size_ {nullptr}; - module::IntegerAttribute *pen_opacity_ {nullptr}; - module::IntegerAttribute *pen_softness_ {nullptr}; - module::ColourAttribute *pen_colour_ {nullptr}; + module::StringChoiceAttribute *drawing_tool_ {nullptr}; + module::IntegerAttribute *draw_pen_size_ {nullptr}; + module::IntegerAttribute *erase_pen_size_ {nullptr}; + module::IntegerAttribute *pen_opacity_ {nullptr}; + module::IntegerAttribute *pen_softness_ {nullptr}; + module::ColourAttribute *pen_colour_ {nullptr}; + module::BooleanAttribute *shape_invert_ {nullptr}; + module::BooleanAttribute *polygon_init_ {nullptr}; + module::BooleanAttribute *interacting_ {nullptr}; + + module::IntegerAttribute *mask_selected_shape_{nullptr}; + module::BooleanAttribute *mask_shapes_visible_{nullptr}; + std::vector mask_shapes_; enum DisplayMode { Mask, Grade }; const std::map display_mode_names_ = { @@ -138,29 +144,23 @@ class GradingTool : public plugin::StandardPlugin { }; module::StringChoiceAttribute *display_mode_attribute_ {nullptr}; - // MVP delivery phase management - module::BooleanAttribute *mvp_1_release_ {nullptr}; - // Shortcuts - utility::Uuid toggle_active_hotkey_; - utility::Uuid toggle_mask_hotkey_; + utility::Uuid bypass_hotkey_; utility::Uuid undo_hotkey_; utility::Uuid redo_hotkey_; // Current media info (eg. for Bookmark creation) bool playhead_is_playing_ {false}; + int playhead_media_frame_ = {0}; - // Grading + media_reader::ImageBufDisplaySetPtr viewport_current_images_; + std::string current_viewport_; + media::AVFrameID current_frame_id_; + // Grading ui::viewport::GradingData grading_data_; - media_reader::ImageBufPtr current_on_screen_frame_; - media_reader::ImageBufPtr grading_data_creation_frame_; - - inline static const size_t maximum_layers_ {8}; - size_t active_layer_ {0}; std::vector grading_colour_op_actors_; - }; } // xstudio::colour_pipeline diff --git a/src/plugin/colour_op/grading/src/grading_colour_op.cpp b/src/plugin/colour_op/grading/src/grading_colour_op.cpp index cb45e9bdf..5ccb709d3 100644 --- a/src/plugin/colour_op/grading/src/grading_colour_op.cpp +++ b/src/plugin/colour_op/grading/src/grading_colour_op.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include "xstudio/ui/opengl/shader_program_base.hpp" #include "xstudio/utility/helpers.hpp" @@ -10,6 +11,7 @@ #include "grading_colour_op.hpp" #include "grading_mask_render_data.h" #include "grading_mask_gl_renderer.h" +#include "grading_common.h" using namespace xstudio; using namespace xstudio::bookmark; @@ -19,194 +21,161 @@ using namespace xstudio::ui::viewport; namespace { -// N.B. Just one layer for now. The shader will become very bloated with -// several layers. Could we reuse one grading function in each layer instead -// and convert the uniforms to an array? See commented shader below for an -// example ... -static const int NUM_LAYERS = 1; const char *fragment_shader = R"( -#version 430 core +#version 410 core +#define SCENE_LINEAR 0 +#define COMPOSITING_LOG 1 -uniform bool grade_tool_op; +struct Grade { + int color_space; + bool grade_active; -// LayerDeclarations -vec4 colour_transform_op(vec4 rgba, vec2 image_pos) { + // Masking + bool mask_active; + bool mask_editing; - vec4 image_col = rgba; - - if (grade_tool_op) { - return image_col; - } - - // LayerInvocations - return image_col; -} -)"; - -const char *layer_template = R"( -// Layer + // CDL + vec3 slope; + vec3 offset; + vec3 power; + float sat; -uniform sampler2D layer_mask; -uniform bool layer_mask_active; -uniform bool layer_mask_editing; -//OCIOTransform -vec4 apply_layer(vec4 image_col, vec2 image_pos) { + float exposure; + float contrast; - vec4 mask_color = layer_mask_active ? texture(layer_mask, image_pos) : vec4(1.0); - float mask_alpha = clamp(mask_color.a, 0.0, 1.0); - - // Output color graded pixels - if (layer_mask_active && !layer_mask_editing) { - vec4 graded_col = OCIOLayer(image_col); - return vec4(mix(image_col.rgb, graded_col.rgb, mask_alpha), image_col.a); - } - // Output mask color pixels - else if (layer_mask_active) { - float mask_opacity = 0.5 * mask_alpha; - return vec4(mix(image_col.rgb, mask_color.rgb, mask_opacity), image_col.a); - } - else { - return OCIOLayer(image_col); - } -} -)"; + // Can be extended with more grading operations +}; -const char *layer_call = R"( - image_col = apply_layer(image_col, image_pos); -)"; +uniform bool tool_active; +uniform int grade_count; +uniform sampler2D masks[8]; +uniform Grade grades[8]; -// here's how it might look with uniform arrays and a single grading function: +vec4 apply_grade(vec4 rgba, int layer_index) +{ -/*const char * temp_static_shader = R"( -#version 430 core + Grade grade = grades[layer_index]; -uniform bool grade_tool_op; + vec4 outColor = rgba; -// Layer0 + // Exposure -uniform sampler2D layer_mask[8]; -uniform bool layer_mask_active[8]; -uniform bool layer_mask_editing[8]; + if (grade.exposure != 0.) + { + outColor.rgb *= pow(2.0, grade.exposure); + } -// Declaration of all variables + // Contrast -uniform vec3 ocio_layer_grading_primary_offset[8]; -uniform vec3 ocio_layer_grading_primary_exposure[8]; -uniform vec3 ocio_layer_grading_primary_contrast[8]; -uniform float ocio_layer_grading_primary_pivot[8]; -uniform float ocio_layer_grading_primary_clampBlack[8]; -uniform float ocio_layer_grading_primary_clampWhite[8]; -uniform float ocio_layer_grading_primary_saturation[8]; -uniform bool ocio_layer_grading_primary_localBypass[8]; + if (grade.contrast != 1.) + { + outColor.rgb = pow( abs(outColor.rgb / 0.18), vec3(grade.contrast)) * sign(outColor.rgb) * 0.18; + } -// Declaration of the OCIO shader function + // CDL SOP -vec4 OCIOLayer0(vec4 inPixel, int layer_index) -{ - vec4 outColor = inPixel; + if (grade.slope != vec3(1., 1., 1.) + || grade.offset != vec3(0., 0., 0.) + || grade.power != vec3(1., 1., 1.)) + { + outColor.rgb *= grade.slope; + outColor.rgb += grade.offset; + if (grade.power != vec3(1., 1., 1.)) + { + // Strict CDL specs do not match Nuke, using OCIO mirrored style here. + // outColor.rgb = pow(clamp(outColor.rgb, 0.0, 1.0), grade.power); + outColor.rgb = pow(abs(outColor.rgb), grade.power) * sign(outColor.rgb); + } + } - // Add GradingPrimary 'linear' forward processing + // CDL Sat - { - if (!ocio_layer_grading_primary_localBypass[layer_index]) + if (grade.sat != 1.) { - outColor.rgb += ocio_layer_grading_primary_offset[layer_index]; - outColor.rgb *= ocio_layer_grading_primary_exposure[layer_index]; - if ( ocio_layer_grading_primary_contrast[layer_index] != vec3(1., 1., 1.) ) - { - outColor.rgb = pow( - abs(outColor.rgb / ocio_layer_grading_primary_pivot[layer_index]), - ocio_layer_grading_primary_contrast[layer_index] ) * sign(outColor.rgb) * -ocio_layer_grading_primary_pivot[layer_index]; - } - vec3 lumaWgts = vec3(0.212599993, 0.715200007, 0.0722000003); - float luma = dot( outColor.rgb, lumaWgts ); - outColor.rgb = luma + ocio_layer_grading_primary_saturation[layer_index] * (outColor.rgb - -luma); outColor.rgb = clamp( outColor.rgb, ocio_layer_grading_primary_clampBlack[layer_index], - ocio_layer_grading_primary_clampWhite[layer_index] - ); + vec3 lumaWgts = vec3(0.212599993, 0.715200007, 0.0722000003); + float luma = dot(outColor.rgb, lumaWgts); + outColor.rgb = luma + grade.sat * (outColor.rgb - luma); } - } - return outColor; + // Can be extended with more grading operations + + return outColor; } -vec4 apply_layer(vec4 image_col, vec2 image_pos, int layer_index) { +vec4 apply_layer(vec4 rgba, vec2 image_pos, int layer_index) +{ - vec4 mask_color = layer_mask_active[layer_index] ? texture(layer_mask[layer_index], -image_pos) : vec4(1.0); float mask_alpha = clamp(mask_color.a, 0.0, 1.0); + Grade grade = grades[layer_index]; + vec4 mask_color = grade.mask_active ? texture(masks[layer_index], image_pos) : vec4(1.0); + float mask_alpha = clamp(mask_color.a, 0.0, 1.0); - // Output color graded pixels - if (layer_mask_active[layer_index] && !layer_mask_editing[layer_index]) { - vec4 graded_col = OCIOGradeFunc(image_col, layer_index); - return vec4(mix(image_col.rgb, graded_col.rgb, mask_alpha), image_col.a); + if (grade.mask_active && !grade.mask_editing) + { + vec4 graded_col = apply_grade(rgba, layer_index); + return vec4(mix(rgba.rgb, graded_col.rgb, mask_alpha), rgba.a); } - // Output mask color pixels - else if (layer_mask_active[layer_index]) { + else if (grade.mask_active) + { float mask_opacity = 0.5 * mask_alpha; - return vec4(mix(image_col.rgb, mask_color.rgb, mask_opacity), image_col.a); + return vec4(mix(rgba.rgb, mask_color.rgb, mask_opacity), rgba.a); } - else { - return OCIOGradeFunc(image_col, layer_index); + else + { + return apply_grade(rgba, layer_index); } } -vec4 colour_transform_op(vec4 rgba, vec2 image_pos) { - - vec4 image_col = rgba; +//INJECT_LIN_TO_LOG +//INJECT_LOG_TO_LIN - if (grade_tool_op) { - return image_col; +vec4 apply_color_conv(vec4 rgba, int source_space, int dest_space) +{ + if (source_space != dest_space) + { + if (source_space == SCENE_LINEAR) + { + rgba = OCIOLinToLog(rgba); + } + else + { + rgba = OCIOLogToLin(rgba); + } } - // would a for loop have better performance? - if (apply_layer[0]) image_col = apply_layer(image_col, image_pos, 0); - if (apply_layer[1]) image_col = apply_layer(image_col, image_pos, 1); - if (apply_layer[2]) image_col = apply_layer(image_col, image_pos, 2); - if (apply_layer[3]) image_col = apply_layer(image_col, image_pos, 3); - if (apply_layer[4]) image_col = apply_layer(image_col, image_pos, 4); - if (apply_layer[5]) image_col = apply_layer(image_col, image_pos, 5); - if (apply_layer[6]) image_col = apply_layer(image_col, image_pos, 6); - if (apply_layer[7]) image_col = apply_layer(image_col, image_pos, 7); - - return image_col; + return rgba; } -)";*/ - -OCIO::GradingPrimary grading_primary_from_cdl( - std::array slope, - std::array offset, - std::array power, - double sat) { - - OCIO::GradingPrimary gp(OCIO::GRADING_LIN); - - for (int i = 0; i < 4; ++i) { - if (slope[i] > 0) { - offset[i] = offset[i] / slope[i]; - slope[i] = std::log2(slope[i]); - } else { - slope[i] = std::numeric_limits::lowest(); + +vec4 colour_transform_op(vec4 rgba, vec2 image_pos) +{ + vec4 image_col = rgba; + + if (tool_active) + { + // xStudio guarantee conversion to scene_linear + int current_space = SCENE_LINEAR; + + for (int i = 0; i < grade_count; ++i) + { + if (grades[i].grade_active) { + if (grades[i].color_space != current_space) + { + image_col = apply_color_conv(image_col, current_space, grades[i].color_space); + current_space = grades[i].color_space; + } + image_col = apply_layer(image_col, image_pos, i); + } } - } - // Lower bound on power is 0.01 - const float power_lower = 0.01f; - for (int i = 0; i < 4; ++i) { - if (power[i] < power_lower) { - power[i] = power_lower; + if (current_space != SCENE_LINEAR) + { + image_col = apply_color_conv(image_col, current_space, SCENE_LINEAR); } } - gp.m_offset = OCIO::GradingRGBM(offset[0], offset[1], offset[2], offset[3]); - gp.m_exposure = OCIO::GradingRGBM(slope[0], slope[1], slope[2], slope[3]); - gp.m_contrast = OCIO::GradingRGBM(power[0], power[1], power[2], power[3]); - gp.m_saturation = sat; - gp.m_pivot = std::log2(1.0 / 0.18); - - return gp; + return image_col; } +)"; } // anonymous namespace @@ -214,19 +183,17 @@ GradingColourOperator::GradingColourOperator( caf::actor_config &cfg, const utility::JsonStore &init_settings) : ColourOpPlugin(cfg, "GradingColourOperator", init_settings) { - // the shader and any LUTs needed for colour transforms is static - // so we only build it once - build_shader_data(); - // ask plugin manager for the instance of the GradingTool plugin auto pm = system().registry().template get(plugin_manager_registry); - request(pm, infinite, plugin_manager::get_resident_atom_v, GradingTool::PLUGIN_UUID) + mail(plugin_manager::get_resident_atom_v, GradingTool::PLUGIN_UUID) + .request(pm, infinite) .then( [=](caf::actor grading_tool) mutable { // ping the grading tool with a pointer to ourselves, so it can // send us updates on the 'bypass' attr. GradingTool of course has // the necessary message handler for this - anon_send(grading_tool, "follow_bypass", caf::actor_cast(this)); + anon_mail("follow_bypass", caf::actor_cast(this)) + .send(grading_tool); }, [=](caf::error &err) mutable { @@ -235,7 +202,7 @@ GradingColourOperator::GradingColourOperator( caf::message_handler GradingColourOperator::message_handler_extensions() { - // here's our handler for the messages coming from the GradintTool about + // here's our handler for the messages coming from the GradingTool about // the state of its 'bypass' attribute. return caf::message_handler( {[=](utility::event_atom, const std::string &desc, bool bypass) { @@ -249,151 +216,191 @@ caf::message_handler GradingColourOperator::message_handler_extensions() { ColourOperationDataPtr GradingColourOperator::colour_op_graphics_data( utility::UuidActor &media_source, const utility::JsonStore &media_source_colour_metadata) { - // N.B. 'colour_op_data_' is 'static' in that it is built once when this - // class is constructed. If it becomes dynamic such that the shader and/or - // LUTs it contains change depending on the grading data being displayed - // then you must create new pointer data here - return colour_op_data_; + // The result of this call will depend on the ocio config, as the grading op + // depends on the lin to lon transform which generally varies per ocio config. + + // It's also possible that the result will depend on the ocio_context because + // for some jobs the log to lin transform varies PER SHOT. To Be extra safe + // we make a hash from the whole context data dictionary + + std::string hash_data; + if (media_source_colour_metadata.contains("ocio_config")) { + hash_data += media_source_colour_metadata.get_or("ocio_config", std::string("")); + } + if (media_source_colour_metadata.contains("ocio_context")) { + hash_data += media_source_colour_metadata["ocio_context"].dump(); + } + + size_t hash = std::hash{}(hash_data); + auto p = colour_op_data_cache_.find(hash); + if (p != colour_op_data_cache_.end()) { + return p->second; + } + + colour_op_data_cache_[hash] = setup_shader_data(media_source_colour_metadata); + return colour_op_data_cache_[hash]; } utility::JsonStore GradingColourOperator::update_shader_uniforms(const media_reader::ImageBufPtr &image) { utility::JsonStore uniforms_dict; - if (!bypass_) { - size_t layer_id = 0; - for (auto &bookmark : image.bookmarks()) { + uniforms_dict["grade_count"] = 0; + uniforms_dict["tool_active"] = false; - auto data = dynamic_cast(bookmark->annotation_.get()); - if (data) { + if (bypass_) { + return uniforms_dict; + } - for (auto &layer : *data) { + size_t grade_count = 0; + auto active_grades = get_active_grades(image); + for (const auto grade_info : active_grades) { + + auto grade_data = grade_info.data; + + std::string grade_str = fmt::format("grades[{}].", grade_count); + + uniforms_dict[grade_str + "grade_active"] = grade_info.isActive; + + // We only support compositing_log as colour space conversion for now. + // All other values will be treated as being the current colour space. + uniforms_dict[grade_str + "color_space"] = + grade_data->colour_space() == "compositing_log" ? 1 : 0; + uniforms_dict[grade_str + "mask_active"] = !grade_data->mask().empty(); + + // NOTE (Ted) .. + // When grade_data->mask_editing() is true, the mask is drawn applied + // to the image as a yellow mix - in other words the grade isn't applied. + // but the mask is shown in transparent yellow. + // This depends on the 'display_mode' attr in the plugin, but ther is + // nowhere in the UI to control this. Therefore, I disable here because + // otherwise I'm sometimes seeing the mask_editing() flag as true, seems + // a bit random and it's undesired + uniforms_dict[grade_str + "mask_editing"] = false; + // Here's what we used to have here: + // uniforms_dict[grade_str + "mask_editing"] = grade_data->mask_editing(); + + uniforms_dict[grade_str + "slope"] = { + "vec3", + 1, + grade_data->grade().slope[0] * grade_data->grade().slope[3], + grade_data->grade().slope[1] * grade_data->grade().slope[3], + grade_data->grade().slope[2] * grade_data->grade().slope[3]}; + uniforms_dict[grade_str + "offset"] = { + "vec3", + 1, + grade_data->grade().offset[0] + grade_data->grade().offset[3], + grade_data->grade().offset[1] + grade_data->grade().offset[3], + grade_data->grade().offset[2] + grade_data->grade().offset[3]}; + uniforms_dict[grade_str + "power"] = { + "vec3", + 1, + grade_data->grade().power[0] * grade_data->grade().power[3], + grade_data->grade().power[1] * grade_data->grade().power[3], + grade_data->grade().power[2] * grade_data->grade().power[3]}; + uniforms_dict[grade_str + "sat"] = grade_data->grade().sat; + uniforms_dict[grade_str + "exposure"] = grade_data->grade().exposure; + uniforms_dict[grade_str + "contrast"] = grade_data->grade().contrast; + + grade_count++; + } - uniforms_dict[fmt::format("layer{}_mask_active", layer_id)] = - layer.mask_active(); - uniforms_dict[fmt::format("layer{}_mask_editing", layer_id)] = - layer.mask_editing(); + if (grade_count) { + uniforms_dict["grade_count"] = grade_count; + uniforms_dict["tool_active"] = true; + } - update_dynamic_parameters( - shader_data_[layer_id].shader_desc, layer.grade()); - update_all_uniforms(shader_data_[layer_id].shader_desc, uniforms_dict); - layer_id++; + //std::cerr << uniforms_dict.dump() << "\n"; - if (layer_id == NUM_LAYERS) { - // have a fixed number of layers for now - break; - } - } - break; - } - } - if (layer_id) { - uniforms_dict["grade_tool_op"] = bypass_; - } else { - // no grade. Turn off! - uniforms_dict["grade_tool_op"] = true; - } - } else { - uniforms_dict["grade_tool_op"] = true; - } return uniforms_dict; } -void GradingColourOperator::build_shader_data() { +std::shared_ptr GradingColourOperator::setup_shader_data( + const utility::JsonStore &media_source_colour_metadata) { - /*if (grading_data->size() == shader_data_.size()) return; - - shader_data_.clear();*/ + auto colour_op_data = + std::make_shared(ColourOperationData(PLUGIN_UUID, "Grade OP")); - size_t layer_id = 0; - for (size_t layer_id = 0; layer_id < NUM_LAYERS; ++layer_id) { + std::string fs_str = fragment_shader; + std::vector &fs_luts = colour_op_data->luts_; + // Inject colour transforms + { auto desc = setup_ocio_shader( - fmt::format("OCIOLayer{}", layer_id), fmt::format("ocio_layer{}_", layer_id)); + "OCIOLinToLog", + "ocio_lin_to_log", + media_source_colour_metadata, + "scene_linear", + "compositing_log"); + fs_str = utility::replace_once(fs_str, "//INJECT_LIN_TO_LOG", desc->getShaderText()); auto luts = setup_ocio_textures(desc); - - shader_data_.emplace_back(LayerShaderData{desc, luts}); - layer_id++; + fs_luts.insert(fs_luts.end(), luts.begin(), luts.end()); + } + { + auto desc = setup_ocio_shader( + "OCIOLogToLin", + "ocio_log_to_lin", + media_source_colour_metadata, + "compositing_log", + "scene_linear"); + fs_str = utility::replace_once(fs_str, "//INJECT_LOG_TO_LIN", desc->getShaderText()); + auto luts = setup_ocio_textures(desc); + fs_luts.insert(fs_luts.end(), luts.begin(), luts.end()); } - setup_colourop_shader(); - - std::string cache_id; - - cache_id += std::to_string(shader_data_.size()); - - colour_op_data_ = - std::make_shared(ColourOperationData(PLUGIN_UUID, "Grade OP")); - - // we allow for LUTs in the grading operation (although for colour SOP no - // LUTs are needed) - std::vector &luts = colour_op_data_->luts_; + gradingop_shader_ = std::make_shared(PLUGIN_UUID, fs_str); - layer_id = 0; - for (auto &data : shader_data_) { - luts.insert(luts.end(), data.luts.begin(), data.luts.end()); - layer_id++; + colour_op_data->shader_ = gradingop_shader_; + colour_op_data->set_cache_id( + fmt::format("{}", std::hash{}(fragment_shader))); + for (const auto &lut : fs_luts) { + colour_op_data->set_cache_id( + colour_op_data->cache_id() + fmt::format("{}", lut->cache_id())); } - - if (!gradingop_shader_) - setup_colourop_shader(); - colour_op_data_->shader_ = gradingop_shader_; - colour_op_data_->luts_ = luts; - // TODO: Update cache later when supporting colour space conversions - colour_op_data_->cache_id_ = cache_id; + return colour_op_data; } plugin::GPUPreDrawHookPtr -GradingColourOperator::make_pre_draw_gpu_hook(const int /*viewer_index*/) { - return plugin::GPUPreDrawHookPtr( - static_cast(new GradingMaskRenderer())); -} - -void GradingColourOperator::setup_colourop_shader() { - - std::string fs_str = fragment_shader; - size_t curr_id = 0; - - for (auto &data : shader_data_) { - std::string layer_str = layer_template; - - layer_str = utility::replace_once( - layer_str, "//OCIOTransform", data.shader_desc->getShaderText()); - - fs_str = utility::replace_once( - fs_str, "// LayerDeclarations", layer_str + std::string("\n// LayerDeclarations")); - - fs_str = utility::replace_once( - fs_str, "// LayerInvocations", layer_call + std::string("// LayerInvocations")); - - fs_str = utility::replace_all(fs_str, "", std::to_string(curr_id)); - - curr_id++; - } - - fs_str = utility::replace_once(fs_str, "// LayerDeclarations", ""); - fs_str = utility::replace_once(fs_str, "// LayerInvocations", ""); - - gradingop_shader_ = std::make_shared(PLUGIN_UUID, fs_str); +GradingColourOperator::make_pre_draw_gpu_hook(const std::string &viewport_name) { + return std::make_shared(viewport_name); } OCIO::ConstGpuShaderDescRcPtr GradingColourOperator::setup_ocio_shader( - const std::string &function_name, const std::string &resource_prefix) { - - // TODO: Use actual media OCIO config here to support colour space conversion - auto config = OCIO::GetCurrentConfig(); - auto gp = OCIO::GradingPrimaryTransform::Create(OCIO::GRADING_LIN); - gp->makeDynamic(); + const std::string &function_name, + const std::string &resource_prefix, + const utility::JsonStore &metadata, + const std::string &src, + const std::string &dst) { auto desc = OCIO::GpuShaderDesc::CreateShaderDesc(); desc->setLanguage(OCIO::GPU_LANGUAGE_GLSL_4_0); desc->setFunctionName(function_name.c_str()); desc->setResourcePrefix(resource_prefix.c_str()); - auto gpu = config->getProcessor(gp)->getDefaultGPUProcessor(); - gpu->extractGpuShaderInfo(desc); + try { + const std::string config_name = metadata.get_or("ocio_config", std::string("")); + auto config = OCIO::Config::CreateFromFile(config_name.c_str()); - return desc; + auto context = config->getCurrentContext()->createEditableCopy(); + if (metadata.contains("ocio_context")) { + if (metadata["ocio_context"].is_object()) { + for (auto &item : metadata["ocio_context"].items()) { + context->setStringVar( + item.key().c_str(), std::string(item.value()).c_str()); + } + } + } + + auto gpu = + config->getProcessor(context, src.c_str(), dst.c_str())->getDefaultGPUProcessor(); + gpu->extractGpuShaderInfo(desc); + return desc; + } catch (const OCIO::Exception &ex) { + auto config = OCIO::Config::CreateRaw(); + auto gpu = config->getProcessor("raw", "raw")->getDefaultGPUProcessor(); + gpu->extractGpuShaderInfo(desc); + return desc; + } } std::vector @@ -471,8 +478,8 @@ GradingColourOperator::setup_ocio_textures(OCIO::ConstGpuShaderDescRcPtr &shader : LUTDescriptor::NEAREST; auto xs_lut = std::make_shared( height > 1 - ? LUTDescriptor::Create2DLUT(width, height, xs_dtype, xs_channels, xs_interp) - : LUTDescriptor::Create1DLUT(width, xs_dtype, xs_channels, xs_interp), + ? LUTDescriptor::Create2DLUT(width, height, xs_dtype, xs_channels, xs_interp) + : LUTDescriptor::Create1DLUT(width, xs_dtype, xs_channels, xs_interp), samplerName); const int channels = channel == OCIO::GpuShaderCreator::TEXTURE_RED_CHANNEL ? 1 : 3; @@ -486,67 +493,3 @@ GradingColourOperator::setup_ocio_textures(OCIO::ConstGpuShaderDescRcPtr &shader return luts; } - -void GradingColourOperator::update_dynamic_parameters( - OCIO::ConstGpuShaderDescRcPtr &shader, const ui::viewport::Grade &grade) const { - - if (shader->hasDynamicProperty(OCIO::DYNAMIC_PROPERTY_GRADING_PRIMARY)) { - - OCIO::DynamicPropertyRcPtr property = - shader->getDynamicProperty(OCIO::DYNAMIC_PROPERTY_GRADING_PRIMARY); - OCIO::DynamicPropertyGradingPrimaryRcPtr primary_prop = - OCIO::DynamicPropertyValue::AsGradingPrimary(property); - - OCIO::GradingPrimary gp = grading_primary_from_cdl( - std::array{ - grade.slope[0], grade.slope[1], grade.slope[2], std::pow(2.0, grade.slope[3])}, - std::array{ - grade.offset[0], grade.offset[1], grade.offset[2], grade.offset[3]}, - std::array{ - grade.power[0], grade.power[1], grade.power[2], grade.power[3]}, - grade.sat); - - primary_prop->setValue(gp); - } -} - -void GradingColourOperator::update_all_uniforms( - OCIO::ConstGpuShaderDescRcPtr &shader, utility::JsonStore &uniforms) const { - - const unsigned max_uniforms = shader->getNumUniforms(); - - for (unsigned idx = 0; idx < max_uniforms; ++idx) { - OCIO::GpuShaderDesc::UniformData uniform_data; - const char *name = shader->getUniform(idx, uniform_data); - - switch (uniform_data.m_type) { - case OCIO::UNIFORM_DOUBLE: { - uniforms[name] = uniform_data.m_getDouble(); - break; - } - case OCIO::UNIFORM_BOOL: { - // TODO: ColSci - // This property is buggy at the moment and might report - // grade_tool_op even though some fields are set (eg. only saturation). - // This can be removed when upgrading to OCIO 2.3 - if (utility::ends_with(name, "grading_primary_localBypass")) { - uniforms[name] = false; - } else { - uniforms[name] = uniform_data.m_getBool(); - } - break; - } - case OCIO::UNIFORM_FLOAT3: { - uniforms[name] = { - "vec3", - 1, - uniform_data.m_getFloat3()[0], - uniform_data.m_getFloat3()[1], - uniform_data.m_getFloat3()[2]}; - break; - } - default: - break; - } - } -} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/grading_colour_op.hpp b/src/plugin/colour_op/grading/src/grading_colour_op.hpp index 8ceb7eb47..e6c4ae07e 100644 --- a/src/plugin/colour_op/grading/src/grading_colour_op.hpp +++ b/src/plugin/colour_op/grading/src/grading_colour_op.hpp @@ -32,18 +32,21 @@ class GradingColourOperator : public ColourOpPlugin { utility::JsonStore update_shader_uniforms(const media_reader::ImageBufPtr &image) override; - plugin::GPUPreDrawHookPtr make_pre_draw_gpu_hook(const int /*viewer_index*/) override; + plugin::GPUPreDrawHookPtr make_pre_draw_gpu_hook(const std::string &viewport_name) override; protected: caf::message_handler message_handler_extensions() override; private: - void build_shader_data(); + std::shared_ptr + setup_shader_data(const utility::JsonStore &media_source_colour_metadata); - void setup_colourop_shader(); - - OCIO::ConstGpuShaderDescRcPtr - setup_ocio_shader(const std::string &function_name, const std::string &resource_prefix); + OCIO::ConstGpuShaderDescRcPtr setup_ocio_shader( + const std::string &function_name, + const std::string &resource_prefix, + const utility::JsonStore &metadata, + const std::string &src, + const std::string &dst); std::vector setup_ocio_textures(OCIO::ConstGpuShaderDescRcPtr &shader); @@ -55,15 +58,7 @@ class GradingColourOperator : public ColourOpPlugin { ui::viewport::GPUShaderPtr gradingop_shader_; - struct LayerShaderData { - OCIO::ConstGpuShaderDescRcPtr shader_desc; - std::vector luts; - }; - using GradingShaderData = std::vector; - - GradingShaderData shader_data_; - - ColourOperationDataPtr colour_op_data_; + std::map colour_op_data_cache_; bool bypass_ = {false}; }; diff --git a/src/plugin/colour_op/grading/src/grading_common.cpp b/src/plugin/colour_op/grading/src/grading_common.cpp new file mode 100644 index 000000000..ab70db58a --- /dev/null +++ b/src/plugin/colour_op/grading/src/grading_common.cpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/media_reader/image_buffer.hpp" +#include "xstudio/utility/helpers.hpp" + +#include "grading.h" +#include "grading_mask_render_data.h" +#include "grading_mask_gl_renderer.h" +#include "grading_colour_op.hpp" +#include "grading_common.h" + +using namespace xstudio; +using namespace xstudio::ui::viewport; + + +std::vector +xstudio::ui::viewport::get_active_grades(const xstudio::media_reader::ImageBufPtr &image) { + + std::vector active_grades; + + // Make sure the bookmarks are applied in a consistent order. + // The order matters when applying multiple CDLs. + auto bookmarks = image.bookmarks(); + std::sort(bookmarks.begin(), bookmarks.end(), [](auto &a, auto &b) { + if (a->detail_.created_ && b->detail_.created_) { + return a->detail_.created_.value() < b->detail_.created_.value(); + } else { + // Make a guess + return true; + } + }); + + for (auto &bookmark : bookmarks) { + auto bookmark_data = dynamic_cast(bookmark->annotation_.get()); + if (bookmark_data) { + auto json_data = bookmark->detail_.user_data_.value_or(utility::JsonStore()); + bool isActive = json_data.get_or("grade_active", true); + active_grades.push_back({bookmark_data, isActive}); + } + } + + // Currently edited grade take precedence over image's bookmark, this improves + // interactivity when drawing a mask. Simply update the vector in-place or append + // if the grade was not saved in a bookmark yet. + utility::BlindDataObjectPtr blind_data = + image.plugin_blind_data(utility::Uuid(colour_pipeline::GradingTool::PLUGIN_UUID)); + if (blind_data) { + const GradingMaskRenderData *render_data = + dynamic_cast(blind_data.get()); + + if (render_data) { + auto blind_uuid = render_data->interaction_grading_data_.bookmark_uuid_; + + if (!active_grades.empty()) { + for (auto &[grade_data, _] : active_grades) { + if (grade_data->bookmark_uuid_ == blind_uuid) { + grade_data = &(render_data->interaction_grading_data_); + break; + } + } + } else { + active_grades.push_back({&(render_data->interaction_grading_data_), true}); + } + } + } + + return active_grades; +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/grading_common.h b/src/plugin/colour_op/grading/src/grading_common.h new file mode 100644 index 000000000..b2f2f5b28 --- /dev/null +++ b/src/plugin/colour_op/grading/src/grading_common.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "grading_data.h" + +namespace xstudio { +namespace ui { +namespace viewport { + + std::vector get_active_grades( + const xstudio::media_reader::ImageBufPtr &image); + +} // end namespace viewport +} // end namespace ui +} // end namespace xstudio \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/grading_data.cpp b/src/plugin/colour_op/grading/src/grading_data.cpp index 10cb7e6f3..0b31936e4 100644 --- a/src/plugin/colour_op/grading/src/grading_data.cpp +++ b/src/plugin/colour_op/grading/src/grading_data.cpp @@ -11,39 +11,27 @@ using namespace xstudio; void xstudio::ui::viewport::from_json(const nlohmann::json &j, Grade &g) { - j.at("slope").get_to(g.slope); - j.at("offset").get_to(g.offset); - j.at("power").get_to(g.power); + j.at("slope").get_to(static_cast &>(g.slope)); + j.at("offset").get_to(static_cast &>(g.offset)); + j.at("power").get_to(static_cast &>(g.power)); j.at("sat").get_to(g.sat); -} - -void xstudio::ui::viewport::to_json(nlohmann::json &j, const Grade &g) { - - j["slope"] = g.slope; - j["offset"] = g.offset; - j["power"] = g.power; - j["sat"] = g.sat; -} - -bool LayerData::identity() const { return (grade_ == Grade() && mask_.empty()); } -void xstudio::ui::viewport::from_json(const nlohmann::json &j, LayerData &l) { - - j.at("grade").get_to(l.grade_); - j.at("mask_active").get_to(l.mask_active_); - j.at("mask_editing").get_to(l.mask_editing_); - j.at("mask").get_to(l.mask_); + if (j.contains("exposure")) + j.at("exposure").get_to(g.exposure); + if (j.contains("contrast")) + j.at("contrast").get_to(g.contrast); } -void xstudio::ui::viewport::to_json(nlohmann::json &j, const LayerData &l) { +void xstudio::ui::viewport::to_json(nlohmann::json &j, const Grade &g) { - j["grade"] = l.grade_; - j["mask_active"] = l.mask_active_; - j["mask_editing"] = l.mask_editing_; - j["mask"] = l.mask_; + j["slope"] = g.slope; + j["offset"] = g.offset; + j["power"] = g.power; + j["sat"] = g.sat; + j["exposure"] = g.exposure; + j["contrast"] = g.contrast; } - GradingData::GradingData(const utility::JsonStore &s) : bookmark::AnnotationBase() { GradingDataSerialiser::deserialise(this, s); @@ -55,29 +43,24 @@ utility::JsonStore GradingData::serialise(utility::Uuid &plugin_uuid) const { return GradingDataSerialiser::serialise((const GradingData *)this); } -bool GradingData::identity() const { - - return (layers_.empty() || (layers_.size() == 1 && layers_.front().identity())); -} - -LayerData *GradingData::layer(size_t idx) { +bool GradingData::identity() const { return (grade_ == Grade() && mask_.empty()); } - if (idx >= 0 && idx < layers_.size()) { - return &layers_[idx]; - } - return nullptr; -} - -void GradingData::push_layer() { layers_.push_back(LayerData()); } +void xstudio::ui::viewport::from_json(const nlohmann::json &j, GradingData &l) { -void GradingData::pop_layer() { layers_.pop_back(); } + if (j.contains("colour_space")) + j.at("colour_space").get_to(l.colour_space_); -void xstudio::ui::viewport::from_json(const nlohmann::json &j, GradingData &gd) { - - j.at("layers").get_to(gd.layers_); + j.at("grade").get_to(l.grade_); + j.at("mask_active").get_to(l.mask_active_); + j.at("mask_editing").get_to(l.mask_editing_); + j.at("mask").get_to(l.mask_); } -void xstudio::ui::viewport::to_json(nlohmann::json &j, const GradingData &gd) { +void xstudio::ui::viewport::to_json(nlohmann::json &j, const GradingData &l) { - j["layers"] = gd.layers_; -} + j["colour_space"] = l.colour_space_; + j["grade"] = l.grade_; + j["mask_active"] = l.mask_active_; + j["mask_editing"] = l.mask_editing_; + j["mask"] = l.mask_; +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/grading_data.h b/src/plugin/colour_op/grading/src/grading_data.h index 013dd0478..50a945a60 100644 --- a/src/plugin/colour_op/grading/src/grading_data.h +++ b/src/plugin/colour_op/grading/src/grading_data.h @@ -2,6 +2,7 @@ #pragma once #include +#include #include "xstudio/plugin_manager/plugin_base.hpp" #include "xstudio/bookmark/bookmark.hpp" @@ -12,17 +13,21 @@ namespace ui { namespace viewport { struct Grade { - std::array slope {1.0, 1.0, 1.0, 0.0}; + std::array slope {1.0, 1.0, 1.0, 1.0}; std::array offset {0.0, 0.0, 0.0, 0.0}; std::array power {1.0, 1.0, 1.0, 1.0}; double sat {1.0}; + double exposure {0.0}; + double contrast {1.0}; bool operator==(const Grade &o) const { return ( slope == o.slope && offset == o.offset && power == o.power && - sat == o.sat + sat == o.sat && + exposure == o.exposure && + contrast == o.contrast ); } }; @@ -30,12 +35,32 @@ namespace viewport { void from_json(const nlohmann::json &j, Grade &g); void to_json(nlohmann::json &j, const Grade &g); - class LayerData { + class GradingData : public bookmark::AnnotationBase { public: - LayerData() = default; + GradingData() = default; + GradingData(const utility::JsonStore &s); + GradingData(const GradingData &o) : + colour_space_(o.colour_space_), + grade_(o.grade_), + mask_active_(o.mask_active_), + mask_editing_(o.mask_editing_), + mask_(o.mask_) { + bookmark_uuid_ = o.bookmark_uuid_; + } + + GradingData & operator=(const GradingData &o) { + colour_space_ = o.colour_space_; + grade_ = o.grade_; + mask_active_ = o.mask_active_; + mask_editing_ = o.mask_editing_; + mask_ = o.mask_; + bookmark_uuid_ = o.bookmark_uuid_; + return *this; + } - bool operator==(const LayerData &o) const { + bool operator==(const GradingData &o) const { return ( + colour_space_ == o.colour_space_ && grade_ == o.grade_ && mask_active_ == o.mask_active_ && mask_editing_ == o.mask_editing_ && @@ -43,8 +68,13 @@ namespace viewport { ); } + [[nodiscard]] utility::JsonStore serialise(utility::Uuid &plugin_uuid) const override; + bool identity() const; + void set_colour_space(const std::string &val) { colour_space_ = val; } + std::string colour_space() const { return colour_space_; } + Grade & grade() { return grade_; } const Grade & grade() const { return grade_; } @@ -58,59 +88,28 @@ namespace viewport { const canvas::Canvas & mask() const { return mask_; } private: - friend void from_json(const nlohmann::json &j, LayerData &l); - friend void to_json(nlohmann::json &j, const LayerData &l); + friend void from_json(const nlohmann::json &j, GradingData &gd); + friend void to_json(nlohmann::json &j, const GradingData &gd); + std::string colour_space_ {"scene_linear"}; Grade grade_; bool mask_active_ {false}; bool mask_editing_ {false}; canvas::Canvas mask_; }; - void from_json(const nlohmann::json &j, LayerData &l); - void to_json(nlohmann::json &j, const LayerData &l); - - class GradingData : public bookmark::AnnotationBase { - public: - GradingData() = default; - explicit GradingData(const utility::JsonStore &s); - - GradingData & operator=(const GradingData &o) = default; - - bool operator==(const GradingData &o) const { - return (layers_ == o.layers_); - } - - [[nodiscard]] utility::JsonStore serialise(utility::Uuid &plugin_uuid) const override; - - bool identity() const; - - size_t size() const { return layers_.size(); } - - std::vector::const_iterator begin() const { - return layers_.begin(); } - std::vector::const_iterator end() const { - return layers_.end(); } - - std::vector& layers() { return layers_; } - const std::vector& layers() const { return layers_; } - - LayerData* layer(size_t idx); - void push_layer(); - void pop_layer(); - - private: - friend void from_json(const nlohmann::json &j, GradingData &gd); - friend void to_json(nlohmann::json &j, const GradingData &gd); - - std::vector layers_; - }; - void from_json(const nlohmann::json &j, GradingData &gd); void to_json(nlohmann::json &j, const GradingData &gd); typedef std::shared_ptr GradingDataPtr; + // This struct is a convenience wrapper to bundle GradingData + // with other metadatas living in the bookmark user_data field. + struct GradingInfo { + const GradingData* data; + bool isActive; + }; + } // end namespace viewport } // end namespace ui } // end namespace xstudio \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/grading_data_serialiser.cpp b/src/plugin/colour_op/grading/src/grading_data_serialiser.cpp index 139634709..4bfc9f414 100644 --- a/src/plugin/colour_op/grading/src/grading_data_serialiser.cpp +++ b/src/plugin/colour_op/grading/src/grading_data_serialiser.cpp @@ -6,7 +6,9 @@ using namespace xstudio; std::map> GradingDataSerialiser::serialisers; +namespace { static const std::string GRADING_VERSION_KEY("Grading Serialiser Version"); +} utility::JsonStore GradingDataSerialiser::serialise(const GradingData *grading_data) { diff --git a/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.cpp b/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.cpp index 3ea9cbdf7..24c967464 100644 --- a/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.cpp +++ b/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.cpp @@ -9,6 +9,7 @@ #include "grading_mask_render_data.h" #include "grading_mask_gl_renderer.h" #include "grading_colour_op.hpp" +#include "grading_common.h" using namespace xstudio; using namespace xstudio::ui::canvas; @@ -16,7 +17,8 @@ using namespace xstudio::ui::opengl; using namespace xstudio::ui::viewport; -GradingMaskRenderer::GradingMaskRenderer() { +GradingMaskRenderer::GradingMaskRenderer(const std::string &viewport_name) + : viewport_name_(viewport_name) { canvas_renderer_.reset(new ui::opengl::OpenGLCanvasRenderer()); } @@ -27,7 +29,6 @@ void GradingMaskRenderer::pre_viewport_draw_gpu_hook( const float viewport_du_dpixel, xstudio::media_reader::ImageBufPtr &image) { - // the data on any grading mask layers and brushstrokes can come from two // sources. // 1) The data can be attached to the bookmarks that are accessed from @@ -37,7 +38,7 @@ void GradingMaskRenderer::pre_viewport_draw_gpu_hook( // 2) The data can be attached to the image as part of the 'plugin_blind_data'. // We request plugin data using our plugin UUID and then dynamic cast to // get to our 'GradingMaskRenderData' object. (Note that this was set in - // GradingTool::prepare_overlay_data). + // GradingTool::onscreen_render_data). // // If we have data via the blind data, this is immediate updated grading data // when the user is interacting with the render by painting a mask. The data @@ -46,33 +47,24 @@ void GradingMaskRenderer::pre_viewport_draw_gpu_hook( // on every mouse event when the user is painting the mask. So we use the // blind data version in favour of the bookmark version when we have both. - utility::BlindDataObjectPtr blind_data = - image.plugin_blind_data(utility::Uuid(colour_pipeline::GradingTool::PLUGIN_UUID)); - - if (blind_data) { - const GradingMaskRenderData *render_data = - dynamic_cast(blind_data.get()); - if (render_data) { - renderGradingDataMasks(&(render_data->interaction_grading_data_), image); - // we exit here as we don't support multiple grading ops on a given - // media source - return; - } + auto active_grades = get_active_grades(image); + if (active_grades.size()) { + render_grading_data_masks(active_grades, image); } +} - for (auto &bookmark : image.bookmarks()) { +void GradingMaskRenderer::add_layer() { - const GradingData *data = - dynamic_cast(bookmark->annotation_.get()); - if (data) { - renderGradingDataMasks(data, image); - break; // we're only supporting a single grading op on a given source - } - } + // using 8 bit texture - should be more efficient than float32 + RenderLayer rl; + rl.offscreen_renderer = std::make_unique(GL_RGBA8); + render_layers_.push_back(std::move(rl)); } -void GradingMaskRenderer::renderGradingDataMasks( - const GradingData *data, xstudio::media_reader::ImageBufPtr &image) { +size_t GradingMaskRenderer::layer_count() const { return render_layers_.size(); } + +void GradingMaskRenderer::render_grading_data_masks( + const std::vector &grades, xstudio::media_reader::ImageBufPtr &image) { // First grab the ColourOperationData for this plugin which has already // been added to the image. This data is the static data for the colour @@ -80,44 +72,58 @@ void GradingMaskRenderer::renderGradingDataMasks( // op and also any colour LUT textures needed for the operation. It does // not (yet) include dynamic texture data such as the mask that the user // can paint the grade through. - colour_pipeline::ColourOperationDataPtr colour_op_data = - image.colour_pipe_data_ ? image.colour_pipe_data_->get_operation_data( - colour_pipeline::GradingColourOperator::PLUGIN_UUID) - : colour_pipeline::ColourOperationDataPtr(); - - if (!colour_op_data) + colour_pipeline::ColourOperationDataPtr colour_op_data; + if (image.colour_pipe_data_) { + colour_op_data = image.colour_pipe_data_->get_operation_data( + colour_pipeline::GradingColourOperator::PLUGIN_UUID); + } else { return; + } // Because we are going to modify the member data of colour_op_data we need // to make ourselves a 'deep' copy since this is shared data and it could // be simultaneously accessed in other places in the application. colour_op_data = std::make_shared(*colour_op_data); - while (data->size() > layer_count()) { - add_layer(); - } - - // Paint the canvas for each grading data / layer - size_t layer_index = 0; + // Paint the canvas associated with each grade (if any) std::string cache_id_modifier; - for (auto &layer_data : *data) { + + size_t grade_index = 0; + size_t masked_grade_index = 0; + for (const auto grade_info : grades) { + + auto grade = grade_info.data; + + // Ignore grade without mask + if (grade->mask().empty()) { + grade_index++; + continue; + } + + if (masked_grade_index >= layer_count()) { + // TODO: ColSci + // We add layer here but never reclaim / free them later. + add_layer(); + } // here the mask is rendered to a GL texture - render_layer(layer_data, render_layers_[layer_index], image, true); + render_layer(*grade, render_layers_[masked_grade_index], image, true); // here we add info on the texture to the colour_op_data since // the colour op needs to use the texture colour_op_data->textures_.emplace_back(colour_pipeline::ColourTexture{ - fmt::format("layer{}_mask", layer_index), + fmt::format("masks[{}]", grade_index), colour_pipeline::ColourTextureTarget::TEXTURE_2D, - render_layers_[layer_index].offscreen_renderer->texture_handle()}); + render_layers_[masked_grade_index].offscreen_renderer->texture_handle()}); // adding info on the mask texture layers to the cache id will // force the viewport to assign new active texture indices to // the layer mask texture, if the number of layers has changed - cache_id_modifier += std::to_string(layer_index); + cache_id_modifier += std::to_string(grade->mask().hash()); + cache_id_modifier += std::to_string(grade_index); - layer_index++; + masked_grade_index++; + grade_index++; } // Again, colour_pipe_data_ is a shared ptr and we don't know who else might @@ -130,68 +136,51 @@ void GradingMaskRenderer::renderGradingDataMasks( // here the relevant shared ptr to the colour op data is reset image.colour_pipe_data_->overwrite_operation_data(colour_op_data); - image.colour_pipe_data_->cache_id_ += cache_id_modifier; -} - -void GradingMaskRenderer::add_layer() { - - // using 8 bit texture - should be more efficient than float32 - RenderLayer rl; - rl.offscreen_renderer = std::make_unique(GL_RGBA8); - render_layers_.push_back(std::move(rl)); + image.colour_pipe_data_->set_cache_id( + image.colour_pipe_data_->cache_id() + cache_id_modifier); } -size_t GradingMaskRenderer::layer_count() const { return render_layers_.size(); } - void GradingMaskRenderer::render_layer( - const LayerData &data, + const GradingData &data, RenderLayer &layer, const xstudio::media_reader::ImageBufPtr &frame, const bool have_alpha_buffer) { + // Use fixed resolution for better performance + const Imath::V2i mask_resolution(960, 540); + const float image_aspect_ratio = image_aspect(frame); + + const float canvas_aspect_ratio = 1.0f * mask_resolution.x / mask_resolution.y; + if (data.mask().uuid() != layer.last_canvas_uuid || - data.mask().last_change_time() != layer.last_canvas_change_time) { - - // instead of varying the canvas size to match the image size, we can - // use a fixed canvas size. The grade mask doesn't need to be - // high res as the strokes have some softness. Low res will perform - // better and have less footprint. - layer.offscreen_renderer->resize( - Imath::V2i(960, 540)); // frame->image_size_in_pixels()); + data.mask().last_change_time() != layer.last_canvas_change_time || + image_aspect_ratio != layer.last_image_aspect_ratio) { + + layer.offscreen_renderer->resize(mask_resolution); layer.offscreen_renderer->begin(); - if (data.mask().size()) { + if (!data.mask().empty()) { + glClearColor(0.0, 0.0, 0.0, 0.0); glClearDepth(0.0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + // Mask drawing don't depend on viewport transformation, however we still + // need to account for the mask texture aspect ratio. Imath::M44f to_canvas; - - // see comment above - /*const float image_aspect_ratio = - frame->pixel_aspect() * - (1.0f * 960 / 540); - const float image_aspect_ratio = - frame->pixel_aspect() * - (1.0f * frame->image_size_in_pixels().x / frame->image_size_in_pixels().y); - to_canvas.setScale(Imath::V3f(1.0f, image_aspect_ratio, 1.0f));*/ - - to_canvas.setScale(Imath::V3f(1.0f, 16.0f / 9.0f, 1.0f)); + to_canvas.setScale(Imath::V3f(1.0f, canvas_aspect_ratio, 1.0f)); canvas_renderer_->render_canvas( data.mask(), HandleState(), - // We are drawing to an offscreen texture and don't need - // any view / projection matrix to account for the viewport - // transformation. However, we still need to account for the - // image aspect ratio. to_canvas, Imath::M44f(), - 2.0 / 960, // 2.0f / frame->image_size_in_pixels().x (see note A) - have_alpha_buffer); + 2.0f / mask_resolution.x, // See A1 + have_alpha_buffer, + image_aspect_ratio); + } else { - // blank (empty) maske with no stokes. In this case we flood - // texture with 1.0s + glClearColor(1.0, 1.0, 1.0, 1.0); glClearDepth(0.0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); @@ -204,7 +193,9 @@ void GradingMaskRenderer::render_layer( // fitted to the span of -1.0 to 1.0 in xSTUDIO's coordinate system. layer.offscreen_renderer->end(); + layer.last_canvas_change_time = data.mask().last_change_time(); layer.last_canvas_uuid = data.mask().uuid(); + layer.last_image_aspect_ratio = image_aspect_ratio; } } \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.h b/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.h index 55e5c61c6..4437ed4d3 100644 --- a/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.h +++ b/src/plugin/colour_op/grading/src/grading_mask_gl_renderer.h @@ -26,11 +26,12 @@ namespace viewport { opengl::OpenGLOffscreenRendererPtr offscreen_renderer; utility::clock::time_point last_canvas_change_time; utility::Uuid last_canvas_uuid; + float last_image_aspect_ratio; }; public: - GradingMaskRenderer(); + GradingMaskRenderer(const std::string &viewport_name); void pre_viewport_draw_gpu_hook( const Imath::M44f &transform_window_to_viewport_space, @@ -43,12 +44,12 @@ namespace viewport { size_t layer_count() const; void add_layer(); - void renderGradingDataMasks( - const GradingData *, + void render_grading_data_masks( + const std::vector &grades, xstudio::media_reader::ImageBufPtr &image); void render_layer( - const LayerData& data, + const GradingData& data, RenderLayer& layer, const xstudio::media_reader::ImageBufPtr &frame, const bool have_alpha_buffer); @@ -58,6 +59,7 @@ namespace viewport { std::vector render_layers_; std::unique_ptr canvas_renderer_; + const std::string viewport_name_; }; using GradingMaskRendererSPtr = std::shared_ptr; diff --git a/src/plugin/colour_op/grading/src/grading_mask_render_data.h b/src/plugin/colour_op/grading/src/grading_mask_render_data.h index 2a5df3986..c12844afd 100644 --- a/src/plugin/colour_op/grading/src/grading_mask_render_data.h +++ b/src/plugin/colour_op/grading/src/grading_mask_render_data.h @@ -10,37 +10,8 @@ namespace viewport { class GradingMaskRenderData : public utility::BlindDataObject { public: - - // As far as I understand it, we only need a single GradingData to - // worry about here. This contains the full data set for the grade - // that the user is interacting with. + // This is the grade the user is currently interacting with. GradingData interaction_grading_data_; - - // Leaving this old code here in case it needs re-instating. - - /*void add_grading_data(const GradingData &data) { - data_vec_.push_back(data); - } - - size_t layer_count() const { - size_t ret = 0; - for (auto& layer : data_vec_) { - ret += layer.size(); - } - return ret; - } - - size_t size() const { return data_vec_.size(); } - - std::vector::const_iterator begin() const { - return data_vec_.cbegin(); - } - std::vector::const_iterator end() const { - return data_vec_.cend(); - }*/ - - private: - //std::vector data_vec_; }; } // end namespace viewport diff --git a/src/plugin/colour_op/grading/src/qml/CMakeLists.txt b/src/plugin/colour_op/grading/src/qml/CMakeLists.txt deleted file mode 100644 index 5c50e0831..000000000 --- a/src/plugin/colour_op/grading/src/qml/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -project(grading VERSION 0.1.0 LANGUAGES CXX) - -if(WIN32) - install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/Grading.1/ DESTINATION ${CMAKE_INSTALL_PREFIX}/plugin/qml/Grading.1) - install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/MaskTool.1/ DESTINATION ${CMAKE_INSTALL_PREFIX}/plugin/qml/MaskTool.1) -else() - install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/Grading.1/ DESTINATION share/xstudio/plugin/qml/Grading.1) - install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/MaskTool.1/ DESTINATION share/xstudio/plugin/qml/MaskTool.1) -endif() - - - -add_custom_target(COPY_GRADE_QML ALL) - -add_custom_command(TARGET COPY_GRADE_QML POST_BUILD - COMMAND ${CMAKE_COMMAND} -E - copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/Grading.1 ${CMAKE_BINARY_DIR}/bin/plugin/qml/Grading.1) - -add_custom_command(TARGET COPY_GRADE_QML POST_BUILD - COMMAND ${CMAKE_COMMAND} -E - copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/MaskTool.1 ${CMAKE_BINARY_DIR}/bin/plugin/qml/MaskTool.1) diff --git a/src/plugin/colour_op/grading/src/qml/Grading.1/GradingButton.qml b/src/plugin/colour_op/grading/src/qml/Grading.1/GradingButton.qml deleted file mode 100644 index 0d3dc27f5..000000000 --- a/src/plugin/colour_op/grading/src/qml/Grading.1/GradingButton.qml +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.12 - -import xStudio 1.0 - -import xstudio.qml.module 1.0 - -XsTrayButton { - // prototype: true - - anchors.fill: parent - text: "Draw" - source: "qrc:/icons/colour_correction.png" - tooltip: "Open the Colour Correction Panel. Apply SOP and LGG colour offsets to selected Media." - buttonPadding: pad - toggled_on: gradingToolActive - onClicked: { - // toggle the value in the "grading_tool_active" backend attribute - if (grading_settings.grading_tool_active != null) { - grading_settings.grading_tool_active = !grading_settings.grading_tool_active - } - } - - property var gradingDialog - - property bool dialogVisible: gradingDialog ? gradingDialog.visible : false - - onDialogVisibleChanged: { - if (grading_settings.grading_tool_active && dialogVisible == false) { - grading_settings.grading_tool_active = false - } - } - - // connect to the backend module to give access to attributes - XsModuleAttributes { - id: grading_settings - attributesGroupNames: "grading_settings" - } - - // make a read only binding to the "grading_tool_active" backend attribute - property bool gradingToolActive: grading_settings.grading_tool_active ? grading_settings.grading_tool_active : false - - onGradingToolActiveChanged: - { - // there are two GradingButtons - one for main win, one for pop-out, - // but we only want one instance of the GradingDialog .. this test - // should ensure that is the case - if (sessionWidget.is_main_window) { - if (gradingDialog === undefined) { - try { - gradingDialog = Qt.createQmlObject("import Grading 1.0; GradingDialog {}", app_window, "dynamic") - } catch (err) { - console.error(err); - } - } - if (gradingToolActive) { - gradingDialog.show() - gradingDialog.requestActivate() - } else { - gradingDialog.hide() - } - } - } - -} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingDialog.qml b/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingDialog.qml deleted file mode 100644 index 9b25729a0..000000000 --- a/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingDialog.qml +++ /dev/null @@ -1,502 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient - -import xStudio 1.1 -import xstudio.qml.module 1.0 -import xstudio.qml.clipboard 1.0 - -import MaskTool 1.0 - -XsWindow { - - id: drawDialog - title: "Colour Correction Tools" - // title: attr.grading_layer ? "Colour Correction Tools - " + attr.grading_layer : "Colour Correction Tools" - - width: minimumWidth - minimumWidth: attr.mvp_1_release != undefined ? 650 : 850 - maximumWidth: minimumWidth - - height: minimumHeight - minimumHeight: attr.mvp_1_release != undefined ? 320 : 340 - maximumHeight: minimumHeight - - onVisibleChanged: { - if (!visible) { - // ensure keyboard events are returned to the viewport - sessionWidget.playerWidget.viewport.forceActiveFocus() - } - } - - XsModuleAttributes { - id: attr - attributesGroupNames: "grading_settings" - } - - XsModuleAttributes { - id: attr_layers - attributesGroupNames: "grading_layers" - roleName: "combo_box_options" - } - - FileDialog { - id: cdl_save_dialog - title: "Save CDL" - defaultSuffix: "cdl" - folder: shortcuts.home - nameFilters: [ "CDL files (*.cdl)", "CC files (*.cc)", "CCC files (*.ccc)" ] - selectExisting: false - - onAccepted: { - // defaultSuffix doesn't seem to work in the current Qt version used - var path = fileUrl.toString() - if (!path.endsWith(".cdl") && !path.endsWith(".cc") && !path.endsWith(".ccc")) { - path += ".cdl" - } - - attr.grading_action = "Save CDL " + path - } - } - - RowLayout { - anchors.fill: parent - anchors.margins: 3 - - MaskDialog { - id: maskDialog - - enabled: attr.mask_tool_active ? attr.mask_tool_active : false - visible: attr.mvp_1_release != undefined ? !attr.mvp_1_release : false - - Layout.minimumWidth: 190 - Layout.maximumWidth: 190 - Layout.fillHeight: true - } - - ColumnLayout { - Layout.topMargin: 1 - spacing: 3 - - Rectangle { - Layout.minimumHeight: 30 - Layout.maximumHeight: 30 - Layout.fillWidth: true - - color: "transparent" - opacity: 1.0 - border.width: 1 - border.color: Qt.rgba( - XsStyle.menuBorderColor.r, - XsStyle.menuBorderColor.g, - XsStyle.menuBorderColor.b, - 0.3) - radius: 2 - - RowLayout { - anchors.fill: parent - Layout.topMargin: 0 - spacing: 3 - - XsButton { - text: "Mask" - textDiv.font.bold: true - tooltip: "Enable masking, default mask starts empty" - isActive: attr.mask_tool_active ? attr.mask_tool_active : false - visible: attr.mvp_1_release != undefined ? !attr.mvp_1_release : false - Layout.maximumHeight: 30 - - onClicked: { - attr.mask_tool_active = !attr.mask_tool_active - } - } - - XsButton { - text: "Basic" - textDiv.font.bold: true - tooltip: "Basic grading controls (restricted to work within a single CDL)" - isActive: attr.grading_panel ? attr.grading_panel == "Basic" : false - Layout.maximumWidth: 70 - Layout.maximumHeight: 30 - - onClicked: { - attr.grading_panel = "Basic" - } - } - - XsButton { - text: "Sliders" - textDiv.font.bold: true - tooltip: "CDL sliders controls" - isActive: attr.grading_panel ? attr.grading_panel == "Sliders" : false - Layout.maximumWidth: 70 - Layout.maximumHeight: 30 - - onClicked: { - attr.grading_panel = "Sliders" - } - } - - XsButton { - text: "Wheels" - textDiv.font.bold: true - tooltip: "CDL colour wheels controls" - isActive: attr.grading_panel ? attr.grading_panel == "Wheels" : false - Layout.maximumWidth: 70 - Layout.maximumHeight: 30 - - onClicked: { - attr.grading_panel = "Wheels" - } - } - - Item { - // Spacer item - Layout.fillWidth: true - Layout.fillHeight: true - } - } - } - - Rectangle { - Layout.leftMargin: 0 - Layout.bottomMargin: 0 - Layout.fillWidth: true - Layout.fillHeight: true - - color: "transparent" - opacity: 1.0 - border.width: 1 - border.color: Qt.rgba( - XsStyle.menuBorderColor.r, - XsStyle.menuBorderColor.g, - XsStyle.menuBorderColor.b, - 0.3) - radius: 2 - - visible: attr.grading_panel ? attr.grading_panel == "Basic" : false - - Column { - anchors.fill: parent - - GradingSliderSimple { - attr_group: "grading_simple" - } - - } - } - - Rectangle { - Layout.leftMargin: 0 - Layout.bottomMargin: 0 - Layout.fillWidth: true - Layout.fillHeight: true - - color: "transparent" - opacity: 1.0 - border.width: 1 - border.color: Qt.rgba( - XsStyle.menuBorderColor.r, - XsStyle.menuBorderColor.g, - XsStyle.menuBorderColor.b, - 0.3) - radius: 2 - - visible: attr.grading_panel ? attr.grading_panel == "Sliders" : false - - Row { - anchors.topMargin: 10 - anchors.leftMargin: 10 - anchors.fill: parent - spacing: 15 - - GradingSliderGroup { - title: "Slope" - fixed_size: 160 - attr_group: "grading_slope" - attr_suffix: "slope" - } - - GradingSliderGroup { - title: "Offset" - fixed_size: 160 - attr_group: "grading_offset" - attr_suffix: "offset" - } - - GradingSliderGroup { - title: "Power" - fixed_size: 160 - attr_group: "grading_power" - attr_suffix: "power" - } - - GradingSliderGroup { - title: "Sat" - fixed_size: 60 - attr_group: "grading_saturation" - } - } - } - - Rectangle { - Layout.leftMargin: 0 - Layout.bottomMargin: 0 - Layout.fillWidth: true - Layout.fillHeight: true - - color: "transparent" - opacity: 1.0 - border.width: 1 - border.color: Qt.rgba( - XsStyle.menuBorderColor.r, - XsStyle.menuBorderColor.g, - XsStyle.menuBorderColor.b, - 0.3) - radius: 2 - - visible: attr.grading_panel ? attr.grading_panel == "Wheels" : false - - Row { - anchors.topMargin: 10 - anchors.leftMargin: 10 - anchors.fill: parent - spacing: 15 - - GradingWheel { - title : "Slope" - attr_group: "grading_slope" - attr_suffix: "slope" - } - - GradingWheel { - title: "Offset" - attr_group: "grading_offset" - attr_suffix: "offset" - } - - GradingWheel { - title: "Power" - attr_group: "grading_power" - attr_suffix: "power" - } - - GradingSliderGroup { - title: "Sat" - fixed_size: 60 - attr_group: "grading_saturation" - } - } - } - - Rectangle { - color: "transparent" - opacity: 1.0 - border.width: 1 - border.color: Qt.rgba( - XsStyle.menuBorderColor.r, - XsStyle.menuBorderColor.g, - XsStyle.menuBorderColor.b, - 0.3) - radius: 2 - - Layout.topMargin: 1 - Layout.minimumHeight: 25 - Layout.maximumHeight: 25 - Layout.fillWidth: true - - RowLayout { - anchors.fill: parent - layoutDirection: Qt.RightToLeft - - XsButton { - Layout.maximumWidth: 50 - Layout.maximumHeight: 25 - text: "Bypass" - tooltip: "Apply CDL or not" - isActive: attr.drawing_bypass ? attr.drawing_bypass : false - - onClicked: { - attr.drawing_bypass = !attr.drawing_bypass - } - } - - XsButton { - Layout.maximumWidth: 58 - Layout.maximumHeight: 25 - text: "Reset All" - tooltip: "Reset CDL parameters to default" - - onClicked: { - attr.grading_action = "Clear" - } - } - - XsButton { - Layout.maximumWidth: 40 - Layout.maximumHeight: 25 - text: "Copy" - tooltip: "Copy current colour correction" - - onClicked: { - var grade_str = "" - grade_str += attr.red_slope + " " - grade_str += attr.green_slope + " " - grade_str += attr.blue_slope + " " - grade_str += attr.master_slope + " " - grade_str += attr.red_offset + " " - grade_str += attr.green_offset + " " - grade_str += attr.blue_offset + " " - grade_str += attr.master_offset + " " - grade_str += attr.red_power + " " - grade_str += attr.green_power + " " - grade_str += attr.blue_power + " " - grade_str += attr.master_power + " " - grade_str += attr.saturation - attr.grading_buffer = grade_str - } - } - - XsButton { - Layout.maximumWidth: 40 - Layout.maximumHeight: 25 - text: "Paste" - tooltip: "Paste colour correction" - enabled: attr.grading_buffer ? attr.grading_buffer != "" : false - - onClicked: { - if (attr.grading_buffer) { - var cdl_items = attr.grading_buffer.split(" ") - if (cdl_items.length == 13) { - attr.red_slope = parseFloat(cdl_items[0]) - attr.green_slope = parseFloat(cdl_items[1]) - attr.blue_slope = parseFloat(cdl_items[2]) - attr.master_slope = parseFloat(cdl_items[3]) - attr.red_offset = parseFloat(cdl_items[4]) - attr.green_offset = parseFloat(cdl_items[5]) - attr.blue_offset = parseFloat(cdl_items[6]) - attr.master_offset = parseFloat(cdl_items[7]) - attr.red_power = parseFloat(cdl_items[8]) - attr.green_power = parseFloat(cdl_items[9]) - attr.blue_power = parseFloat(cdl_items[10]) - attr.master_power = parseFloat(cdl_items[11]) - attr.saturation = parseFloat(cdl_items[12]) - } - } - } - } - - XsButton { - Layout.maximumWidth: 40 - Layout.maximumHeight: 25 - text: ">" - tooltip: "Toggle to next layer" - enabled: attr.grading_layer && attr_layers.grading_layer ? parseInt(attr.grading_layer.slice(-1)) < attr_layers.grading_layer.length : false - visible: attr.mvp_1_release != undefined ? !attr.mvp_1_release : false - - onClicked: { - attr.grading_action = "Next Layer" - } - } - - XsButton { - Layout.maximumWidth: 40 - Layout.maximumHeight: 25 - text: "<" - tooltip: "Toggle to prev layer" - enabled: attr.grading_layer ? parseInt(attr.grading_layer.slice(-1)) >= 2 : false - visible: attr.mvp_1_release != undefined ? !attr.mvp_1_release : false - - onClicked: { - attr.grading_action = "Prev Layer" - } - } - - XsButton { - Layout.maximumWidth: 40 - Layout.maximumHeight: 25 - text: "+" - tooltip: "Add a grade layer on top" - enabled: attr_layers.grading_layer ? attr_layers.grading_layer.length < 8 : false - visible: attr.mvp_1_release != undefined ? !attr.mvp_1_release : false - - onClicked: { - attr.grading_action = "Add Layer" - } - } - - XsButton { - Layout.maximumWidth: 40 - Layout.maximumHeight: 25 - text: "-" - tooltip: "Remove the top grade layer" - enabled: attr_layers.grading_layer ? attr_layers.grading_layer.length > 1 : false - visible: attr.mvp_1_release != undefined ? !attr.mvp_1_release : false - - onClicked: { - attr.grading_action = "Remove Layer" - } - } - - Item { - // Spacer item - Layout.fillWidth: true - Layout.fillHeight: true - } - - XsButton { - Layout.maximumWidth: 80 - Layout.maximumHeight: 25 - text: "Save CDL ..." - tooltip: "Save CDL to disk as a .cdl, .cc or .ccc" - - onClicked: { - cdl_save_dialog.open() - } - } - - XsButton { - Layout.minimumWidth: 110 - Layout.maximumWidth: 120 - Layout.maximumHeight: 25 - text: "Copy Nuke Node" - tooltip: "Copy CDL as a Nuke OCIOCDLTransform node to the clipboard - paste into Nuke node graph with CTRL+V" - - onClicked: { - var cdl_node = "OCIOCDLTransform {\n" - cdl_node += " slope { " - cdl_node += (attr.red_slope * attr.master_slope) + " " - cdl_node += (attr.green_slope * attr.master_slope) + " " - cdl_node += (attr.blue_slope * attr.master_slope) + " " - cdl_node += "}\n" - cdl_node += " offset { " - cdl_node += (attr.red_offset + attr.master_offset) + " " - cdl_node += (attr.green_offset + attr.master_offset) + " " - cdl_node += (attr.blue_offset + attr.master_offset) + " " - cdl_node += "}\n" - cdl_node += " power { " - cdl_node += (attr.red_power * attr.master_power) + " " - cdl_node += (attr.green_power * attr.master_power) + " " - cdl_node += (attr.blue_power * attr.master_power) + " " - cdl_node += "}\n" - cdl_node += " saturation " + attr.saturation + "\n" - cdl_node += "}" - - clipboard.text = cdl_node - } - } - - } - } - } - - } -} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingHSlider.qml b/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingHSlider.qml deleted file mode 100644 index bbcf892f2..000000000 --- a/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingHSlider.qml +++ /dev/null @@ -1,217 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient - -import xStudio 1.1 -import xstudio.qml.module 1.0 - -Item { - id: root - - width: sliderRow.width - height: sliderRow.height - - property string title: model.abbr_title - property real value: model.value - property real default_value: model.default_value - property real from: model.float_scrub_min - property real to: model.float_scrub_max - property real step: model.float_scrub_step - property var colour: model.attr_colour - // TODO: Ideally the C++ side should specify whether slider should - // use linear or log scaling. There don't seem to be support for - // custom roles we could use to communicate this at the moment. - property bool linear_scale: model.abbr_title == "Exposure" - - // Manually update the value, needed in case the default value match - // the type default construction value. For example Offset slider has - // a default value of 0 that should map to 0.5 in the Slider. - Component.onCompleted: { - update_value() - } - - onValueChanged: { - update_value() - } - - function update_value() { - if (!slider.pressed) { - slider.value = val_to_pos(value) - } - } - - // Note this is a naive log scale, in case the min and max are not - // mirrored around mid, the derivate will not be continous at the - // mid point. - - function pos_to_val(v) { - var min = root.from - var mid = root.default_value - var max = root.to - var steepness = 4 - - function lin_to_log(v) { - var log = Math.log - var antilog = Math.exp - return (antilog(v * steepness) - antilog(0.0)) / (antilog(1.0 * steepness) - antilog(0.0)) - } - - if (root.linear_scale) { - if (v < 0.5) { - return (1 - (1 - v * 2)) * (mid - min) + min - } else { - return ((v - 0.5) * 2) * (max - mid) + mid - } - } else { - if (v < 0.5) { - return (1 - lin_to_log(1 - v * 2)) * (mid - min) + min - } else { - return lin_to_log((v - 0.5) * 2) * (max - mid) + mid - } - } - } - - function val_to_pos(v) { - var min = root.from - var mid = root.default_value - var max = root.to - var steepness = 4 - - function log_to_lin(v) { - var log = Math.log - var antilog = Math.exp - return log(v * (antilog(1.0 * steepness) - antilog(0.0)) + antilog(0.0)) / steepness - } - - if (linear_scale) { - if (v < pos_to_val(0.5, min, mid, max, steepness)) { - return (1 - (1 - ((v - min) / (mid - min)))) / 2.0 - } else { - return ((v - mid) / (max - mid)) / 2.0 + 0.5 - } - } else { - if (v < pos_to_val(0.5, min, mid, max, steepness)) { - return (1 - log_to_lin(1 - ((v - min) / (mid - min)))) / 2.0 - } else { - return log_to_lin((v - mid) / (max - mid)) / 2.0 + 0.5 - } - } - } - - Row { - id: sliderRow - spacing: 15 - - Text { - text: root.title - font.pixelSize: 15 - color: "white" - width: 80 - } - - XsButton { - id: reloadButton - width: 15; height: 15 - bgColorNormal: "transparent" - borderWidth: 0 - - onClicked: { - model.value = model.default_value - } - - Image { - source: "qrc:/feather_icons/rotate-ccw.svg" - sourceSize.width: 15 - sourceSize.height: 15 - - layer { - enabled: true - effect: ColorOverlay { - color: reloadButton.down || reloadButton.hovered ? "white" : XsStyle.controlTitleColor - } - } - } - } - - Slider { - id: slider - width: 400 - from: 0.0 - to: 1.0 - // Adjust to make sure the desired step size is achieved after - // linear interpolation in the pos_to_val and val_to_pos functions - stepSize: root.linear_scale ? (root.step - root.default_value) / (2 * (root.to - root.default_value)) : 0.01 - orientation: Qt.Horizontal - - onValueChanged: { - if (pressed) { - model.value = pos_to_val(value) - } - } - - background: Rectangle { - x: slider.leftPadding - y: slider.topPadding + slider.availableHeight / 2 - height / 2 - width: slider.availableWidth - height: 4 - radius: 2 - color: "grey" - - Rectangle { - width: slider.visualPosition * parent.width - height: parent.height - color: "white" - radius: 2 - } - } - - handle: Rectangle { - id: sliderHandle - - x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) - y: (slider.height - height) / 2 - width: 15 - height: 15 - - radius: 15 - color: root.colour - border.color: "white" - } - } - - XsTextField { - id: sliderInput - width: 60 - bgColorNormal: "transparent" - borderColor: bgColorNormal - text: root.value.toFixed(5) - - validator: DoubleValidator { - bottom: model.float_scrub_min - top: model.float_scrub_max - } - - onFocusChanged: { - if(focus) { - selectAll() - forceActiveFocus() - } - else { - deselect() - } - } - - onEditingFinished: { - model.value = parseFloat(text) - } - } - } -} diff --git a/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingSliderGroup.qml b/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingSliderGroup.qml deleted file mode 100644 index b2baaf272..000000000 --- a/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingSliderGroup.qml +++ /dev/null @@ -1,217 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient - -import xStudio 1.1 -import xstudio.qml.module 1.0 - -Item { - id: root - - width: Math.max(titleRow.width, sliderList.width) - height: titleRow.height + sliderList.height - - property string title - property string attr_group - property string attr_suffix - property real fixed_size: -1 - - XsModuleAttributesModel { - id: attr_model - attributesGroupNames: root.attr_group - } - XsModuleAttributes { - id: attr - attributesGroupNames: root.attr_group - - onAttrAdded: { - // Master is added last, we know all the other attributes are here - if (attr_name.includes("master")) { - inputRed.text = Qt.binding(function() { return attr["red_" + root.attr_suffix].toFixed(5) }) - inputGreen.text = Qt.binding(function() { return attr["green_" + root.attr_suffix].toFixed(5) }) - inputBlue.text = Qt.binding(function() { return attr["blue_" + root.attr_suffix].toFixed(5) }) - inputMaster.text = Qt.binding(function() { return attr["master_" + root.attr_suffix].toFixed(5) }) - } else if (attr_name === "saturation") { - inputRed.text = Qt.binding(function() { return attr[attr_name].toFixed(5) }) - } - } - } - XsModuleAttributes { - id: attr_default_value - attributesGroupNames: root.attr_group - roleName: "default_value" - } - XsModuleAttributes { - id: attr_float_scrub_min - attributesGroupNames: root.attr_group - roleName: "float_scrub_min" - - onAttrAdded: { - if (attr_name.includes("master")) { - inputRed.validator.bottom = Qt.binding(function() { return attr_float_scrub_min["red_" + root.attr_suffix] }) - inputGreen.validator.bottom = Qt.binding(function() { return attr_float_scrub_min["green_" + root.attr_suffix] }) - inputBlue.validator.bottom = Qt.binding(function() { return attr_float_scrub_min["blue_" + root.attr_suffix] }) - inputMaster.validator.bottom = Qt.binding(function() { return attr_float_scrub_min["master_" + root.attr_suffix] }) - } - } - } - XsModuleAttributes { - id: attr_float_scrub_max - attributesGroupNames: root.attr_group - roleName: "float_scrub_max" - - onAttrAdded: { - if (attr_name.includes("master")) { - inputRed.validator.top = Qt.binding(function() { return attr_float_scrub_max["red_" + root.attr_suffix] }) - inputGreen.validator.top = Qt.binding(function() { return attr_float_scrub_max["green_" + root.attr_suffix] }) - inputBlue.validator.top = Qt.binding(function() { return attr_float_scrub_max["blue_" + root.attr_suffix] }) - inputMaster.validator.top = Qt.binding(function() { return attr_float_scrub_max["master_" + root.attr_suffix] }) - } - } - } - - Column { - anchors.topMargin: 5 - anchors.fill: parent - - Row { - id: titleRow - spacing: 10 - anchors.horizontalCenter: parent.horizontalCenter - height: 30 - - Text { - text: root.title - font.pixelSize: 20 - color: "white" - } - - XsButton { - id: reloadButton - width: 20; height: 20 - bgColorNormal: "transparent" - borderWidth: 0 - - onClicked: { - if (root.title === "Sat") { - attr["saturation"] = attr_default_value["saturation"] - } - else { - attr["red_" + root.attr_suffix] = attr_default_value["red_" + root.attr_suffix] - attr["green_" + root.attr_suffix] = attr_default_value["green_" + root.attr_suffix] - attr["blue_" + root.attr_suffix] = attr_default_value["blue_" + root.attr_suffix] - attr["master_" + root.attr_suffix] = attr_default_value["master_" + root.attr_suffix] - } - } - - Image { - source: "qrc:/feather_icons/rotate-ccw.svg" - - layer { - enabled: true - effect: ColorOverlay { - color: reloadButton.down || reloadButton.hovered ? "white" : XsStyle.controlTitleColor - } - } - } - } - } - - ListView { - id: sliderList - anchors.horizontalCenter: parent.horizontalCenter - anchors.horizontalCenterOffset: -8 - width: root.fixed_size > 0 ? root.fixed_size : contentItem.childrenRect.width - height: 155 - - orientation: Qt.Horizontal - model: attr_model - delegate: GradingVSlider {} - } - - Row { - anchors.horizontalCenter: parent.horizontalCenter - leftPadding: 20 - - Column { - id: sliderInputCol - - XsTextField { - id: inputRed - width: 60 - bgColorNormal: "transparent" - borderColor: bgColorNormal - validator: DoubleValidator {} - - onEditingFinished: { - if (root.title === "Sat") { - attr["saturation"] = parseFloat(text) - } - else { - attr["red_" + root.attr_suffix] = parseFloat(text) - } - } - } - XsTextField { - id: inputGreen - visible: root.title != "Sat" - width: 60 - bgColorNormal: "transparent" - borderColor: bgColorNormal - validator: DoubleValidator {} - - onEditingFinished: { - attr["green_" + root.attr_suffix] = parseFloat(text) - } - } - XsTextField { - id: inputBlue - visible: root.title != "Sat" - width: 60 - bgColorNormal: "transparent" - borderColor: bgColorNormal - validator: DoubleValidator {} - - onEditingFinished: { - attr["blue_" + root.attr_suffix] = parseFloat(text) - } - } - } - - Column { - anchors.verticalCenter: parent.verticalCenter - leftPadding: 5 - - Label { - visible: root.title != "Sat" - text: root.title == "Offset" ? "+" : "x" - } - } - - Column { - anchors.verticalCenter: parent.verticalCenter - - XsTextField { - id: inputMaster - visible: root.title != "Sat" - width: 60 - bgColorNormal: "transparent" - borderColor: bgColorNormal - validator: DoubleValidator {} - - onEditingFinished: { - attr["master_" + root.attr_suffix] = parseFloat(text) - } - } - } - } - } -} diff --git a/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingSliderSimple.qml b/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingSliderSimple.qml deleted file mode 100644 index 26b3224bc..000000000 --- a/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingSliderSimple.qml +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient - -import xStudio 1.1 -import xstudio.qml.module 1.0 - -Item { - id: root - - width: 500 - height: 30 - - property string attr_group - - XsModuleAttributesModel { - id: model - attributesGroupNames: root.attr_group - } - - Column { - anchors.fill: parent - anchors.topMargin: 50 - anchors.leftMargin: 15 - spacing: 10 - - Repeater { - model: model - delegate: GradingHSlider {} - } - } -} diff --git a/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingVSlider.qml b/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingVSlider.qml deleted file mode 100644 index 5463d46f7..000000000 --- a/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingVSlider.qml +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient - -import xStudio 1.1 -import xstudio.qml.module 1.0 - -Item { - id: root - - width: slider.width - height: slider.height - - property real value: model.value - property real default_value: model.default_value - property real from: model.float_scrub_min - property real to: model.float_scrub_max - property real step: model.float_scrub_step - property var colour: model.attr_colour - - // Manually update the value, needed in case the default value match - // the type default construction value. For example Offset slider has - // a default value of 0 that should map to 0.5 in the Slider. - Component.onCompleted: { - update_value() - } - - onValueChanged: { - update_value() - } - - function update_value() { - if (!slider.pressed) { - slider.value = val_to_pos(value) - } - } - - // Note this is a naive log scale, in case the min and max are not - // mirrored around mid, the derivate will not be continous at the - // mid point. - - function pos_to_val(v) { - var min = root.from - var mid = root.default_value - var max = root.to - var steepness = 4 - - function lin_to_log(v) { - var log = Math.log - var antilog = Math.exp - return (antilog(v * steepness) - antilog(0.0)) / (antilog(1.0 * steepness) - antilog(0.0)) - } - - if (v < 0.5) { - return (1 - lin_to_log(1 - v * 2)) * (mid - min) + min - } else { - return lin_to_log((v - 0.5) * 2) * (max - mid) + mid - } - } - - function val_to_pos(v) { - var min = root.from - var mid = root.default_value - var max = root.to - var steepness = 4 - - function log_to_lin(v) { - var log = Math.log - var antilog = Math.exp - return log(v * (antilog(1.0 * steepness) - antilog(0.0)) + antilog(0.0)) / steepness - } - - if (v < pos_to_val(0.5, min, mid, max, steepness)) { - return (1 - log_to_lin(1 - ((v - min) / (mid - min)))) / 2.0 - } else { - return log_to_lin((v - mid) / (max - mid)) / 2.0 + 0.5 - } - } - - Column { - - Slider { - id: slider - anchors.left: parent.horizontalCenter - width: sliderHandle.width + 20 - height: 145 - from: 0.0 - to: 1.0 - stepSize: 0.01 - orientation: Qt.Vertical - - onValueChanged: { - if (pressed) { - model.value = pos_to_val(value) - } - } - - background: Rectangle { - x: slider.leftPadding + slider.availableWidth / 2 - width / 2 - y: slider.topPadding - width: 4 - height: slider.availableHeight - color: "grey" - radius: 2 - - Rectangle { - y: slider.visualPosition * parent.height - width: parent.width - height: parent.height - y - color: root.colour - radius: 2 - } - } - - handle: Rectangle { - id: sliderHandle - - x: (slider.width - width) / 2 - y: slider.topPadding + slider.visualPosition * (slider.availableHeight - height) - width: 15 - height: 15 - - radius: 15 - color: root.colour - border.color: Qt.darker(root.colour, 4) - } - } - } -} diff --git a/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingWheel.qml b/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingWheel.qml deleted file mode 100644 index 3ba0794b2..000000000 --- a/src/plugin/colour_op/grading/src/qml/Grading.1/GradingDialog/GradingWheel.qml +++ /dev/null @@ -1,545 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient - -import xStudio 1.1 -import xstudio.qml.module 1.0 - - -Item { - id: root - - width: wheel.width + 25 - height: titleRow.height + wheel.height + wheelInputCol.height - - property real size: 135 - - property string title - property var value - property real default_value - property real from - property real to - property string attr_group - property string attr_suffix - - onValueChanged: { - if (!carea.pressed) { - wheel.color = val_to_pos_color(value) - } - } - - XsModuleAttributes { - id: attr - attributesGroupNames: root.attr_group - - onAttrAdded: { - // For undetermined reasons, directly binding the value property - // to the attribute with "attr.red_slope ? attr.red_slope : 0" - // kind of syntax doesn't work, so we instead use this hack until - // we understand how to make it work directly. - // We know blue is added last so now create the full binding.. - if (attr_name.includes("blue")) { - root.value = Qt.binding(function() { - return Qt.vector4d( - attr["red_" + root.attr_suffix], - attr["green_" + root.attr_suffix], - attr["blue_" + root.attr_suffix], - attr["master_" + root.attr_suffix]) - }) - if (typeof val_to_pos_color === "function") { - wheel.color = val_to_pos_color(root.value) - } - } - } - } - XsModuleAttributes { - id: attr_default_value - attributesGroupNames: root.attr_group - roleName: "default_value" - - onAttrAdded: { - if (attr_name.includes("blue")) { - root.default_value = Qt.binding(function() { return attr_default_value["red_" + root.attr_suffix] }) - } - } - } - XsModuleAttributes { - id: attr_float_scrub_min - attributesGroupNames: root.attr_group - roleName: "float_scrub_min" - - onAttrAdded: { - if (attr_name.includes("master")) { - redInput.validator.bottom = Qt.binding(function() { return attr_float_scrub_min["red_" + root.attr_suffix] }) - greenInput.validator.bottom = Qt.binding(function() { return attr_float_scrub_min["green_" + root.attr_suffix] }) - blueInput.validator.bottom = Qt.binding(function() { return attr_float_scrub_min["blue_" + root.attr_suffix] }) - masterInput.validator.bottom = Qt.binding(function() { return attr_float_scrub_min["master_" + root.attr_suffix] }) - root.from = Qt.binding(function() { return attr_float_scrub_min["red_" + root.attr_suffix] }) - } - } - } - XsModuleAttributes { - id: attr_float_scrub_max - attributesGroupNames: root.attr_group - roleName: "float_scrub_max" - - onAttrAdded: { - if (attr_name.includes("master")) { - redInput.validator.top = Qt.binding(function() { return attr_float_scrub_max["red_" + root.attr_suffix] }) - greenInput.validator.top = Qt.binding(function() { return attr_float_scrub_max["green_" + root.attr_suffix] }) - blueInput.validator.top = Qt.binding(function() { return attr_float_scrub_max["blue_" + root.attr_suffix] }) - masterInput.validator.top = Qt.binding(function() { return attr_float_scrub_max["master_" + root.attr_suffix] }) - root.to = Qt.binding(function() { return attr_float_scrub_max["red_" + root.attr_suffix] }) - } - } - } - - function clamp(number, min, max) { - return Math.max(min, Math.min(number, max)) - } - - function clamp_v4d(v) { - return Qt.vector4d( - clamp(v.x, 0.0, 1.0), - clamp(v.y, 0.0, 1.0), - clamp(v.z, 0.0, 1.0), - clamp(v.w, 0.0, 1.0) - ) - } - - function v4d_to_color(v) { - return Qt.rgba(v.x, v.y, v.z, v.w) - } - - function color_to_v4d(c) { - return Qt.vector4d(c.r, c.g, c.b, c.a) - } - - // Note this is a naive log scale, in case the min and max are not - // mirrored around mid, the derivate will not be continous at the - // mid point. - - // Colour wheels only support adding / scaling up values. - - function pos_to_val(v) { - var min = root.default_value - var max = root.to - var steepness = 4 - - function lin_to_log(v) { - var log = Math.log - var antilog = Math.exp - return (antilog(v * steepness) - antilog(0.0)) / (antilog(1.0 * steepness) - antilog(0.0)) - } - - return lin_to_log(v) * (max - min) + min - } - - function val_to_pos(v) { - var min = root.default_value - var max = root.to - var steepness = 4 - - function log_to_lin(v) { - var log = Math.log - var antilog = Math.exp - return log(v * (antilog(1.0 * steepness) - antilog(0.0)) + antilog(0.0)) / steepness - } - - if (v < min) - v = min - else if (v > max) - v = max - - return log_to_lin((v - min) / (max - min)) - } - - function pos_to_val_color(color) { - return Qt.vector4d( - pos_to_val(color.x), - pos_to_val(color.y), - pos_to_val(color.z), - 1.0 - ); - } - - function val_to_pos_color(color) { - return Qt.vector4d( - val_to_pos(color.x), - val_to_pos(color.y), - val_to_pos(color.z), - 1.0 - ); - } - - function rgb_to_hsv(color) { - - var h, s, v = 0.0 - var r = color.x - var g = color.y - var b = color.z - - var max = Math.max(r, g, b) - var min = Math.min(r, g, b) - var delta = max - min - - v = max - s = max === 0 ? 0 : delta / max - - if (max === min) { - h = 0 - } else if (r === max) { - h = (g - b) / delta - } else if (g === max) { - h = 2 + (b - r) / delta - } else if (b === max) { - h = 4 + (r - g) / delta - } - - h = h < 0 ? h + 6 : h - h /= 6 - - // Handle extended range inputs (from OpenColorIO RGB_TO_HSV builtin) - if (min < 0) { - v += min - } - if (-min > max) { - s = delta / -min - } - - return Qt.vector3d(h, s, v) - } - - function hsv_to_rgb(color) { - - var MAX_SAT = 1.999 - - var r, g, b = 0.0 - var h = color.x - var s = color.y - var v = color.z - - h = ( h - Math.floor( h ) ) * 6.0 - s = clamp( s, 0.0, MAX_SAT ) - v = v - - r = clamp( Math.abs(h - 3.0) - 1.0, 0.0, 1.0 ) - g = clamp( 2.0 - Math.abs(h - 2.0), 0.0, 1.0 ) - b = clamp( 2.0 - Math.abs(h - 4.0), 0.0, 1.0 ) - - var max = v - var min = v * (1.0 - s) - - // Handle extended range inputs (from OpenColorIO HSV_TO_RGB builtin) - if (s > 1.0) - { - min = v * (1.0 - s) / (2.0 - s) - max = v - min - } - if (v < 0.0) - { - min = v / (2.0 - s) - max = v - min - } - - var delta = max - min - r = r * delta + min - g = g * delta + min - b = b * delta + min - - return Qt.vector3d(r, g, b) - } - - function rgb_to_pos(color) { - - var hsv = rgb_to_hsv(color) - hsv = Qt.vector3d(hsv.x, hsv.z, hsv.y) - - var angle = (1 - hsv.x) * (2 * Math.PI) - var dist = Math.abs(hsv.y) - return Qt.vector2d( - Math.sin(angle) * dist, - Math.cos(angle) * dist - ) - } - - Column { - anchors.topMargin: 5 - anchors.fill: parent - spacing: 10 - - Row { - id: titleRow - spacing: 10 - anchors.horizontalCenter: parent.horizontalCenter - height: 30 - - Text { - text: root.title - font.pixelSize: 20 - color: "white" - } - - XsButton { - id: reloadButton - width: 20; height: 20 - bgColorNormal: "transparent" - borderWidth: 0 - - onClicked: { - attr["red_" + root.attr_suffix] = attr_default_value["red_" + root.attr_suffix] - attr["green_" + root.attr_suffix] = attr_default_value["green_" + root.attr_suffix] - attr["blue_" + root.attr_suffix] = attr_default_value["blue_" + root.attr_suffix] - attr["master_" + root.attr_suffix] = attr_default_value["master_" + root.attr_suffix] - } - - Image { - source: "qrc:/feather_icons/rotate-ccw.svg" - - layer { - enabled: true - effect: ColorOverlay { - color: reloadButton.down || reloadButton.hovered ? "white" : XsStyle.controlTitleColor - } - } - } - } - } - - Control { - id: wheel - anchors.horizontalCenter: parent.horizontalCenter - - property int radius: root.size / 2 - property int center: root.size / 2 - property real ring_rel_size: 0.1 - property real cursor_width: 17 - - property real value: 1.0 - property real saturation: 1.0 - property vector4d color: Qt.vector4d(1.0, 1.0, 1.0, 1.0) - - onColorChanged: { - - if (carea.pressed) { - var color_out = pos_to_val_color(color) - attr["red_" + root.attr_suffix] = color_out.x - attr["green_" + root.attr_suffix] = color_out.y - attr["blue_" + root.attr_suffix] = color_out.z - } else { - var pos = rgb_to_pos(color) - cdrag.x = center + pos.x * radius - cdrag.y = center - pos.y * radius - } - } - - contentItem: Item { - implicitWidth: root.size - implicitHeight: width - - ShaderEffect { - id: shadereffect - width: parent.width - height: parent.height - - readonly property real radius: 0.5 - readonly property real ring_radius: radius - radius * wheel.ring_rel_size - readonly property real saturation: wheel.saturation - readonly property real value: wheel.value - - fragmentShader: " - #version 330 - - #define M_PI 3.1415926535897932384626433832795 - #define M_PI_2 (2.0 * M_PI) - - varying highp vec2 qt_TexCoord0; - - uniform highp float qt_Opacity; - uniform highp float radius; - uniform highp float ring_radius; - uniform highp float saturation; - uniform highp float value; - - vec3 hsv_to_rgb(vec3 c) { - vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); - vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); - return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); - } - - void main() { - highp vec2 coord = qt_TexCoord0 - vec2(0.5); - highp float r = length(coord); - highp float h = atan(coord.x, coord.y); - highp float s = r <= ring_radius ? saturation * 0.5 : saturation; - highp float v = r <= ring_radius ? value * 0.35 : value; - - if (r <= radius) { - vec3 rgb = hsv_to_rgb( vec3(h / M_PI_2 + 0.5, s, v) ); - gl_FragColor = vec4(rgb, 1.0); - } else { - gl_FragColor = vec4(0.0); - } - } - " - } - - // Cross in the center - Rectangle { - color: "grey" - width: wheel.width - wheel.ring_rel_size * wheel.width - height: 1 - x: wheel.ring_rel_size * wheel.width / 2 - y: wheel.center - } - Rectangle { - color: "grey" - width: 1 - height: wheel.height - wheel.ring_rel_size * wheel.height - x: wheel.center - y: wheel.ring_rel_size * wheel.height / 2 - } - - // Cursor - Rectangle { - id: cursor - - width: wheel.cursor_width - height: width - radius: width/2 - - x: (cdrag.radius <= wheel.radius ? cdrag.x : wheel.center + (cdrag.x - wheel.center) * (wheel.radius / cdrag.radius)) - (width / 2) - y: (cdrag.radius <= wheel.radius ? cdrag.y : wheel.center + (cdrag.y - wheel.center) * (wheel.radius / cdrag.radius)) - (height / 2) - - color: Qt.darker(cursor_color(wheel.color), 1.25) - border.color: Qt.darker(color) - border.width: 0.75 - - function cursor_color(color) { - var rgb_norm = clamp_v4d(color) - var hsv = rgb_to_hsv(Qt.vector3d(rgb_norm.x, rgb_norm.y, rgb_norm.z)) - var rgb = hsv_to_rgb(Qt.vector3d(hsv.x, hsv.z, 1.0)) - return Qt.rgba(rgb.x, rgb.y, rgb.z, 1.0) - } - - MouseArea { - id: carea - anchors.fill: parent - drag.threshold: 0 - drag.target: Item { - id: cdrag - - readonly property real radius: Math.hypot(x - wheel.center, y - wheel.center) - - x: wheel.center - y: wheel.center - } - - onPositionChanged: { - - var cursor_pos = Qt.vector2d(cursor.x, cursor.y) - var offset = Qt.vector2d(cursor.width / 2, cursor.height / 2) - var pos = cursor_pos.plus(offset) - - // Hue angle normalised [0,1] - var hue = Math.atan2( - pos.x - wheel.center, - pos.y - wheel.center) - hue = hue / (2 * Math.PI) + 0.5 - // Distance from center normalised [0,1] - var dist = Math.hypot( - pos.x - wheel.center, - pos.y - wheel.center) - dist /= wheel.radius - - var hsv = Qt.vector3d(hue, 1.0, dist) - var rgb = hsv_to_rgb(hsv) - wheel.color = Qt.vector4d(rgb.x, rgb.y, rgb.z, 1.0) - } - } - } - } - } - - Row { - anchors.horizontalCenter: parent.horizontalCenter - leftPadding: 20 - - Column { - id: wheelInputCol - - XsTextField { - id: redInput - width: 60 - bgColorNormal: "transparent" - borderColor: bgColorNormal - text: root.value ? root.value.x.toFixed(5) : "" - validator: DoubleValidator {} - - onEditingFinished: { - attr["red_" + root.attr_suffix] = parseFloat(text) - } - } - XsTextField { - id: greenInput - width: 60 - bgColorNormal: "transparent" - borderColor: bgColorNormal - text: root.value ? root.value.y.toFixed(5) : "" - validator: DoubleValidator {} - - onEditingFinished: { - attr["green_" + root.attr_suffix] = parseFloat(text) - } - } - XsTextField { - id: blueInput - width: 60 - bgColorNormal: "transparent" - borderColor: bgColorNormal - text: root.value ? root.value.z.toFixed(5) : "" - validator: DoubleValidator {} - - onEditingFinished: { - attr["blue_" + root.attr_suffix] = parseFloat(text) - } - } - } - - Column { - anchors.verticalCenter: parent.verticalCenter - leftPadding: 5 - - Label { - visible: root.title != "Sat" - text: root.title == "Offset" ? "+" : "x" - } - } - - Column { - anchors.verticalCenter: parent.verticalCenter - - XsTextField { - id: masterInput - width: 60 - bgColorNormal: "transparent" - borderColor: bgColorNormal - text: root.value ? root.value.w.toFixed(5) : "" - validator: DoubleValidator {} - - onEditingFinished: { - attr["master_" + root.attr_suffix] = parseFloat(text) - } - } - } - } - } -} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.1/qmldir b/src/plugin/colour_op/grading/src/qml/Grading.1/qmldir deleted file mode 100644 index a96396b12..000000000 --- a/src/plugin/colour_op/grading/src/qml/Grading.1/qmldir +++ /dev/null @@ -1,9 +0,0 @@ -module Grading - -GradingButton 1.0 GradingButton.qml -GradingDialog 1.0 GradingDialog/GradingDialog.qml -GradingHSlider 1.0 GradingDialog/GradingHSlider.qml -GradingVSlider 1.0 GradingDialog/GradingVSlider.qml -GradingSliderGroup 1.0 GradingDialog/GradingSliderGroup.qml -GradingSliderSimple 1.0 GradingDialog/GradingSliderSimple.qml -GradingWheel 1.0 GradingDialog/GradingWheel.qml \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/GTAttributes.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/GTAttributes.qml new file mode 100644 index 000000000..1142c18e0 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/GTAttributes.qml @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + +import xstudio.qml.models 1.0 +import xStudio 1.0 + +Item { + + /* This connects to the backend model data named grading_settings to which + many of our attributes have been added*/ + XsModuleData { + id: grading_tool_attrs_data + modelDataName: "grading_settings" + } + property alias grading_settings: grading_tool_attrs_data + + /////////////////////////////////////////////////////////////////////// + // to DIRECTLY expose attribute role data we use XsAttributeValue and + // give it the title (name) of the attribute. By default it will expose + // the 'value' role data of the attribute but you can override this to + // get to other role datas such as 'combo_box_options' (the string + // choices in a StringChoiceAttribute) or 'default_value' etc. + + XsAttributeValue { + id: __tool_opened_count + attributeTitle: "tool_opened_count" + model: grading_tool_attrs_data + } + property alias tool_opened_count: __tool_opened_count.value + + XsAttributeValue { + id: __grading_action + attributeTitle: "grading_action" + model: grading_tool_attrs_data + } + property alias grading_action: __grading_action.value + + XsAttributeValue { + id: __media_colour_managed + attributeTitle: "media_colour_managed" + model: grading_tool_attrs_data + } + property alias media_colour_managed: __media_colour_managed.value + + XsAttributeValue { + id: __grading_bookmark + attributeTitle: "grading_bookmark" + model: grading_tool_attrs_data + } + property alias grading_bookmark: __grading_bookmark.value + + XsAttributeValue { + id: __grading_bypass + attributeTitle: "grading_bypass" + model: grading_tool_attrs_data + } + property alias grading_bypass: __grading_bypass.value + + XsAttributeValue { + id: __working_space + attributeTitle: "working_space" + model: grading_tool_attrs_data + } + property alias working_space: __working_space.value + + XsAttributeValue { + id: __colour_space + attributeTitle: "colour_space" + model: grading_tool_attrs_data + } + property alias colour_space: __colour_space.value + + XsAttributeValue { + id: __mask_tool_active + attributeTitle: "mask_tool_active" + model: grading_tool_attrs_data + } + property alias mask_tool_active: __mask_tool_active.value + + /////////////////////////////////////////////////////////////////////// + // Get access to all 'role' data items of the actual grading attributes + // The 'values' object has properties that map to the names of attribute + // role data. E.g. 'value' 'default_value' 'float_scrub_min' 'combo_box_options' + // etc. + + XsAttributeFullData { + id: __slope + attributeTitle: "Slope" + model: grading_tool_attrs_data + } + property alias slope: __slope.values + + XsAttributeFullData { + id: __offset + attributeTitle: "Offset" + model: grading_tool_attrs_data + } + property alias offset: __offset.values + + XsAttributeFullData { + id: __power + attributeTitle: "Power" + model: grading_tool_attrs_data + } + property alias power: __power.values + + XsAttributeFullData { + id: __saturation + attributeTitle: "Saturation" + model: grading_tool_attrs_data + } + property alias saturation: __saturation.values + + XsAttributeFullData { + id: __exposure + attributeTitle: "Exposure" + model: grading_tool_attrs_data + } + property alias exposure: __exposure.values + + XsAttributeFullData { + id: __contrast + attributeTitle: "Contrast" + model: grading_tool_attrs_data + } + property alias contrast: __contrast.values + + /////////////////////////////////////////////////////////////////////// + // this provides access to the attributes in the "grading_sliders" group + XsModuleData { + id: grading_wheels_model + modelDataName: "grading_wheels" + } + property alias grading_wheels_model: grading_wheels_model + + XsModuleData { + id: grading_sliders_model + modelDataName: "grading_sliders" + } + property alias grading_sliders_model: grading_sliders_model + + /////////////////////////////////////////////////////////////////////// + // helpers + function getAttrValue(attr_name) { + var idx = grading_wheels_model.searchRecursive(attr_name,"title") + if (idx.valid) { + return grading_wheels_model.get(idx, "value") + } + + var idx = grading_sliders_model.searchRecursive(attr_name,"title") + if (idx.valid) { + return grading_sliders_model.get(idx, "value") + } + + console.log("GradingAttrs: attribute named", attr_name, "not found.") + return undefined + } + +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/GradingOverlay.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/GradingOverlay.qml new file mode 100644 index 000000000..4fff30a1d --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/GradingOverlay.qml @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick +import QtQuick.Shapes 1.6 +import xstudio.qml.models 1.0 +import xstudio.qml.viewport 1.0 + +import xStudio 1.0 + +Item { + id: root + + GTAttributes { + id: grd_attrs + } + + MAttributes { + id: attrs + } + + XsModuleData { + id: grading_tool_overlay_shapes + modelDataName: "grading_tool_overlay_shapes" + } + + // Note: Viewport has property imageBoundariesInViewport - this is a vector + // of QRect desribing the coordinates of the image(s) in the viewport. + // We use this, with the keySubplayheadIndex of the playhead, to work out + // the geometry of the image being modified in the viewport. + // If multiple images are visibe (e.g. contact sheet/grid layout) then we + // are modifying the image in the vector with an index that is the + // keySubplayheadIndex. If only one image is visible, this image is coming + // from keySubplayheadIndex so we use index zero. + property var keyPlayheadIdx: viewportPlayhead.keySubplayheadIndex + property var imBoundaries: view.imageBoundariesInViewport ? view.imageBoundariesInViewport : [] + property var imResolutions: view.imageResolutions + + property var viewportName: view.name + property var imageBox: keyPlayheadIdx < imBoundaries.length ? imBoundaries[keyPlayheadIdx] : imBoundaries.length ? imBoundaries[0] : Qt.rect(0,0,0,0) + property var imageResolution: keyPlayheadIdx < imResolutions.length ? imResolutions[keyPlayheadIdx] : imResolutions.length ? imResolutions[0] : Qt.size(0, 0) + property real imageAspectRatio: imageBox.width/imageBox.height + property real viewportScale: 1.0 + property var viewportOffset: Qt.point(0, 0) + + property var tool_opened_count: grd_attrs.tool_opened_count + property var mask_shapes_visible: attrs.mask_shapes_visible + property var mask_selected_shape: attrs.mask_selected_shape + property var drawing_action: attrs.drawing_action + + visible: mask_shapes_visible && tool_opened_count > 0 && !isQuickview + + property var polygon_points: [] + property var polygon_shape + + property var activeShape: -1 + + onActiveShapeChanged: { + attrs.mask_selected_shape = activeShape; + } + + onMask_selected_shapeChanged: { + if (mask_selected_shape != activeShape) { + activeShape = mask_selected_shape; + } + } + + onImageBoxChanged: { + // OpenGL normalized device coordinates to window pixel coordinates + viewportScale = imageBox.width / 2.0; + viewportOffset = Qt.point( + imageBox.x + imageBox.width / 2.0, + imageBox.y + imageBox.height / 2.0 + ); + } + + function handleDoubleClick(mouse) { + if (activeShape >= 0 && activeShape < repeater.count) { + repeater.itemAt(activeShape).handleDoubleClick(mouse); + } + } + + function set_interaction_shape(modelIndex) { + activeShape = modelIndex + } + // Event handling + + XsHotkey { + sequence: "Escape" + name: "unselect" + context: "any" + onActivated: { + construction_polygon.item.cleanupPolygon() + } + } + + // indicates the 'current' (hero) image in multi image layours + Rectangle { + visible: viewportPlayhead.numSubPlayheads > 1 + x: imageBox.x + y: imageBox.y + width: imageBox.width + height: imageBox.height + color: "transparent" + border.color: palette.highlight + border.width: 2 + } + + // If we use a MouseArea in the overlay, it stops the Viewport having + // full control over the pointer (like setting the Cursor shape). It's + // better to listen to mouse events coming from the Viewport itself. + Connections { + + target: view // the viewport + + function onMouseRelease(buttons) { + + if (!attrs.polygon_init) attrs.interacting = false + construction_polygon.item.mouseReleased(buttons) + for (var i = 0; i < repeater.count; ++i) { + repeater.itemAt(i).item.mouseReleased(buttons) + } + + } + + function onMousePress(position, buttons, modifiers) { + + if (construction_polygon.item.mousePressed(position, buttons, modifiers)) return + + for (var i = 0; i < repeater.count; ++i) { + if (repeater.itemAt(i).item.mousePressed(position, buttons, modifiers)) { + return + } + } + + } + + function onMouseDoubleClick(position, buttons, modifiers) { + if (activeShape >= 0) { + repeater.itemAt(activeShape).item.mouseDoubleClicked(position, buttons, modifiers) + } + } + + function onMousePositionChanged(position, buttons, modifiers) { + + construction_polygon.item.mouseMoved(position, buttons, modifiers) + for (var i = 0; i < repeater.count; ++i) { + repeater.itemAt(i).item.mouseMoved(position, buttons, modifiers) + } + + } + } + + // Overlay shapes + + Repeater { + id: repeater + model: grading_tool_overlay_shapes + + onItemAdded: (index) => { + activeShape = index; + } + + onItemRemoved: { + if (activeShape >= count) { + activeShape = count - 1; + } + } + + delegate: Loader { + + property var modelIndex: index + property var modelValue: value + + sourceComponent: { + if (value.type === "polygon") + polygon; + else if (value.type === "ellipse") { + ellipse; + } + else + console.log("Unknown shape type: " + value.type); + } + + // Update logic + function updateModelValue(v) { + value = v; + } + + function updateAttr(name, val) { + var v = value; + v[name] = val; + value = v; + } + + // Events + function handleDoubleClick(mouse) { + if (item.handleDoubleClick) { + item.handleDoubleClick(mouse); + } + } + + } + } + + Component { + id: polygon + + MPolygon { + canvas: root + viewScale: viewportScale + + transform: [ + Scale { xScale: viewportScale; yScale: viewportScale }, + Translate { x: viewportOffset.x; y: viewportOffset.y } + ] + + } + } + + Component { + id: ellipse + + MEllipse { + canvas: root + viewScale: viewportScale + + transform: [ + Scale { xScale: viewportScale; yScale: viewportScale }, + Translate { x: viewportOffset.x; y: viewportOffset.y } + ] + + } + } + + Component { + + id: cp + MPolygon { + + canvas: root + viewScale: viewportScale + under_construction: attrs.polygon_init + mv: parent.modelValue + + transform: [ + Scale { xScale: viewportScale; yScale: viewportScale }, + Translate { x: viewportOffset.x; y: viewportOffset.y } + ] + + onUnder_constructionChanged: { + if (under_construction) { + startPolygon() + } else { + finalizePolygon() + } + } + + function startPolygon() { + activeShape = -2; + updateModelValue({"points": []}) + attrs.interacting = true + } + + function finalizePolygon() { + + if (modelValue.points.length >= 3) { + var str_action = "Add Polygon "; + var new_pts = modelValue.points + + // Construct action string with list of point coordinates + for (var i = 0; i < new_pts.length; i++) { + + str_action += new_pts[i][2] + "," + new_pts[i][3] + ","; + } + attrs.drawing_action = str_action; + } + cleanupPolygon(); + + } + + function cleanupPolygon() { + updateModelValue({"points": []}) + attrs.polygon_init = false; + activeShape = -2; + attrs.interacting = false + + } + + } + } + + Loader { + + id: construction_polygon + + property var modelIndex: -2 + property var modelValue: {"points": []} + + sourceComponent: cp + + // Update logic + function updateModelValue(v) { + modelValue = v; + } + + } +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/GradingTools.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/GradingTools.qml new file mode 100644 index 000000000..997e74e17 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/GradingTools.qml @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic + +import xStudio 1.0 +import xstudio.qml.bookmarks 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.models 1.0 + +Item { id: dialog + anchors.fill: parent + + GTAttributes { id: attrs } + + MAttributes { id: mask_attrs } + + property real itemSpacing: 1 + property real buttonSpacing: 1 + property real btnWidth: XsStyleSheet.primaryButtonStdWidth + property real btnHeight: XsStyleSheet.widgetStdHeight + 4 + property real panelPadding: XsStyleSheet.panelPadding + property color panelColor: XsStyleSheet.panelBgColor + + property alias grading_sliders_model: attrs.grading_sliders_model + property alias grading_wheels_model: attrs.grading_wheels_model + + Component.onCompleted: { + // If created in hidden state, just rely on onVisibleChanged + // to increment the property when the component becomes visible. + if (visible) { + attrs.tool_opened_count += 1 + } + } + Component.onDestruction: { + attrs.tool_opened_count -= 1 + } + + onVisibleChanged: { + if (attrs.tool_opened_count === undefined) { + return + } + + if (visible) { + attrs.tool_opened_count += 1 + } else { + attrs.tool_opened_count -= 1 + } + } + + function hasActiveGrade() { + return attrs.grading_bookmark && attrs.grading_bookmark != "00000000-0000-0000-0000-000000000000" + } + + XsBookmarkFilterModel { + id: bookmarkFilterModel + sourceModel: bookmarkModel + currentMedia: currentPlayhead.mediaUuid + showHidden: true + showUserType: "Grading" + sortbyCreated: true + } + + + property alias moreMenu: menus.moreMenu + + Sec0Menu{ + id: menus + } + + + property alias bookmarkList: listDiv.bookmarkList + + Item { + anchors.fill: parent + anchors.margins: panelPadding + + XsSplitView { + id: rightPanel + width: parent.width + height: parent.height + + thumbWidth: XsStyleSheet.panelPadding + colorHandleBg: XsStyleSheet.panelBgGradTopColor + + ColumnLayout{ id: leftBar + SplitView.minimumWidth: 270 + Layout.preferredWidth: 290 + SplitView.fillHeight: true + spacing: panelPadding + + Sec1Header{ id: header + Layout.fillWidth: true + Layout.preferredHeight: btnHeight + y: 0 //header.height + panelPaddingeight + Layout.maximumHeight: btnHeight + } + Sec2LayerList{ id: listDiv + Layout.fillWidth: true + Layout.fillHeight: true + } + } + + ColumnLayout{ id: rightBar + SplitView.minimumWidth: 200 + SplitView.fillWidth: true + SplitView.fillHeight: true + spacing: panelPadding + + RowLayout{ + Layout.fillWidth: true + Layout.preferredHeight: btnHeight + SplitView.fillHeight: true + + Sec3MaskTools{ + Layout.fillWidth: true + Layout.preferredHeight: btnHeight + } + XsPrimaryButton{ id: moreBtn + Layout.preferredWidth: btnWidth + Layout.preferredHeight: btnHeight + Layout.alignment: Qt.AlignRight + imgSrc: "qrc:/icons/more_vert.svg" + onClicked:{ + if(moreMenu.visible) moreMenu.visible = false + else{ + moreMenu.x = x + width + leftBar.width + moreMenu.y = y + height + moreMenu.visible = true + } + } + } + } + RowLayout{ + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 1 + + GTSliderItem { + Layout.fillWidth: true + Layout.preferredWidth: 150 + Layout.maximumWidth: rightPanel.width/4 + Layout.fillHeight: true + } + Repeater{ + model: attrs.grading_wheels_model + + GTWheelItem { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 150 + Layout.maximumWidth: rightPanel.width/4 + } + } + } + } + + + } + + + } + +} diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/MAttributes.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/MAttributes.qml new file mode 100644 index 000000000..ee09de393 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/MAttributes.qml @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + +import xstudio.qml.models 1.0 +import xStudio 1.0 + +Item { + + /* This connects to the backend model data named grading_settings to which + many of our attributes have been added*/ + XsModuleData { + id: mask_tool_attrs_data + modelDataName: "mask_tool_settings" + } + property alias model: mask_tool_attrs_data + + // to DIRECTLY expose attribute role data we use XsAttributeValue and give it the + // title (name) of the attribute. By default it will expose the 'value' + // role data of the attribute but you can override this to get to other + // role datas such as 'combo_box_options' (the string choices in a + // StringChoiceAttribute) or 'default_value' etc. + + XsAttributeValue { + id: __draw_pen_size + attributeTitle: "Draw Pen Size" + model: mask_tool_attrs_data + } + property alias draw_pen_size: __draw_pen_size.value + + XsAttributeValue { + id: __erase_pen_size + attributeTitle: "Erase Pen Size" + model: mask_tool_attrs_data + } + property alias erase_pen_size: __erase_pen_size.value + + XsAttributeValue { + id: __pen_colour + attributeTitle: "Pen Colour" + model: mask_tool_attrs_data + } + property alias pen_colour: __pen_colour.value + + XsAttributeValue { + id: __pen_opacity + attributeTitle: "Pen Opacity" + model: mask_tool_attrs_data + } + property alias pen_opacity: __pen_opacity.value + + XsAttributeValue { + id: __pen_softness + attributeTitle: "Pen Softness" + model: mask_tool_attrs_data + } + property alias pen_softness: __pen_softness.value + + XsAttributeValue { + id: __drawing_tool + attributeTitle: "drawing_tool" + model: mask_tool_attrs_data + } + property alias drawing_tool: __drawing_tool.value + + XsAttributeValue { + id: __drawing_action + attributeTitle: "drawing_action" + model: mask_tool_attrs_data + } + property alias drawing_action: __drawing_action.value + + XsAttributeValue { + id: __display_mode + attributeTitle: "display_mode" + model: mask_tool_attrs_data + } + property alias display_mode: __display_mode.value + + XsAttributeValue { + id: __mask_selected_shape + attributeTitle: "mask_selected_shape" + model: mask_tool_attrs_data + } + property alias mask_selected_shape: __mask_selected_shape.value + + XsAttributeValue { + id: __mask_shapes_visible + attributeTitle: "mask_shapes_visible" + model: mask_tool_attrs_data + } + property alias mask_shapes_visible: __mask_shapes_visible.value + + XsAttributeValue { + id: __shape_invert + attributeTitle: "shape_invert" + model: mask_tool_attrs_data + } + property alias shape_invert: __shape_invert.value + + XsAttributeValue { + id: __polygon_init + attributeTitle: "polygon_init" + model: mask_tool_attrs_data + } + property alias polygon_init: __polygon_init.value + + XsAttributeValue { + id: __interacting + attributeTitle: "interacting" + model: mask_tool_attrs_data + } + property alias interacting: __interacting.value + +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTSliderDiv.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTSliderDiv.qml new file mode 100644 index 000000000..52398d6f0 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTSliderDiv.qml @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick + +import QtQuick.Layouts + +import xstudio.qml.bookmarks 1.0 + + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import Grading 2.0 + +Item{ + + property string titleText: "" + + ColumnLayout{ id: col + anchors.fill: parent + anchors.horizontalCenter: parent.horizontalCenter + anchors.margins: 2 + spacing: 1 + + RowLayout{ id: titleSlider + Layout.fillWidth: true + Layout.preferredHeight: XsStyleSheet.secondaryButtonStdWidth + 2 + Layout.maximumHeight: XsStyleSheet.secondaryButtonStdWidth + 2 + spacing: 0 + + Item{ + Layout.fillWidth: true + Layout.fillHeight: true + } + XsText { id: textDiv + Layout.preferredWidth: textWidth + Layout.fillHeight: true + text: titleText + elide: Text.ElideRight + font.pixelSize: XsStyleSheet.fontSize*1.2 + font.bold: true + + MouseArea{ id: textMA + anchors.fill: parent + hoverEnabled: true + onClicked:{ + resetButton.clicked() + } + onPressed: resetButton.down = true + onReleased: resetButton.down = undefined + } + } + Item{ + Layout.fillWidth: true + Layout.minimumWidth: 0 + Layout.maximumWidth: 4 + Layout.fillHeight: true + } + XsSecondaryButton { id: resetButton + Layout.minimumWidth: XsStyleSheet.secondaryButtonStdWidth + Layout.preferredWidth: XsStyleSheet.secondaryButtonStdWidth + Layout.fillHeight: true + imgSrc: "qrc:/icons/rotate-ccw.svg" + imageDiv.sourceSize.width: 14 + imageDiv.sourceSize.height: 14 + forcedHover: textMA.containsMouse + + onClicked: { + value = default_value + } + } + Item{ + Layout.fillWidth: true + Layout.fillHeight: true + } + } + + Item{ + Layout.fillWidth: true + Layout.minimumHeight: 2 + } + + Item{ id: controlDiv + Layout.fillWidth: true + Layout.fillHeight: true + + GTSlider{ id: slider + height: parent.height + anchors.horizontalCenter: parent.horizontalCenter + + backend_value: value + onSetValue: (newVal)=> { + var _value = (typeof value == "number") ? value : value[index] + _value = newVal + if(typeof value == "number") value = _value + else value[index] = _value + } + } + } + + Item{ id: valuesDiv + Layout.fillWidth: true + Layout.minimumHeight: XsStyleSheet.widgetStdHeight + + GTValueEditor{ + width: 45 + height: XsStyleSheet.widgetStdHeight + valueText: value.toFixed(3) + indicatorColor: "transparent" + anchors.horizontalCenter: parent.horizontalCenter + + onEdited:{ + value = parseFloat(currentText) + } + } + } + + } + +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTSliderItem.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTSliderItem.qml new file mode 100644 index 000000000..f8c61dd1f --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTSliderItem.qml @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick + +import QtQuick.Layouts + +import xstudio.qml.bookmarks 1.0 + + + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import Grading 2.0 + +Item { + property real dividerWidth: 2 + + Rectangle{ id: bg + anchors.fill: parent + color: XsStyleSheet.baseColor + } + + RowLayout{ id: controlsDiv + anchors.fill: bg + anchors.margins: spacing + spacing: 0 + + Repeater{ id: repeater + model: attrs.grading_sliders_model + + Row{ + Layout.minimumWidth: (parent.width-(dividerWidth*(repeater.count-1)))/repeater.count + dividerWidth + Layout.preferredWidth: (parent.width-(dividerWidth*(repeater.count-1)))/repeater.count + dividerWidth + Layout.fillHeight: true + + GTSliderDiv{ + titleText: abbr_title + width: parent.width - dividerWidth + height: parent.height + } + Rectangle{ id: divider + width: dividerWidth + height: parent.height + color: XsStyleSheet.panelBgColor + } + } + } + + Item{ + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 0 + } + } + +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTWheelDivHorz.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTWheelDivHorz.qml new file mode 100644 index 000000000..b56bb2736 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTWheelDivHorz.qml @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick + +import QtQuick.Layouts + + + + +import xStudio 1.0 +import xstudio.qml.bookmarks 1.0 +import Grading 2.0 + +ColumnLayout { id: wheelDiv + spacing: 1 + + property bool isWider: false + onWidthChanged: { + if(width > 150) isWider = true + else isWider = false + } + + Item{ + Layout.fillWidth: true + Layout.minimumHeight: defaultWheelSize/2 + Layout.preferredHeight: defaultWheelSize + Layout.fillHeight: true + + onWidthChanged:{ + wheel.scale = Math.min(width, height) < defaultWheelSize ? + Math.min(width, height)/defaultWheelSize : 1 + } + onHeightChanged:{ + wheel.scale = Math.min(width, height) < defaultWheelSize ? + Math.min(width, height)/defaultWheelSize : 1 + } + + GTWheel { + id: wheel + anchors.centerIn: parent + + backend_color: value + wheelSize: defaultWheelSize + + } + } + + Item{ + Layout.fillWidth: true + Layout.minimumHeight: visible? 4:0 + Layout.fillHeight: true + visible: isWider && wheelDiv.height > 150 + } + + Item{ + Layout.fillWidth: true + Layout.preferredHeight: visible? (XsStyleSheet.widgetStdHeight) : 0 + visible: isWider && wheelDiv.width > 150 + + GridLayout{ id: rgbGrid + rows: 1 + rowSpacing: 0 + columns: 3 + columnSpacing: 1.5 + height: XsStyleSheet.widgetStdHeight + anchors.horizontalCenter: parent.horizontalCenter + + Repeater { + model: 3 + + GTValueEditor{ + Layout.preferredWidth: 46 + Layout.preferredHeight: XsStyleSheet.widgetStdHeight + valueText: value[index].toFixed(4) + indicatorColor: index==0?"red":index==1?"green":"blue" + + onEdited:{ + var _value = value + _value[index] = parseFloat(currentText) + value = _value + } + } + } + + } + + } + Item{ + Layout.fillWidth: true + Layout.maximumHeight: 4 + } + + +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTWheelDivVert.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTWheelDivVert.qml new file mode 100644 index 000000000..7bac06db7 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTWheelDivVert.qml @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick + +import QtQuick.Layouts + + + + +import xStudio 1.0 +import xstudio.qml.bookmarks 1.0 +import Grading 2.0 + +ColumnLayout { id: wheelDiv + spacing: 1 + + property bool isTaller: false + onHeightChanged: { + if(height > 195) isTaller = true + else isTaller = false + } + + Item{ + Layout.fillWidth: true + Layout.minimumHeight: defaultWheelSize/2 + Layout.preferredHeight: defaultWheelSize + Layout.fillHeight: true + + onWidthChanged:{ + wheel.scale = Math.min(width, height) < defaultWheelSize ? + Math.min(width, height)/defaultWheelSize : 1 + } + onHeightChanged:{ + wheel.scale = Math.min(width, height) < defaultWheelSize ? + Math.min(width, height)/defaultWheelSize : 1 + } + + GTWheel { + id: wheel + anchors.centerIn: parent + + backend_color: value + wheelSize: defaultWheelSize + } + } + + Item{ + Layout.fillWidth: true + Layout.minimumHeight: visible? 4:0 + Layout.fillHeight: true + visible: isTaller + } + + Repeater { + model: 3 + + Item{ + Layout.fillWidth: true + Layout.preferredHeight: visible? XsStyleSheet.widgetStdHeight : 0 + visible: isTaller + + GTValueEditor{ + width: 40+10 + height: parent.height + anchors.horizontalCenter: parent.horizontalCenter + valueText: value[index].toFixed(4) + indicatorColor: index==0?"red":index==1?"green":"blue" + + onEdited:{ + var _value = value + _value[index] = parseFloat(currentText) + value = _value + } + } + } + } + + +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTWheelItem.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTWheelItem.qml new file mode 100644 index 000000000..ab1ea8880 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/delegates/GTWheelItem.qml @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick + +import QtQuick.Layouts + + + + +import xStudio 1.0 +import xstudio.qml.bookmarks 1.0 +import Grading 2.0 + +Item { id: wheelItem + property real dividerWidth: 2 + + Rectangle{ id: bg + anchors.fill: parent + color: XsStyleSheet.baseColor + } + Rectangle{ id: divider + width: dividerWidth + height: parent.height + color: XsStyleSheet.panelBgColor + anchors.left: parent.left + } + + ColumnLayout { id: col + anchors.fill: parent + anchors.margins: 1 + spacing: 1 + + Item{ + Layout.fillWidth: true + Layout.preferredHeight: 2 + Layout.maximumHeight: 2 + } + RowLayout{ id: titleDiv + Layout.fillWidth: true + Layout.preferredHeight: XsStyleSheet.secondaryButtonStdWidth + 2 + Layout.maximumHeight: XsStyleSheet.secondaryButtonStdWidth + 2 + spacing: 1 + + Item{ + Layout.fillWidth: true + Layout.fillHeight: true + } + XsText { id: textDiv + Layout.preferredWidth: textWidth + Layout.fillHeight: true + text: abbr_title + elide: Text.ElideRight + font.pixelSize: XsStyleSheet.fontSize*1.2 + font.bold: true + + MouseArea{ id: textMA + anchors.fill: parent + hoverEnabled: true + onClicked:{ + resetButton.clicked() + } + onPressed: resetButton.down = true + onReleased: resetButton.down = undefined + } + } + Item{ + Layout.fillWidth: true + Layout.minimumWidth: 0 + Layout.maximumWidth: 4 + Layout.fillHeight: true + } + XsSecondaryButton { id: resetButton + Layout.preferredWidth: XsStyleSheet.secondaryButtonStdWidth + Layout.fillHeight: true + imgSrc: "qrc:/icons/rotate-ccw.svg" + imageDiv.sourceSize.width: 14 + imageDiv.sourceSize.height: 14 + forcedHover: textMA.containsMouse + + onClicked: { + // 'value' and 'default_value' exposed from the model used + // to instantiate the wheel + var _value = value + _value[0] = default_value[0] + _value[1] = default_value[1] + _value[2] = default_value[2] + _value[3] = default_value[3] + value = _value + } + } + Item{ + Layout.fillWidth: true + Layout.fillHeight: true + } + } + + RowLayout{ id: controlsDiv + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 1 + + property real sideMargin: [ (wheelItem.width - (2*2)) / 3 + 2 ] /8 + + Item{ + Layout.fillWidth: true + Layout.minimumWidth: 1 + Layout.maximumWidth: controlsDiv.sideMargin + Layout.fillHeight: true + } + + Loader{ id: wheelDiv + property int defaultWheelSize: width + + Layout.fillWidth: true + Layout.fillHeight: true + + sourceComponent: GTWheelDivHorz{ anchors.fill: wheelDiv } + } + + Item{ + Layout.fillWidth: true + Layout.minimumWidth: 1 + Layout.preferredWidth: 2 + Layout.maximumWidth: 4 + Layout.fillHeight: true + } + + ColumnLayout { id: sliderDiv + Layout.minimumWidth: 45 + Layout.preferredWidth: 45 + Layout.maximumWidth: 45 + Layout.preferredHeight: parent.height + spacing: 1 + + Item{ + Layout.fillWidth: true + Layout.minimumHeight: 2 + } + + Item{ + Layout.fillWidth: true + Layout.fillHeight: true + + GTSlider{ + id: slider + height: parent.height + anchors.horizontalCenter: parent.horizontalCenter + backend_value: value[3] + default_backend_value: default_value[3] + from: float_scrub_min[3] + to: float_scrub_max[3] + step: float_scrub_step[3] + + onSetValue: (newVal) => { + var _value = value + _value[3] = newVal + value = _value + } + } + } + + Item{ + Layout.fillWidth: true + Layout.minimumHeight: XsStyleSheet.widgetStdHeight + + GTValueEditor{ + width: parent.width + height: parent.height + valueText: value[3].toFixed(3) + indicatorColor: "transparent" + + onEdited:{ + var _value = value + _value[3] = parseFloat(currentText) + value = _value + } + } + } + + } + + Item{ + Layout.fillWidth: true + Layout.minimumWidth: 1 + Layout.maximumWidth: controlsDiv.sideMargin + Layout.fillHeight: true + } + + } + + } + + +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/grading.qrc b/src/plugin/colour_op/grading/src/qml/Grading.2/grading.qrc new file mode 100644 index 000000000..40f57b34c --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/grading.qrc @@ -0,0 +1,14 @@ + + + icons/brightness_low.svg + icons/brightness_high.svg + icons/hexagon.svg + icons/mask_domino.svg + icons/invert_colors.svg + + icons/wheel_256px.png + + icons/all_inclusive.svg + icons/step_into.svg + + \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/icons/all_inclusive.svg b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/all_inclusive.svg new file mode 100644 index 000000000..10463bf64 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/all_inclusive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/icons/brightness_high.svg b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/brightness_high.svg new file mode 100644 index 000000000..5aa286a32 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/brightness_high.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/icons/brightness_low.svg b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/brightness_low.svg new file mode 100644 index 000000000..ddbe68844 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/brightness_low.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/icons/hexagon.svg b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/hexagon.svg new file mode 100644 index 000000000..fe2f9d2f3 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/hexagon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/icons/invert_colors.svg b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/invert_colors.svg new file mode 100644 index 000000000..0d6a9c438 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/invert_colors.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/icons/mask_domino.svg b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/mask_domino.svg new file mode 100644 index 000000000..52136f729 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/mask_domino.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/icons/step_into.svg b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/step_into.svg new file mode 100644 index 000000000..bb7a1d450 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/step_into.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/icons/wheel_256px.png b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/wheel_256px.png new file mode 100644 index 000000000..2daf79433 Binary files /dev/null and b/src/plugin/colour_op/grading/src/qml/Grading.2/icons/wheel_256px.png differ diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/overlays/MEllipse.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/overlays/MEllipse.qml new file mode 100644 index 000000000..39b4a009b --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/overlays/MEllipse.qml @@ -0,0 +1,284 @@ +import QtQuick +import QtQuick.Shapes 1.6 + +import "VecLib.js" as VL + +Item { + id: root + + property var id + property var canvas + property real viewScale: 1 + // Qt's shape don't support drawing with sub pixel coordinates very well. + // We provide an arbitrary scale here that can applied externally. + property real drawScale: 200 + property real strokeWidth: 2 / viewScale * drawScale + property real handleSize: 8 / viewScale * drawScale + property real handleDetectSize: 10 / viewScale + property real handleRadius: 5 / viewScale * drawScale + property var handleColor: interacting ? "lime" : "red" + property bool interacting: activeShape == modelIndex + readonly property var shape: shape + readonly property var shapePath: sp + + + transform: [ + Scale { xScale: 1.0 / root.drawScale; yScale: 1.0 / root.drawScale } + ] + + Shape { + id: shape + + ShapePath { + id: sp + strokeColor: "gray" + strokeWidth: root.strokeWidth + fillColor: "transparent" + + property var center: Qt.point( + modelValue.center[2] * drawScale, + modelValue.center[3] * drawScale * -1.0) + property var radius: Qt.point( + modelValue.radius[2] * drawScale, + modelValue.radius[3] * drawScale) + property real angle: modelValue.angle + + // Derived properties for convenience + property var left: VL.rotateVec(Qt.point(center.x - radius.x, center.y), center, angle) + property var top: VL.rotateVec(Qt.point(center.x, center.y - radius.y), center, angle) + property var right: VL.rotateVec(Qt.point(center.x + radius.x, center.y), center, angle) + property var bottom: VL.rotateVec(Qt.point(center.x, center.y + radius.y), center, angle) + + // Used during communication to backend + readonly property var points: [center, radius, Qt.point(angle, angle)] + + // Use PathArc as PathAngleArc doesn't seem to support rotation + startX: left.x + startY: left.y + + PathArc { + direction : PathArc.Clockwise + radiusX : sp.radius.x + radiusY : sp.radius.y + useLargeArc : false + x : sp.right.x + y : sp.right.y + xAxisRotation : sp.angle + } + + PathArc { + direction : PathArc.Clockwise + radiusX : sp.radius.x + radiusY : sp.radius.y + useLargeArc : false + x : sp.left.x + y : sp.left.y + xAxisRotation : sp.angle + } + + } + } + + function mouseReleased(buttons) { + if (hovered_object) { + attrs.interacting = false + hovered_object = undefined + } + } + + property var hovered_object + + function mouseMoved(mousePosition, buttons, modifiers) { + + if (hovered_object && buttons == 1) { + + hovered_object.drag(mousePosition, modifiers) + + } else if (buttons == 0) { + + // mouse over/hover logic + var nh = undefined + var dist_to_handle = handleDetectSize + for (var i = 0; i < handles.children.length; ++i) { + + var d = VL.subtractVec( + Qt.point(mousePosition.x, -mousePosition.y), + VL.scaleVec(handles.children[i].pos,1.0/drawScale) + ) + var l = VL.lengthVec(d) + if (l < dist_to_handle) { + dist_to_handle = l + nh = handles.children[i] + } + } + + if (nh != hovered_object) { + hovered_object = nh + } + + } + } + + Item { + + id: handles + + Rectangle { + id: centre_handle + property bool hovered: hovered_object == centre_handle + property var pos: sp.center + x: pos.x - width / 2 + y: pos.y - height / 2 + radius: root.handleRadius + color: hovered ? "yellow" : root.handleColor + width: root.handleSize + height: root.handleSize + function drag(position, modifiers) { + var v = modelValue; + v.center[2] = position.x; + v.center[3] = position.y; + updateModelValue(v); + } + function dragstart(position, modifiers) {} + } + + Rectangle { + id: right_handle + property bool hovered: hovered_object == right_handle + property var pos: sp.right + x: pos.x - width / 2 + y: pos.y - height / 2 + radius: root.handleRadius + color: hovered ? "yellow" : root.handleColor + width: root.handleSize + height: root.handleSize + property var ref_point + function dragstart(position, modifiers) { + + var rd = VL.rotateVec(Qt.point(-modelValue.radius[2], 0), Qt.point(0, 0), sp.angle) + ref_point = {"A": VL.addVec(Qt.point(modelValue.center[2], modelValue.center[3]), rd), + "B": Qt.point(modelValue.center[2], modelValue.center[3])} + } + + function drag(position, modifiers) { + + var v = modelValue; + var d = VL.subtractVec(position, ((modifiers & Qt.ShiftModifier) ? ref_point.A : ref_point.B)); + var norm = VL.rotateVec(Qt.point(1, 0), Qt.point(0, 0), sp.angle); + var fac = Math.max(VL.dotProduct(d, norm), 0.001); + if (modifiers & Qt.ShiftModifier) { + + v.radius[2] = fac*0.5 + v.center[2] = ref_point.A.x+VL.scaleVec(norm, -fac*0.5).x + v.center[3] = ref_point.A.y+VL.scaleVec(norm, -fac*0.5).y + + } else { + + v.radius[2] = fac + + } + updateModelValue(v); + + } + } + + Rectangle { + id: top_handle + property bool hovered: hovered_object == top_handle + property var pos: sp.top + x: pos.x - width / 2 + y: pos.y - height / 2 + radius: root.handleRadius + color: hovered ? "yellow" : root.handleColor + width: root.handleSize + height: root.handleSize + property var ref_point + function dragstart(position, modifiers) { + + var rd = VL.rotateVec(Qt.point(0, modelValue.radius[3]), Qt.point(0, 0), sp.angle) + ref_point = {"A": VL.addVec(Qt.point(modelValue.center[2], modelValue.center[3]), rd), + "B": Qt.point(modelValue.center[2], modelValue.center[3])} + } + + function drag(position, modifiers) { + + var v = modelValue; + var d = VL.subtractVec(position, ((modifiers & Qt.ShiftModifier) ? ref_point.A : ref_point.B)); + var norm = VL.rotateVec(Qt.point(0, -1), Qt.point(0, 0), sp.angle); + var fac = Math.max(VL.dotProduct(d, norm), 0.001); + if (modifiers & Qt.ShiftModifier) { + + v.radius[3] = fac*0.5 + v.center[2] = ref_point.A.x+VL.scaleVec(norm, fac*0.5).x + v.center[3] = ref_point.A.y+VL.scaleVec(norm, fac*0.5).y + + } else { + + v.radius[3] = fac + + } + updateModelValue(v); + + } + } + + Rectangle { + id: rotate_handle + property bool hovered: hovered_object == rotate_handle + property var pos: VL.addVec(sp.center, VL.scaleVec(VL.subtractVec(sp.right, sp.center), 0.5)) + x: pos.x - width / 2 + y: pos.y - height / 2 + radius: root.handleRadius + color: hovered ? "yellow" : root.handleColor + width: root.handleSize + height: root.handleSize + function dragstart(position, modifiers) {} + function drag(position, modifiers) { + + var v = modelValue; + var centre = Qt.point(v.center[2], v.center[3]) + var d = VL.subtractVec(position, centre); + v.angle = (Math.atan2(d.y, d.x) * 180) / Math.PI + updateModelValue(v); + + } + } + } + + function mousePressed(position, buttons, modifiers) { + if (hovered_object && buttons == 1) { + attrs.interacting = true + set_interaction_shape(modelIndex) + hovered_object.dragstart(position, modifiers) + } + } + + function mouseDoubleClicked(mousePosition, buttons, modifiers) { + } + + // Rotation handle + Item { + id: rr_rotation + + property bool hovering: false + property bool interacting: false + + x: sp.center.x + y: sp.center.y - height / 2 + + width: sp.radius.x / 2 + height: root.handleDetectSize / 2 + transformOrigin: Item.Left + rotation: sp.angle + + Rectangle { + anchors.centerIn: parent + width: sp.radius.x / 2 + height: root.handleSize / 2 + radius: root.handleRadius / 4 + color: rr_rotation.hovering || interacting ? "yellow" : root.handleColor + } + + } + +} diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/overlays/MPolygon.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/overlays/MPolygon.qml new file mode 100644 index 000000000..6509ee776 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/overlays/MPolygon.qml @@ -0,0 +1,228 @@ +import QtQuick +import QtQuick.Shapes 1.6 + +import "VecLib.js" as VL + +Item { + id: root + property var canvas + property real viewScale: 1 + property real strokeWidth: 2 / viewScale + property real handleDetectSize: 10 / viewScale + property real handleSize: 8 / viewScale + property real handleRadius: 5 / viewScale + property var handleColor: interacting ? "lime" : "red" + readonly property var shape: shape + readonly property var shapePath: sp + property bool under_construction: false + property bool interacting: activeShape == modelIndex + + function addPoint(pt) { + + var rpt = Qt.point(pt.x, -pt.y) + var segmentsDist = 0.5 + var idx = -1 + var n_segments = loop_points.length-1 + for (var i = 0; i < n_segments; ++i) { + var r = VL.distToSegment2( + loop_points[i], + loop_points[i+1], + rpt) + if (r < segmentsDist) { + segmentsDist = r + idx = i; + } + } + if (idx != -1) { + + var vv = modelValue + vv.points.splice(idx+1, 0, [ + "vec2", + "1", + pt.x, + pt.y + ]) + updateModelValue(vv) + + } + + } + + + function removePoint(idx) { + // Polygon needs 3 edges minimum + if (modelValue.points.length >= 4) { + var vv = modelValue + vv.points.splice(idx, 1) + updateModelValue(vv) + } + + } + + property var points: [] + property var loop_points: [] + property var mv: modelValue.points + onMvChanged: updatePoints() + + function updatePoints() { + var pp = [] + for (var i = 0; i < modelValue.points.length; ++i) { + pp.push(Qt.point(modelValue.points[i][2], -modelValue.points[i][3])) + } + + if (!under_construction && pp.length) { + + // add centre point + var p = VL.centerOfPoints(pp) + pp.push(p) + points = pp + // have to avoid copy by reference here as we manipulate pp for loop_points + var pp2 = [] + for (var i = 0; i < pp.length; ++i) pp2.push(pp[i]); + if (pp2.length) { + // for the shape path loop, replace centre point with + // the first point + pp2[pp2.length-1] = pp2[0] + } + loop_points = pp2 + + } else { + + points = pp + loop_points = pp + + } + + } + + Shape { + id: shape + + ShapePath { + id: sp + strokeColor: "gray" + strokeWidth: root.strokeWidth + fillColor: "transparent" + + PathPolyline { + path: loop_points + } + + } + } + + property var under_mouse_pt_index + + function pointUnderMouseIndex(mousePosition) { + + var d = root.handleDetectSize + var idx = -1; + for (var i = 0; i < points.length; ++i) { + var dx = points[i].x-mousePosition.x + var dy = points[i].y+mousePosition.y + var dist = Math.sqrt(dx*dx + dy*dy); + if (dist < d) { + d = dist + idx = i + } + + } + return idx + } + + function mouseReleased(buttons) { + under_mouse_pt_index = -1 + } + + property var drag_start + property var points_before_drag + + function mouseMoved(mousePosition, buttons, modifiers) { + + if (buttons != 1) { + under_mouse_pt_index = pointUnderMouseIndex(mousePosition) + } else if (under_mouse_pt_index != -1 && !under_construction) { + var delta = Qt.point(mousePosition.x-drag_start.x, mousePosition.y-drag_start.y) + var vv = modelValue + if (under_mouse_pt_index == vv.points.length || (modifiers & Qt.AltModifier)) { + // center point / Alt drag to move all points + for (var i = 0; i < vv.points.length; ++i) { + vv.points[i] = [ + "vec2", + "1", + points_before_drag[i].x + delta.x, + -points_before_drag[i].y + delta.y + ] + } + updateModelValue(vv) + } else { + vv.points[under_mouse_pt_index] = [ + "vec2", + "1", + points_before_drag[under_mouse_pt_index].x + delta.x, + -points_before_drag[under_mouse_pt_index].y + delta.y + ] + updateModelValue(vv) + } + } + } + + function mousePressed(position, buttons, modifiers) { + + if (!under_construction) { + if (under_mouse_pt_index == -1) return + attrs.interacting = true + set_interaction_shape(modelIndex) + if ((modifiers & Qt.ShiftModifier) && under_mouse_pt_index < modelValue.points.length) { + removePoint(under_mouse_pt_index) + } else { + points_before_drag = points + drag_start = position + } + return true + } else { + if (under_mouse_pt_index == 0 && modelValue.points.length >= 3) { + finalizePolygon() + return true + } else if (under_mouse_pt_index == -1) { + // add a point + var vv = modelValue + vv.points.push([ + "vec2", + "1", + position.x, + position.y]) + updateModelValue(vv) + updatePoints() + return true; + } + } + return false + } + + function mouseDoubleClicked(mousePosition, buttons, modifiers) { + if (under_mouse_pt_index != -1) { + removePoint(under_mouse_pt_index) + } else { + addPoint(mousePosition) + } + } + + Repeater { + + model: points + + Rectangle { + + property bool hovering: under_mouse_pt_index == index + x: points[index].x-width/2 + y: points[index].y-width/2 + radius: width/2 + color: hovering ? "yellow" : root.handleColor + width: root.handleSize + height: root.handleSize + + } + } + +} diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/overlays/VecLib.js b/src/plugin/colour_op/grading/src/qml/Grading.2/overlays/VecLib.js new file mode 100644 index 000000000..ced491cf0 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/overlays/VecLib.js @@ -0,0 +1,163 @@ + +// Below operates mainly on Qt.point type but should also be +// compatible with Qt.vector2d to some extend + +function toVector2d(v1) { + return Qt.vector2d(v1.x, v1.y); +} + +function addVec(v1, v2) { + // Accounts for Qt top -> down Y axis + return Qt.point(v1.x + v2.x, v1.y - v2.y); +} + +function subtractVec(v1, v2) { + // Accounts for Qt top -> down Y axis + return Qt.point(v1.x - v2.x, v2.y - v1.y); +} + +function scaleVec(v1, s) { + return Qt.point(v1.x * s, v1.y * s); +} + +function dotProduct(v1, v2) { + return v1.x * v2.x + v1.y * v2.y; +} + +function crossProduct(v1, v2) { + return v1.x * v2.y - v2.x * v1.y; +} + +function normVec(v) { + var norm = lengthVec(v); + return Qt.point(v.x / norm, v.y / norm); +} + +function lengthVec(v) { + return Math.sqrt(v.x*v.x + v.y*v.y); +} + +function checkConvex(pt, prevprev, prev, next, nextnext) { + // Check point crossing prev (clockwise) side (extended) + var ptOrig = subtractVec(pt, prev); + var sideDir = normVec(subtractVec(prev, prevprev)); + var cross = crossProduct(normVec(ptOrig), sideDir); + + if (cross < 0) { + var t = dotProduct(ptOrig, sideDir); + pt.x = prev.x + sideDir.x * t; + pt.y = prev.y - sideDir.y * t; + } + + // Check point crossing next (clockwise) side (extended) + var ptOrig = subtractVec(pt, next); + var sideDir = normVec(subtractVec(next, nextnext)); + var cross = crossProduct(normVec(ptOrig), sideDir); + + if (cross >= 0) { + var t = dotProduct(ptOrig, sideDir); + pt.x = next.x + sideDir.x * t; + pt.y = next.y - sideDir.y * t; + } + + // Check point crossing opposite triangle boundary + var ptOrig = subtractVec(pt, next); + var sideDir = normVec(subtractVec(prev, next)); + var cross = crossProduct(normVec(ptOrig), sideDir); + + if (cross < 0) { + var t = dotProduct(ptOrig, sideDir); + pt.x = next.x + sideDir.x * t; + pt.y = next.y - sideDir.y * t; + } + + return pt; +} + +function extendSides(pt, prevprev, prev, curr, next, nextnext, delta) { + // Compute prev point position + var prevDir = normVec(subtractVec(prev, prevprev)); + var tOrig = dotProduct(prevDir, subtractVec(curr, prevprev)); + var tNew = dotProduct(prevDir, subtractVec(pt, prevprev)); + var lengthOrig = lengthVec(subtractVec(prev, prevprev)); + var t = lengthOrig + (tNew - tOrig); + + // Prevent collapse of prev and prevprev points position + if (t <= delta) { + t = delta; + var sideDir = normVec(subtractVec(curr, prev)); + var t2 = dotProduct(sideDir, subtractVec(pt, prevprev)); + pt.x = prevprev.x + prevDir.x * t + sideDir.x * t2; + pt.y = prevprev.y - prevDir.y * t - sideDir.y * t2; + } + + var newPrev = Qt.point( + prevprev.x + prevDir.x * t, + prevprev.y - prevDir.y * t + ); + + // Compute next point position + var nextDir = normVec(subtractVec(next, prevprev)); + var tOrig = dotProduct(nextDir, subtractVec(curr, prevprev)); + var tNew = dotProduct(nextDir, subtractVec(pt, prevprev)); + var lengthOrig = lengthVec(subtractVec(next, prevprev)); + var t = lengthOrig + (tNew - tOrig); + + // Prevent collapse of next and prevprev points position + if (t <= delta) { + t = delta; + var sideDir = normVec(subtractVec(curr, next)); + var t2 = dotProduct(sideDir, subtractVec(pt, prevprev)); + pt.x = prevprev.x + prevDir.x * t + sideDir.x * t2; + pt.y = prevprev.y - prevDir.y * t - sideDir.y * t2; + } + + var newNext = Qt.point( + prevprev.x + nextDir.x * t, + prevprev.y - nextDir.y * t + ); + + return [newPrev, newNext]; +} + +function centerOfPoints(points) { + var mean_x = 0.0; + var mean_y = 0.0; + for (var i = 0; i < points.length; i++) { + mean_x += points[i].x; + mean_y += points[i].y; + } + mean_x /= points.length; + mean_y /= points.length; + return Qt.point(mean_x, mean_y); +} + +function rotateVec(v, origin, angle) { + // TODO: Could do this manually instead of using Qt.matrix + var m = Qt.matrix4x4(); + m.rotate(angle, Qt.vector3d(0,0,1)); + + var v3d = toVector2d(v).toVector3d(); + var o3d = toVector2d(origin).toVector3d(); + + v3d = v3d.minus(o3d); + v3d = m.times(v3d); + v3d = v3d.plus(o3d); + + return Qt.point(v3d.x, v3d.y); +} + +// From https://stackoverflow.com/a/1501725 +function sqr(x) { return x * x } +function dist2(a, b) { return sqr(a.x - b.x) + sqr(a.y - b.y) } +function distToSegment2(a, b, pt) { + var l2 = dist2(a, b); + // Distance to point + if (l2 == 0) return dist2(pt, a); + // Project point onto the line segment (dot product(pt - a, b - a)), normalize by segment length + var t = ((pt.x - a.x) * (b.x - a.x) + (pt.y - a.y) * (b.y - a.y)) / l2; + // Ignore projections outside the segment + t = Math.max(0, Math.min(1, t)); + // Distance from point to projection on segment (v + t (b - a)) + return dist2(pt, { x: a.x + t * (b.x - a.x), y: a.y + t * (b.y - a.y) }); +} diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/qmldir b/src/plugin/colour_op/grading/src/qml/Grading.2/qmldir new file mode 100644 index 000000000..84c7c0b84 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/qmldir @@ -0,0 +1,26 @@ +module Grading + +GradingTools 2.0 GradingTools.qml +GradingOverlay 2.0 GradingOverlay.qml +GTAttributes 2.0 GTAttributes.qml +MAttributes 2.0 MAttributes.qml + +Sec0Menu 2.0 sections/Sec0Menu.qml +Sec1Header 2.0 sections/Sec1Header.qml +Sec2LayerList 2.0 sections/Sec2LayerList.qml +Sec3MaskTools 2.0 sections/Sec3MaskTools.qml + +GTSliderDiv 2.0 delegates/GTSliderDiv.qml +GTSliderItem 2.0 delegates/GTSliderItem.qml +GTWheelItem 2.0 delegates/GTWheelItem.qml +GTWheelDivHorz 2.0 delegates/GTWheelDivHorz.qml +GTWheelDivVert 2.0 delegates/GTWheelDivVert.qml + +GTSlider 2.0 widgets/GTSlider.qml +GTToolButtonHorz 2.0 widgets/GTToolButtonHorz.qml +GTValueEditor 2.0 widgets/GTValueEditor.qml +GTWheel 2.0 widgets/GTWheel.qml + +MEllipse 2.0 overlays/MEllipse.qml +MQuad 2.0 overlays/MQuad.qml +MPolygon 2.0 overlays/MPolygon.qml diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec0Menu.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec0Menu.qml new file mode 100644 index 000000000..5e0352d44 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec0Menu.qml @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQuick.Dialogs + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.clipboard 1.0 + +Item{ id: menuDiv + + + property var copy_buffer: [] + property alias moreMenu: moreMenu + + + Clipboard{ + id: clipboard + } + + + XsPopupMenu { + id: moreMenu + visible: false + menu_model_name: "moreMenu"+menuDiv + } + + XsMenuModelItem { + menuItemType: "radiogroup" + choices: attrs.media_colour_managed ? ["scene_linear", "compositing_log"] : ["raw"] + + property string currentColorSpace: attrs.media_colour_managed ? attrs.colour_space : "raw" + + currentChoice: currentColorSpace + onCurrentChoiceChanged: { + if (currentChoice != "raw") + attrs.colour_space = currentChoice + } + + enabled: hasActiveGrade() && attrs.media_colour_managed + text: "" + menuPath: "Color Space" + menuItemPosition: 1 + menuModelName: moreMenu.menu_model_name + } + XsMenuModelItem { + menuItemType: "divider" + menuPath: "" + menuItemPosition: 2 + menuModelName: moreMenu.menu_model_name + } + + XsMenuModelItem { + text: "Rename..." + enabled: false + menuPath: "" + menuItemPosition: 3 + menuModelName: moreMenu.menu_model_name + onActivated: {} + } + XsMenuModelItem { + menuItemType: "divider" + menuPath: "" + menuItemPosition: 4 + menuModelName: moreMenu.menu_model_name + } + XsMenuModelItem { + text: "Copy" + enabled: hasActiveGrade() + menuPath: "" + menuItemPosition: 5 + menuModelName: moreMenu.menu_model_name + onActivated: { + copyFunction() + } + } + XsMenuModelItem { + text: "Paste" + enabled: copy_buffer.length == (grading_sliders_model.length + grading_wheels_model.length) + menuPath: "" + menuItemPosition: 6 + menuModelName: moreMenu.menu_model_name + onActivated: { + pasteFunction(); + } + } + XsMenuModelItem { + text: "Reset" + enabled: hasActiveGrade() + menuPath: "" + menuItemPosition: 6.5 + menuModelName: moreMenu.menu_model_name + onActivated: { + attrs.grading_action = "Clear" + } + } + XsMenuModelItem { + menuItemType: "divider" + menuPath: "" + menuItemPosition: 7 + menuModelName: moreMenu.menu_model_name + } + XsMenuModelItem { + text: "Copy Nuke Node" + menuPath: "" + menuItemPosition: 8 + menuModelName: moreMenu.menu_model_name + onActivated: { + copyNukeNode(); + } + } + + function saveCDL(fileUrl, folder) { + var path = fileUrl.toString() + if (!path.endsWith(".cdl") && !path.endsWith(".cc") && !path.endsWith(".ccc")) { + path += ".cdl" + } + attrs.grading_action = "Save CDL " + path + } + + XsMenuModelItem { + text: "Save CDL..." + menuPath: "" + menuItemPosition: 9 + menuModelName: moreMenu.menu_model_name + onActivated: { + dialogHelpers.showFileDialog( + menuDiv.saveCDL, + file_functions.defaultSessionFolder(), + "Save CDL", + "cdl", + [ "CDL files (*.cdl)", "CC files (*.cc)", "CCC files (*.ccc)" ], + false, + false + ) + } + } + + function copyFunction() { + var attr_values = [] + for (var i = 0; i < grading_sliders_model.length; ++i) { + attr_values.push(grading_sliders_model.get(grading_sliders_model.index(i,0),"value")) + } + for (var i = 0; i < grading_wheels_model.length; ++i) { + attr_values.push(grading_wheels_model.get(grading_wheels_model.index(i,0),"value")) + } + copy_buffer = attr_values + } + + function pasteFunction() { + for (var i = 0; i < grading_sliders_model.length; ++i) { + grading_sliders_model.set( + grading_sliders_model.index(i,0), + copy_buffer[i], + "value" + ) + } + for (var i = 0; i < grading_wheels_model.length; ++i) { + grading_wheels_model.set( + grading_wheels_model.index(i,0), + copy_buffer[grading_sliders_model.length + i], + "value" + ) + } + } + + function copyNukeNode() { + + // TODO: ColSci + // Use Grade node instead of OCIOCDLTransform to handle contrast? + + var offset = attrs.getAttrValue("Offset") + var power = attrs.getAttrValue("Power") + var slope = attrs.getAttrValue("Slope") + var sat = attrs.getAttrValue("Saturation") + var exp = attrs.getAttrValue("Exposure") + var cont = attrs.getAttrValue("Contrast") + + var cdl_node = "OCIOCDLTransform {\n" + if (attrs.colour_space != "scene_linear") { + cdl_node += " working_space " + attrs.colour_space + "\n" + } + cdl_node += " slope { " + cdl_node += (slope[0] * slope[3] * Math.pow(2.0, exp)) + " " + cdl_node += (slope[1] * slope[3] * Math.pow(2.0, exp)) + " " + cdl_node += (slope[2] * slope[3] * Math.pow(2.0, exp)) + " " + cdl_node += "}\n" + cdl_node += " offset { " + cdl_node += (offset[0] + offset[3]) + " " + cdl_node += (offset[1] + offset[3]) + " " + cdl_node += (offset[2] + offset[3]) + " " + cdl_node += "}\n" + cdl_node += " power { " + cdl_node += (power[0] * power[3]) + " " + cdl_node += (power[1] * power[3]) + " " + cdl_node += (power[2] * power[3]) + " " + cdl_node += "}\n" + cdl_node += " saturation " + sat + "\n" + cdl_node += "}" + + clipboard.text = cdl_node + } + + +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec1Header.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec1Header.qml new file mode 100644 index 000000000..64538ab1c --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec1Header.qml @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 + +Item{ id: toolDiv + + property real categoryBtnWidth: btnWidth * 1.3 + + RowLayout{ + spacing: buttonSpacing + width: parent.width + height: btnHeight + anchors.centerIn: parent + + XsPrimaryButton{ id: addBtn + Layout.preferredWidth: btnWidth + Layout.preferredHeight: btnHeight + Layout.alignment: Qt.AlignLeft + imgSrc: "qrc:/icons/add.svg" + //tooltip: "Add a color correction" + onClicked: { + attrs.grading_action = "Add CC" + } + } + XsPrimaryButton{ id: deleteBtn + Layout.preferredWidth: btnWidth + Layout.preferredHeight: btnHeight + imgSrc: "qrc:/icons/delete.svg" + //tooltip: "Remove the currently selected color correction" + enabled: bookmarkList.count > 0 + onClicked: { + attrs.grading_action = "Remove CC" + } + } + Item{ + Layout.fillWidth: true + Layout.minimumWidth: 0//btnWidth + Layout.preferredHeight: btnHeight + } + XsPrimaryButton{ id: hideBtn + Layout.preferredWidth: btnWidth*2 + Layout.preferredHeight: btnHeight + text: "Hide Shapes" + imgSrc: "" + font.pixelSize: XsStyleSheet.fontSize + font.family: XsStyleSheet.fontFamily + isActive: !mask_attrs.mask_shapes_visible + onClicked:{ + mask_attrs.mask_shapes_visible = !mask_attrs.mask_shapes_visible; + } + } + XsPrimaryButton{ id: bypassBtn + Layout.preferredWidth: btnWidth*1.5 + Layout.preferredHeight: btnHeight + text: "Bypass" + imgSrc: "" + font.pixelSize: XsStyleSheet.fontSize + font.family: XsStyleSheet.fontFamily + //tooltip: "Bypass all CDLs or not" + isActive: attrs.grading_bypass + onClicked: { + attrs.grading_bypass = !attrs.grading_bypass + } + toolTip: "Bypass all grades applied on all media " + hotkeyNameForTooltip: "Bypass all grades" + + } + + } + +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec2LayerList.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec2LayerList.qml new file mode 100644 index 000000000..5424bb321 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec2LayerList.qml @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts + +import xStudio 1.0 +import Grading 2.0 + +Item{ id: listDiv + + property alias bookmarkList: bookmarkList + + Rectangle{ + anchors.fill: parent + color: panelColor + + XsListView { id: bookmarkList + width: parent.width - x*2 + height: parent.height - y + x: itemSpacing + y: itemSpacing + model: bookmarkFilterModel + spacing: itemSpacing + + property bool userSelect: false + + ScrollBar.vertical: XsScrollBar { + visible: bookmarkList.height < bookmarkList.contentHeight + } + + property var curr_bookmark_id: helpers.QUuidFromUuidString(attrs.grading_bookmark) + + delegate: XsPrimaryButton { id: bookmark + width: bookmarkList.width + height: btnHeight * 1.1 + + isActive: isSelected + isActiveIndicatorAtLeft: true + activeIndicator.width: (1*3) * 3 + + property var uuid: uuidRole + + readonly property bool isSelected: uuid == bookmarkList.curr_bookmark_id + property bool isHovered: hovered + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: (mouse) => { + if (mouse.button == Qt.LeftButton && attrs.grading_bookmark != uuidRole){ + if (currentPlayhead.mediaFrame < startFrameRole || currentPlayhead.mediaFrame > endFrameRole) { + // jump to frame of grade + currentPlayhead.logicalFrame = currentPlayhead.logicalFrame + (startFrameRole-currentPlayhead.mediaFrame) + } + attrs.grading_bookmark = uuidRole + } + else if(mouse.button == Qt.RightButton){ + if(moreMenu.visible) moreMenu.visible = false + else{ + moreMenu.x = x + width + moreMenu.y = y + height + moreMenu.visible = true + } + } + } + } + + RowLayout{ + spacing: 0 + anchors.fill: parent + + XsText{ id: nameDiv + Layout.fillWidth: true + Layout.minimumWidth: 50 + Layout.preferredWidth: 100 + Layout.fillHeight: true + + text: layerUIName() + font.weight: isSelected? Font.Bold : Font.Normal + horizontalAlignment: Text.AlignLeft + leftPadding: bookmark.activeIndicator.width + 5 + elide: Text.ElideRight + + function layerUIName() { + var name = ""; + if (userDataRole.layer_name) { + name = userDataRole.layer_name + if (startFrameRole == endFrameRole) { + name += " - Frame " + startFrameRole + } + } else { + name = "Grade Layer " + (index+1) + } + + return name + } + } + Item{ id: maskDiv + Layout.preferredWidth: !maskBtn.visible? 0 : height * 1.2 + Layout.fillHeight: true + + XsSecondaryButton{ id: maskBtn + width: parent.width - 1 + height: parent.height - 4 + anchors.verticalCenter: parent.verticalCenter + + isActive: false + visible: userDataRole.mask_active + imgSrc: "qrc:/grading_icons/mask_domino.svg" + text: "Mask Active" + scale: 0.95 + imageSrcSize: 20 + hoverEnabled: false + enabled: false + onlyVisualyEnabled: true + } + } + Item{ id: rangeDiv + Layout.preferredWidth: height * 1.2 + Layout.fillHeight: true + + XsPrimaryButton{ id: rangeBtn + width: parent.width - 1 + height: parent.height - 4 + anchors.verticalCenter: parent.verticalCenter + + property bool isFullClip: startFrameRole != -1 && startFrameRole != endFrameRole + + isActiveViaIndicator: false + isActive: false + // enabled: hasActiveGrade() + imgSrc: isFullClip? "qrc:/grading_icons/all_inclusive.svg" : "qrc:/grading_icons/step_into.svg" + text: isFullClip? "Full Clip" : "Single Frame" + scale: 0.95 + + onClicked: { + if(!isFullClip){ + + attrs.grading_action = "Set Bookmark Full Range|" + uuidRole + + } else { + + attrs.grading_action = "Set Bookmark One Frame|" + uuidRole + "|" + currentPlayhead.mediaFrame + } + } + + } + } + Item{ id: visibilityDiv + Layout.preferredWidth: height * 1.2 + Layout.fillHeight: true + + XsPrimaryButton{ id: visibilityBtn + width: parent.width - 1 + height: parent.height - 4 + anchors.verticalCenter: parent.verticalCenter + + isActive: !userDataRole.grade_active + imgSrc: userDataRole.grade_active? "qrc:/icons/visibility.svg" : "qrc:/icons/visibility_off.svg" + isActiveViaIndicator: false + text: "Visibility" + scale: 0.95 + + onClicked: { + var tmp = userDataRole + tmp.grade_active = !tmp.grade_active + userDataRole = tmp + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec3MaskTools.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec3MaskTools.qml new file mode 100644 index 000000000..e3d18c454 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/sections/Sec3MaskTools.qml @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 +import Grading 2.0 + +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.bookmarks 1.0 + +Item{ + + property real maskBtnWidth: 100 + property string activeButton: "Polygon" + property real itemHeight: XsStyleSheet.widgetStdHeight + + ListModel{ id: mask1ToolsModel + ListElement{ + toolName: "Polygon" + toolImg: "qrc:/grading_icons/hexagon.svg" + } + ListElement{ + toolName: "Ellipse" + toolImg: "qrc:/icons/radio_button_unchecked.svg" + } + } + ListModel{ id: mask2ToolsModel + + ListElement{ + toolName: "Dodge" + toolImg: "qrc:/grading_icons/brightness_low.svg" + } + ListElement{ + toolName: "Burn" + toolImg: "qrc:/grading_icons/brightness_high.svg" + } + } + + RowLayout { + width: parent.width + height: btnHeight + anchors.centerIn: parent + spacing: buttonSpacing + + RowLayout { + spacing: buttonSpacing + Layout.preferredWidth: maskBtnWidth*2 + spacing + Layout.maximumWidth: maskBtnWidth*2 + spacing + Layout.preferredHeight: btnHeight + + Repeater{ + model: mask1ToolsModel + + GTToolButtonHorz{ + Layout.fillWidth: true + Layout.minimumWidth: maskBtnWidth/1.5 + Layout.preferredWidth: maskBtnWidth + Layout.maximumWidth: maskBtnWidth + Layout.preferredHeight: btnHeight + txt: toolName + src: toolImg + + Component.onCompleted: { + if (toolName == "Polygon") { + isActive = Qt.binding(function() { return mask_attrs.polygon_init }) + } + } + + onClicked: { + activeButton = toolName; + mask_attrs.mask_shapes_visible = true + + if (toolName == "Polygon") { + mask_attrs.polygon_init = !mask_attrs.polygon_init; + } else if (toolName == "Ellipse") { + mask_attrs.drawing_action = "Add ellipse"; + } + } + } + + } + + } + Item{ + // Layout.fillWidth: true + Layout.minimumWidth: 2 + Layout.maximumWidth: 2 + Layout.fillHeight: true + } + + RowLayout{ id: propertiesGrid + enabled: mask_attrs.mask_selected_shape >= 0 + spacing: buttonSpacing + + // Layout.fillWidth: true + Layout.preferredWidth: opacityBtn.width*3 + removeBtn.width + Layout.preferredHeight: btnHeight + + XsIntegerAttrControl { id: opacityBtn + Layout.minimumWidth: maskBtnWidth/2 + Layout.preferredWidth: maskBtnWidth + Layout.maximumWidth: maskBtnWidth + Layout.fillHeight: true + text: "Opacity" + attr_group_model: mask_attrs.model + attr_title: "Pen Opacity" + } + XsIntegerAttrControl { + Layout.minimumWidth: maskBtnWidth/2 + Layout.preferredWidth: maskBtnWidth + Layout.maximumWidth: maskBtnWidth + Layout.fillHeight: true + text: "Softness" + attr_group_model: mask_attrs.model + attr_title: "Pen Softness" + } + XsIntegerAttrControl { + Layout.minimumWidth: maskBtnWidth/2 + Layout.preferredWidth: maskBtnWidth + Layout.maximumWidth: maskBtnWidth + Layout.fillHeight: true + visible: activeButton == "Dodge" || activeButton == "Burn" + text: "Size" + attr_group_model: mask_attrs.model + attr_title: "Shapes Pen Size" + } + XsPrimaryButton{ id: invertBtn + Layout.fillWidth: true + Layout.minimumWidth: maskBtnWidth/2 + Layout.preferredWidth: maskBtnWidth + Layout.maximumWidth: maskBtnWidth + Layout.fillHeight: true + font.pixelSize: XsStyleSheet.fontSize + text: "Invert" + // imgSrc: "qrc:/grading_icons/invert_colors.svg" + isActive: mask_attrs.shape_invert + onClicked:{ + mask_attrs.shape_invert = !mask_attrs.shape_invert + } + } + Item{ + Layout.minimumWidth: 2 + Layout.maximumWidth: 2 + Layout.fillHeight: true + } + XsPrimaryButton{ id: removeBtn + Layout.fillWidth: true + Layout.minimumWidth: maskBtnWidth/4 + Layout.preferredWidth: maskBtnWidth/2 + Layout.maximumWidth: maskBtnWidth/2 + Layout.fillHeight: true + font.pixelSize: XsStyleSheet.fontSize + text: "Remove" + imgSrc: "qrc:/icons/delete.svg" + onClicked:{ + mask_attrs.drawing_action = "Remove shape" + } + } + } + + Item{ + Layout.fillWidth: true + Layout.minimumWidth: 2 + // Layout.maximumWidth: 2 + Layout.fillHeight: true + } + + } +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/widgets/GTSlider.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/widgets/GTSlider.qml new file mode 100644 index 000000000..0c6a452bc --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/widgets/GTSlider.qml @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick +import QtQuick.Layouts + +import xstudio.qml.bookmarks 1.0 +import xStudio 1.0 + +Item { + + id: root + width: 20 + height: parent.height - 5 + + // Initialize to an arbitrary non-zero value so that we get a changed + // event when the slider is constructed (eg. Offset slider maps + // 0.0 to 0.5 position). + property real backend_value: -1e6 + property real default_backend_value: (typeof default_value == "number") ? default_value : default_value[index] + property real from: (typeof float_scrub_min == "number") ? float_scrub_min : float_scrub_min[index] + property real to: (typeof float_scrub_max == "number") ? float_scrub_max : float_scrub_max[index] + property real step: (typeof float_scrub_step == "number") ? float_scrub_step : float_scrub_step[index] + + onBackend_valueChanged: { + if (!slider.pressed) { + slider.value = val_to_pos(backend_value) + } + } + + signal setValue(newVal: real) + + // Note this is a naive log scale, in case the min and max are not + // mirrored around mid, the derivate will not be continous at the + // mid point. + + function pos_to_val(v) { + var min = root.from + var mid = root.default_backend_value + var max = root.to + var steepness = 4 + + function lin_to_log(v) { + var log = Math.log + var antilog = Math.exp + return (antilog(v * steepness) - antilog(0.0)) / (antilog(1.0 * steepness) - antilog(0.0)) + } + + if (v < 0.5) { + return (1 - lin_to_log(1 - v * 2)) * (mid - min) + min + } else { + return lin_to_log((v - 0.5) * 2) * (max - mid) + mid + } + } + + function val_to_pos(v) { + var min = root.from + var mid = root.default_backend_value + var max = root.to + var steepness = 4 + + function log_to_lin(v) { + var log = Math.log + var antilog = Math.exp + return log(v * (antilog(1.0 * steepness) - antilog(0.0)) + antilog(0.0)) / steepness + } + + if (v < pos_to_val(0.5, min, mid, max, steepness)) { + return (1 - log_to_lin(1 - ((v - min) / (mid - min)))) / 2.0 + } else { + return log_to_lin((v - mid) / (max - mid)) / 2.0 + 0.5 + } + } + + Rectangle{ + y: slider.height/2 - 3 + anchors.right: slider.left + anchors.rightMargin: -5 + width: 10 + height: 2 + color: palette.base + } + Rectangle{ + y: slider.height/2 - 3 + anchors.left: slider.right + anchors.leftMargin: -5 + width: 10 + height: 2 + color: palette.base + } + + XsSlider { id: slider + anchors.fill: parent + from: 0.0 + to: 1.0 + stepSize: 0.01 + orientation: Qt.Vertical + focus: true + + fillColor: palette.base + + onValueChanged: { + if (pressed) { + root.setValue(pos_to_val(slider.value)) + } + } + + } + +} diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/widgets/GTToolButtonHorz.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/widgets/GTToolButtonHorz.qml new file mode 100644 index 000000000..2e3c4a627 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/widgets/GTToolButtonHorz.qml @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick + +import QtQuick.Layouts + +import xstudio.qml.bookmarks 1.0 + + + +import xStudio 1.0 + +XsPrimaryButton{ id: control + + property alias txt: txtDiv.text + property alias src: imgDiv.source + + font.pixelSize: XsStyleSheet.fontSize + + RowLayout{ + width: parent.width + height: 16 + anchors.verticalCenter: parent.verticalCenter + anchors.margins: 1 + spacing: 2 + opacity: parent.enabled || parent.isUnClickable? 1.0 : 0.33 + + Item{ + Layout.fillWidth: true + Layout.fillHeight: true + } + XsIcon { id: imgDiv + Layout.maximumWidth: parent.height + Layout.preferredWidth: parent.height + Layout.fillHeight: true + antialiasing: true + smooth: true + imgOverlayColor: palette.text + source: "" + } + XsText { id: txtDiv + Layout.preferredWidth: textWidth + Layout.fillHeight: true + text: "" + font: control.font + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + Item{ + Layout.fillWidth: true + Layout.fillHeight: true + } + + } + + onClicked: { + } +} diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/widgets/GTValueEditor.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/widgets/GTValueEditor.qml new file mode 100644 index 000000000..8db144c27 --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/widgets/GTValueEditor.qml @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 +import xstudio.qml.bookmarks 1.0 + +Item { id: widget + + property string valueText: "" + property alias currentText: textField.text + property bool isHovered: mArea.containsMouse + property bool isPressed: false + + property color bgColorNormal: XsStyleSheet.widgetBgNormalColor + property color bgColorPressed: palette.highlight + property color indicatorColor: "transparent" + property real borderWidth: 1 + property color borderColorHovered: bgColorPressed + property color borderColorNormal: "transparent" + + signal edited() + + XsGradientRectangle { id: bgDiv + + anchors.fill: parent + border.color: widget.down || widget.isHovered ? borderColorHovered: borderColorNormal + border.width: borderWidth + + opacity: enabled? 1.0 : 0.33 + flatColor: topColor + topColor: isPressed? bgColorPressed: bgColorNormal + bottomColor: isPressed? bgColorPressed: bgColorNormal + + Rectangle{ id: activeIndicator + anchors.bottom: parent.bottom + width: borderWidth*5 + height: parent.height + color: indicatorColor + } + } + + MouseArea{ id: mArea + anchors.fill: parent + hoverEnabled: true + } + + XsTextField{ id: textField + width: parent.width + height: parent.height + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + text: valueText + font.bold: true + + bgColorNormal: "transparent" + borderColor: bgColorNormal + // validator: DoubleValidator { //to fix support for GTSliderItem & GTWheelItem + // bottom: float_scrub_min[index] + // } + onEditingCompleted: { + edited() + } + } + +} diff --git a/src/plugin/colour_op/grading/src/qml/Grading.2/widgets/GTWheel.qml b/src/plugin/colour_op/grading/src/qml/Grading.2/widgets/GTWheel.qml new file mode 100644 index 000000000..65de5e0ce --- /dev/null +++ b/src/plugin/colour_op/grading/src/qml/Grading.2/widgets/GTWheel.qml @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts + +import xStudio 1.0 + +Control { id: wheel + + property real wheelSize: 0 //135 + onWheelSizeChanged:{ + update_frontend() + } + property int radius: wheelSize / 2 + onRadiusChanged:{ + update_frontend() + } + property int center: wheelSize / 2 + onCenterChanged:{ + update_frontend() + } + property real ring_rel_size: 0.1 + property real cursor_width: 17 //wheelSize/8 > 17 ? 17 : wheelSize/8 + property bool interacting: cArea.pressed || mArea.pressed + + property real local_value: 1.0 + property real saturation: 1.0 + readonly property vector4d default_color: Qt.vector4d(1.0, 1.0, 1.0, 1.0) + + // "Front end" color value + property vector4d color: default_color + property var backend_color: value + + onBackend_colorChanged: { + if (!interacting) { + update_frontend() + } + } + + function update_frontend() { + color = val_to_pos_color(backend_color) + var pos = rgb_to_pos(color) + cDrag.x = center + pos.x * radius + cDrag.y = center - pos.y * radius + } + + function update_backend() { + var color_out = pos_to_val_color(color) + var v = value + v[0] = color_out.x + v[1] = color_out.y + v[2] = color_out.z + value = v + } + + function update_backend_from_cursor() { + var cursor_pos = Qt.vector2d(cursor.x, cursor.y) + var offset = Qt.vector2d(cursor.width / 2, cursor.height / 2) + var pos = cursor_pos.plus(offset) + + // Hue angle normalised [0,1] + var hue = Math.atan2( + pos.x - center, + pos.y - center) + hue = hue / (2 * Math.PI) + 0.5 + // Distance from center normalised [0,1] + var dist = Math.hypot( + pos.x - center, + pos.y - center) + dist /= radius + + var hsv = Qt.vector3d(hue, 1.0, dist) + var rgb = hsv_to_rgb(hsv) + color = Qt.vector4d(rgb.x, rgb.y, rgb.z, 1.0) + + update_backend() + } + + + function clamp(number, min, max) { + return Math.max(min, Math.min(number, max)) + } + + function clamp_v4d(v) { + return Qt.vector4d( + clamp(v.x, 0.0, 1.0), + clamp(v.y, 0.0, 1.0), + clamp(v.z, 0.0, 1.0), + clamp(v.w, 0.0, 1.0) + ) + } + + function v4d_to_color(v) { + return Qt.rgba(v.x, v.y, v.z, v.w) + } + + function color_to_v4d(c) { + return Qt.vector4d(c.r, c.g, c.b, c.a) + } + + // Note this is a naive log scale, in case the min and max are not + // mirrored around mid, the derivate will not be continous at the + // mid point. + + // Colour wheels only support adding / scaling up values. + + function pos_to_val(v, idx) { + var min = default_value[idx] + var max = float_scrub_max[idx] + var steepness = 4 + + function lin_to_log(v) { + var log = Math.log + var antilog = Math.exp + return (antilog(v * steepness) - antilog(0.0)) / (antilog(1.0 * steepness) - antilog(0.0)) + } + + return lin_to_log(v) * (max - min) + min + } + + function val_to_pos(v, idx) { + var min = default_value[idx] + var max = float_scrub_max[idx] + var steepness = 4 + + function log_to_lin(v) { + var log = Math.log + var antilog = Math.exp + return log(v * (antilog(1.0 * steepness) - antilog(0.0)) + antilog(0.0)) / steepness + } + + if (v < min) + v = min + else if (v > max) + v = max + + return log_to_lin((v - min) / (max - min)) + } + + function pos_to_val_color(color) { + return Qt.vector4d( + pos_to_val(color.x, 0), + pos_to_val(color.y, 1), + pos_to_val(color.z, 2), + 1.0 + ); + } + + function val_to_pos_color(color) { + return Qt.vector4d( + val_to_pos(color[0],0), + val_to_pos(color[1],1), + val_to_pos(color[2],2), + 1.0 + ); + } + + function rgb_to_hsv(color) { + + var h, s, v = 0.0 + var r = color.x + var g = color.y + var b = color.z + + var max = Math.max(r, g, b) + var min = Math.min(r, g, b) + var delta = max - min + + v = max + s = max === 0 ? 0 : delta / max + + if (max === min) { + h = 0 + } else if (r === max) { + h = (g - b) / delta + } else if (g === max) { + h = 2 + (b - r) / delta + } else if (b === max) { + h = 4 + (r - g) / delta + } + + h = h < 0 ? h + 6 : h + h /= 6 + + // Handle extended range inputs (from OpenColorIO RGB_TO_HSV builtin) + if (min < 0) { + v += min + } + if (-min > max) { + s = delta / -min + } + + return Qt.vector3d(h, s, v) + } + + function hsv_to_rgb(color) { + + var MAX_SAT = 1.999 + + var r, g, b = 0.0 + var h = color.x + var s = color.y + var v = color.z + + h = ( h - Math.floor( h ) ) * 6.0 + s = clamp( s, 0.0, MAX_SAT ) + v = v + + r = clamp( Math.abs(h - 3.0) - 1.0, 0.0, 1.0 ) + g = clamp( 2.0 - Math.abs(h - 2.0), 0.0, 1.0 ) + b = clamp( 2.0 - Math.abs(h - 4.0), 0.0, 1.0 ) + + var max = v + var min = v * (1.0 - s) + + // Handle extended range inputs (from OpenColorIO HSV_TO_RGB builtin) + if (s > 1.0) + { + min = v * (1.0 - s) / (2.0 - s) + max = v - min + } + if (v < 0.0) + { + min = v / (2.0 - s) + max = v - min + } + + var delta = max - min + r = r * delta + min + g = g * delta + min + b = b * delta + min + + return Qt.vector3d(r, g, b) + } + + function rgb_to_pos(color) { + + var hsv = rgb_to_hsv(color) + hsv = Qt.vector3d(hsv.x, hsv.z, hsv.y) + + var angle = (1 - hsv.x) * (2 * Math.PI) + var dist = Math.abs(hsv.y) + return Qt.vector2d( + Math.sin(angle) * dist, + Math.cos(angle) * dist + ) + } + + contentItem: Item { + implicitWidth: wheelSize + implicitHeight: implicitWidth + + XsImage { id: imgDiv + width: parent.width + height: parent.height + antialiasing: true + smooth: true + imgOverlayColor: "transparent" + source: "qrc:/grading_icons/wheel_256px.png" + } + + // Cross in the center + Rectangle { + color: "grey" + width: wheel.width - wheel.ring_rel_size * wheel.width + height: 1 + x: wheel.ring_rel_size * wheel.width / 2 + y: wheel.center + } + Rectangle { + color: "grey" + width: 1 + height: wheel.height - wheel.ring_rel_size * wheel.height + x: wheel.center + y: wheel.ring_rel_size * wheel.height / 2 + } + + MouseArea { + id: mArea + anchors.fill: parent + propagateComposedEvents: true + + onPressed: (mouse)=> { + cDrag.x = mouse.x + cDrag.y = mouse.y + update_backend_from_cursor() + } + } + + // Cursor + Rectangle { + id: cursor + + width: wheel.cursor_width + height: width + radius: width/2 + + x: (cDrag.radius <= wheel.radius ? cDrag.x : wheel.center + (cDrag.x - wheel.center) * (wheel.radius / cDrag.radius)) - (width / 2) + y: (cDrag.radius <= wheel.radius ? cDrag.y : wheel.center + (cDrag.y - wheel.center) * (wheel.radius / cDrag.radius)) - (height / 2) + + color: Qt.darker(cursor_color(wheel.color), 1.25) + border.color: Qt.darker(color) + border.width: 0.75 + + function cursor_color(color) { + var rgb_norm = clamp_v4d(color) + var hsv = rgb_to_hsv(Qt.vector3d(rgb_norm.x, rgb_norm.y, rgb_norm.z)) + var rgb = hsv_to_rgb(Qt.vector3d(hsv.x, hsv.z, 1.0)) + return Qt.rgba(rgb.x, rgb.y, rgb.z, 1.0) + } + + MouseArea { + id: cArea + anchors.fill: parent + propagateComposedEvents: true + + drag.filterChildren: true + drag.threshold: 0 + drag.target: Item { + id: cDrag + readonly property real radius: Math.hypot(x - wheel.center, y - wheel.center) + + x: wheel.center + y: wheel.center + } + + onDoubleClicked: { + cDrag.x = wheel.center + cDrag.y = wheel.center + update_backend_from_cursor() + } + + onPositionChanged: { + update_backend_from_cursor() + } + } + } + + } +} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/MaskTool.1/MaskDialog/MaskDialog.qml b/src/plugin/colour_op/grading/src/qml/MaskTool.1/MaskDialog/MaskDialog.qml deleted file mode 100644 index 6af14496c..000000000 --- a/src/plugin/colour_op/grading/src/qml/MaskTool.1/MaskDialog/MaskDialog.qml +++ /dev/null @@ -1,1085 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient - -import xStudio 1.1 -import xstudio.qml.module 1.0 - -Item { - - id: drawDialog - - property int maxDrawSize: 600 - - onVisibleChanged: { - if (!visible) { - // ensure keyboard events are returned to the viewport - sessionWidget.playerWidget.viewport.forceActiveFocus() - } - } - - property real buttonHeight: 20 - property real toolPropLoaderHeight: 0 - property real defaultHeight: toolSelectorFrame.height + toolActionFrame.height + framePadding*3 - - - property real itemSpacing: framePadding/2 - property real framePadding: 6 - property real framePadding_x2: framePadding*2 - property real frameWidth: 1 - property real frameRadius: 2 - property real frameOpacity: 0.3 - property color frameColor: XsStyle.menuBorderColor - - - property color hoverTextColor: palette.text //-whitish //XsStyle.hoverBackground - property color hoverToolInactiveColor: XsStyle.indevColor //-greyish - property color toolActiveBgColor: palette.highlight //-orangish - property color toolActiveTextColor: "white" //palette.highlightedText - property color toolInactiveBgColor: palette.base //-greyish - property color toolInactiveTextColor: XsStyle.controlTitleColor//-greyish - - property real fontSize: XsStyle.menuFontSize/1.1 - property string fontFamily: XsStyle.menuFontFamily - property color textButtonColor: toolInactiveTextColor - property color textValueColor: "white" - - - property bool isAnyToolSelected: currentTool !== "None" - - XsModuleAttributes { - id: grading_settings - attributesGroupNames: "grading_settings" - } - - XsModuleAttributes { - id: mask_tool_settings - attributesGroupNames: "mask_tool_settings" - } - - - // make a local binding to the backend attribute - property int currentDrawPenSizeBackendValue: mask_tool_settings.draw_pen_size ? mask_tool_settings.draw_pen_size : 0 - property int currentErasePenSizeBackendValue: mask_tool_settings.erase_pen_size ? mask_tool_settings.erase_pen_size : 0 - property color currentToolColourBackendValue: mask_tool_settings.pen_colour ? mask_tool_settings.pen_colour : "#000000" - property int currentOpacityBackendValue: mask_tool_settings.pen_opacity ? mask_tool_settings.pen_opacity : 0 - property int currentSoftnessBackendValue: mask_tool_settings.pen_softness ? mask_tool_settings.pen_softness : 0 - property string currentToolBackendValue: mask_tool_settings.drawing_tool ? mask_tool_settings.drawing_tool : "" - - property color currentToolColour: currentToolColourBackendValue - property int currentToolSize: currentTool === "Erase" ? currentErasePenSizeBackendValue : currentDrawPenSizeBackendValue - property int currentToolOpacity: currentOpacityBackendValue - property int currentToolSoftness: currentSoftnessBackendValue - property string currentTool: currentToolBackendValue - - function setPenSize(penSize) { - if(currentTool === "Draw") - { //Draw - mask_tool_settings.draw_pen_size = penSize - } - else if(currentTool === "Erase") - { //Erase - mask_tool_settings.erase_pen_size = penSize - } - } - - onCurrentToolChanged: { - if(currentTool === "Draw") - { //Draw - currentColorPresetModel = drawColourPresetsModel - } - else if(currentTool === "Erase") - { //Erase - currentColorPresetModel = eraseColorPresetModel - } - } - - // make a read only binding to the "mask_tool_active" backend attribute - property bool maskToolActive: mask_tool_settings.mask_tool_active ? mask_tool_settings.mask_tool_active : false - - // Are we in an active drawing mode? - property bool drawingActive: maskToolActive && currentTool !== "None" - - // Set the Cursor as required - property var activeCursor: drawingActive ? Qt.CrossCursor : Qt.ArrowCursor - - onActiveCursorChanged: { - playerWidget.viewport.setRegularCursor(activeCursor) - } - - // map the local property for currentToolSize to the backend value ... to modify the tool size, we only change the backend - // value binding - - property ListModel currentColorPresetModel: drawColourPresetsModel - - // We wrap all the widgets in a top level Item that can forward keyboard - // events back to the viewport for consistent - Item { - anchors.fill: parent - Keys.forwardTo: [sessionWidget] - focus: true - - Rectangle{ - id: toolSelectorFrame - width: parent.width - framePadding_x2 - x: framePadding - anchors.top: parent.top - anchors.topMargin: framePadding - anchors.bottom: toolProperties.bottom - anchors.bottomMargin: -framePadding - - color: "transparent" - border.width: frameWidth - border.color: frameColor - opacity: frameOpacity - radius: frameRadius - - } - - ToolSelector { - id: toolSelector - opacity: 1 - anchors.fill: toolSelectorFrame - } - - Loader { - id: toolProperties - width: toolSelectorFrame.width - height: toolPropLoaderHeight - x: toolSelectorFrame.x - y: buttonHeight*2+framePadding_x2//toolSelectorFrame.toolSelector.y + toolSelectorFrame.toolSelector.height - - sourceComponent: - Item{ - - Row{id: row1 - x: framePadding //+ itemSpacing/2 - y: itemSpacing*5 //row1.y + row1.height - z: 1 - width: toolProperties.width - framePadding*2 - height: (buttonHeight*4) + (spacing*2) - spacing: itemSpacing*2 - - Column { - z: 2 - width: parent.width/2-spacing - spacing: itemSpacing - - XsButton{ id: sizeProp - property bool isPressed: false - property bool isMouseHovered: sizeMArea.containsMouse - property real prevValue: maxDrawSize/2 - property real newValue: maxDrawSize/2 - enabled: isAnyToolSelected - isActive: isPressed - x: spacing/2 - width: parent.width-x; height: buttonHeight; - // color: isPressed || isMouseHovered? (enabled? toolActiveBgColor: hoverToolInactiveColor): toolInactiveBgColor; - - Text{ - text: (currentTool=="Shapes")?"Width": "Size" - font.pixelSize: fontSize - font.family: fontFamily - color: parent.isPressed || parent.isMouseHovered? textValueColor: textButtonColor - width: parent.width/1.8 - horizontalAlignment: Text.AlignHCenter - anchors.left: parent.left - anchors.leftMargin: 2 - topPadding: framePadding/1.4 - } - XsTextField{ id: sizeDisplay - text: currentToolSize - property var backendSize: currentToolSize - onBackendSizeChanged: { - text = currentToolSize - } - focus: sizeMArea.containsMouse && !parent.isPressed - onFocusChanged:{ - if(focus) { - selectAll() - forceActiveFocus() - } - else{ - deselect() - } - } - maximumLength: 3 - inputMask: "900" - inputMethodHints: Qt.ImhDigitsOnly - // validator: IntValidator {bottom: 0; top: maxDrawSize;} - selectByMouse: false - font.pixelSize: fontSize - font.family: fontFamily - color: parent.enabled? textValueColor : Qt.darker(textValueColor,1.5) - width: parent.width/2.2 - height: parent.height - horizontalAlignment: TextInput.AlignHCenter - anchors.right: parent.right - topPadding: framePadding/5 - onEditingCompleted: { - accepted() - } - onAccepted:{ - if(parseInt(text) >= maxDrawSize){ - setPenSize(maxDrawSize) - } - else if(parseInt(text) <= 1){ - setPenSize(1) - } - else{ - setPenSize(parseInt(text)) - } - - text = "" + backendSize - selectAll() - } - } - MouseArea{ - id: sizeMArea - anchors.fill: parent - cursorShape: Qt.SizeHorCursor - hoverEnabled: true - propagateComposedEvents: true - property real prevMX: 0 - property real deltaMX: 0 - property real stepSize: 0.25 - property int valueOnPress: 0 - onMouseXChanged: { - if(parent.isPressed && parent.enabled) - { - deltaMX = mouseX - prevMX - - let deltaValue = parseInt(deltaMX*stepSize) - let valueToApply = Math.round(valueOnPress + deltaValue) - - if(deltaMX>0) - { - if(valueToApply >= maxDrawSize){ - setPenSize(maxDrawSize) - valueOnPress = maxDrawSize - prevMX = mouseX - } - else { - setPenSize(valueToApply) - } - } - else { - if(valueToApply < 1){ - setPenSize(1) - valueOnPress = 1 - prevMX = mouseX - } - else { - setPenSize(valueToApply) - } - } - - sizeDisplay.text = currentToolSize - - if(deltaMX!=0){ - sizeProp.newValue = currentToolSize - } - } - } - onPressed: { - prevMX = mouseX - valueOnPress = currentToolSize - - parent.isPressed = true - focus = true - } - onReleased: { - if(prevMX !== mouseX) { - sizeProp.prevValue = valueOnPress - sizeProp.newValue = currentToolSize - } - parent.isPressed = false - focus = false - } - onDoubleClicked: { - if(currentToolSize == sizeProp.newValue){ - setPenSize(sizeProp.prevValue) - } - else{ - sizeProp.prevValue = currentToolSize - setPenSize(sizeProp.newValue) - } - sizeDisplay.text = currentToolSize - } - } - } - XsButton{ id: opacityProp - property bool isPressed: false - property bool isMouseHovered: opacityMArea.containsMouse - property real prevValue: defaultValue/2 - property real defaultValue: 100 - enabled: isAnyToolSelected && currentTool != "Erase" - isActive: isPressed - x: spacing/2 - width: parent.width-x; height: buttonHeight; - // color: isPressed || isMouseHovered? (enabled? toolActiveBgColor: hoverToolInactiveColor): toolInactiveBgColor; - Text{ - text: "Opacity" - font.pixelSize: fontSize - font.family: fontFamily - color: parent.isPressed || parent.isMouseHovered? textValueColor: textButtonColor - width: parent.width/1.8 - horizontalAlignment: Text.AlignHCenter - anchors.left: parent.left - anchors.leftMargin: 2 - topPadding: framePadding/1.4 - } - XsTextField{ id: opacityDisplay - bgColorNormal: parent.enabled?palette.base:"transparent" - borderColor: bgColorNormal - text: currentTool != "Erase" ? currentToolOpacity : 100 - property var backendOpacity: currentTool != "Erase" ? currentToolOpacity : 100 // we don't set this anywhere else, so this is read-only - always tracks the backend opacity value - onBackendOpacityChanged: { - // if the backend value has changed, update the text - text = currentTool != "Erase" ? currentToolOpacity : 100 - } - focus: opacityMArea.containsMouse && !parent.isPressed - onFocusChanged:{ - if(focus) { - selectAll() - forceActiveFocus() - } - else{ - deselect() - } - } - maximumLength: 3 - inputMask: "900" - inputMethodHints: Qt.ImhDigitsOnly - // validator: IntValidator {bottom: 0; top: 100;} - selectByMouse: false - font.pixelSize: fontSize - font.family: fontFamily - color: parent.enabled? textValueColor : Qt.darker(textValueColor,1.5) - width: parent.width/2.2 - height: parent.height - horizontalAlignment: TextInput.AlignHCenter - anchors.right: parent.right - topPadding: framePadding/5 - onEditingCompleted:{ - accepted() - } - onAccepted:{ - if(currentTool != "Erase"){ - if(parseInt(text) >= 100) { - mask_tool_settings.pen_opacity = 100 - } - else if(parseInt(text) <= 1) { - mask_tool_settings.pen_opacity = 1 - } - else { - mask_tool_settings.pen_opacity = parseInt(text) - } - - text = "" + backendOpacity - selectAll() - } - } - } - MouseArea{ - id: opacityMArea - anchors.fill: parent - cursorShape: Qt.SizeHorCursor - hoverEnabled: true - propagateComposedEvents: true - property real prevMX: 0 - property real deltaMX: 0.0 - property real stepSize: 0.25 - property int valueOnPress: 0 - onMouseXChanged: { - if(parent.isPressed) - { - deltaMX = mouseX - prevMX - // prevMX = mouseX - // var new_opac = (Math.max(Math.min(100.0, mask_tool_settings.pen_opacity + stepSize), 0.0) + 0.1) - 0.1 - // mask_tool_settings.pen_opacity = parseInt(new_opac) - - let deltaValue = parseInt(deltaMX*stepSize) - let valueToApply = Math.round(valueOnPress + deltaValue) - - if(deltaMX>0) - { - if(valueToApply >= 100) { - mask_tool_settings.pen_opacity=100 - valueOnPress = 100 - prevMX = mouseX - } - else { - mask_tool_settings.pen_opacity = valueToApply - } - } - else { - if(valueToApply < 1){ - mask_tool_settings.pen_opacity=1 - valueOnPress = 1 - prevMX = mouseX - } - else { - mask_tool_settings.pen_opacity = valueToApply - } - } - - opacityDisplay.text = currentTool != "Erase" ? currentToolOpacity : 100 - } - } - onPressed: { - prevMX = mouseX - valueOnPress = mask_tool_settings.pen_opacity - - parent.isPressed = true - focus = true - } - onReleased: { - parent.isPressed = false - focus = false - } - onDoubleClicked: { - if(mask_tool_settings.pen_opacity == opacityProp.defaultValue){ - mask_tool_settings.pen_opacity = opacityProp.prevValue - } - else{ - opacityProp.prevValue = mask_tool_settings.pen_opacity - mask_tool_settings.pen_opacity = opacityProp.defaultValue - } - opacityDisplay.text = currentTool != "Erase" ? currentToolOpacity : 100 - } - } - } - XsButton{ id: softnessProp - property bool isPressed: false - property bool isMouseHovered: softnessMArea.containsMouse - property real prevValue: defaultValue/2 - property real defaultValue: 100 - enabled: isAnyToolSelected && currentTool != "Erase" - isActive: isPressed - x: spacing/2 - width: parent.width-x; height: buttonHeight; - // color: isPressed || isMouseHovered? (enabled? toolActiveBgColor: hoverToolInactiveColor): toolInactiveBgColor; - Text{ - text: "Softness" - font.pixelSize: fontSize - font.family: fontFamily - color: parent.isPressed || parent.isMouseHovered? textValueColor: textButtonColor - width: parent.width/1.8 - horizontalAlignment: Text.AlignHCenter - anchors.left: parent.left - anchors.leftMargin: 2 - topPadding: framePadding/1.4 - } - XsTextField{ id: softnessDisplay - bgColorNormal: parent.enabled?palette.base:"transparent" - borderColor: bgColorNormal - text: currentTool != "Erase" ? currentToolSoftness : 100 - property var backendSoftness: currentTool != "Erase" ? currentToolSoftness : 100 // we don't set this anywhere else, so this is read-only - always tracks the backend opacity value - onBackendSoftnessChanged: { - // if the backend value has changed, update the text - text = currentTool != "Erase" ? currentToolSoftness : 100 - } - focus: softnessMArea.containsMouse && !parent.isPressed - onFocusChanged:{ - if(focus) { - selectAll() - forceActiveFocus() - } - else{ - deselect() - } - } - maximumLength: 3 - inputMask: "900" - inputMethodHints: Qt.ImhDigitsOnly - // validator: IntValidator {bottom: 0; top: 100;} - selectByMouse: false - font.pixelSize: fontSize - font.family: fontFamily - color: parent.enabled? textValueColor : Qt.darker(textValueColor,1.5) - width: parent.width/2.2 - height: parent.height - horizontalAlignment: TextInput.AlignHCenter - anchors.right: parent.right - topPadding: framePadding/5 - onEditingCompleted:{ - accepted() - } - onAccepted:{ - if(currentTool != "Erase"){ - if(parseInt(text) >= 100) { - mask_tool_settings.pen_softness = 100 - } - else if(parseInt(text) <= 0) { - mask_tool_settings.pen_softness = 0 - } - else { - mask_tool_settings.pen_softness = parseInt(text) - } - - text = "" + backendSoftness - selectAll() - } - } - } - MouseArea{ - id: softnessMArea - anchors.fill: parent - cursorShape: Qt.SizeHorCursor - hoverEnabled: true - propagateComposedEvents: true - property real prevMX: 0 - property real deltaMX: 0.0 - property real stepSize: 0.25 - property int valueOnPress: 0 - onMouseXChanged: { - if(parent.isPressed) - { - deltaMX = mouseX - prevMX - // prevMX = mouseX - // var new_opac = (Math.max(Math.min(100.0, mask_tool_settings.pen_softness + stepSize), 0.0) + 0.1) - 0.1 - // mask_tool_settings.pen_softness = parseInt(new_opac) - - let deltaValue = parseInt(deltaMX*stepSize) - let valueToApply = Math.round(valueOnPress + deltaValue) - - if(deltaMX>0) - { - if(valueToApply >= 100) { - mask_tool_settings.pen_softness=100 - valueOnPress = 100 - prevMX = mouseX - } - else { - mask_tool_settings.pen_softness = valueToApply - } - } - else { - if(valueToApply < 0){ - mask_tool_settings.pen_softness=0 - valueOnPress = 0 - prevMX = mouseX - } - else { - mask_tool_settings.pen_softness = valueToApply - } - } - - softnessDisplay.text = currentTool != "Erase" ? currentToolSoftness : 100 - } - } - onPressed: { - prevMX = mouseX - valueOnPress = mask_tool_settings.pen_softness - - parent.isPressed = true - focus = true - } - onReleased: { - parent.isPressed = false - focus = false - } - onDoubleClicked: { - if(mask_tool_settings.pen_softness == softnessProp.defaultValue){ - mask_tool_settings.pen_softness = softnessProp.prevValue - } - else{ - softnessProp.prevValue = mask_tool_settings.pen_softness - mask_tool_settings.pen_softness = softnessProp.defaultValue - } - softnessDisplay.text = currentTool != "Erase" ? currentToolSoftness : 0 - } - } - } - XsButton{ id: colorProp - property bool isPressed: false - property bool isMouseHovered: colorMArea.containsMouse - enabled: (isAnyToolSelected && currentTool !== "Erase") - isActive: isPressed - x: spacing/2 - width: parent.width-x; height: buttonHeight; - // color: isPressed || isMouseHovered? (enabled? toolActiveBgColor: hoverToolInactiveColor): toolInactiveBgColor; - - MouseArea{ - id: colorMArea - // enabled: currentTool !== 1 - hoverEnabled: true - anchors.fill: parent - onClicked: { - parent.isPressed = false - colorDialog.open() - } - onPressed: { - parent.isPressed = true - } - onReleased: { - parent.isPressed = false - } - } - Text{ - text: "Colour" - font.pixelSize: fontSize - font.family: fontFamily - color: parent.isPressed || parent.isMouseHovered? textValueColor: textButtonColor - width: parent.width/2 - horizontalAlignment: Text.AlignHCenter - anchors.right: parent.horizontalCenter - anchors.rightMargin: -3 - topPadding: framePadding/1.2 - } - Rectangle{ id: colorPreviewDuplicate - opacity: (!isAnyToolSelected || currentTool === "Erase")? (parent.enabled?1:0.5): 0 - height: parent.height/1.4; - color: currentTool === "Erase" ? "white" : currentToolColour - border.width: frameWidth - border.color: parent.enabled? (currentToolColour=="white" || currentToolColour=="#ffffff")? "black": "white" : Qt.darker("white",1.5) - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.horizontalCenter - anchors.leftMargin: parent.width/7 - anchors.right: parent.right - anchors.rightMargin: parent.width/10 - } - Rectangle{ id: colorPreview - visible: (isAnyToolSelected && currentTool !== "Erase") - x: colorPreviewDuplicate.x - y: colorPreviewDuplicate.y - width: colorPreviewDuplicate.width - onWidthChanged: { - x= colorPreviewDuplicate.x - y= colorPreviewDuplicate.y - } - height: colorPreviewDuplicate.height - color: currentTool === "Erase" ? "white" : currentToolColour; - border.width: frameWidth; - border.color: (color=="white" || color=="#ffffff")? "black": "white" - - scale: dragArea.drag.active? 0.6: 1 - Behavior on scale {NumberAnimation{ duration: 250 }} - - Drag.active: dragArea.drag.active - Drag.hotSpot.x: colorPreview.width/2 - Drag.hotSpot.y: colorPreview.height/2 - MouseArea{ - id: dragArea - anchors.fill: parent - drag.target: parent - - drag.minimumX: -framePadding - drag.maximumX: toolSelectorFrame.width - framePadding*5 - drag.minimumY: buttonHeight - drag.maximumY: buttonHeight*2.5 - - onReleased: { - colorProp.isPressed = false - parent.Drag.drop() - parent.x = colorPreviewDuplicate.x - parent.y = colorPreviewDuplicate.y - } - onClicked: { - colorProp.isPressed = false - colorDialog.open() - } - onPressed: { - colorProp.isPressed = true - } - } - } - } - } - - Rectangle { id: toolPreview - width: parent.width/2 - spacing - height: parent.height - spacing - color: "#595959" //"transparent" - border.color: frameColor - border.width: frameWidth - // clip: true - - Grid {id: checkerBg; - property real tileSize: framePadding - anchors.fill: parent; - anchors.centerIn: parent - anchors.margins: tileSize/2; - clip: true; - rows: Math.floor(height/tileSize); - columns: Math.floor(width/tileSize); - Repeater { - model: checkerBg.columns*checkerBg.rows - Rectangle { - property int oddRow: Math.floor(index / checkerBg.columns)%2 - property int oddColumn: (index % checkerBg.columns)%2 - width: checkerBg.tileSize; height: checkerBg.tileSize - color: (oddRow == 1 ^ oddColumn == 1) ? "#949494": "#595959" - } - } - } - - Rectangle{ - - id: clippedPreview - anchors.fill: parent - color: "transparent" - clip: true - - Rectangle {id: drawPreview - visible: currentTool === "Draw" - anchors.centerIn: parent - property real sizeScaleFactor: (parent.height)/maxDrawSize - width: currentToolSize *sizeScaleFactor - height: width - radius: width/2 - color: currentToolColour - opacity: currentToolOpacity/100 - - RadialGradient { - visible: false - anchors.fill: parent - source: parent - gradient: - Gradient { - GradientStop { - position: 0.1; color: currentToolColour - } - GradientStop { - position: 1.0; color: "black" - } - } - } - - } - - Rectangle { id: erasePreview - visible: currentTool === "Erase" - anchors.centerIn: parent - property real sizeScaleFactor: (parent.height)/maxDrawSize - width: currentToolSize * sizeScaleFactor - height: width - radius: width/2 - color: "white" - opacity: 1 - } - - } - } - } - - - Rectangle{ id: row2 - y: row1.y + row1.height + presetColours.spacing - width: toolProperties.width - height: buttonHeight *1.5 - visible: (isAnyToolSelected && currentTool !== "Erase") - color: "transparent" - - ListView{ id: presetColours - x: frameWidth +spacing*2 - width: parent.width - frameWidth*2 - spacing*2 - height: parent.height - anchors.verticalCenter: parent.verticalCenter - spacing: (itemSpacing!==0)?itemSpacing/2: 0 - clip: true - interactive: false - orientation: ListView.Horizontal - - model: currentColorPresetModel - delegate: - Item{ - property bool isMouseHovered: presetMArea.containsMouse - width: presetColours.width/9-presetColours.spacing; - height: presetColours.height - Rectangle { - anchors.centerIn: parent - width: parent.width - height: width - radius: width/2 - color: preset - border.width: 1 - border.color: parent.isMouseHovered? toolActiveBgColor: (currentToolColour === preset)? toolActiveTextColor: "black" - - MouseArea{ - id: presetMArea - property color temp_color - anchors.fill: parent - hoverEnabled: true - onClicked: { - - temp_color = currentColorPresetModel.get(index).preset; - mask_tool_settings.pen_colour = temp_color - - } - } - - DropArea { - anchors.fill: parent - Image { - visible: parent.containsDrag - anchors.fill: parent - source: "qrc:///feather_icons/plus-circle.svg" - layer { - enabled: (preset=="black" || preset=="#000000") - effect: - ColorOverlay { - color: "white" - } - } - } - onDropped: { - currentColorPresetModel.setProperty(index, "preset", currentToolColour.toString()) - } - } - } - } - } - } - - Component.onCompleted: { - toolPropLoaderHeight = row2.y + row2.height - } - } - - - ColorDialog { id: colorDialog - title: "Please pick a color" - color: currentToolColour - onAccepted: { - mask_tool_settings.pen_colour = currentColor - close() - } - onRejected: { - close() - } - } - - ListModel{ id: eraseColorPresetModel - ListElement{ - preset: "white" - } - } - ListModel{ id: drawColourPresetsModel - ListElement{ - preset: "#ff0000" //- "red" - } - ListElement{ - preset: "#ffa000" //- "orange" - } - ListElement{ - preset: "#ffff00" //- "yellow" - } - ListElement{ - preset: "#28dc00" //- "green" - } - ListElement{ - preset: "#0050ff" //- "blue" - } - ListElement{ - preset: "#8c00ff" //- "violet" - } - ListElement{ - preset: "#ff64ff" //- "pink" - } - ListElement{ - preset: "#ffffff" //- "white" - } - ListElement{ - preset: "#000000" //- "black" - } - } - ListModel{ id: textColourPresetsModel - ListElement{ - preset: "#ff0000" //- "red" - } - ListElement{ - preset: "#ffa000" //- "orange" - } - ListElement{ - preset: "#ffff00" //- "yellow" - } - ListElement{ - preset: "#28dc00" //- "green" - } - ListElement{ - preset: "#0050ff" //- "blue" - } - ListElement{ - preset: "#8c00ff" //- "violet" - } - ListElement{ - preset: "#ff64ff" //- "pink" - } - ListElement{ - preset: "#ffffff" //- "white" - } - ListElement{ - preset: "#000000" //- "black" - } - } - ListModel{ id: shapesColourPresetsModel - ListElement{ - preset: "#ff0000" //- "red" - } - ListElement{ - preset: "#ffa000" //- "orange" - } - ListElement{ - preset: "#ffff00" //- "yellow" - } - ListElement{ - preset: "#28dc00" //- "green" - } - ListElement{ - preset: "#0050ff" //- "blue" - } - ListElement{ - preset: "#8c00ff" //- "violet" - } - ListElement{ - preset: "#ff64ff" //- "pink" - } - ListElement{ - preset: "#ffffff" //- "white" - } - ListElement{ - preset: "#000000" //- "black" - } - } - } - - Rectangle{ id: toolActionFrame - x: framePadding - anchors.top: toolSelectorFrame.bottom - anchors.topMargin: framePadding - - width: parent.width - framePadding_x2 - height: toolSelectorFrame.height/1.5 - - color: "transparent" - opacity: frameOpacity - border.width: frameWidth - border.color: frameColor - radius: frameRadius - } - Item{ id: toolActionSection - x: toolActionFrame.x - width: toolActionFrame.width - - ListView{ id: toolActionUndoRedo - - width: parent.width - framePadding_x2 - height: buttonHeight - x: framePadding + spacing/2 - y: toolActionFrame.y + framePadding + spacing/2 - - spacing: itemSpacing - clip: true - interactive: false - orientation: ListView.Horizontal - - model: - ListModel{ - id: modelUndoRedo - ListElement{ - action: "Undo" - } - ListElement{ - action: "Redo" - } - } - delegate: - XsButton{ - text: model.action - width: toolActionUndoRedo.width/modelUndoRedo.count - toolActionUndoRedo.spacing - height: buttonHeight - onClicked: { - grading_settings.drawing_action = text - } - } - } - - ListView{ id: toolActionCopyPasteClear - - width: parent.width - framePadding_x2 - height: buttonHeight - x: framePadding + spacing/2 - y: toolActionUndoRedo.y + toolActionUndoRedo.height + spacing - - spacing: itemSpacing - clip: true - interactive: false - orientation: ListView.Horizontal - - model: - ListModel{ - id: modelCopyPasteClear - ListElement{ - action: "Copy" - } - ListElement{ - action: "Paste" - } - ListElement{ - action: "Clear" - } - } - delegate: - XsButton{ - text: model.action - width: toolActionCopyPasteClear.width/modelCopyPasteClear.count - toolActionCopyPasteClear.spacing - height: buttonHeight - enabled: text == "Clear" - onClicked: { - grading_settings.drawing_action = text - } - - } - } - - ListView{ id: toolActionDisplayMode - - width: parent.width - framePadding_x2 - height: buttonHeight - x: framePadding + spacing/2 - y: toolActionCopyPasteClear.y + toolActionCopyPasteClear.height + spacing - - spacing: itemSpacing - clip: true - interactive: false - orientation: ListView.Horizontal - - model: - ListModel{ - id: modelDisplayMode - ListElement{ - action: "Mask" - tooltip: "Show mask being draw" - } - ListElement{ - action: "Grade" - tooltip: "Show masked grade result" - } - } - delegate: - XsButton{ - isActive: mask_tool_settings.display_mode == text - text: model.action - tooltip: model.tooltip - width: toolActionDisplayMode.width/modelDisplayMode.count - toolActionDisplayMode.spacing - height: buttonHeight - onClicked: { - mask_tool_settings.display_mode = text - } - } - } - - } - } - -} \ No newline at end of file diff --git a/src/plugin/colour_op/grading/src/qml/MaskTool.1/MaskDialog/ToolSelector.qml b/src/plugin/colour_op/grading/src/qml/MaskTool.1/MaskDialog/ToolSelector.qml deleted file mode 100644 index ebb6e1130..000000000 --- a/src/plugin/colour_op/grading/src/qml/MaskTool.1/MaskDialog/ToolSelector.qml +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient - -import xStudio 1.1 -import xstudio.qml.module 1.0 - -Item{ - - anchors.fill: parent - - // note this model only has one item, which is the 'tool type' attribute - // in the backend. - - XsModuleAttributesModel { - id: mask_tool_types - attributesGroupNames: "mask_tool_types" - } - - property var toolImages: [ - "qrc:///icons/drawing.png", - "qrc:///feather_icons/book.svg" - ] - - // we have to use a repeater to hook the model into the ListView - Repeater { - - id: the_view - anchors.fill: parent - anchors.margins: framePadding - - // by using mask_tool_types as the 'model' we are exposing the - // attributes in the "mask_tool_types" group and their role. - // The 'ListView' is instanced for each attribute, and each instance - // can 'see' the attribute role data items (like 'value', 'combo_box_options'). - // In this case, there is only one attribute in the group which tracks - // the 'active tool' selection for the annotations plugin. - model: mask_tool_types - - ListView{ - - id: toolSelector - - anchors.fill: parent - anchors.margins: framePadding - - spacing: itemSpacing - // clip: true - interactive: false - orientation: ListView.Horizontal - - model: combo_box_options // this is 'role data' from the backend attr - - delegate: toolSelectorDelegate - - // read only convenience binding to backend. - currentIndex: combo_box_options.indexOf(value) - - Component{ - - id: toolSelectorDelegate - - - Rectangle{ - - width: (toolSelector.width-toolSelector.spacing*(combo_box_options.length-1))/combo_box_options.length// - toolSelector.spacing - height: buttonHeight*2 - color: "transparent" - property bool isEnabled: true//index != 2 // Text disabled while WIP - can be enabled to see where it is - enabled: isEnabled - - XsButton{ id: toolBtn - width: parent.width - height: parent.height - text: "" - isActive: toolSelector.currentIndex===index - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - hoverEnabled: isEnabled - - ToolTip { - parent: toolBtn - visible: !isEnabled && toolBtn.down - text: "Text captions coming soon!" - } - - Text{ - id: tText - text: combo_box_options[index] - - font.pixelSize: fontSize - font.family: fontFamily - color: enabled? toolSelector.currentIndex===index || toolBtn.down || toolBtn.hovered || parent.isActive? toolActiveTextColor: toolInactiveTextColor : Qt.darker(toolInactiveTextColor,1.5) - horizontalAlignment: Text.AlignHCenter - - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: framePadding/2 - } - Image { - anchors.bottom: parent.bottom - anchors.bottomMargin: framePadding/2 - width: 20 - height: width - source: toolImages[index] - anchors.horizontalCenter: parent.horizontalCenter - layer { - enabled: true - effect: - ColorOverlay { - color: enabled? (toolSelector.currentIndex===index || toolBtn.down || toolBtn.hovered)? toolActiveTextColor: toolInactiveTextColor : Qt.darker(toolInactiveTextColor,1.5) - } - } - } - - onClicked: { - if (!isEnabled) return; - if(toolSelector.currentIndex == index) - { - //Disables tool by setting the 'value' of the 'active tool' - // attribute in the plugin backend to 'None' - value = "None" - } - else - { - value = tText.text - } - } - - } - - // Rectangle { - // anchors.fill: parent - // visible: !isEnabled - // color: "black" - // opacity: 0.2 - // } - } - } - } - } -} - diff --git a/src/plugin/colour_op/grading/src/qml/MaskTool.1/qmldir b/src/plugin/colour_op/grading/src/qml/MaskTool.1/qmldir deleted file mode 100644 index 9cfe0d0cd..000000000 --- a/src/plugin/colour_op/grading/src/qml/MaskTool.1/qmldir +++ /dev/null @@ -1,3 +0,0 @@ -module MaskTool - -MaskDialog 1.0 MaskDialog/MaskDialog.qml diff --git a/src/plugin/colour_op/grading/src/serialisers/1.0/serialiser_1_pt_0.cpp b/src/plugin/colour_op/grading/src/serialisers/1.0/serialiser_1_pt_0.cpp index 3edf2a097..e0e0b0ea5 100644 --- a/src/plugin/colour_op/grading/src/serialisers/1.0/serialiser_1_pt_0.cpp +++ b/src/plugin/colour_op/grading/src/serialisers/1.0/serialiser_1_pt_0.cpp @@ -22,11 +22,11 @@ RegisterGradingDataSerialiser(GradingDataSerialiser_1_pt_0, 1, 0) void GradingDataSerialiser_1_pt_0::_serialise( const GradingData *grading_data, nlohmann::json &d) const { - d = grading_data->layers(); + d = *grading_data; } void GradingDataSerialiser_1_pt_0::_deserialise( GradingData *grading_data, const nlohmann::json &d) { - grading_data->layers() = d.template get>(); + *grading_data = d.template get(); } diff --git a/src/plugin/colour_op/grading/test/CMakeLists.txt b/src/plugin/colour_op/grading/test/CMakeLists.txt index a73825679..66caa1970 100644 --- a/src/plugin/colour_op/grading/test/CMakeLists.txt +++ b/src/plugin/colour_op/grading/test/CMakeLists.txt @@ -1,7 +1,7 @@ include(CTest) SET(LINK_DEPS - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/plugin/colour_pipeline/ocio/src/CMakeLists.txt b/src/plugin/colour_pipeline/ocio/src/CMakeLists.txt index 8aa0216be..1ce7e3883 100644 --- a/src/plugin/colour_pipeline/ocio/src/CMakeLists.txt +++ b/src/plugin/colour_pipeline/ocio/src/CMakeLists.txt @@ -18,4 +18,4 @@ SET(LINK_DEPS Imath::Imath ) -create_plugin_with_alias(colour_pipeline_ocio xstudio::colour_pipeline::ocio 0.1.0 "${LINK_DEPS}") +create_plugin_with_alias(colour_pipeline_ocio xstudio::colour_pipeline::ocio ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/plugin/colour_pipeline/ocio/src/dneg.hpp b/src/plugin/colour_pipeline/ocio/src/dneg.hpp deleted file mode 100644 index e8ad2373f..000000000 --- a/src/plugin/colour_pipeline/ocio/src/dneg.hpp +++ /dev/null @@ -1,177 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include - -#include //NOLINT - - -/* -This function checks the playback machines yaml file to determine if the playback -machine display is a cinema screen or HDR TV. -*/ -std::string get_playback_display() { - const std::string CONST_PLAYBACK_FILE = "/var/playback/sys-config.yaml"; - - std::map CONST_DISPLAY = { - {"cinema", "DCI-P3"}, {"hdr", "HDR"}, {"playback", "Playback"}}; - - std::fstream data_load; - data_load.open(CONST_PLAYBACK_FILE, std::ios::in); - - if (data_load.is_open()) { - std::string temp; - while (std::getline(data_load, temp)) { - if (temp.find("cinema") != std::string::npos) { - return CONST_DISPLAY["cinema"]; - } else if (temp.find("hdr") != std::string::npos) { - return CONST_DISPLAY["hdr"]; - } - } - data_load.close(); - } - - return CONST_DISPLAY["playback"]; -} - - -std::string dneg_ocio_default_display( - const OCIO::ConstConfigRcPtr &ocio_config, const std::string &device) { - std::string display = ""; - std::map> display_views; - std::vector displays; - - std::map CONST_DISPLAY = { - {"srgb", "sRGB"}, {"eizo", "EIZO"}, {"hdr", "HDR"}, {"playback", "Playback"}}; - - // Helper lambda to check for string in displays vector - auto check_displays = [&displays](std::string &check_string) { - if (std::find(displays.begin(), displays.end(), check_string) != displays.end()) { - return true; - } - - return false; - }; - - // Get the Hostname of the machine - const char *hostname_env = std::getenv("HOSTNAME"); - std::string hostname; - std::string hostname_lower; - - if (hostname_env && *hostname_env) { - hostname = hostname_env; - hostname_lower = hostname; - } - hostname[0] = std::toupper(hostname[0]); - - std::transform( - hostname_lower.begin(), hostname_lower.end(), hostname_lower.begin(), ::tolower); - - // Get ocio config and - const std::string default_display = ocio_config->getDefaultDisplay(); - - // Parse display views - for (int i = 0; i < ocio_config->getNumDisplays(); ++i) { - const std::string display = ocio_config->getDisplay(i); - displays.push_back(display); - - display_views[display] = std::vector(); - for (int j = 0; j < ocio_config->getNumViews(display.c_str()); ++j) { - const std::string view = ocio_config->getView(display.c_str(), j); - display_views[display].push_back(view); - } - } - - // Check for India sRGB lock - const char *site_name_env = std::getenv("DN_SITE"); - const bool srgb_calibration = (bool)std::getenv("DN_SRGB_CALIBRATION"); - std::string site_name; - - if (site_name_env && *site_name_env) { - site_name = site_name_env; - } - - if (((site_name == "mumbai") || (site_name == "chennai")) && srgb_calibration) { - // Return sRGB - return CONST_DISPLAY["srgb"]; - } - - // At-desk monitor Logic - std::string device_upper = xstudio::utility::to_upper(device); - - // EIZO monitors - if (device_upper.find(CONST_DISPLAY["eizo"]) != std::string::npos) { - /* - NOTE: this rule is kept for backward compatibility only, a time - where each EIZO model had its specific calibration. - Some OCIO configs have a display per Device model, whereas others - have one for all EIZO monitors - */ - if (check_displays(CONST_DISPLAY["eizo"])) { - display = CONST_DISPLAY["eizo"]; - } - /* - Try to match the model exactly something like this is expected - 'Eizo CG247X (DFP-0)', in the OCIO config, this would be EIZO247X - */ - else { - std::regex expression("CG([0-9A-Z]+)"); - std::smatch match; - if (std::regex_search(device_upper, match, expression)) { - std::string config_name = "EIZO" + match.str(1); - if (check_displays(config_name)) { - display = config_name; - } - } - } - - if (display.empty()) { - display = default_display; - spdlog::warn("Could not find OCIO Display for device " + device); - } - } - // DELL monitors - else if (device_upper.find("DELL") != std::string::npos) { - if (check_displays(CONST_DISPLAY["srgb"])) { - display = CONST_DISPLAY["srgb"]; - } else { - display = default_display; - } - } - - // Playback room logic - - // KONA video cards - else if (device_upper.find("KONA") != std::string::npos) { - if (check_displays(CONST_DISPLAY["hdr"])) { - display = CONST_DISPLAY["hdr"]; - } else { - display = default_display; - spdlog::warn("Could not set HDR as the display for device " + device); - } - } - - /* - Fallback: Projectors - New style is to have a 'Playback' display. Fallback to old-style where - we check whether the hostname is one of the display options. - Note that for DCI, playback display default view is a straight - DCI-P3 output (playback characterization tranforms are identity matrix - and 1D LUT). - */ - - else if ( - hostname_lower.find("playback") != std::string::npos && - check_displays(CONST_DISPLAY["playback"])) { - display = get_playback_display(); - } - - else if (check_displays(hostname)) { - display = hostname; - } - - // Fall back to default - if (display.empty()) { - display = default_display; - } - - return display; -} diff --git a/src/plugin/colour_pipeline/ocio/src/ocio.cpp b/src/plugin/colour_pipeline/ocio/src/ocio.cpp deleted file mode 100644 index b3507f586..000000000 --- a/src/plugin/colour_pipeline/ocio/src/ocio.cpp +++ /dev/null @@ -1,1156 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "ocio.hpp" - -#include "xstudio/utility/string_helpers.hpp" -#include "xstudio/ui/opengl/shader_program_base.hpp" - -#include "dneg.hpp" -#include "shaders.hpp" - -using namespace xstudio::colour_pipeline; -using namespace xstudio; - - -namespace { -const static utility::Uuid PLUGIN_UUID{"b39d1e3d-58f8-475f-82c1-081a048df705"}; - -OCIO::GradingPrimary grading_primary_from_cdl(const OCIO::ConstCDLTransformRcPtr &cdl) { - OCIO::GradingPrimary gp(OCIO::GRADING_LIN); - - std::array slope; - std::array offset; - std::array power; - double sat; - - cdl->getSlope(slope.data()); - cdl->getOffset(offset.data()); - cdl->getPower(power.data()); - sat = cdl->getSat(); - - // TODO: ColSci - // GradingPrimary with only saturation is currently considered identity - // Temporary workaround until OCIO fix the issue by adding a clamp to - // an arbitrary low value. - if (slope == decltype(slope){1.0, 1.0, 1.0} && offset == decltype(offset){0.0, 0.0, 0.0} && - power == decltype(power){1.0, 1.0, 1.0} && sat != 1.0) { - gp.m_clampBlack = -1e6; - } - - for (int i = 0; i < 3; ++i) { - offset[i] = offset[i] / slope[i]; - slope[i] = std::log2(slope[i]); - } - - gp.m_offset = OCIO::GradingRGBM(offset[0], offset[1], offset[2], 0.0); - gp.m_exposure = OCIO::GradingRGBM(slope[0], slope[1], slope[2], 0.0); - gp.m_contrast = OCIO::GradingRGBM(power[0], power[1], power[2], 1.0); - gp.m_saturation = sat; - gp.m_pivot = std::log2(1.0 / 0.18); - - return gp; -} - -OCIO::TransformRcPtr to_dynamic_transform( - const OCIO::ConstTransformRcPtr &transform, - const OCIO::GradingPrimary &cdl_grading_primary, - const std::string &cdl_file_transform) { - if (transform->getTransformType() == OCIO::TRANSFORM_TYPE_GROUP) { - auto group = OCIO::DynamicPtrCast(transform); - auto new_group = OCIO::GroupTransform::Create(); - - int num_transforms = group->getNumTransforms(); - for (int i = 0; i < num_transforms; ++i) { - auto dyn_transform = to_dynamic_transform( - group->getTransform(i), cdl_grading_primary, cdl_file_transform); - new_group->appendTransform(dyn_transform); - } - return OCIO::DynamicPtrCast(new_group); - } else if (transform->getTransformType() == OCIO::TRANSFORM_TYPE_FILE) { - auto file = OCIO::DynamicPtrCast(transform); - if (std::string(file->getSrc()) == cdl_file_transform) { - auto grading_primary = OCIO::GradingPrimaryTransform::Create(OCIO::GRADING_LIN); - grading_primary->setValue(cdl_grading_primary); - grading_primary->makeDynamic(); - return OCIO::DynamicPtrCast(grading_primary); - } - } - - return transform->createEditableCopy(); -} - -} // anonymous namespace - -std::string OCIOColourPipeline::MediaParams::compute_hash() const { - std::string hash; - hash += metadata.dump(2); - hash += user_input_cs; - hash += user_input_display; - hash += user_input_view; - hash += output_view; - return hash; -} - -OCIOColourPipeline::OCIOColourPipeline( - caf::actor_config &cfg, const utility::JsonStore &init_settings) - : ColourPipeline(cfg, init_settings) { - - setup_ui(); -} - -void OCIOColourPipeline::on_exit() { - auto main_ocio = - system().registry().template get("MAIN_VIEWPORT_OCIO_INSTANCE"); - if (main_ocio == self()) { - system().registry().erase("MAIN_VIEWPORT_OCIO_INSTANCE"); - } -} - -std::string OCIOColourPipeline::linearise_op_hash( - const utility::Uuid &source_uuid, const utility::JsonStore &colour_params) { - - if (colour_bypass_->value()) - return "LineariseTransformBypass"; - - - // By making the hash include our UUID we can make sure that any other - // OCIO colour pipelines that construct the colour op data hash Id won't - // clash. - std::string result_id = to_string(PLUGIN_UUID); - - try { - - const MediaParams media_param = get_media_params(source_uuid, colour_params); - auto main_proc = make_to_lin_processor(media_param); - result_id += main_proc->getCacheID(); - - } catch (const std::exception &e) { - spdlog::warn("OCIOColourPipeline: Failed to compute linearise hash: {}", e.what()); - } - - return result_id; -} - -std::string OCIOColourPipeline::linear_to_display_op_hash( - const utility::Uuid &source_uuid, const utility::JsonStore &colour_params) { - - if (colour_bypass_->value()) - return "DisplayTransformBypass"; - - std::string result_id = to_string(PLUGIN_UUID); - - try { - - const MediaParams media_param = get_media_params(source_uuid, colour_params); - auto main_proc = make_display_processor(media_param, false); - result_id += main_proc->getCacheID(); - { - // Here we make sure the cacheID string depends on any grading primaries data - // that may have been picked up for the source (if we are applying the primary - // grade). - const MediaParams media_param = get_media_params(source_uuid, colour_params); - std::stringstream ss; - ss << media_param.primary; - result_id += ss.str(); - } - - } catch (const std::exception &e) { - spdlog::warn("OCIOColourPipeline: Failed to compute display hash: {}", e.what()); - } - - return result_id; -} - -std::string OCIOColourPipeline::fast_display_transform_hash(const media::AVFrameID &media_ptr) { - return get_media_params(media_ptr.source_uuid_, media_ptr.params_).compute_hash() + - (colour_bypass_->value() ? "null" : display_->value() + view_->value()); -} - -ColourOperationDataPtr OCIOColourPipeline::linearise_op_data( - const utility::Uuid &source_uuid, const utility::JsonStore &colour_params) { - - using xstudio::utility::replace_once; - - auto data = std::make_shared("OCIO Linearise OP"); - - try { - - const MediaParams media_param = get_media_params(source_uuid, colour_params); - - // Construct OCIO processor, shader and extract texture(s) - auto to_lin_proc = make_to_lin_processor(media_param); - auto to_lin_shader = make_shader(to_lin_proc, "OCIOLinearise", "to_linear_"); - setup_textures(to_lin_shader, data); - - std::string to_lin_shader_src = replace_once( - ShaderTemplates::OCIO_linearise, "//OCIOLinearise", to_lin_shader->getShaderText()); - - data->shader_ = std::make_shared( - utility::Uuid::generate(), to_lin_shader_src); - - // Store GPUShaderDesc objects for later use during uniform binding / update. - // Note that we need updated MediaParams object, which may include primary grading - // data, when we update the shader uniforms at draw time. Hence we add the MediaParams - // as part of the user_data_ blind data object. - auto desc = std::make_shared(); - desc->shader_desc = to_lin_shader; - desc->params = get_media_params(source_uuid, colour_params); - ; - data->user_data_ = desc; - - } catch (const std::exception &e) { - spdlog::warn("OCIOColourPipeline: Failed to setup lin shader: {}", e.what()); - } - - return data; -} - -ColourOperationDataPtr OCIOColourPipeline::linear_to_display_op_data( - const utility::Uuid &source_uuid, const utility::JsonStore &colour_params) { - - using xstudio::utility::replace_once; - - auto data = std::make_shared(ColourOperationData("OCIO Display OP")); - - try { - - const MediaParams media_param = get_media_params(source_uuid, colour_params); - - // Construct OCIO processor, shader and extract texture(s) - auto display_proc = make_display_processor(media_param, false); - auto display_shader = make_shader(display_proc, "OCIODisplay", "to_display"); - setup_textures(display_shader, data); - - std::string display_shader_src = replace_once( - ShaderTemplates::OCIO_display, "//OCIODisplay", display_shader->getShaderText()); - - data->shader_ = std::make_shared( - utility::Uuid::generate(), display_shader_src); - - // Store GPUShaderDesc objects for later use during uniform binding / update. - // Note that we need updated MediaParams object, which may include primary grading - // data, when we update the shader uniforms at draw time. Hence we add the MediaParams - // as part of the user_data_ blind data object. - - auto desc = std::make_shared(); - desc->shader_desc = display_shader; - desc->params = get_media_params(source_uuid, colour_params); - - data->user_data_ = desc; - - } catch (const std::exception &e) { - spdlog::warn("OCIOColourPipeline: Failed to setup display shader: {}", e.what()); - } - - return data; -} - -utility::JsonStore OCIOColourPipeline::update_shader_uniforms( - const media_reader::ImageBufPtr &image, std::any &user_data) { - - utility::JsonStore uniforms; - if (channel_->value() == "Red") { - uniforms["show_chan"] = 1; - } else if (channel_->value() == "Green") { - uniforms["show_chan"] = 2; - } else if (channel_->value() == "Blue") { - uniforms["show_chan"] = 3; - } else if (channel_->value() == "Alpha") { - uniforms["show_chan"] = 4; - } else if (channel_->value() == "Luminance") { - uniforms["show_chan"] = 5; - } else { - uniforms["show_chan"] = 0; - } - - // TODO: ColSci - // Saturation is not managed by OCIO currently - uniforms["saturation"] = enable_saturation_->value() ? saturation_->value() : 1.0f; - - if (user_data.has_value() && user_data.type() == typeid(ShaderDescriptorPtr)) { - try { - auto shader = std::any_cast(user_data); - if (shader && shader->shader_desc) { - // ColourOperationDataPtr is shared among MediaSource using the same OCIO - // shader. Hence ShaderDesc objects holding OCIO DynamicProperty are shared too. - // DynamicProperty are used to hold the uniforms representing the transform, - // we need to update them before querying the updated uniforms. The mutex is - // needed to protect against multiple workers concurrently updating the - // ShaderDesc's DynamicProperty resulting in queried uniforms not reflecting - // values for the current shot. - std::scoped_lock lock(shader->mutex); - update_dynamic_parameters(shader->shader_desc, shader->params); - update_all_uniforms( - shader->shader_desc, uniforms, image.frame_id().source_uuid_); - } - } catch (const std::exception &e) { - spdlog::warn("OCIOColourPipeline: Failed to update shader uniforms: {}", e.what()); - } - } - return uniforms; -} - -thumbnail::ThumbnailBufferPtr OCIOColourPipeline::process_thumbnail( - const media::AVFrameID &media_ptr, const thumbnail::ThumbnailBufferPtr &buf) { - - if (buf->format() != thumbnail::TF_RGBF96) { - spdlog::warn("OCIOColourPipeline: Invalid thumbnail buffer type, expects TF_RGBF96"); - return buf; - } - - try { - const MediaParams media_param = - get_media_params(media_ptr.source_uuid_, media_ptr.params_); - - // TODO: just use a single pass processor rather than two step processors. - auto to_lin_proc = make_to_lin_processor(media_param); - auto lin_to_display_proc = make_display_processor(media_param, true); - - auto cpu_to_lin_proc = to_lin_proc->getOptimizedCPUProcessor( - OCIO::BIT_DEPTH_F32, OCIO::BIT_DEPTH_F32, OCIO::OPTIMIZATION_DEFAULT); - - auto cpu_lin_to_display_proc = lin_to_display_proc->getOptimizedCPUProcessor( - OCIO::BIT_DEPTH_F32, OCIO::BIT_DEPTH_UINT8, OCIO::OPTIMIZATION_DEFAULT); - - auto thumb = std::make_shared( - buf->width(), buf->height(), thumbnail::TF_RGB24); - auto src = reinterpret_cast(buf->data().data()); - auto dst = reinterpret_cast(thumb->data().data()); - - std::vector intermediate(buf->width() * buf->height() * buf->channels()); - - OCIO::PackedImageDesc in_img( - src, - buf->width(), - buf->height(), - buf->channels(), - OCIO::BIT_DEPTH_F32, - OCIO::AutoStride, - OCIO::AutoStride, - OCIO::AutoStride); - - OCIO::PackedImageDesc intermediate_img( - intermediate.data(), - buf->width(), - buf->height(), - buf->channels(), - OCIO::BIT_DEPTH_F32, - OCIO::AutoStride, - OCIO::AutoStride, - OCIO::AutoStride); - - - OCIO::PackedImageDesc out_img( - dst, - thumb->width(), - thumb->height(), - thumb->channels(), - OCIO::BIT_DEPTH_UINT8, - OCIO::AutoStride, - OCIO::AutoStride, - OCIO::AutoStride); - - cpu_to_lin_proc->apply(in_img, intermediate_img); - cpu_lin_to_display_proc->apply(intermediate_img, out_img); - return thumb; - - } catch (const std::exception &e) { - spdlog::warn("OCIOColourPipeline: Failed to compute thumbnail: {}", e.what()); - } - - return buf; -} - -void OCIOColourPipeline::extend_pixel_info( - media_reader::PixelInfo &pixel_info, const media::AVFrameID &frame_id) { - - if (pixel_info.raw_channels_info().size() < 3) - return; - - try { - - MediaParams media_param = get_media_params(frame_id.source_uuid_, frame_id.params_); - - media_param.output_view = view_->value(); - - auto raw_info = pixel_info.raw_channels_info(); - - if (media_param.compute_hash() != last_pixel_probe_source_hash_) { - pixel_probe_to_display_proc_ = - make_display_processor(media_param, false)->getDefaultCPUProcessor(); - pixel_probe_to_lin_proc_ = - make_to_lin_processor(media_param)->getDefaultCPUProcessor(); - last_pixel_probe_source_hash_ = media_param.compute_hash(); - } - - // Update Dynamic Properties on the CPUProcessor instance - - try { - { - // Exposure - OCIO::DynamicPropertyRcPtr property = - pixel_probe_to_display_proc_->getDynamicProperty( - OCIO::DYNAMIC_PROPERTY_EXPOSURE); - OCIO::DynamicPropertyDoubleRcPtr exposure_prop = - OCIO::DynamicPropertyValue::AsDouble(property); - exposure_prop->setValue(exposure_->value()); - } - { - // Gamma - OCIO::DynamicPropertyRcPtr property = - pixel_probe_to_display_proc_->getDynamicProperty( - OCIO::DYNAMIC_PROPERTY_GAMMA); - OCIO::DynamicPropertyDoubleRcPtr gamma_prop = - OCIO::DynamicPropertyValue::AsDouble(property); - gamma_prop->setValue(enable_gamma_->value() ? gamma_->value() : 1.0f); - } - } catch ([[maybe_unused]] const OCIO::Exception &e) { - // TODO: ColSci - // Update when OCIO::CPUProcessor include hasDynamicProperty() - } - - // Source - - if (!source_colour_space_->value().empty()) { - pixel_info.set_raw_colourspace_name( - std::string("Source (") + source_colour_space_->value() + std::string(")")); - } - - // Working space (scene_linear) - - { - float RGB[3] = { - raw_info[0].pixel_value, raw_info[1].pixel_value, raw_info[2].pixel_value}; - - pixel_probe_to_lin_proc_->applyRGB(RGB); - - pixel_info.add_linear_channel_info(raw_info[0].channel_name, RGB[0]); - pixel_info.add_linear_channel_info(raw_info[1].channel_name, RGB[1]); - pixel_info.add_linear_channel_info(raw_info[2].channel_name, RGB[2]); - - pixel_info.set_linear_colourspace_name( - std::string("Scene Linear (") + working_space(media_param) + std::string(")")); - } - - // Display output - - { - float RGB[3] = { - raw_info[0].pixel_value, raw_info[1].pixel_value, raw_info[2].pixel_value}; - - pixel_probe_to_display_proc_->applyRGB(RGB); - - // TODO: ColSci - // Saturation is not managed by OCIO currently - if (saturation_->value() != 1.0) { - const float W[3] = {0.2126f, 0.7152f, 0.0722f}; - const float LUMA = RGB[0] * W[0] + RGB[1] * W[1] + RGB[2] * W[2]; - RGB[0] = LUMA + saturation_->value() * (RGB[0] - LUMA); - RGB[1] = LUMA + saturation_->value() * (RGB[1] - LUMA); - RGB[2] = LUMA + saturation_->value() * (RGB[2] - LUMA); - } - - pixel_info.add_display_rgb_info("R", RGB[0]); - pixel_info.add_display_rgb_info("G", RGB[1]); - pixel_info.add_display_rgb_info("B", RGB[2]); - - pixel_info.set_display_colourspace_name( - std::string("Display (") + media_param.output_view + std::string("|") + - display_->value() + std::string(")")); - } - - } catch (const std::exception &e) { - spdlog::warn("OCIOColourPipeline: Failed to compute pixel probe: {}", e.what()); - } -} - -void OCIOColourPipeline::init_media_params(MediaParams &media_param) const { - - const auto &metadata = media_param.metadata; - - const std::string config_name = metadata.get_or("ocio_config", std::string("")); - - media_param.ocio_config = load_ocio_config(config_name); - media_param.ocio_config_name = config_name; - media_param.output_view = preferred_ocio_view(media_param, preferred_view_->value()); - - if (metadata.contains("active_displays")) { - const std::string displays = metadata.get_or("active_displays", std::string("")); - - auto config = media_param.ocio_config->createEditableCopy(); - config->setActiveDisplays(displays.c_str()); - media_param.ocio_config = config; - } - if (metadata.contains("active_views")) { - const std::string views = metadata.get_or("active_views", std::string("")); - - auto config = media_param.ocio_config->createEditableCopy(); - config->setActiveViews(views.c_str()); - media_param.ocio_config = config; - } -} - -OCIOColourPipeline::MediaParams OCIOColourPipeline::get_media_params( - const utility::Uuid &source_uuid, const utility::JsonStore &colour_params) const { - - // Create an entry if empty and initialize the OCIO config. - if (media_params_.find(source_uuid) == media_params_.end()) { - MediaParams media_param; - media_param.source_uuid = source_uuid; - media_param.metadata = colour_params; - init_media_params(media_param); - media_params_[source_uuid] = media_param; - } - // Update and reload OCIO config if source metadata have changed. - else { - MediaParams &media_param = media_params_[source_uuid]; - if (not colour_params.is_null() and media_param.metadata != colour_params) { - media_param.metadata = colour_params; - init_media_params(media_param); - } - } - - return media_params_[source_uuid]; -} - -void OCIOColourPipeline::set_media_params(const MediaParams &new_media_param) const { - media_params_[new_media_param.source_uuid] = new_media_param; -} - -std::string OCIOColourPipeline::input_space_for_view( - const MediaParams &media_param, const std::string &view) const { - - std::string new_colourspace; - - auto colourspace_or = [media_param](const std::string &cs, const std::string &fallback) { - const bool has_cs = bool(media_param.ocio_config->getColorSpace(cs.c_str())); - return has_cs ? cs : fallback; - }; - - if (media_param.metadata.contains("input_category")) { - const auto is_untonemapped = view == "Un-tone-mapped"; - const auto category = media_param.metadata["input_category"]; - if (category == "internal_movie") { - new_colourspace = is_untonemapped ? "disp_Rec709-G24" - : colourspace_or("DNEG_Rec709", "Film_Rec709"); - } else if (category == "edit_ref" or category == "movie_media") { - new_colourspace = is_untonemapped ? "disp_Rec709-G24" - : colourspace_or("Client_Rec709", "Film_Rec709"); - } else if (category == "still_media") { - new_colourspace = - is_untonemapped ? "disp_sRGB" : colourspace_or("DNEG_sRGB", "Film_sRGB"); - } - - // Double check the new colourspace actually exists - new_colourspace = colourspace_or(new_colourspace, ""); - } - - return new_colourspace; -} - -std::string OCIOColourPipeline::preferred_ocio_view( - const MediaParams &media_param, const std::string &ocio_view) const { - - // Get the default display from OCIO config - const OCIO::ConstConfigRcPtr ocio_config = media_param.ocio_config; - const std::string default_display = ocio_config->getDefaultDisplay(); - const std::string default_view = ocio_config->getDefaultView(default_display.c_str()); - - std::string preferred_view; - if (ocio_view == ui_text_.DEFAULT_VIEW) { - preferred_view = default_view; - } else if (ocio_view == ui_text_.AUTOMATIC_VIEW) { - preferred_view = media_param.metadata.get_or("automatic_view", default_view); - } else { - preferred_view = ocio_view; - } - - // Validate that the view is in ocio config - std::map> display_views; - const auto displays = parse_display_views(ocio_config, display_views); - - for (auto it = begin(display_views); it != end(display_views); ++it) { - for (auto view_in_ocio_config : it->second) { - if (view_in_ocio_config == preferred_view) { - return preferred_view; - } - } - } - // If view is not avaialble return the default view - return ocio_config->getDefaultView(default_display.c_str()); -} - -OCIO::ConstConfigRcPtr -OCIOColourPipeline::load_ocio_config(const std::string &config_name) const { - - auto it = ocio_config_cache_.find(config_name); - if (it != ocio_config_cache_.end()) { - return it->second; - } - - // This specific OCIO config has not been loaded yet. - OCIO::ConstConfigRcPtr config; - - try { - if (config_name == "__raw__") { - config = OCIO::Config::CreateRaw(); - } else if (config_name == "__current__") { - config = OCIO::GetCurrentConfig(); - } else if (!config_name.empty()) { - config = OCIO::Config::CreateFromFile(config_name.c_str()); - } else { - config = OCIO::GetCurrentConfig(); - } - } catch (const std::exception &e) { - spdlog::warn( - "OCIOColourPipeline: Failed to load OCIO config {}: {}", config_name, e.what()); - spdlog::warn("OCIOColourPipeline: Fallback on raw config"); - config = OCIO::Config::CreateRaw(); - } - - ocio_config_cache_[config_name] = config; - - return config; -} - -const char *OCIOColourPipeline::working_space(const MediaParams &media_param) const { - if (not media_param.ocio_config) { - return ""; - } else if (media_param.ocio_config->hasRole(OCIO::ROLE_SCENE_LINEAR)) { - return OCIO::ROLE_SCENE_LINEAR; - } else { - return OCIO::ROLE_DEFAULT; - } -} - -const char *OCIOColourPipeline::default_display( - const MediaParams &media_param, const std::string &monitor_name) const { - if (media_param.metadata.get_or("viewing_rules", false)) { - return dneg_ocio_default_display(media_param.ocio_config, monitor_name).c_str(); - } else { - return media_param.ocio_config->getDefaultDisplay(); - } -} - -// Return the transform to bring incoming data to scene_linear space. -OCIO::TransformRcPtr -OCIOColourPipeline::source_transform(const MediaParams &media_param) const { - - const std::string working_cs = working_space(media_param); - - const std::string user_input_cs = media_param.user_input_cs; - const std::string user_display = media_param.user_input_display; - const std::string user_view = media_param.user_input_view; - - const std::string filepath = media_param.metadata.get_or("path", std::string("")); - const std::string display = media_param.metadata.get_or("input_display", std::string("")); - const std::string view = media_param.metadata.get_or("input_view", std::string("")); - - // Expand input_colorspace to the first valid colorspace found. - std::string input_cs = media_param.metadata.get_or("input_colorspace", std::string("")); - - for (const auto &cs : xstudio::utility::split(input_cs, ':')) { - if (media_param.ocio_config->getColorSpace(cs.c_str())) { - input_cs = cs; - break; - } - } - - if (!user_input_cs.empty()) { - OCIO::ColorSpaceTransformRcPtr csc = OCIO::ColorSpaceTransform::Create(); - csc->setSrc(user_input_cs.c_str()); - csc->setDst(working_cs.c_str()); - return csc; - } else if (!user_display.empty() and !user_view.empty()) { - return display_transform( - working_cs, user_display, user_view, OCIO::TRANSFORM_DIR_INVERSE); - } else if (!input_cs.empty()) { - OCIO::ColorSpaceTransformRcPtr csc = OCIO::ColorSpaceTransform::Create(); - csc->setSrc(input_cs.c_str()); - csc->setDst(working_cs.c_str()); - return csc; - } else if (!display.empty() and !view.empty()) { - return display_transform(working_cs, display, view, OCIO::TRANSFORM_DIR_INVERSE); - } else { - std::string auto_input_cs; - - // FileRules, ignore if only matched by the default rule - if (!media_param.ocio_config->filepathOnlyMatchesDefaultRule(filepath.c_str())) { - auto_input_cs = - media_param.ocio_config->getColorSpaceFromFilepath(filepath.c_str()); - } - - // Manual file type (int8, int16, float, etc) to role / colorspace mapping - // => Not Implemented - - // Fallback to default rule - auto_input_cs = media_param.ocio_config->getColorSpaceFromFilepath(filepath.c_str()); - - OCIO::ColorSpaceTransformRcPtr csc = OCIO::ColorSpaceTransform::Create(); - csc->setSrc(auto_input_cs.c_str()); - csc->setDst(working_cs.c_str()); - return csc; - } -} - -OCIO::ConstConfigRcPtr OCIOColourPipeline::display_transform( - const MediaParams &media_param, - OCIO::ContextRcPtr context, - OCIO::GroupTransformRcPtr group) const { - - const auto &metadata = media_param.metadata; - auto ocio_config = media_param.ocio_config; - const auto &ocio_config_name = media_param.ocio_config_name; - - // Determines which OCIO display / view to use - - std::string display; - std::string view; - - if (ocio_config_name == current_config_name_ || is_worker()) { - // if we are a worker, our view has been set by the main - // OCIOColourPipeline, we don't need to worry about fallback - // to defaults etc. - display = display_->value(); - view = global_view_->value() ? view_->value() : media_param.output_view; - } else { - auto it = per_config_settings_.find(ocio_config_name); - if (it != per_config_settings_.end()) { - display = it->second.display; - view = global_view_->value() ? it->second.view : media_param.output_view; - } - } - - if (display.empty() or view.empty()) { - display = ocio_config->getDefaultDisplay(); - view = ocio_config->getDefaultView(display.c_str()); - } - - // Turns per shot CDLs into dynamic transform - - std::string view_looks = ocio_config->getDisplayViewLooks(display.c_str(), view.c_str()); - std::string dynamic_look; - std::string dynamic_file; - - if (metadata.contains("dynamic_cdl")) { - if (metadata["dynamic_cdl"].is_object()) { - for (auto &item : metadata["dynamic_cdl"].items()) { - if (view_looks.find(item.key()) != std::string::npos) { - dynamic_look = item.key(); - dynamic_file = item.value(); - } - } - } else { - spdlog::warn( - "OCIOColourPipeline: 'dynamic_cdl' should be a dictionary, got {} instead", - metadata["dynamic_cdl"].dump(2)); - } - } - - if (!dynamic_look.empty() and !dynamic_file.empty()) { - ocio_config = make_dynamic_display_processor( - media_param, - ocio_config, - context, - group, - display, - view, - dynamic_look, - dynamic_file); - } else { - group->appendTransform(display_transform( - working_space(media_param), display, view, OCIO::TRANSFORM_DIR_FORWARD)); - } - - return ocio_config; -} - -OCIO::TransformRcPtr OCIOColourPipeline::display_transform( - const std::string &source, - const std::string &display, - const std::string &view, - OCIO::TransformDirection direction) const { - - OCIO::DisplayViewTransformRcPtr dt = OCIO::DisplayViewTransform::Create(); - dt->setSrc(source.c_str()); - dt->setDisplay(display.c_str()); - dt->setView(view.c_str()); - dt->setDirection(direction); - - return dt; -} - -OCIO::TransformRcPtr OCIOColourPipeline::identity_transform() const { - return OCIO::MatrixTransform::Create(); -} - -OCIO::ContextRcPtr -OCIOColourPipeline::setup_ocio_context(const MediaParams &media_param) const { - - const auto &metadata = media_param.metadata; - const auto &ocio_config = media_param.ocio_config; - - OCIO::ContextRcPtr context = ocio_config->getCurrentContext()->createEditableCopy(); - - // Setup the OCIO context based on incoming metadata - - if (metadata.contains("ocio_context")) { - if (metadata["ocio_context"].is_object()) { - for (auto &item : metadata["ocio_context"].items()) { - context->setStringVar(item.key().c_str(), std::string(item.value()).c_str()); - } - } else { - spdlog::warn( - "OCIOColourPipeline: 'ocio_context' should be a dictionary, got {} instead", - metadata["ocio_context"].dump(2)); - } - } - - return context; -} - -OCIO::ConstProcessorRcPtr -OCIOColourPipeline::make_to_lin_processor(const MediaParams &media_param) const { - - const auto &metadata = media_param.metadata; - const auto &ocio_config = media_param.ocio_config; - const auto &ocio_config_name = media_param.ocio_config_name; - - try { - - if (colour_bypass_->value()) { - return ocio_config->getProcessor(identity_transform()); - } - - OCIO::GroupTransformRcPtr group = OCIO::GroupTransform::Create(); - group->appendTransform(source_transform(media_param)); - - OCIO::ContextRcPtr context = setup_ocio_context(media_param); - return ocio_config->getProcessor(context, group, OCIO::TRANSFORM_DIR_FORWARD); - - } catch (const std::exception &e) { - spdlog::warn( - "OCIOColourPipeline: Failed to construct OCIO lin processor: {}", e.what()); - spdlog::warn("OCIOColourPipeline: Defaulting to no-op processor"); - return ocio_config->getProcessor(identity_transform()); - } -} - -OCIO::ConstProcessorRcPtr OCIOColourPipeline::make_display_processor( - const MediaParams &media_param, bool is_thumbnail) const { - - auto ocio_config = media_param.ocio_config; - - try { - - OCIO::ContextRcPtr context = setup_ocio_context(media_param); - - OCIO::GroupTransformRcPtr group = OCIO::GroupTransform::Create(); - - if (!is_thumbnail) { - auto ect = OCIO::ExposureContrastTransform::Create(); - ect->setStyle(OCIO::EXPOSURE_CONTRAST_LINEAR); - ect->setDirection(OCIO::TRANSFORM_DIR_FORWARD); - ect->makeExposureDynamic(); - ect->setExposure(exposure_->value()); - - group->appendTransform(ect); - } - - if (!colour_bypass_->value()) { - // To support dynamic CDLs we currently have to edit the OCIO - // config in place, hence the need to return the new config - // here to later query the processor from it. - ocio_config = display_transform(media_param, context, group); - } - - if (!is_thumbnail) { - auto ect = OCIO::ExposureContrastTransform::Create(); - ect->setStyle(OCIO::EXPOSURE_CONTRAST_LINEAR); - ect->setDirection(OCIO::TRANSFORM_DIR_INVERSE); - ect->makeGammaDynamic(); - // default gamma pivot in OCIO is 0.18, avoid divide by zero too - ect->setPivot(1.0f); - ect->setGamma(enable_gamma_->value() ? gamma_->value() : 1.0f); - - group->appendTransform(ect); - } - - return ocio_config->getProcessor(context, group, OCIO::TRANSFORM_DIR_FORWARD); - - } catch (const std::exception &e) { - if (media_param.ocio_config_name != "__raw__") { - spdlog::warn( - "OCIOColourPipeline: Failed to construct OCIO processor: {}", e.what()); - spdlog::warn("OCIOColourPipeline: Defaulting to no-op processor"); - } - return ocio_config->getProcessor(identity_transform()); - } -} - -OCIO::ConstConfigRcPtr OCIOColourPipeline::make_dynamic_display_processor( - const MediaParams &media_param, - const OCIO::ConstConfigRcPtr &config, - const OCIO::ConstContextRcPtr &context, - const OCIO::GroupTransformRcPtr &group, - const std::string &display, - const std::string &view, - const std::string &look_name, - const std::string &cdl_file_name) const { - - try { - // Load the CDL and derive a GradingPrimary from it - auto look_path = context->resolveFileLocation(cdl_file_name.c_str()); - auto cdl_transform = OCIO::CDLTransform::CreateFromFile(look_path, ""); - - - // Update the MediaParams here so that each shots gets to know it's - // own GradingPrimary value to be used as uniforms. The pipeline data - // will otherwise be shared. - MediaParams updated_media_params = media_param; - auto primary = grading_primary_from_cdl(cdl_transform); - - updated_media_params.primary = primary; - set_media_params(updated_media_params); - - // Create a dynamic version of the look - auto dynamic_config = config->createEditableCopy(); - - auto dynamic_look = config->getLook(look_name.c_str())->createEditableCopy(); - std::string dynamic_look_name = std::string("__xstudio__dynamic_") + look_name; - dynamic_look->setName(dynamic_look_name.c_str()); - - auto forward_transform = dynamic_look->getTransform(); - if (forward_transform) { - dynamic_look->setTransform( - to_dynamic_transform(forward_transform, primary, cdl_file_name)); - } - auto inverse_transform = dynamic_look->getInverseTransform(); - if (inverse_transform) { - dynamic_look->setInverseTransform( - to_dynamic_transform(inverse_transform, primary, cdl_file_name)); - } - - dynamic_config->addLook(dynamic_look); - - // Add a view using the dynamic look - std::string colorspace_name = - dynamic_config->getDisplayViewColorSpaceName(display.c_str(), view.c_str()); - std::string view_name = view + " Dynamic"; - std::string view_looks = config->getDisplayViewLooks(display.c_str(), view.c_str()); - view_looks = xstudio::utility::replace_once(view_looks, look_name, dynamic_look_name); - dynamic_config->addDisplayView( - display.c_str(), view_name.c_str(), colorspace_name.c_str(), view_looks.c_str()); - - group->appendTransform(display_transform( - working_space(media_param), display, view_name, OCIO::TRANSFORM_DIR_FORWARD)); - - return dynamic_config; - - } catch ([[maybe_unused]] const OCIO::Exception &ex) { - group->appendTransform(display_transform( - working_space(media_param), display, view, OCIO::TRANSFORM_DIR_FORWARD)); - - return config; - } -} - -OCIO::ConstGpuShaderDescRcPtr OCIOColourPipeline::make_shader( - OCIO::ConstProcessorRcPtr &processor, - const char *function_name, - const char *resource_prefix) const { - - OCIO::GpuShaderDescRcPtr shader_desc = OCIO::GpuShaderDesc::CreateShaderDesc(); - shader_desc->setLanguage(OCIO::GPU_LANGUAGE_GLSL_4_0); - shader_desc->setFunctionName(function_name); - shader_desc->setResourcePrefix(resource_prefix); - auto gpu_proc = processor->getDefaultGPUProcessor(); - gpu_proc->extractGpuShaderInfo(shader_desc); - return shader_desc; -} - -void OCIOColourPipeline::setup_textures( - OCIO::ConstGpuShaderDescRcPtr &shader_desc, ColourOperationDataPtr op_data) const { - - // Process 3D LUTs - const unsigned max_texture_3D = shader_desc->getNum3DTextures(); - for (unsigned idx = 0; idx < max_texture_3D; ++idx) { - const char *textureName = nullptr; - const char *samplerName = nullptr; - unsigned edgelen = 0; - OCIO::Interpolation interpolation = OCIO::INTERP_LINEAR; - - shader_desc->get3DTexture(idx, textureName, samplerName, edgelen, interpolation); - if (!textureName || !*textureName || !samplerName || !*samplerName || edgelen == 0) { - throw std::runtime_error( - "OCIO::ShaderDesc::get3DTexture - The texture data is corrupted"); - } - - const float *ocio_lut_data = nullptr; - shader_desc->get3DTextureValues(idx, ocio_lut_data); - if (!ocio_lut_data) { - throw std::runtime_error( - "OCIO::ShaderDesc::get3DTextureValues - The texture values are missing"); - } - - auto xs_dtype = LUTDescriptor::FLOAT32; - auto xs_channels = LUTDescriptor::RGB; - auto xs_interp = interpolation == OCIO::INTERP_LINEAR ? LUTDescriptor::LINEAR - : LUTDescriptor::NEAREST; - auto xs_lut = std::make_shared( - LUTDescriptor::Create3DLUT(edgelen, xs_dtype, xs_channels, xs_interp), samplerName); - - const int channels = 3; - const std::size_t data_size = edgelen * edgelen * edgelen * channels * sizeof(float); - auto *xs_lut_data = (float *)xs_lut->writeable_data(); - std::memcpy(xs_lut_data, ocio_lut_data, data_size); - - xs_lut->update_content_hash(); - op_data->luts_.push_back(xs_lut); - } - - // Process 1D LUTs - const unsigned max_texture_2D = shader_desc->getNumTextures(); - for (unsigned idx = 0; idx < max_texture_2D; ++idx) { - const char *textureName = nullptr; - const char *samplerName = nullptr; - unsigned width = 0; - unsigned height = 0; - OCIO::GpuShaderDesc::TextureType channel = OCIO::GpuShaderDesc::TEXTURE_RGB_CHANNEL; - OCIO::Interpolation interpolation = OCIO::INTERP_LINEAR; - - shader_desc->getTexture( - idx, textureName, samplerName, width, height, channel, interpolation); - - if (!textureName || !*textureName || !samplerName || !*samplerName || width == 0) { - throw std::runtime_error( - "OCIO::ShaderDesc::getTexture - The texture data is corrupted"); - } - - const float *ocio_lut_data = nullptr; - shader_desc->getTextureValues(idx, ocio_lut_data); - if (!ocio_lut_data) { - throw std::runtime_error( - "OCIO::ShaderDesc::getTextureValues - The texture values are missing"); - } - - auto xs_dtype = LUTDescriptor::FLOAT32; - auto xs_channels = channel == OCIO::GpuShaderCreator::TEXTURE_RED_CHANNEL - ? LUTDescriptor::RED - : LUTDescriptor::RGB; - auto xs_interp = interpolation == OCIO::INTERP_LINEAR ? LUTDescriptor::LINEAR - : LUTDescriptor::NEAREST; - auto xs_lut = std::make_shared( - height > 1 - ? LUTDescriptor::Create2DLUT(width, height, xs_dtype, xs_channels, xs_interp) - : LUTDescriptor::Create1DLUT(width, xs_dtype, xs_channels, xs_interp), - samplerName); - - const int channels = channel == OCIO::GpuShaderCreator::TEXTURE_RED_CHANNEL ? 1 : 3; - const std::size_t data_size = width * height * channels * sizeof(float); - auto *xs_lut_data = (float *)xs_lut->writeable_data(); - std::memcpy(xs_lut_data, ocio_lut_data, data_size); - - xs_lut->update_content_hash(); - op_data->luts_.push_back(xs_lut); - } -} - -void OCIOColourPipeline::update_dynamic_parameters( - OCIO::ConstGpuShaderDescRcPtr &shader, const MediaParams &media_param) const { - - // Exposure property - if (shader->hasDynamicProperty(OCIO::DYNAMIC_PROPERTY_EXPOSURE)) { - OCIO::DynamicPropertyRcPtr property = - shader->getDynamicProperty(OCIO::DYNAMIC_PROPERTY_EXPOSURE); - OCIO::DynamicPropertyDoubleRcPtr exposure_prop = - OCIO::DynamicPropertyValue::AsDouble(property); - exposure_prop->setValue(exposure_->value()); - } - // Gamma property - if (shader->hasDynamicProperty(OCIO::DYNAMIC_PROPERTY_GAMMA)) { - OCIO::DynamicPropertyRcPtr property = - shader->getDynamicProperty(OCIO::DYNAMIC_PROPERTY_GAMMA); - OCIO::DynamicPropertyDoubleRcPtr gamma_prop = - OCIO::DynamicPropertyValue::AsDouble(property); - gamma_prop->setValue(enable_gamma_->value() ? gamma_->value() : 1.0f); - } - // Shot CDL - if (shader->hasDynamicProperty(OCIO::DYNAMIC_PROPERTY_GRADING_PRIMARY)) { - - OCIO::DynamicPropertyRcPtr property = - shader->getDynamicProperty(OCIO::DYNAMIC_PROPERTY_GRADING_PRIMARY); - OCIO::DynamicPropertyGradingPrimaryRcPtr primary_prop = - OCIO::DynamicPropertyValue::AsGradingPrimary(property); - OCIO::GradingPrimary primary = media_param.primary; - primary_prop->setValue(primary); - } -} - -void OCIOColourPipeline::update_all_uniforms( - OCIO::ConstGpuShaderDescRcPtr &shader, - utility::JsonStore &uniforms, - const utility::Uuid &source_uuid) const { - - const unsigned max_uniforms = shader->getNumUniforms(); - - for (unsigned idx = 0; idx < max_uniforms; ++idx) { - OCIO::GpuShaderDesc::UniformData uniform_data; - const char *name = shader->getUniform(idx, uniform_data); - - switch (uniform_data.m_type) { - case OCIO::UNIFORM_DOUBLE: { - uniforms[name] = uniform_data.m_getDouble(); - break; - } - case OCIO::UNIFORM_BOOL: { - uniforms[name] = uniform_data.m_getBool(); - break; - } - case OCIO::UNIFORM_FLOAT3: { - uniforms[name] = { - "vec3", - 1, - uniform_data.m_getFloat3()[0], - uniform_data.m_getFloat3()[1], - uniform_data.m_getFloat3()[2]}; - break; - } - // Note, the below has not been tested and might require adjustments. - // This can show up if we start using more advanced dynamic grading. - // case OCIO::UNIFORM_VECTOR_FLOAT:{ - // std::vector vector_float; - // for (int i = 0; i < uniform_data.m_vectorFloat.m_getSize(); i++){ - // vector_float.push_back(uniform_data.m_vectorFloat.m_getVector()[i]); - // } - // pipe_data.shader_parameters_[name] = vector_float; - // break; - // } - // case OCIO::UNIFORM_VECTOR_INT:{ - // std::vector vector_int; - // for (int i = 0; i < uniform_data.m_vectorInt.m_getSize(); i++){ - // vector_int.push_back(uniform_data.m_vectorInt.m_getVector()[i]); - // } - // pipe_data.shader_parameters_[name] = vector_int; - // break; - // } - default: - spdlog::warn("OCIOColourPipeline: Unknown uniform type for dynamic property"); - break; - } - } -} - -extern "C" { -plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { - return new plugin_manager::PluginFactoryCollection( - std::vector>( - {std::make_shared>( - PLUGIN_UUID, - "OCIOColourPipeline", - plugin_manager::PluginFlags::PF_COLOUR_MANAGEMENT, - false, - "xStudio", - "OCIO (v2) Colour Pipeline", - semver::version("1.0.0"))})); -} -} \ No newline at end of file diff --git a/src/plugin/colour_pipeline/ocio/src/ocio.hpp b/src/plugin/colour_pipeline/ocio/src/ocio.hpp deleted file mode 100644 index 3625024f6..000000000 --- a/src/plugin/colour_pipeline/ocio/src/ocio.hpp +++ /dev/null @@ -1,264 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include //NOLINT - -#include "xstudio/colour_pipeline/colour_pipeline_actor.hpp" -#include "xstudio/plugin_manager/plugin_manager.hpp" -#include "xstudio/utility/logging.hpp" -#include "xstudio/global_store/global_store.hpp" -#include "xstudio/thumbnail/thumbnail.hpp" -#include "ui_text.hpp" - -namespace OCIO = OCIO_NAMESPACE; - - -namespace xstudio::colour_pipeline { - -class OCIOColourPipeline : public ColourPipeline { - private: - struct PerConfigSettings { - std::string display; - std::string view; - }; - - struct MediaParams { - utility::Uuid source_uuid; - - OCIO::ConstConfigRcPtr ocio_config; - std::string ocio_config_name; - - utility::JsonStore metadata; - std::string user_input_cs; - std::string user_input_display; - std::string user_input_view; - - OCIO::GradingPrimary primary = OCIO::GradingPrimary(OCIO::GRADING_LIN); - - std::string output_view; - - std::string compute_hash() const; - }; - - struct ShaderDescriptor { - OCIO::ConstGpuShaderDescRcPtr shader_desc; - MediaParams params; - std::mutex mutex; - }; - typedef std::shared_ptr ShaderDescriptorPtr; - - public: - explicit OCIOColourPipeline( - caf::actor_config &cfg, const utility::JsonStore &init_settings); - - void on_exit() override; - - std::string fast_display_transform_hash(const media::AVFrameID &media_ptr) override; - - [[nodiscard]] std::string linearise_op_hash( - const utility::Uuid &source_uuid, - const utility::JsonStore &media_source_colour_metadata) override; - - [[nodiscard]] std::string linear_to_display_op_hash( - const utility::Uuid &source_uuid, - const utility::JsonStore &media_source_colour_metadata) override; - - /* Create the ColourOperationDataPtr containing the necessary LUT and - shader data for linearising the source colourspace RGB data from the - given media source on the screen */ - ColourOperationDataPtr linearise_op_data( - const utility::Uuid &source_uuid, - const utility::JsonStore &media_source_colour_metadata) override; - - /* Create the ColourOperationDataPtr containing the necessary LUT and - shader data for transforming linear colour values into display space */ - ColourOperationDataPtr linear_to_display_op_data( - const utility::Uuid &source_uuid, - const utility::JsonStore &media_source_colour_metadata) override; - - // Update colour pipeline shader dynamic parameters. - utility::JsonStore update_shader_uniforms( - const media_reader::ImageBufPtr &image, std::any &user_data) override; - - thumbnail::ThumbnailBufferPtr process_thumbnail( - const media::AVFrameID &media_ptr, const thumbnail::ThumbnailBufferPtr &buf) override; - - // GUI handling - void media_source_changed( - const utility::Uuid &source_uuid, const utility::JsonStore &colour_params) override; - void attribute_changed(const utility::Uuid &attribute_uuid, const int /*role*/) override; - void hotkey_pressed(const utility::Uuid &hotkey_uuid, const std::string &context) override; - void hotkey_released(const utility::Uuid &hotkey_uuid, const std::string &context) override; - bool pointer_event(const ui::PointerEvent &e) override; - void screen_changed( - const std::string &name, - const std::string &model, - const std::string &manufacturer, - const std::string &serialNumber) override; - - void register_hotkeys() override; - - void connect_to_viewport( - const std::string &viewport_name, - const std::string &viewport_toolbar_name, - bool connect) override; - - void extend_pixel_info( - media_reader::PixelInfo &pixel_info, const media::AVFrameID &frame_id) override; - - // CAF details - bool allow_workers() const override { return true; } - - caf::actor self_spawn(const utility::JsonStore &s) override { - return spawn(s); - } - - private: - void init_media_params(MediaParams &media_param) const; - - MediaParams get_media_params( - const utility::Uuid &source_uuid, - const utility::JsonStore &colour_params = utility::JsonStore()) const; - - void set_media_params(const MediaParams &media_param) const; - - // OCIO logic - - std::string - input_space_for_view(const MediaParams &media_param, const std::string &view) const; - - std::string - preferred_ocio_view(const MediaParams &media_param, const std::string &view) const; - - const char *working_space(const MediaParams &media_param) const; - - const char * - default_display(const MediaParams &media_param, const std::string &monitor_name = "") const; - - // OCIO Transform helpers - - OCIO::TransformRcPtr source_transform(const MediaParams &media_param) const; - - OCIO::ConstConfigRcPtr display_transform( - const MediaParams &media_param, - OCIO::ContextRcPtr context, - OCIO::GroupTransformRcPtr group) const; - - OCIO::TransformRcPtr display_transform( - const std::string &source, - const std::string &display, - const std::string &view, - OCIO::TransformDirection direction) const; - - OCIO::TransformRcPtr identity_transform() const; - - // OCIO setup - - OCIO::ConstConfigRcPtr load_ocio_config(const std::string &config_name) const; - - OCIO::ContextRcPtr setup_ocio_context(const MediaParams &media_param) const; - - OCIO::ConstProcessorRcPtr make_to_lin_processor(const MediaParams &media_param) const; - - OCIO::ConstProcessorRcPtr - make_display_processor(const MediaParams &media_param, bool is_thumbnail) const; - - OCIO::ConstConfigRcPtr make_dynamic_display_processor( - const MediaParams &media_param, - const OCIO::ConstConfigRcPtr &config, - const OCIO::ConstContextRcPtr &context, - const OCIO::GroupTransformRcPtr &group, - const std::string &display, - const std::string &view, - const std::string &look_name, - const std::string &cdl_file_name) const; - - OCIO::ConstGpuShaderDescRcPtr make_shader( - OCIO::ConstProcessorRcPtr &processor, - const char *function_name, - const char *resource_prefix) const; - - void setup_textures( - OCIO::ConstGpuShaderDescRcPtr &shader_desc, ColourOperationDataPtr op_data) const; - - // OCIO dynamic properties - - void update_dynamic_parameters( - OCIO::ConstGpuShaderDescRcPtr &shader, const MediaParams &media_param) const; - - void update_all_uniforms( - OCIO::ConstGpuShaderDescRcPtr &shader, - utility::JsonStore &uniforms, - const utility::Uuid &source_uuid) const; - - // GUI handling - - void setup_ui(); - - void populate_ui(const MediaParams &media_param); - - std::vector parse_display_views( - OCIO::ConstConfigRcPtr ocio_config, - std::map> &view_map) const; - - std::vector parse_all_colourspaces(OCIO::ConstConfigRcPtr ocio_config) const; - - void update_cs_from_view(const MediaParams &media_param, const std::string &view); - - void update_views(OCIO::ConstConfigRcPtr ocio_config); - - void update_bypass(module::StringChoiceAttribute *viewer, bool bypass); - - private: - mutable std::map ocio_config_cache_; - mutable std::map media_params_; - mutable std::map per_config_settings_; - - // GUI handling - bool ui_initialized_ = false; - UiText ui_text_; - - module::StringChoiceAttribute *channel_; - module::StringChoiceAttribute *display_; - module::StringChoiceAttribute *view_; - module::FloatAttribute *exposure_; - module::FloatAttribute *gamma_; - module::FloatAttribute *saturation_; - module::StringChoiceAttribute *source_colour_space_; - module::BooleanAttribute *colour_bypass_; - module::StringChoiceAttribute *preferred_view_; - module::BooleanAttribute *global_view_; - module::BooleanAttribute *adjust_source_; - module::BooleanAttribute *enable_gamma_; - module::BooleanAttribute *enable_saturation_; - - std::map channel_hotkeys_; - utility::Uuid exposure_hotkey_; - utility::Uuid gamma_hotkey_; - utility::Uuid saturation_hotkey_; - utility::Uuid reset_hotkey_; - - // Holds info about the currently on screen media - utility::Uuid current_source_uuid_; - std::string current_config_name_; - MediaParams current_source_media_params_; - - // Holds data on display screen option - std::string monitor_name_; - - // Pixel probe - std::string last_pixel_probe_source_hash_; - OCIO::ConstCPUProcessorRcPtr pixel_probe_to_display_proc_; - OCIO::ConstCPUProcessorRcPtr pixel_probe_to_lin_proc_; -}; - -} // namespace xstudio::colour_pipeline diff --git a/src/plugin/colour_pipeline/ocio/src/ocio_engine.cpp b/src/plugin/colour_pipeline/ocio/src/ocio_engine.cpp new file mode 100644 index 000000000..4db68b756 --- /dev/null +++ b/src/plugin/colour_pipeline/ocio/src/ocio_engine.cpp @@ -0,0 +1,1298 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "ocio_engine.hpp" + +#include "xstudio/utility/string_helpers.hpp" +#include "xstudio/ui/opengl/shader_program_base.hpp" +#include "xstudio/thumbnail/thumbnail.hpp" + +#include "shaders.hpp" + +using namespace xstudio::colour_pipeline; +using namespace xstudio::colour_pipeline::ocio; +using namespace xstudio; + + +namespace { +const static utility::Uuid PLUGIN_UUID{"b39d1e3d-58f8-475f-82c1-081a048df705"}; + +OCIO::GradingPrimary grading_primary_from_cdl(const OCIO::ConstCDLTransformRcPtr &cdl) { + OCIO::GradingPrimary gp(OCIO::GRADING_LIN); + + std::array slope; + std::array offset; + std::array power; + double sat; + + cdl->getSlope(slope.data()); + cdl->getOffset(offset.data()); + cdl->getPower(power.data()); + sat = cdl->getSat(); + + // TODO: ColSci + // GradingPrimary with only saturation is currently considered identity + // Temporary workaround until OCIO fix the issue by adding a clamp to + // an arbitrary low value. + if (slope == decltype(slope){1.0, 1.0, 1.0} && offset == decltype(offset){0.0, 0.0, 0.0} && + power == decltype(power){1.0, 1.0, 1.0} && sat != 1.0) { + gp.m_clampBlack = -1e6; + } + + for (int i = 0; i < 3; ++i) { + offset[i] = offset[i] / slope[i]; + slope[i] = std::log2(slope[i]); + } + + gp.m_offset = OCIO::GradingRGBM(offset[0], offset[1], offset[2], 0.0); + gp.m_exposure = OCIO::GradingRGBM(slope[0], slope[1], slope[2], 0.0); + gp.m_contrast = OCIO::GradingRGBM(power[0], power[1], power[2], 1.0); + gp.m_saturation = sat; + gp.m_pivot = std::log2(1.0 / 0.18); + + return gp; +} + +OCIO::TransformRcPtr to_dynamic_transform( + const OCIO::ConstTransformRcPtr &transform, + OCIOEngine::DynamicCDLMap &dynamic_cdls, + int &dynamic_cdls_start_idx) { + if (transform->getTransformType() == OCIO::TRANSFORM_TYPE_GROUP) { + auto group = OCIO::DynamicPtrCast(transform); + auto new_group = OCIO::GroupTransform::Create(); + + int num_transforms = group->getNumTransforms(); + for (int i = 0; i < num_transforms; ++i) { + auto dyn_transform = to_dynamic_transform( + group->getTransform(i), dynamic_cdls, dynamic_cdls_start_idx); + new_group->appendTransform(dyn_transform); + } + return OCIO::DynamicPtrCast(new_group); + } else if (transform->getTransformType() == OCIO::TRANSFORM_TYPE_FILE) { + auto file = OCIO::DynamicPtrCast(transform); + + for (const auto &[name, dynamic_cdl] : dynamic_cdls) { + if (std::string(file->getSrc()) == dynamic_cdl.file_name) { + if (dynamic_cdl.id == -1) { + dynamic_cdls[name].id = dynamic_cdls_start_idx++; + } + int cdl_id = dynamic_cdls[name].id; + + OCIO::GradingPrimary cdl_gp(OCIO::GRADING_LIN); + cdl_gp.m_offset = OCIO::GradingRGBM(cdl_id, cdl_id, cdl_id, 0.0); + + auto gp = OCIO::GradingPrimaryTransform::Create(OCIO::GRADING_LIN); + gp->setValue(cdl_gp); + + return OCIO::DynamicPtrCast(gp); + } + } + } + + return transform->createEditableCopy(); +} + +OCIO::ConfigRcPtr to_dynamic_config( + const OCIO::ConstConfigRcPtr &config, OCIOEngine::DynamicCDLMap &dynamic_cdls) { + + // Need at least v2 for GradingPrimaryTransform + auto dynamic_config = config->createEditableCopy(); + if (dynamic_config->getMajorVersion() < 2) { + dynamic_config->setMajorVersion(2); + dynamic_config->setMinorVersion(0); + } + + // Turn every dynamic CDL FileTransform into GradingPrimary + + int dynamic_cdls_start_idx = 4096; + + // Grab all transforms from the ColorSpaces. + OCIO::SearchReferenceSpaceType cs_type = OCIO::SEARCH_REFERENCE_SPACE_ALL; + OCIO::ColorSpaceVisibility cs_vis = OCIO::COLORSPACE_ALL; + for (int i = 0; i < dynamic_config->getNumColorSpaces(cs_type, cs_vis); ++i) { + + const auto cs_name = dynamic_config->getColorSpaceNameByIndex(cs_type, cs_vis, i); + auto cs = dynamic_config->getColorSpace(cs_name); + auto cs_edit = cs->createEditableCopy(); + + auto to_ref = cs->getTransform(OCIO::COLORSPACE_DIR_TO_REFERENCE); + if (to_ref) { + auto new_tr = to_dynamic_transform(to_ref, dynamic_cdls, dynamic_cdls_start_idx); + cs_edit->setTransform(new_tr, OCIO::COLORSPACE_DIR_TO_REFERENCE); + } + + auto from_ref = cs->getTransform(OCIO::COLORSPACE_DIR_FROM_REFERENCE); + if (from_ref) { + auto new_tr = to_dynamic_transform(from_ref, dynamic_cdls, dynamic_cdls_start_idx); + cs_edit->setTransform(new_tr, OCIO::COLORSPACE_DIR_FROM_REFERENCE); + } + + dynamic_config->addColorSpace(cs_edit); + } + + // Grab all transforms from the Looks. + for (int i = 0; i < dynamic_config->getNumLooks(); ++i) { + + auto look = dynamic_config->getLook(dynamic_config->getLookNameByIndex(i)); + auto look_edit = look->createEditableCopy(); + + auto fwd_tr = look->getTransform(); + if (fwd_tr) { + auto new_tr = to_dynamic_transform(fwd_tr, dynamic_cdls, dynamic_cdls_start_idx); + look_edit->setTransform(new_tr); + } + + auto inv_tr = look->getInverseTransform(); + if (inv_tr) { + auto new_tr = to_dynamic_transform(inv_tr, dynamic_cdls, dynamic_cdls_start_idx); + look_edit->setInverseTransform(new_tr); + } + + dynamic_config->addLook(look_edit); + } + + // Grab all transforms from the view transforms. + for (int i = 0; i < dynamic_config->getNumViewTransforms(); ++i) { + + const auto vt_name = dynamic_config->getViewTransformNameByIndex(i); + auto vt = dynamic_config->getViewTransform(vt_name); + auto vt_edit = vt->createEditableCopy(); + + auto to_ref = vt->getTransform(OCIO::VIEWTRANSFORM_DIR_TO_REFERENCE); + if (to_ref) { + auto new_tr = to_dynamic_transform(to_ref, dynamic_cdls, dynamic_cdls_start_idx); + vt_edit->setTransform(new_tr, OCIO::VIEWTRANSFORM_DIR_TO_REFERENCE); + } + + auto from_ref = vt->getTransform(OCIO::VIEWTRANSFORM_DIR_FROM_REFERENCE); + if (from_ref) { + auto new_tr = to_dynamic_transform(from_ref, dynamic_cdls, dynamic_cdls_start_idx); + vt_edit->setTransform(new_tr, OCIO::VIEWTRANSFORM_DIR_FROM_REFERENCE); + } + + dynamic_config->addViewTransform(vt_edit); + } + + // Grab all transforms from the named transforms. + for (int i = 0; i < dynamic_config->getNumNamedTransforms(); ++i) { + + const auto nt_name = dynamic_config->getNamedTransformNameByIndex(i); + auto nt = dynamic_config->getNamedTransform(nt_name); + auto nt_edit = nt->createEditableCopy(); + + auto fwd_tr = nt->getTransform(OCIO::TRANSFORM_DIR_FORWARD); + if (fwd_tr) { + auto new_tr = to_dynamic_transform(fwd_tr, dynamic_cdls, dynamic_cdls_start_idx); + nt_edit->setTransform(new_tr, OCIO::TRANSFORM_DIR_FORWARD); + } + + auto inv_tr = nt->getTransform(OCIO::TRANSFORM_DIR_INVERSE); + if (inv_tr) { + auto new_tr = to_dynamic_transform(inv_tr, dynamic_cdls, dynamic_cdls_start_idx); + nt_edit->setTransform(new_tr, OCIO::TRANSFORM_DIR_INVERSE); + } + + dynamic_config->addNamedTransform(nt_edit); + } + + return dynamic_config; +} + +struct ShaderDescriptor { + OCIO::ConstGpuShaderDescRcPtr shader_desc; + OCIOEngine::DynamicCDLMap dynamic_cdls; + std::mutex mutex; +}; +typedef std::shared_ptr ShaderDescriptorPtr; + +} // anonymous namespace + +ColourOperationDataPtr OCIOEngine::linearise_op_data( + const utility::JsonStore &src_colour_mgmt_metadata, + const bool bypass, + const bool auto_adjust_source_cs, + const std::string &view) { + + using xstudio::utility::replace_once; + + auto result = std::make_shared("OCIO Linearise OP"); + + // Construct OCIO processor, shader and extract texture(s) + auto to_lin_proc = + make_to_lin_processor(src_colour_mgmt_metadata, bypass, auto_adjust_source_cs, view); + auto to_lin_shader = make_shader(to_lin_proc, "OCIOLinearise", "to_linear_"); + setup_textures(to_lin_shader, result); + + std::string to_lin_shader_src = replace_once( + ShaderTemplates::OCIO_linearise, "//OCIOLinearise", to_lin_shader->getShaderText()); + + result->shader_ = std::make_shared( + utility::Uuid::generate(), to_lin_shader_src); + + // Store GPUShaderDesc objects for later use during uniform binding / update. + // Note that we need updated MediaParams object, which may include primary grading + // data, when we update the shader uniforms at draw time. Hence we add the MediaParams + // as part of the user_data_ blind data object. + auto desc = std::make_shared(); + desc->shader_desc = to_lin_shader; + result->user_data_ = desc; + result->set_cache_id(to_string(PLUGIN_UUID) + to_lin_proc->getCacheID()); + + return result; +} + +ColourOperationDataPtr OCIOEngine::linear_to_display_op_data( + const utility::JsonStore &src_colour_mgmt_metadata, + const std::string &display, + const std::string &view, + const bool bypass) { + + using xstudio::utility::replace_once; + + auto data = std::make_shared(ColourOperationData("OCIO Display OP")); + + // Construct OCIO processor, shader and extract texture(s) + DynamicCDLMap dynamic_cdls; + + auto display_proc = make_display_processor( + src_colour_mgmt_metadata, false, display, view, dynamic_cdls, bypass); + auto display_shader = make_shader(display_proc, "OCIODisplay", "to_display"); + setup_textures(display_shader, data); + + std::string shader_text = display_shader->getShaderText(); + shader_text = patch_dynamic_cdls(shader_text, dynamic_cdls); + + std::string display_shader_src = + replace_once(ShaderTemplates::OCIO_display, "//OCIODisplay", shader_text); + + data->shader_ = std::make_shared( + utility::Uuid::generate(), display_shader_src); + + // Store GPUShaderDesc objects for later use during uniform binding / update. + // Note that we need updated MediaParams object, which may include primary grading + // data, when we update the shader uniforms at draw time. Hence we add the MediaParams + // as part of the user_data_ blind data object. + + auto desc = std::make_shared(); + desc->shader_desc = display_shader; + desc->dynamic_cdls = dynamic_cdls; + data->user_data_ = desc; + + std::stringstream ss; + ss << to_string(PLUGIN_UUID); + ss << display_proc->getCacheID(); + for (const auto &[name, dynamic_cdl] : dynamic_cdls) { + ss << dynamic_cdl.grading_primary; + } + data->set_cache_id(ss.str()); + + return data; +} + +thumbnail::ThumbnailBufferPtr OCIOEngine::process_thumbnail( + const utility::JsonStore &src_colour_mgmt_metadata, + const thumbnail::ThumbnailBufferPtr &buf, + const std::string &display, + const std::string &view) { + + if (buf->format() != thumbnail::TF_RGBF96) { + throw std::runtime_error( + "OCIOEngine: Invalid thumbnail buffer type, expects TF_RGBF96"); + } + + // Create to_lin and to_display processors and concatenate them + + const auto &ocio_config = get_ocio_config(src_colour_mgmt_metadata); + + auto to_lin_group = make_to_lin_processor(src_colour_mgmt_metadata, false, false, view) + ->createGroupTransform(); + DynamicCDLMap dynamic_cdls; + auto to_display_group = + make_display_processor(src_colour_mgmt_metadata, true, display, view, dynamic_cdls) + ->createGroupTransform(); + + OCIO::GroupTransformRcPtr concat_group = OCIO::GroupTransform::Create(); + concat_group->appendTransform(to_lin_group); + concat_group->appendTransform(to_display_group); + + auto cpu_proc = + ocio_config->getProcessor(concat_group) + ->getOptimizedCPUProcessor( + OCIO::BIT_DEPTH_F32, OCIO::BIT_DEPTH_UINT8, OCIO::OPTIMIZATION_DEFAULT); + + auto thumb = std::make_shared( + buf->width(), buf->height(), thumbnail::TF_RGB24); + auto src = reinterpret_cast(buf->data().data()); + auto dst = reinterpret_cast(thumb->data().data()); + + OCIO::PackedImageDesc in_img( + src, + buf->width(), + buf->height(), + buf->channels(), + OCIO::BIT_DEPTH_F32, + OCIO::AutoStride, + OCIO::AutoStride, + OCIO::AutoStride); + + OCIO::PackedImageDesc out_img( + dst, + thumb->width(), + thumb->height(), + thumb->channels(), + OCIO::BIT_DEPTH_UINT8, + OCIO::AutoStride, + OCIO::AutoStride, + OCIO::AutoStride); + + cpu_proc->apply(in_img, out_img); + + return thumb; +} + +void OCIOEngine::extend_pixel_info( + media_reader::PixelInfo &pixel_info, + const media::AVFrameID &frame_id, + const std::string &display, + const std::string &view, + const bool auto_adjust_source, + const float exposure, + const float gamma, + const float saturation) { + + if (pixel_info.raw_channels_info().size() < 3) + return; + + auto raw_info = pixel_info.raw_channels_info(); + + if (compute_hash(frame_id.params()) != last_pixel_probe_source_hash_) { + DynamicCDLMap dynamic_cdls; + auto display_proc = + make_display_processor(frame_id.params(), false, display, view, dynamic_cdls); + pixel_probe_to_display_proc_ = display_proc->getDefaultCPUProcessor(); + pixel_probe_to_lin_proc_ = + make_to_lin_processor(frame_id.params(), false, auto_adjust_source, view) + ->getDefaultCPUProcessor(); + last_pixel_probe_source_hash_ = compute_hash(frame_id.params()); + } + + // Update Dynamic Properties on the CPUProcessor instance + try { + { + // Exposure + OCIO::DynamicPropertyRcPtr property = + pixel_probe_to_display_proc_->getDynamicProperty( + OCIO::DYNAMIC_PROPERTY_EXPOSURE); + OCIO::DynamicPropertyDoubleRcPtr exposure_prop = + OCIO::DynamicPropertyValue::AsDouble(property); + exposure_prop->setValue(exposure); + } + { + // Gamma + OCIO::DynamicPropertyRcPtr property = + pixel_probe_to_display_proc_->getDynamicProperty(OCIO::DYNAMIC_PROPERTY_GAMMA); + OCIO::DynamicPropertyDoubleRcPtr gamma_prop = + OCIO::DynamicPropertyValue::AsDouble(property); + gamma_prop->setValue(gamma); + } + } catch (const OCIO::Exception &e) { + // TODO: ColSci + // Update when OCIO::CPUProcessor include hasDynamicProperty() + } + + // Source + + std::string source_cs = + detect_source_colourspace(frame_id.params(), auto_adjust_source, view); + + if (!source_cs.empty()) { + + pixel_info.set_raw_colourspace_name( + std::string("Source (") + source_cs + std::string(")")); + } + + // Working space (scene_linear) + + { + float RGB[3] = { + raw_info[0].pixel_value, raw_info[1].pixel_value, raw_info[2].pixel_value}; + + pixel_probe_to_lin_proc_->applyRGB(RGB); + + pixel_info.add_linear_channel_info(raw_info[0].channel_name, RGB[0]); + pixel_info.add_linear_channel_info(raw_info[1].channel_name, RGB[1]); + pixel_info.add_linear_channel_info(raw_info[2].channel_name, RGB[2]); + + pixel_info.set_linear_colourspace_name( + std::string("Scene Linear (") + working_space(frame_id.params()) + + std::string(")")); + } + + // Display output + + { + float RGB[3] = { + raw_info[0].pixel_value, raw_info[1].pixel_value, raw_info[2].pixel_value}; + + pixel_probe_to_display_proc_->applyRGB(RGB); + + // TODO: ColSci + // Saturation is not managed by OCIO currently + if (saturation != 1.0) { + const float W[3] = {0.2126f, 0.7152f, 0.0722f}; + const float LUMA = RGB[0] * W[0] + RGB[1] * W[1] + RGB[2] * W[2]; + RGB[0] = LUMA + saturation * (RGB[0] - LUMA); + RGB[1] = LUMA + saturation * (RGB[1] - LUMA); + RGB[2] = LUMA + saturation * (RGB[2] - LUMA); + } + + pixel_info.add_display_rgb_info("R", RGB[0]); + pixel_info.add_display_rgb_info("G", RGB[1]); + pixel_info.add_display_rgb_info("B", RGB[2]); + + pixel_info.set_display_colourspace_name( + std::string("Display (") + view + std::string("|") + display + std::string(")")); + } +} + +OCIO::ConstConfigRcPtr +OCIOEngine::get_ocio_config(const utility::JsonStore &src_colour_mgmt_metadata) const { + + const std::string config_name = + src_colour_mgmt_metadata.get_or("ocio_config", std::string("")); + const std::string displays = + src_colour_mgmt_metadata.get_or("active_displays", std::string("")); + const std::string views = src_colour_mgmt_metadata.get_or("active_views", std::string("")); + const std::string concat = config_name + displays + views; + + auto it = ocio_config_cache_.find(concat); + if (it != ocio_config_cache_.end()) { + return it->second; + } + + // This specific OCIO config has not been loaded yet. + OCIO::ConstConfigRcPtr config; + + try { + if (config_name == "__raw__") { + config = OCIO::Config::CreateRaw(); + } else if (config_name == "__current__") { + config = OCIO::GetCurrentConfig(); + } else if (!config_name.empty()) { + +#if OCIO_VERSION_HEX >= 0x02020100 + // CreateFromBuiltinConfig introduced in OCIO v2.2 + if (fs::exists(config_name)) { + config = OCIO::Config::CreateFromFile(config_name.c_str()); + } else { + config = OCIO::Config::CreateFromBuiltinConfig(config_name.c_str()); + } +#else + config = OCIO::Config::CreateFromFile(config_name.c_str()); +#endif + + } else { + config = OCIO::GetCurrentConfig(); + } + } catch (const std::exception &e) { + spdlog::warn("OCIOEngine: Failed to load OCIO config {}: {}", config_name, e.what()); + spdlog::warn("OCIOEngine: Fallback on raw config"); + config = OCIO::Config::CreateRaw(); + } + + auto econfig = config->createEditableCopy(); + econfig->setName(concat.c_str()); + if (!displays.empty()) + econfig->setActiveDisplays(displays.c_str()); + if (!views.empty()) + econfig->setActiveViews(views.c_str()); + config = econfig; + ocio_config_cache_[concat] = config; + + return config; +} + +const char * +OCIOEngine::working_space(const utility::JsonStore &src_colour_mgmt_metadata) const { + auto config = get_ocio_config(src_colour_mgmt_metadata); + if (not config) { + return ""; + } else if (config->hasRole(OCIO::ROLE_SCENE_LINEAR)) { + return OCIO::ROLE_SCENE_LINEAR; + } else { + return OCIO::ROLE_DEFAULT; + } +} + +std::string OCIOEngine::input_space_for_view( + const utility::JsonStore &src_colour_mgmt_metadata, const std::string &view) const { + + auto config = get_ocio_config(src_colour_mgmt_metadata); + const std::string empty; + std::string new_colourspace; + + auto colourspace_or = [config](const std::string &cs, const std::string &fallback) { + const bool has_cs = bool(config->getColorSpace(cs.c_str())); + return has_cs ? cs : fallback; + }; + + const auto is_untonemapped = view == "Un-tone-mapped"; + const auto input_space = src_colour_mgmt_metadata.get_or("input_colorspace", empty); + const auto untonemapped_space = + src_colour_mgmt_metadata.get_or("untonemapped_colorspace", empty); + + new_colourspace = is_untonemapped ? untonemapped_space : input_space; + + for (const auto &cs : xstudio::utility::split(new_colourspace, ':')) { + new_colourspace = colourspace_or(new_colourspace, cs); + } + + // Double check the new colourspace actually exists + new_colourspace = colourspace_or(new_colourspace, ""); + + // Avoid role names, helps with the source colour space menu + if (!new_colourspace.empty()) { + new_colourspace = config->getCanonicalName(new_colourspace.c_str()); + } + + return new_colourspace; +} + +const char * +OCIOEngine::default_display(const utility::JsonStore &src_colour_mgmt_metadata) const { + + auto config = get_ocio_config(src_colour_mgmt_metadata); + return config->getDefaultDisplay(); +} + +// Return the transform to bring incoming data to scene_linear space. +OCIO::TransformRcPtr OCIOEngine::source_transform( + const utility::JsonStore &src_colour_mgmt_metadata, + const bool auto_adjust_source, + const std::string &view_for_auto_adjust) const { + + const std::string working_cs = working_space(src_colour_mgmt_metadata); + auto config = get_ocio_config(src_colour_mgmt_metadata); + + // get the user source colourspace override, if it has been set + std::string override_input_cs = + src_colour_mgmt_metadata.get_or("override_input_cs", std::string("")); + + if (auto_adjust_source) { + // When 'auto adjust source' global setting is on, the source colourspace + // is overriden by our own logic + override_input_cs = + input_space_for_view(src_colour_mgmt_metadata, view_for_auto_adjust); + } + + const std::string filepath = src_colour_mgmt_metadata.get_or("path", std::string("")); + const std::string display = + src_colour_mgmt_metadata.get_or("input_display", std::string("")); + const std::string view = src_colour_mgmt_metadata.get_or("input_view", std::string("")); + + // Expand input_colorspace to the first valid colorspace found. + std::string input_cs = src_colour_mgmt_metadata.get_or("input_colorspace", std::string("")); + + for (const auto &cs : xstudio::utility::split(input_cs, ':')) { + if (config->getColorSpace(cs.c_str())) { + input_cs = cs; + break; + } + } + + if (!override_input_cs.empty()) { + OCIO::ColorSpaceTransformRcPtr csc = OCIO::ColorSpaceTransform::Create(); + csc->setSrc(override_input_cs.c_str()); + csc->setDst(working_cs.c_str()); + return csc; + } else if (!input_cs.empty()) { + OCIO::ColorSpaceTransformRcPtr csc = OCIO::ColorSpaceTransform::Create(); + csc->setSrc(input_cs.c_str()); + csc->setDst(working_cs.c_str()); + return csc; + } else if (!display.empty() and !view.empty()) { + return display_transform(working_cs, display, view, OCIO::TRANSFORM_DIR_INVERSE); + } else { + std::string auto_input_cs; + + // FileRules, ignore if only matched by the default rule + if (!config->filepathOnlyMatchesDefaultRule(filepath.c_str())) { + auto_input_cs = config->getColorSpaceFromFilepath(filepath.c_str()); + } + + // Manual file type (int8, int16, float, etc) to role / colorspace mapping + // => Not Implemented + + // Fallback to default rule + auto_input_cs = config->getColorSpaceFromFilepath(filepath.c_str()); + + OCIO::ColorSpaceTransformRcPtr csc = OCIO::ColorSpaceTransform::Create(); + csc->setSrc(auto_input_cs.c_str()); + csc->setDst(working_cs.c_str()); + return csc; + } +} + +OCIO::ConstConfigRcPtr OCIOEngine::display_transform( + const utility::JsonStore &src_colour_mgmt_metadata, + OCIO::ContextRcPtr context, + OCIO::GroupTransformRcPtr group, + std::string display, + std::string view, + DynamicCDLMap &dynamic_cdls) const { + + auto ocio_config = get_ocio_config(src_colour_mgmt_metadata); + + // Determines which OCIO display / view to use + + if (display.empty() or view.empty()) { + display = ocio_config->getDefaultDisplay(); + view = ocio_config->getDefaultView(display.c_str()); + } + + // Turns per shot CDLs into dynamic transform + // This happen in two steps: + // - First, we create a new config where every FileTransform used that + // points to identified CDLs sources (``dynamic_cdl`` plugin param) + // are replaced by GradingPrimary Transform. + // - Then after processor and shader generation, manual patch of the + // GLSL to turn GradingPrimary transforms into dynamic transforms + // using uniforms. + // While it is not strictly needed to use GradingPrimaryTransform as + // a placehodler for the dynamic CDL code, it prevents OCIO optimizer + // from merging with neighbors transforms and might make the migration + // easier when/if OCIO implements support for multiple dynamic + // GradingPrimary transforms within a processor. + + if (src_colour_mgmt_metadata.contains("dynamic_cdl")) { + if (src_colour_mgmt_metadata["dynamic_cdl"].is_object()) { + for (auto &item : src_colour_mgmt_metadata["dynamic_cdl"].items()) { + try { + // Make sure the file is a CDL + auto file_path = + context->resolveFileLocation(std::string(item.value()).c_str()); + OCIO::CDLTransform::CreateFromFile(file_path, "" /* cccid */); + + const std::string xstudio_name = fmt::format("xstudio_cdl_{}", item.key()); + dynamic_cdls[xstudio_name].look_name = item.key(); + dynamic_cdls[xstudio_name].file_name = item.value(); + dynamic_cdls[xstudio_name].resolved_path = file_path; + } catch (const OCIO::Exception &e) { + } + } + } else { + spdlog::warn( + "OCIOEngine: 'dynamic_cdl' should be a dictionary, got {} instead", + src_colour_mgmt_metadata["dynamic_cdl"].dump(2)); + } + } + + if (!dynamic_cdls.empty()) { + ocio_config = make_dynamic_display_processor( + src_colour_mgmt_metadata, ocio_config, group, display, view, dynamic_cdls); + } else { + group->appendTransform(display_transform( + working_space(src_colour_mgmt_metadata), + display, + view, + OCIO::TRANSFORM_DIR_FORWARD)); + } + + return ocio_config; +} + +OCIO::TransformRcPtr OCIOEngine::display_transform( + const std::string &source, + const std::string &display, + const std::string &view, + OCIO::TransformDirection direction) const { + + OCIO::DisplayViewTransformRcPtr dt = OCIO::DisplayViewTransform::Create(); + dt->setSrc(source.c_str()); + dt->setDisplay(display.c_str()); + dt->setView(view.c_str()); + dt->setDirection(direction); + + return dt; +} + +OCIO::TransformRcPtr OCIOEngine::identity_transform() const { + return OCIO::MatrixTransform::Create(); +} + +OCIO::ContextRcPtr OCIOEngine::setup_ocio_context(const utility::JsonStore &metadata) const { + + const auto &ocio_config = get_ocio_config(metadata); + + OCIO::ContextRcPtr context = ocio_config->getCurrentContext()->createEditableCopy(); + + // Setup the OCIO context based on incoming metadata + + if (metadata.contains("ocio_context")) { + if (metadata["ocio_context"].is_object()) { + for (auto &item : metadata["ocio_context"].items()) { + context->setStringVar(item.key().c_str(), std::string(item.value()).c_str()); + } + } else { + spdlog::warn( + "OCIOEngine: 'ocio_context' should be a dictionary, got {} instead", + metadata["ocio_context"].dump(2)); + } + } + + return context; +} + +OCIO::ConstProcessorRcPtr OCIOEngine::make_to_lin_processor( + const utility::JsonStore &src_colour_mgmt_metadata, + const bool bypass, + const bool auto_adjust_source, + const std::string &view) const { + + const auto &ocio_config = get_ocio_config(src_colour_mgmt_metadata); + + try { + + if (bypass) { + return ocio_config->getProcessor(identity_transform()); + } + + OCIO::GroupTransformRcPtr group = OCIO::GroupTransform::Create(); + group->appendTransform( + source_transform(src_colour_mgmt_metadata, auto_adjust_source, view)); + + OCIO::ContextRcPtr context = setup_ocio_context(src_colour_mgmt_metadata); + return ocio_config->getProcessor(context, group, OCIO::TRANSFORM_DIR_FORWARD); + + } catch (const std::exception &e) { + spdlog::warn("OCIOEngine: Failed to construct OCIO lin processor: {}", e.what()); + spdlog::warn("OCIOEngine: Defaulting to no-op processor"); + return ocio_config->getProcessor(identity_transform()); + } +} + +OCIO::ConstProcessorRcPtr OCIOEngine::make_display_processor( + const utility::JsonStore &src_colour_mgmt_metadata, + bool is_thumbnail, + const std::string &display, + const std::string &view, + DynamicCDLMap &dynamic_cdls, + const bool bypass) const { + + auto ocio_config = get_ocio_config(src_colour_mgmt_metadata); + + try { + + OCIO::ContextRcPtr context = setup_ocio_context(src_colour_mgmt_metadata); + + OCIO::GroupTransformRcPtr group = OCIO::GroupTransform::Create(); + + { + auto ect = OCIO::ExposureContrastTransform::Create(); + ect->setStyle(OCIO::EXPOSURE_CONTRAST_LINEAR); + ect->setDirection(OCIO::TRANSFORM_DIR_FORWARD); + ect->makeExposureDynamic(); + ect->setExposure(1.0); + group->appendTransform(ect); + } + + if (!bypass) { + // To support dynamic CDLs we currently have to edit the OCIO + // config in place, hence the need to return the new config + // here to later query the processor from it. + + ocio_config = display_transform( + src_colour_mgmt_metadata, context, group, display, view, dynamic_cdls); + } + + { + auto ect = OCIO::ExposureContrastTransform::Create(); + ect->setStyle(OCIO::EXPOSURE_CONTRAST_LINEAR); + ect->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + ect->makeGammaDynamic(); + // default gamma pivot in OCIO is 0.18, avoid divide by zero too + ect->setPivot(1.0f); + ect->setGamma(1.0f); + group->appendTransform(ect); + } + + return ocio_config->getProcessor(context, group, OCIO::TRANSFORM_DIR_FORWARD); + + } catch (const std::exception &e) { + spdlog::warn("OCIOEngine: Failed to construct OCIO processor: {}", e.what()); + spdlog::warn("OCIOEngine: Defaulting to no-op processor"); + return ocio_config->getProcessor(identity_transform()); + } +} + +OCIO::ConstConfigRcPtr OCIOEngine::make_dynamic_display_processor( + const utility::JsonStore &src_colour_mgmt_metadata, + const OCIO::ConstConfigRcPtr &config, + const OCIO::GroupTransformRcPtr &group, + const std::string &display, + const std::string &view, + DynamicCDLMap &dynamic_cdls) const { + + auto dynamic_config = to_dynamic_config(config, dynamic_cdls); + + + group->appendTransform(display_transform( + working_space(src_colour_mgmt_metadata), display, view, OCIO::TRANSFORM_DIR_FORWARD)); + + return dynamic_config; +} + +OCIO::ConstGpuShaderDescRcPtr OCIOEngine::make_shader( + OCIO::ConstProcessorRcPtr &processor, + const char *function_name, + const char *resource_prefix) const { + + OCIO::GpuShaderDescRcPtr shader_desc = OCIO::GpuShaderDesc::CreateShaderDesc(); + shader_desc->setLanguage(OCIO::GPU_LANGUAGE_GLSL_4_0); + shader_desc->setFunctionName(function_name); + shader_desc->setResourcePrefix(resource_prefix); + auto gpu_proc = processor->getDefaultGPUProcessor(); + gpu_proc->extractGpuShaderInfo(shader_desc); + return shader_desc; +} + +std::string OCIOEngine::patch_dynamic_cdls( + const std::string &shader_text, DynamicCDLMap &dynamic_cdls) const { + + std::vector new_shader_lines; + + auto lines = utility::split(shader_text, '\n'); + for (int i = 0; i < lines.size(); ++i) { + if (utility::starts_with(lines[i], " // Add GradingPrimary")) { + for (auto &[name, dynamic_cdl] : dynamic_cdls) { + if (lines[i + 3].find(std::to_string(dynamic_cdl.id)) != std::string::npos) { + + std::string new_code = + R"( // Add GradingPrimary 'linear' forward processing for __name__ + + { + if (!__name___localBypass) + { + outColor.rgb += __name___offset; + outColor.rgb *= __name___exposure; + if ( __name___contrast != vec3(1., 1., 1.) ) + { + outColor.rgb = pow( abs(outColor.rgb / __name___pivot), __name___contrast ) * sign(outColor.rgb) * __name___pivot; + } + vec3 lumaWgts = vec3(0.212599993, 0.715200007, 0.0722000003); + float luma = dot( outColor.rgb, lumaWgts ); + outColor.rgb = luma + __name___saturation * (outColor.rgb - luma); + outColor.rgb = clamp( outColor.rgb, __name___clampBlack, __name___clampWhite ); + } + } +)"; + new_code = utility::replace_all(new_code, "__name__", name); + auto new_code_lines = utility::split(new_code, '\n'); + new_shader_lines.insert( + new_shader_lines.end(), new_code_lines.begin(), new_code_lines.end()); + + std::string new_declarations = + R"( +uniform vec3 __name___offset; +uniform vec3 __name___exposure; +uniform vec3 __name___contrast; +uniform float __name___pivot; +uniform float __name___clampBlack; +uniform float __name___clampWhite; +uniform float __name___saturation; +uniform bool __name___localBypass;)"; + + new_declarations = utility::replace_all(new_declarations, "__name__", name); + new_code_lines = utility::split(new_declarations, '\n'); + new_shader_lines.insert( + new_shader_lines.begin(), new_code_lines.begin(), new_code_lines.end()); + + auto cdl_transform = OCIO::CDLTransform::CreateFromFile( + dynamic_cdl.resolved_path.c_str(), ""); + auto gp = grading_primary_from_cdl(cdl_transform); + dynamic_cdls[name].grading_primary = gp; + dynamic_cdls[name].in_use = true; + + // Find the offset to the closing scope and skip lines + int offset_to_end = 0; + bool offset_found = false; + while (!offset_found) { + if (lines[i + offset_to_end] == std::string(" }")) { + offset_found = true; + } else { + offset_to_end++; + } + } + i = i + offset_to_end; + + break; + } + } + } else { + new_shader_lines.push_back(lines[i]); + } + } + + auto shader = utility::join_as_string(new_shader_lines, "\n"); + + return shader; +} + +void OCIOEngine::setup_textures( + OCIO::ConstGpuShaderDescRcPtr &shader_desc, ColourOperationDataPtr op_data) const { + + // Process 3D LUTs + const unsigned max_texture_3D = shader_desc->getNum3DTextures(); + for (unsigned idx = 0; idx < max_texture_3D; ++idx) { + const char *textureName = nullptr; + const char *samplerName = nullptr; + unsigned edgelen = 0; + OCIO::Interpolation interpolation = OCIO::INTERP_LINEAR; + + shader_desc->get3DTexture(idx, textureName, samplerName, edgelen, interpolation); + if (!textureName || !*textureName || !samplerName || !*samplerName || edgelen == 0) { + throw std::runtime_error( + "OCIO::ShaderDesc::get3DTexture - The texture data is corrupted"); + } + + const float *ocio_lut_data = nullptr; + shader_desc->get3DTextureValues(idx, ocio_lut_data); + if (!ocio_lut_data) { + throw std::runtime_error( + "OCIO::ShaderDesc::get3DTextureValues - The texture values are missing"); + } + + auto xs_dtype = LUTDescriptor::FLOAT32; + auto xs_channels = LUTDescriptor::RGB; + auto xs_interp = interpolation == OCIO::INTERP_LINEAR ? LUTDescriptor::LINEAR + : LUTDescriptor::NEAREST; + auto xs_lut = std::make_shared( + LUTDescriptor::Create3DLUT(edgelen, xs_dtype, xs_channels, xs_interp), samplerName); + + const int channels = 3; + const std::size_t data_size = edgelen * edgelen * edgelen * channels * sizeof(float); + auto *xs_lut_data = (float *)xs_lut->writeable_data(); + std::memcpy(xs_lut_data, ocio_lut_data, data_size); + + xs_lut->update_content_hash(); + op_data->luts_.push_back(xs_lut); + } + + // Process 1D LUTs + const unsigned max_texture_2D = shader_desc->getNumTextures(); + for (unsigned idx = 0; idx < max_texture_2D; ++idx) { + const char *textureName = nullptr; + const char *samplerName = nullptr; + unsigned width = 0; + unsigned height = 0; + OCIO::GpuShaderDesc::TextureType channel = OCIO::GpuShaderDesc::TEXTURE_RGB_CHANNEL; + OCIO::Interpolation interpolation = OCIO::INTERP_LINEAR; + + shader_desc->getTexture( + idx, textureName, samplerName, width, height, channel, interpolation); + + if (!textureName || !*textureName || !samplerName || !*samplerName || width == 0) { + throw std::runtime_error( + "OCIO::ShaderDesc::getTexture - The texture data is corrupted"); + } + + const float *ocio_lut_data = nullptr; + shader_desc->getTextureValues(idx, ocio_lut_data); + if (!ocio_lut_data) { + throw std::runtime_error( + "OCIO::ShaderDesc::getTextureValues - The texture values are missing"); + } + + auto xs_dtype = LUTDescriptor::FLOAT32; + auto xs_channels = channel == OCIO::GpuShaderCreator::TEXTURE_RED_CHANNEL + ? LUTDescriptor::RED + : LUTDescriptor::RGB; + auto xs_interp = interpolation == OCIO::INTERP_LINEAR ? LUTDescriptor::LINEAR + : LUTDescriptor::NEAREST; + auto xs_lut = std::make_shared( + height > 1 + ? LUTDescriptor::Create2DLUT(width, height, xs_dtype, xs_channels, xs_interp) + : LUTDescriptor::Create1DLUT(width, xs_dtype, xs_channels, xs_interp), + samplerName); + + const int channels = channel == OCIO::GpuShaderCreator::TEXTURE_RED_CHANNEL ? 1 : 3; + const std::size_t data_size = width * height * channels * sizeof(float); + auto *xs_lut_data = (float *)xs_lut->writeable_data(); + std::memcpy(xs_lut_data, ocio_lut_data, data_size); + + xs_lut->update_content_hash(); + op_data->luts_.push_back(xs_lut); + } +} + +std::string OCIOEngine::detect_source_colourspace( + const utility::JsonStore &src_colour_mgmt_metadata, + const bool auto_adjust_source, + const std::string &view) { + + // Extract the input colorspace as detected by the plugin and update the UI + std::string detected_cs; + OCIO::TransformRcPtr transform = + source_transform(src_colour_mgmt_metadata, auto_adjust_source, view); + if (transform->getTransformType() == OCIO::TRANSFORM_TYPE_COLORSPACE) { + OCIO::ColorSpaceTransformRcPtr csc = + std::static_pointer_cast(transform); + detected_cs = csc->getSrc(); + } else if (transform->getTransformType() == OCIO::TRANSFORM_TYPE_DISPLAY_VIEW) { + // TODO: ColSci + // Note that in case the view uses looks, the colour space returned here + // is not representing the input processing accurately and will be out + // of sync with what the input transform actually is (inverse display view). + OCIO::DisplayViewTransformRcPtr disp = + std::static_pointer_cast(transform); + auto config = get_ocio_config(src_colour_mgmt_metadata); + const std::string view_cs = + config->getDisplayViewColorSpaceName(disp->getDisplay(), disp->getView()); + detected_cs = view_cs; + } else { + spdlog::warn( + "OCIOColourPipeline: Internal error trying to extract source colour space."); + } + + return detected_cs; +} + +std::string OCIOEngine::preferred_view( + const utility::JsonStore &src_colour_mgmt_metadata, + const std::string user_preferred_view) const { + + auto ocio_config = get_ocio_config(src_colour_mgmt_metadata); + + // Get the default display from OCIO config + const std::string default_display = ocio_config->getDefaultDisplay(); + const std::string default_view = ocio_config->getDefaultView(default_display.c_str()); + + std::string _preferred_view; + if (user_preferred_view == "Default") { + _preferred_view = default_view; + } else if (user_preferred_view == "Automatic") { + _preferred_view = src_colour_mgmt_metadata.get_or("automatic_view", default_view); + } else { + _preferred_view = user_preferred_view; + } + + // Validate that the view is in ocio config + std::map> display_views; + std::vector displays, cs; + get_ocio_displays_view_colourspaces(src_colour_mgmt_metadata, cs, displays, display_views); + + for (auto it = begin(display_views); it != end(display_views); ++it) { + for (auto view_in_ocio_config : it->second) { + if (view_in_ocio_config == _preferred_view) { + return _preferred_view; + } + } + } + // If view is not avaialble return the default view + return ocio_config->getDefaultView(default_display.c_str()); +} + +void OCIOEngine::get_ocio_displays_view_colourspaces( + const utility::JsonStore &src_colour_mgmt_metadata, + std::vector &all_colourspaces, + std::vector &displays, + std::map> &display_views) const { + + auto ocio_config = get_ocio_config(src_colour_mgmt_metadata); + + all_colourspaces.clear(); + displays.clear(); + display_views.clear(); + + for (int i = 0; i < ocio_config->getNumDisplays(); ++i) { + const std::string display = ocio_config->getDisplay(i); + displays.push_back(display); + + display_views[display] = std::vector(); + for (int j = 0; j < ocio_config->getNumViews(display.c_str()); ++j) { + const std::string view = ocio_config->getView(display.c_str(), j); + display_views[display].push_back(view); + } + } + + for (int i = 0; i < ocio_config->getNumColorSpaces(); ++i) { + const char *cs_name = ocio_config->getColorSpaceNameByIndex(i); + if (cs_name && *cs_name) { + all_colourspaces.emplace_back(cs_name); + } + } +} + +void OCIOEngine::update_shader_uniforms( + std::any &user_data, + utility::JsonStore &uniforms, + const float exposure, + const float gamma) { + if (user_data.has_value() && user_data.type() == typeid(ShaderDescriptorPtr)) { + try { + auto shader = std::any_cast(user_data); + if (shader && shader->shader_desc) { + // ColourOperationDataPtr is shared among MediaSource using the same OCIO + // shader. Hence ShaderDesc objects holding OCIO DynamicProperty are shared too. + // DynamicProperty are used to hold the uniforms representing the transform, + // we need to update them before querying the updated uniforms. The mutex is + // needed to protect against multiple workers concurrently updating the + // ShaderDesc's DynamicProperty resulting in queried uniforms not reflecting + // values for the current shot. + std::scoped_lock lock(shader->mutex); + update_dynamic_parameters(shader->shader_desc, exposure, gamma); + update_ocio_uniforms(shader->shader_desc, uniforms); + update_xstudio_uniforms(shader->dynamic_cdls, uniforms); + } + } catch (const std::exception &e) { + spdlog::warn("OCIOColourPipeline: Failed to update shader uniforms: {}", e.what()); + } + } +} + +void OCIOEngine::update_ocio_uniforms( + OCIO::ConstGpuShaderDescRcPtr &shader, utility::JsonStore &uniforms) const { + + const unsigned max_uniforms = shader->getNumUniforms(); + + for (unsigned idx = 0; idx < max_uniforms; ++idx) { + OCIO::GpuShaderDesc::UniformData uniform_data; + const char *name = shader->getUniform(idx, uniform_data); + + switch (uniform_data.m_type) { + case OCIO::UNIFORM_DOUBLE: { + uniforms[name] = uniform_data.m_getDouble(); + break; + } + case OCIO::UNIFORM_BOOL: { + uniforms[name] = uniform_data.m_getBool(); + break; + } + case OCIO::UNIFORM_FLOAT3: { + uniforms[name] = { + "vec3", + 1, + uniform_data.m_getFloat3()[0], + uniform_data.m_getFloat3()[1], + uniform_data.m_getFloat3()[2]}; + break; + } + // Note, the below has not been tested and might require adjustments. + // This can show up if we start using more advanced dynamic grading. + // case OCIO::UNIFORM_VECTOR_FLOAT:{ + // std::vector vector_float; + // for (int i = 0; i < uniform_data.m_vectorFloat.m_getSize(); i++){ + // vector_float.push_back(uniform_data.m_vectorFloat.m_getVector()[i]); + // } + // pipe_data.shader_parameters_[name] = vector_float; + // break; + // } + // case OCIO::UNIFORM_VECTOR_INT:{ + // std::vector vector_int; + // for (int i = 0; i < uniform_data.m_vectorInt.m_getSize(); i++){ + // vector_int.push_back(uniform_data.m_vectorInt.m_getVector()[i]); + // } + // pipe_data.shader_parameters_[name] = vector_int; + // break; + // } + default: + spdlog::warn("OCIOEngine: Unknown uniform type for dynamic property"); + break; + } + } +} + +void OCIOEngine::update_xstudio_uniforms( + const DynamicCDLMap &dynamic_cdls, utility::JsonStore &uniforms) const { + + for (auto &[name, dynamic_cdl] : dynamic_cdls) { + if (dynamic_cdl.in_use) { + auto gp = dynamic_cdl.grading_primary; + + uniforms[name + "_offset"] = { + "vec3", 1, gp.m_offset.m_red, gp.m_offset.m_green, gp.m_offset.m_blue}; + uniforms[name + "_exposure"] = { + "vec3", + 1, + powf(2.f, gp.m_exposure.m_red), + powf(2.f, gp.m_exposure.m_green), + powf(2.f, gp.m_exposure.m_blue)}; + uniforms[name + "_contrast"] = { + "vec3", 1, gp.m_contrast.m_red, gp.m_contrast.m_green, gp.m_contrast.m_blue}; + uniforms[name + "_pivot"] = 0.18 * std::pow(2., gp.m_pivot); + uniforms[name + "_clampBlack"] = gp.m_clampBlack; + uniforms[name + "_clampWhite"] = gp.m_clampWhite; + uniforms[name + "_saturation"] = gp.m_saturation; + uniforms[name + "_localBypass"] = false; + } + } +} + +void OCIOEngine::update_dynamic_parameters( + OCIO::ConstGpuShaderDescRcPtr &shader, const float exposure, const float gamma) const { + + // Exposure property + if (shader->hasDynamicProperty(OCIO::DYNAMIC_PROPERTY_EXPOSURE)) { + OCIO::DynamicPropertyRcPtr property = + shader->getDynamicProperty(OCIO::DYNAMIC_PROPERTY_EXPOSURE); + OCIO::DynamicPropertyDoubleRcPtr exposure_prop = + OCIO::DynamicPropertyValue::AsDouble(property); + exposure_prop->setValue(exposure); + } + // Gamma property + if (shader->hasDynamicProperty(OCIO::DYNAMIC_PROPERTY_GAMMA)) { + OCIO::DynamicPropertyRcPtr property = + shader->getDynamicProperty(OCIO::DYNAMIC_PROPERTY_GAMMA); + OCIO::DynamicPropertyDoubleRcPtr gamma_prop = + OCIO::DynamicPropertyValue::AsDouble(property); + gamma_prop->setValue(gamma); + } +} + +size_t OCIOEngine::compute_hash( + const utility::JsonStore &src_colour_mgmt_metadata, const std::string &extra) const { + size_t hash = 0; + hash = std::hash{}(src_colour_mgmt_metadata.dump() + extra); + return hash; +} + +OCIOEngineActor::OCIOEngineActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { + + behavior_.assign( + [=](colour_pipe_display_data_atom, + const utility::JsonStore &media_metadata, + const std::string &display, + const std::string &view, + const bool bypass) -> result { + // This message is sent from main OCIOColourPipeline + try { + return m_engine_.linear_to_display_op_data( + media_metadata, display, view, bypass); + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } + }, + [=](colour_pipe_linearise_data_atom, + const utility::JsonStore &media_metadata, + const bool bypass, + const bool auto_adjust_source_cs, + const std::string &view) -> result { + // This message is sent from main OCIOColourPipeline + try { + return m_engine_.linearise_op_data( + media_metadata, bypass, auto_adjust_source_cs, view); + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } + }, + [=](media_reader::process_thumbnail_atom, + const utility::JsonStore &media_metadata, + const thumbnail::ThumbnailBufferPtr &buf, + const std::string &display, + const std::string &view) -> result { + // This message is sent from main OCIOColourPipeline + try { + return m_engine_.process_thumbnail(media_metadata, buf, display, view); + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } + }); +} diff --git a/src/plugin/colour_pipeline/ocio/src/ocio_engine.hpp b/src/plugin/colour_pipeline/ocio/src/ocio_engine.hpp new file mode 100644 index 000000000..8b17e446a --- /dev/null +++ b/src/plugin/colour_pipeline/ocio/src/ocio_engine.hpp @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include //NOLINT +#include +#include "xstudio/colour_pipeline/colour_pipeline.hpp" + +namespace OCIO = OCIO_NAMESPACE; + +namespace xstudio::colour_pipeline::ocio { + +/* Class providing an interface to the OCIO API providing functions required +by xstudio for colour management. */ +class OCIOEngine { + public: + struct DynamicCDL { + int id{-1}; + bool in_use{false}; + std::string look_name; + std::string file_name; + std::string resolved_path; + OCIO::GradingPrimary grading_primary{OCIO::GRADING_LIN}; + }; + using DynamicCDLMap = std::map; + + OCIOEngine() = default; + + /* Extend the PixelInfo object with information about colour space transform + and resulting RGB transformed values for PixelInfo HUD plugin */ + void extend_pixel_info( + media_reader::PixelInfo &pixel_info, + const media::AVFrameID &frame_id, + const std::string &display, + const std::string &view, + const bool auto_adjust_source, + const float exposure, + const float gamma, + const float saturation); + + /* Get the default OCIO display*/ + const char *default_display(const utility::JsonStore &src_colour_mgmt_metadata) const; + + /* Get the name (filesystem path) of the OCIO config that applied to the + given source colour management metadata*/ + std::string get_ocio_config_name(const utility::JsonStore &src_colour_mgmt_metadata) const { + return get_ocio_config(src_colour_mgmt_metadata)->getName(); + } + + /* Pick the preferred OCIO view for the given source colour management + metadata and a client-defined override preferred view*/ + std::string preferred_view( + const utility::JsonStore &src_colour_mgmt_metadata, + const std::string user_preferred_view) const; + + /* This function is executed just before rendering an image to screen. + In our case user_data carries the OCIO GPU shader desc and GradingPrimary + objects. We use the GradpingPrimary, exposure and gamma to update the + shader descriptor. We then use get the shader descriptor to tell us the + shader uniforms (names and values) which we then store in the 'uniforms' + object, later attached to the image. The xSTUDIO viewport opengl renderer + then uses the uniforms dict to set the shader uniforms at draw time. + */ + void update_shader_uniforms( + std::any &user_data, + utility::JsonStore &uniforms, + const float exposure, + const float gamma); + + /* For given media source colour metadata we fetch the appropriate OCIO + config and query it for possible source colour spaces, the list of available + displays and the views per display.*/ + void get_ocio_displays_view_colourspaces( + const utility::JsonStore &src_colour_mgmt_metadata, + std::vector &all_colourspaces, + std::vector &displays, + std::map> &display_views) const; + + /* Make a unique size_t from the source colour metadata that will change + when any aspect of the colour management of the source is expected to cahnge + the way the source will be transformed to display space.*/ + size_t compute_hash( + const utility::JsonStore &src_colour_mgmt_metadata, + const std::string &extra = std::string()) const; + + /* For given media source colour metadata determine the expected source + colourspace.*/ + std::string detect_source_colourspace( + const utility::JsonStore &src_colour_mgmt_metadata, + const bool auto_adjust_source, + const std::string &view); + + /* Select the most appropriate source colour space for the provided OCIO view */ + std::string input_space_for_view( + const utility::JsonStore &src_colour_mgmt_metadata, const std::string &view) const; + + /* For the given information about the frame returns the ColourOperationData + object with GPU shader and LUT data required for tranforming from + source colourspace to linear colourspace. */ + ColourOperationDataPtr linearise_op_data( + const utility::JsonStore &src_colour_mgmt_metadata, + const bool colour_bypass_, + const bool auto_adjust_source_cs, + const std::string &view); + + /* For the given information about the frame plus OCIO display and view + return the ColourOperationData object with GPU shader and LUT data required + for tranforming from linear colourspace to display colourspace*/ + ColourOperationDataPtr linear_to_display_op_data( + const utility::JsonStore &src_colour_mgmt_metadata, + const std::string &display, + const std::string &view, + const bool bypass); + + /*Process an RGB float format thumbnail image from the source colourspace + of the source media into display space*/ + thumbnail::ThumbnailBufferPtr process_thumbnail( + const utility::JsonStore &src_colour_mgmt_metadata, + const thumbnail::ThumbnailBufferPtr &buf, + const std::string &display, + const std::string &view); + + private: + // OCIO logic + const char *working_space(const utility::JsonStore &src_colour_mgmt_metadata) const; + + // OCIO Transform helpers + OCIO::TransformRcPtr source_transform( + const utility::JsonStore &src_colour_mgmt_metadata, + const bool auto_adjust_source, + const std::string &view) const; + + OCIO::ConstConfigRcPtr display_transform( + const utility::JsonStore &src_colour_mgmt_metadata, + OCIO::ContextRcPtr context, + OCIO::GroupTransformRcPtr group, + std::string display, + std::string view, + DynamicCDLMap &dynamic_cdls) const; + + OCIO::TransformRcPtr display_transform( + const std::string &source, + const std::string &display, + const std::string &view, + OCIO::TransformDirection direction) const; + + OCIO::TransformRcPtr identity_transform() const; + + // OCIO setup + + OCIO::ConstConfigRcPtr + get_ocio_config(const utility::JsonStore &src_colour_mgmt_metadata) const; + + OCIO::ContextRcPtr + setup_ocio_context(const utility::JsonStore &src_colour_mgmt_metadata) const; + + OCIO::ConstProcessorRcPtr make_to_lin_processor( + const utility::JsonStore &src_colour_mgmt_metadata, + const bool bypass, + const bool auto_adjust_source, + const std::string &view) const; + + OCIO::ConstProcessorRcPtr make_display_processor( + const utility::JsonStore &src_colour_mgmt_metadata, + bool is_thumbnail, + const std::string &view, + const std::string &display, + DynamicCDLMap &dynamic_cdls, + const bool bypass = false) const; + + OCIO::ConstConfigRcPtr make_dynamic_display_processor( + const utility::JsonStore &src_colour_mgmt_metadata, + const OCIO::ConstConfigRcPtr &config, + const OCIO::GroupTransformRcPtr &group, + const std::string &display, + const std::string &view, + DynamicCDLMap &dynamic_cdls) const; + + OCIO::ConstGpuShaderDescRcPtr make_shader( + OCIO::ConstProcessorRcPtr &processor, + const char *function_name, + const char *resource_prefix) const; + + std::string + patch_dynamic_cdls(const std::string &shader_text, DynamicCDLMap &dynamic_cdls) const; + + void setup_textures( + OCIO::ConstGpuShaderDescRcPtr &shader_desc, ColourOperationDataPtr op_data) const; + + // OCIO dynamic properties + void update_dynamic_parameters( + OCIO::ConstGpuShaderDescRcPtr &shader, const float exposure, const float gamma) const; + + void update_ocio_uniforms( + OCIO::ConstGpuShaderDescRcPtr &shader, utility::JsonStore &uniforms) const; + + void update_xstudio_uniforms( + const DynamicCDLMap &dynamic_cdls, utility::JsonStore &uniforms) const; + + std::vector parse_all_colourspaces(OCIO::ConstConfigRcPtr ocio_config) const; + + private: + mutable std::map ocio_config_cache_; + + // Pixel probe + size_t last_pixel_probe_source_hash_; + OCIO::ConstCPUProcessorRcPtr pixel_probe_to_display_proc_; + OCIO::ConstCPUProcessorRcPtr pixel_probe_to_lin_proc_; +}; + +/* Actor wrapper for OCIOEngine, allowing us to execute 'heavy' OCIO based +IO and computation via CAF messaging so that OCIOColourPipeline instances +can offload tasks to a worker pool. */ +class OCIOEngineActor : public caf::event_based_actor { + + public: + OCIOEngineActor(caf::actor_config &cfg); + + caf::behavior make_behavior() override { return behavior_; } + + const char *name() const override { return NAME.c_str(); } + + private: + inline static const std::string NAME = "OCIOEngineActor"; + + caf::behavior behavior_; + + OCIOEngine m_engine_; +}; + +} // namespace xstudio::colour_pipeline::ocio diff --git a/src/plugin/colour_pipeline/ocio/src/ocio_plugin.cpp b/src/plugin/colour_pipeline/ocio/src/ocio_plugin.cpp new file mode 100644 index 000000000..4d960c618 --- /dev/null +++ b/src/plugin/colour_pipeline/ocio/src/ocio_plugin.cpp @@ -0,0 +1,866 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "ocio_plugin.hpp" +#include "ocio_shared_settings.hpp" + +#include "xstudio/utility/string_helpers.hpp" +#include "xstudio/ui/opengl/shader_program_base.hpp" + +using namespace xstudio::colour_pipeline::ocio; +using namespace xstudio; + + +namespace { +const static utility::Uuid PLUGIN_UUID{"b39d1e3d-58f8-475f-82c1-081a048df705"}; +} // anonymous namespace + + +OCIOColourPipeline::OCIOColourPipeline( + caf::actor_config &cfg, const utility::JsonStore &init_settings) + : ColourPipeline(cfg, init_settings) { + + viewport_name_ = init_settings["viewport_name"]; + window_id_ = init_settings.value("window_id", std::string()); + + global_controls_ = system().registry().template get(OCIOGlobalControls::NAME()); + if (!global_controls_) { + global_controls_ = spawn(init_settings); + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + + worker_pool_ = caf::actor_pool::make( + system(), + 4, + [&] { return system().spawn(); }, + caf::actor_pool::round_robin()); + link_to(worker_pool_); +#pragma GCC diagnostic pop + + setup_ui(); + + mail(global_ocio_controls_atom_v, caf::actor_cast(this)).send(global_controls_); +} + +OCIOColourPipeline::~OCIOColourPipeline() { global_controls_ = caf::actor(); } + +caf::message_handler OCIOColourPipeline::message_handler_extensions() { + + return caf::message_handler( + { + + [=](global_ocio_controls_atom atom, + const std::string &oico_config) -> utility::JsonStore { + utility::JsonStore res; + + if (oico_config == current_config_name_) { + res["source_colour_space"] = source_colour_space_->value(); + res["display"] = display_->value(); + res["view"] = view_->value(); + res["channel"] = channel_->value(); + res["exposure"] = exposure_->value(); + res["gamma"] = gamma_->value(); + res["saturation"] = saturation_->value(); + } + + return res; + }, + [=](global_ocio_controls_atom, const utility::JsonStore &settings) { + if (settings.is_null()) + return; + + OCIOGlobalData old_settings = global_settings_; + from_json(settings, global_settings_); + + // if the 'auto adjust source' global setting is ON, the user + // cannot set the source colourspace as this is automatically + // set by logic in this plugin. We disable/enable + // the Source Colourspace menu items thus: + source_colour_space_->set_role_data( + module::Attribute::StringChoicesEnabled, + std::vector( + source_colour_space_->options().size(), + !global_settings_.adjust_source), + false // don't call attribute_changed + ); + + if (global_settings_.colour_bypass != old_settings.colour_bypass) { + update_bypass(global_settings_.colour_bypass); + } + + if (global_settings_.global_view != old_settings.global_view) { + media_source_changed( + current_source_uuid_, current_source_colour_mgmt_metadata_); + } + + if (global_settings_.adjust_source != old_settings.adjust_source) { + media_source_changed( + current_source_uuid_, current_source_colour_mgmt_metadata_); + } + }, + [=](global_ocio_controls_atom atom, + const std::string &attr_title, + const int attr_role, + const utility::JsonStore &attr_value, + const std::string &window_id) { + // quickview windows DON'T sync OCIO settings + if (window_id_.find("xstudio_quickview_window") != std::string::npos || + window_id.find("xstudio_quickview_window") != std::string::npos) + return; + + auto attr = get_attribute(attr_title); + if (attr) { + attr->update_role_data_from_json(attr_role, attr_value, false); + redraw_viewport(); + } + }, + [=](global_ocio_controls_atom atom, + const std::string &ocio_config, + const std::string &attr_title, + const int attr_role, + const utility::JsonStore &attr_value, + const std::string &window_id) { + // quickview windows DON'T sync OCIO settings + if (window_id_.find("xstudio_quickview_window") != std::string::npos || + window_id.find("xstudio_quickview_window") != std::string::npos) + return; + + // snapshot viewport settings don't affect other viewports + if (window_id == "snapshot_viewport") + return; + + if (ocio_config == current_config_name_) { + + // we don't sync OCIO Display if it's coming from a different window + if (attr_title == "Display" && window_id != window_id_) + return; + + auto attr = get_attribute(attr_title); + if (attr) { + attr->update_role_data_from_json(attr_role, attr_value, false); + redraw_viewport(); + } + } + }}) + .or_else(ColourPipeline::message_handler_extensions()); +} + +size_t OCIOColourPipeline::fast_display_transform_hash(const media::AVFrameID &media_ptr) { + + size_t hash = 0; + + if (!global_settings_.colour_bypass) { + hash = m_engine_.compute_hash( + media_ptr.params(), + display_->value() + view_->value() + + (global_settings_.adjust_source ? "auto_adjust" : "")); + } + + return hash; +} + +void OCIOColourPipeline::linearise_op_data( + caf::typed_response_promise &rp, + const media::AVFrameID &media_ptr) { + + rp.delegate( + worker_pool_, + colour_pipe_linearise_data_atom_v, + media_ptr.params(), // media_metadata + global_settings_.colour_bypass, + global_settings_.adjust_source, + view_->value()); +} + +void OCIOColourPipeline::linear_to_display_op_data( + caf::typed_response_promise &rp, + const media::AVFrameID &media_ptr) { + + // TODO: Adjust the view depending on settings and media + // If global_view ON, use the plugin view_ + // If gobal_view OFF, use the media detected view (or user overrided) + + rp.delegate( + worker_pool_, + colour_pipe_display_data_atom_v, + media_ptr.params(), // media_metadata + display_->value(), + view_->value(), + global_settings_.colour_bypass); +} + +utility::JsonStore OCIOColourPipeline::update_shader_uniforms( + const media_reader::ImageBufPtr &image, std::any &user_data) { + + utility::JsonStore uniforms; + if (channel_->value() == "Red") { + uniforms["show_chan"] = 1; + } else if (channel_->value() == "Green") { + uniforms["show_chan"] = 2; + } else if (channel_->value() == "Blue") { + uniforms["show_chan"] = 3; + } else if (channel_->value() == "Alpha") { + uniforms["show_chan"] = 4; + } else if (channel_->value() == "Luminance") { + uniforms["show_chan"] = 5; + } else { + uniforms["show_chan"] = 0; + } + + // TODO: ColSci + // Saturation is not managed by OCIO currently + uniforms["saturation"] = saturation_->value(); + + m_engine_.update_shader_uniforms(user_data, uniforms, exposure_->value(), gamma_->value()); + + return uniforms; +} + +void OCIOColourPipeline::process_thumbnail( + caf::typed_response_promise &rp, + const media::AVFrameID &media_ptr, + const thumbnail::ThumbnailBufferPtr &buf) { + + // TODO: Adjust the view depending on settings and media + // If global_view ON, use the plugin view_ + // If gobal_view OFF, use the media detected view (or user overrided) + + rp.delegate( + worker_pool_, + media_reader::process_thumbnail_atom_v, + media_ptr.params(), // media_metadata + buf, + display_->value(), + view_->value()); +} + +void OCIOColourPipeline::extend_pixel_info( + media_reader::PixelInfo &pixel_info, const media::AVFrameID &frame_id) { + + try { + + m_engine_.extend_pixel_info( + pixel_info, + frame_id, + display_->value(), + view_->value(), + global_settings_.adjust_source, + exposure_->value(), + gamma_->value(), + saturation_->value()); + + } catch (const std::exception &e) { + + spdlog::warn("OCIOColourPipeline: Failed to compute pixel probe: {}", e.what()); + } +} + +void OCIOColourPipeline::register_hotkeys() { + + for (const auto &hotkey_props : ui_text_.channel_hotkeys) { + auto hotkey_id = register_hotkey( + hotkey_props.key, + hotkey_props.modifier, + hotkey_props.name, + hotkey_props.description, + false, + "Viewer"); + + channel_hotkeys_[hotkey_id] = hotkey_props.channel_name; + } + + reset_hotkey_ = register_hotkey( + int('R'), + ui::ControlModifier, + "Reset Colour Viewing Setting", + "Resets viewer exposure and channel mode", + false, + "Viewer"); + + exposure_hotkey_ = register_hotkey( + int('E'), + ui::NoModifier, + "Exposure Scrubbing", + "Hold this key down and click-scrub the mouse pointer left/right in the viewport to " + "adjust viewer exposure", + false, + "Viewer"); + + gamma_hotkey_ = register_hotkey( + int('Y'), + ui::NoModifier, + "Gamma Scrubbing", + "Hold this key down and click-scrub the mouse pointer left/right in the viewport to " + "adjust viewer gamma", + false, + "Viewer"); + + saturation_hotkey_ = register_hotkey( + int('S'), + ui::AltModifier, + "Saturation Scrubbing", + "Hold this key down and click-scrub the mouse pointer left/right in the viewport to " + "adjust viewer saturation", + false, + "Viewer"); +} + +void OCIOColourPipeline::hotkey_pressed( + const utility::Uuid &hotkey_uuid, const std::string &context, const std::string &window) { + + // Ignore hotkey events that have not come from the viewport that this + // OCIOColourPipeline is connected to. + if (context != viewport_name_) + return; + + // If user hits 'R' hotkey and we're already looking at the red channel, + // then we revert back to RGB, same for 'G' and 'B'. + auto p = channel_hotkeys_.find(hotkey_uuid); + if (p != channel_hotkeys_.end()) { + if (channel_->value() == p->second) { + channel_->set_value("RGB"); + } else { + channel_->set_value(p->second); + } + } else if (hotkey_uuid == reset_hotkey_) { + reset(); + } else if (hotkey_uuid == exposure_hotkey_) { + exposure_->set_role_data(module::Attribute::Activated, true); + grab_mouse_focus(); + } else if (hotkey_uuid == gamma_hotkey_) { + gamma_->set_role_data(module::Attribute::Activated, true); + grab_mouse_focus(); + } else if (hotkey_uuid == saturation_hotkey_) { + saturation_->set_role_data(module::Attribute::Activated, true); + grab_mouse_focus(); + } +} + +void OCIOColourPipeline::hotkey_released( + const utility::Uuid &hotkey_uuid, const std::string &context) { + + if (hotkey_uuid == exposure_hotkey_) { + exposure_->set_role_data(module::Attribute::Activated, false); + release_mouse_focus(); + } else if (hotkey_uuid == gamma_hotkey_) { + gamma_->set_role_data(module::Attribute::Activated, false); + release_mouse_focus(); + } else if (hotkey_uuid == saturation_hotkey_) { + saturation_->set_role_data(module::Attribute::Activated, false); + release_mouse_focus(); + } +} + +bool OCIOColourPipeline::pointer_event(const ui::PointerEvent &e) { + + module::FloatAttribute *active_attr = nullptr; + if (exposure_->get_role_data(module::Attribute::Activated)) { + active_attr = exposure_; + } else if (gamma_->get_role_data(module::Attribute::Activated)) { + active_attr = gamma_; + } else if (saturation_->get_role_data(module::Attribute::Activated)) { + active_attr = saturation_; + } + + // Nothing to be done + if (!active_attr) { + return false; + } + + // Implementing exposure scrubbing in viewport + static int x_down; + static float e_down; + static auto dragging = false; + bool used = false; + + if (e.type() == ui::EventType::ButtonDown && e.buttons() == ui::Signature::Left) { + x_down = e.x(); + e_down = active_attr->value(); + dragging = true; + used = true; + } else if (dragging && e.buttons() == ui::Signature::Left) { + const auto sensitivity = + active_attr->get_role_data(module::Attribute::FloatScrubSensitivity); + const auto step = active_attr->get_role_data(module::Attribute::FloatScrubStep); + const auto min = active_attr->get_role_data(module::Attribute::FloatScrubMin); + const auto max = active_attr->get_role_data(module::Attribute::FloatScrubMax); + + auto val = 0.0f; + val = round((e_down + (e.x() - x_down) * sensitivity) / step) * step; + val = std::max(std::min(val, max), min); + active_attr->set_value(val); + used = true; + } else if ( + e.type() == ui::EventType::ButtonRelease && (e.buttons() & ui::Signature::Left)) { + dragging = false; + used = true; + } + + if (e.type() == ui::EventType::DoubleClick) { + static auto last_value = 0.0f; + const auto def = active_attr->get_role_data(module::Attribute::DefaultValue); + + if (active_attr->value() == def) { + active_attr->set_value(last_value); + } else { + last_value = active_attr->value(); + active_attr->set_value(def); + } + used = true; + } + return used; +} + +void OCIOColourPipeline::media_source_changed( + const utility::Uuid &source_uuid, const utility::JsonStore &src_colour_mgmt_metadata) { + + current_source_uuid_ = source_uuid; + current_source_colour_mgmt_metadata_ = src_colour_mgmt_metadata; + + if (global_settings_.colour_bypass) + return; + + if (!source_uuid) + return; + + // Update the UI if the OCIO config changes + // (xStudio support per media OCIO config) + populate_ui(src_colour_mgmt_metadata); + + // Adjust view + if (!global_settings_.global_view) { + const auto preferred_view = + m_engine_.preferred_view(src_colour_mgmt_metadata, global_settings_.preferred_view); + const auto view = src_colour_mgmt_metadata.get_or("override_view", preferred_view); + view_->set_value(view, false); + } + + // Determine the source colourspace. + std::string src_cs = m_engine_.detect_source_colourspace( + src_colour_mgmt_metadata, global_settings_.adjust_source, view_->value()); + + if (!src_cs.empty()) { + // We do not 'notify' this attribute change as it's not the user selecting + // a source colourspace but (possibly) driven by dynamic plugin logic + source_colour_space_->set_value(src_cs, false); + } +} + +void OCIOColourPipeline::attribute_changed( + const utility::Uuid &attribute_uuid, const int role) { + + // These attributes are synchronized for viewports sharing the same + // current OCIO config. + if (attribute_uuid == source_colour_space_->uuid()) { + + // we only get notified here when the USER has changed the source + // colourspace via the OCIO menu options in the UI + update_media_metadata( + current_source_uuid_, + "/colour_pipeline/override_input_cs", + source_colour_space_->value()); + + synchronize_attribute(attribute_uuid, role, true); + + } else if (attribute_uuid == display_->uuid()) { + + if (role == module::Attribute::Value) { + + update_views(display_->value()); + synchronize_attribute(attribute_uuid, role, true); + } + + } else if (attribute_uuid == view_->uuid()) { + + // Adjust the per media view override + if (!global_settings_.global_view) { + update_media_metadata( + current_source_uuid_, "/colour_pipeline/override_view", view_->value()); + } + + synchronize_attribute(attribute_uuid, role, true); + + // Adjust the per media input colour space override + if (global_settings_.adjust_source) { + const std::string src_cs = m_engine_.input_space_for_view( + current_source_colour_mgmt_metadata_, view_->value()); + if (!src_cs.empty()) { + source_colour_space_->set_value(src_cs); + } + } + + // Remaning attributes are synchronized unconditionally + } else { + + synchronize_attribute(attribute_uuid, role, false); + } + + redraw_viewport(); +} + +void OCIOColourPipeline::screen_changed( + const std::string &name, + const std::string &model, + const std::string &manufacturer, + const std::string &serialNumber) { + + auto menu_populated = [](module::StringChoiceAttribute *attr) { + return attr->get_role_data>(module::Attribute::StringChoices) + .size() > 0; + }; + + if (menu_populated(display_) && !monitor_name_.empty()) { + + // we only override the display if the screen info is *changing* ... if + // it's being set for the first time we don't want to auto-set the + // display as it has already been chosen either in populate_ui or + // by the user + + const std::string detected_display = detect_display( + name, model, manufacturer, serialNumber, current_source_colour_mgmt_metadata_); + + display_->set_value(detected_display); + } + + monitor_name_ = name; + monitor_model_ = model; + monitor_manufacturer_ = manufacturer; + monitor_serialNumber_ = serialNumber; +} + +void OCIOColourPipeline::connect_to_viewport( + const std::string &viewport_name, + const std::string &viewport_toolbar_name, + bool connect, + caf::actor viewport) { + + viewport_name_ = viewport_name; + + Module::connect_to_viewport(viewport_name, viewport_toolbar_name, connect, viewport); + + // Here we can add attrs to show up in the viewer context menu (right click) + std::string viewport_context_menu_model_name = viewport_name + "_context_menu"; + + insert_menu_item( + viewport_context_menu_model_name, "Source", "OCIO", 1.0f, source_colour_space_, false); + insert_menu_item( + viewport_context_menu_model_name, "Display", "OCIO", 2.0f, display_, false); + insert_menu_item(viewport_context_menu_model_name, "View", "OCIO", 3.0f, view_, false); + insert_menu_item(viewport_context_menu_model_name, "Channel", "", 21.5f, channel_, false); + insert_menu_item(viewport_context_menu_model_name, "", "", 22.0f, nullptr, true); // divider + + set_submenu_position_in_parent(viewport_context_menu_model_name, "OCIO", 19.0f); + + make_attribute_visible_in_viewport_toolbar(view_); + make_attribute_visible_in_viewport_toolbar(display_); + + // the OCIOGlobalControls actor instance needs to connect to the viewport + // too so it can expose its controls in the viewport toolbar etc. + mail( + colour_pipeline::connect_to_viewport_atom_v, + viewport_name, + viewport_toolbar_name, + connect, + viewport) + .send(global_controls_); +} + +void OCIOColourPipeline::setup_ui() { + + // OCIO Source colour space + + source_colour_space_ = + add_string_choice_attribute(ui_text_.SOURCE_CS, ui_text_.SOURCE_CS_SHORT); + source_colour_space_->set_redraw_viewport_on_change(true); + source_colour_space_->set_role_data(module::Attribute::ToolTip, ui_text_.SOURCE_CS_TOOLTIP); + + // OCIO display selection + + display_ = add_string_choice_attribute(ui_text_.DISPLAY, ui_text_.DISPLAY_SHORT); + display_->set_redraw_viewport_on_change(true); + display_->set_role_data( + module::Attribute::UIDataModels, nlohmann::json{"colour_pipe_attributes"}); + display_->set_role_data(module::Attribute::Enabled, true); + display_->set_role_data(module::Attribute::ToolbarPosition, 10.0f); + display_->set_role_data(module::Attribute::ToolTip, ui_text_.DISPLAY_TOOLTIP); + + // OCIO view selection + + view_ = add_string_choice_attribute(ui_text_.VIEW, ui_text_.VIEW); + view_->set_redraw_viewport_on_change(true); + view_->set_role_data(module::Attribute::Enabled, true); + view_->set_role_data(module::Attribute::ToolbarPosition, 11.0f); + view_->set_role_data(module::Attribute::ToolTip, ui_text_.VIEW_TOOLTIP); + + // Hot channel selection + + channel_ = add_string_choice_attribute( + ui_text_.CHANNEL, + ui_text_.CHANNEL_SHORT, + ui_text_.RGB, + {ui_text_.RGB, + ui_text_.RED, + ui_text_.GREEN, + ui_text_.BLUE, + ui_text_.ALPHA, + ui_text_.LUMINANCE}, + {ui_text_.RGB, ui_text_.R, ui_text_.G, ui_text_.B, ui_text_.A, ui_text_.L}); + channel_->set_redraw_viewport_on_change(true); + channel_->set_role_data(module::Attribute::Enabled, true); + channel_->set_role_data(module::Attribute::ToolbarPosition, 8.0f); + channel_->set_role_data(module::Attribute::ToolTip, ui_text_.CS_MSG_CMS_SELECT_CLR_TIP); + + // Exposure slider + + exposure_ = add_float_attribute( + ui_text_.EXPOSURE, ui_text_.EXPOSURE_SHORT, 0.0f, -10.0f, 10.0f, 0.05f); + exposure_->set_redraw_viewport_on_change(true); + exposure_->set_role_data(module::Attribute::ToolbarPosition, 4.0f); + exposure_->set_role_data(module::Attribute::Activated, false); + exposure_->set_role_data(module::Attribute::DefaultValue, 0.0f); + exposure_->set_role_data(module::Attribute::ToolTip, ui_text_.CS_MSG_CMS_SET_EXP_TIP); + + // Gamma slider + + gamma_ = add_float_attribute(ui_text_.GAMMA, ui_text_.GAMMA_SHORT, 1.0f, 0.0f, 5.0f, 0.05f); + gamma_->set_redraw_viewport_on_change(true); + gamma_->set_role_data(module::Attribute::ToolbarPosition, 4.1f); + gamma_->set_role_data(module::Attribute::Activated, false); + gamma_->set_role_data(module::Attribute::DefaultValue, 1.0f); + gamma_->set_role_data(module::Attribute::ToolTip, ui_text_.CS_MSG_CMS_SET_GAMMA_TIP); + + // Saturation slider + + saturation_ = add_float_attribute( + ui_text_.SATURATION, ui_text_.SATURATION_SHORT, 1.0f, 0.0f, 10.0f, 0.05f); + saturation_->set_redraw_viewport_on_change(true); + saturation_->set_role_data(module::Attribute::ToolbarPosition, 4.2f); + saturation_->set_role_data(module::Attribute::Activated, false); + saturation_->set_role_data(module::Attribute::DefaultValue, 1.0f); + saturation_->set_role_data( + module::Attribute::ToolTip, ui_text_.CS_MSG_CMS_SET_SATURATION_TIP); + + make_attribute_visible_in_viewport_toolbar(exposure_); + make_attribute_visible_in_viewport_toolbar(channel_); + make_attribute_visible_in_viewport_toolbar(gamma_); + make_attribute_visible_in_viewport_toolbar(saturation_); +} + +void OCIOColourPipeline::populate_ui(const utility::JsonStore &src_colour_mgmt_metadata) { + + const auto config_name = m_engine_.get_ocio_config_name(src_colour_mgmt_metadata); + + if (current_config_name_ != config_name) { + + current_config_name_ = config_name; + + // Config has changed, so update views and displays + std::vector all_colourspaces; + std::vector displays; + m_engine_.get_ocio_displays_view_colourspaces( + src_colour_mgmt_metadata, all_colourspaces, displays, display_views_); + + // Try to re-use the previously selected display and view (if any) + // If no longer available, pick sensible defaults + std::string display = display_->value(); + std::string view = view_->value(); + if (std::find(displays.begin(), displays.end(), display) == displays.end()) { + display = detect_display( + monitor_name_, + monitor_model_, + monitor_manufacturer_, + monitor_serialNumber_, + src_colour_mgmt_metadata); + } + if (std::find(display_views_[display].begin(), display_views_[display].end(), view) == + display_views_[display].end()) { + view = m_engine_.preferred_view( + src_colour_mgmt_metadata, + global_settings_.global_view ? "Default" : global_settings_.preferred_view); + } + + source_colour_space_->set_role_data( + module::Attribute::StringChoices, all_colourspaces, false); + + // set the 'enabled' value to false on each of the source colourspace + // options if the global 'Adjust source colourspace mode' is active + source_colour_space_->set_role_data( + module::Attribute::StringChoicesEnabled, + std::vector(all_colourspaces.size(), !global_settings_.adjust_source), + false // don't call attribute_changed + ); + + // we may have used this config before, and stored the display and view settings + // against the config. If so, use those stored settings rather than the + // preferred/defaults found above + try { + + scoped_actor sys{system()}; + + auto stored_config_settings = utility::request_receive( + *sys, + global_controls_, + global_ocio_controls_atom_v, + current_config_name_, + window_id_); + + if (stored_config_settings.contains("Display")) { + display = stored_config_settings["Display"]; + } + if (stored_config_settings.contains("View")) { + view = stored_config_settings["View"]; + } + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + display_->set_role_data(module::Attribute::StringChoices, displays, false); + display_->set_value(display, false); + view_->set_role_data(module::Attribute::StringChoices, display_views_[display], false); + view_->set_value(view, false); + } +} + +void OCIOColourPipeline::update_views(const std::string &new_display) { + + if (!current_source_uuid_) + return; + + const std::vector &new_views = display_views_[new_display]; + view_->set_role_data(module::Attribute::StringChoices, new_views, false); + + // Reset the view if no longer available under the new display + if (std::find(new_views.begin(), new_views.end(), view_->value()) == new_views.end()) { + view_->set_value(m_engine_.preferred_view( + current_source_colour_mgmt_metadata_, + global_settings_.global_view ? "Default" : global_settings_.preferred_view)); + } +} + +void OCIOColourPipeline::update_bypass(bool bypass) { + + // Just disable these settings when colour management is off. + view_->set_role_data(module::Attribute::Enabled, !bypass, false); + display_->set_role_data(module::Attribute::Enabled, !bypass, false); +} + +void OCIOColourPipeline::update_media_metadata( + const utility::Uuid &media_uuid, const std::string &key, const std::string &val) { + + if (!media_uuid) + return; + + try { + scoped_actor sys{system()}; + + // first, get to the session + auto session = utility::request_receive( + *sys, + system().registry().template get(global_registry), + session::session_atom_v); + + // get the session to search its playlists for the media source + auto media_source_actor = utility::request_receive( + *sys, session, media::get_media_source_atom_v, media_uuid); + + if (media_source_actor) { + auto colour_data = utility::request_receive( + *sys, + media_source_actor, + json_store::set_json_atom_v, + utility::JsonStore(val), + key); + } + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + +void OCIOColourPipeline::synchronize_attribute(const utility::Uuid &uuid, int role, bool ocio) { + + // Don't bother synchronizing anything beyond values + if (role != (int)module::Attribute::Value) + return; + + const auto attr = get_attribute(uuid); + if (!attr) + return; + + const auto title = attr->get_role_data((int)module::Attribute::Title); + const auto value = utility::JsonStore(attr->role_data_as_json(role)); + + if (ocio) { + mail(global_ocio_controls_atom_v, current_config_name_, title, role, value, window_id_) + .send(global_controls_); + } else { + mail(global_ocio_controls_atom_v, title, role, value, window_id_) + .send(global_controls_); + } +} + +void OCIOColourPipeline::reset() { + + channel_->set_value("RGB"); + exposure_->set_value(exposure_->get_role_data(module::Attribute::DefaultValue)); + gamma_->set_value(gamma_->get_role_data(module::Attribute::DefaultValue)); + saturation_->set_value(saturation_->get_role_data(module::Attribute::DefaultValue)); +} + +std::string OCIOColourPipeline::detect_display( + const std::string &name, + const std::string &model, + const std::string &manufacturer, + const std::string &serialNumber, + const utility::JsonStore &meta) { + + std::string detected_display = m_engine_.default_display(meta); + + if (meta.get_or("viewing_rules", false)) { + + try { + + auto hook = system().registry().template get(media_hook_registry); + + if (hook) { + caf::scoped_actor sys(system()); + const std::string display = utility::request_receive( + *sys, + hook, + media_hook::detect_display_atom_v, + name, + model, + manufacturer, + serialNumber, + meta); + if (!display.empty()) { + detected_display = display; + } + } + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + } + + return detected_display; +} + + +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>( + {std::make_shared>( + PLUGIN_UUID, + "OCIOColourPipeline", + plugin_manager::PluginFlags::PF_COLOUR_MANAGEMENT, + false, + "xStudio", + "OCIO (v2) Colour Pipeline", + semver::version("1.0.0"))})); +} +} \ No newline at end of file diff --git a/src/plugin/colour_pipeline/ocio/src/ocio_plugin.hpp b/src/plugin/colour_pipeline/ocio/src/ocio_plugin.hpp new file mode 100644 index 000000000..56b873bd0 --- /dev/null +++ b/src/plugin/colour_pipeline/ocio/src/ocio_plugin.hpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include //NOLINT + +#include "xstudio/colour_pipeline/colour_pipeline_actor.hpp" +#include "xstudio/plugin_manager/plugin_manager.hpp" +#include "xstudio/utility/logging.hpp" +#include "xstudio/global_store/global_store.hpp" +#include "xstudio/thumbnail/thumbnail.hpp" +#include "ui_text.hpp" +#include "ocio_shared_settings.hpp" +#include "ocio_engine.hpp" + +namespace OCIO = OCIO_NAMESPACE; + +namespace xstudio::colour_pipeline::ocio { + +/* OCIOColourPipeline provides colour management for medias. + + It is instanciated per xStudio viewport and maintains it's own list of + settings (source space, display, view, gamma, exposure, saturation, etc). + Settings that should be shared accross multiple viewports are sync'ed + using a global OCIOGlobalControls actor. The latter also provides + application wide settings related to colour management. + + Long running tasks are delegated to a worker pool of OCIOEngineActor. + This includes linearization and display shaders generation, thumbnail + colour rendering. +*/ + +class OCIOColourPipeline : public ColourPipeline { + + public: + explicit OCIOColourPipeline( + caf::actor_config &cfg, const utility::JsonStore &init_settings = utility::JsonStore()); + + virtual ~OCIOColourPipeline() override; + + size_t fast_display_transform_hash(const media::AVFrameID &media_ptr) override; + + /* Create the ColourOperationDataPtr containing the necessary LUT and + shader data for linearising the source colourspace RGB data from the + given media source on the screen */ + void linearise_op_data( + caf::typed_response_promise &rp, + const media::AVFrameID &media_ptr) override; + + /* Create the ColourOperationDataPtr containing the necessary LUT and + shader data for transforming linear colour values into display space */ + void linear_to_display_op_data( + caf::typed_response_promise &rp, + const media::AVFrameID &media_ptr) override; + + // Update colour pipeline shader dynamic parameters. + utility::JsonStore update_shader_uniforms( + const media_reader::ImageBufPtr &image, std::any &user_data) override; + + void process_thumbnail( + caf::typed_response_promise &rp, + const media::AVFrameID &media_ptr, + const thumbnail::ThumbnailBufferPtr &buf) override; + + // GUI handling + + void register_hotkeys() override; + + void hotkey_pressed( + const utility::Uuid &hotkey_uuid, + const std::string &context, + const std::string &window) override; + + void hotkey_released(const utility::Uuid &hotkey_uuid, const std::string &context) override; + + bool pointer_event(const ui::PointerEvent &e) override; + + void media_source_changed( + const utility::Uuid &source_uuid, + const utility::JsonStore &src_colour_mgmt_metadata) override; + + void attribute_changed(const utility::Uuid &attribute_uuid, const int /*role*/) override; + + void screen_changed( + const std::string &name, + const std::string &model, + const std::string &manufacturer, + const std::string &serialNumber) override; + + void connect_to_viewport( + const std::string &viewport_name, + const std::string &viewport_toolbar_name, + bool connect, + caf::actor viewport) override; + + void extend_pixel_info( + media_reader::PixelInfo &pixel_info, const media::AVFrameID &frame_id) override; + + caf::message_handler message_handler_extensions() override; + + private: + void setup_ui(); + + void populate_ui(const utility::JsonStore &src_colour_mgmt_metadata); + + void update_views(const std::string &new_display); + + void update_bypass(bool bypass); + + void reset() override; + + void update_media_metadata( + const utility::Uuid &media_uuid, const std::string &key, const std::string &val); + + void synchronize_attribute(const utility::Uuid &uuid, int role, bool ocio); + + std::string detect_display( + const std::string &name, + const std::string &model, + const std::string &manufacturer, + const std::string &serialNumber, + const utility::JsonStore &meta); + + private: + caf::actor global_controls_; + caf::actor worker_pool_; + + // GUI handling + UiText ui_text_; + + std::map channel_hotkeys_; + utility::Uuid exposure_hotkey_; + utility::Uuid gamma_hotkey_; + utility::Uuid saturation_hotkey_; + utility::Uuid reset_hotkey_; + + // Viewport colour settings + module::StringChoiceAttribute *source_colour_space_; + module::StringChoiceAttribute *display_; + module::StringChoiceAttribute *view_; + module::StringChoiceAttribute *channel_; + module::FloatAttribute *exposure_; + module::FloatAttribute *gamma_; + module::FloatAttribute *saturation_; + + // Global colour settings + OCIOGlobalData global_settings_; + + // Holds info about the currently on screen media + utility::Uuid current_source_uuid_; + std::string current_config_name_; + std::string last_update_hash_; + utility::JsonStore current_source_colour_mgmt_metadata_; + + // Holds data on display screen option + std::string monitor_name_; + std::string monitor_model_; + std::string monitor_manufacturer_; + std::string monitor_serialNumber_; + std::string viewport_name_; + + // The ID of the window that this instance of the colour pipeline is used + // to display images into + std::string window_id_; + + std::map> display_views_; + + // Processing + OCIOEngine m_engine_; +}; + +} // namespace xstudio::colour_pipeline::ocio diff --git a/src/plugin/colour_pipeline/ocio/src/ocio_shared_settings.cpp b/src/plugin/colour_pipeline/ocio/src/ocio_shared_settings.cpp new file mode 100644 index 000000000..3bfcc7083 --- /dev/null +++ b/src/plugin/colour_pipeline/ocio/src/ocio_shared_settings.cpp @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "ocio_shared_settings.hpp" + +#include "xstudio/utility/string_helpers.hpp" +#include "xstudio/utility/helpers.hpp" + +using namespace xstudio::colour_pipeline; +using namespace xstudio; + + +void xstudio::colour_pipeline::from_json(const nlohmann::json &j, OCIOGlobalData &d) { + + j.at("colour_bypass").get_to(d.colour_bypass); + j.at("global_view").get_to(d.global_view); + j.at("adjust_source").get_to(d.adjust_source); + j.at("preferred_view").get_to(d.preferred_view); +} + +void xstudio::colour_pipeline::to_json(nlohmann::json &j, const OCIOGlobalData &d) { + + j["colour_bypass"] = d.colour_bypass; + j["global_view"] = d.global_view; + j["adjust_source"] = d.adjust_source; + j["preferred_view"] = d.preferred_view; +} + +OCIOGlobalControls::OCIOGlobalControls( + caf::actor_config &cfg, const utility::JsonStore &init_settings) + : StandardPlugin(cfg, "OCIOGlobalControls", init_settings) { + + system().registry().put(OCIOGlobalControls::NAME(), this); + + // make sure we get cleaned up when the global actor exits + link_to(system().registry().template get(global_registry)); + + // Colour bypass + + colour_bypass_ = add_boolean_attribute(ui_text_.CMS_OFF, ui_text_.CMS_OFF_SHORT, false); + colour_bypass_->set_redraw_viewport_on_change(true); + colour_bypass_->set_role_data( + module::Attribute::UIDataModels, nlohmann::json{"colour_pipe_attributes"}); + // colour_bypass_->set_role_data(module::Attribute::Enabled, false); + colour_bypass_->set_role_data(module::Attribute::ToolTip, ui_text_.CS_BYPASS_TOOLTIP); + // 'colour bypass' is a global setting. + colour_bypass_->set_role_data( + module::Attribute::UuidRole, utility::Uuid("222902ee-167b-4c74-91aa-04eb74fd4357")); + + // View mode + + global_view_ = add_boolean_attribute(ui_text_.VIEW_MODE, ui_text_.GLOBAL_VIEW_SHORT, true); + global_view_->set_redraw_viewport_on_change(true); + global_view_->set_role_data( + module::Attribute::UIDataModels, nlohmann::json{"colour_pipe_attributes"}); + global_view_->set_role_data(module::Attribute::ToolTip, ui_text_.GLOBAL_VIEW_TOOLTIP); + global_view_->set_preference_path("/plugin/colour_pipeline/global_view"); + + // Preferred view + + preferred_view_ = add_string_choice_attribute( + ui_text_.PREF_VIEW, ui_text_.PREF_VIEW, ui_text_.DEFAULT_VIEW); + preferred_view_->set_redraw_viewport_on_change(true); + preferred_view_->set_role_data(module::Attribute::Enabled, !global_view_->value()); + preferred_view_->set_role_data(module::Attribute::ToolbarPosition, 11.0f); + preferred_view_->set_role_data(module::Attribute::ToolTip, ui_text_.PREF_VIEW_TOOLTIP); + preferred_view_->set_role_data( + module::Attribute::StringChoices, ui_text_.PREF_VIEW_OPTIONS, false); + preferred_view_->set_preference_path("/plugin/colour_pipeline/ocio/preferred_view"); + + // Source mode + + adjust_source_ = + add_boolean_attribute(ui_text_.SOURCE_CS_MODE, ui_text_.SOURCE_CS_MODE_SHORT, true); + adjust_source_->set_redraw_viewport_on_change(true); + adjust_source_->set_role_data( + module::Attribute::UIDataModels, nlohmann::json{"colour_pipe_attributes"}); + // adjust_source_->set_role_data(module::Attribute::Enabled, false); + adjust_source_->set_role_data(module::Attribute::ToolTip, ui_text_.SOURCE_CS_MODE_TOOLTIP); + adjust_source_->set_preference_path("/plugin/colour_pipeline/ocio/user_source_mode"); + + // this attr is used to store the display/view that the user has selected + // so the settings can be restored between xstudio instances + user_view_display_settings_attr_ = add_attribute("User OCIO Settings"); + user_view_display_settings_attr_->set_preference_path( + "/plugin/colour_pipeline/ocio/user_ocio_settings"); + + // we need to call this base class method before calling insert_menu_item + make_behavior(); + + insert_menu_item("main menu bar", ui_text_.CMS_OFF, "Colour", 1.0f, colour_bypass_, false); + insert_menu_item("main menu bar", ui_text_.VIEW_MODE, "Colour", 2.0f, global_view_, false); + insert_menu_item( + "main menu bar", ui_text_.PREF_VIEW, "Colour", 3.0f, preferred_view_, false); + insert_menu_item( + "main menu bar", ui_text_.SOURCE_CS_MODE, "Colour", 4.0f, adjust_source_, false); + + // make sure the colour menu appears in the right place in the main menu bar. + set_submenu_position_in_parent("main menu bar", "Colour", 30.0f); + + ui_initialized_ = true; + + // the user_view_display_settings_ attr should be updated from the preferences + // store at this point + user_view_display_settings_ = + user_view_display_settings_attr_->role_data_as_json(module::Attribute::Value); + + synchronize_attributes(); + connect_to_ui(); +} + +void OCIOGlobalControls::on_exit() { + system().registry().erase(OCIOGlobalControls::NAME()); + watchers_.clear(); +} + +caf::message_handler OCIOGlobalControls::message_handler_extensions() { + + return caf::message_handler( + {[=](connect_to_viewport_atom, + const std::string &viewport_name, + const std::string &viewport_toolbar_name, + bool connect, + caf::actor vp) { + connect_to_viewport(viewport_name, viewport_toolbar_name, connect, vp); + }, + // TODO: Most likely need more fine grained control over what's linked + // or not between the different viewports, maybe a way to allow user + // to override the default linking behaviour per attribute and set which + // viewport is the reference. This handler would then accept optionaly + // the list of attributes to synchronize. + [=](global_ocio_controls_atom, caf::actor watcher) { + monitor(watcher, [this, addr = watcher.address()](const error &) { + auto p = watchers_.begin(); + while (p != watchers_.end()) { + if (addr == *p) + p = watchers_.erase(p); + else + p++; + } + }); + + watchers_.push_back(watcher); + mail(global_ocio_controls_atom_v, settings_json()).send(watcher); + }, + // Allow new viewport to query the last Display/View settings for + // a given config + [=](global_ocio_controls_atom atom, + const std::string &ocio_config, + const std::string &window_id) -> utility::JsonStore { + utility::JsonStore res; + + if (user_view_display_settings_.contains(ocio_config)) { + + const auto &ds = user_view_display_settings_[ocio_config]; + + // N.B. View is the same for all viewers (for a given ocio_config) + // but Display is independent per window_id + if (ds.contains("View")) { + res["View"] = ds["View"]; + } + if (ds.contains(window_id) && ds[window_id].contains("Display")) { + res["Display"] = ds[window_id]["Display"]; + } else if ( + window_id.find("xstudio_quickview_window") != std::string::npos) { + + // special case - a quickview window wants to set its + // display but no quickview has been set-up before for + // the current ocio_config so we fallback to the main window display + if (ds.contains("xstudio_main_window") && + ds["xstudio_main_window"].contains("Display")) { + res["Display"] = ds["xstudio_main_window"]["Display"]; + } + } + return res; + } + return utility::JsonStore(); + }, + [=](global_ocio_controls_atom atom, + const std::string &attr_title, + const int attr_role, + const utility::JsonStore &attr_value, + const std::string &window_id) { + for (auto &watcher : watchers_) { + if (watcher != current_sender()) { + mail(atom, attr_title, attr_role, attr_value, window_id) + .send(watcher); + } + } + }, + [=](global_ocio_controls_atom atom, + const std::string &ocio_config, + const std::string &attr_title, + const int attr_role, + const utility::JsonStore &attr_value, + const std::string &window_id) { + // we need to store Display and View (per OCIO config) between + // xstudio sessions. This attribute has a preference path so + // it's value is written (and retrieved from) user prefs. + // Note Display is independent for xstudio window ids, but + // View is the same across all windows + if (attr_title == "Display") { + user_view_display_settings_[ocio_config][window_id][attr_title] = + attr_value; + user_view_display_settings_attr_->set_role_data( + module::Attribute::Value, user_view_display_settings_, false); + } else if (attr_title == "View") { + user_view_display_settings_[ocio_config][attr_title] = attr_value; + user_view_display_settings_attr_->set_role_data( + module::Attribute::Value, user_view_display_settings_, false); + } + + for (auto &watcher : watchers_) { + if (watcher != current_sender()) { + mail( + atom, ocio_config, attr_title, attr_role, attr_value, window_id) + .send(watcher); + } + } + }}) + .or_else(StandardPlugin::message_handler_extensions()); +} + +void OCIOGlobalControls::connect_to_viewport( + const std::string &viewport_name, + const std::string &viewport_toolbar_name, + bool connect, + caf::actor viewport) { + + Module::connect_to_viewport(viewport_name, viewport_toolbar_name, connect, viewport); + std::string viewport_context_menu_model_name = viewport_name + "_context_menu"; +} + +void OCIOGlobalControls::attribute_changed( + const utility::Uuid &attribute_uuid, const int role) { + + if (!ui_initialized_) + return; + + if (attribute_uuid == global_view_->uuid()) { + preferred_view_->set_role_data(module::Attribute::Enabled, !global_view_->value()); + } + + synchronize_attributes(); +} + +utility::JsonStore OCIOGlobalControls::settings_json() { + + utility::JsonStore j; + j["colour_bypass"] = colour_bypass_->value(); + j["global_view"] = global_view_->value(); + j["adjust_source"] = adjust_source_->value(); + j["preferred_view"] = preferred_view_->value(); + return j; +} + +void OCIOGlobalControls::synchronize_attributes() { + + for (auto &watcher : watchers_) { + mail(global_ocio_controls_atom_v, settings_json()).send(watcher); + } +} \ No newline at end of file diff --git a/src/plugin/colour_pipeline/ocio/src/ocio_shared_settings.hpp b/src/plugin/colour_pipeline/ocio/src/ocio_shared_settings.hpp new file mode 100644 index 000000000..cdfc958e4 --- /dev/null +++ b/src/plugin/colour_pipeline/ocio/src/ocio_shared_settings.hpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/plugin_manager/plugin_base.hpp" +#include "ui_text.hpp" + +namespace xstudio { +namespace colour_pipeline { + + /* OCIOGlobalData is used for communications to OCIOPlugin(s). */ + struct OCIOGlobalData { + bool colour_bypass{false}; + bool global_view{true}; + bool adjust_source{true}; + std::string preferred_view; + }; + + void from_json(const nlohmann::json &j, OCIOGlobalData &d); + void to_json(nlohmann::json &j, const OCIOGlobalData &d); + + /* OCIOGlobalControls provides application wide colour management settings + and facilitate cross viewport settings synchronization. + + It is instanciated once for the whole application. + */ + class OCIOGlobalControls : public plugin::StandardPlugin { + + public: + explicit OCIOGlobalControls( + caf::actor_config &cfg, const utility::JsonStore &init_settings); + + void attribute_changed(const utility::Uuid &attribute_uuid, const int role) override; + + caf::message_handler message_handler_extensions() override; + + void on_exit() override; + + void connect_to_viewport( + const std::string &viewport_name, + const std::string &viewport_toolbar_name, + bool connect, + caf::actor viewport) override; + + inline static std::string NAME() { return "OCIO_GLOBAL_CONTROLS"; } + + private: + utility::JsonStore settings_json(); + + void synchronize_attributes(); + + private: + UiText ui_text_; + bool ui_initialized_{false}; + + // General settings + module::BooleanAttribute *colour_bypass_; + module::BooleanAttribute *global_view_; + module::BooleanAttribute *adjust_source_; + module::StringChoiceAttribute *preferred_view_; + module::Attribute *user_view_display_settings_attr_; + + utility::JsonStore user_view_display_settings_; + + std::vector watchers_; + }; + +} // namespace colour_pipeline +} // namespace xstudio diff --git a/src/plugin/colour_pipeline/ocio/src/ocio_ui.cpp b/src/plugin/colour_pipeline/ocio/src/ocio_ui.cpp deleted file mode 100644 index 220c7107e..000000000 --- a/src/plugin/colour_pipeline/ocio/src/ocio_ui.cpp +++ /dev/null @@ -1,678 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "ocio.hpp" - -using namespace xstudio::colour_pipeline; -using namespace xstudio; - - -void OCIOColourPipeline::media_source_changed( - const utility::Uuid &source_uuid, const utility::JsonStore &colour_params) { - // If the OCIO config for the new media is new, we need to update the UI - const MediaParams old_media_param = get_media_params(current_source_uuid_); - const MediaParams new_media_param = get_media_params(source_uuid, colour_params); - - const bool need_ui_update = - not current_source_uuid_ or - current_source_media_params_.ocio_config_name != new_media_param.ocio_config_name; - - if (need_ui_update) { - populate_ui(new_media_param); - } - - current_source_uuid_ = source_uuid; - current_config_name_ = new_media_param.ocio_config_name; - current_source_media_params_ = new_media_param; - - // Extract the input colorspace as detected by the plugin and update the UI - std::string detected_cs; - - OCIO::TransformRcPtr transform = source_transform(new_media_param); - if (transform->getTransformType() == OCIO::TRANSFORM_TYPE_COLORSPACE) { - OCIO::ColorSpaceTransformRcPtr csc = - std::static_pointer_cast(transform); - detected_cs = csc->getSrc(); - } else if (transform->getTransformType() == OCIO::TRANSFORM_TYPE_DISPLAY_VIEW) { - // TODO: ColSci - // Note that in case the view uses looks, the colour space returned here - // is not representing the input processing accurately and will be out - // of sync with what the input transform actually is (inverse display view). - OCIO::DisplayViewTransformRcPtr disp = - std::static_pointer_cast(transform); - const std::string view_cs = new_media_param.ocio_config->getDisplayViewColorSpaceName( - disp->getDisplay(), disp->getView()); - detected_cs = view_cs; - } else { - spdlog::warn( - "OCIOColourPipeline: Internal error trying to extract source colour space."); - } - - // Do not notify to avoid this being interpreted as a manual user selection. - if (not detected_cs.empty()) { - source_colour_space_->set_value( - new_media_param.ocio_config->getCanonicalName(detected_cs.c_str()), false); - } - - // Update the per media assigned view - if (!global_view_->value()) { - // When the main viewport gets the event and change the view here, - // it will be propagated to the popout viewer because the view_ - // attribute is linked accross viewports. If the popout viewport - // hasn't got the source change event, or didn't process it yet, - // it might receive the view_ attribute_changed event and go on - // to update the per media parameters with the new view for the - // wrong media. This then cause a mix up of view assigned to - // the incorrect media. - // Hence we make sure to not notify the change here. - view_->set_value(new_media_param.output_view, false); - } - - // Update the assigned source colour space depending on the current view - if (adjust_source_->value()) { - update_cs_from_view(new_media_param, view_->value()); - } -} - -void OCIOColourPipeline::attribute_changed( - const utility::Uuid &attribute_uuid, const int role) { - - MediaParams media_param = get_media_params(current_source_uuid_); - - if (attribute_uuid == display_->uuid()) { - update_views(media_param.ocio_config); - } else if (attribute_uuid == view_->uuid() && !view_->value().empty()) { - if (!global_view_->value()) { - media_param.output_view = view_->value(); - set_media_params(media_param); - } - if (adjust_source_->value()) { - update_cs_from_view(media_param, view_->value()); - } - } else if (attribute_uuid == source_colour_space_->uuid()) { - media_param.user_input_cs = source_colour_space_->value(); - set_media_params(media_param); - } else if (attribute_uuid == colour_bypass_->uuid()) { - update_bypass(display_, colour_bypass_->value()); - } else if ( - exposure_ && attribute_uuid == exposure_->uuid() || - gamma_ && attribute_uuid == gamma_->uuid() || - saturation_ && attribute_uuid == saturation_->uuid()) { - redraw_viewport(); - } else if (attribute_uuid == preferred_view_->uuid()) { - bool enable_global = preferred_view_->value() != ui_text_.AUTOMATIC_VIEW; - global_view_->set_value(enable_global, false); - } else if (attribute_uuid == enable_gamma_->uuid()) { - - make_attribute_visible_in_viewport_toolbar(gamma_, enable_gamma_->value()); - - } else if (attribute_uuid == enable_saturation_->uuid()) { - - make_attribute_visible_in_viewport_toolbar(saturation_, enable_saturation_->value()); - } -} - -void OCIOColourPipeline::hotkey_pressed( - const utility::Uuid &hotkey_uuid, const std::string &context) { - - // If user hits 'R' hotkey and we're already looking at the red channel, - // then we revert back to RGB, same for 'G' and 'B'. - auto p = channel_hotkeys_.find(hotkey_uuid); - if (p != channel_hotkeys_.end()) { - if (channel_->value() == p->second) { - channel_->set_value("RGB"); - } else { - channel_->set_value(p->second); - } - } else if (hotkey_uuid == reset_hotkey_) { - channel_->set_value("RGB"); - exposure_->set_value(exposure_->get_role_data(module::Attribute::DefaultValue)); - gamma_->set_value(gamma_->get_role_data(module::Attribute::DefaultValue)); - saturation_->set_value( - saturation_->get_role_data(module::Attribute::DefaultValue)); - } else if (hotkey_uuid == exposure_hotkey_) { - exposure_->set_role_data(module::Attribute::Activated, true); - grab_mouse_focus(); - } else if (hotkey_uuid == gamma_hotkey_) { - gamma_->set_role_data(module::Attribute::Activated, true); - grab_mouse_focus(); - } else if (hotkey_uuid == saturation_hotkey_) { - saturation_->set_role_data(module::Attribute::Activated, true); - grab_mouse_focus(); - } -} - -void OCIOColourPipeline::hotkey_released( - const utility::Uuid &hotkey_uuid, const std::string & /*context*/) { - if (hotkey_uuid == exposure_hotkey_) { - exposure_->set_role_data(module::Attribute::Activated, false); - release_mouse_focus(); - } else if (hotkey_uuid == gamma_hotkey_) { - gamma_->set_role_data(module::Attribute::Activated, false); - release_mouse_focus(); - } else if (hotkey_uuid == saturation_hotkey_) { - saturation_->set_role_data(module::Attribute::Activated, false); - release_mouse_focus(); - } -} - -bool OCIOColourPipeline::pointer_event(const ui::PointerEvent &e) { - - module::FloatAttribute *active_attr = nullptr; - if (exposure_->get_role_data(module::Attribute::Activated)) { - active_attr = exposure_; - } else if (gamma_->get_role_data(module::Attribute::Activated)) { - active_attr = gamma_; - } else if (saturation_->get_role_data(module::Attribute::Activated)) { - active_attr = saturation_; - } - - // Nothing to be done - if (!active_attr) { - return false; - } - - // Implementing exposure scrubbing in viewport - static int x_down; - static float e_down; - static auto dragging = false; - bool used = false; - - if (e.type() == ui::Signature::EventType::ButtonDown && - e.buttons() == ui::Signature::Left) { - x_down = e.x(); - e_down = active_attr->value(); - dragging = true; - used = true; - } else if (dragging && e.buttons() == ui::Signature::Left) { - const auto sensitivity = - active_attr->get_role_data(module::Attribute::FloatScrubSensitivity); - const auto step = active_attr->get_role_data(module::Attribute::FloatScrubStep); - const auto min = active_attr->get_role_data(module::Attribute::FloatScrubMin); - const auto max = active_attr->get_role_data(module::Attribute::FloatScrubMax); - - auto val = 0.0f; - val = round((e_down + (e.x() - x_down) * sensitivity) / step) * step; - val = std::max(std::min(val, max), min); - active_attr->set_value(val); - used = true; - } else if ( - e.type() == ui::Signature::EventType::ButtonRelease && - (e.buttons() & ui::Signature::Left)) { - dragging = false; - used = true; - } - - if (e.type() == ui::Signature::EventType::DoubleClick) { - static auto last_value = 0.0f; - const auto def = active_attr->get_role_data(module::Attribute::DefaultValue); - - if (active_attr->value() == def) { - active_attr->set_value(last_value); - } else { - last_value = active_attr->value(); - active_attr->set_value(def); - } - used = true; - } - - return used; -} - -void OCIOColourPipeline::screen_changed( - const std::string &name, - const std::string &model, - const std::string &manufacturer, - const std::string &serialNumber) { - - const MediaParams media_param = get_media_params(current_source_uuid_); - const std::string monitor_name = manufacturer + " " + model; - const std::string display = default_display(media_param, monitor_name); - - auto menu_populated = [](module::StringChoiceAttribute *attr) { - return attr->get_role_data>(module::Attribute::StringChoices) - .size() > 0; - }; - - if (menu_populated(display_)) { - display_->set_value(display); - } - monitor_name_ = monitor_name; -} - -void OCIOColourPipeline::connect_to_viewport( - const std::string &viewport_name, const std::string &viewport_toolbar_name, bool connect) { - - Module::connect_to_viewport(viewport_name, viewport_toolbar_name, connect); - - if (viewport_name == "viewport0") { - // this is the OCIO actor for the main viewport... we register ourselves - // so other OCIO actors can talk to us - system().registry().put("MAIN_VIEWPORT_OCIO_INSTANCE", this); - } - - add_multichoice_attr_to_menu( - display_, viewport_name + "_context_menu_section2", "OCIO Display"); - - add_multichoice_attr_to_menu(view_, viewport_name + "_context_menu_section2", "OCIO View"); - - add_multichoice_attr_to_menu(channel_, viewport_name + "_context_menu_section1", "Channel"); - - if (viewport_name == "viewport0") { - - add_multichoice_attr_to_menu(view_, "Colour", "OCIO View"); - - add_multichoice_attr_to_menu(display_, "Colour", "OCIO Display"); - - add_multichoice_attr_to_menu(channel_, "Colour", "Channel"); - - add_boolean_attr_to_menu(colour_bypass_, "Colour"); - - add_boolean_attr_to_menu(global_view_, "Colour"); - - add_boolean_attr_to_menu(adjust_source_, "Colour"); - - add_multichoice_attr_to_menu(source_colour_space_, "Colour", "Source Colour Space"); - - add_multichoice_attr_to_menu(preferred_view_, "Colour", "OCIO Preferred View"); - - add_boolean_attr_to_menu(enable_saturation_, "panels_menu|Toolbar"); - - add_boolean_attr_to_menu(enable_gamma_, "panels_menu|Toolbar"); - - } else if (viewport_name == "viewport1") { - - add_multichoice_attr_to_menu(display_, "Colour", "OCIO Pop-Out Viewer Display"); - } -} - -void OCIOColourPipeline::setup_ui() { - // OCIO Source colour space - - source_colour_space_ = - add_string_choice_attribute(ui_text_.SOURCE_CS, ui_text_.SOURCE_CS_SHORT); - source_colour_space_->set_redraw_viewport_on_change(true); - source_colour_space_->set_role_data( - module::Attribute::Groups, nlohmann::json{"colour_pipe_attributes"}); - source_colour_space_->set_role_data(module::Attribute::Enabled, false); - source_colour_space_->set_role_data(module::Attribute::ToolbarPosition, 12.0f); - source_colour_space_->set_role_data(module::Attribute::ToolTip, ui_text_.SOURCE_CS_TOOLTIP); - - // OCIO display selection (main viewer) - - display_ = add_string_choice_attribute(ui_text_.DISPLAY, ui_text_.DISPLAY_SHORT); - display_->set_redraw_viewport_on_change(true); - display_->set_role_data( - module::Attribute::Groups, nlohmann::json{"colour_pipe_attributes"}); - display_->set_role_data(module::Attribute::Enabled, true); - display_->set_role_data(module::Attribute::ToolbarPosition, 10.0f); - display_->set_role_data(module::Attribute::ToolTip, ui_text_.DISPLAY_TOOLTIP); - - // OCIO view selection - - view_ = add_string_choice_attribute(ui_text_.VIEW, ui_text_.VIEW); - view_->set_redraw_viewport_on_change(true); - view_->set_role_data(module::Attribute::Enabled, true); - view_->set_role_data(module::Attribute::ToolbarPosition, 11.0f); - view_->set_role_data(module::Attribute::ToolTip, ui_text_.VIEW_TOOLTIP); - - // Preferred view - - preferred_view_ = add_string_choice_attribute( - ui_text_.PREF_VIEW, ui_text_.PREF_VIEW, ui_text_.DEFAULT_VIEW); - preferred_view_->set_redraw_viewport_on_change(true); - preferred_view_->set_role_data( - module::Attribute::UuidRole, "06f57aaa-0be8-47ab-ac65-fb742bda410b"); - preferred_view_->set_role_data(module::Attribute::Enabled, true); - preferred_view_->set_role_data(module::Attribute::ToolbarPosition, 11.0f); - preferred_view_->set_role_data(module::Attribute::ToolTip, ui_text_.PREF_VIEW_TOOLTIP); - preferred_view_->set_role_data( - module::Attribute::StringChoices, ui_text_.PREF_VIEW_OPTIONS, false); - preferred_view_->set_preference_path("/plugin/colour_pipeline/ocio/user_preferred_view"); - - // Hot channel selection - - channel_ = add_string_choice_attribute( - ui_text_.CHANNEL, - ui_text_.CHANNEL_SHORT, - ui_text_.RGB, - {ui_text_.RGB, - ui_text_.RED, - ui_text_.GREEN, - ui_text_.BLUE, - ui_text_.ALPHA, - ui_text_.LUMINANCE}, - {ui_text_.RGB, ui_text_.R, ui_text_.G, ui_text_.B, ui_text_.A, ui_text_.L}); - channel_->set_redraw_viewport_on_change(true); - channel_->set_role_data(module::Attribute::Enabled, true); - channel_->set_role_data(module::Attribute::ToolbarPosition, 8.0f); - channel_->set_role_data(module::Attribute::ToolTip, ui_text_.CS_MSG_CMS_SELECT_CLR_TIP); - - // Exposure slider - - exposure_ = add_float_attribute( - ui_text_.EXPOSURE, ui_text_.EXPOSURE_SHORT, 0.0f, -10.0f, 10.0f, 0.05f); - exposure_->set_redraw_viewport_on_change(true); - exposure_->set_role_data(module::Attribute::ToolbarPosition, 4.0f); - exposure_->set_role_data(module::Attribute::Activated, false); - exposure_->set_role_data(module::Attribute::DefaultValue, 0.0f); - exposure_->set_role_data(module::Attribute::ToolTip, ui_text_.CS_MSG_CMS_SET_EXP_TIP); - - // Gamma slider - - gamma_ = add_float_attribute(ui_text_.GAMMA, ui_text_.GAMMA_SHORT, 1.0f, 0.0f, 5.0f, 0.05f); - gamma_->set_redraw_viewport_on_change(true); - gamma_->set_role_data(module::Attribute::ToolbarPosition, 4.1f); - gamma_->set_role_data(module::Attribute::Activated, false); - gamma_->set_role_data(module::Attribute::DefaultValue, 1.0f); - gamma_->set_role_data(module::Attribute::ToolTip, ui_text_.CS_MSG_CMS_SET_GAMMA_TIP); - - enable_gamma_ = - add_boolean_attribute(ui_text_.ENABLE_GAMMA, ui_text_.ENABLE_GAMMA_SHORT, false); - enable_gamma_->set_redraw_viewport_on_change(true); - enable_gamma_->set_preference_path("/plugin/colour_pipeline/ocio/enable_gamma"); - - // Saturation slider - - saturation_ = add_float_attribute( - ui_text_.SATURATION, ui_text_.SATURATION_SHORT, 1.0f, 0.0f, 10.0f, 0.05f); - saturation_->set_redraw_viewport_on_change(true); - saturation_->set_role_data(module::Attribute::ToolbarPosition, 4.2f); - saturation_->set_role_data(module::Attribute::Activated, false); - saturation_->set_role_data(module::Attribute::DefaultValue, 1.0f); - saturation_->set_role_data( - module::Attribute::ToolTip, ui_text_.CS_MSG_CMS_SET_SATURATION_TIP); - - enable_saturation_ = add_boolean_attribute( - ui_text_.ENABLE_SATURATION, ui_text_.ENABLE_SATURATION_SHORT, false); - enable_saturation_->set_redraw_viewport_on_change(true); - enable_saturation_->set_preference_path("/plugin/colour_pipeline/ocio/enable_saturation"); - - // Colour bypass - - colour_bypass_ = add_boolean_attribute(ui_text_.CMS_OFF, ui_text_.CMS_OFF_SHORT, false); - - colour_bypass_->set_redraw_viewport_on_change(true); - colour_bypass_->set_role_data( - module::Attribute::Groups, nlohmann::json{"colour_pipe_attributes"}); - colour_bypass_->set_role_data(module::Attribute::Enabled, false); - colour_bypass_->set_role_data(module::Attribute::ToolTip, ui_text_.CS_BYPASS_TOOLTIP); - - // View mode - - global_view_ = add_boolean_attribute(ui_text_.VIEW_MODE, ui_text_.GLOBAL_VIEW_SHORT, false); - - global_view_->set_redraw_viewport_on_change(true); - global_view_->set_role_data( - module::Attribute::UuidRole, "ac970f58-3243-4533-8bcb-296849b58277"); - global_view_->set_role_data( - module::Attribute::Groups, nlohmann::json{"colour_pipe_attributes"}); - global_view_->set_role_data(module::Attribute::Enabled, false); - global_view_->set_role_data(module::Attribute::ToolTip, ui_text_.GLOBAL_VIEW_TOOLTIP); - global_view_->set_preference_path("/plugin/colour_pipeline/ocio/user_view_mode"); - - // Source colour space mode - - adjust_source_ = - add_boolean_attribute(ui_text_.SOURCE_CS_MODE, ui_text_.SOURCE_CS_MODE_SHORT, true); - - adjust_source_->set_redraw_viewport_on_change(true); - adjust_source_->set_role_data( - module::Attribute::UuidRole, "4eada6a9-7969-4b29-9476-ef8a9344096c"); - adjust_source_->set_role_data( - module::Attribute::Groups, nlohmann::json{"colour_pipe_attributes"}); - adjust_source_->set_role_data(module::Attribute::Enabled, false); - adjust_source_->set_role_data(module::Attribute::ToolTip, ui_text_.SOURCE_CS_MODE_TOOLTIP); - adjust_source_->set_preference_path("/plugin/colour_pipeline/ocio/user_source_mode"); - - ui_initialized_ = true; - - make_attribute_visible_in_viewport_toolbar(exposure_); - make_attribute_visible_in_viewport_toolbar(channel_); - make_attribute_visible_in_viewport_toolbar(display_); - make_attribute_visible_in_viewport_toolbar(view_); - - // Here we register particular attributes to be 'linked'. The main viewer and - // the pop-out viewer have their own instances of this class. We want certain - // attributes to always have the same value between these two instances. When - // the pop-out viewport is created, its colour pipeline instance is 'linked' - // to the colour pipeline belonging to the main viewport - any changes on one - // of the attributes below that happens in one instance is immediately synced - // to the corresponding attribute on the other instance. - link_attribute(source_colour_space_->uuid()); - link_attribute(exposure_->uuid()); - link_attribute(channel_->uuid()); - link_attribute(view_->uuid()); - link_attribute(gamma_->uuid()); - link_attribute(saturation_->uuid()); - link_attribute(global_view_->uuid()); - link_attribute(adjust_source_->uuid()); - link_attribute(enable_gamma_->uuid()); - link_attribute(enable_saturation_->uuid()); -} - -void OCIOColourPipeline::register_hotkeys() { - - for (const auto &hotkey_props : ui_text_.channel_hotkeys) { - auto hotkey_id = register_hotkey( - hotkey_props.key, - hotkey_props.modifier, - hotkey_props.name, - hotkey_props.description); - - channel_hotkeys_[hotkey_id] = hotkey_props.channel_name; - } - - reset_hotkey_ = register_hotkey( - int('R'), - ui::ControlModifier, - "Reset Colour Viewing Setting", - "Resets viewer exposure and channel mode"); - - exposure_hotkey_ = register_hotkey( - int('E'), - ui::NoModifier, - "Exposure Scrubbing", - "Hold this key down and click-scrub the mouse pointer left/right in the viewport to " - "adjust viewer exposure"); - - gamma_hotkey_ = register_hotkey( - int('Y'), - ui::NoModifier, - "Gamma Scrubbing", - "Hold this key down and click-scrub the mouse pointer left/right in the viewport to " - "adjust viewer gamma"); - - saturation_hotkey_ = register_hotkey( - int('S'), - ui::AltModifier, - "Saturation Scrubbing", - "Hold this key down and click-scrub the mouse pointer left/right in the viewport to " - "adjust viewer saturation"); -} - - -void OCIOColourPipeline::populate_ui(const MediaParams &media_param) { - - const auto ocio_config = media_param.ocio_config; - const auto ocio_config_name = media_param.ocio_config_name; - - // Store the display/view settings for the current config that's - // about to be switched out. - if (not current_config_name_.empty() and current_config_name_ != ocio_config_name) { - auto &settings = per_config_settings_[current_config_name_]; - settings.display = display_->value(); - settings.view = view_->value(); - } - - if (is_worker()) - return; - - std::map> display_views; - const auto displays = parse_display_views(ocio_config, display_views); - const auto all_colour_spaces = parse_all_colourspaces(ocio_config); - - const std::string default_disp = ocio_config->getDefaultDisplay(); - const std::string default_view = ocio_config->getDefaultView(default_disp.c_str()); - const std::string default_input_cs = ocio_config->getCanonicalName(OCIO::ROLE_DEFAULT); - - // OCIO Source colour space (do not notify as it will be updated later) - source_colour_space_->set_role_data( - module::Attribute::StringChoices, all_colour_spaces, false); - source_colour_space_->set_value(default_input_cs, false); - - // OCIO display list - display_->set_role_data(module::Attribute::StringChoices, displays, false); - - // Restore settings for the config if we've already used it, else use defaults. - std::string display; - std::string popout_display; - std::string view; - - auto it = per_config_settings_.find(ocio_config_name); - - if (it != per_config_settings_.end() and - display_views.find(it->second.display) != display_views.end()) { - display = it->second.display; - view = it->second.view; - } else { - - display = default_display(media_param, monitor_name_); - // Do not try to re-use view from other config to avoid case where - // an unmanaged media with Raw view match a Raw view in an actual - // OCIO config. - view = default_view; - - // .. however, let's see if we can use the view setting from the main - // viewport if there's a match (useful for 'quickview' windows) - auto main_ocio = - system().registry().template get("MAIN_VIEWPORT_OCIO_INSTANCE"); - if (main_ocio && main_ocio != self()) { - - try { - caf::scoped_actor sys(system()); - - auto data = utility::request_receive( - *sys, main_ocio, module::attribute_value_atom_v, "View"); - - if (data.is_string()) { - auto p = std::find( - display_views[display].begin(), - display_views[display].end(), - data.get()); - if (p != display_views[display].end()) { - view = data.get(); - } - } - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - } - } - - // Don't notify while current_source_uuid_ is not up to date. - display_->set_value(display, false); - view_->set_role_data(module::Attribute::StringChoices, display_views[display]); - view_->set_value(view, false); -} - -std::vector OCIOColourPipeline::parse_display_views( - OCIO::ConstConfigRcPtr ocio_config, - std::map> &display_views) const { - std::vector displays; - display_views.clear(); - - for (int i = 0; i < ocio_config->getNumDisplays(); ++i) { - const std::string display = ocio_config->getDisplay(i); - displays.push_back(display); - - display_views[display] = std::vector(); - for (int j = 0; j < ocio_config->getNumViews(display.c_str()); ++j) { - const std::string view = ocio_config->getView(display.c_str(), j); - display_views[display].push_back(view); - } - } - - return displays; -} - -std::vector -OCIOColourPipeline::parse_all_colourspaces(OCIO::ConstConfigRcPtr ocio_config) const { - std::vector colourspaces; - - for (int i = 0; i < ocio_config->getNumColorSpaces(); ++i) { - const char *cs_name = ocio_config->getColorSpaceNameByIndex(i); - if (cs_name && *cs_name) { - colourspaces.emplace_back(cs_name); - } - } - - return colourspaces; -} - -void OCIOColourPipeline::update_cs_from_view( - const MediaParams &media_param, const std::string &view) { - - const auto new_cs = input_space_for_view(media_param, view_->value()); - - if (!new_cs.empty() && new_cs != source_colour_space_->value()) { - MediaParams update_media_param = media_param; - update_media_param.user_input_cs = new_cs; - set_media_params(update_media_param); - - source_colour_space_->set_value(new_cs, false); - } -} - -void OCIOColourPipeline::update_views(OCIO::ConstConfigRcPtr ocio_config) { - - if (is_worker()) - return; - - std::map> display_views; - parse_display_views(ocio_config, display_views); - - const std::string new_display = display_->value(); - const auto new_views = display_views[new_display]; - - view_->set_role_data(module::Attribute::StringChoices, display_views[new_display]); - view_->set_role_data(module::Attribute::AbbrStringChoices, display_views[new_display]); - - // Check whether the current view is available under the new display or not. - const std::string curr_view = view_->value(); - bool has_curr_view = - !new_views.empty() && - std::find(new_views.begin(), new_views.end(), curr_view) != new_views.end(); - if (!has_curr_view) { - std::string default_view = ocio_config->getDefaultView(new_display.c_str()); - if (!default_view.empty()) { - view_->set_value(default_view); - } - } -} - -void OCIOColourPipeline::update_bypass(module::StringChoiceAttribute *viewer, bool bypass) { - const bool enable_options = not colour_bypass_->value(); - const std::string text = colour_bypass_->value() ? ui_text_.CMS_OFF_ICON : ""; - - const std::size_t display_options_count = - viewer->get_role_data>(module::Attribute::StringChoices) - .size(); - const std::size_t view_options_count = - view_->get_role_data>(module::Attribute::StringChoices).size(); - const std::size_t source_colourspace_options_count = - source_colour_space_ - ->get_role_data>(module::Attribute::StringChoices) - .size(); - - const std::vector display_options(display_options_count, enable_options); - viewer->set_role_data(module::Attribute::StringChoicesEnabled, display_options, false); - viewer->set_role_data(module::Attribute::OverrideValue, text, false); - - const std::vector view_options(view_options_count, enable_options); - view_->set_role_data(module::Attribute::StringChoicesEnabled, view_options, false); - view_->set_role_data(module::Attribute::OverrideValue, text, false); - - const std::vector colourspace_options( - source_colourspace_options_count, enable_options); - source_colour_space_->set_role_data( - module::Attribute::StringChoicesEnabled, colourspace_options, false); -} diff --git a/src/plugin/colour_pipeline/ocio/src/shaders.hpp b/src/plugin/colour_pipeline/ocio/src/shaders.hpp index 880104508..d36fa31f1 100644 --- a/src/plugin/colour_pipeline/ocio/src/shaders.hpp +++ b/src/plugin/colour_pipeline/ocio/src/shaders.hpp @@ -5,19 +5,23 @@ struct ShaderTemplates { static constexpr auto OCIO_display = R"( -#version 420 core +#version 410 core uniform int show_chan; uniform float saturation; //OCIODisplay +float dot(vec3 a, vec3 b) { + return a.x*b.x + a.y*b.y + a.z*b.z; +} + vec4 colour_transform_op(vec4 rgba, vec2 image_pos) { rgba = OCIODisplay(rgba); if (saturation != 1.0) { - vec3 luma_weights = { 0.2126f, 0.7152f, 0.0722f }; + vec3 luma_weights = vec3(0.2126f, 0.7152f, 0.0722f); float luma = dot(rgba.rgb, luma_weights); rgba.rgb = luma + saturation * (rgba.rgb - luma); } @@ -31,7 +35,7 @@ vec4 colour_transform_op(vec4 rgba, vec2 image_pos) } else if (show_chan == 4) { rgba = vec4(rgba.a); } else if (show_chan == 5) { - vec3 luma_weights = { 0.2126f, 0.7152f, 0.0722f }; + vec3 luma_weights = vec3(0.2126f, 0.7152f, 0.0722f); rgba = vec4(dot(rgba.rgb, luma_weights)); } @@ -40,7 +44,7 @@ vec4 colour_transform_op(vec4 rgba, vec2 image_pos) )"; static constexpr auto OCIO_linearise = R"( -#version 420 core +#version 410 core //OCIOLinearise diff --git a/src/plugin/colour_pipeline/ocio/src/ui_text.hpp b/src/plugin/colour_pipeline/ocio/src/ui_text.hpp index eaedb5412..1e9f8b29c 100644 --- a/src/plugin/colour_pipeline/ocio/src/ui_text.hpp +++ b/src/plugin/colour_pipeline/ocio/src/ui_text.hpp @@ -6,6 +6,7 @@ #include "xstudio/ui/keyboard.hpp" struct UiText { + std::string RED = "Red"; std::string GREEN = "Green"; std::string BLUE = "Blue"; @@ -61,8 +62,8 @@ struct UiText { std::string DISPLAY = "Display"; std::string DISPLAY_SHORT = "Disp"; std::string VIEW = "View"; - std::string EXPOSURE = "Exposure"; - std::string EXPOSURE_SHORT = "Exp"; + std::string EXPOSURE = "Exposure (E)"; + std::string EXPOSURE_SHORT = "Exp (E)"; std::string GAMMA = "Gamma"; std::string GAMMA_SHORT = "Gam"; std::string ENABLE_GAMMA = "Gamma Control"; @@ -78,9 +79,9 @@ struct UiText { std::string CMS_OFF = "Bypass Colour Management"; std::string CMS_OFF_SHORT = "CMS OFF"; std::string CMS_OFF_ICON = "--"; - std::string PREF_VIEW = "Preferred View"; - std::string VIEW_MODE = "Global View Control"; - std::string GLOBAL_VIEW_SHORT = "Global View"; + std::string PREF_VIEW = "OCIO Preferred View"; + std::string VIEW_MODE = "Global OCIO View"; + std::string GLOBAL_VIEW_SHORT = "Global view"; std::string SOURCE_CS_MODE = "Auto adjust source"; std::string SOURCE_CS_MODE_SHORT = "Adjust source"; @@ -111,8 +112,8 @@ struct UiText { std::string CS_MSG_CMS_SELECT_CLR_TIP = "Select colour channel to display. You can also use R,G,B,A,Ctrl+L hotkeys."; - std::string CS_MSG_CMS_SET_EXP_TIP = "Set viewer Exposure in f-stops. Double click to " - "toggle between last set value and default of 0.0."; + std::string CS_MSG_CMS_SET_EXP_TIP = "Set viewer Exposure in f-stops. Double click to " + "toggle between last set value and default of 0.0."; std::string CS_MSG_CMS_SET_GAMMA_TIP = "Set viewer Gamma. Double click to " "toggle between last set value and default of 1.0."; std::string CS_MSG_CMS_SET_SATURATION_TIP = diff --git a/src/plugin/colour_pipeline/ocio/test/CMakeLists.txt b/src/plugin/colour_pipeline/ocio/test/CMakeLists.txt index a73825679..66caa1970 100644 --- a/src/plugin/colour_pipeline/ocio/test/CMakeLists.txt +++ b/src/plugin/colour_pipeline/ocio/test/CMakeLists.txt @@ -1,7 +1,7 @@ include(CTest) SET(LINK_DEPS - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/plugin/conform/dneg/conform_shotbrowser/share/preference/plugin_conformer_shotbrowser.json b/src/plugin/conform/dneg/conform_shotbrowser/share/preference/plugin_conformer_shotbrowser.json new file mode 100644 index 000000000..29bd6017d --- /dev/null +++ b/src/plugin/conform/dneg/conform_shotbrowser/share/preference/plugin_conformer_shotbrowser.json @@ -0,0 +1,19 @@ +{ + "plugin": { + "conformer": { + "shotbrowser": { + "purge_sequence_on_import": { + "context": [ + "PLUGIN" + ], + "datatype": "bool", + "default_value": true, + "description": "Purge additional tracks on import.", + "path": "/plugin/conformer/shotbrowser/purge_sequence_on_import", + "value": true, + "category": "Pipeline" + } + } + } + } +} diff --git a/src/plugin/conform/dneg/conform_shotbrowser/src/CMakeLists.txt b/src/plugin/conform/dneg/conform_shotbrowser/src/CMakeLists.txt new file mode 100644 index 000000000..a0d22f9ef --- /dev/null +++ b/src/plugin/conform/dneg/conform_shotbrowser/src/CMakeLists.txt @@ -0,0 +1,6 @@ +SET(LINK_DEPS + xstudio::conform + xstudio::utility +) + +create_plugin_with_alias(conform_shotbrowser xstudio::conform::shotbrowser ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/plugin/conform/dneg/conform_shotbrowser/src/conform_shotbrowser.cpp b/src/plugin/conform/dneg/conform_shotbrowser/src/conform_shotbrowser.cpp new file mode 100644 index 000000000..041ee0a27 --- /dev/null +++ b/src/plugin/conform/dneg/conform_shotbrowser/src/conform_shotbrowser.cpp @@ -0,0 +1,1304 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "xstudio/conform/conformer.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/string_helpers.hpp" +#include "xstudio/utility/json_store.hpp" +#include "xstudio/timeline/track_actor.hpp" +#include "xstudio/utility/json_store_sync.hpp" + +#include "../../../../data_source/dneg/shotbrowser/src/query_engine.hpp" + +using namespace xstudio; +using namespace xstudio::conform; +using namespace xstudio::utility; +using namespace std::chrono_literals; + +class ShotbrowserConform : public Conformer { + public: + ShotbrowserConform(const utility::JsonStore &prefs = utility::JsonStore()) + : Conformer(prefs) {} + ~ShotbrowserConform() = default; + + + void update_preferences(const utility::JsonStore &prefs) override { + try { + purge_sequence_on_import_ = global_store::preference_value( + prefs, "/plugin/conformer/shotbrowser/purge_sequence_on_import"); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + + std::vector conform_tasks() override { + if (visible_tasks_.empty()) + spdlog::warn("conform_tasks() empty ? {}", __PRETTY_FUNCTION__); + return visible_tasks_; + } + + bool update_tasks(const utility::JsonStoreSync &presets) { + auto result = false; + std::vector tasks; + std::vector visible_tasks; + std::map task_uuids; + + try { + for (const auto &i : presets.as_json().at("children")) { + if (i.value("hidden", false) or not i.value("favourite", false)) + continue; + + auto flags = i.value("flags", std::set()); + + if (flags.count("Replace") or flags.count("Conform") or + flags.count("Compare")) { + // scan children.. + for (const auto &j : i.at("children").at(1).at("children")) { + if (j.value("hidden", false)) + continue; + + task_uuids[j.value("name", "")] = j.value("id", utility::Uuid()); + tasks.emplace_back(j.value("name", "")); + + + if (not j.value("favourite", true)) + continue; + + visible_tasks.emplace_back(j.value("name", "")); + } + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + if (tasks != tasks_ or visible_tasks_ != visible_tasks) { + visible_tasks_ = visible_tasks; + tasks_ = tasks; + task_uuids_ = task_uuids; + result = true; + } + + return result; + } + + std::optional get_task_id(const std::string &name) const { + if (task_uuids_.count(name)) + return task_uuids_.at(name); + + return {}; + } + + std::optional get_cut_query_id(const utility::JsonStoreSync &presets) const { + try { + // iterate over groups looking for flags containing "View In Sequence" + for (const auto &i : presets.as_json().at("children")) { + if (i.value("hidden", false) or not i.value("favourite", false)) + continue; + + auto flags = i.value("flags", std::vector()); + if (std::find(flags.begin(), flags.end(), "View In Sequence") != + std::end(flags)) { + // scan children.. + for (const auto &j : i.at("children").at(1).at("children")) { + if (j.value("hidden", false)) + continue; + + if (not j.value("favourite", true)) + continue; + + return j.value("id", utility::Uuid()); + } + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return {}; + } + + [[nodiscard]] bool purge_sequence_on_import() const { return purge_sequence_on_import_; } + + private: + std::vector tasks_; + std::vector visible_tasks_; + std::map task_uuids_; + bool purge_sequence_on_import_{true}; +}; + +template class ShotbrowserConformActor : public caf::event_based_actor { + public: + ShotbrowserConformActor( + caf::actor_config &cfg, const utility::JsonStore &prefs = utility::JsonStore()) + : caf::event_based_actor(cfg), conform_(prefs) { + spdlog::debug("Created ShotbrowserConformActor"); + utility::print_on_exit(this, "ShotbrowserConformActor"); + + { + auto prefs = global_store::GlobalStoreHelper(system()); + utility::JsonStore js; + utility::join_broadcast(this, prefs.get_group(js)); + conform_.update_preferences(js); + } + + // we need to subscribe to the shotbrowsers preset model + + + behavior_.assign( + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + + // conform media into clips, doesn't require shot browser.. as we only key off + // show/shot. + [=](conform_atom, const ConformRequest &crequest) -> result { + auto rp = make_response_promise(); + conform_request(rp, crequest); + return rp; + }, + + [=](conform_atom, + const std::string &conform_task, + const ConformRequest &crequest) -> result { + auto rp = make_response_promise(); + conform_task_request(rp, conform_task, crequest); + return rp; + }, + + // manipulate timeline to populate conform track + // and also other initial preparations + [=](conform_atom, + const UuidActor &timeline, + const bool only_create_conform_track) -> result { + // get timeline detail. + auto rp = make_response_promise(); + prepare_timeline(rp, timeline, only_create_conform_track); + return rp; + }, + + // return media / sequence pairs. + // used when conforming media into new sequences + [=](conform_atom, + const std::vector> &media) + -> result>>> { + auto rp = make_response_promise>>>(); + conform_media(rp, media); + return rp; + }, + + [=](conform_atom, + const std::string &key, + const std::pair &needle, + const std::vector> &haystack) + -> result { + auto rp = make_response_promise(); + find_matching(rp, key, needle, haystack); + return rp; + }, + + // return valid tasks for this conform engine + [=](conform_tasks_atom) -> std::vector { + setup(); + conform_.update_tasks(presets_); + return conform_.conform_tasks(); + }, + + [=](utility::event_atom, + json_store::sync_atom, + const Uuid &uuid, + const JsonStore &event) { + if (uuid == user_preset_event_id_) { + presets_.process_event(event); + if (conform_.update_tasks(presets_)) + anon_mail(conform_tasks_atom_v) + .send( + system().registry().template get(conform_registry)); + } + }, + + [=](json_store::update_atom, + const utility::JsonStore & /*change*/, + const std::string & /*path*/, + const utility::JsonStore &full) { + return mail(json_store::update_atom_v, full) + .delegate(actor_cast(this)); + }, + + [=](json_store::update_atom, const utility::JsonStore &js) { + try { + conform_.update_preferences(js); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + }); + } + + + ~ShotbrowserConformActor() override = default; + caf::behavior make_behavior() override { return behavior_; } + + private: + void conform_task_request( + caf::typed_response_promise rp, + const std::string &conform_task, + const ConformRequest &crequest) { + try { + + if (not connected_) { + setup(); + conform_.update_tasks(presets_); + } + + // spdlog::warn("conform_request {} {}", conform_task, + // conform_detail.dump(2)); spdlog::warn("conform_request {}", + // crequest.playlist_json_.dump(2)); + + if (crequest.items_.empty()) { + rp.deliver(ConformReply(crequest)); + } else { + auto query_id = conform_.get_task_id(conform_task); + + if (query_id) { + // build a query.... + + auto shotbrowser = + system().registry().template get("SHOTBROWSER"); + + if (not shotbrowser) + throw std::runtime_error("Failed to find shotbrowser"); + + + auto shotgrid_count = std::make_shared(crequest.items_.size()); + auto shotgrid_results = + std::make_shared>(crequest.items_.size()); + + // dispatch requests for shotgrid data. + for (size_t i = 0; i < crequest.items_.size(); i++) { + auto metadata = + crequest.metadata_.at(crequest.items_.at(i).item_.uuid()); + + // clip metadata has precedence + auto media_uuid = metadata.value("media_uuid", Uuid()); + if (not media_uuid.is_null() and crequest.metadata_.count(media_uuid)) { + auto tmp = crequest.metadata_.at(media_uuid); + + // if source clip media is edit ref, don't use it + // as it'll be pointing to a sequence movie,, + // but this fails if we're conforming a edit ref clip.. + // check the medi asn't also linked to a sequence instead of a shot + // ? + + auto sg_entity_type = nlohmann::json::json_pointer( + "/metadata/shotgun/version/relationships/entity/data/type"); + auto sg_twig_type_code = nlohmann::json::json_pointer( + "/metadata/shotgun/version/attributes/sg_twig_type_code"); + + if (tmp.value(sg_entity_type, "") == "Sequence" and + (tmp.value(sg_twig_type_code, "") == "cut" or + tmp.value(sg_twig_type_code, "") == "edl")) { + // ignore media metadata + } else { + tmp.update(metadata, true); + metadata = tmp; + } + // metadata.update(crequest.metadata_.at(media_uuid)); + } + + auto project_id = QueryEngine::get_project_id(metadata, JsonStore()); + + // if (not project_id) + // throw std::runtime_error("Failed to find project_id"); + // here we go.... + + auto req = JsonStore(GetExecutePreset); + req["project_id"] = project_id; + req["preset_id"] = *query_id; + req["metadata"] = metadata; + req["context"] = R"({ + "type": null, + "epoc": null, + "audio_source": [], + "sequence_source": [], + "visual_source": [], + "flag_text": "", + "flag_colour": "", + "truncated": false + })"_json; + + // req["env"] = qvariant_to_json(env); + // req["custom_terms"] = qvariant_to_json(custom_terms); + + // req["context"]["epoc"] = + // utility::to_epoc_milliseconds(utility::clock::now()); + // req["context"]["type"] = "result"; + + mail(data_source::get_data_atom_v, req) + .request(shotbrowser, infinite) + .then( + [=](const JsonStore &result) mutable { + mail( + playlist::add_media_atom_v, + result, + crequest.container_.uuid(), + crequest.container_.actor(), + crequest.items_.at(i).before_) + .request(shotbrowser, infinite) + .then( + [=](const UuidActorVector &new_media) mutable { + (*shotgrid_results)[i] = new_media; + (*shotgrid_count)--; + if (not *shotgrid_count) + process_results( + rp, *shotgrid_results, crequest); + }, + [=](caf::error &err) mutable { + (*shotgrid_count)--; + if (not *shotgrid_count) + process_results( + rp, *shotgrid_results, crequest); + }); + }, + [=](caf::error &err) mutable { + (*shotgrid_count)--; + if (not *shotgrid_count) + process_results(rp, *shotgrid_results, crequest); + }); + } + } else { + spdlog::warn( + "{} Failed to find query id {}", __PRETTY_FUNCTION__, conform_task); + rp.deliver(ConformReply(crequest)); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + } + + void conform_request( + caf::typed_response_promise rp, const ConformRequest &crequest) { + try { + auto creply = ConformReply(crequest); + auto clips = crequest.template_tracks_.at(0).find_all_items(timeline::IT_CLIP); + const auto fake_shot_ptr = + json::json_pointer("/metadata/shotgun/version/attributes/sg_pipe_tag_3"); + + // build clip lookup. + std::map clip_project_map; + std::map clip_shot_map; + std::map clip_meta_shot_map; + + // remap start frame if requested via metadata.. + for (auto &t : creply.request_.template_tracks_) { + for (auto &c : t.find_all_items(timeline::IT_CLIP)) { + + auto item_meta = c.get().prop(); + const auto cut_start_ptr = + json::json_pointer("/metadata/external/DNeg/cut/start"); + const auto override_cut_ptr = + json::json_pointer("/metadata/external/DNeg/cut/override"); + const auto override_comp_ptr = + json::json_pointer("/metadata/external/DNeg/comp/override"); + + if (item_meta.contains(override_cut_ptr) and + item_meta.at(override_cut_ptr).get() and + item_meta.contains(cut_start_ptr) and + not item_meta.at(cut_start_ptr).is_null()) { + // get item trimmed start frame + auto trimmed_range = c.get().trimmed_range(); + + // spdlog::warn("conform_request {} {} {} {}", + // item_meta.at(cut_start_ptr).get(), + // trimmed_range.rate().to_seconds(), + // trimmed_range.rate().to_fps(),trimmed_range.rate().count()); + + item_meta.at(override_cut_ptr) = false; + item_meta.at(override_comp_ptr) = false; + trimmed_range.set_start(FrameRate( + trimmed_range.rate() * item_meta.at(cut_start_ptr).get())); + + c.get().set_prop(item_meta); + c.get().set_active_range(trimmed_range); + } + } + } + + for (const auto &c : clips) { + auto clip_uuid = c.get().uuid(); + auto media_uuid = c.get().prop().value("media_uuid", Uuid()); + + // spdlog::warn("{}", c.get().prop().dump(2)); + + auto clip_project = + QueryEngine::get_project_name(crequest.metadata_.at(clip_uuid)); + auto clip_shot = QueryEngine::get_shot_name(crequest.metadata_.at(clip_uuid)); + auto clip_meta_shot = std::string(); + + if (clip_project.empty() and not media_uuid.is_null()) + clip_project = + QueryEngine::get_project_name(crequest.metadata_.at(media_uuid)); + + if (clip_shot.empty() and not media_uuid.is_null()) + clip_shot = QueryEngine::get_shot_name(crequest.metadata_.at(media_uuid)); + + if (not media_uuid.is_null() and + crequest.metadata_.at(media_uuid).contains(fake_shot_ptr) and + not crequest.metadata_.at(media_uuid).at(fake_shot_ptr).is_null()) + clip_meta_shot = crequest.metadata_.at(media_uuid).at(fake_shot_ptr); + + clip_project_map[clip_uuid] = clip_project; + clip_shot_map[clip_uuid] = clip_shot; + clip_meta_shot_map[clip_uuid] = clip_meta_shot; + + if (clip_project.empty() or clip_shot.empty()) { + spdlog::warn( + "Clip metadata not found, {} project: '{}', 'shot': {}, 'meta shot': " + "{}", + c.get().name(), + clip_project, + clip_shot, + clip_meta_shot); + } else { + // spdlog::warn("CLIP {} project: '{}', 'shot': {}, 'meta shot': {}", + // to_string(clip_uuid), clip_project, + // clip_shot, clip_meta_shot); + } + } + + std::set matched_clips; + const auto only_one_match = + crequest.operations_.value("only_one_clip_match", false); + + auto clip_track_uuid = Uuid(); + + // we're matching media to clips. + for (const auto &i : crequest.items_) { + // find match in clips.. + // get show shot.. + if (clip_track_uuid != i.clip_track_uuid_) { + matched_clips.clear(); + clip_track_uuid = i.clip_track_uuid_; + } + + if (clips.empty()) { + spdlog::warn("No clips found on selected conform track."); + creply.items_.push_back({}); + } else { + try { + // spdlog::warn("GET MEDIA SHOW/SHOT {} {}", + // to_string(std::get<0>(i).uuid()), + // crequest.metadata_.count(std::get<0>(i).uuid())); + const auto meta = crequest.metadata_.at(i.item_.uuid()); + auto project = QueryEngine::get_project_name(meta); + auto shot = QueryEngine::get_shot_name(meta); + auto meta_shot = std::string(); + + if (meta.contains(fake_shot_ptr) and + not meta.at(fake_shot_ptr).is_null()) + meta_shot = meta.at(fake_shot_ptr); + + + if (project.empty() or shot.empty()) { + creply.items_.push_back({}); + spdlog::warn( + "Media is missing metadata, {} project: {}, shot: {}", + to_string(i.item_.uuid()), + project, + shot); + } else { + // spdlog::warn( + // "Media metadata, project: {}, shot: {}, meta_shot: {}", + // project, + // shot, + // meta_shot); + auto ritems = std::vector(); + + for (const auto &c : clips) { + auto clip_uuid = c.get().uuid(); + if ((not only_one_match or + not matched_clips.count(clip_uuid)) and + clip_project_map.at(clip_uuid) == project and + clip_shot_map.at(clip_uuid) == shot and + clip_meta_shot_map.at(clip_uuid) == meta_shot) { + ritems.push_back(std::make_tuple(c.get().uuid_actor())); + matched_clips.insert(clip_uuid); + if (only_one_match) + break; + } + } + if (ritems.empty()) { + spdlog::warn( + "Media has no matching clip {} project: {}, shot: " + "{}, meta_shot: {}", + to_string(i.item_.uuid()), + project, + shot, + meta_shot); + creply.items_.push_back({}); + } else + creply.items_.push_back(ritems); + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + creply.items_.push_back({}); + } + } + } + + rp.deliver(creply); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + } + + void prepare_timeline( + caf::typed_response_promise rp, + const UuidActor &timeline, + const bool only_create_conform_track) { + + scoped_actor sys{system()}; + try { + auto found_project = std::string(); + auto timeline_item = + request_receive(*sys, timeline.actor(), timeline::item_atom_v); + + + auto timeline_path = timeline_item.prop().value("path", std::string()); + if (not timeline_path.empty()) { + std::cmatch m; + auto uri_path = caf::make_uri(timeline_path); + const auto SHOW_REGEX = + std::regex(R"(^(?:/jobs|/hosts/[^/]+/user_data\d*)/([A-Z0-9]+)/.+$)"); + + if (uri_path) { + auto posix_path = uri_to_posix_path(*uri_path); + if (std::regex_match(posix_path.c_str(), m, SHOW_REGEX)) { + found_project = m[1]; + } + } + } + // process timeline.. + // purge empty tracks. + + auto video_tracks = timeline_item.find_all_items(timeline::IT_VIDEO_TRACK); + auto insert_index = static_cast(video_tracks.size()); + auto vcount = video_tracks.size(); + if (not only_create_conform_track) { + for (auto &i : video_tracks) { + if (i.get().empty() and vcount > 1) { + auto pactor = find_parent_actor(timeline_item, i.get().uuid()); + if (pactor) { + insert_index--; + vcount--; + request_receive( + *sys, + pactor, + timeline::erase_item_atom_v, + i.get().uuid(), + true); + } + } + } + + auto audio_tracks = timeline_item.find_all_items(timeline::IT_AUDIO_TRACK); + auto acount = audio_tracks.size(); + for (auto &i : audio_tracks) { + if (i.get().empty() and acount > 1) { + auto pactor = find_parent_actor(timeline_item, i.get().uuid()); + if (pactor) { + acount--; + request_receive( + *sys, + pactor, + timeline::erase_item_atom_v, + i.get().uuid(), + true); + } + } + } + } + // create a new track with empty clips based off markers and scan track.. + // populate clips with metadata required to conform timeline + auto vtrack = timeline::Item(timeline::IT_NONE); + std::reverse(video_tracks.begin(), video_tracks.end()); + + for (const auto &i : video_tracks) { + if (not i.get().empty()) { + vtrack = i.get(); + break; + } + } + + vtrack.set_name("Conform Track"); + vtrack.set_locked(true); + vtrack.set_enabled(false); + // populate vtrack name/metadata + if (not only_create_conform_track) + anon_mail(timeline::item_lock_atom_v, true).send(vtrack.actor()); + + auto media_metadata = std::map(); + auto tframe = timeline_item.trimmed_start(); + const auto trate = timeline_item.rate(); + + auto unknown_name = std::string("UNKNOWN"); + + for (auto &i : vtrack.children()) { + if (i.item_type() == timeline::IT_CLIP) { + auto is_valid = false; + auto comp_start = R"(null)"_json; + auto comp_end = R"(null)"_json; + auto cut_start = R"(null)"_json; + auto cut_end = R"(null)"_json; + auto project = R"(null)"_json; + auto shot = R"(null)"_json; + + auto pm = + R"({"metadata": {"external": {"DNeg": {"shot": null, "show":null, "comp": {"start": null, "end":null, "override": true}, "cut": {"start": null, "end":null, "override":true}}}}})"_json; + + // from check clip metadata (FEAT ANIM) + if (not is_valid and not found_project.empty()) { + auto cm = i.prop(); + if (cm.contains("media_stalk_dnuuid")) { + project = found_project; + + cut_start = i.trimmed_frame_start().frames(); + cut_end = i.trimmed_frame_start().frames() + + i.trimmed_frame_duration().frames() - 1; + + shot = i.name(); + is_valid = true; + // if (auto tmp = cm.value("shot_label", ""); not tmp.empty()) { + // shot = tmp; + // is_valid = true; + // } + } + } + + // from turnover auto markers + if (not is_valid) { + // marker should have same start time as clip.. + // markers exist on stack.. + for (const auto &m : timeline_item.children().front().markers()) { + if (m.start() == tframe) { + if (m.prop().contains("comp")) { + if (auto tmp = m.prop().at("comp").value( + "start", std::numeric_limits::max()); + tmp != std::numeric_limits::max()) + comp_start = tmp; + if (auto tmp = m.prop().at("comp").value( + "end", std::numeric_limits::max()); + tmp != std::numeric_limits::max()) + comp_end = tmp; + } + + if (m.prop().contains("cut")) { + if (auto tmp = m.prop().at("cut").value( + "start", std::numeric_limits::max()); + tmp != std::numeric_limits::max()) + cut_start = tmp; + if (auto tmp = m.prop().at("cut").value( + "end", std::numeric_limits::max()); + tmp != std::numeric_limits::max()) + cut_end = tmp; + } + + if (auto tmp = m.prop().value("show", found_project); + not tmp.empty()) + project = tmp; + + if (auto tmp = m.prop().value("shot", ""); not tmp.empty()) { + shot = tmp; + // got shot close enough... + is_valid = true; + } + } + + if (is_valid) + break; + } + } + + // from premiere markers + if (not is_valid) { + + // need a list of clips at this point in time backed down. + // do we need to check markers.. + // marker should have same start time as clip.. + // markers exist on stack.. + const auto fcpp = json::json_pointer("/fcp_xml/comment"); + + for (const auto &m : timeline_item.children().front().markers()) { + if (m.start() == tframe) { + if (m.prop().contains(fcpp) and + m.prop().at(fcpp).is_string() and not m.name().empty()) { + const static auto cutcompre = + std::regex("\\s*(\\d+)\\s*,\\s*(\\d+)\\s*-\\s*(\\d+)" + "\\s*,\\s*(\\d+)\\s*"); + auto comment = m.prop().at(fcpp).get(); + std::cmatch match; + if (std::regex_match(comment.c_str(), match, cutcompre)) { + auto start_frame = std::stoi(match[2]); + i.set_active_range(FrameRange( + FrameRate(start_frame * trate.to_flicks()), + i.trimmed_duration(), + i.rate())); + i.set_available_range(*i.active_range()); + + comp_start = std::stoi(match[1]); + cut_start = std::stoi(match[2]); + cut_end = std::stoi(match[3]); + comp_end = std::stoi(match[4]); + shot = m.name(); + project = found_project; + is_valid = true; + } else { + // spdlog::warn("no match {}", comment); + } + } + + if (is_valid) + break; + } + } + } + + // from media metadata + if (not is_valid) { + auto items = timeline_item.resolve_time_raw(tframe); + + for (const auto &j : items) { + auto clip = j.first; + + try { + project = QueryEngine::get_project_name(clip.prop()); + shot = QueryEngine::get_shot_name(clip.prop(), true); + + if (project.get().empty() or + shot.get().empty()) { + // try media metadata.. + auto media_uuid = clip.prop().value("media_uuid", Uuid()); + + if (not media_uuid.is_null() and + not media_metadata.count(media_uuid)) { + try { + auto tries = 10; + auto metadata = JsonStore(); + while (true) { + metadata = request_receive( + *sys, + clip.actor(), + playlist::get_media_atom_v, + json_store::get_json_atom_v, + Uuid(), + ""); + tries--; + if (not tries or not metadata.is_null()) + break; + std::this_thread::sleep_for(200ms); + // spdlog::warn("retry"); + } + + if (metadata.is_null()) + spdlog::warn( + "No metadata {} {} {}", + clip.name(), + metadata.dump(2), + tries); + + media_metadata[media_uuid] = metadata; + } catch (const std::exception &err) { + spdlog::warn( + "{} {} {}", + __PRETTY_FUNCTION__, + clip.name(), + err.what()); + media_metadata[media_uuid] = R"({})"_json; + } + } + + if (project.get().empty()) + project = QueryEngine::get_project_name( + media_metadata.at(media_uuid)); + if (shot.get().empty()) + shot = QueryEngine::get_shot_name( + media_metadata.at(media_uuid), true); + } + + if (not project.get().empty()) + found_project = project; + + if (not project.get().empty() and + not shot.get().empty()) { + cut_start = clip.trimmed_frame_start().frames(); + cut_end = clip.trimmed_frame_start().frames() + + clip.trimmed_frame_duration().frames() - 1; + + i.set_active_range(FrameRange( + clip.trimmed_start(), i.trimmed_duration(), i.rate())); + i.set_available_range(*i.active_range()); + is_valid = true; + break; + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + } + + // validate shot name ? + const static std::regex valid_shot_re(R"(^[a-zA-Z0-9_]+$)"); + if (not shot.is_null() and + not std::regex_match(shot.get(), valid_shot_re)) + is_valid = false; + + // update clips with results + if (not is_valid) { + auto meta = i.prop(); + pm[json::json_pointer("/metadata/external/DNeg/shot")] = unknown_name; + pm[json::json_pointer("/metadata/external/DNeg/show")] = + found_project.empty() ? unknown_name : found_project; + + meta.update(pm, true); + + if (not only_create_conform_track) { + anon_mail(timeline::item_prop_atom_v, meta).send(i.actor()); + anon_mail(timeline::item_name_atom_v, unknown_name).send(i.actor()); + anon_mail(timeline::item_flag_atom_v, "#FFFF0000").send(i.actor()); + } + + i.set_prop(meta); + i.set_name(unknown_name); + i.set_flag("#FFFF0000"); + i.set_enabled(false); + } else { + pm[json::json_pointer("/metadata/external/DNeg/show")] = project; + pm[json::json_pointer("/metadata/external/DNeg/shot")] = shot; + pm[json::json_pointer("/metadata/external/DNeg/comp/start")] = + comp_start; + pm[json::json_pointer("/metadata/external/DNeg/comp/end")] = comp_end; + pm[json::json_pointer("/metadata/external/DNeg/cut/start")] = cut_start; + pm[json::json_pointer("/metadata/external/DNeg/cut/end")] = cut_end; + + if (found_project.empty()) + found_project = project; + + auto meta = i.prop(); + meta.update(pm, true); + + i.set_prop(meta); + i.set_name(shot); + i.set_flag("#FF00FF00"); + i.set_enabled(true); + + if (not only_create_conform_track) { + anon_mail(timeline::item_prop_atom_v, meta).send(i.actor()); + anon_mail(timeline::item_name_atom_v, shot.get()) + .send(i.actor()); + anon_mail(timeline::item_flag_atom_v, "#FF00FF00").send(i.actor()); + } + } + } + tframe += i.trimmed_duration(); + } + + if (only_create_conform_track) { + // clean before adding + vtrack.reset_uuid(true); + vtrack.reset_actor(true); + vtrack.reset_media_uuid(); + auto vua = UuidActor(vtrack.uuid(), spawn(vtrack)); + + request_receive( + *sys, + timeline_item.children().front().actor(), + timeline::insert_item_atom_v, + insert_index, + UuidActorVector({vua})); + } + + auto tprop = timeline_item.prop(); + tprop["conform_track_uuid"] = vtrack.uuid(); + request_receive( + *sys, timeline_item.actor(), timeline::item_prop_atom_v, tprop); + + // purge other video tracks but only if turnover.. + if (not only_create_conform_track and conform_.purge_sequence_on_import() and + vcount > 1) { + + // check meta + auto tmeta = request_receive( + *sys, + timeline.actor(), + json_store::get_json_atom_v, + "/metadata/shotgun/version/attributes"); + if (tmeta.at("sg_twig_type") == "data/clip/cut" and + ends_with(tmeta.at("sg_twig_name").get(), "_turnover")) + request_receive( + *sys, + timeline_item.children().front().actor(), + timeline::erase_item_atom_v, + 0, + static_cast(vcount - 1), + true); + } + + // we shouldn't return until the main timeline item is fully sync'd with it's + // children. how do we tell ? + + if (not only_create_conform_track) { + // we get here when this function is called by the system + // we might be chaining a conform after this.. + // so we need to be in sync.. + // request item + auto done = false; + while (not done) { + try { + auto conform_item = request_receive( + *sys, timeline.actor(), timeline::item_atom_v, vtrack.uuid()); + done = true; + for (const auto &i : conform_item) { + // all clips in the base video track should be red or green.. + if (i.item_type() == timeline::IT_CLIP and + not(i.flag() == "#FFFF0000" or i.flag() == "#FF00FF00")) { + done = false; + spdlog::info("Waiting for timeline to sync. {}", timeline_path); + std::this_thread::sleep_for(1s); + break; + } + } + } catch (...) { + spdlog::warn("can't find conform track"); + break; + } + } + spdlog::info("Timeline is ready. {}", timeline_path); + } + + rp.deliver(true); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(false); + } + } + + void conform_media( + caf::typed_response_promise>>> rp, + const std::vector> &media) { + + // for(const auto &i: media) { + // spdlog::warn("{}", i.second.dump(2)); + // } + + try { + + if (not connected_) { + setup(); + conform_.update_tasks(presets_); + } + + auto query_id = conform_.get_cut_query_id(presets_); + + auto shotbrowser = system().registry().template get("SHOTBROWSER"); + + if (not shotbrowser) + throw std::runtime_error("Failed to find shotbrowser"); + + auto ivy = system().registry().template get("IVYDATASOURCE"); + + if (not ivy) + throw std::runtime_error("Failed to find Ivy Datasource"); + + if (query_id) { + + auto shotgrid_count = std::make_shared(media.size()); + auto shotgrid_results = std::make_shared>>>( + media.size()); + + auto count = 0; + for (const auto &i : media) { + auto project_id = QueryEngine::get_project_id(i.second, JsonStore()); + + auto req = JsonStore(GetExecutePreset); + req["project_id"] = project_id; + req["preset_id"] = *query_id; + req["metadata"] = i.second; + req["context"] = R"({ + "type": null, + "epoc": null, + "audio_source": [], + "sequence_source": [], + "visual_source": [], + "flag_text": "", + "flag_colour": "", + "truncated": false + })"_json; + + mail(data_source::get_data_atom_v, req) + .request(shotbrowser, infinite) + .then( + [=](const JsonStore &result) mutable { + auto vpath = nlohmann::json::json_pointer("/result/data/0"); + auto jpath = + nlohmann::json::json_pointer("/result/data/0/attributes"); + auto spath = nlohmann::json::json_pointer( + "/result/data/0/relationships/entity/data"); + + auto preferred_sequence = + nlohmann::json::json_pointer("/context/sequence_source"); + + // we need to call ivy to get leafs for this version so we can + // select the otio + // spdlog::warn("{}", result.dump(2)); + + if (result.contains(jpath)) { + // make request to ivy.. + mail( + data_source::get_data_atom_v, + result.at(jpath).value( + "sg_project_name", std::string()), + result.at(jpath).value("sg_ivy_dnuuid", Uuid())) + .request(ivy, infinite) + .then( + [=](const JsonStore &ivyresult) mutable { + // find otio.. + // iterate over preferred_sequence + auto otiopath = std::string(); + for (const auto &i : + result.at(preferred_sequence)) { + for (const auto &file : + ivyresult.at("data") + .at("versions_by_id") + .at(0) + .at("files")) { + if (file.at("name") == i and + fs::exists(forward_remap_file_path(file.at("path")))) { + otiopath = file.at("path"); + break; + } + } + if (not otiopath.empty()) + break; + } + + auto name = result.at(spath).value( + "name", std::string()); + + try { + auto uri = posix_path_to_uri(otiopath); + JsonStore meta( + R"({"metadata":{"shotgun":{"version":{}}}})"_json); + meta["metadata"]["shotgun"]["version"] = + result.at(vpath); + (*shotgrid_results)[count] = + std::make_tuple(name, uri, meta); + } catch (...) { + (*shotgrid_results)[count] = {}; + } + + (*shotgrid_count)--; + if (not *shotgrid_count) + rp.deliver(*shotgrid_results); + }, + [=](caf::error &err) mutable { + (*shotgrid_count)--; + if (not *shotgrid_count) + rp.deliver(*shotgrid_results); + }); + } else { + (*shotgrid_results)[count] = {}; + (*shotgrid_count)--; + if (not *shotgrid_count) + rp.deliver(*shotgrid_results); + } + }, + [=](caf::error &err) mutable { + (*shotgrid_count)--; + if (not *shotgrid_count) + rp.deliver(*shotgrid_results); + }); + count++; + } + } else { + spdlog::warn("{} Sequence preset not found", __PRETTY_FUNCTION__); + rp.deliver( + std::vector< + std::optional>>( + media.size())); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + } + + + void setup() { + if (not connected_) { + sb_actor_ = system().registry().template get("SHOTBROWSER"); + if (sb_actor_) { + scoped_actor sys{system()}; + try { + auto uuids = + request_receive(*sys, sb_actor_, json_store::sync_atom_v); + user_preset_event_id_ = uuids[0]; + + // get system presets + auto data = request_receive( + *sys, sb_actor_, json_store::sync_atom_v, user_preset_event_id_); + presets_ = JsonStoreSync(data); + + + // leave events. + if (preset_events_) { + try { + request_receive( + *sys, preset_events_, broadcast::leave_broadcast_atom_v, this); + } catch (const std::exception &) { + } + preset_events_ = caf::actor(); + } + + // join events + try { + preset_events_ = request_receive( + *sys, sb_actor_, get_event_group_atom_v); + request_receive( + *sys, preset_events_, broadcast::join_broadcast_atom_v, this); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + connected_ = true; + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } else { + spdlog::warn("{} Failed to connect to ShotBrowser", __PRETTY_FUNCTION__); + } + } + } + + + void process_results( + caf::typed_response_promise rp, + const std::vector &results, + const ConformRequest &crequest) { + auto creply = ConformReply(crequest); + + // adjust the clip start frame if it doesn't match the clip metadata + // to support using the client movie as the conform track, which will have useless + // trimmed clip start frames. item_ should be a clone ? + if (crequest.item_type_ == "Clip") { + scoped_actor sys{system()}; + + for (auto &i : creply.request_.items_) { + // check item_metadata + const auto &item_meta = creply.request_.metadata_.at(i.item_.uuid()); + const auto cut_start_ptr = + json::json_pointer("/metadata/external/DNeg/cut/start"); + const auto override_cut_ptr = + json::json_pointer("/metadata/external/DNeg/cut/override"); + const auto override_comp_ptr = + json::json_pointer("/metadata/external/DNeg/comp/override"); + + if (item_meta.contains(override_cut_ptr) and + item_meta.at(override_cut_ptr).get() and + item_meta.contains(cut_start_ptr) and + not item_meta.at(cut_start_ptr).is_null()) { + // get item trimmed start frame + auto prop = request_receive( + *sys, i.item_.actor(), timeline::item_prop_atom_v); + auto trimmed_range = request_receive( + *sys, i.item_.actor(), timeline::trimmed_range_atom_v); + + // spdlog::warn("conform_request {} {} {} {}", + // item_meta.at(cut_start_ptr).get(), + // trimmed_range.rate().to_seconds(), + // trimmed_range.rate().to_fps(),trimmed_range.rate().count()); + + prop.at(override_cut_ptr) = false; + prop.at(override_comp_ptr) = false; + trimmed_range.set_start(FrameRate( + trimmed_range.rate() * item_meta.at(cut_start_ptr).get())); + + anon_mail(timeline::trimmed_range_atom_v, trimmed_range, trimmed_range) + .send(i.item_.actor()); + anon_mail(timeline::item_prop_atom_v, prop).send(i.item_.actor()); + } + + // spdlog::warn("{}", creply.request_.metadata_.at(i.item_.uuid()).dump(2)); + } + } + + for (const auto &i : results) { + auto ritems = std::vector(); + + for (const auto &j : i) + ritems.emplace_back(std::make_tuple(j)); + + creply.items_.push_back(ritems); + } + + creply.operations_["create_media"] = true; + creply.operations_["insert_media"] = true; + + rp.deliver(creply); + } + + void find_matching( + caf::typed_response_promise rp, + const std::string &key, + const std::pair &needle, + const std::vector> &haystack) { + // spdlog::warn("{}", needle.second.dump(2)); + + auto result = utility::UuidActorVector(); + auto pointer = + nlohmann::json::json_pointer("/metadata/shotgun/version/attributes/sg_ivy_dnuuid"); + + for (const auto &i : haystack) { + if (i.first.uuid() == needle.first.uuid()) + continue; + + try { + if (i.second.value(pointer, Uuid()) == needle.second.value(pointer, Uuid())) + result.push_back(i.first); + } catch (...) { + } + + // spdlog::warn("{}", i.second.dump(2)); + } + + + rp.deliver(result); + } + + private: + caf::behavior behavior_; + T conform_; + utility::Uuid user_preset_event_id_; + utility::JsonStoreSync presets_; + caf::actor sb_actor_; + caf::actor preset_events_; + bool connected_{false}; +}; + +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>( + {std::make_shared>>( + Uuid("ebeecb15-75c0-4aa2-9cc7-1b3ad2491c39"), + "DNeg", + "DNeg", + "DNeg Shotbrowser Conformer", + semver::version("1.0.0"))})); +} +} diff --git a/src/plugin/conform/dneg/conform_shotbrowser/test/CMakeLists.txt b/src/plugin/conform/dneg/conform_shotbrowser/test/CMakeLists.txt new file mode 100644 index 000000000..66caa1970 --- /dev/null +++ b/src/plugin/conform/dneg/conform_shotbrowser/test/CMakeLists.txt @@ -0,0 +1,7 @@ +include(CTest) + +SET(LINK_DEPS + CAF::core +) + +create_tests("${LINK_DEPS}") diff --git a/src/plugin/conform/dneg/shotgun/src/CMakeLists.txt b/src/plugin/conform/dneg/shotgun/src/CMakeLists.txt deleted file mode 100644 index 2e8036d48..000000000 --- a/src/plugin/conform/dneg/shotgun/src/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -SET(LINK_DEPS - xstudio::conform - xstudio::utility -) - -create_plugin_with_alias(conform_shotgun xstudio::conform::shotgun 0.1.0 "${LINK_DEPS}") diff --git a/src/plugin/conform/dneg/shotgun/src/conform_shotgun.cpp b/src/plugin/conform/dneg/shotgun/src/conform_shotgun.cpp deleted file mode 100644 index 5114daff7..000000000 --- a/src/plugin/conform/dneg/shotgun/src/conform_shotgun.cpp +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include "xstudio/conform/conformer.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/string_helpers.hpp" -#include "xstudio/utility/json_store.hpp" - -using namespace xstudio; -using namespace xstudio::conform; -using namespace xstudio::utility; - -class DNegConform : public Conformer { - public: - DNegConform(const utility::JsonStore &prefs = utility::JsonStore()) : Conformer(prefs) {} - ~DNegConform() override = default; - std::vector conform_tasks() override { - return std::vector({"Test"}); - } - - ConformReply conform_request( - const std::string &conform_task, - const utility::JsonStore &conform_detail, - const ConformRequest &request) override { - spdlog::warn("conform_request {} {}", conform_task, conform_detail.dump(2)); - spdlog::warn("conform_request {}", request.playlist_json_.dump(2)); - - for (const auto &i : request.items_) { - spdlog::warn("conform_request {}", std::get<0>(i).dump(2)); - } - - return ConformReply(); - } -}; - -extern "C" { -plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { - return new plugin_manager::PluginFactoryCollection( - std::vector>( - {std::make_shared>>( - Uuid("ebeecb15-75c0-4aa2-9cc7-1b3ad2491c39"), - "DNeg", - "DNeg", - "DNeg Conformer", - semver::version("1.0.0"))})); -} -} diff --git a/src/plugin/conform/dneg/shotgun/test/CMakeLists.txt b/src/plugin/conform/dneg/shotgun/test/CMakeLists.txt deleted file mode 100644 index a73825679..000000000 --- a/src/plugin/conform/dneg/shotgun/test/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -include(CTest) - -SET(LINK_DEPS - caf::core -) - -create_tests("${LINK_DEPS}") diff --git a/src/plugin/data_source/dneg/ivy/python/CMakeLists.txt b/src/plugin/data_source/dneg/ivy/python/CMakeLists.txt new file mode 100644 index 000000000..3aaeac4e0 --- /dev/null +++ b/src/plugin/data_source/dneg/ivy/python/CMakeLists.txt @@ -0,0 +1,30 @@ +find_package(Python COMPONENTS Interpreter Development) + +set(SETUP_PY_IN "${CMAKE_CURRENT_SOURCE_DIR}/setup.py.in") +set(SETUP_PY "${CMAKE_CURRENT_BINARY_DIR}/setup.py") + +file(GLOB_RECURSE DEPS ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio_pipequery *.py) + +set(OUTPUT "${CMAKE_BINARY_DIR}/bin/python") + +set(PYTHONVP "python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}") + +configure_file(${SETUP_PY_IN} ${SETUP_PY}) + +add_custom_command(OUTPUT ${OUTPUT}/lib/${PYTHONVP}/site-packages/xstudio_pipequery + COMMAND ${Python_EXECUTABLE} + ARGS setup.py install --old-and-unmanageable --prefix=${OUTPUT} + DEPENDS ${DEPS} ${CMAKE_CURRENT_SOURCE_DIR}/setup.py.in) + +add_custom_target(pipequery_python_module ALL DEPENDS __pybind_xstudio ${OUTPUT}/lib/${PYTHONVP}/site-packages/xstudio_pipequery) + + + +if(WIN32) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio_pipequery DESTINATION python/ FILES_MATCHING PATTERN "*.py") +else() + if(INSTALL_PYTHON_MODULE) + install(DIRECTORY ${OUTPUT}/lib/${PYTHONVP}/site-packages/xstudio_pipequery + DESTINATION lib/python/) + endif() +endif() diff --git a/src/plugin/data_source/dneg/ivy/python/setup.py.in b/src/plugin/data_source/dneg/ivy/python/setup.py.in new file mode 100644 index 000000000..b899196ce --- /dev/null +++ b/src/plugin/data_source/dneg/ivy/python/setup.py.in @@ -0,0 +1,9 @@ +import os +from setuptools import setup, find_packages + +setup( + name='xstudio pipequery', + version=os.getenv('ID_FULL_VERSION', '0.0.0'), + package_dir={'': '${CMAKE_CURRENT_SOURCE_DIR}/src'}, + packages=find_packages('${CMAKE_CURRENT_SOURCE_DIR}/src'), +) diff --git a/src/plugin/data_source/dneg/ivy/python/src/xstudio_pipequery/__init__.py b/src/plugin/data_source/dneg/ivy/python/src/xstudio_pipequery/__init__.py new file mode 100644 index 000000000..5892c9220 --- /dev/null +++ b/src/plugin/data_source/dneg/ivy/python/src/xstudio_pipequery/__init__.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: Apache-2.0 +"""xStudio pipequery API.""" + +from xstudio.api.auxiliary import ActorConnection +from xstudio.core import get_data_atom + +class PipeQuery(ActorConnection): + def __init__(self, connection): + """Create PipeQuery object. + + Args: + api(API): xStudio Api + """ + + ac = connection.get_actor_from_registry("IVYDATASOURCE") + + ActorConnection.__init__(self, ac.connection, ac.remote) + + + def execute_query(self, query): + """Execute pipequery query + + Args: + query(Json): call to execute + + Result: + return(Json): result + + """ + + return self.connection.request_receive( + self.remote, + get_data_atom(), + query + )[0] + + # def get_projects(self, fields=None): + # if fields is None: + # fields = ["id", "name"] + + + + # request = """{{latest_versions( + # show: \"{show}\" + # scopes: [{{name:\"{show}\"}}] + # kinds: "iss" + # statuses: APPROVED + # name_tags: {{tags: [{{name: \"type\", value: \"task\"}}], match: ALL}} + # ) + # {{ + # name + # id + # files {{ + # path + # }} + # }} + # }}""".format(show=show) + + # return self.pq.execute_query(request) + diff --git a/src/plugin/data_source/dneg/ivy/share/preference/plugin_data_source_ivy.json b/src/plugin/data_source/dneg/ivy/share/preference/plugin_data_source_ivy.json new file mode 100644 index 000000000..ecc6beefa --- /dev/null +++ b/src/plugin/data_source/dneg/ivy/share/preference/plugin_data_source_ivy.json @@ -0,0 +1,19 @@ +{ + "plugin": { + "data_source": { + "ivy": { + "enable_audio_autoload": { + "context": [ + "PLUGIN" + ], + "datatype": "bool", + "default_value": true, + "description": "Automatically load audio sources.", + "path": "/plugin/data_source/ivy/enable_audio_autoload", + "value": true, + "category": "Pipeline" + } + } + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/ivy/src/CMakeLists.txt b/src/plugin/data_source/dneg/ivy/src/CMakeLists.txt index a303fbac3..1d2663e17 100644 --- a/src/plugin/data_source/dneg/ivy/src/CMakeLists.txt +++ b/src/plugin/data_source/dneg/ivy/src/CMakeLists.txt @@ -1,12 +1,9 @@ SET(LINK_DEPS xstudio::data_source xstudio::utility - xstudio::event xstudio::module xstudio::media xstudio::http_client ) -create_plugin_with_alias(data_source_ivy xstudio::data_source::ivy 0.1.0 "${LINK_DEPS}") - -add_subdirectory(qml) \ No newline at end of file +create_plugin_with_alias(data_source_ivy xstudio::data_source::ivy ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") \ No newline at end of file diff --git a/src/plugin/data_source/dneg/ivy/src/data_source_ivy.cpp b/src/plugin/data_source/dneg/ivy/src/data_source_ivy.cpp index 4eed4fee4..d877ab6f1 100644 --- a/src/plugin/data_source/dneg/ivy/src/data_source_ivy.cpp +++ b/src/plugin/data_source/dneg/ivy/src/data_source_ivy.cpp @@ -2,7 +2,9 @@ #include #include + #include +#include #include "data_source_ivy.hpp" #include "xstudio/atoms.hpp" @@ -10,7 +12,6 @@ #include "xstudio/global_store/global_store.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/uuid.hpp" -#include "xstudio/event/event.hpp" #include "xstudio/utility/chrono.hpp" #include "xstudio/http_client/http_client_actor.hpp" @@ -25,6 +26,8 @@ const auto GetShotFromId = R"({"shot_id": null, "operation": "GetShotFromI const auto ShotgunMetadataPath = std::string("/metadata/shotgun"); const auto IvyMetadataPath = std::string("/metadata/ivy"); const auto SHOW_REGEX = std::regex(R"(^(?:/jobs|/hosts/[^/]+/user_data\d*)/([A-Z0-9]+)/.+$)"); +const auto VALID_SHOW_REGEX = std::regex(R"(^[A-Z0-9]+$)"); + const auto GetVersionIvyUuid = R"({"operation": "VersionIvyUuid", "job":null, "ivy_uuid": null})"_json; @@ -63,6 +66,7 @@ class IvyMediaWorker : public caf::event_based_actor { caf::typed_response_promise rp, const JsonStore &jsn, const FrameRate &media_rate); + void add_media_source( caf::typed_response_promise rp, const JsonStore &jsn, @@ -145,8 +149,8 @@ IvyDataSource::IvyDataSource() : DataSource("Ivy"), module::Module("IvyDataSourc else show_ = "NSFL"; - // billing_code_ = show_; - billing_code_ = "costcentre"; + billing_code_ = show ? *show : std::string("costcentre"); + // billing_code_ = "costcentre"; auto site = get_env("DNSITEDATA_SHORT_NAME"); if (site) @@ -155,6 +159,15 @@ IvyDataSource::IvyDataSource() : DataSource("Ivy"), module::Module("IvyDataSourc site_ = "gps"; } +template void IvyDataSourceActor::update_preferences(const JsonStore &js) { + try { + enable_audio_autoload_ = + preference_value(js, "/plugin/data_source/ivy/enable_audio_autoload"); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} + template IvyDataSourceActor::IvyDataSourceActor(caf::actor_config &cfg, const utility::JsonStore &) : caf::event_based_actor(cfg) { @@ -166,15 +179,26 @@ IvyDataSourceActor::IvyDataSourceActor(caf::actor_config &cfg, const utility: http_ = spawn(CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND, 20, 20); link_to(http_); + try { + auto prefs = GlobalStoreHelper(system()); + JsonStore j; + join_broadcast(this, prefs.get_group(j)); + update_preferences(j); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } size_t worker_count = 5; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" pool_ = caf::actor_pool::make( - system().dummy_execution_unit(), + system(), worker_count, [&] { return system().template spawn(actor_cast(this)); }, caf::actor_pool::round_robin()); link_to(pool_); +#pragma GCC diagnostic pop system().registry().put(ivy_registry, this); @@ -189,7 +213,20 @@ IvyDataSourceActor::IvyDataSourceActor(caf::actor_config &cfg, const utility: get_show_stalk_uuid(rp, media); return rp; }, + [=](json_store::update_atom, + const JsonStore & /*change*/, + const std::string & /*path*/, + const JsonStore &full) { + return mail(json_store::update_atom_v, full).delegate(actor_cast(this)); + }, + [=](json_store::update_atom, const JsonStore &js) { + try { + update_preferences(js); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + }, [=](data_source::use_data_atom, const std::string &show, const std::vector &paths) -> result { @@ -198,10 +235,16 @@ IvyDataSourceActor::IvyDataSourceActor(caf::actor_config &cfg, const utility: for (const auto &i : paths) ppaths.emplace_back(uri_to_posix_path(i)); + if (not std::regex_match(show.c_str(), VALID_SHOW_REGEX)) { + spdlog::warn("{} Invalid show {}", __PRETTY_FUNCTION__, show); + rp.deliver(make_error(xstudio_error::error, "Invalid show" + show)); + return rp; + } + auto httpquery = std::string(fmt::format( R"({{ files_by_path(show: "{}", paths: ["{}"]){{ - id, name, path + id, name, path, timeline_range version{{ id, name, show{{id, name}}, scope {{id, name}}, kind{{id, name}} }}, @@ -210,15 +253,14 @@ IvyDataSourceActor::IvyDataSourceActor(caf::actor_config &cfg, const utility: show, join_as_string(ppaths, "\",\""))); - request( - http_, - infinite, + mail( http_client::http_post_atom_v, data_source_.url(), data_source_.path(), data_source_.get_headers(), httpquery, data_source_.content_type()) + .request(http_, infinite) .then( [=](const httplib::Response &response) mutable { try { @@ -253,29 +295,23 @@ IvyDataSourceActor::IvyDataSourceActor(caf::actor_config &cfg, const utility: // get media metadata. auto rp = make_response_promise(); - request( - caf::actor_cast(this), - infinite, - data_source::use_data_atom_v, - media) + mail(data_source::use_data_atom_v, media) + .request(caf::actor_cast(this), infinite) .then( [=](const std::pair &uuid_show) mutable { + // spdlog::warn("{} {}", to_string(uuid_show.first), uuid_show.second); // we've got a uuid // get ivy data.. if (uuid_show.first.is_null()) return rp.deliver(UuidActorVector()); // get shotgun data. - anon_send( - pool_, - use_data_atom_v, - media, - uuid_show.second, - uuid_show.first, - true); + anon_mail( + use_data_atom_v, media, uuid_show.second, uuid_show.first, true) + .send(pool_); // delegate.. get sources from ivy ivy_load_version_sources( - rp, uuid_show.second, uuid_show.first, media_rate); + rp, uuid_show.second, uuid_show.first, media, media_rate); }, [=](const error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -307,14 +343,33 @@ IvyDataSourceActor::IvyDataSourceActor(caf::actor_config &cfg, const utility: return rp; }, + // run query.. + [=](get_data_atom, const std::string &query) -> result { + // get pipequery data. + auto rp = make_response_promise(); + pipequery(rp, query); + return rp; + }, + + // get version + [=](get_data_atom, + const std::string &show, + const utility::Uuid &version_id) -> result { + // get pipequery data. + auto rp = make_response_promise(); + get_version(rp, show, version_id); + return rp; + }, + // create media sources from stalk.. [=](use_data_atom, const std::string &show, const utility::Uuid &dnuuid, + const caf::actor &media_actor, const FrameRate &media_rate) -> result { // get pipequery data. auto rp = make_response_promise(); - ivy_load_version_sources(rp, show, dnuuid, media_rate); + ivy_load_version_sources(rp, show, dnuuid, media_actor, media_rate); return rp; }, @@ -325,6 +380,25 @@ IvyDataSourceActor::IvyDataSourceActor(caf::actor_config &cfg, const utility: if (uri.scheme() != "ivy") return UuidActorVector(); + if (to_string(uri.authority()) == "load") { + auto rp = make_response_promise(); + ivy_load(rp, uri, media_rate); + return rp; + } else { + spdlog::warn( + "Invalid Ivy action {} {}", to_string(uri.authority()), to_string(uri)); + } + return UuidActorVector(); + }, + + // handle ivy URI + [=](use_data_atom, + const caf::uri &uri, + const FrameRate &media_rate, + const bool create_playlist) -> result { + if (uri.scheme() != "ivy") + return UuidActorVector(); + if (to_string(uri.authority()) == "load") { auto rp = make_response_promise(); ivy_load(rp, uri, media_rate); @@ -410,41 +484,36 @@ void IvyMediaWorker::add_sources_to_media( auto media = spawn(i.first, uuid, media_sources); if (not select_uuid.is_null()) - anon_send(media, media::current_media_source_atom_v, select_uuid); - - anon_send( - media, - json_store::set_json_atom_v, - utility::Uuid(), - jsn, - IvyMetadataPath + "/version"); + anon_mail(media::current_media_source_atom_v, select_uuid).send(media); + anon_mail( + json_store::set_json_atom_v, utility::Uuid(), jsn, IvyMetadataPath + "/version") + .send(media); result.emplace_back(UuidActor(uuid, media)); // only bother with shotgun / audio on single media if (media_list.size() == 1) try { - anon_send( - caf::actor_cast(this), + anon_mail( data_source::use_data_atom_v, media, jsn.at("show").get(), utility::Uuid(jsn.at("id")), - true); + true) + .send(caf::actor_cast(this)); } catch (...) { } - request( - ivy_actor_, - infinite, + mail( use_data_atom_v, jsn.at("show").get(), jsn.at("scope").at("id").get(), media_rate, true) + .request(ivy_actor_, infinite) .then( [=](const UuidActorVector &uas) { - anon_send(media, media::add_media_source_atom_v, uas); + anon_mail(media::add_media_source_atom_v, uas).send(media); }, [=](const error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -455,7 +524,6 @@ void IvyMediaWorker::add_sources_to_media( rp.deliver(result); } - void IvyMediaWorker::add_media_source( caf::typed_response_promise rp, const JsonStore &jsn, @@ -468,12 +536,21 @@ void IvyMediaWorker::add_media_source( auto name = jsn.at("name").get(); if (jsn.at("version").at("kind").at("name") == "Audio") { + auto label = std::string(); + auto type = std::string(); for (const auto &nt : jsn.at("version").at("name_tags")) { - if (nt.at("name") == "type") { - name = nt.at("value"); - break; + if (nt.at("name") == "label") { + label = nt.at("value"); + name = label; + } else if (nt.at("name") == "type") { + type = nt.at("value"); + name = type; } } + + if (not label.empty() and not type.empty()) { + name = label + "-" + type; + } } const auto source_uuid = Uuid::generate(); @@ -481,7 +558,7 @@ void IvyMediaWorker::add_media_source( frame_list.empty() ? spawn(name, uri, media_rate, source_uuid) : spawn(name, uri, frame_list, media_rate, source_uuid); - anon_send(source, json_store::set_json_atom_v, jsn, IvyMetadataPath + "/file"); + anon_mail(json_store::set_json_atom_v, jsn, IvyMetadataPath + "/file").send(source); rp.deliver(UuidActor(source_uuid, source)); } @@ -510,12 +587,8 @@ void IvyMediaWorker::add_media( continue; } - request( - caf::actor_cast(this), - infinite, - media::add_media_source_atom_v, - JsonStore(i), - media_rate) + mail(media::add_media_source_atom_v, JsonStore(i), media_rate) + .request(caf::actor_cast(this), infinite) .then( [=](const UuidActor &ua) mutable { if (not ua.uuid().is_null()) @@ -537,33 +610,50 @@ void IvyMediaWorker::add_media( void IvyMediaWorker::get_show_stalk_uuid( caf::typed_response_promise> rp, const caf::actor &media) { - request(media, infinite, media::media_reference_atom_v) + mail(media::media_reference_atom_v) + .request(media, infinite) .then( [=](const std::vector &mr) mutable { static std::regex res_re(R"(^\d+x\d+$)"); std::cmatch m; auto dnuuid = utility::Uuid(); - auto show = std::string(""); + auto show = std::string(); // turn paths into possible dir locations... std::set paths; for (const auto &i : mr) { - auto tmp = fs::path(uri_to_posix_path(i.uri())).parent_path(); - if (std::regex_match(tmp.filename().string().c_str(), m, res_re)) - paths.insert(tmp.parent_path()); - else { + auto tmp = fs::path(uri_to_posix_path(i.uri())).parent_path(); + const auto filename = tmp.filename().string(); + + if (std::regex_match(filename.c_str(), m, res_re)) { + // depth is sort of random.. + // we recurse up till we reach the TWIGTYPE level e.g. SHOT/CG + tmp = tmp.parent_path(); + paths.insert(tmp); + + while (true) { + tmp = tmp.parent_path(); + auto str = tmp.string(); + if (std::count_if(str.begin(), str.end(), [](char c) { + return c == '/'; + }) < 5) + break; + paths.insert(tmp); + } + } else { // movie path... // extract - auto stalk = tmp.filename().string(); + // best try... + auto stalk = filename; tmp = tmp.parent_path(); - auto type = to_upper(tmp.filename().string()); + auto type = to_upper(filename); if (type == "ELMR") type = "ELEMENT"; else if (type == "CGR") type = "CG"; tmp = tmp.parent_path(); - tmp = tmp.parent_path(); + tmp = tmp.parent_path(); // FIXED bug in fs::filesystem. tmp /= type; tmp /= stalk; paths.insert(tmp); @@ -578,7 +668,8 @@ void IvyMediaWorker::get_show_stalk_uuid( auto fn = de.path().filename().string(); if (starts_with(fn, ".stalk_")) { // extract uuid.. - if (std::regex_match(i.string().c_str(), m, SHOW_REGEX)) { + const auto dirname = i.string(); + if (std::regex_match(dirname.c_str(), m, SHOW_REGEX)) { dnuuid = utility::Uuid(fn.substr(7)); show = m[1]; } @@ -592,6 +683,7 @@ void IvyMediaWorker::get_show_stalk_uuid( break; } + // spdlog::warn("{} {}", to_string(dnuuid), show); rp.deliver(std::make_pair(dnuuid, show)); }, [=](const error &err) mutable { @@ -616,18 +708,14 @@ void IvyMediaWorker::get_shotgun_version( const std::string &project, const utility::Uuid &stalk_dnuuid) { - auto shotgun_actor = system().registry().template get("SHOTGUNDATASOURCE"); + auto shotgun_actor = system().registry().template get("SHOTBROWSER"); if (not shotgun_actor) rp.deliver(false); else { // check it's not already there.. - request( - media, - infinite, - json_store::get_json_atom_v, - utility::Uuid(), - ShotgunMetadataPath + "/version") + mail(json_store::get_json_atom_v, utility::Uuid(), ShotgunMetadataPath + "/version") + .request(media, infinite) .then( [=](const JsonStore &jsn) mutable { try { @@ -646,17 +734,17 @@ void IvyMediaWorker::get_shotgun_version( jsre["ivy_uuid"] = to_string(stalk_dnuuid); jsre["job"] = project; - request(shotgun_actor, infinite, data_source::get_data_atom_v, jsre) + mail(data_source::get_data_atom_v, jsre) + .request(shotgun_actor, infinite) .then( [=](const JsonStore &jsn) mutable { if (jsn.count("payload")) { - request( - media, - infinite, + mail( json_store::set_json_atom_v, utility::Uuid(), JsonStore(jsn.at("payload")), ShotgunMetadataPath + "/version") + .request(media, infinite) .then( [=](const bool result) mutable { try { @@ -694,18 +782,14 @@ void IvyMediaWorker::get_shotgun_version( void IvyMediaWorker::get_shotgun_shot( caf::typed_response_promise rp, const caf::actor &media, const int shot_id) { - auto shotgun_actor = system().registry().template get("SHOTGUNDATASOURCE"); + auto shotgun_actor = system().registry().template get("SHOTBROWSER"); if (not shotgun_actor) rp.deliver(false); else { // check it's not already there.. - request( - media, - infinite, - json_store::get_json_atom_v, - utility::Uuid(), - ShotgunMetadataPath + "/shot") + mail(json_store::get_json_atom_v, utility::Uuid(), ShotgunMetadataPath + "/shot") + .request(media, infinite) .then( [=](const JsonStore &jsn) mutable { rp.deliver(true); }, [=](const error &err) mutable { @@ -714,17 +798,22 @@ void IvyMediaWorker::get_shotgun_shot( auto shotreq = JsonStore(GetShotFromId); shotreq["shot_id"] = shot_id; - request(shotgun_actor, infinite, get_data_atom_v, shotreq) + mail(get_data_atom_v, shotreq) + .request(shotgun_actor, infinite) .then( [=](const JsonStore &jsn) mutable { try { - anon_send( - media, - json_store::set_json_atom_v, - utility::Uuid(), - JsonStore(jsn.at("data")), - ShotgunMetadataPath + "/shot"); - rp.deliver(true); + if (jsn.contains("data")) { + anon_mail( + json_store::set_json_atom_v, + utility::Uuid(), + JsonStore(jsn.at("data")), + ShotgunMetadataPath + "/shot") + .send(media); + rp.deliver(true); + } else { + rp.deliver(false); + } } catch (const std::exception &err) { rp.deliver(false); spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -764,116 +853,143 @@ void IvyDataSourceActor::ivy_load( } // load stalk as vector of sources. - template -void IvyDataSourceActor::ivy_load_version_sources( - caf::typed_response_promise rp, +void IvyDataSourceActor::get_version( + caf::typed_response_promise rp, const std::string &show, - const utility::Uuid &stalk_dnuuid, - const utility::FrameRate &media_rate) { + const utility::Uuid &stalk_dnuuid) { + + if (not std::regex_match(show.c_str(), VALID_SHOW_REGEX)) { + spdlog::warn("{} Invalid show {}", __PRETTY_FUNCTION__, show); + return rp.deliver(make_error(xstudio_error::error, "Invalid show" + show)); + } + auto httpquery = std::string(fmt::format( R"({{ versions_by_id(show: "{}", ids: ["{}"]){{ - id, name, number{{major,minor,micro}}, kind{{id,name}},scope{{id,name}} + id, name, show{{name}}, number{{major,minor,micro}}, kind{{id,name}},scope{{id,name}} files{{ - id,name,path,type,version{{id,name,kind{{id,name}},scope{{id,name}}}} + id,name,path,timeline_range,type,version{{id,name,kind{{id,name}},scope{{id,name}}}} }}, }} }})", show, to_string(stalk_dnuuid))); - request( - http_, - infinite, + mail( http_client::http_post_atom_v, data_source_.url(), data_source_.path(), data_source_.get_headers(), httpquery, data_source_.content_type()) + .request(http_, infinite) .then( [=](const httplib::Response &response) mutable { try { auto jsn = nlohmann::json::parse(response.body); - - // spdlog::warn("ivy_load_version_sources {}", jsn.dump(2)); - if (jsn.count("errors")) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, jsn.dump(2)); - rp.deliver(UuidActorVector()); + rp.deliver(make_error(xstudio_error::error, "Ivy Query Failed")); } else { - // spdlog::warn("{}", jsn.dump(2)); - // we got valid results.. - // get number of leafs - const auto &ivy_files = - jsn.at("data").at("versions_by_id").at(0).at("files"); + rp.deliver(jsn); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(std::move(err)); + }); +} - const auto scope_uuid = jsn.at("data") - .at("versions_by_id") - .at(0) - .at("scope") - .at("id") - .get(); - auto files = JsonStore(R"([])"_json); - for (const auto &i : ivy_files) { - // check we want it.. - if (i.at("type") == "METADATA" or i.at("type") == "THUMBNAIL" or - i.at("type") == "SOURCE") - continue; - - // need to filter unsupported leafs.. - FrameList frame_list; - auto uri = parse_cli_posix_path(i.at("path"), frame_list, false); - if (is_file_supported(uri)) - files.push_back(i); - // spdlog::warn("{}", i.at("name")); - } +template +void IvyDataSourceActor::ivy_load_version_sources( + caf::typed_response_promise rp, + const std::string &show, + const utility::Uuid &stalk_dnuuid, + const caf::actor &media_actor, + const utility::FrameRate &media_rate) { - auto count = std::make_shared(files.size()); - auto results = std::make_shared(); - if (not *count) - return rp.deliver(UuidActorVector()); - for (const auto &i : files) { - auto payload = JsonStore(i); - payload["show"] = show; - request( - pool_, - infinite, - media::add_media_source_atom_v, - payload, - media_rate) - .then( - [=](const UuidActor &ua) mutable { - (*count)--; - if (not ua.uuid().is_null()) - results->push_back(ua); - if (not(*count)) { + mail(get_data_atom_v, show, stalk_dnuuid) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &jsn) mutable { + // spdlog::warn("{}", jsn.dump(2)); + // we got valid results.. + // get number of leafs + + const auto &ivy_files = jsn.at("data").at("versions_by_id").at(0).at("files"); + + if (media_actor) { + anon_mail( + json_store::set_json_atom_v, + utility::Uuid(), + JsonStore(jsn.at("data").at("versions_by_id").at(0)), + IvyMetadataPath + "/version") + .send(media_actor); + } - // rp.deliver(*results); - ivy_load_audio_sources( - rp, show, scope_uuid, media_rate, *results); - } - }, - [=](error &err) mutable { - (*count)--; - if (not(*count)) { - ivy_load_audio_sources( - rp, show, scope_uuid, media_rate, *results); - // rp.deliver(*results); - } - spdlog::warn( - "{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), response.body); - rp.deliver(UuidActorVector()); + + const auto scope_uuid = + jsn.at("data").at("versions_by_id").at(0).at("scope").at("id").get(); + + auto files = JsonStore(R"([])"_json); + for (const auto &i : ivy_files) { + // check we want it.. + if (i.at("type") == "METADATA" or i.at("type") == "THUMBNAIL" or + i.at("type") == "SOURCE") + continue; + + // need to filter unsupported leafs.. + FrameList frame_list; + auto uri = parse_cli_posix_path(i.at("path"), frame_list, false); + if (is_file_supported(uri)) + files.push_back(i); + // spdlog::warn("{}", i.at("name")); + } + + auto count = std::make_shared(files.size()); + auto results = std::make_shared(); + if (not *count) + return rp.deliver(UuidActorVector()); + + for (const auto &i : files) { + auto payload = JsonStore(i); + payload["show"] = show; + mail(media::add_media_source_atom_v, payload, media_rate) + .request(pool_, infinite) + .then( + [=](const UuidActor &ua) mutable { + (*count)--; + if (not ua.uuid().is_null()) + results->push_back(ua); + if (not(*count)) { + if (enable_audio_autoload_) + ivy_load_audio_sources( + rp, show, scope_uuid, media_rate, *results); + else + rp.deliver(*results); + } + }, + [=](error &err) mutable { + (*count)--; + if (not(*count)) { + if (enable_audio_autoload_) + ivy_load_audio_sources( + rp, show, scope_uuid, media_rate, *results); + else + rp.deliver(*results); + } + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); } }, + [=](error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); rp.deliver(UuidActorVector()); @@ -895,22 +1011,27 @@ void IvyDataSourceActor::ivy_load_version( auto httpquery = std::string(fmt::format( R"({{ versions_by_id(show: "{}", ids: [{}]){{ - id, name, number{{major,minor,micro}}, kind{{id,name}},scope{{id,name}} - files{{id,name,path,type,version{{id,name,kind{{id,name}},scope{{id,name}}}}}}, + id, name, show{{name}}, number{{major,minor,micro}}, kind{{id,name}},scope{{id,name}} + files{{id,name,path,timeline_range,type,version{{id,name,kind{{id,name}},scope{{id,name}}}}}}, }} }})", show, ids)); - request( - http_, - infinite, + if (not std::regex_match(show.c_str(), VALID_SHOW_REGEX)) { + spdlog::warn("{} Invalid show {}", __PRETTY_FUNCTION__, show); + return rp.deliver(make_error(xstudio_error::error, "Invalid show" + show)); + } + + + mail( http_client::http_post_atom_v, data_source_.url(), data_source_.path(), data_source_.get_headers(), httpquery, data_source_.content_type()) + .request(http_, infinite) .then( [=](const httplib::Response &response) mutable { try { @@ -935,12 +1056,8 @@ void IvyDataSourceActor::ivy_load_version( payload["show"] = show; - request( - pool_, - infinite, - playlist::add_media_atom_v, - payload, - media_rate) + mail(playlist::add_media_atom_v, payload, media_rate) + .request(pool_, infinite) .then( [=](const UuidActorVector &uav) mutable { (*count)--; @@ -984,21 +1101,25 @@ void IvyDataSourceActor::ivy_load_file( auto httpquery = std::string(fmt::format( R"({{ files_by_id(show: "{}", ids: [{}]){{ - id, name, path, type, version{{id,name,kind{{id,name}},scope{{id,name}}}} + id, name, path,timeline_range, type, version{{id,name,kind{{id,name}},scope{{id,name}}}} }} }})", show, ids)); - request( - http_, - infinite, + if (not std::regex_match(show.c_str(), VALID_SHOW_REGEX)) { + spdlog::warn("{} Invalid show {}", __PRETTY_FUNCTION__, show); + return rp.deliver(make_error(xstudio_error::error, "Invalid show" + show)); + } + + mail( http_client::http_post_atom_v, data_source_.url(), data_source_.path(), data_source_.get_headers(), httpquery, data_source_.content_type()) + .request(http_, infinite) .then( [=](const httplib::Response &response) mutable { try { @@ -1021,12 +1142,8 @@ void IvyDataSourceActor::ivy_load_file( payload["show"] = show; // process media files. - request( - pool_, - infinite, - media::add_media_source_atom_v, - payload, - media_rate) + mail(media::add_media_source_atom_v, payload, media_rate) + .request(pool_, infinite) .then( [=](const UuidActor &ua) mutable { // may have a new media item to add to playlist.. @@ -1039,13 +1156,13 @@ void IvyDataSourceActor::ivy_load_file( utility::UuidActorVector({ua})); try { // add shotgun ? - anon_send( - pool_, + anon_mail( data_source::use_data_atom_v, media, std::string(show), utility::Uuid(i.at("version").at("id")), - true); + true) + .send(pool_); } catch (...) { } @@ -1133,8 +1250,8 @@ void IvyDataSourceActor::handle_drop( "&ids=" + terms[terms.size() - 2]); if (uri) uris.push_back(*uri); - // anon_send(caf::actor_cast(this), use_data_atom_v, *uri, - // playlist.second.second); + // anon_mail(use_data_atom_v, *uri, + // playlist.second.second).send(caf::actor_cast(this)); } } } @@ -1144,7 +1261,8 @@ void IvyDataSourceActor::handle_drop( auto media = std::make_shared(); for (const auto &i : uris) { - request(caf::actor_cast(this), infinite, use_data_atom_v, i, media_rate) + mail(use_data_atom_v, i, media_rate) + .request(caf::actor_cast(this), infinite) .then( [=](const UuidActorVector &uav) mutable { (*count)--; @@ -1170,7 +1288,8 @@ template void IvyDataSourceActor::get_show_stalk_uuid( caf::typed_response_promise> rp, const caf::actor &media) { - request(media, infinite, json_store::get_json_atom_v, utility::Uuid(), "") + mail(json_store::get_json_atom_v, utility::Uuid(), "") + .request(media, infinite) .then( [=](const JsonStore &jsn) mutable { // spdlog::warn("{}", jsn.dump(2)); @@ -1217,7 +1336,8 @@ void IvyDataSourceActor::get_show_stalk_uuid( } catch (...) { // try finding via media source paths... // collect media source paths. - request(media, infinite, media::media_reference_atom_v) + mail(media::media_reference_atom_v) + .request(media, infinite) .then( [=](const std::vector &refs) mutable { // collect paths. @@ -1248,12 +1368,9 @@ void IvyDataSourceActor::get_show_stalk_uuid( // spdlog::error("615 {}", show); // get stalks from paths - request( - caf::actor_cast(this), - infinite, - data_source::use_data_atom_v, - show, - paths) + mail(data_source::use_data_atom_v, show, paths) + .request( + caf::actor_cast(this), infinite) .then( [=](const JsonStore &data) mutable { try { @@ -1310,6 +1427,33 @@ void IvyDataSourceActor::get_show_stalk_uuid( }); } +template +void IvyDataSourceActor::pipequery( + caf::typed_response_promise rp, const std::string &query) { + + mail( + http_client::http_post_atom_v, + data_source_.url(), + data_source_.path(), + data_source_.get_headers(), + query, + data_source_.content_type()) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + auto jsn = nlohmann::json::parse(response.body); + rp.deliver(JsonStore(jsn)); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); +} template void IvyDataSourceActor::ivy_load_audio_sources( @@ -1332,6 +1476,7 @@ void IvyDataSourceActor::ivy_load_audio_sources( ) {{ id name + show{{name}} number {{ major minor @@ -1346,6 +1491,7 @@ void IvyDataSourceActor::ivy_load_audio_sources( id name path + timeline_range type version{{ id @@ -1363,15 +1509,19 @@ void IvyDataSourceActor::ivy_load_audio_sources( show, to_string(stem_dnuuid))); - request( - http_, - infinite, + if (not std::regex_match(show.c_str(), VALID_SHOW_REGEX)) { + spdlog::warn("{} Invalid show {}", __PRETTY_FUNCTION__, show); + return rp.deliver(make_error(xstudio_error::error, "Invalid show" + show)); + } + + mail( http_client::http_post_atom_v, data_source_.url(), data_source_.path(), data_source_.get_headers(), httpquery, data_source_.content_type()) + .request(http_, infinite) .then( [=](const httplib::Response &response) mutable { try { @@ -1412,12 +1562,8 @@ void IvyDataSourceActor::ivy_load_audio_sources( // spdlog::warn("{}", i.dump(2)); auto payload = JsonStore(i); payload["show"] = show; - request( - pool_, - infinite, - media::add_media_source_atom_v, - payload, - media_rate) + mail(media::add_media_source_atom_v, payload, media_rate) + .request(pool_, infinite) .then( [=](const UuidActor &ua) mutable { (*count)--; diff --git a/src/plugin/data_source/dneg/ivy/src/data_source_ivy.hpp b/src/plugin/data_source/dneg/ivy/src/data_source_ivy.hpp index 0a0d75350..28f4c51d8 100644 --- a/src/plugin/data_source/dneg/ivy/src/data_source_ivy.hpp +++ b/src/plugin/data_source/dneg/ivy/src/data_source_ivy.hpp @@ -39,7 +39,7 @@ class IvyDataSource : public DataSource, public module::Module { return utility::JsonStore(); } - std::string url() const { return "http://pipequery.zro"; } + std::string url() const { return "http://pipequery"; } std::string path() const { return "/v1/graphql"; } std::string show() const { return show_; } std::string content_type() const { return "application/graphql"; } @@ -66,6 +66,14 @@ template class IvyDataSourceActor : public caf::event_based_actor { void on_exit() override; private: + void get_version( + caf::typed_response_promise rp, + const std::string &show, + const utility::Uuid &id); + + void + pipequery(caf::typed_response_promise rp, const std::string &query); + void ivy_load( caf::typed_response_promise rp, const caf::uri &uri, @@ -85,6 +93,7 @@ template class IvyDataSourceActor : public caf::event_based_actor { caf::typed_response_promise rp, const std::string &show, const utility::Uuid &stalk_dnuuid, + const caf::actor &media_actor, const utility::FrameRate &media_rate); void ivy_load_audio_sources( @@ -103,9 +112,12 @@ template class IvyDataSourceActor : public caf::event_based_actor { const utility::JsonStore &jsn, const utility::FrameRate &media_rate); + void update_preferences(const utility::JsonStore &js); + caf::behavior behavior_; T data_source_; caf::actor http_; caf::actor pool_; + bool enable_audio_autoload_{true}; utility::Uuid uuid_ = {utility::Uuid::generate()}; }; diff --git a/src/plugin/data_source/dneg/ivy/test/CMakeLists.txt b/src/plugin/data_source/dneg/ivy/test/CMakeLists.txt index a73825679..66caa1970 100644 --- a/src/plugin/data_source/dneg/ivy/test/CMakeLists.txt +++ b/src/plugin/data_source/dneg/ivy/test/CMakeLists.txt @@ -1,7 +1,7 @@ include(CTest) SET(LINK_DEPS - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/plugin/data_source/dneg/shotbrowser/python/CMakeLists.txt b/src/plugin/data_source/dneg/shotbrowser/python/CMakeLists.txt new file mode 100644 index 000000000..792a93f27 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/python/CMakeLists.txt @@ -0,0 +1,28 @@ +find_package(Python COMPONENTS Interpreter Development) + +set(SETUP_PY_IN "${CMAKE_CURRENT_SOURCE_DIR}/setup.py.in") +set(SETUP_PY "${CMAKE_CURRENT_BINARY_DIR}/setup.py") + +file(GLOB_RECURSE DEPS ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio_shotbrowser *.py) + +set(OUTPUT "${CMAKE_BINARY_DIR}/bin/python") + +set(PYTHONVP "python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}") + +configure_file(${SETUP_PY_IN} ${SETUP_PY}) + +add_custom_command(OUTPUT ${OUTPUT}/lib/${PYTHONVP}/site-packages/xstudio_shotbrowser + COMMAND ${Python_EXECUTABLE} + ARGS setup.py install --old-and-unmanageable --prefix=${OUTPUT} + DEPENDS ${DEPS} ${CMAKE_CURRENT_SOURCE_DIR}/setup.py.in) + +add_custom_target(shotbrowser_python_module ALL DEPENDS __pybind_xstudio ${OUTPUT}/lib/${PYTHONVP}/site-packages/xstudio_shotbrowser) + +if(WIN32) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/xstudio_shotbrowser DESTINATION python/ FILES_MATCHING PATTERN "*.py") +else() + if(INSTALL_PYTHON_MODULE) + install(DIRECTORY ${OUTPUT}/lib/${PYTHONVP}/site-packages/xstudio_shotbrowser + DESTINATION lib/python/) + endif() +endif() diff --git a/src/plugin/data_source/dneg/shotbrowser/python/setup.py.in b/src/plugin/data_source/dneg/shotbrowser/python/setup.py.in new file mode 100644 index 000000000..942a9166e --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/python/setup.py.in @@ -0,0 +1,9 @@ +import os +from setuptools import setup, find_packages + +setup( + name='xstudio shotbrowser', + version=os.getenv('ID_FULL_VERSION', '0.0.0'), + package_dir={'': '${CMAKE_CURRENT_SOURCE_DIR}/src'}, + packages=find_packages('${CMAKE_CURRENT_SOURCE_DIR}/src'), +) diff --git a/src/plugin/data_source/dneg/shotbrowser/python/src/xstudio_shotbrowser/__init__.py b/src/plugin/data_source/dneg/shotbrowser/python/src/xstudio_shotbrowser/__init__.py new file mode 100644 index 000000000..f731ff895 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/python/src/xstudio_shotbrowser/__init__.py @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: Apache-2.0 +"""xStudio pipequery API.""" + +from xstudio.api.auxiliary import ActorConnection +from xstudio.core import get_data_atom, JsonStore, shotgun_update_entity_atom, VectorString + +class ShotBrowser(ActorConnection): + def __init__(self, connection): + """Create PipeQuery object. + + Args: + api(API): xStudio Api + """ + + ac = connection.get_actor_from_registry("SHOTBROWSER") + + ActorConnection.__init__(self, ac.connection, ac.remote) + + def get_data(self, data_type, project_id=-1): + request = {"operation": "GetData", "type": data_type, "project_id": project_id} + return self.connection.request_receive( + self.remote, + get_data_atom(), + JsonStore(request) + )[0] + + def get_projects(self): + request = {"operation": "GetData", "type": "project", "project_id": 0} + return self.connection.request_receive( + self.remote, + get_data_atom(), + JsonStore(request) + )[0] + + def get_project_sequence(self, project_id): + request = {"operation": "GetData", "type": "sequence", "project_id": project_id} + return self.connection.request_receive( + self.remote, + get_data_atom(), + JsonStore(request) + )[0] + + def get_project_episode(self, project_id): + request = {"operation": "GetData", "type": "episode", "project_id": project_id} + return self.connection.request_receive( + self.remote, + get_data_atom(), + JsonStore(request) + )[0] + + def get_project_shot(self, project_id): + request = {"operation": "GetData", "type": "sequence_shot", "project_id": project_id} + return self.connection.request_receive( + self.remote, + get_data_atom(), + JsonStore(request) + )[0] + + def get_project_scope(self, project_id): + seq = self.get_project_sequence(project_id) + shot = self.get_project_shot(project_id) + return seq.get() + shot.get() + + + def update_entity(self, entity, record_id, body, fields=None): + if fields is None: + fields = [] + return self.connection.request_receive( + self.remote, + shotgun_update_entity_atom(), + entity, + record_id, + JsonStore(body), + VectorString(fields))[0] diff --git a/src/plugin/data_source/dneg/shotbrowser/share/preference/plugin_data_source_shotbrowser.json b/src/plugin/data_source/dneg/shotbrowser/share/preference/plugin_data_source_shotbrowser.json new file mode 100644 index 000000000..fd8f2eb8e --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/share/preference/plugin_data_source_shotbrowser.json @@ -0,0 +1,26702 @@ +{ + "plugin": { + "data_source": { + "shotbrowser": { + "authentication": { + "client_id": { + "context": [ + "PLUGIN" + ], + "datatype": "string", + "default_value": "", + "description": "Client Id.", + "path": "/plugin/data_source/shotbrowser/authentication/client_id", + "value": "" + }, + "client_secret": { + "context": [ + "PLUGIN" + ], + "datatype": "string", + "default_value": "", + "description": "Client Secret.", + "path": "/plugin/data_source/shotbrowser/authentication/client_secret", + "value": "" + }, + "grant_type": { + "context": [ + "PLUGIN" + ], + "datatype": "string", + "default_value": "password", + "description": "Authentication method.", + "path": "/plugin/data_source/shotbrowser/authentication/grant_type", + "value": "password" + }, + "password": { + "context": [ + "PLUGIN" + ], + "datatype": "string", + "default_value": "", + "description": "Authentication password.", + "path": "/plugin/data_source/shotbrowser/authentication/password", + "value": "" + }, + "refresh_token": { + "context": [ + "PLUGIN" + ], + "datatype": "string", + "default_value": "", + "description": "Authentication refresh token.", + "path": "/plugin/data_source/shotbrowser/authentication/refresh_token", + "value": "" + }, + "session_token": { + "context": [ + "PLUGIN" + ], + "datatype": "string", + "default_value": "", + "description": "Authentication session_token.", + "path": "/plugin/data_source/shotbrowser/authentication/session_token", + "value": "" + }, + "username": { + "context": [ + "PLUGIN" + ], + "datatype": "string", + "default_value": "${USER}", + "description": "Authentication Username.", + "path": "/plugin/data_source/shotbrowser/authentication/username", + "value": "${USER}" + } + }, + "download": { + "path": { + "context": [ + "PLUGIN" + ], + "datatype": "string", + "default_value": "${HOME}/xStudio/shotbrowser_cache", + "description": "Path to shotbrowser download cache.", + "path": "/plugin/data_source/shotbrowser/download/path", + "value": "${TMPDIR}/${USER}/xStudio/shotbrowser_cache" + }, + "size": { + "context": [ + "PLUGIN" + ], + "datatype": "int", + "default_value": 5, + "description": "Cache size in GBytes.", + "path": "/plugin/data_source/shotbrowser/download/size", + "value": 5 + } + }, + "browser": { + "location": { + "context": [ + "PLUGIN" + ], + "datatype": "string", + "default_value": "${DNSITEDATA_SHORT_NAME}", + "description": "Location.", + "path": "/plugin/data_source/shotbrowser/browser/location", + "value": "${DNSITEDATA_SHORT_NAME}" + }, + "project": { + "context": [ + "PLUGIN" + ], + "datatype": "string", + "default_value": "NSFL", + "description": "Project.", + "path": "/plugin/data_source/shotbrowser/browser/project", + "value": "NSFL" + }, + "popout": { + "context": [ + "PLUGIN" + ], + "datatype": "json", + "default_value": {}, + "description": "Popout prefs.", + "path": "/plugin/data_source/shotbrowser/browser/popout", + "value": {} + }, + "pipestep": { + "context": [ + "PLUGIN" + ], + "datatype": "json", + "default_value": [], + "description": "Default pipesteps.", + "path": "/plugin/data_source/shotbrowser/browser/pipestep", + "value": [ + { + "name": "Anim" + }, + { + "name": "Body Track" + }, + { + "name": "Camera Track" + }, + { + "name": "Comp" + }, + { + "name": "Creature" + }, + { + "name": "Creature FX" + }, + { + "name": "Crowd" + }, + { + "name": "Digimatte" + }, + { + "name": "DMP" + }, + { + "name": "Dmpsetup" + }, + { + "name": "Editorial" + }, + { + "name": "Envfinishing" + }, + { + "name": "Environ" + }, + { + "name": "Envlighting" + }, + { + "name": "Envsetup" + }, + { + "name": "FX" + }, + { + "name": "Groom" + }, + { + "name": "Layout" + }, + { + "name": "Lighting" + }, + { + "name": "Look Dev" + }, + { + "name": "Model" + }, + { + "name": "Muscle" + }, + { + "name": "Paintover" + }, + { + "name": "Postvis" + }, + { + "name": "Prep" + }, + { + "name": "Previs" + }, + { + "name": "Retime Layout" + }, + { + "name": "Rig" + }, + { + "name": "Roto" + }, + { + "name": "Scan" + }, + { + "name": "Shot Sculpt" + }, + { + "name": "Skin" + }, + { + "name": "Sweatbox" + }, + { + "name": "TD" + }, + { + "name": "None" + } + ] + } + }, + "note_publishing": { + "note_publish_settings": { + "context": [ + "PLUGIN" + ], + "datatype": "json", + "default_value": { + "__ignore__": true, + "addFrame": false, + "addPlaylistName": true, + "addType": false, + "combine": true, + "defaultType": "", + "ignoreEmpty": false, + "notifyCreator": true, + "skipAlreadyPublished": false, + "notifyGroups": [], + "settings": {} + }, + "description": "Prefs relating to note publishing.", + "path": "/plugin/data_source/shotbrowser/note_publishing/note_publish_settings", + "value": { + "__ignore__": true, + "addFrame": false, + "addPlaylistName": true, + "addType": false, + "combine": true, + "defaultType": "", + "ignoreEmpty": false, + "notifyCreator": true, + "skipAlreadyPublished": false, + "notifyGroups": [], + "settings": {} + } + } + }, + "server": { + "host": { + "context": [ + "PLUGIN" + ], + "datatype": "string", + "default_value": "shotgun.dneg.com", + "description": "Shotgun host.", + "path": "/plugin/data_source/shotbrowser/server/host", + "value": "shotgun.dneg.com" + }, + "port": { + "context": [ + "PLUGIN" + ], + "datatype": "int", + "default_value": 0, + "description": "Shotgun host port.", + "path": "/plugin/data_source/shotbrowser/server/port", + "value": 0 + }, + "protocol": { + "context": [ + "PLUGIN" + ], + "datatype": "string", + "default_value": "https", + "description": "Connection protocol.", + "path": "/plugin/data_source/shotbrowser/server/protocol", + "value": "http" + }, + "timeout": { + "context": [ + "PLUGIN" + ], + "datatype": "int", + "default_value": 120, + "description": "Connection timeout.", + "path": "/plugin/data_source/shotbrowser/server/timeout", + "value": 120 + } + }, + + "fields": + { + "version": { + "context": [ + "PLUGIN" + ], + "datatype": "json", + "default_value": [], + "description": "Additional entity properties.", + "path": "/plugin/data_source/shotbrowser/fields/version", + "value": [] + }, + "note": { + "context": [ + "PLUGIN" + ], + "datatype": "json", + "default_value": [], + "description": "Additional entity properties.", + "path": "/plugin/data_source/shotbrowser/fields/note", + "value": [] + }, + "shot": { + "context": [ + "PLUGIN" + ], + "datatype": "json", + "default_value": [], + "description": "Additional entity properties.", + "path": "/plugin/data_source/shotbrowser/fields/shot", + "value": [] + }, + "playlist": { + "context": [ + "PLUGIN" + ], + "datatype": "json", + "default_value": [], + "description": "Additional entity properties.", + "path": "/plugin/data_source/shotbrowser/fields/playlist", + "value": [] + } + }, + + "add_after_selection": { + "context": [ + "PLUGIN" + ], + "datatype": "bool", + "default_value": true, + "description": "Add new media after selection, if false will add to end.", + "path": "/plugin/data_source/shotbrowser/add_after_selection", + "value": true, + "category": "Pipeline" + }, + + "disable_integration": { + "context": [ + "PLUGIN" + ], + "datatype": "bool", + "default_value": false, + "description": "Disable Shotbrowser integration.", + "path": "/plugin/data_source/shotbrowser/disable_integration", + "value": false, + "category": "Pipeline" + }, + "pause_update_on_playing": { + "context": [ + "PLUGIN" + ], + "datatype": "bool", + "default_value": true, + "description": "Pauses updates whilst playing.", + "path": "/plugin/data_source/shotbrowser/pause_update_on_playing", + "value": true, + "category": "Pipeline" + }, + "project_presets": { + "context": [ + "PLUGIN" + ], + "datatype": "json", + "default_value": [], + "description": "Project presets.", + "path": "/plugin/data_source/shotbrowser/project_presets", + "value": null + }, + "site_presets": { + "context": [ + "PLUGIN" + ], + "datatype": "json", + "default_value": [ + + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "2f9ade18-871e-427b-9fcb-514250fd95f6", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "bf03f7f1-c075-41fa-b3fb-72b4687c5159", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": false, + "id": "bed22455-15b2-4e1a-9f3d-05fbf8ad783d", + "livelink": null, + "negated": false, + "term": "On Disk", + "type": "term", + "value": "${DNSITEDATA_SHORT_NAME}" + }, + { + "enabled": true, + "id": "015b2c3d-cd0d-49d5-a880-631446983fa5", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "44a7931b-ef33-438d-8fbc-5012452c19c2", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "2134006a-04ea-4d8d-ae69-5c2ee1bb58a8", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "3d0e8097-fa17-4519-8865-02bca12defae", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "38c3ddf7-73d0-4c90-a383-af5608486609", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "ae68c288-cb40-4249-9fb5-13fdcc3e147c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "8b9f20b2-5ae4-4905-9ca0-d36145416f2f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "f8f04bad-7919-4eb9-af93-55782f55c163", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "ce53779f-a5ec-42d2-b8eb-ec0522d26cc1", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "9e0af42a-ef45-4d41-bc08-a417fa2276fc", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": true, + "id": "f695d59d-d64d-4d12-aefd-4874df272915", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": true, + "id": "cadf6737-885d-433b-8c09-ac777b593097", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "cut" + } + ], + "favourite": false, + "hidden": true, + "id": "214e3cdc-d8f8-4d0b-b786-b4c51ddaa2db", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "20640618-33df-4132-951f-59db1010d7bd", + "livelink": true, + "negated": null, + "term": "Newer Version", + "type": "term", + "value": "" + }, + { + "enabled": false, + "id": "126091de-ac6d-49e8-8861-e559e7dde14d", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "bcabf64a-6557-40d5-a12e-5f96b9e99f31", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version ASC" + }, + { + "enabled": true, + "id": "939400c2-1d59-46c0-a69f-b3ecf500a926", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "57ae2dec-6fe5-4fd4-8937-5279c1446af3", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "" + } + ], + "favourite": true, + "hidden": false, + "id": "54c11cb1-8ea3-4b9b-8c12-8f4f21b93087", + "name": "Next Version", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "d8a70e62-5c3c-4a69-89ce-ad4e14de1169", + "livelink": true, + "negated": null, + "term": "Older Version", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "dc60edbc-8ab8-40f3-ace7-8b0292abdc9b", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + }, + { + "enabled": false, + "id": "1bdf60f0-3b1d-4f85-9f62-1d02d7db3270", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "145090c8-d0d2-401d-a2e2-c93d9fc439a1", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + } + ], + "favourite": true, + "hidden": false, + "id": "ffcc7cd7-fa02-4c94-92da-dae337e4f75a", + "name": "Previous Version", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "dc499841-37ac-49ed-8e76-9e28c95fb3cf", + "livelink": true, + "negated": null, + "term": "Newer Version", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "ad00bcff-24e4-4de0-b07a-c08182ab0a0c", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "bbe30915-f98f-444a-aeac-e7f1e9b8cfef", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "71dcc50b-d4f2-405f-b292-2928f2553ad9", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + } + ], + "favourite": true, + "hidden": false, + "id": "6518312c-99d7-4cf4-840c-4834eb36a1be", + "name": "Highest Version", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "0ca66da0-835d-45a7-a413-033de5930a9f", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "32a9144d-28b0-4c14-b32f-6827a1dc46ce", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "98be696b-cae9-4c7e-b3ab-30ed2a8649be", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "898175b1-3b70-4cdd-b9bd-306e34a57b0d", + "livelink": null, + "negated": false, + "term": "On Disk", + "type": "term", + "value": "${DNSITEDATA_SHORT_NAME}" + } + ], + "favourite": true, + "hidden": false, + "id": "7888e447-31a9-4159-89e9-ec667f702b41", + "name": "Latest Available Version", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "4ad2b3e5-9690-4d27-addf-40d5ac6638c4", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": false, + "id": "de63f29a-5f2f-457a-89e9-5c8eb9f9f7f7", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "eed96e61-2034-4b80-8aac-45bdae51354b", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "witness" + }, + { + "enabled": true, + "id": "1ce58a2e-3304-408c-8942-0aad0a82f75e", + "livelink": null, + "negated": false, + "term": "On Disk", + "type": "term", + "value": "${DNSITEDATA_SHORT_NAME}" + } + ], + "favourite": true, + "hidden": false, + "id": "6e497ef0-905a-4543-8312-1327739a69f3", + "name": "Latest Anything", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "50f398ba-8fe0-43e9-84d2-b463ee308147", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Compare", + "Replace", + "Conform", + "System Group" + ], + "hidden": false, + "id": "10bc65e9-8286-4ef1-a00a-02539f62f741", + "name": "Version Up / Down", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "fa23e3d9-06c6-4535-be50-f3d02836c283", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "42a9c7fe-9eaa-424a-bb86-4c1cf702498e", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "3f2de72d-657c-4420-bfc9-f431967a0220", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "04abf0a4-2d04-4130-9355-aa41c45578e3", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "3bdd86ac-6149-4199-be3f-bfa9093e40ab", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "2fbfdb2d-d68a-492f-8607-f524c7a127a0", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "6db67754-4858-41bc-a199-fc24b9be554d", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "99bd74a4-a0f0-4b2e-95b3-3d0a0937a545", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "28bc412f-f709-408f-9dd8-619b1c501747", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + } + ], + "favourite": false, + "hidden": true, + "id": "7429d231-0ca5-4522-91ff-662282b3c309", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "728bf9c0-96d4-4a7d-ab43-f56a9469679b", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": true, + "id": "bdd14ba7-10f1-42bd-ab39-275616646a25", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "anim$" + }, + { + "enabled": true, + "id": "13644a3d-e4c0-4cac-b8da-9d7f3de9cd94", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "1c48ffa7-ff59-413b-b25b-ef381e89643a", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "df508627-7474-43db-82bb-5e42b3a5c8d6", + "livelink": null, + "negated": true, + "term": "Production Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": true, + "id": "d97f6fae-f41a-4eb2-87aa-745a36f6a16f", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "pip" + }, + { + "enabled": true, + "id": "e703b0b7-bdec-4214-a50f-8a713dac1a34", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "caf41cc6-f881-4a03-87d6-e845418b4e83", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "4644d3f6-d807-44a7-a103-2dacc839be50", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "qc" + } + ], + "favourite": true, + "hidden": false, + "id": "9cd3f6a8-758c-4ee9-8c09-0dfd751ac16a", + "name": "Anim : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2c2ae43f-2480-4cc4-be03-100fb8d4ce76", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + }, + { + "enabled": true, + "id": "1ef73762-c1e5-4e22-ac5c-960749f5d2fc", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "ebea176c-02d5-4fad-aa9d-5ede2d37b49b", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Client Submit DESC" + }, + { + "enabled": true, + "id": "67797cec-d0b1-402d-b93c-fc9836fd6fb5", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + } + ], + "favourite": false, + "hidden": false, + "id": "281796f4-fbf6-4d52-9ee9-76f216f4547d", + "name": "Anim : Last Client Send", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "b60f9326-b627-43ec-a070-0ea1064c1c12", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": true, + "id": "9e9b04b6-4db5-40f3-9ff4-88a83fa087dc", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "8fb66d03-b809-4072-b739-011baf04ba4b", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Result Limit" + }, + { + "enabled": true, + "id": "b2e7e993-01e0-4d4e-bb29-10d23cffee94", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "qc" + }, + { + "enabled": true, + "id": "d757cb52-8772-4233-9d14-3cae731b2ed6", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "98cd4bd5-8332-4ded-8ff2-e4fea0953fb3", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "73667e51-64f2-4b9a-b8e0-808b0dd60389", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "anim$" + }, + { + "enabled": false, + "id": "0cf1ac11-bab0-4427-bed5-a3127828699d", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "070c80fc-6b1b-47a1-9128-df847bf6d30a", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "pip" + }, + { + "enabled": true, + "id": "6cb1f587-1531-42aa-b2d3-9df8cc7134ed", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Awaiting Anim Dir Review" + } + ], + "favourite": false, + "hidden": false, + "id": "95eef446-925d-4114-88d6-0ceafeb5533e", + "name": "Anim : Anim Dir Review", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "4c3ba5e1-f7e4-4e79-a6f4-d5d55829d787", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": true, + "id": "d8791eb1-fda8-49a4-8fc8-b9010b0bb8af", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": false, + "id": "8e48b258-cab4-4d96-8dbe-507b36dc8f5a", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "2ef0be24-4f5d-460d-8e48-c5058e7500a2", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "c2fa637f-19c1-4447-b48a-2e4ba38b6161", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "pip" + }, + { + "enabled": true, + "id": "6c8d5229-601c-46fa-a62f-c48c4898ea08", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_client" + }, + { + "enabled": true, + "id": "1b15fa87-c0f7-4305-922a-720f9933dbed", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + }, + { + "enabled": true, + "id": "54e76455-9719-4dce-b63e-b7f5b6a53990", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "For Director Review" + } + ], + "favourite": false, + "hidden": false, + "id": "c1992390-996d-41bc-ad75-114d298a32de", + "name": "Anim : For Director Review", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "fb9da8eb-d8c3-4627-be3c-1d0abbc9993e", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": true, + "id": "d0fb0d52-53a9-445e-a7c8-bd17ca26073a", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": false, + "id": "154ca0af-9409-49ac-8a45-e2d1318a2950", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "ea9d8994-3f83-4ee9-8b40-0c623a29f190", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "49bfd384-810d-43fd-a45d-d0dcc92a7353", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "pip" + }, + { + "enabled": true, + "id": "3bc7d5f2-3fe5-4aaf-a437-4618c471b635", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_client" + }, + { + "enabled": true, + "id": "4a416e48-fa62-422e-b1cc-4368c8f98fc9", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + }, + { + "enabled": true, + "id": "cf04cd53-c1d2-49b8-9d77-12093f93f3b3", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "For Director Review" + } + ], + "favourite": false, + "hidden": false, + "id": "a84256da-8aff-443f-b8a8-bd5af099019c", + "name": "Anim : For Director Review PIP", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "81637396-f5fc-4675-a2a8-ee6420e2d7d2", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": false, + "id": "dd8dd643-1886-4a5d-bf62-980ab6684400", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "b3ae15c9-dc0c-4990-aca2-ad243fcdd9bf", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": true, + "id": "77618bee-236b-4185-af54-ebde8e0f239f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "9fed0391-48a8-4c9e-b9e8-c084d962c6fc", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": false, + "id": "3658ac9d-e31c-42a6-8156-9c63aa74015b", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "99b4e720-f564-4c39-a33e-b5ff7a1c9d23", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "qc" + }, + { + "enabled": true, + "id": "f5cb05e7-70a4-4abb-a7cb-d43a35e0c337", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "10031461-131a-470f-a798-9a0c4ff5186c", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "555a5caa-20a8-4110-891c-febfb504443a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "anim_pip" + } + ], + "favourite": false, + "hidden": false, + "id": "245b91ea-f652-4284-b4bc-7110d5befaa5", + "name": "Anim : PIP Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "798941db-bc6e-484a-aed2-973d92665881", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "5018a3a5-4490-449e-9ff9-dd40b2ed6f65", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "gesture" + }, + { + "enabled": true, + "id": "33ac9407-8808-4ef7-b75a-9b6b2fec0731", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witcam" + }, + { + "enabled": false, + "id": "285e4dc3-fc5b-4da6-8128-0e927df14427", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "1eb43664-78b0-4acb-a381-436d546b4f32", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "8b362740-d01b-4244-bab1-6c76598304fc", + "livelink": false, + "negated": true, + "term": "Twig Type", + "type": "term", + "value": "audio" + } + ], + "favourite": false, + "hidden": false, + "id": "136bdd24-8698-44c9-af56-b1181126c98c", + "name": "Anim : Ref Latest ", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "ff61e2ad-18a8-4dbd-b24d-1f454b485373", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": true, + "id": "5cc86929-d35d-4ebb-8de5-fcfd1a7822f8", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": false, + "id": "b9e2afc1-73be-49f2-a303-51baaa574b6a", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "ff11a7f3-26f6-48da-baae-ce73b29982a9", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "qc" + }, + { + "enabled": false, + "id": "6bcf39c0-089b-4636-8767-6b5b38cffd54", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "21f03695-5d70-42ca-80dd-40613e02344c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": false, + "id": "5dc640d2-6168-449d-acd3-bab808e31fbd", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + } + ], + "favourite": false, + "hidden": false, + "id": "b0bd7f1b-b34b-49e9-b023-516ae6dad28a", + "name": "Anim : ShoCo/Slap Latest", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "27fba2ab-cda2-416f-8aa2-53ab3df35f61", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Compare", + "Conform", + "Replace", + "System Group", + "Ignore Toolbar" + ], + "hidden": false, + "id": "c96f3f73-9854-4211-8271-d5dc3f136eb5", + "name": "Anim", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "9280de1f-e2e6-4a23-8203-3ff2ae1c6f4a", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "a6f1e249-3781-4395-85aa-93031d6d9dcb", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": false, + "id": "c4f4740b-8772-4b2e-977b-acd703c0fdee", + "livelink": null, + "negated": false, + "term": "On Disk", + "type": "term", + "value": "${DNSITEDATA_SHORT_NAME}" + }, + { + "enabled": true, + "id": "14f4fbde-5ab6-4fce-8e0d-3985987de37a", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "9cba3218-90e7-4163-9188-7369cfe5388d", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "7e71927f-1d85-4e1d-9ab0-dd6df7cf1692", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "9a7050f6-06be-4fb5-89cb-59fe4bf39615", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "ba9a9e01-b7e6-4eeb-8e66-5d1b7f4a4acb", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "3d2eeb0b-e347-4576-8414-7798cd6b4854", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "4a4adeb2-6991-466e-80ef-c6f1c347fac9", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "51b1c5e7-31e2-4042-8f17-b365648f62e1", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "4b2a6bd9-565e-468a-a041-dccdae89770f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "8daa8dae-b0aa-4fda-9a26-8ad51dba6be1", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": true, + "id": "76c06c9b-889d-40a1-8a54-aa23480040f5", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": true, + "id": "46194aa1-4fd8-4353-ba8a-4c3e8ed7871d", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "cut" + } + ], + "favourite": false, + "hidden": true, + "id": "260b1b73-7f16-4db9-9ac5-fc45843de986", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "8836c7f3-e4aa-4ae2-af0b-48da236b620b", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + } + ], + "favourite": true, + "hidden": false, + "id": "0c0c9eef-9511-489a-9cc7-7810a88dc4ba", + "name": "Client Send : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "ef7b8cdb-e284-4333-9b44-5ba4704552fd", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "f66ce9ba-610b-49a1-9c42-ec01bb939948", + "livelink": true, + "negated": null, + "term": "Older Version", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "e4d93303-34bd-4e49-9071-d4d351de508a", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + } + ], + "favourite": false, + "hidden": false, + "id": "5c8081f6-a979-4f8f-b59d-6830fe9ab992", + "name": "Client Send : Previous", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "f2b13653-776d-4344-a18a-b1de5b377f99", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "54423a2d-862e-4f3a-8e99-066e77743752", + "livelink": true, + "negated": null, + "term": "Newer Version", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "e10d8637-5f6a-4475-8c8f-7a768f971a27", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + } + ], + "favourite": false, + "hidden": false, + "id": "b16f5d58-e4ce-4c86-9623-868f8b9419ff", + "name": "Client Send : Next", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "1fb4473a-1403-4eec-bbcc-cab5263bc8a7", + "livelink": true, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "61207255-e831-42f0-9b7e-7f53f85bed14", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + } + ], + "favourite": false, + "hidden": false, + "id": "a7823689-e349-4a53-81b5-6e199f765846", + "name": "Client Send : Latest Same Step", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "d36f5e86-1657-4ab5-957e-05eda08184f1", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "659f6c2e-25fa-4eb0-a81d-bbfb62705b74", + "livelink": true, + "negated": null, + "term": "Older Version", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "7b6cda9e-156e-4dea-a56e-efab537af42f", + "livelink": true, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "5543731f-6212-4dc3-90ac-7e9f1c43b3e1", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + } + ], + "favourite": false, + "hidden": false, + "id": "0a760eb5-3389-46b3-a294-0ddf70d3564c", + "name": "Client Send : Previous Same Step", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "eba7f52a-9ddd-4326-b6da-fad1f4c5bade", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "2234efcd-af32-4822-aa58-525e726b33ba", + "livelink": true, + "negated": null, + "term": "Newer Version", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "b7662e5a-97f3-431b-8434-dbebd9d6e0c5", + "livelink": true, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "eefd937a-0eb6-4811-bb5a-bad2865d5cfd", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + } + ], + "favourite": false, + "hidden": false, + "id": "3e378475-94d4-4165-a6be-b55ac9ba9141", + "name": "Client Send : Next Same Step", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "d32763bf-5ae5-4d9b-8874-ca2b4b3ed42a", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Compare", + "Replace", + "Conform" + ], + "hidden": false, + "id": "f2a8d25a-ee1f-4c6b-a1c5-22582d38de89", + "name": "Client Sends", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "80232887-01cd-4bac-b59a-09b29a7af80f", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "3ee87aab-c230-4883-9377-20cc749fdfb1", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "00477245-6713-4588-aa4e-9f615bc8457f", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "03f7696c-549c-419d-b17e-69ea258011a8", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "7e033c69-a136-47f1-902f-ec34ae73cd30", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "a6afd889-5c6c-4c38-8969-93c1bdd2eb5d", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "cee5c9c9-08d3-46e8-af5b-ade9beb8db98", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "6d210912-0e5d-4e5c-b08b-c3f251471639", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "7305256c-906e-44e8-8bb7-a9ce7325715f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "560106fa-a145-4d7c-b41e-6834e65d8477", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "2fb53f7c-8309-418d-88ff-edfc308c1fa4", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + } + ], + "favourite": false, + "hidden": true, + "id": "b311c0a3-0438-41d2-9741-9e173f9df87c", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": false, + "id": "ef27bf9c-fa0d-4422-b843-841e720b37a1", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "3fe0d754-2b22-4820-a5f1-d2debe574de4", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Comp" + }, + { + "enabled": true, + "id": "ccd07a9f-7c47-428c-a962-d0ad56df043b", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "comp$" + }, + { + "enabled": true, + "id": "71d93484-dbbd-4a92-85ab-4218f59e03aa", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "^O_" + }, + { + "enabled": false, + "id": "c2ef2376-c85f-477a-be28-de0e30693336", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "9b499b03-b658-489e-b936-1c904aaaabf3", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactsheet" + }, + { + "enabled": false, + "id": "78cbe259-3483-49e9-954c-78f6ceef81bb", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + }, + { + "enabled": false, + "id": "40e3cad8-02f9-40d4-9e4d-6f7b057071ef", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "review_proxy_1" + }, + { + "enabled": true, + "id": "1dc87aa9-37b5-4c6c-8587-e1c262c4cce8", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "matte" + }, + { + "enabled": false, + "id": "360ca5fc-41c3-490c-9996-32a9d3fa90dd", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + } + ], + "favourite": true, + "hidden": false, + "id": "8002c53b-64c5-4109-a5dd-c2bdc6dc37ee", + "name": "Comp : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "7cb71959-25cb-4d5f-96fc-bd033194ec0d", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "150beadb-d3fc-4397-bc92-aaeed38fa371", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Comp" + }, + { + "enabled": false, + "id": "b75f0a30-7f87-4b06-a7c1-f619f267dcaf", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "comp$" + }, + { + "enabled": false, + "id": "b215c0c5-16dd-4eff-ae8a-61a0845c9255", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "^O_" + }, + { + "enabled": false, + "id": "8d1724a1-93f0-4369-9019-274f08a269d4", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "b75ee40e-441b-469f-a05d-42d6ee59836f", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactsheet" + }, + { + "enabled": false, + "id": "7f04edc3-d97d-4aaa-a394-31b34efe77cd", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + }, + { + "enabled": false, + "id": "9730792b-3120-430b-908e-a29d9d2a4a03", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "review_proxy_1" + }, + { + "enabled": false, + "id": "4b864af6-5d52-4514-9671-eab47d257cc2", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "matte" + }, + { + "enabled": false, + "id": "1257d20a-93f6-40a0-94cb-aa3d5f51f375", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "0a9c2b17-c16c-4901-ae62-57d956131f35", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + } + ], + "favourite": false, + "hidden": false, + "id": "d483af79-f389-488d-ab23-f47713e761fe", + "name": "Comp : Elements", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "fbadfee5-47fc-4906-b1f5-b18bbf60382c", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "3cc50368-4c18-4fef-a543-34968afdb7f9", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Comp" + }, + { + "enabled": false, + "id": "619a1ff0-1f06-4150-9867-499b32314916", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "comp$" + }, + { + "enabled": false, + "id": "aa6578b3-1a81-4ab0-90db-916bd70be9d9", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "^O_" + }, + { + "enabled": false, + "id": "c26abbce-875e-4fc7-801d-b6fbecd7b75d", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "51cb569a-d9dd-4d27-9d91-8179cdd97278", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactsheet" + }, + { + "enabled": false, + "id": "a1201792-d47f-4ff8-bd7d-c832ffb19ac4", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + }, + { + "enabled": false, + "id": "1a9fadfb-daef-4415-848b-c391f72798c5", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "review_proxy_1" + }, + { + "enabled": false, + "id": "5fd06824-ff83-40c7-84aa-1e9f5f316746", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "matte" + }, + { + "enabled": false, + "id": "cfb481d1-78b0-47c3-b02a-a7c4e2a94585", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": false, + "id": "e03b2446-bfc6-40d1-ad1f-5b94e5e9d5e5", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "988b7c6e-7bb7-425d-9ad9-e0aedb0a67e1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "comp_retime" + } + ], + "favourite": true, + "hidden": false, + "id": "9cecff1e-109f-4e55-bc1b-f9e9dde1359e", + "name": "Comp : Retimes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "09e7e1fa-2c35-42be-8423-25364172bd10", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "27fff081-9ed4-48eb-966e-2b2db9ca1187", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Comp" + }, + { + "enabled": false, + "id": "d4650e7a-0cd5-413d-b6a6-4989809a3201", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "comp$" + }, + { + "enabled": false, + "id": "7da47a69-4293-4d0b-8dc5-6164a1e8589a", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "^O_" + }, + { + "enabled": false, + "id": "7f2a249f-e74e-488a-a3a6-8e52adc8fdfb", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "0bc3079b-698a-4984-8b71-cab26db5f5f7", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "contactsheet" + }, + { + "enabled": false, + "id": "4b022274-df96-48d4-b273-3cb51adbad38", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + }, + { + "enabled": false, + "id": "e281b2be-10ad-461f-9352-a219a3671f94", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "review_proxy_1" + }, + { + "enabled": false, + "id": "fbf4132a-4c58-4dc2-9d38-b0df91b353a2", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "matte" + }, + { + "enabled": false, + "id": "d4d3282f-eba4-418d-a771-3ce9e1842a81", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": false, + "id": "a5336261-dbf6-4ce6-948a-3d56ed57e3b1", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + } + ], + "favourite": false, + "hidden": false, + "id": "a854aeaf-acdc-4e22-8d0b-c4c4b697756c", + "name": "Comp : Contact Sheets", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "e2293cfa-e489-487b-bdd9-a9219aa70c26", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Compare", + "Replace", + "Conform" + ], + "hidden": false, + "id": "1835262d-b063-45fc-9ba7-c521675db4f2", + "name": "Comp", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "945a4249-38ad-46dc-bde3-e66e77c7ff8f", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "b6535690-6a8d-43ca-926b-53b45b5924e1", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": false, + "id": "ae4dde25-f88a-42e7-9ce7-0d02f6fd4cfa", + "livelink": null, + "negated": false, + "term": "On Disk", + "type": "term", + "value": "lon" + }, + { + "enabled": true, + "id": "e16422cd-31eb-4330-8d5e-0ea9284b4b84", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "7036782b-7729-4f6c-a085-839c6175aac5", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "65b3712f-c1e9-4afc-8f7f-7977b0024d98", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "5f900b95-5606-4545-bba9-43db93c87bfb", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "05d705cf-9f0d-46b7-a27f-fee66ab356ce", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "a2f974fa-e993-456b-9b3e-8e2265b3060a", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "61e9c7cb-8c9d-4547-9523-680aa7e2ee3c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "39a106c6-a310-42a7-8d0f-ff8b6ca6b2bf", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "db5433f0-b8e4-4c0e-a73b-9466ca560ff4", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "b7274fc2-d3f7-404d-87de-7f8026154a5c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": true, + "id": "ef201321-0234-492a-aa1c-71bfcfef45dc", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": true, + "id": "202c8aaf-2345-4771-9049-2b09ef5c7c08", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "cut" + } + ], + "favourite": false, + "hidden": true, + "id": "5529bbf5-6664-496d-82e0-e47041283a87", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "5ed0a12a-e5a5-4fec-82ee-8c8064fa6262", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Dailies" + } + ], + "favourite": true, + "hidden": false, + "id": "5ce55b60-cbfe-4149-919d-05139fafaaff", + "name": "Dailies : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "f265c87d-1e6e-40f2-981c-28d435dcff04", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "d4fa3c6a-c20b-4d7b-a261-a1cc612f66c0", + "livelink": true, + "negated": null, + "term": "Older Version", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "2b204b32-4014-4d60-a7f5-31cce26477ee", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Dailies" + } + ], + "favourite": true, + "hidden": false, + "id": "d356ff96-b234-4f76-82ea-c1ff31096110", + "name": "Dailies : Previous", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "080e0533-776a-4a59-9698-34b0250cee42", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "3f7e28f4-d76f-4755-b2f9-27d40ca5d500", + "livelink": true, + "negated": null, + "term": "Newer Version", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "19ea4d5d-0456-4738-87ee-24657b6f4eb4", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Dailies" + } + ], + "favourite": false, + "hidden": false, + "id": "95bbe083-4505-4b61-9e26-7721796962b2", + "name": "Dailies: Next", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "ab2ec795-dbf9-4fe7-b271-38f7e81a278c", + "livelink": true, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "afb13c4f-e0d2-4e63-b93e-d7e039568677", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Dailies" + } + ], + "favourite": false, + "hidden": false, + "id": "8eb7618e-ca7b-4bdd-9ef9-e4ba1695b16c", + "name": "Dailies : Latest Same Step", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "e1e4b3f9-0cb3-44d8-b893-1da3b169f144", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "ca915e11-e0e7-4d88-b3b1-89784dcd2ace", + "livelink": true, + "negated": null, + "term": "Older Version", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "e168e01e-7d9a-44c6-ad53-ac73ebf43043", + "livelink": true, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "32ee9d3b-36ed-43be-a415-9a106885c5ec", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Dailies" + } + ], + "favourite": false, + "hidden": false, + "id": "d068e67e-1c2d-4172-90a9-02cbaa802095", + "name": "Dailies : Previous Same Step", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "0d137251-eaaf-45b5-b0a6-36f1e62dbfb2", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "fd2729ce-bff7-4c1b-bbb0-37736f68e7c2", + "livelink": true, + "negated": null, + "term": "Newer Version", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "ca6247a2-c5f5-4dd1-8b63-6e5611e0b450", + "livelink": true, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "a5ae9b53-a1e6-47b4-873a-aa7ad09a5323", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Dailies" + } + ], + "favourite": false, + "hidden": false, + "id": "ce6c6ee9-2d95-4dbc-91f9-b4fda394dd4c", + "name": "Dailies : Next Same Step", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "df44845e-7ffc-4b0a-9bba-010f94985746", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Compare", + "Replace", + "Conform" + ], + "hidden": false, + "id": "823f836f-22fa-4824-9f15-e1c361bc442d", + "name": "Dailies", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "3364c170-7183-41dd-a72d-a76b17ccdb56", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "a3acac8d-dc6b-4770-8492-676abf574c41", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "2b2a97ff-fa01-4f5b-ad11-35587ff71a76", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "90eacad3-ec18-4333-8bc9-59dc617e9bb6", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "0c1e461f-b784-4168-a3a4-3566fd256278", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "758e7e7c-1135-4568-a544-f128cdd3fa6f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": false, + "id": "61c12409-0f93-458c-a648-10f348aae067", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "1ad6bef4-76a8-4fe3-8e94-338dd1d024a0", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "e083d861-f1b6-495d-ac97-f938d345e3f1", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "2c7c1e2a-0ed6-4a0b-8d82-2be7071cfd65", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + } + ], + "favourite": false, + "hidden": true, + "id": "9f962e02-b599-460f-8de3-6b5550ddefac", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": false, + "id": "959daf0b-6432-49a9-b6ec-cb4ba22b7b00", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": false, + "id": "919b7b16-7a8f-4d83-88a8-d9c83484e470", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + }, + { + "enabled": false, + "id": "83fb7392-28d8-4dea-8ca2-43e0260efb0d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "edit_ref" + }, + { + "enabled": true, + "id": "502b82a7-ae01-4e4b-b2fe-6d2f33435038", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "1faa06c1-f6bd-4b89-826f-ee6169b7cc66", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "edit_ref$" + }, + { + "enabled": true, + "id": "4dbbb446-8036-4d09-8532-3d9d2c8b6916", + "livelink": false, + "negated": true, + "term": "Twig Name", + "type": "term", + "value": "hero" + } + ], + "favourite": true, + "hidden": false, + "id": "1e6f1ceb-b517-4172-ad57-d8efa34fc084", + "name": "Edit Ref : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "da8be51b-622e-4ddd-911d-b52383d3808e", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": false, + "id": "f48db595-b7a1-4c24-a5fe-e0f11b514a53", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + }, + { + "enabled": false, + "id": "a0145e61-e593-4568-a3ef-e8a3983b9e1f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "edit_ref" + }, + { + "enabled": true, + "id": "efac4c26-dcc4-4757-9e47-f9cc31e231c3", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "4ae06a81-4890-4394-a7e7-f15ece4b45ec", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "edit_ref_storyboard$" + }, + { + "enabled": true, + "id": "36ded890-96e3-4af2-bc99-863ca12d0cf8", + "livelink": false, + "negated": true, + "term": "Twig Name", + "type": "term", + "value": "hero" + } + ], + "favourite": false, + "hidden": false, + "id": "08f58f68-face-4b4b-a63f-b78be7d5a052", + "name": "Edit Ref : Storyboards Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2317d25a-25be-4138-8012-357bd5889b49", + "livelink": true, + "negated": false, + "term": "Id", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "63cc8167-9b32-424d-99e8-74371a5eb445", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_client_editorial" + } + ], + "favourite": true, + "hidden": false, + "id": "48b8029a-70c1-4daa-acbe-f0b6784fdda2", + "name": "Client Movie (Leaf)", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "762cf632-f965-4623-9672-9527b2ecdbe2", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": false, + "flags": [ + "Compare", + "Conform", + "Replace", + "Ignore Toolbar" + ], + "hidden": false, + "id": "9ef2526e-da44-449b-b95d-abfd3c2c1028", + "name": "Editorial (FEAT)", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "f1dadce0-a2c9-467e-b1ff-17ac6683c83d", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "4eb0e33f-f592-4964-8b34-1b0987253083", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": false, + "id": "cc27e681-238a-4aa9-9d4e-ce21bfd1c69d", + "livelink": null, + "negated": false, + "term": "On Disk", + "type": "term", + "value": "${DNSITEDATA_SHORT_NAME}" + }, + { + "enabled": true, + "id": "ad3eadc2-7187-46e8-926d-9b4f08157a16", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "1cc39cee-c7b3-4087-81e8-171f8da25781", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "9cb3f165-82e4-4e2d-9a61-058ef3548bdf", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "9deb387e-d69f-415b-b99e-2cc459d3a6e8", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "dea60e10-1447-4208-9b10-d639951ee50c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "01e6536a-11da-4f9b-a7b0-7b501ab48e99", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "1667498b-1307-4682-be1c-4238d45257c6", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "786ee70d-139c-42d5-919c-615d398a9feb", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "228bf989-5db2-4171-9d6d-0aedcb8d31f3", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "6c4dd93f-de0c-4ceb-bc2f-2f83e9588a05", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": true, + "id": "71cd0518-10bf-4de8-9c10-992b7b6e0805", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": true, + "id": "fe113fb9-6499-484b-af9f-a8a822e848dc", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "cut" + } + ], + "favourite": false, + "hidden": true, + "id": "6fbbeab9-aa5d-4382-b290-9ee21c4cd94b", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": false, + "id": "1966f83d-f872-4dea-aa36-68bb362fc642", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": false, + "id": "6a4ee67a-0175-41d1-b3b6-0e183c72b472", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + }, + { + "enabled": false, + "id": "aa3e3fcb-a539-4031-9456-ee5d39687fab", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "edit_ref" + }, + { + "enabled": true, + "id": "e0cf62e8-9e94-437e-88db-f3d658517dc2", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "b16b78b9-717d-4e1b-bc36-3dbaaa5b82d0", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "edit_ref$" + }, + { + "enabled": true, + "id": "add2433d-daa4-4ad7-be95-9d827322fa15", + "livelink": false, + "negated": true, + "term": "Twig Name", + "type": "term", + "value": "hero" + } + ], + "favourite": true, + "hidden": false, + "id": "4c9a25a9-4cb4-497f-be15-44562a539ce4", + "name": "Edit Ref : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "9e7522f0-7563-44a7-963d-6a7c70489e73", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "e38ecad6-ab31-48f0-8d84-50201e989849", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "6d4b8b4f-103b-4241-9cfc-564f88141ebf", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "heroplate" + } + ], + "favourite": false, + "hidden": false, + "id": "4acc6ddf-6da7-488e-9a0a-2e74a610426c", + "name": "Edit Ref : Hero Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "0d3897c5-c75e-4dfa-b131-5116faeaf2bc", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": false, + "id": "fbb68404-377f-49c5-bc66-26b4bab255e2", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Result Limit" + }, + { + "enabled": true, + "id": "c34428a1-14b6-4de9-a6c1-0691c746e947", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "_postvis_" + }, + { + "enabled": true, + "id": "f59c42cd-40da-492d-bf87-3dfc47c1fa1b", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + } + ], + "favourite": false, + "hidden": false, + "id": "3a058459-6f92-42a0-837f-5b45f62c9af1", + "name": "Edit Ref : Postvis", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "a9ca982c-a724-4495-816c-0fa44b432e37", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Compare", + "Replace", + "Conform" + ], + "hidden": false, + "id": "a528bba9-e501-4c2d-8534-729058de5f0e", + "name": "Editorial (VFX)", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "c739919a-233c-4263-879e-e619bbd9d805", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "333b7eaf-be42-4a6e-ba7c-a7cfa4a4a955", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": false, + "id": "8635424c-4903-478d-bbb3-e76f79a0c9a9", + "livelink": null, + "negated": false, + "term": "On Disk", + "type": "term", + "value": "${DNSITEDATA_SHORT_NAME}" + }, + { + "enabled": true, + "id": "2e58931c-ea4a-41ad-835d-890ca64282d8", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "40b122ad-3363-4ed7-ab2a-abc7e7777d96", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "53804730-4aca-4d5b-9f17-2e2ee470a91f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "2c08cb37-4679-4d01-b281-ffafd2ca9f6f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "298779d5-f68f-4127-a2b9-b9e6863f3bd6", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "b044e650-f01f-4d32-b55a-eb572e4991fb", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "b901be9f-aa4b-4cd0-8f9e-a4a44cd99554", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "dc501a9e-535d-4450-99ae-57861a065ad5", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "1e092125-532f-4f5a-8b47-06a046deb292", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "f98c27ee-3315-4a0f-89ca-0e43a14b3bcc", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": true, + "id": "5a579082-d9a6-499f-b389-9a7b72543cd4", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": true, + "id": "ed3b6863-9ce7-4b60-a105-98ca69c7e32b", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "cut" + } + ], + "favourite": false, + "hidden": true, + "id": "e354350d-bb7b-46f4-baaa-c9904d730fff", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "1a8279ea-559c-46a4-8d3b-ebd606ab1d59", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "6596881d-6eee-4ec9-89da-5dd709cabc81", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "DMP" + }, + { + "enabled": true, + "id": "c3f132df-9ad8-49bc-9ced-50b7bc563607", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Digimatte" + } + ], + "favourite": true, + "hidden": false, + "id": "e13dcee7-7035-4c93-a03b-2ac69c8bef97", + "name": "DMP : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "4c6c5799-14db-4661-8520-b02ca9ec5748", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "1e43a295-1e2e-44e9-8d43-ad808d329fb6", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Environ" + }, + { + "enabled": true, + "id": "98e6d122-0968-4f34-959c-fb06425219f4", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Envfinishing" + }, + { + "enabled": true, + "id": "acb576fd-88e9-45af-98d1-4a8fe4a6954b", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Envsetup" + }, + { + "enabled": true, + "id": "2b94e4d5-e727-4232-ba09-38e4977c951b", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Digimatte" + }, + { + "enabled": false, + "id": "3faccf54-8f09-4fc4-b465-e991bb151a42", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": true, + "hidden": false, + "id": "a6f09c2f-e8cb-42b4-af1c-ecd64dd1d2ad", + "name": "Env : Latest", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "e1bd71e7-e4ca-47dc-8bf9-b5d49fa22b11", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Compare", + "Replace", + "Conform" + ], + "hidden": false, + "id": "6e320814-2448-465a-af28-a1b95988ba32", + "name": "Env / DMP", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "0d378664-0f6d-4bd8-b59d-4835b8b935a5", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "b931e162-196d-4ff3-aff8-55f02dcda882", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "467034c0-2f4d-4d4b-8486-653a88caaf91", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "f6830e70-d1d6-4b21-b6d7-d0eb2cd3fe55", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "484f752b-929e-49ec-bb51-68419112dff5", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "bcd780ba-c3ba-428e-8ba6-f7d642c3b4e6", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": false, + "id": "2d5ede1b-efc8-4d5b-bc3b-903929971009", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": false, + "id": "608b1def-4b29-4b6d-bbb1-1227672991bf", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": false, + "id": "9a573459-35e0-43f4-b8ac-1a9d024c12ab", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": false, + "id": "17298237-79d5-44e0-8c20-800d38204ea5", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + } + ], + "favourite": false, + "hidden": true, + "id": "cc1e4fe8-5fa2-4536-a496-a11007d827de", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "d3e12669-92c8-4d39-8b33-2085b331eea3", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "5eb7ad66-8a70-43be-9db6-8bf044a94531", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Shot Sculpt" + }, + { + "enabled": true, + "id": "861f1e5a-00c2-4d0a-9513-9103963a38e5", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Creature FX" + }, + { + "enabled": true, + "id": "7ed64598-0b42-426b-85fb-81f9363542d5", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "88b49cda-1f45-44be-9ebc-dfaca566a11b", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "8" + } + ], + "favourite": true, + "hidden": false, + "id": "769b7e7f-1b64-403a-86c4-1f06c6103a75", + "name": "CFX : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "db7a28dd-84e5-4a5c-badc-bda1f1ebfbbb", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Shot Sculpt" + }, + { + "enabled": true, + "id": "98896d01-596e-48a1-9ce4-bda0fec14dc0", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Creature FX" + }, + { + "enabled": true, + "id": "762e631f-03f7-47ef-bbab-f200ba89e3ba", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "809d5a7f-c31b-414b-bc5f-1e01f7323071", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "ab82acb8-5158-425d-bb12-b5c1ea640bca", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "4cd089b3-88bc-4ec2-9fe6-eef027064e1f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "muscle" + } + ], + "favourite": false, + "hidden": false, + "id": "4ea5ad42-99cb-4fe0-9c57-f64a59c9187d", + "name": "CFX : Muscle Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "6977ddc6-29b3-4ed5-a2ea-1cad0994b6c0", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Shot Sculpt" + }, + { + "enabled": true, + "id": "1b6b7a69-26aa-4b3d-a079-bd47c974150f", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Creature FX" + }, + { + "enabled": true, + "id": "078a7117-ce29-4c06-bc2c-39a979e0bbbd", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "890255f9-b942-4c65-9a0c-74be8e124de4", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "6ec8ca2b-58e9-4135-9cb2-7604996b58d5", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "de09c455-abcb-469e-b2c0-711a67dd84a0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "skin" + } + ], + "favourite": false, + "hidden": false, + "id": "17f4aa92-de26-44a4-b30d-a9a5287b8604", + "name": "CFX : Skin Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "069e6629-49eb-4bc6-b51c-346b7672781d", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Shot Sculpt" + }, + { + "enabled": true, + "id": "57200fa6-6c6c-4e3b-9be6-59a6c79b7351", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Creature FX" + }, + { + "enabled": true, + "id": "798a3da8-9d6f-4f95-a119-d74e55986565", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "ffbb7a85-3267-484e-ab4c-3a310f3f5cf0", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "6a8c10fb-ff77-4103-88c7-1cd26ed71e38", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "acfa9ada-eb93-48df-9d74-72bcf85e9b4a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "skinsculpt" + } + ], + "favourite": false, + "hidden": false, + "id": "5b08cc74-64fa-4976-b141-6a07d305d299", + "name": "CFX : Skin Sculpt Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "677bb888-84e5-4e6d-aad8-664796a36a14", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Shot Sculpt" + }, + { + "enabled": true, + "id": "144e5dcb-ff47-4aee-9721-f109d81b50fe", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Creature FX" + }, + { + "enabled": true, + "id": "34665949-42ad-47e7-bd92-03d448260980", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "ae440dd7-950e-4733-95a7-3286f28977ba", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "ccb0d49f-1645-4f27-9bdd-29af7b037b6f", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "a759401b-8a48-47ec-bb80-726f6c252c8a", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "23b19755-eb61-4565-a2ff-4eb05d1d118f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "hair" + } + ], + "favourite": false, + "hidden": false, + "id": "0e7346d9-af37-4d68-ba56-acac20aaaecb", + "name": "CFX : Hair Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "f6de9eff-a3ca-41fc-b029-4121dc355b50", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Shot Sculpt" + }, + { + "enabled": true, + "id": "a48f81d7-5da5-442d-a394-05922a564576", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Creature FX" + }, + { + "enabled": true, + "id": "e8968c6e-e0e0-46bc-951d-e56cf01e50c0", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "74e597df-1eae-4572-857b-7d563374a03e", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "3d8f5d2b-12a3-483a-888d-b9cd25a7fee1", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "9be3d5db-63ac-4282-8ea9-8f9789562711", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "costume" + }, + { + "enabled": true, + "id": "5e4fc0bb-94e4-43a5-8106-e3afc9e98760", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "cloth" + } + ], + "favourite": false, + "hidden": false, + "id": "68b82fc5-80a8-42ae-8ed8-b35bb3ab129a", + "name": "CFX : Costume/Cloth Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "8529913f-5df5-4c42-a898-853c7936e8e0", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Shot Sculpt" + }, + { + "enabled": true, + "id": "b541708d-7c4c-475c-8d70-3c6cd94a2c71", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Creature FX" + }, + { + "enabled": true, + "id": "be16c984-23c5-4334-9583-d22be489891b", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "ec74c73b-c7fa-4fa3-85a9-57d95fcf040c", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "5405ef11-103d-4623-af9a-bcc8e4af5e29", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "1a0196a8-2965-478d-9400-20b6a5f5ae37", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "feathers" + } + ], + "favourite": false, + "hidden": false, + "id": "60a529d8-6fd1-408e-a86a-1975af0d7e02", + "name": "CFX : Feathers Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "7e5c6e82-8f4d-4223-be4c-6239cd1af4a0", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Shot Sculpt" + }, + { + "enabled": true, + "id": "0f07226a-f883-4298-b690-18e7c2a181cb", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Creature FX" + }, + { + "enabled": true, + "id": "23ee11c4-a2bd-4d9b-8b93-9210fa85d090", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "e63885db-9a10-4062-ad1d-9c535d576f34", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "4f07c493-899b-4a7b-99a7-18ecd43aeb63", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "05e4c695-d3cc-4a9d-a4a4-8ac7df51bf63", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "creaturesculpt" + } + ], + "favourite": false, + "hidden": false, + "id": "ec07d38e-11e1-4e76-8b97-c6796c1f7aaa", + "name": "CFX : Creature Sculpt Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "db206053-fc81-4b3f-a2fe-bf9a039d8adc", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "65da831a-2d38-461f-8703-af3820b840ad", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Crowd" + }, + { + "enabled": false, + "id": "38521645-2098-44bd-9865-275b7fff577b", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": false, + "id": "c3366496-c94d-4eab-a366-35dc5e7522d3", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": false, + "hidden": false, + "id": "d42a69a3-cbaf-4a99-b351-61511a425497", + "name": "Crowd : Latest", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "9ace8cb8-f5d0-4953-bc4d-2b1a90089b85", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Compare", + "Replace", + "Conform" + ], + "hidden": false, + "id": "b39da3fa-6427-435e-8c66-d1d9271aabdc", + "name": "CFX / Crowd", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "6737abfe-4c4d-4c81-973a-31ad30e2e032", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "8073dae6-a308-4138-b834-d33bd5e63360", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "2ab418ba-8c8a-49d5-93a0-b380f97fb7d9", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + }, + { + "enabled": true, + "id": "87138dd7-5cce-4fa5-88b4-39eaad99d563", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "edf94123-748a-4403-8640-99a742661b4b", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "9911fde7-f01f-4d77-a42a-0d4bab8443cd", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "4b00d2ca-8dfc-4032-9aa3-c1cc9c8f3cf8", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "0d67d5a0-6c1b-4690-a308-4a05bb37b0da", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "2a0f10a3-66d4-4602-a879-984d98897db5", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "3f128615-7b6c-40d5-a38d-7006e2783347", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + } + ], + "favourite": false, + "hidden": true, + "id": "5eacd02b-cc01-4aa8-bbe5-d5f44ccf0433", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "29678311-0b71-40b1-9a02-16d9a2a6cd27", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "0419dee7-c426-4b9d-9c13-f3992b1cf725", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "FX" + }, + { + "enabled": false, + "id": "22ad229b-6430-4647-9f1f-b0537bcf9546", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "10" + }, + { + "enabled": true, + "id": "ca3193c5-a422-41c4-8fce-4f00869d6a62", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "eec4d5c7-b45e-42fd-b1c7-ceccec255817", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "CG_" + }, + { + "enabled": true, + "id": "5d26b386-e500-481d-a5f3-a1a70dafafe6", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactsheet" + } + ], + "favourite": true, + "hidden": false, + "id": "e14dc891-cdce-403e-a352-53f139bfa30a", + "name": "FX : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "46a52c8a-a1b2-4e4d-9713-16542d46e179", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "8c7c2c9b-4a48-4494-9949-2d36de496802", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "FX" + }, + { + "enabled": false, + "id": "0170d2af-0d3a-4e38-a683-d66cf70d9df2", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "10" + }, + { + "enabled": true, + "id": "2ccc4a65-ec30-465a-8024-5323825a7c06", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "ddc8716b-efb6-4eef-9000-7202414b41b5", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "CG_" + }, + { + "enabled": true, + "id": "d33a2afb-8ff7-4e8e-9c99-ff57ceab386c", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactsheet" + }, + { + "enabled": true, + "id": "272db616-fdb7-411e-a7a4-68489df7bd0e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "slap" + } + ], + "favourite": false, + "hidden": false, + "id": "a6c57d98-debb-41b2-821d-a893dbaf3312", + "name": "FX : Latest Slaps", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "39cfd04b-290b-463b-9438-976e1c45e5f3", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "c3e987c9-1a24-468a-8a3e-0195405c3892", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "FX" + }, + { + "enabled": false, + "id": "7a09cbfd-d955-458f-a97b-accacc261d77", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "10" + }, + { + "enabled": true, + "id": "e4ca6613-d635-44dc-868c-ce883bc7f27d", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "d3422010-83ce-4497-bfbd-c85cbc28f8d3", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "CG_" + }, + { + "enabled": true, + "id": "a47375ff-fca4-4bcb-8e83-cb9144e3032e", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactsheet" + }, + { + "enabled": true, + "id": "85fd5125-a9e5-425b-ab61-a5b854fbea53", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "comp" + } + ], + "favourite": false, + "hidden": false, + "id": "84d04d9f-aec5-47d7-8db3-3ca6ecdab3d2", + "name": "FX : Latest Comps", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "df5d47aa-3279-4268-8d88-60db08d07e29", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "1c15a3fc-316c-407e-994f-74aea98a0d3a", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "FX" + }, + { + "enabled": false, + "id": "1074806b-1d33-4fd4-899d-7ec875e2cd49", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "10" + }, + { + "enabled": true, + "id": "3a7d8280-4846-4f83-871b-d2887547ba2d", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "07447c26-6cf3-43d1-bc84-564a62b13d4e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "contactsheet" + }, + { + "enabled": true, + "id": "7451f684-f138-4aa2-9ec4-3152ee7868c1", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "CG_" + } + ], + "favourite": false, + "hidden": false, + "id": "de78aa48-c89f-4f60-82cf-3f2c10354dd9", + "name": "FX : Latest Contact Sheets", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "43af24db-eeed-4df3-b801-cd82c15223d9", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "86e3c915-abdf-4e5a-8ae6-e588ec0a0b19", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "FX" + }, + { + "enabled": false, + "id": "18ab7a73-75e4-4b32-bc43-9ad55c025f3e", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "10" + }, + { + "enabled": true, + "id": "264ed6bf-0641-48ea-b87a-66cc2fbfb729", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "02e32378-2045-44f7-9e0d-333351bc7bf2", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + } + ], + "favourite": false, + "hidden": false, + "id": "e847fa13-1cad-49c5-91e5-7d11bc3a9098", + "name": "FX : Latest CG", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "f33a0841-dad1-47dd-85be-8173ecfd625c", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "805e2bb5-238f-4b93-a80b-1bbc07b65500", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "FX" + }, + { + "enabled": false, + "id": "79cfe41a-0df2-4a3f-833d-4ec3d1896284", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "10" + }, + { + "enabled": true, + "id": "e8ef5412-b18c-4653-bfc6-a527ae39f5cc", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "47c966b8-a202-4322-8d2d-6ed93dcc637f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": false, + "id": "4fa845cd-269a-49c5-a6e8-05722c21906d", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": false, + "id": "f1e141e9-ef7b-49db-a4ba-216190753e78", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + } + ], + "favourite": false, + "hidden": false, + "id": "d8a145a4-44f2-4724-b345-5688b919be0d", + "name": "FX : Latest ShoCo", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "7f3d0764-9085-4d6c-83c5-e0f946bd10b8", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "9734bb3f-2409-4d57-a15a-ec056c6e41ce", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "FX" + }, + { + "enabled": false, + "id": "42e94e85-de19-471f-91b7-3ded4882f90c", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "10" + }, + { + "enabled": true, + "id": "e2273d66-1a34-455b-adb8-94d7baadf800", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "c54a7a39-2c65-42fb-812b-b09f118df429", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "4583a445-39d9-46ea-9bc8-622eaa335d7e", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + } + ], + "favourite": false, + "hidden": false, + "id": "cc4b3270-351c-458f-982e-c90a8b4eb2ca", + "name": "FX : Latest Playblasts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2471ac50-ba8b-4fea-a9fa-08802bc6e6d6", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "22f4d69a-a46f-478e-9d70-367d20aa4097", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "FX" + }, + { + "enabled": false, + "id": "e2596c62-4d7c-49d8-baa4-652143078fa6", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "10" + }, + { + "enabled": true, + "id": "e509b7e4-e45c-46be-9264-f62c0418f039", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "09b59620-ec85-4000-bb8a-933d172c9320", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "2b392c34-21b6-4b49-9f0e-d8b57c59e7d4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "master" + }, + { + "enabled": true, + "id": "70dbbf3c-c6a2-4132-b26b-6a9a6f20c442", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "main" + }, + { + "enabled": true, + "id": "863a7222-ffde-46b2-ac49-2bd0d66d6988", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactsheet" + } + ], + "favourite": false, + "hidden": false, + "id": "ef48d9f9-ba58-4c63-a9b3-4ffb6489facd", + "name": "FX : Latest Master", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "647757e5-0d49-422d-84d5-104914f67daf", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Compare", + "Replace", + "Conform" + ], + "hidden": false, + "id": "0d912e9b-2a25-48bc-9cef-b756f40b3dbf", + "name": "FX", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "c90b43ba-2904-4641-b9a8-9a71ae851e8b", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "86aa0918-6b45-4afe-afcd-92a3b02084ca", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "be5fd526-a90c-419e-b544-e30ce990c881", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "3abe157b-f60e-4459-bfbe-fe335e073e2a", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "8ebfeedf-0004-4c50-a3de-74ba5a7dfcf4", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "943755b3-3e6d-4573-bd7a-fc0e19e082df", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": false, + "id": "16d38d92-f9cd-4e56-9433-57f2e7bfbadc", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "52b27c7b-4e97-4aa1-89cf-75ca8e2942cb", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "a71db352-0ea7-4588-9e59-dcfebb67b005", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "b2f3092a-7a5e-4384-b4c5-3ba23c851a74", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + } + ], + "favourite": false, + "hidden": true, + "id": "ab58fded-2aaa-4773-adb4-89aa1179ac1d", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "63e29639-05d2-4fe0-9c1e-a3b38af7cdef", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "f5dca32e-1bcb-4505-94c1-4378eabced6a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "layout_v" + }, + { + "enabled": true, + "id": "ac62eda7-1353-47ec-b80b-17a38f15a93a", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "aae3b067-1aea-4df5-bac7-a074ebc758f0", + "livelink": null, + "negated": true, + "term": "Production Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": true, + "id": "4975578d-bf38-43da-b741-91e8164b8aba", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "WP" + }, + { + "enabled": true, + "id": "aa5733d5-26c7-489a-bd51-263d57106829", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + } + ], + "favourite": true, + "hidden": false, + "id": "2e360241-9ed6-4cba-9eba-c10ac9015e00", + "name": "Layout : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "8711d12e-17bd-4e9d-a592-681e0c8725ac", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "998820df-daad-44fe-8da2-cbc233f5b627", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "layout_v" + }, + { + "enabled": true, + "id": "49b85a9f-84cf-4c93-a3ef-db650489dee3", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "4a700898-7f97-4068-8548-6084f1fce4b7", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "3e7653e6-e1d3-442f-925f-7a7aaa848f41", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + }, + { + "enabled": true, + "id": "1885b9a4-e3bf-4aec-9afc-f71e905f9084", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "WP" + } + ], + "favourite": false, + "hidden": false, + "id": "82724da8-b95e-4104-917f-547b2e17b536", + "name": "Layout : Approved", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "daa6306d-021f-45f8-b8cd-59abc007edaa", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "046dc411-5a9a-4ffa-9e2b-edccc60722c0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "100cefb7-bcac-4496-8ea5-cc00cead12ff", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "4f8796c5-a1f6-4890-a646-86186f445ba3", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "b7471339-147d-4e26-98b8-246e5b89949e", + "livelink": null, + "negated": true, + "term": "Production Status", + "type": "term", + "value": "Declined" + } + ], + "favourite": false, + "hidden": false, + "id": "cf2bca4c-06e7-43fa-aa51-42f0f73fa042", + "name": "Layout : DeptSlap Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "af3a3e6b-9e51-4c06-9728-361281b13863", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "e869ab1d-577a-42cc-9661-ebd6f0208ac4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "a551973b-6c68-4196-9662-7044353bc67a", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "4fdc59b1-4475-49e0-8c68-b14bf03a1d72", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "ffa78413-9c47-449b-b0e4-38e34d6aca7d", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + } + ], + "favourite": false, + "hidden": false, + "id": "d41cc100-9e42-4226-a611-5d5a621fdbb2", + "name": "Layout : DeptSlap Approved", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "103ad802-db80-4f26-980d-a53ade82008b", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "bbc0eac6-f4ed-48e5-868f-e0ece3aad9dc", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "SHOCO" + }, + { + "enabled": true, + "id": "9401fd23-30ae-4224-840e-78f7f108b5f9", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "bfd4a15a-abf7-4064-8fe0-8b2efaa77bad", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "9633f429-66b4-4cc0-b795-cf1c3a9224c7", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + } + ], + "favourite": false, + "hidden": false, + "id": "a197a6e5-9a65-42c2-92f9-5611d5227596", + "name": "Layout : ShoCo Approved", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "e5e0f31a-d5df-4b10-b85a-4c27727a70d4", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "79bf8057-acf6-4fc0-8232-636af8b77bf5", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "SHOCO" + }, + { + "enabled": true, + "id": "802440ef-419a-4c89-9a07-19df9c206fcc", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "5e8448a9-8eb9-4f6f-b0e6-43542d5b1103", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "c3221d33-2d93-4933-864d-66138e6c3113", + "livelink": null, + "negated": true, + "term": "Production Status", + "type": "term", + "value": "Declined" + } + ], + "favourite": false, + "hidden": false, + "id": "b18e541b-1b95-4a7e-9e53-4b44cd005f03", + "name": "Layout : ShoCo Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "178e913c-9562-47af-98c6-939c1f43d297", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "c96e7a77-fef9-4d77-b5a7-e47c2dacc1ee", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "3b165102-21b3-4b90-9a12-51769d9b8381", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "45bb9ada-bfc0-49ce-af7a-faabdd720d50", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "tech" + }, + { + "enabled": true, + "id": "62297ea3-374a-476b-abd0-da8f44c2e8cf", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + }, + { + "enabled": true, + "id": "275c0bff-e05b-413e-a498-9a8d6ea50927", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "WP" + } + ], + "favourite": false, + "hidden": false, + "id": "60402ade-cf8f-47fc-a31b-726b03d88a20", + "name": "Layout : Tech Approved", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "02abbb11-6dd1-4eff-acba-12f390a5b10e", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "92a9b61b-7be9-4b87-8a31-d6feeb1a35a6", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "a477ea2e-eaa7-4152-b8b4-d72b067b1e6c", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "ae30cf46-6b92-43c6-81e8-a5c1516f0c55", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "tech" + }, + { + "enabled": true, + "id": "7987a5df-4a70-494a-861a-16a6f383094b", + "livelink": null, + "negated": true, + "term": "Production Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": true, + "id": "616659bd-5f36-428b-a3c7-a79c774e609d", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "WP" + } + ], + "favourite": false, + "hidden": false, + "id": "2737adbc-6fd5-40b8-a82e-475ff2bd938f", + "name": "Layout : Tech Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "ce7ed917-bf50-4549-9595-4cd7ad14ed36", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "443f6f49-38b2-4bb0-88bd-929a59951bb3", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "0c15bdd1-b0eb-4d24-9391-dcb4119f9e5c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witnessPersp" + }, + { + "enabled": true, + "id": "cc02a702-fdae-475b-8f93-a9df6c64871b", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "113731d5-b707-4217-a1d7-c29776c12a20", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + }, + { + "enabled": true, + "id": "10e27bbb-c131-4df0-bf6d-d9bb2ecdfbde", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "WP" + } + ], + "favourite": false, + "hidden": false, + "id": "fd14e9fe-b03c-4fb3-9a1c-5b081ffcffda", + "name": "Layout : Witness Persp Approved", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2ed26f0e-e912-4f7f-a947-757d923e5c80", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "2a99f75e-ae45-4cda-a0fb-8d1d69083a94", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "694ddf1a-55b1-4b07-884f-caa087d75679", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witnessPersp" + }, + { + "enabled": true, + "id": "e395cdf6-e2fb-4893-bc31-e634abebe366", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "9e963124-3abc-4e8b-bffa-695a134f3da4", + "livelink": null, + "negated": true, + "term": "Production Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": true, + "id": "19bff3e7-444b-476e-81e0-53373380168e", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "WP" + } + ], + "favourite": false, + "hidden": false, + "id": "7cef39a3-4a6d-45be-aec6-d769c391515d", + "name": "Layout : Witness Persp Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "a7c16ee8-e38d-4f49-a272-2661ee691ea7", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "b4cf0f55-0861-42ab-b3a6-d43d1d5e8571", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "92f0a9ac-dafd-409e-a5e9-18f31fa1c4c8", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witnessTop" + }, + { + "enabled": true, + "id": "6c0f4434-a81d-4e8a-bd24-505cf69130e2", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "fd2de951-49b5-4c2e-8243-240ca46ce0cd", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + }, + { + "enabled": true, + "id": "47fff2d1-f86b-4f64-975b-74698ae95bb7", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "WP" + } + ], + "favourite": false, + "hidden": false, + "id": "b0c99ae7-25c6-4b03-8e2d-4760f6010037", + "name": "Layout : Witness Top Approved", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "6b4febcb-716f-41e5-8f3f-2518ec673477", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "ecd9f350-07de-498f-86de-00b8716d46de", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "0fdf03ad-1859-4a88-b269-d65d4fb9e616", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witnessTop" + }, + { + "enabled": true, + "id": "b6daf350-e8e2-4466-8476-df76504df5eb", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "cb5f1058-ef01-4c2f-a665-9bd51c4eb34f", + "livelink": null, + "negated": true, + "term": "Production Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": true, + "id": "467c3a57-fab1-4793-9d50-e1e4842c6644", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "WP" + } + ], + "favourite": false, + "hidden": false, + "id": "ef04540e-20eb-496e-90d9-159b6f1b2733", + "name": "Layout : Witness Top Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "28ae6ec9-7d0e-4898-9c52-99e937cf3242", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "79d678a1-12c8-4903-9261-de7b6a143a49", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "fa6c72f3-0c1b-4165-841d-7a778ba614a2", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "3d5d023b-6b2d-4457-910f-4070c84ef5e3", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "f033bdd5-0602-4a29-94a3-45a17db1ee25", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "4f27338f-e91d-4677-9b40-b0a871bf81fa", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "qc" + }, + { + "enabled": true, + "id": "726503b9-a5e0-4807-a40f-c70adb4bffd5", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "c5e4d9ca-6cf7-449c-b06c-0059fcdf1ca4", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "7b20f882-d8cf-419d-9cbc-2bad0da52eff", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "WP" + } + ], + "favourite": false, + "hidden": false, + "id": "2ef7bb22-3fbe-40b2-9c05-9e477db8c92b", + "name": "Layout : WP Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "7703a415-8516-4cf6-99a6-8540559f79bd", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "3f7cd130-50f0-4e1d-aa38-c1da1c061468", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "f26a79df-983b-4edb-a5a5-564321746629", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "d28be310-b106-4baa-9976-6374cfbe5e28", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + }, + { + "enabled": true, + "id": "527aafb5-aa30-4fec-977a-b987602059bf", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "c8876d09-bb21-4f50-afde-b4306a98a650", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_diagnostic" + } + ], + "favourite": false, + "hidden": false, + "id": "dbda469e-42a4-48a8-b102-027d9a406e6b", + "name": "Layout : Diagnostic Approved", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "26e05da2-109d-46ea-bc7f-ac79c25192ef", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "f64f26e8-2737-4eb1-889d-827fd785bd32", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "6f5aebb2-1a3e-4fbf-8241-f50b57a87647", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "b4510d2a-6b89-4f75-a8d0-7d73a39c13c4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "17920521-3511-41e2-87bf-a4a0b877657d", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_diagnostic" + } + ], + "favourite": false, + "hidden": false, + "id": "2fe8dd1e-6d70-427c-93ec-ec1ebb265edf", + "name": "Layout : Diagnostic Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "b60651c4-050e-4835-b79f-dda48d756732", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + }, + { + "enabled": true, + "id": "fc94e166-64e8-4770-92d7-26de356fc567", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "48c11cef-f562-46d7-94d1-16147fe627e6", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Client Submit DESC" + }, + { + "enabled": true, + "id": "02b131ba-e0d2-474a-a509-e822a7828ad6", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + } + ], + "favourite": false, + "hidden": false, + "id": "57d20e86-1ab8-4062-b7eb-d4485cd68d84", + "name": "Layout : Latest Client Send", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "208efe5f-8876-4759-b94f-a0171fe3cdf8", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Compare", + "Conform", + "Replace" + ], + "hidden": false, + "id": "f0a18af0-fd4a-4098-b792-115ca91bdfa8", + "name": "Layout", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "edbac712-735b-4f45-bfe0-6961e65cd797", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "84b4f51a-cc60-4929-a629-82cf69d674b6", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": false, + "id": "6c7a2c3f-34a3-47d4-bc8d-c87126325ce0", + "livelink": null, + "negated": false, + "term": "On Disk", + "type": "term", + "value": "${DNSITEDATA_SHORT_NAME}" + }, + { + "enabled": true, + "id": "3a0c3bfc-a26b-4d18-bbef-256623ea5762", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "533d6d4f-8ddd-426e-b316-f262ab0ae779", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "4b51d4c0-d41f-495a-89e0-a0d576b0270f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "afa4c40b-305c-4725-981d-d09831970401", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "ec2c3f24-f02e-439f-a0b4-950ea3f61364", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "cd563c00-2544-48a6-b953-590d958292fb", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "cc16ca46-bd1e-46ff-911a-0160b8e8c1d3", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "6df0b84e-2064-425c-9f0a-89482436e994", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "84e409f3-376a-47ce-b84c-35a75dcfbe31", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": false, + "id": "b039487f-d75a-46e6-aafe-b9fa8fba9ce3", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": false, + "id": "d9318d5a-27f4-43a8-8d0e-c7d37fb43578", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": false, + "id": "7a504874-d3d9-410b-8ef3-70332431f54b", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "cut" + } + ], + "favourite": false, + "hidden": true, + "id": "ae2f90ad-cc4e-44b9-8092-0f71bc4ced73", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "9ad21c1b-b731-49b2-bb20-197789aeac24", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Lighting" + }, + { + "enabled": true, + "id": "f9e8f7d7-5a23-4d47-9e34-7add985e99a0", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "9297730e-98fc-4b1e-ae90-42be83ede17a", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "CG_" + }, + { + "enabled": false, + "id": "f3b177e2-5fc2-4687-aba1-7e80d120c713", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": false, + "id": "23458328-b9fa-4619-9f88-7afd6ee307ef", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": true, + "hidden": false, + "id": "49ca5df3-864b-45ac-a54c-fc0234cdabc7", + "name": "Lighting : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "05932f11-43ec-47e4-b10c-7f9fdee61f81", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "ffc67de2-3ed8-46a0-9f64-bf20ec3093ae", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Lighting" + }, + { + "enabled": true, + "id": "11d6f125-33fb-4452-8ba8-9c7b816d342e", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": false, + "id": "a64869a9-43f9-4b31-9701-f469bdde86af", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "b243f61a-6351-4e24-9508-b9320a87c287", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "3b4431ad-a6a0-4219-a973-45f010091f70", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "comp" + }, + { + "enabled": false, + "id": "32b733f1-e0c8-404c-a6bf-f3ee55772dc5", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + } + ], + "favourite": false, + "hidden": false, + "id": "bd916ae3-ff80-4a98-9143-a5b663c44eda", + "name": "Lighting : Comp Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "2f29caf1-9100-4851-b087-a379e7f38680", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "fe5a38a0-d75c-4c21-8f86-3e5cb935cd39", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Lighting" + }, + { + "enabled": true, + "id": "97a129ce-3a2b-441c-a109-f0917949eaa7", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": false, + "id": "ffe955ed-0814-498e-99d1-287657ea21a7", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "6592f79e-856d-4a43-ad9d-3c2280be474f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": false, + "id": "d4440114-2187-4cff-bcc4-f3ccc73b420c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "comp" + }, + { + "enabled": false, + "id": "6b6baf83-0a6a-4d75-b6a8-55bebd808479", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + } + ], + "favourite": true, + "hidden": false, + "id": "c49a7865-a22e-4218-8518-8fae2735b097", + "name": "Lighting : Slap Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "3ae586d4-0240-48de-a47e-8bdd7b3e7830", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Lighting" + }, + { + "enabled": true, + "id": "696a069f-553f-49cc-8545-9c87b7d006af", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "fb7f8f1b-9b54-4f61-9c6c-27cf16e19192", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "_ref_" + } + ], + "favourite": false, + "hidden": false, + "id": "f981e5b0-e8a9-4338-b7ff-b708e31b4643", + "name": "Lighting : Ref", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "1858b2fa-2c73-4d39-81e6-269b0994331f", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Compare", + "Replace", + "Conform" + ], + "hidden": false, + "id": "d3dc2022-450d-4cb4-90b0-1643ddfa3aaa", + "name": "Lighting", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "4ac3c96a-85b6-4612-aeca-41719a533a6e", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "6957bb86-a368-4997-a958-a9a8ee82360b", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": false, + "id": "9cb0bb68-057f-4a63-9783-16cfd51c7697", + "livelink": null, + "negated": false, + "term": "On Disk", + "type": "term", + "value": "${DNSITEDATA_SHORT_NAME}" + }, + { + "enabled": false, + "id": "6fc92941-d860-49c4-9fde-612a598eae31", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "e9afbc81-af0f-4d26-8916-25bee6ffece1", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "ac2e2cc2-c647-4428-94a3-6b835e083883", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "075cdb17-d294-415f-8c7b-c1f4ff590ccd", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "c14cb0aa-5d2e-4065-9279-a03e876a7f07", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "116d4d60-fb75-4650-a649-e2886f3b2780", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "705718a8-f126-4070-a5bf-9530403a35cd", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "ccb9b190-f9b8-4986-9014-d4f4f15326b1", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "7f8bba5c-9694-4c79-b80e-7df1d722c24e", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": false, + "id": "6ee7817a-c8aa-423e-898c-550c07a2ade9", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": true, + "id": "dc5ee2b8-c838-4b7a-8cd4-14e6cdb8a132", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": false, + "id": "1cb93981-3edf-44d1-94d9-963e30d91fdd", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "cut" + } + ], + "favourite": false, + "hidden": true, + "id": "2dbf404e-dd46-4e41-baf2-e8065eda544c", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": false, + "id": "81823aa8-73cf-46e0-b8f8-377427bbc8cb", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "ab22511d-9628-4e5d-8405-36818f628c64", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Body Track" + }, + { + "enabled": true, + "id": "46c696c0-aaf3-489d-ac0f-f36f842333b6", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2" + }, + { + "enabled": true, + "id": "6baf4019-1213-49b2-badb-3dc9b96394d9", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "wireframe" + }, + { + "enabled": true, + "id": "63ae9955-65e1-4718-ba4d-07e50309696a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "outline" + } + ], + "favourite": true, + "hidden": false, + "id": "6b75d34a-6c52-495d-a388-f5b80c268aeb", + "name": "RPM : Body Track Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "e77ce5a9-9239-4671-8e13-5ff8f68ab514", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "68858e0b-e1d2-42e7-8b05-925727cf7322", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Camera Track" + }, + { + "enabled": false, + "id": "fccbd5c7-e9b9-47b8-b086-f200b14c42fb", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "8a718793-6e6a-4995-86f0-faf384edaf06", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "wireframe" + } + ], + "favourite": true, + "hidden": false, + "id": "693dccc2-e320-4888-82db-f8e8e92965bf", + "name": "RPM : Camera Track Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "63464e1b-bab9-4351-8876-43f8f484512f", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "d3fd28f4-3bf1-4437-bc91-90af54432a9b", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Prep" + }, + { + "enabled": true, + "id": "b52ea132-cc23-448e-a8a4-61b1c81f4593", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Pending" + }, + { + "enabled": true, + "id": "6e259401-5b4b-4326-aecb-19797bd69bde", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": false, + "id": "589f322e-10fb-4f3f-838e-25c7f09916f8", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": false, + "id": "419ef077-e7a4-41c0-b453-05867d641864", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": false, + "id": "4a4ad4ea-41e9-4815-b831-aee1fb73261a", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + } + ], + "favourite": false, + "hidden": false, + "id": "c48da073-7062-420b-9c57-19e9ad4493ea", + "name": "RPM : Prep Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "6dfc7a14-5b83-4394-b6d7-897a9918462e", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "cbd1bc40-44be-499b-a704-45f7c705118f", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Prep" + }, + { + "enabled": true, + "id": "c0758828-d450-4e20-a7a5-078c02c14105", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "beauty" + }, + { + "enabled": true, + "id": "a90e1a5b-7ebd-4bca-a0f2-290a5c6d2517", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "e851d0d4-def9-4591-9c1d-d5d35d9cccda", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "2a7c0993-6391-4eb9-afb1-8c85c66ed589", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "643d7f3f-ea84-4594-a54f-dfc915884df4", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "precomp" + }, + { + "enabled": true, + "id": "a4522793-282f-4f12-bd68-a3810244c3cc", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Pending" + }, + { + "enabled": true, + "id": "a243757a-b478-4198-b235-b324d81b52b8", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "wip" + } + ], + "favourite": false, + "hidden": false, + "id": "26147304-3985-4135-a67e-150f853f16e6", + "name": "RPM : Prep Beauty", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "3cb385c8-366e-4e1c-b792-75d2390e6836", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Roto" + }, + { + "enabled": true, + "id": "4f54c637-25b1-4514-b883-a419e6c7417f", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "cb0cff5b-48d4-4162-8a29-94403c1fd479", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "4c887708-ce1e-4261-bc96-7c0b6557dcd6", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "a96b0ad4-5452-4cc8-bc5c-b6cbfcc75dcf", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "id" + } + ], + "favourite": false, + "hidden": false, + "id": "aadf08dc-cdc7-4e7d-b677-4c4984a59e69", + "name": "RPM : Roto ID Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "45edc28c-4e26-4a86-8d72-95d1316db859", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Roto" + }, + { + "enabled": true, + "id": "3896aaee-5495-44f1-8d68-5f88b3b46ed9", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "abd73b29-8ed1-48f7-9d1b-732475e6dafb", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "9caec25d-bbd6-423f-9b89-87c033b90b13", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/test" + }, + { + "enabled": true, + "id": "6565b083-dcd9-44a1-9ea2-d207fdb2ef3c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "rubylith" + } + ], + "favourite": false, + "hidden": false, + "id": "a021f5c4-8853-44b3-8781-c7f01cd51f05", + "name": "RPM : Roto Rubylith Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "76746c60-6914-41f9-bdd7-bf8b1f11cf02", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Roto" + }, + { + "enabled": true, + "id": "8a6f85e1-87fd-4f13-8e1c-82685a8c3b10", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "1e72c328-7f07-4955-b65f-901361fb3503", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "1f8b90c3-6017-4904-bbe6-33ac4f22d228", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/test" + }, + { + "enabled": false, + "id": "1c0825d6-70f6-4327-9959-bcfa04093846", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "rubylith" + }, + { + "enabled": true, + "id": "e11f6739-8594-4726-989b-afdc5ceac5d2", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "L1" + } + ], + "favourite": false, + "hidden": false, + "id": "c0b541fe-488d-494a-9003-8e650090add6", + "name": "RPM : Roto Set Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "e7b5b25a-60ac-46af-8e52-e3d8985c6238", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Roto" + }, + { + "enabled": true, + "id": "d2d871c2-9445-4de4-a200-2df72e841a8b", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "36b30526-a6cf-4f1e-89a4-5fb571b7a169", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "fe734c3e-4945-4285-8045-fbf341878725", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/test" + }, + { + "enabled": false, + "id": "612f59ae-7997-422c-8d63-2df5c548604e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "rubylith" + }, + { + "enabled": true, + "id": "69a5ad11-c52d-4987-b73e-76f4e8908587", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "L0" + } + ], + "favourite": false, + "hidden": false, + "id": "e3648311-43f4-4135-9739-ff3703b03a38", + "name": "RPM : Roto Character Latest", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "7f6c2760-23d0-4d0d-96c5-452dae024335", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Compare", + "Replace", + "Conform" + ], + "hidden": false, + "id": "3eed6903-2031-4fbc-b92a-eb8dee07e613", + "name": "RPM", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "eb751900-ba93-48ea-866d-bd8e0bb14107", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "29e2c17e-acbc-466a-9766-00b71e4792fe", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": false, + "id": "7a0aa6ff-f891-4d71-9c16-83fd6325d830", + "livelink": null, + "negated": false, + "term": "On Disk", + "type": "term", + "value": "${DNSITEDATA_SHORT_NAME}" + }, + { + "enabled": true, + "id": "18c8e6f8-f53c-4317-9d32-c78b315767cf", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "0fca5d4d-ced4-4802-85a6-d269e5acb030", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "9bc24f74-e37e-4e91-9dbd-b52a27013d8c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "de5219e5-47b4-44f3-b658-72411c51796b", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "949d2994-26c8-45d1-9636-c3db709ab8a5", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "07f645ec-5d49-4b0a-ae62-7c92c5e79af8", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "5d3226df-2772-4994-8751-0ed7d111bab4", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "9b718dec-12c9-4187-a256-9e96609438b4", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "f48bc7cd-4608-452a-8690-38d742f9c533", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "1a6d8601-3c36-485a-8567-ade2af4b3d49", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": true, + "id": "4330d3c0-de7a-43d0-ac29-2f21f83545f5", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": true, + "id": "ae67e764-e5df-42ff-9ee2-084dbcdd38cf", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "cut" + }, + { + "enabled": true, + "id": "656417d4-8531-4cb5-b40b-e1de48696e64", + "livelink": null, + "negated": null, + "term": "Viewable", + "type": "term", + "value": "True" + } + ], + "favourite": false, + "hidden": true, + "id": "cb98075d-c7fa-4f8e-8be7-cbdb7be4a932", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "7abb39c4-0b40-4363-876f-523cca3ab097", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "rerack" + }, + { + "enabled": true, + "id": "25af70be-ad11-4023-8c79-62ac045729b3", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "repo" + } + ], + "favourite": true, + "hidden": false, + "id": "b1b1ba25-3659-4b05-bc47-f72798f3c45b", + "name": "Repo / Rerack : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "b27dff71-d359-4666-a740-4dbccb0d4e80", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Retime Layout" + }, + { + "enabled": true, + "id": "33e818dc-9f89-4223-8aed-68a1efd90112", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactsheet" + }, + { + "enabled": true, + "id": "9a396153-d65d-45f6-a4d4-c99b66d31ccc", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + } + ], + "favourite": true, + "hidden": false, + "id": "e10c568f-eb96-4b15-a78b-509b4548f5fd", + "name": "Scan : Retime Editorial", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "1c36c817-4f84-4256-a462-f4fa80f324ea", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Retime Layout" + }, + { + "enabled": true, + "id": "7ee2eb7e-fc4d-4071-bd35-5ad63f73f7b1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "contactsheet" + }, + { + "enabled": true, + "id": "75b298bf-06f8-4791-87b9-9401a21585fd", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + } + ], + "favourite": false, + "hidden": false, + "id": "ded3105b-1893-4fc3-87bf-f6dc992984e3", + "name": "Scan : Retime Contact Sheet", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "6fedf5c6-231b-4696-a368-97787e544c7a", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "baace18f-053e-4e2d-acef-d3fd2b2e4056", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "be29e4f8-983e-4702-af04-19aa43dfc4b2", + "livelink": null, + "negated": null, + "term": "Is Hero", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "8795a9cc-b2a1-4730-86c6-59e38125a993", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": false, + "id": "28771f3c-3187-47c2-bde1-df7a1ffdac54", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": false, + "id": "ac57f550-4748-4f51-83bc-b9d1a6a18d45", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": true, + "hidden": false, + "id": "01220f1d-1cf3-4c26-93a0-5342d445bfad", + "name": "Scan : Hero Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "d1ead3b5-fef8-4dd2-b249-82de13e5bae3", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "85cf8dd5-7158-4469-9429-1bc92a84d601", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": false, + "id": "3e116bea-10fd-4000-afc7-06b01863a33c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^S_" + }, + { + "enabled": false, + "id": "4e5a84d6-ba86-4492-b9e6-5cec9e16b376", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "ref" + }, + { + "enabled": false, + "id": "25044e9f-d529-4ed0-be22-5b03a31b0669", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "_lr" + }, + { + "enabled": true, + "id": "7c9aa133-2e9b-49ad-bf5a-d444b5698303", + "livelink": null, + "negated": null, + "term": "Is Hero", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "0b614277-e680-4beb-9abf-6f9da186cc2d", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": false, + "id": "b01df6cc-5f28-4740-b461-1b93df2a1e09", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": false, + "id": "23540803-fdb6-4a1f-8311-05bb83ef0f42", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "02b28379-9de2-4d1e-a233-f4c5d69dc5ae", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "orig_proxy0" + } + ], + "favourite": false, + "hidden": false, + "id": "5700b87e-9cb0-4cb8-811d-b4b9b5cf86b9", + "name": "Scan : Orig", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "4b70bc9a-6db2-4851-ad28-3b7e0c0171ff", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Compare", + "Replace", + "Conform" + ], + "hidden": false, + "id": "b48b00b1-2f09-4f25-8f6b-a22e2349ac3a", + "name": "Scans & Retimes", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "d01f602d-635a-4497-bd86-1696de189f5f", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "2cd63f71-83a4-4aee-9785-6fe56608a651", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": false, + "id": "d053c029-64cd-45f5-91b7-a5f7e843e16c", + "livelink": null, + "negated": false, + "term": "On Disk", + "type": "term", + "value": "${DNSITEDATA_SHORT_NAME}" + }, + { + "enabled": true, + "id": "49f33994-d8cf-4f5b-8b05-85be293d7648", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "96038804-802d-4aff-8bc8-3aab772c5462", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "69b89d57-b2fb-4b88-adfe-ef02bf3d6bb0", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "283593dc-3ba9-4ddb-9c72-62dd783847f4", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "8e974015-affb-465a-bf41-4309d0a8780b", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "24a57f86-09f1-4714-b43d-c4a6c2d6bb4b", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "95d0c946-0e59-42c4-807d-20b53d74d4b3", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "7c337056-79be-4024-a2ba-f192dde91348", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "bd0da150-4224-4acf-994c-c11f1c4308db", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "d6e5a4ad-b794-421d-9ee0-a27bc9337cf3", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": true, + "id": "4111c558-90ad-4a6a-88ee-6332b1c0c885", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Declined" + }, + { + "enabled": true, + "id": "6a5e5e38-162c-4c2f-bef6-6a08cdeb2764", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "cut" + } + ], + "favourite": false, + "hidden": true, + "id": "2a0d988c-9070-468d-b01b-0e44650d0cb4", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": false, + "id": "e739da89-028e-4e8a-ada6-d4dd0b832c48", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + }, + { + "enabled": true, + "id": "ab8ecced-a4d6-4802-977b-b65db49414b4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witcam" + }, + { + "enabled": true, + "id": "4965fb30-3697-40ec-91e8-056f94d429ea", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "2b7ebd59-863e-4076-9515-927fc0689d99", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Result Limit" + }, + { + "enabled": true, + "id": "8d3a9628-a77a-4bdf-9685-7d9e25dc3095", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + } + ], + "favourite": true, + "hidden": false, + "id": "93de6454-15e2-41c4-b487-2218d1d2dbf9", + "name": "Witness Cameras", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "f5834ddc-9d33-4412-88a4-53d7812789fa", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": false, + "flags": [ + "Ignore Toolbar", + "Compare", + "Replace", + "Conform", + "System Group" + ], + "hidden": false, + "id": "b6e0ca0e-2d23-4b1d-a33a-761596183d5f", + "name": "User Presets", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "52f31750-6595-4a5e-9647-bfb6585e84ad", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "4ea3fca3-4d9e-4a0b-b9b4-7f8f2d715dc3", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "de3e5274-87f8-4029-9a99-6b1958236fb4", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "46c178e0-578a-4fde-aec8-74fb838428c4", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "f4ec7b44-3428-43df-a48f-56f41f77dcd7", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "ced576a8-ccfb-4c2a-bd99-b886e701c4a5", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "b5d4430b-ba24-4a2b-9a8f-e65c4ec1db6a", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "44852cc7-699e-4ae8-8d45-593b36bf1e4b", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "cc3d5598-4fd5-4f86-9568-e3f76ad947ee", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "3057de96-f7b3-481e-9401-dfc9e56bf71e", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + } + ], + "favourite": false, + "hidden": true, + "id": "119bc765-02e8-4da8-a6d8-db335e0461ef", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "93c68a66-da6e-4849-8b34-1b13eb44cf2a", + "livelink": true, + "negated": null, + "term": "Twig Name", + "type": "term", + "value": "" + } + ], + "favourite": true, + "hidden": false, + "id": "b8d1c72e-c885-41fd-9187-6f3f5afaa01b", + "name": "Stream", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "51e0b692-9434-4dea-8885-1f1e1be11df4", + "livelink": true, + "negated": null, + "term": "Pipeline Step", + "type": "term", + "value": "" + } + ], + "favourite": true, + "hidden": false, + "id": "8e140a38-08e5-49c2-b473-4c17ff318527", + "name": "Step", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [], + "favourite": true, + "hidden": false, + "id": "e33d0a5f-7040-49fb-93d9-97995f9ab3b1", + "name": "Shot", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "d5aa96f5-5640-4657-a1ec-b3133ddf6856", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": false, + "id": "1cf83e03-8e33-4ebd-ba78-e8ee09a7695a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "retime" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "237526b0-e21d-492a-b87f-660195b13bf0", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Scan" + } + ], + "enabled": true, + "id": "bba1c648-f691-4a54-9fa3-59e70e9d5884", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + }, + { + "children": [ + { + "enabled": true, + "id": "3a85fe80-fd9f-45d8-ba84-f6fd42427308", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "retime" + }, + { + "enabled": true, + "id": "8ce17d59-6e95-40aa-9552-3fe765eae9ea", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "repo" + }, + { + "enabled": true, + "id": "e50083eb-baa2-4d45-81e6-99d43bb9907d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "rerack" + } + ], + "enabled": true, + "id": "337a029f-b87c-411f-80dc-f6184782b13b", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "enabled": true, + "id": "17bebec3-dc10-4e7f-8c4a-a1fd1711702a", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": true, + "hidden": false, + "id": "7ea9bac4-1ac2-4830-88ff-023b5e7514f5", + "name": "Scans", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "9bbbf34a-957f-44ba-9d40-fb2c172907d4", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "f3cf9363-ee9c-4172-8f17-f92219eb8fea", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "CUT_" + }, + { + "enabled": true, + "id": "cfeebba2-a6eb-4723-a257-cb75d6296e0b", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witcam" + } + ], + "enabled": true, + "id": "d20192bd-9bc4-4f5e-a2f3-b993e8df3290", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "dbe7ebe6-d919-46c7-9eab-323a66fe8607", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "CUT_" + }, + { + "enabled": true, + "id": "5419c6f3-f5be-42aa-874b-aac5d812bf48", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "gesture" + } + ], + "enabled": true, + "id": "5683d79e-7517-4ef0-b3ff-517aa60035e9", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "enabled": true, + "id": "e1b2eb8b-62b1-46ab-b2e4-bb331c58a0d0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "WITVIDREF" + }, + { + "enabled": true, + "id": "ab72ced2-3fc7-4683-92e5-7d17a12f6f05", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "OSREF" + } + ], + "enabled": true, + "id": "cba67754-744c-487b-b0ca-4f5a5176515a", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": true, + "hidden": false, + "id": "acb0bf88-1a27-4691-9cb1-0a453fde0f45", + "name": "Ref", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "d0864c81-77cf-4d74-8f97-99bae06c5af1", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Entity" + }, + { + "enabled": true, + "id": "a32e5a60-d6e4-471d-ab93-93ed7ba87d46", + "livelink": true, + "negated": false, + "term": "Sequence", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "a109fe21-0b22-444c-9fff-5c88bb2c215b", + "livelink": true, + "negated": null, + "term": "Shot Alternative", + "type": "term", + "value": "" + } + ], + "favourite": false, + "hidden": false, + "id": "cb4321a1-240b-49f3-a824-237852c4e665", + "name": "Alternate", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "c5ce1db6-dac0-4481-a42b-202e637ac819", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Shot History Scope", + "System Group" + ], + "hidden": false, + "id": "4c512dae-e1e3-43b7-a02a-4fb7d93fde62", + "name": "Shot History Panel", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "465a9364-1b1d-47a7-b02e-1e7141c2c77c", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": false, + "id": "bd978c76-bd91-40b4-9b6d-8b48ab934d20", + "livelink": null, + "negated": null, + "term": "Flag Media", + "type": "term", + "value": "Orange" + } + ], + "favourite": false, + "hidden": true, + "id": "d0b15051-275b-4601-bf6b-8d44ecd0fb4f", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "dfd505c8-a394-42fc-8bed-138c24c141d9", + "livelink": true, + "negated": null, + "term": "Version Name", + "type": "term", + "value": "" + } + ], + "favourite": true, + "hidden": false, + "id": "5d3d87ad-ea87-4349-bb2e-f6baea908486", + "name": "Current", + "type": "preset", + "update": false, + "userdata": "scope" + }, + { + "children": [ + { + "enabled": true, + "id": "e47db10a-7d8f-4f9b-8e22-c79ec716602f", + "livelink": true, + "negated": null, + "term": "Twig Name", + "type": "term", + "value": "" + } + ], + "favourite": true, + "hidden": false, + "id": "12ca841f-b6e5-4ffa-8345-f8843588f84f", + "name": "Stream", + "type": "preset", + "update": false, + "userdata": "scope" + }, + { + "children": [ + { + "enabled": true, + "id": "a9933769-247b-4169-9604-66ab1c14f74f", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "8edd718f-5fee-42b3-a889-bd0363efb33d", + "livelink": true, + "negated": null, + "term": "Pipeline Step", + "type": "term", + "value": "" + } + ], + "favourite": true, + "hidden": false, + "id": "636667ac-d691-4934-be2a-78cf169bb377", + "name": "Step", + "type": "preset", + "update": false, + "userdata": "scope" + }, + { + "children": [ + { + "enabled": true, + "id": "af3aef89-f132-48f3-afd9-308732b29e3b", + "livelink": true, + "negated": null, + "term": "Entity", + "type": "term", + "value": "" + } + ], + "favourite": true, + "hidden": false, + "id": "facaba43-3e3c-4f56-8c40-74d8f9c05987", + "name": "Shot", + "type": "preset", + "update": false, + "userdata": "scope" + }, + { + "children": [ + { + "enabled": true, + "id": "a0abc5cd-48f0-45ee-ae6a-f84732e41c76", + "livelink": true, + "negated": false, + "term": "Sequence", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "cf61b293-8153-4450-b922-edea6a3cb753", + "livelink": true, + "negated": null, + "term": "Shot Alternative", + "type": "term", + "value": "" + } + ], + "favourite": false, + "hidden": false, + "id": "b7001730-2e20-4804-aa22-be89cbe465a1", + "name": "Alternate", + "type": "preset", + "update": false, + "userdata": "scope" + } + ], + "id": "aac8207e-129d-4988-9e05-b59f75ae2f75", + "type": "presets" + } + ], + "entity": "Notes", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Note History Scope", + "System Group" + ], + "hidden": false, + "id": "28612cf7-a814-4714-a4eb-443126cf0cd4", + "name": "Note History Panel (Scope)", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "9372e556-c42e-499e-b1ae-5b99d69bc66a", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "enabled": true, + "id": "0f3cae7e-3728-433f-8f90-fa3e13d3a16e", + "livelink": null, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + }, + { + "enabled": true, + "id": "b1001592-8165-455b-8660-dc0aea5fdddf", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "review_proxy_2" + }, + { + "enabled": true, + "id": "84748f8b-886f-4e1d-9ba1-3219de90b288", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "4939e1ac-8e61-44af-96f7-c51e91a48c93", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "2a2f8d27-49df-468d-a01f-bd6d83878910", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "1e3033a7-b1f0-48ce-bc25-05364c70b2f0", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "97188e24-9119-4c2b-b015-0c46c9bcab08", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "948da98a-04be-4b47-ab55-fa7bc13749a1", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "a1ca1f90-16d0-4557-9e46-5e077bb5008c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + } + ], + "favourite": false, + "hidden": true, + "id": "3446efbd-3cc5-4af4-8c99-bde94b495102", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "31c206fb-29b1-47fa-9e65-b779cbb8badc", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Dailies" + }, + { + "enabled": true, + "id": "de2e4a23-54fc-4c82-8e8c-45d0ac98a944", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "Today" + } + ], + "favourite": true, + "hidden": false, + "id": "2fa09c2e-4676-4b2e-9e79-6c39f3f4d735", + "name": "Today's Dailies", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "fed40206-03a9-41be-89bd-66196a8320c1", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Dailies" + }, + { + "enabled": true, + "id": "44778b1a-40ee-4eb6-8f43-fbda8a390356", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "Today" + }, + { + "enabled": true, + "id": "f2b5ed2f-30b2-4a34-b354-165c3d451278", + "livelink": false, + "negated": true, + "term": "Pipeline Step", + "type": "term", + "value": "Creature FX" + }, + { + "enabled": true, + "id": "8404ea7b-999b-4e9d-a82e-6eca645c71a3", + "livelink": false, + "negated": true, + "term": "Pipeline Step", + "type": "term", + "value": "Shot Sculpt" + }, + { + "enabled": true, + "id": "3da76e23-c868-4d26-86c2-76ca80df8279", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "626983bd-ff73-455b-8cfe-b564d73590c8", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactsheet" + }, + { + "enabled": true, + "id": "ce767691-5096-4970-99f3-5dea0db5cce7", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "pip" + }, + { + "enabled": true, + "id": "8e78ba75-ebab-43d7-b5f8-9fddeea1fb57", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "qc" + } + ], + "favourite": false, + "hidden": false, + "id": "d4429a5a-2c20-488e-93f4-ce672a411229", + "name": "Today's Nice Dailies", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "c1dc9c08-9f2c-4130-b4a3-ffb8336e0606", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "Today" + }, + { + "enabled": true, + "id": "ce88d843-f148-4b34-a957-bebb569714e9", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "1d1c1452-d5b6-43f7-8523-f89c81ed4ffe", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "4bd7394b-de67-43ce-9783-56c696c3faa5", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/deep" + }, + { + "enabled": true, + "id": "3cd9279d-828f-4720-9e97-a579a84c1f30", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "b032e64a-c03b-4161-ae7e-48c92a40e2f5", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "9c89e531-333e-4b51-8744-209c3cbe356a", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "f9abe9fa-7c05-4bea-a519-6150ddbd740e", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "cb20befa-a23f-4cb5-ad59-bd17109b6b5d", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/wireframe" + }, + { + "enabled": true, + "id": "cfffcdd1-4212-41b9-9ba8-1e76d3201f8e", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "cut" + }, + { + "enabled": true, + "id": "c1c94d0b-f17b-472f-8a2a-c5a24b83cf83", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "2486e29d-df7d-412f-b673-0e73e540fc0e", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "video_ref" + }, + { + "enabled": true, + "id": "d65cbb41-6bea-4cfb-96ef-9b2007f3a1a6", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "video_ref_witness" + }, + { + "enabled": true, + "id": "967db6a8-5ed6-4d30-b675-f41af6389fed", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "ref/onset" + }, + { + "enabled": true, + "id": "bfa4812a-7701-49cd-8bee-0e65515931fe", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/ref" + }, + { + "enabled": true, + "id": "bac27370-17d1-4c4a-854b-55ec6583fc31", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "concept_art" + }, + { + "enabled": true, + "id": "336547ff-fdd2-44e1-8bde-d0458a3c739a", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "ref" + }, + { + "enabled": true, + "id": "f6aaa104-07e7-48d4-a12b-7ac2279d366f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "concept_art" + }, + { + "enabled": true, + "id": "27ea2530-77df-45ad-b7ed-9c3b4933ea15", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "graphic_art" + }, + { + "enabled": true, + "id": "d40db48d-3c91-4f67-b690-c1d6c63adf0c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "image/artwork" + }, + { + "enabled": true, + "id": "602b6caf-2b1c-478a-9689-e52fe8b4b583", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "image/storyboard" + }, + { + "enabled": true, + "id": "ade8c3a6-f4a8-4782-9d29-39c8bc90a5fb", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render" + } + ], + "favourite": true, + "hidden": false, + "id": "a08388dd-fbef-4931-9c54-8972f19bf99c", + "name": "Today's Versions", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "eaa4c3e6-1553-44b4-9ace-685d6c752567", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "7 Days" + }, + { + "enabled": true, + "id": "30f77b80-b831-4d60-a23e-41041b3c1f68", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": false, + "id": "73b9e55a-e4fb-4214-88ed-684a3fd64406", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "3b99af2c-ff88-45d1-9275-4661c09f879b", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "witness" + }, + { + "enabled": true, + "id": "cb214456-d415-4cfa-8f0d-d1661fb1f4a0", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "tech" + }, + { + "enabled": true, + "id": "6195f470-791a-46db-ae0e-d42ef93e08ed", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "layout_v" + }, + { + "enabled": true, + "id": "47d76579-9f58-474d-9199-aa56cdb8d3a4", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Dailies" + } + ], + "favourite": false, + "hidden": false, + "id": "bb8acf07-c58d-4c5e-8432-8142762bb7fc", + "name": "Latest Layout", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "663448bc-e6a0-4730-a982-d44456bd7546", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "7 Days" + }, + { + "enabled": true, + "id": "6d9aedc4-1c2a-4a90-88c4-e12fcbb5ec9d", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": false, + "id": "1e050ed9-3b3e-4bee-bbaa-a85c7a820ef9", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "e0b55f04-0a3c-4e72-95cf-449a367d6fa5", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "WP_" + }, + { + "enabled": true, + "id": "e033ccd6-510d-456b-8c03-0bf9f5194027", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "anim_v" + }, + { + "enabled": false, + "id": "1fb8c236-1e55-4386-8cb0-9338b300d35a", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + } + ], + "favourite": false, + "hidden": false, + "id": "cf5b51cc-00b9-43af-8480-a7af829723ab", + "name": "Latest Anim", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "20611ac2-617e-44c5-a930-07a71e5dae37", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "20 Days" + }, + { + "enabled": true, + "id": "33dc25e2-6784-4a15-b641-42c004f88167", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Creature FX" + }, + { + "enabled": false, + "id": "6cc4c473-bdfa-4d87-a852-c89a30328a67", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": false, + "id": "32d503a6-48e9-45af-9da0-2928d9b19c36", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "comp_v" + }, + { + "enabled": false, + "id": "69752559-092b-40a3-a546-180b5438d129", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "O_" + }, + { + "enabled": false, + "id": "7a827b7f-bea3-44c8-966b-ec7e384c77ac", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "30-60 Days" + }, + { + "enabled": true, + "id": "2e361410-42dc-4e0a-b777-0b3a28c5936a", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + } + ], + "favourite": false, + "hidden": false, + "id": "2238a890-decc-49fe-9d8d-5f3df8bda742", + "name": "Latest CFX", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "83968fbe-e77d-4702-8205-715210812d78", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "20 Days" + }, + { + "enabled": true, + "id": "44c5a8ac-d7e9-4215-a20c-6eaf128fa763", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Lighting" + }, + { + "enabled": false, + "id": "b5846516-45be-4f29-afd6-04b1fad02da8", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "b09f8d94-2dda-4fa8-97b8-ed9c4e8b67bf", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "35abd495-c361-41ce-a00e-b240e009e9de", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "comp" + } + ], + "favourite": false, + "hidden": false, + "id": "b18f558b-de06-4cff-9ce6-a29899c73311", + "name": "Latest Lighting", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "c2f52797-e9c1-4bb3-a0f5-f0a98cb50b22", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "Today" + }, + { + "enabled": true, + "id": "22f63494-6f2b-4d09-bbe8-2f560661cdee", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Comp" + }, + { + "enabled": false, + "id": "5ca5f4a7-c131-4d9b-8170-af9d08da2cc1", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "b876e30b-0d52-4063-89b3-aebb00814d68", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "comp_v" + }, + { + "enabled": false, + "id": "ed450948-0190-40c2-bc2e-abf6f6fff792", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "O_" + }, + { + "enabled": false, + "id": "e7d0bc46-b342-4a22-9b74-34bc481dd94b", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "30-60 Days" + }, + { + "enabled": false, + "id": "b84885ea-9660-4168-97fc-ac7f6105b1b6", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "review_proxy_1" + } + ], + "favourite": false, + "hidden": false, + "id": "3d167e7c-950a-477e-a7ab-1efbd1044f2a", + "name": "Latest Comps", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "bc45e233-909e-4f33-b12f-5b4efa0911cc", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "7 Days" + }, + { + "enabled": true, + "id": "47b50922-4204-4fe5-8330-b03eb3cafc15", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "0f8b642f-1b14-4873-bde8-b6e693475dcd", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + }, + { + "enabled": false, + "id": "36ddfc3f-d274-4e23-a6c0-ae48d3ce7c83", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "30-60 Days" + } + ], + "favourite": false, + "hidden": false, + "id": "6d077c45-27ae-4da0-b31b-1d028e46c6e0", + "name": "Recent Client Versions", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "83d6a230-66eb-4900-8bee-f36333d150b4", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "bf855900-b356-4fd8-a1bd-38421965fa0c", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Proposed Final" + }, + { + "enabled": true, + "id": "246e77e1-4040-428b-8e48-3699e3cbf1ee", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Proposed Final Pending" + }, + { + "enabled": true, + "id": "afe4bf30-e059-47ba-8173-06e1edd9d752", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Proposed Final TF" + }, + { + "enabled": true, + "id": "f590fe54-c4d5-4455-9200-560a5376d45a", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Proposed Trailer Final" + }, + { + "enabled": false, + "id": "e3b3d0f1-a1fe-41f4-acec-7addf7bd096b", + "livelink": null, + "negated": null, + "term": "Stage", + "type": "term", + "value": "STAGE 06" + } + ], + "favourite": false, + "hidden": false, + "id": "bf881458-a07e-4e33-b316-a40c96f72ea1", + "name": "Prop Finals", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "84b917e3-ce24-4ce0-96e2-12c5400042b4", + "livelink": null, + "negated": null, + "term": "Stage", + "type": "term", + "value": "STAGE 05" + }, + { + "enabled": false, + "id": "b696a371-9918-4cb1-8444-da3a83a1b3bd", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final" + }, + { + "enabled": false, + "id": "f5ddb03d-8fa8-4ea9-a58a-93fec8782b87", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final - Other" + }, + { + "enabled": false, + "id": "c8c7723b-6175-49aa-89d3-9eb313071a2b", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final - Trailer" + }, + { + "enabled": false, + "id": "7b6bedef-7b26-48c7-9f80-488682c5cffa", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final CBB" + }, + { + "enabled": false, + "id": "7d06e902-936b-40fc-aed1-b0d5dca88ab0", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final Pending" + }, + { + "enabled": false, + "id": "7789d157-5b2f-4535-beeb-7b28865a6a05", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final Pending TC" + }, + { + "enabled": false, + "id": "4ad2f9f7-6f35-4fa9-a993-4ff47835d00e", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final Pending TF" + }, + { + "enabled": false, + "id": "6475115b-d5f0-4873-8fc6-245683751bcf", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final TC" + }, + { + "enabled": false, + "id": "75516723-7b24-4508-8c2a-e1e8b28325d3", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final TF" + }, + { + "enabled": true, + "id": "19a1b071-2379-4465-a157-9e080306020a", + "livelink": null, + "negated": null, + "term": "Stage", + "type": "term", + "value": "STAGE 06" + }, + { + "enabled": false, + "id": "5a1dfbc1-1c56-421b-828a-a51e9a0e7fb0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "comp_v" + }, + { + "enabled": true, + "id": "1b6289fc-8b06-407a-87e3-a61ee0890d94", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Comp" + }, + { + "enabled": true, + "id": "c2c3ef69-982b-4d3f-b440-ece2e6095528", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + } + ], + "favourite": false, + "hidden": false, + "id": "c0bd7635-9641-41cf-b7e6-492c7e16c249", + "name": "All Finals", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "93e8ac7e-019b-4c16-8538-53472788e398", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": false, + "id": "e326e9cf-711b-437a-a457-201048fb86d1", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final" + }, + { + "enabled": false, + "id": "d373a351-1af9-48a3-8eda-e9865b436f23", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final - Other" + }, + { + "enabled": false, + "id": "f6e25b00-202f-465b-99b8-fd0d567ce7b8", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final - Trailer" + }, + { + "enabled": true, + "id": "e0425d3e-6458-4acb-a569-34cd99b6e1cf", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final CBB" + }, + { + "enabled": false, + "id": "f8308390-ed9e-4047-991a-85c933b6a5d9", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final Pending" + }, + { + "enabled": false, + "id": "bf2d1bcd-4b15-49c6-bb98-316e458e4c2e", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final Pending TC" + }, + { + "enabled": false, + "id": "a01b05e6-98b5-4ca2-b7b3-1e3bc8b33c3e", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final Pending TF" + }, + { + "enabled": false, + "id": "a30a09e9-7886-4dac-846b-d6f3e7e44ab8", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final TC" + }, + { + "enabled": false, + "id": "c3f7c59a-f0ee-45fc-bc05-99ff58cca2b0", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Final TF" + } + ], + "favourite": false, + "hidden": false, + "id": "371e5ee9-887f-4508-89d6-305b3c127368", + "name": "CBB's", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "42c57b0d-5d67-4759-be39-35d338bbb468", + "livelink": false, + "negated": null, + "term": "Author", + "type": "term", + "value": "${USERFULLNAME}" + }, + { + "enabled": true, + "id": "404fdff6-b1b2-425a-8d96-14a8b295b0ac", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "pip" + }, + { + "enabled": false, + "id": "fc832b56-9854-4f32-99bf-20169b64c582", + "livelink": false, + "negated": null, + "term": "Author", + "type": "term", + "value": "Colin McEvoy" + } + ], + "favourite": false, + "hidden": false, + "id": "4e80620b-61c6-4214-98e3-81f313f5b2e2", + "name": "My Shots", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "8f5f809e-d23d-4655-91ed-f1e1db0ad2d9", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "main_proxy0" + }, + { + "enabled": true, + "id": "5ce4e60d-4583-4477-858c-d35aac933391", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "review_proxy_1" + }, + { + "enabled": true, + "id": "f3cade19-0788-4626-8ed9-6c33ae332fb4", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + }, + { + "enabled": true, + "id": "8c647092-5e80-42a0-b216-3e017602cdc0", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "026_dnw_0040_cp_v030" + }, + { + "enabled": true, + "id": "bc3f18fe-162e-4c6d-9681-cd8f4fbc9109", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "026_dnw_0060_cp_v035" + }, + { + "enabled": true, + "id": "29ae64e6-1bdf-4c47-ae80-bf001b7c558b", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "028_dnw_0160_cp_v015" + }, + { + "enabled": true, + "id": "4ede3081-c1b7-4e58-a48e-c716d1b56bec", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "028_dnw_0220_cp_v014" + }, + { + "enabled": true, + "id": "fd9da709-ab09-49f9-8c55-8e3624f47035", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "028_dnw_0240_cp_v013" + }, + { + "enabled": true, + "id": "5b78a52d-2e8e-4b50-bafc-1f7630a1b51e", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "028_dnw_0340_cp_v016" + }, + { + "enabled": true, + "id": "8310f5f6-1de2-4d0b-a9a4-e666c536ff3c", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "028_dnw_0380_cp_v019" + }, + { + "enabled": true, + "id": "87bf8baf-d439-4edf-a60f-beea78a8e433", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "028_dnw_0440_cp_v017" + }, + { + "enabled": true, + "id": "fe94fef2-55a1-4ea9-93f4-97cf6f477b26", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "028_dnw_0760_cp_v017" + }, + { + "enabled": true, + "id": "2f410106-efd5-4e53-87eb-8b7a4655648a", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "028_dnw_0780_cp_v017" + }, + { + "enabled": true, + "id": "a24091a8-b734-489c-a4f2-8e016ec377b7", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "028_dnw_0800_cp_v014" + }, + { + "enabled": true, + "id": "b20784c8-b918-46a5-8708-888d108eba26", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "028_dnw_0860_cp_v014" + }, + { + "enabled": true, + "id": "86b7f6b5-b72d-4e45-a221-d5ea3baa814d", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "028_dnw_1000_cp_v008" + }, + { + "enabled": true, + "id": "931ee7a7-eeae-41e2-9d0f-4910680fab7e", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "028_dnw_1000_cp_v009" + }, + { + "enabled": true, + "id": "45fdfc87-67de-4436-a3bb-01a7ea8f1a0f", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "032_rea_0140_cp_v011" + }, + { + "enabled": true, + "id": "0978be9c-6602-4321-ac2b-a3e19a2d422e", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "036_crb_0160_cp_v011" + }, + { + "enabled": true, + "id": "91364bde-ae66-4952-94f4-3f506c673f3a", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "036_crb_0160_cp_v012" + }, + { + "enabled": true, + "id": "98d24426-bc73-4896-b2e4-f148c7879f4c", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "037_rva_1200_cp_v012" + }, + { + "enabled": true, + "id": "8ff3cd31-e27d-4ee1-beb8-7bbb343e2d2b", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "045_all_0040_cp_v013" + }, + { + "enabled": true, + "id": "acaa9665-3815-4272-b928-151898f074e2", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "054_cab_0360_cp_v012" + }, + { + "enabled": true, + "id": "96b4113e-dbbf-4364-a3f1-47dc7745cbc8", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "058_daa_0860_cp_v011" + }, + { + "enabled": true, + "id": "c7ac501f-c0cb-483d-ad6c-4afec3e633a9", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "058_daa_0890_cp_v010" + }, + { + "enabled": true, + "id": "ecb69063-058b-442d-93ef-10520acb2fea", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "059_daa_1380_cp_v012" + }, + { + "enabled": true, + "id": "4e155d27-3ba2-4538-9873-a53a878a302d", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "093_tbu_0500_cp_v013" + }, + { + "enabled": true, + "id": "1ce44c0a-7b86-412e-b85b-6e656dcdff47", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "104_pat_3060_cp_v022" + }, + { + "enabled": true, + "id": "79913b47-3c64-4e32-89e2-6f8d8971ed1e", + "livelink": null, + "negated": false, + "term": "Client Filename", + "type": "term", + "value": "104_pat_4880_cp_v014" + } + ], + "favourite": false, + "hidden": false, + "id": "325b0a9b-7140-47d2-bf5d-14cab12b3abe", + "name": "Find", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "7d85dd2d-753c-4063-a31d-99a5d7095608", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "System Group" + ], + "hidden": false, + "id": "4689c10f-eb27-4e16-8164-468cdd69142e", + "name": "Versions", + "type": "group", + "update": false, + "userdata": "recent" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "35fe5de6-1201-4599-b07e-72591e18e876", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + }, + { + "enabled": true, + "id": "5dddcc96-cb6f-4057-bad0-f8811f779251", + "livelink": null, + "negated": null, + "term": "Has Contents", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "e30b57a5-bf37-45c4-b966-ddb753c72461", + "livelink": false, + "negated": null, + "term": "Preferred Audio", + "type": "term", + "value": "SG Movie" + } + ], + "favourite": false, + "hidden": true, + "id": "963185ba-25d7-4a22-8b56-cda119ce8105", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "c9ade46f-6056-4c41-890b-647cacdf9036", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "7 Days" + }, + { + "enabled": false, + "id": "bf33cb2d-285f-4422-a012-d0dc1f7d9952", + "livelink": null, + "negated": true, + "term": "Playlist Type", + "type": "term", + "value": "Client Send" + }, + { + "enabled": false, + "id": "c08e670a-e14a-46e2-9c60-c350f1302998", + "livelink": null, + "negated": false, + "term": "Playlist Type", + "type": "term", + "value": "Client Review" + } + ], + "favourite": true, + "hidden": false, + "id": "dc3b3e35-98ab-46be-a647-4688e035ee57", + "name": "Latests Playlists", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "e87fde28-02a2-4497-b0dd-8a18c2571a67", + "livelink": null, + "negated": false, + "term": "Playlist Type", + "type": "term", + "value": "Cinesync" + }, + { + "enabled": true, + "id": "29fe6a5a-39e6-49cd-a3a9-56d4318080fe", + "livelink": null, + "negated": false, + "term": "Playlist Type", + "type": "term", + "value": "Client Review" + }, + { + "enabled": true, + "id": "afee5849-49b0-489e-a62d-8126978c66bf", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "u1" + }, + { + "enabled": true, + "id": "762cff6d-a86e-45f0-9ad2-e0e9e4037925", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "u2" + }, + { + "enabled": true, + "id": "3c0712c9-b02d-40c6-b0cf-b9a322a5a534", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "u3" + }, + { + "enabled": true, + "id": "0dd830d3-1397-4ab3-9da3-d0c2f2711b62", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "u4" + }, + { + "enabled": true, + "id": "efd31421-7164-4d39-8266-91b2f82e93b1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "unit" + }, + { + "enabled": true, + "id": "8dae5933-0967-4f5f-84cc-4acfe2618244", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "review_proxy_1" + }, + { + "enabled": true, + "id": "05564619-07fa-4e2c-aff2-44958cbdfa10", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + } + ], + "favourite": false, + "hidden": false, + "id": "cac69aa6-2e63-46b3-9f8f-84b9c6add6fe", + "name": "Unit Playlists", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "8ddce99d-59cb-40d5-89f4-8ec3c794e3ab", + "livelink": null, + "negated": null, + "term": "Has Contents", + "type": "term", + "value": "True" + }, + { + "enabled": false, + "id": "dacec624-626e-4bd3-9fe4-87de245ee777", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "1 Day" + }, + { + "enabled": false, + "id": "05ca182a-8bf0-4e3e-aa93-a8838610ba1e", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Lookback" + } + ], + "favourite": false, + "hidden": false, + "id": "17cd0d67-314a-4717-a692-3df8baeb224f", + "name": "All Playlists (slow)", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "d06cbe61-d872-40a9-8a61-3a4541650477", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "Dailies" + }, + { + "enabled": true, + "id": "7e2273fa-ef3e-4a99-b41b-c77d3f6ad3b9", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "3 Days" + }, + { + "enabled": true, + "id": "1bfd9f20-d4e6-4d00-803a-aea9bc0b227c", + "livelink": null, + "negated": false, + "term": "Playlist Type", + "type": "term", + "value": "Dailies" + } + ], + "favourite": false, + "hidden": false, + "id": "1025cc97-f781-465c-9b99-3dbcff9510e6", + "name": "Dailies Playlists", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "723564f4-2e4e-42d0-aad5-6f1e824e5f94", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "Dailies" + }, + { + "enabled": true, + "id": "c70f80b5-9c95-4734-aa1b-9a3d21bfad88", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "3 Days" + }, + { + "enabled": true, + "id": "258c0b85-729f-43bb-ba63-655b34f8fafd", + "livelink": null, + "negated": false, + "term": "Playlist Type", + "type": "term", + "value": "Department Dailies" + } + ], + "favourite": false, + "hidden": false, + "id": "dcec749e-1767-4fd5-8334-190cdfa26858", + "name": "Dept. Dailies Playlists", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "96a99992-0e1d-4a3f-a999-22d6242a5cc8", + "livelink": null, + "negated": false, + "term": "Playlist Type", + "type": "term", + "value": "Cinesync" + }, + { + "enabled": true, + "id": "706c87d9-2629-4812-9115-37ba220234d1", + "livelink": null, + "negated": false, + "term": "Playlist Type", + "type": "term", + "value": "Client Review" + }, + { + "enabled": true, + "id": "33b22a7a-f802-4495-b2ae-2aaca5677196", + "livelink": null, + "negated": false, + "term": "Playlist Type", + "type": "term", + "value": "Reference Playlist" + }, + { + "enabled": false, + "id": "08d55677-e2ec-4970-af80-1d3fe15f2aa9", + "livelink": null, + "negated": false, + "term": "Playlist Type", + "type": "term", + "value": "Client Send" + }, + { + "enabled": true, + "id": "cee8728d-644c-430e-8eee-41ebe8bb8e93", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "master" + }, + { + "enabled": true, + "id": "a4afc6aa-d6cb-4bcf-a262-fa863038e318", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Has Contents" + } + ], + "favourite": false, + "hidden": false, + "id": "6cc0d544-adbf-408b-9f45-e2fc6f02157a", + "name": "Master Playlists", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "e09337c9-52dc-47bb-93a7-7c510eb7b32e", + "livelink": null, + "negated": false, + "term": "Playlist Type", + "type": "term", + "value": "Client Review" + }, + { + "enabled": true, + "id": "121f765b-4758-41c5-a147-21e6dc4e5e6e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "DNEG_VFX_" + }, + { + "enabled": false, + "id": "fdbfc8f4-2987-40b6-8aab-8b956bf183fc", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "DIRECTOR" + }, + { + "enabled": false, + "id": "28d85321-c391-4010-80c0-83aa2e066edb", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "ANIM_SUPER" + } + ], + "favourite": false, + "hidden": false, + "id": "afda9cef-ffa6-4a2a-a8e1-c5e5307669d2", + "name": "Supervisor Playlists", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "a1754b27-4912-4b6c-ae7a-6aa0f1b5d5f8", + "livelink": null, + "negated": false, + "term": "Playlist Type", + "type": "term", + "value": "Client Review" + }, + { + "enabled": false, + "id": "08a744ad-77d0-4aa4-9551-60e1ad9daffd", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "DNEG_VFX_" + }, + { + "enabled": true, + "id": "1c702d63-4c7b-428b-9505-4740a2563e2c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "DIRECTOR" + }, + { + "enabled": false, + "id": "d5964a74-9302-40f1-86a8-3026c40c32fe", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "ANIM_SUPER" + } + ], + "favourite": false, + "hidden": false, + "id": "bdbadc8d-a80f-4850-894e-69e0083c3fc5", + "name": "Director Playlists", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "b0ddcaa2-197c-405d-9882-4f83bf91246d", + "livelink": null, + "negated": false, + "term": "Playlist Type", + "type": "term", + "value": "Client Send" + }, + { + "enabled": false, + "id": "02d980cb-7b4b-4053-b36b-27bd76bf65f6", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "DNEG_VFX_" + }, + { + "enabled": false, + "id": "e31277b5-3c4e-4304-930d-3ab177679ba0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "DIRECTOR" + }, + { + "enabled": false, + "id": "b652dadb-f7de-4f2a-874d-0af1708e1d4d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "ANIM_SUPER" + } + ], + "favourite": false, + "hidden": false, + "id": "fce4a46f-ecb7-4221-8437-909bf43e83ef", + "name": "Client Sends", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "e411ccce-e80f-4698-9af5-5e60e42f0ecf", + "type": "presets" + } + ], + "entity": "Playlists", + "favourite": true, + "flags": [ + "System Group" + ], + "hidden": false, + "id": "b85255fd-ad54-4829-bae1-4eac422fc5b5", + "name": "Playlists", + "type": "group", + "update": false, + "userdata": "recent" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "2d915195-9b00-4a8a-aa1e-7ac3c48e3b02", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": true, + "id": "700a9d48-0be3-4614-b2c2-6100df2d9937", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/cdl" + } + ], + "favourite": false, + "hidden": true, + "id": "1e2fd402-cc8e-4c89-bd6d-2f0405671b83", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "1fbce276-cb52-4463-b82a-8e0cd4cac139", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "turnover" + }, + { + "enabled": true, + "id": "5ea2e2e1-63ef-4d27-ab05-4708115fe953", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + }, + { + "enabled": false, + "id": "11f2bd63-ee3e-4a66-9eb7-9807e4dee0f7", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + } + ], + "favourite": false, + "hidden": false, + "id": "dc3d82f2-9874-4453-acb2-43ff4fc07519", + "name": "Approved Cuts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2cb73172-c27e-4524-bb8d-14db292be8ad", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "turnover" + }, + { + "enabled": false, + "id": "da087ef9-ae76-4f85-a5fd-eaa3c61040a6", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + }, + { + "enabled": false, + "id": "9459aba6-f16a-4b92-80c2-07e7faef8051", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "minicut" + } + ], + "favourite": false, + "hidden": false, + "id": "5e01ddf0-5543-4a5b-aab6-04867486573d", + "name": "All Turnover Cuts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [], + "favourite": false, + "hidden": false, + "id": "39a9dde1-e9a7-4230-9cb8-7972c7e62492", + "name": "All Cuts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "ad37cff5-d88d-44c6-9268-dc4b53e68d75", + "livelink": false, + "negated": true, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + } + ], + "favourite": false, + "hidden": false, + "id": "c51b96c2-6e0e-41a1-94d3-d75ab8a704d4", + "name": "Artist Cuts", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "4e00004b-4bce-4e79-a1c7-c98a7cd02dd3", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Load Sequence", + "System Group" + ], + "hidden": false, + "id": "7cb214ce-e14c-4626-b2c4-76c188bf12b9", + "name": "Cuts (VFX)", + "type": "group", + "update": false, + "userdata": "recent" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "a967cfdd-99a1-4199-aa90-c7a46f78decb", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1000" + }, + { + "enabled": true, + "id": "5782ccaf-da09-40c4-9366-b990d2430619", + "livelink": null, + "negated": null, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": false, + "id": "02d05fc7-cd64-4c10-9824-7c51831128c2", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + } + ], + "favourite": false, + "hidden": true, + "id": "d7eedba5-fad3-4447-a7f4-87052357197d", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "6d2ee197-43f4-4154-834f-5e218787af07", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + }, + { + "enabled": true, + "id": "5ecb547b-988a-4f7d-bf7a-ed933e96f361", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "children": [ + { + "enabled": true, + "id": "43bcc955-975d-4473-9b3e-71596d0899c6", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^CUT_" + }, + { + "enabled": true, + "id": "736f2d0b-4575-4ef7-bc71-e0d3ed695b71", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "seq_turnover_" + } + ], + "enabled": true, + "id": "671ecf45-6a47-4336-a2ea-31ec92a4eae1", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "enabled": true, + "id": "c4cf7b26-c1c0-4c0f-a32b-c5115d9e8ecb", + "livelink": null, + "negated": null, + "term": "Viewable", + "type": "term", + "value": "True" + } + ], + "favourite": false, + "hidden": false, + "id": "90b45e88-7539-40f8-949e-f96ed0d9ede5", + "name": "Latest Turnover QTs", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "cd57cf54-4d7a-47b0-be7e-a8e7597221e9", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "041b62ae-3a11-4a6c-8ee6-611c0d756589", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "minicut" + } + ], + "favourite": false, + "hidden": false, + "id": "4cac8e60-d3a9-4d31-9d05-08cd4a025c80", + "name": "Latest Minicut Outputs", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "c014f205-c554-4dec-aac8-e10c1ae9deae", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "6008c3e3-0961-494b-9c8d-f51c2e359adc", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "bidding" + }, + { + "enabled": true, + "id": "d4c03b4c-865c-44ed-a5e6-f1c03611e4c4", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + } + ], + "favourite": false, + "hidden": false, + "id": "53b1adb2-cb8f-42cc-b5bc-1f306d428d00", + "name": "Latest Bidding QTs", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "eecc18e4-de13-4af9-80df-6c041c2fc8dc", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "037f791c-984e-4bf0-b549-b0895b3a4063", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "seq" + } + ], + "favourite": false, + "hidden": false, + "id": "7a1722dd-1f56-4107-97b3-8687edd57a5a", + "name": "Latest Previs", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "c6dfc8d2-4618-40ce-a90f-057a9358e42c", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "System Group" + ], + "hidden": false, + "id": "619d509d-4ca9-4791-963a-56212db55d3e", + "name": "QTs (VFX)", + "type": "group", + "update": false, + "userdata": "recent" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "20eec131-0722-4b29-bb09-c6bd35e3e085", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + } + ], + "favourite": false, + "hidden": true, + "id": "b4e83342-71dd-45df-8ec2-3f5b74c6f03f", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "a0bcafc6-a933-4f95-8233-590714b30647", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render" + }, + { + "enabled": true, + "id": "de9b0afa-eef9-401d-8f61-d6a9cc37adb9", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "14ce50db-3a8a-465a-a042-401dad722a08", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "c7e41322-289d-445d-b53e-7944b4717031", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/deep" + }, + { + "enabled": true, + "id": "b2564698-0e3c-40aa-b4d5-25138682d9d5", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "21c77d17-27c4-4adb-a956-b996cfe5670a", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "6341b729-d4c3-4f9e-a12b-471f0778fb46", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "59961350-271e-4837-851f-f86fb9751a5e", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "3284fb37-ea40-4539-88ab-489cd701ac23", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/wireframe" + }, + { + "enabled": true, + "id": "3f9b4a9c-2dbc-4216-9c1c-cd2b6b79c3dd", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "cut" + }, + { + "enabled": true, + "id": "7fd2dc0e-3eda-4db2-8b08-ca420d7a7682", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "abc968bf-ef0b-456a-920c-039f183496ad", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "video_ref" + }, + { + "enabled": true, + "id": "238afb6c-9881-4c3e-b5de-1884eac71908", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "video_ref_witness" + }, + { + "enabled": true, + "id": "455621cf-5c90-4137-b188-91f5b88c9549", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "ref/onset" + }, + { + "enabled": true, + "id": "3463be4d-5d9e-4051-872c-f209397cdf43", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/ref" + }, + { + "enabled": true, + "id": "a98f3f46-722d-481b-88fb-9cae846b9309", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "concept_art" + }, + { + "enabled": true, + "id": "03b3ca7c-7060-435e-ad64-64991dc40e26", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "ref" + }, + { + "enabled": true, + "id": "2cb5c81a-4d23-411e-b46d-1dfc8fdcb685", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "graphic_art" + }, + { + "enabled": true, + "id": "6170d717-3f5d-41d5-a9db-7883fe4c26ea", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "image/artwork" + }, + { + "enabled": true, + "id": "3adb6b4c-547f-4c0c-a757-89fd1ff485f8", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "image/storyboard" + }, + { + "enabled": true, + "id": "2c4b2209-ff3a-48ec-a9cf-8a176b76d119", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render" + } + ], + "favourite": true, + "hidden": false, + "id": "d2423374-859b-4ecc-9edc-a1b6e6eb4eee", + "name": "All Versions", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "d3e87349-cd54-46d0-9239-d6c60c39f5b2", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "73c207cb-5b4d-487e-9056-ad78fa46563f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "28fd9fb9-8abc-495e-b905-9e78298e44cd", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "3b5931bd-fd27-4251-bc5d-35003b4a026a", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "8f8e56a2-a5f7-4f66-ad04-172095145ed1", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "cbb04917-c82c-43d7-a2e6-a0a068efd730", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Comp" + }, + { + "enabled": true, + "id": "348e0c41-13d1-4b6a-82e5-06348bb3c1da", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "review_proxy_1" + } + ], + "favourite": false, + "hidden": false, + "id": "f7149213-e490-464e-9f42-7ddf42c81130", + "name": "Comps", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "3146f6bb-ab6e-4510-ad19-fdec87ca6bb0", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "0f6cc964-1d30-4c74-b945-3a8572a00ae8", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "24dcd11d-33cd-43b4-a047-877b913446c3", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "1f349edc-ffbf-4a9b-9f8f-d88810734ebe", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "e87697f6-c21e-4e8b-8f40-b80f700bad92", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "f1dcfb7f-958f-46d0-99fb-74dcfa90e873", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": false, + "id": "50ef942e-da46-4059-9337-d2792edf5d96", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "review_proxy_1" + }, + { + "enabled": true, + "id": "240f189c-c527-4164-a9ee-6ddd573dd51f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "anim_v" + }, + { + "enabled": true, + "id": "5ef65b5f-6ae0-42cc-a5d3-1a2c5312cdf0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "pip" + } + ], + "favourite": false, + "hidden": false, + "id": "82ac4a3d-c172-4daa-beb0-00a03b5f7edd", + "name": "Anim", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "02467636-ea64-4b7d-97ce-427d00a3671a", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "da081a3a-1376-42d2-aad9-7e88488d037d", + "livelink": null, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "SG Frames" + } + ], + "favourite": false, + "hidden": false, + "id": "fbaece6c-e91d-4aac-8a9a-56a99748d4bf", + "name": "CG Renders", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "276bdec5-7e52-4f04-b946-2e9666b67581", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": false, + "id": "1e6fa2a9-37e1-46e9-b60f-7e715d19e365", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": false, + "id": "29ad89f0-354c-442d-9db2-1120a8d2fa86", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "2008703e-c1f6-49ee-8724-32a15335290b", + "livelink": null, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "main_proxy0" + } + ], + "favourite": false, + "hidden": false, + "id": "e2be6fbd-98ec-41c5-8e6a-e77ebcc2e4e3", + "name": "2D Elements", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "d5685b81-7ba7-427c-8160-b074fc421c11", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Camera Track" + }, + { + "enabled": true, + "id": "22513f5e-fda3-4847-a98d-c31ebdd48c7a", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Body Track" + }, + { + "enabled": true, + "id": "250c79b6-38d0-43d9-9256-4e32e962dd35", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "3824f198-c584-4d05-99b4-8d894b886703", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "85c90ddf-4eee-4f02-86cd-2388387340a6", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "69ab5a89-f5f5-47e4-812c-177244331f76", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + } + ], + "favourite": false, + "hidden": false, + "id": "46df7ee1-8ce2-490d-9f63-5b74b5856330", + "name": "Camera & Body Tracks", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "ad5c2fd4-0119-4c92-9044-fbee536c450c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "370599f0-0a32-4065-baae-44ec1ceb11bc", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "ec2f5f5b-77e4-432a-ba2e-4c260b2af6ec", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "f78aaacf-8dc7-4193-b6c3-11aae92e90ce", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "77f80cc2-ced1-4ac6-bd03-e8280bfc7fd1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "retime" + }, + { + "enabled": true, + "id": "a736147a-0cd3-42cc-afb3-122a866a5344", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "repo" + }, + { + "enabled": true, + "id": "35274fb5-2450-468c-b530-533d98507911", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "3ca5e764-1d41-47c2-9895-5addae745ec7", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "rerack" + } + ], + "favourite": false, + "hidden": false, + "id": "bba9beff-f7cf-45fc-a68e-f4a7e7f1d830", + "name": "Retimes & Repos", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "cd108259-37cb-4f73-8e47-632d0a8d8ee3", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^S_" + }, + { + "enabled": true, + "id": "0a06da43-4cdd-41f6-a9d6-910cf991804c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": false, + "id": "b6955257-a1bd-4adc-81df-c83027e9b21c", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Scan" + }, + { + "enabled": false, + "id": "9b914963-9004-48f5-bc38-a7632ed35828", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Comp" + }, + { + "enabled": false, + "id": "fecaefce-3417-4983-939e-7b88192d72a4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "retime" + }, + { + "enabled": false, + "id": "ce97af87-058c-43c8-a9df-4b0023e055be", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "repo" + }, + { + "enabled": false, + "id": "dac0cd95-d2ea-4bcd-b0cd-6cde30345d3a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^REF_" + }, + { + "enabled": false, + "id": "5ba3e75f-68d1-4180-b8aa-2a645f4fb11d", + "livelink": null, + "negated": null, + "term": "Is Hero", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "b46fe4cf-eccd-420b-9bfc-365e7845039b", + "livelink": null, + "negated": true, + "term": "Pipeline Status", + "type": "term", + "value": "Declined" + } + ], + "favourite": false, + "hidden": false, + "id": "ea16bd8c-ad5f-4322-8898-a05554418474", + "name": "Scans", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "44d6a077-ab6e-44be-a7ef-f085c62a43af", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "edit_ref" + } + ], + "favourite": false, + "hidden": false, + "id": "e35efbdc-1b4f-4f1c-9a6c-b7b97a8b04fb", + "name": "Edit Ref", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "3cce03c6-2147-42f4-9272-55977459505b", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Dailies" + }, + { + "enabled": false, + "id": "f196ad02-1114-42ec-9405-ddc843a3e0e9", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + } + ], + "favourite": false, + "hidden": false, + "id": "5693511f-56a4-435a-8e8f-d7e2c632f218", + "name": "Sent To Dailies", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "b228401c-89f4-40c5-bec9-46342a653904", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + }, + { + "enabled": false, + "id": "3c92d000-c558-41c0-b313-76e7e8c38eed", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Comp" + } + ], + "favourite": false, + "hidden": false, + "id": "4cc534f4-3626-44c5-a558-5c3e28c1cb49", + "name": "Sent To Client", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "a07eab89-7672-4b9d-8559-57b1b6b20577", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "System Group" + ], + "hidden": false, + "id": "c8c5c2de-23ca-44ec-95c1-ea44384f43aa", + "name": "Versions", + "type": "group", + "update": false, + "userdata": "tree" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "8c0a2058-a7d2-4776-99c6-3ea2079a0215", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": true, + "id": "ee79b3a0-f903-4956-9f2e-5a42c5e0371c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/edl" + } + ], + "favourite": false, + "hidden": true, + "id": "b7a77c2d-efe6-4ba3-a5be-a060ee4c3678", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "239eb1ff-22fc-450f-aaaf-1dc0f575945e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "turnover_v" + }, + { + "enabled": false, + "id": "3994c1d5-a4c5-4752-a664-e477ed8935fc", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "turnover_eng" + }, + { + "enabled": true, + "id": "90f00922-14f1-49e9-8349-6647ec46f40c", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + } + ], + "favourite": true, + "hidden": false, + "id": "d7b60fad-b655-46cf-b187-2b817fc4a8b8", + "name": "Approved T/O Cuts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "bc23247d-be70-47e3-9d52-be2d3ff591be", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "turnover" + } + ], + "favourite": false, + "hidden": false, + "id": "cfc9304d-f102-4b16-adf5-cf1516c9ecb0", + "name": "All T/O Cuts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2dee7971-b4ef-44bc-af4b-4ea5544c417b", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + } + ], + "favourite": false, + "hidden": false, + "id": "292083f5-2f65-4d24-8782-1657d3a101b3", + "name": "All Editorial Cuts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [], + "favourite": false, + "hidden": false, + "id": "6b4b37f7-c600-47b2-83e5-6eedb8762107", + "name": "All Cuts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "27bb15d4-2bbb-49a8-b0a0-8b2fef410624", + "livelink": false, + "negated": true, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + } + ], + "favourite": false, + "hidden": false, + "id": "69eb3564-e3df-4f28-ad91-8ec50f5cb6d6", + "name": "Artist Cuts", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "71c983d9-731f-472d-95b4-6437cd179ab5", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Load Sequence", + "System Group" + ], + "hidden": false, + "id": "087c4ff5-2da0-4e54-afcf-c7914a247fae", + "name": "Cuts (VFX)", + "type": "group", + "update": false, + "userdata": "tree" + }, + { + "children": [ + { + "children": [ + { + "enabled": false, + "id": "97167ba0-96b6-4bcc-acfa-ef98c6af100d", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": false, + "id": "00202a20-0312-46b5-b06d-b019b726600c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/edl" + } + ], + "favourite": false, + "hidden": true, + "id": "9f349b78-e4de-43fb-b46d-31ea296cd751", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "6a72cd74-fe06-4634-b3b9-fbb098693b97", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + }, + { + "enabled": true, + "id": "8be0c776-3a00-47ec-ae03-2a1c5e68f4d6", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "minicut" + } + ], + "enabled": true, + "id": "dfc0afdb-afd2-47d1-a649-213060610415", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "77abb8be-4147-476b-ac4b-b39f18b18b6e", + "name": "All Editorial QTs", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "6a48f16e-3d4d-45dc-81a1-49fc5b628341", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "turnover" + }, + { + "enabled": true, + "id": "8575b272-6b41-4144-8083-597c3bd3106b", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "edit_ref" + }, + { + "enabled": false, + "id": "989b7220-d195-46fe-999f-0f222096864f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + } + ], + "favourite": false, + "hidden": false, + "id": "adaf07c1-d62e-43d0-8be3-645b5917005f", + "name": "Turnover & Edit Ref", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "9db20e65-5faf-4b4a-bac7-e1793f5e930e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "minicut" + } + ], + "favourite": false, + "hidden": false, + "id": "7238c33d-f858-4228-a719-b6b3ace1d569", + "name": "Minicuts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "4d1f775a-49cf-45b2-91de-8e07e605c4bb", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witcam" + }, + { + "enabled": true, + "id": "c772e0f8-3b6c-42aa-a097-33701ed19907", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "gesture" + }, + { + "enabled": false, + "id": "c189eae0-f19c-42dc-998a-c475d751567f", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + } + ], + "favourite": false, + "hidden": false, + "id": "96adfa18-1c63-475c-affb-2b90056d5dff", + "name": "Witness Cameras", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "85bb3fa4-59cb-40a6-8215-7ee85423289a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "previs" + } + ], + "favourite": false, + "hidden": false, + "id": "e1f64b1f-16a4-4ba3-ba0d-937618c4d9cb", + "name": "Previs", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "7e803de9-75bf-4ce3-8500-8c1961c5bd14", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "postvis" + }, + { + "enabled": true, + "id": "847cb248-b631-4729-b4a3-17f36b00b42e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "sketchvis" + } + ], + "favourite": false, + "hidden": false, + "id": "c5ad64bb-51aa-4351-b04c-0445f03c789b", + "name": "Postvis", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "80af1ce6-d61d-4d60-8c2f-3d6f0f4ecd20", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "bidding" + } + ], + "favourite": false, + "hidden": false, + "id": "f633473a-6c46-4173-a264-955b2882b3b0", + "name": "Bidding QTs", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "0ca04425-2262-482f-848f-b57872206359", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "client" + }, + { + "enabled": true, + "id": "acffad6b-3c28-4b84-91e7-b16a0ab1cec4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "send" + } + ], + "enabled": true, + "id": "b98fa325-efd0-4531-b7cc-ef31ff110a43", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "6f603d04-5a29-4efd-a2ad-08ec33691eed", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "client" + }, + { + "enabled": true, + "id": "0fe23ffa-9390-4d44-910b-c8836aa047ba", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "review" + } + ], + "enabled": true, + "id": "7f2909b7-a140-4d63-b6af-0fe86902b4fa", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "4a03e24b-feed-4339-a2e5-e5a85be6ae06", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "08ded7e2-deb7-4cae-98f5-0cf5fc7bd1cb", + "name": "To Client", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "a88a44dd-a892-4f4d-b88a-f5c6537ac441", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "client" + }, + { + "enabled": true, + "id": "70b76f7d-44ad-4204-94ff-8aef0be19291", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "send" + } + ], + "enabled": true, + "id": "ebcf0304-4c7f-4f12-af58-677b10ee5384", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "43d3816c-d1f1-46d9-ad20-41cd1f08ed2d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "client" + }, + { + "enabled": true, + "id": "dfbc8f30-995b-4f12-8a3b-cab8d391cd8d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "review" + } + ], + "enabled": true, + "id": "3919ba21-c14e-48ae-b45b-b6126ed63ae7", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": false, + "id": "605a27c0-654d-4fbd-927c-0856795bdf6c", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + }, + { + "enabled": true, + "id": "462b7ba7-545b-4b96-a078-f47dfdd968ba", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "animnote" + }, + { + "enabled": true, + "id": "af3bfe91-1ec7-4fef-9e6d-9fdacb855d60", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "layoutnote" + } + ], + "favourite": false, + "hidden": false, + "id": "507c265b-10ce-4a43-a97a-e88ad602cde5", + "name": "From Client - Notes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "5b4a2054-f743-4297-aff3-b288a6e5380c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "storyboard" + } + ], + "favourite": false, + "hidden": false, + "id": "354f4b31-1a2e-4199-9a6e-c838df85b4a9", + "name": "Storyboard QTs ", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "c2e07efc-60d4-4393-b0ec-4e8d9f67db02", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "dialogue" + } + ], + "favourite": false, + "hidden": false, + "id": "12e87d57-6f81-4b1f-8e26-b389f19875b6", + "name": "Lipstick Cameras", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "b0c1b7f1-2f46-4655-93cc-89a61822326d", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "System Group" + ], + "hidden": false, + "id": "9731813e-81e8-4e05-9e16-6e4a1dee5d5f", + "name": "QTs (VFX)", + "type": "group", + "update": false, + "userdata": "tree" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "830c1ff4-351b-4d21-98bd-3c7046292e6b", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": true, + "id": "0e2886c0-dd01-428c-85ca-b98b6e2c3851", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/edl" + } + ], + "favourite": false, + "hidden": true, + "id": "185689ad-4f7c-44f7-9973-17011c6162fe", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "5b2176b6-c6ab-4d6f-983c-3927d22bf1d9", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "turnover_v" + }, + { + "enabled": true, + "id": "16fa2bcc-f65f-4ec9-883b-a8f4cc14758a", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + }, + { + "enabled": true, + "id": "49c64efc-e129-42fe-b9fa-577a09dfdcce", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + } + ], + "favourite": true, + "hidden": false, + "id": "3d0a5a34-e286-4ee1-8f32-de0d6e711d9e", + "name": "Approved T/O Cuts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "1570bf8e-5251-4f71-95a4-9dcbc7558c92", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + } + ], + "favourite": false, + "hidden": false, + "id": "1c8e13a4-8ea3-4bf0-9c0d-e6415af2c3b6", + "name": "All Editorial Cuts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "39b69ecc-4258-4079-b6ac-b1dbcf413db5", + "livelink": false, + "negated": true, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + } + ], + "favourite": false, + "hidden": false, + "id": "601a9618-ef72-4791-b89f-fa31ccd47a89", + "name": "Dept. Cuts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "4633d5a1-56c3-4ead-8c10-66717688de30", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "editorial_ingest" + }, + { + "enabled": true, + "id": "05aa5414-eb58-4b1e-873e-e778ec3cef16", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + }, + { + "enabled": true, + "id": "9604c0e9-2732-4873-af58-a1f33b8d0f91", + "livelink": null, + "negated": null, + "term": "Preferred Sequence", + "type": "term", + "value": "shot_mixdown" + }, + { + "enabled": false, + "id": "571ddbb0-8aad-45ae-874f-38d0688f8843", + "livelink": null, + "negated": null, + "term": "Preferred Sequence", + "type": "term", + "value": "dneg_shot_mixdown" + }, + { + "enabled": false, + "id": "787138d2-a4c5-44ee-ab1f-afc7c9519c2d", + "livelink": null, + "negated": null, + "term": "Preferred Sequence", + "type": "term", + "value": "extracted_otio" + } + ], + "favourite": true, + "hidden": false, + "id": "cf8afae6-b5fd-4f6b-b0e8-471f4ebff8fd", + "name": "EDL RLO Ingest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "db397c66-07a2-433b-9139-a3f90f0240f8", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "previs_editorial_v" + }, + { + "enabled": true, + "id": "2b346764-1597-42e3-a93e-af8162929ad7", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + } + ], + "favourite": false, + "hidden": false, + "id": "06409c4a-3776-493f-aa56-cf91c78efb63", + "name": "EDL Anim T/O", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [], + "favourite": false, + "hidden": false, + "id": "a19d4545-3238-4bd4-b2ce-4cc6137329c0", + "name": "All Cuts", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "d5bbb488-fcb5-4282-9d5a-2f69af1f3d2f", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": false, + "flags": [ + "Load Sequence", + "System Group" + ], + "hidden": false, + "id": "8175aaef-bdf7-42b7-8cc2-ce8c8dca7422", + "name": "Cuts (FEAT)", + "type": "group", + "update": false, + "userdata": "tree" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "01bd5c64-5799-4f58-96c7-f0ba256c4c64", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": true, + "id": "c433290c-09dd-4b76-9fc9-26aef24a66f7", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/cdl" + } + ], + "favourite": false, + "hidden": true, + "id": "92352b80-ffb5-45fb-accf-6cda02d66f0f", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "fedce580-2572-44a3-8f39-b4f7a1032987", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "turnover" + }, + { + "enabled": true, + "id": "9c745448-1f55-4f8b-b3ba-3b669f924a47", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + }, + { + "enabled": false, + "id": "4ac12db2-b237-4036-b898-ba26c75f098a", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + } + ], + "favourite": false, + "hidden": false, + "id": "93a267f0-656e-4c50-8d5c-db5ce11568ef", + "name": "All Turnover Cuts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [], + "favourite": false, + "hidden": false, + "id": "7c061b9b-845d-4f99-9117-e223aceffbc4", + "name": "All Cuts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2cf3318f-0cf9-4e79-b739-93db81d01999", + "livelink": false, + "negated": true, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + } + ], + "favourite": false, + "hidden": false, + "id": "56ee3aa8-8a6b-4d46-8afe-a8e4e1fa4f3e", + "name": "Dept. Cuts", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "14799199-97d8-42b4-b321-d1d9293102be", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": false, + "flags": [ + "Load Sequence" + ], + "hidden": false, + "id": "f127f93b-fefc-4272-9652-6e43130a74ce", + "name": "Cuts (FEAT)", + "type": "group", + "update": false, + "userdata": "recent" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "2dbffa8c-6c11-453c-b4f0-8d5f8b0760b3", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1000" + }, + { + "enabled": true, + "id": "ee6b5df2-404d-4401-9939-37b04ad95bf1", + "livelink": null, + "negated": null, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": false, + "id": "67d3c040-8dce-43e6-8e32-a4e4c71b27b1", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + } + ], + "favourite": false, + "hidden": true, + "id": "483ebf7c-b8a6-4d54-b2ca-272732e026bf", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "f2c10d52-61d3-418d-8c88-99471c7d2ae2", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "turnover" + }, + { + "enabled": true, + "id": "85c615c4-fa50-47da-b3bf-40460b423eda", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + }, + { + "enabled": false, + "id": "4bf46ad8-78e9-4b65-bb9e-50054bb86509", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + } + ], + "favourite": false, + "hidden": false, + "id": "02d9e404-57a5-48ce-b7f9-9ed2a273dfc8", + "name": "Latest T/O QTs", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "47bad3a8-3f1e-4dc2-8232-ca5e726f3c2a", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "91cad633-d569-4b75-be75-9f07b1a90c39", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "storyboard" + }, + { + "enabled": true, + "id": "ea94f8d7-2f25-4c94-9271-649136ba9f3f", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "edit_ref" + } + ], + "favourite": false, + "hidden": false, + "id": "03d62b00-9db5-4d06-a6c8-bee860936794", + "name": "Latest Storyboards", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "0eeb95e4-6ba8-418b-80d5-db1c90dfdb55", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "dialogue" + }, + { + "enabled": true, + "id": "e43d1dfb-ab3c-46af-a6e4-446eb0c18ba5", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + } + ], + "favourite": false, + "hidden": false, + "id": "504b2b06-c68a-451c-a08c-514ee5d1dd34", + "name": "All Lipstick Cams", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "4a2b6379-1fef-46da-a9bd-bf20f0afc338", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": false, + "flags": [], + "hidden": false, + "id": "f558b2dd-b045-46a1-80aa-9a4515887625", + "name": "QTs (FEAT)", + "type": "group", + "update": false, + "userdata": "recent" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "33683243-526f-4090-abae-7a861fec18a8", + "livelink": null, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + } + ], + "favourite": false, + "hidden": true, + "id": "16ed5522-8a35-4849-bda2-ad98f1e7ccff", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "cb5ce114-73a3-44fb-b6f0-a065086a6b5d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^ARTCON_" + } + ], + "favourite": false, + "hidden": false, + "id": "5da432ec-e017-4b8d-9bbe-cd199bdaacbf", + "name": "Concept Art", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "617c8bcd-d7d5-43a6-879e-37ab4a5c0fb2", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^VIDREF_" + }, + { + "enabled": true, + "id": "b24657d8-0b68-44ec-8bdc-f76fb2a6a7ad", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1000" + }, + { + "enabled": false, + "id": "97b3dadd-fbe4-4346-88da-b8cd6b45c352", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "0fbaa95f-9d1f-4c2b-9b05-4d7e1a8a27b8", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + } + ], + "favourite": false, + "hidden": false, + "id": "d3813842-e11f-43e3-b6b6-e36ff62c6abf", + "name": "Video Reference", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "440fa63f-2e91-44f0-a201-b836128d9997", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^HDR_" + } + ], + "favourite": false, + "hidden": false, + "id": "84683d75-0f02-46e1-a150-cb109f05ceeb", + "name": "HDRI", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "026c0e71-b051-4750-936e-6831dd75f931", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^WIT" + }, + { + "enabled": true, + "id": "b4ac4130-04b5-4425-a593-f3e369ee72d4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witcam" + } + ], + "favourite": false, + "hidden": false, + "id": "0ee25a3b-758b-4ef5-b2fd-9ef04273b414", + "name": "Witness Cams", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "1e2ad668-760b-450f-86f1-2b1ad2fad46b", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^TEXREF_" + } + ], + "favourite": false, + "hidden": false, + "id": "10d4d0f1-4c5f-4a44-b74e-c2b847b7ab74", + "name": "Texture Reference", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "f29aea18-7a4f-4624-a03c-9b846c07b8f2", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^REF_" + }, + { + "enabled": false, + "id": "19c9be17-aea2-4a80-9564-60683e29ed6a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "REF_" + }, + { + "enabled": false, + "id": "39ec09b9-1996-400f-9d66-f7dd1a49cc2f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/ref" + } + ], + "favourite": false, + "hidden": false, + "id": "5f9161d2-b249-4aa4-aff7-2cbcaab44827", + "name": "Reference", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "4a5ba67f-3fd1-46f0-9ea8-9aa0cbd9b5d2", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "System Group" + ], + "hidden": false, + "id": "dfca8981-e759-4ccc-bdff-95019200f1f9", + "name": "Reference", + "type": "group", + "update": false, + "userdata": "recent" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "beb6abcc-4771-4c9b-a6d0-5914a6e3a8ba", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": false, + "id": "abeefd80-6fed-474e-a4e1-3a98c444008f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/edl" + } + ], + "favourite": false, + "hidden": true, + "id": "480b2559-e997-4796-bd52-39c1fd5cebb3", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "5238618f-0de5-45ab-97b7-c5860d617267", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + }, + { + "enabled": true, + "id": "b862f7b8-9f5b-49af-b4d6-9df561c09342", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "minicut" + } + ], + "enabled": true, + "id": "fe8af23a-2831-427e-b35f-3ee9044f77a4", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + }, + { + "enabled": true, + "id": "6f311947-066d-401b-9f81-2015e9b5d86e", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + } + ], + "favourite": false, + "hidden": false, + "id": "21717b4c-43a7-433f-858d-06ff84fd3a9c", + "name": "All Editorial QTs", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "3d7317c8-a3f7-4c85-b40f-b5cf3a9b2770", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "turnover" + }, + { + "enabled": true, + "id": "21f06f06-f110-4176-9705-5c81f9fa9b44", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "edit_ref" + }, + { + "enabled": false, + "id": "0d755b91-470d-476b-baf4-abad624730a2", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + } + ], + "favourite": false, + "hidden": false, + "id": "d8a2d04a-3b99-42ff-b76d-b4eee3e9d354", + "name": "Turnover & Edit Ref", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2004cc1c-d7ab-4957-8fa4-dedbbd9e8ada", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "minicut" + } + ], + "favourite": false, + "hidden": false, + "id": "8af94192-0849-4a20-9cab-a49d4ac9abda", + "name": "Minicuts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "f39de61c-02d6-4c73-a051-db99a515aa4f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "client" + }, + { + "enabled": true, + "id": "1385b062-a47b-4898-b4fd-f5fdcbf18256", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "send" + } + ], + "enabled": true, + "id": "6ec73afa-05df-40f1-ae22-3164eb07c1b7", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "ebb43072-2e43-4b68-a76e-b6b3dd76f647", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "client" + }, + { + "enabled": true, + "id": "a8d03e4a-0885-4383-9191-42fe7c6325db", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "review" + } + ], + "enabled": true, + "id": "f488aa4b-da39-417b-88be-ad9b972fc5e2", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "820d21c9-6825-4650-9bd5-ee6902ec8f4d", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "45e2306a-cc9a-454d-87c7-12796e09730c", + "name": "To Client", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "631d7121-36f2-4170-998f-4698e8f18cf2", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "client" + }, + { + "enabled": true, + "id": "30086b68-61a0-4ce0-b33d-b6d3e48026c6", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "send" + } + ], + "enabled": true, + "id": "dfe8fd7a-c042-4886-9af9-512fca199ac3", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "f843179a-0ad5-4120-be0e-9cefea1b5502", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "client" + }, + { + "enabled": true, + "id": "fdfe3255-9dcb-48d9-8c38-9ffee5ae9f84", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "review" + } + ], + "enabled": true, + "id": "2d181117-ee52-4b86-9d1d-8039e5895111", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": false, + "id": "5dd66dac-5a8f-4d53-ae5a-5de5f1abc53e", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + }, + { + "enabled": true, + "id": "5b30edd5-7fc7-42bb-b744-b7f3d276ddfd", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "animnote" + }, + { + "enabled": true, + "id": "3dde6b07-8c39-471c-a6f8-3c345bcbb716", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "layoutnote" + } + ], + "favourite": false, + "hidden": false, + "id": "65d7f01f-26ac-4452-974e-e509a049b1d5", + "name": "From Client - Notes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "c2f8625b-c4a6-47ea-9835-2151c651cf5b", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "storyboard" + } + ], + "favourite": false, + "hidden": false, + "id": "775cc752-0d4b-4656-8a35-b1a400106d8b", + "name": "Storyboard QTs ", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "8af67d65-2f55-4a9f-adae-90d2ae66c40e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "dialogue" + } + ], + "favourite": false, + "hidden": false, + "id": "9f3a4680-0a41-4489-a446-d5842b1c6b37", + "name": "Lipstick Cameras", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "87af5ff7-9019-4ea3-8673-3e5d036966df", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": false, + "flags": [], + "hidden": false, + "id": "49b10c46-af0d-42a8-b4cd-cad7cfa12bcc", + "name": "QTs (FEAT)", + "type": "group", + "update": false, + "userdata": "tree" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "45c5fb78-3521-4990-be8e-d1453d5270c3", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1000" + } + ], + "favourite": false, + "hidden": true, + "id": "dfd7b1c1-3ed3-4af7-a480-3f901d7dfbcf", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": false, + "id": "0e69a7d0-db4e-4ad9-98e7-46eee0d38845", + "livelink": null, + "negated": true, + "term": "Note Type", + "type": "term", + "value": "Director" + }, + { + "enabled": false, + "id": "ce55d610-c0a7-4060-8779-c1e3c35f3da1", + "livelink": null, + "negated": true, + "term": "Note Type", + "type": "term", + "value": "Client" + }, + { + "enabled": false, + "id": "669e5fc9-b830-4369-bd42-f787bfecd161", + "livelink": null, + "negated": true, + "term": "Note Type", + "type": "term", + "value": "Editorial" + }, + { + "enabled": false, + "id": "7301097d-c469-4cdb-8f11-3931a69ee08c", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "200" + } + ], + "favourite": false, + "hidden": false, + "id": "ede1a2c4-af8b-4107-8019-8e4c9a76339b", + "name": "All Notes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "6ac818ed-506d-4d7c-85d6-985c49c3f967", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Director" + }, + { + "enabled": false, + "id": "aa61cd15-5755-4228-8756-b63c1bbddb52", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Client" + }, + { + "enabled": false, + "id": "30879422-f08c-4039-9a87-3db0cb100c1e", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Editorial" + }, + { + "enabled": true, + "id": "7135b42c-2fe4-41ce-9e2b-718103130cf4", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "500" + }, + { + "enabled": false, + "id": "cc21b2a4-9a8b-4d8b-88bf-5384067f7b60", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "20 Days" + }, + { + "enabled": true, + "id": "f993b520-ffde-41db-9370-9e7ab72d8311", + "livelink": false, + "negated": null, + "term": "Recipient", + "type": "term", + "value": "${USERFULLNAME}" + } + ], + "favourite": false, + "hidden": false, + "id": "d4920a8f-9570-4e2c-bbf7-f94dde82854e", + "name": "Notes To Me", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "ba182486-a65b-4a97-9bda-05a42a94ae55", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Director" + }, + { + "enabled": false, + "id": "bf2c43c6-d8ff-4731-9a91-3eaaf9cfb7bf", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Client" + }, + { + "enabled": false, + "id": "652be906-0dbe-4419-9905-2921a776a9d9", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Editorial" + }, + { + "enabled": true, + "id": "3cc67f0c-2f33-407c-bbe6-dbb924ae88e5", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "500" + }, + { + "enabled": false, + "id": "d1181502-cc4c-4dbc-9edc-d9b9c904f0f3", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "20 Days" + }, + { + "enabled": true, + "id": "e41fed0c-c8bc-4bc2-ae47-7cf8ade10a08", + "livelink": false, + "negated": null, + "term": "Author", + "type": "term", + "value": "${USERFULLNAME}" + } + ], + "favourite": false, + "hidden": false, + "id": "528583ec-94ff-4e93-a872-22b400687104", + "name": "Notes From Me", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "54c643f2-bd1f-49a7-b886-8234999c9eff", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Director" + }, + { + "enabled": true, + "id": "e2c6e343-c1a7-4a2e-9a95-af4bdabf6ded", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Client" + }, + { + "enabled": true, + "id": "b0caa48d-5562-4731-8cd6-86a845b877da", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Editorial" + }, + { + "enabled": true, + "id": "a8ef5500-a654-47be-9c5a-e8af94f98868", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "500" + }, + { + "enabled": false, + "id": "777e87c7-2cd8-43c5-ae61-1c455f3bcfc6", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "20 Days" + } + ], + "favourite": false, + "hidden": false, + "id": "f3c6530e-f5fa-4aa2-bea3-e83e528d097c", + "name": "Client Notes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "44eb7e82-7b72-4b4a-a570-0479193b6b78", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Director" + }, + { + "enabled": false, + "id": "00c8475c-631c-4458-be1c-eff1813273e8", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Client" + }, + { + "enabled": false, + "id": "1dd9df64-8403-4953-8038-56ad13e9f4bd", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Editorial" + }, + { + "enabled": true, + "id": "7e1108f3-f87f-472e-ba84-d77d9cc8bcd6", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "500" + }, + { + "enabled": false, + "id": "652ea17c-dda5-49d8-9ae6-058b5903fc85", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "20 Days" + } + ], + "favourite": false, + "hidden": false, + "id": "fda5e46e-aa41-4761-ada0-dd24960a0ed6", + "name": "Director Notes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "4eb4161d-5ff3-4128-bd88-2fb8c44a754b", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "VFXSupe" + }, + { + "enabled": true, + "id": "e09f841e-c52d-4e4b-9972-586672b6f6bb", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "DFXSupe" + }, + { + "enabled": true, + "id": "69d8e122-0bee-4940-8c6d-fa8acdeb7a0c", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "CgSupe" + }, + { + "enabled": true, + "id": "b4f2c43a-68e3-4d4f-b13d-54c6cb27e4b3", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Anim DIR" + } + ], + "favourite": false, + "hidden": false, + "id": "112aadfa-cc44-443e-b6b3-7761c587d794", + "name": "Supervisor Notes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "4a95bea7-6c46-43a8-bc93-d305d7f0b63f", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "VFXSupe" + }, + { + "enabled": true, + "id": "f955b20b-8bfc-416a-940d-b6ae1a25f5b4", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "DFXSupe" + }, + { + "enabled": true, + "id": "9279096b-fe1f-4307-95eb-d6b8bf795b39", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "CgSupe" + }, + { + "enabled": true, + "id": "ea90f8d4-bcb7-4027-9de4-7070de999e4b", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "200" + } + ], + "favourite": false, + "hidden": false, + "id": "f2932cf8-3b26-4dc5-bb6b-4a2693cfa7c4", + "name": "Internal Notes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "94ad9d84-7c5c-4621-809e-47210753b77a", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Anim DIR" + } + ], + "favourite": false, + "hidden": false, + "id": "d3b848b9-248c-4c48-b843-82e86c3b5228", + "name": "Anim Director Notes", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "f6c99361-12af-4ad8-a110-a11408274d61", + "type": "presets" + } + ], + "entity": "Notes", + "favourite": true, + "flags": [ + "System Group" + ], + "hidden": false, + "id": "5dd3137d-1464-4433-bbd8-1f53626c26b7", + "name": "Notes", + "type": "group", + "update": false, + "userdata": "recent" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "b7066b47-891f-48a6-b1df-2c2b5517cb60", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + } + ], + "favourite": false, + "hidden": true, + "id": "4e62e3eb-e210-4d9d-9594-11ae45dc32df", + "name": "Notes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [], + "favourite": false, + "hidden": false, + "id": "170ab49e-d3ab-4e1a-a088-2dc581468adb", + "name": "All Notes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "a6f88d29-df0d-46a5-b5a1-203a90851345", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Director" + }, + { + "enabled": true, + "id": "641244d2-72b1-417e-a12b-eb3e57282abb", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Client" + }, + { + "enabled": true, + "id": "c1307348-9bae-4217-833a-f50174db077b", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Editorial" + } + ], + "favourite": false, + "hidden": false, + "id": "2bda49f8-0db9-4a5d-a69a-486893fb3ecd", + "name": "Client Notes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "3479ea6f-a9a3-468c-9d4f-1a9c2fac7f3c", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Briefing" + }, + { + "enabled": true, + "id": "1142423a-f9e0-474e-8704-27ab4195b2a7", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Show Brief" + } + ], + "favourite": false, + "hidden": false, + "id": "b8c27210-2070-4593-81ba-99edead68ef1", + "name": "Briefing Notes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "77d7dc65-5356-4401-91e0-61fb4a4afe55", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "VFXSupe" + }, + { + "enabled": true, + "id": "e618a745-f3ba-47bc-af72-c676a22ed6b7", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "DFXSupe" + }, + { + "enabled": true, + "id": "1fa45b53-12f4-48f9-832c-8f79163cc791", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "CgSupe" + }, + { + "enabled": true, + "id": "afad2537-78e0-418d-bbc2-05b3179fa3f1", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Anim DIR" + } + ], + "favourite": false, + "hidden": false, + "id": "199124f0-faa9-4cba-b375-eacda8e303ca", + "name": "Internal Supes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "02c0b0d3-f9d4-4319-ab7a-971609494723", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "DeptSupe" + } + ], + "favourite": false, + "hidden": false, + "id": "7064342a-c135-488c-9ded-13915eaeabc2", + "name": "Department Notes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "434ec257-0d4f-4e68-ba72-fc5ab59d26bb", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Facility" + }, + { + "enabled": true, + "id": "552cc89f-3c20-4367-a68a-49d9169f81b0", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Submission" + } + ], + "favourite": false, + "hidden": false, + "id": "4f60137e-e7e4-4298-8b0c-5107e5c84c09", + "name": "Facility Notes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2b3356e8-8861-409e-87b4-6953ab2c2585", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Wizard Dailies" + } + ], + "favourite": false, + "hidden": false, + "id": "f0322215-8d82-4870-8974-b89c53897ffb", + "name": "Artist's Notes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "214ed442-71b8-4e8a-b874-55c343e0a05c", + "livelink": false, + "negated": null, + "term": "Recipient", + "type": "term", + "value": "${USERFULLNAME}" + } + ], + "favourite": false, + "hidden": false, + "id": "46540248-cc0b-41e7-9e02-9d6eba4bfef6", + "name": "To Me", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "5e588683-e450-4c07-8865-4ba7826be84b", + "livelink": false, + "negated": null, + "term": "Author", + "type": "term", + "value": "${USERFULLNAME}" + } + ], + "favourite": false, + "hidden": false, + "id": "fcd2e23c-3b4b-4306-9031-c2829b44106f", + "name": "From Me", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "394165e3-859f-4218-bccc-c64afae20fd9", + "type": "presets" + } + ], + "entity": "Notes", + "favourite": true, + "flags": [ + "System Group" + ], + "hidden": false, + "id": "4262d5ac-7732-4e5a-9092-6bb31369d119", + "name": "Notes", + "type": "group", + "update": false, + "userdata": "tree" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "5040dcfb-336c-4ac5-a607-4a0b98cacecb", + "livelink": null, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + }, + { + "enabled": true, + "id": "72ff3917-828c-44c8-a276-489b9a270bec", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + } + ], + "favourite": false, + "hidden": true, + "id": "55009e65-58ee-41e5-9503-bf6fdb012551", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": false, + "id": "3f38d30c-f5e5-4da3-8168-c95dd33ea683", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^REF_" + }, + { + "enabled": true, + "id": "c58fc4d0-7730-488b-a029-982a8881cd17", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "REF_" + }, + { + "enabled": false, + "id": "7b8c6f04-155f-43c0-9123-1b5c5e4a5f7b", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/ref" + } + ], + "favourite": true, + "hidden": false, + "id": "8472a7e8-45a9-44f3-8731-b88a5a18cf55", + "name": "Reference", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "43be422c-f0de-4eaa-b86d-c55eea7a9fe0", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "art" + }, + { + "enabled": false, + "id": "a89a15bb-3802-42d2-828e-31db97c1208f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "concept_art" + }, + { + "enabled": true, + "id": "18c10ea7-e218-40a8-b9f6-7c4775a709ed", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^ARTCON_" + } + ], + "favourite": false, + "hidden": false, + "id": "ebcc8123-4670-4535-8c87-37d380af0fda", + "name": "Concept Art", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "49b6edc4-913b-4533-bc2a-2d68dcf0829d", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "AUD" + }, + { + "enabled": true, + "id": "87745bd4-9a57-4185-a00b-9bf97f832e95", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witcam" + }, + { + "enabled": true, + "id": "942cb425-3f11-4346-9222-42d32e9a2dff", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "_dx" + }, + { + "enabled": true, + "id": "26067442-c2de-4bf7-9cb6-173e3470ccd2", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + } + ], + "favourite": false, + "hidden": false, + "id": "24a25b0b-6099-4f9f-9fce-7ba7f3f5fdd0", + "name": "Witness Cams", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "5e292ff5-4c22-43ec-b1d3-c2471720ca19", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^OSREF" + }, + { + "enabled": false, + "id": "d7f86bc5-0f5c-4895-bc08-72452346acdd", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "texture_ref" + }, + { + "enabled": false, + "id": "d8cb70e3-36aa-4cef-bec2-049b59c8f080", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "video_ref" + }, + { + "enabled": false, + "id": "d322cd37-b6b1-4f8f-9286-732dfc7d3e04", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "video_ref_witness" + } + ], + "favourite": false, + "hidden": false, + "id": "c1411e93-b27c-461a-8fbe-e56039fdd700", + "name": "On Set Ref", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "2aad577c-6496-4c99-bfd7-d6aaec5a7ddd", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "" + }, + { + "enabled": false, + "id": "1cddb1d7-9b2f-4c44-8957-1b7dc6eeaa28", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "texture_ref" + }, + { + "enabled": false, + "id": "b0f9d4d9-5614-4664-a26f-21981618e5bd", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "video_ref" + }, + { + "enabled": false, + "id": "f1402913-da27-4ba1-a18c-d651ffd48251", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "video_ref_witness" + }, + { + "enabled": true, + "id": "880fc59c-45f2-49f9-aead-8c82a3aa1c72", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "tex" + } + ], + "favourite": false, + "hidden": false, + "id": "4efb3ec4-a722-4064-8699-ffd681cedda7", + "name": "Texture Ref", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "1620b2ab-6572-4d6e-80c5-076486b3ff48", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "hdr" + }, + { + "enabled": false, + "id": "9345e2bd-76e3-4fa3-91d3-60e6918055ce", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "video_ref" + }, + { + "enabled": false, + "id": "1cd70537-1017-4e6f-9459-8fd35d3db7f1", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "video_ref_witness" + }, + { + "enabled": false, + "id": "4a5cbce3-22d0-4328-b57f-bf05baa359ac", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "tex" + }, + { + "enabled": true, + "id": "6245523c-b795-4cb5-8d16-3b5712850991", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "hdri" + } + ], + "favourite": false, + "hidden": false, + "id": "c6c5f383-fb47-4274-acac-be0aef3d1d1b", + "name": "HDRIs", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "9a28ca83-4092-492a-9d19-1d1398619f06", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "System Group" + ], + "hidden": false, + "id": "1318d120-a016-448f-8537-14b654fa430d", + "name": "Reference", + "type": "group", + "update": false, + "userdata": "tree" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "30829520-0591-4b83-aff3-8efada3f1de6", + "livelink": true, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "" + }, + { + "enabled": false, + "id": "6df47cc3-78f7-46cf-9645-21ffe7a9bf70", + "livelink": null, + "negated": null, + "term": "Flag Media", + "type": "term", + "value": "Orange" + } + ], + "favourite": false, + "hidden": true, + "id": "5a8fef93-a302-4eac-a6dc-758d0708290a", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "79fc54d2-8835-49a8-9430-3001c08b7f97", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "Artist" + }, + { + "enabled": true, + "id": "d4c76cd3-4d2f-4a97-ab23-087e925b9231", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "Submission" + }, + { + "enabled": true, + "id": "1b4b193a-0eba-4d5d-807d-4d02c7238b54", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "Wizard Dailies" + } + ], + "favourite": true, + "hidden": false, + "id": "0543a552-2cd5-41d6-87ee-0df879fdfb87", + "name": "Artist", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "5aa514f8-fe7b-4e15-8bcf-d0b2a0e58d63", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "DeptSupe" + } + ], + "favourite": true, + "hidden": false, + "id": "4e9a9127-9591-4056-af99-c160c854812f", + "name": "Dept", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "7dd3282d-703a-4738-b94b-9c9b37759cd7", + "livelink": true, + "negated": null, + "term": "Version Name", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "e84965d2-e17d-47df-ada2-b328b904be7e", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "2DSupe" + }, + { + "enabled": true, + "id": "f39536f6-35ea-488c-b026-f6868311b025", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "Anim DIR" + }, + { + "enabled": true, + "id": "63711d12-0709-4421-936f-21249ee4e773", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "CgSupe" + }, + { + "enabled": true, + "id": "65d69f6f-2934-4dd3-a509-9fcc175000b7", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "DFXSupe" + }, + { + "enabled": true, + "id": "b7fe8e27-e6a5-44c0-bbb8-0f3f31046af0", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "DeptSupe" + }, + { + "enabled": true, + "id": "f3c8f2b8-b1eb-473d-9f32-66facdb469a7", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "VFXSupe" + } + ], + "favourite": true, + "hidden": false, + "id": "1c9de6ef-f9b0-48b6-a26f-30d4a66950db", + "name": "Supes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "540f9873-8f26-424d-ac6c-d9c297caa128", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "Client Editorial" + }, + { + "enabled": false, + "id": "bc2fd877-604b-498b-aa0d-1f6b7b0354d7", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "Editorial" + }, + { + "enabled": false, + "id": "22bb99f3-82ea-4112-92c8-b2860ed496cd", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "Editorial Query" + }, + { + "enabled": true, + "id": "22055fb8-6582-4fbe-861e-915a5453b7ed", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Facility" + } + ], + "favourite": true, + "hidden": false, + "id": "31980c5d-1d88-4a9f-adbd-f6639b62faf1", + "name": "Facility", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "682ba215-44b2-4b9a-982f-3b10577af11e", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "Client" + }, + { + "enabled": true, + "id": "5e32fd1f-3eff-4cce-8fb2-339e62a5e69f", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "Client Stereo" + }, + { + "enabled": true, + "id": "596c4077-08f2-43c2-b547-8d9c649edf0d", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "Director" + }, + { + "enabled": false, + "id": "d719e516-3696-4794-8a3d-c261dfa696d8", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Show Brief" + } + ], + "favourite": true, + "hidden": false, + "id": "5cc27243-80e7-4a9c-8166-cb0ebf970d29", + "name": "Client", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "fa1e3376-c5e7-4fc5-b5a4-a9d97b8dbbdc", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "Client" + }, + { + "enabled": false, + "id": "e8368eb2-2501-40d4-a46e-22484aa97c44", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "Client Stereo" + }, + { + "enabled": false, + "id": "a9737660-b0af-448c-b9ff-c5763d348d7a", + "livelink": null, + "negated": null, + "term": "Note Type", + "type": "term", + "value": "Director" + }, + { + "enabled": true, + "id": "f8336766-eff2-49a3-baee-2089518289de", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Show Brief" + }, + { + "enabled": true, + "id": "2d2e2b34-250a-4ebe-920a-5b04f4709d0c", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Briefing" + }, + { + "enabled": true, + "id": "f1eb8be8-658a-4a67-918e-23651635b623", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "Breakdown" + }, + { + "enabled": true, + "id": "0298d160-f656-485b-a04e-9400148a8a78", + "livelink": null, + "negated": false, + "term": "Note Type", + "type": "term", + "value": "" + } + ], + "favourite": false, + "hidden": false, + "id": "ae62301b-a42d-4e88-bdfa-adefb9779e30", + "name": "Brief", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [], + "favourite": true, + "hidden": false, + "id": "2ce0a044-fdd2-495e-a093-5ae4872b9ae5", + "name": "All", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "c9855f42-7095-4bfc-b0c8-b9ca842fa35f", + "type": "presets" + } + ], + "entity": "Notes", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Note History Type", + "System Group" + ], + "hidden": false, + "id": "821b8e77-a8ef-42ea-8c48-0b9c65fc28d0", + "name": "Note History Panel (Type)", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "3509e6c0-3662-49bc-8054-e38d55eb3b1d", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "data/clip/cut" + }, + { + "enabled": true, + "id": "2bd3057e-0eed-4b6b-9d26-06da62b890f9", + "livelink": true, + "negated": false, + "term": "Sequence", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "e95e9a5c-9294-45f6-b681-e7a421cf266a", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": false, + "hidden": true, + "id": "9e7d7c56-12ea-4d99-bcec-defc24591ed5", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "dd6c0019-1e98-48f8-9da6-c9f83c68278a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "turnover" + }, + { + "enabled": true, + "id": "4a5dea39-2047-4722-b228-93f8c8aee2c1", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + } + ], + "favourite": true, + "hidden": false, + "id": "cc520e16-e076-431c-9390-70f2c1bcae90", + "name": "Latest Approved Turnover", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "ededda2e-3d88-4fc9-9b65-7f0b1621c155", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "View In Sequence", + "Load Sequence", + "System Group" + ], + "hidden": false, + "id": "86439af3-16be-4a2f-89f3-ee1e5810ae47", + "name": "View In Cut", + "type": "group", + "update": false, + "userdata": "menus" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "b60d93b9-ace1-40cd-b8de-82454df1f2ea", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1000" + }, + { + "enabled": true, + "id": "ea7c2aab-1672-4b6f-9463-89dcc421325d", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "70a0e7e1-3401-408a-affa-2fd267e72066", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "9485f707-9662-4ef2-b478-e6419e88004b", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/wo…" + }, + { + "enabled": true, + "id": "ce8efe4c-f522-4a13-91d5-c0f298ecd747", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + }, + { + "enabled": false, + "id": "c691515c-76b5-4679-8681-cbe4c45e2814", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactSheet" + }, + { + "enabled": true, + "id": "7b2a9b7a-d808-44fe-bfb9-1f80cdc89f36", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "023dfd85-8763-4537-9bdf-f5d96a85a530", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": false, + "id": "f7f9c00e-83a6-4952-830a-051f85269d36", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "3 Days" + } + ], + "favourite": false, + "hidden": true, + "id": "46afa89c-89d5-4249-a502-e27194063c6a", + "name": "PRESET", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "0b143cd5-2a52-406a-9c39-0717eedb718f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "6ec4bc42-7424-430a-98d4-caaed053d1a4", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": false, + "id": "efc24569-a92f-41c5-91c8-29a842c4c752", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": false, + "id": "4601e4ab-2ad4-444a-9d0a-3ce01a720cf8", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "273f0494-0626-4fbd-b056-b6e4ed46dbae", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1000" + }, + { + "enabled": true, + "id": "96538c92-df5d-4035-be13-e2f7b6186b72", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "qc" + }, + { + "enabled": false, + "id": "7acd66ac-f328-4402-8b47-79ff54383600", + "livelink": null, + "negated": false, + "term": "On Disk", + "type": "term", + "value": "lon" + }, + { + "enabled": true, + "id": "52164900-1f75-4989-b408-b6bcdd0e49a0", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "20 Days" + } + ], + "favourite": false, + "hidden": false, + "id": "62032f78-66c2-412e-9183-77f42c9c3142", + "name": "Latest Anything", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "f33e60a8-e7db-4687-b67a-80d1f88283e6", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "53fc4ab4-b305-4acb-84c3-277e40a1f610", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "FX" + }, + { + "enabled": true, + "id": "02847316-8968-43f0-ba43-1a5ee87285a6", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "500" + }, + { + "enabled": true, + "id": "2d05b1db-cf05-4688-afb5-07bcdc10eed7", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "20 Days" + }, + { + "enabled": true, + "id": "57be151b-71d4-4eb9-95eb-cc2e00a3c39d", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "c3c8b35f-03f1-44ae-85dd-49cbe970bda2", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactSheet" + }, + { + "enabled": false, + "id": "edac2ae6-9755-4e29-a209-4a74ee30236c", + "livelink": false, + "negated": null, + "term": "Author", + "type": "term", + "value": "${USERFULLNAME}" + }, + { + "enabled": false, + "id": "d8dc6cf3-9a6e-4e39-93b8-78fd8c6163aa", + "livelink": false, + "negated": null, + "term": "Author", + "type": "term", + "value": "Valerio Andrea Costa" + }, + { + "enabled": false, + "id": "0eaec234-c7f6-4e71-8006-9532be89f1c2", + "livelink": null, + "negated": false, + "term": "Unit", + "type": "term", + "value": "Unit 04" + }, + { + "enabled": false, + "id": "3479f5d0-f301-4dd0-a5d0-e4830e71739a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "master" + } + ], + "favourite": false, + "hidden": false, + "id": "c7cfca6b-7beb-4a6c-bd79-539605a07bfb", + "name": "Latest FX", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "7ad45d7d-0b77-40fc-bbc8-fc5d879e4162", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "b8aee22a-8ed3-4cf3-a7f1-3068a11c11c1", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "FX" + }, + { + "enabled": true, + "id": "f06c32b7-7c0a-4364-91ec-9424cd084f65", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1000" + }, + { + "enabled": true, + "id": "e2b86bb4-cc8c-443f-aa32-d84ff1303c47", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "20 Days" + }, + { + "enabled": true, + "id": "1cb7e337-4699-4b0b-bc29-23532d22b352", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/cg" + }, + { + "enabled": true, + "id": "2a6dc738-c16d-42b8-9757-4727e30d95c3", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": false, + "id": "c6ce7a77-0491-4e64-973c-f6833bb37615", + "livelink": false, + "negated": null, + "term": "Author", + "type": "term", + "value": "Thom Chang" + } + ], + "favourite": false, + "hidden": false, + "id": "a43ae69e-accc-4251-ac39-948bea9c1b48", + "name": "Latest FX CG", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "2c0cdfc1-d02a-4d3a-8e0c-522659797ac0", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "82d96c3d-fc87-49c8-8ea4-ad345cc44c79", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Comp" + }, + { + "enabled": false, + "id": "1a7219a0-e920-4626-a7e1-94aeba6d3eaf", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "comp$" + }, + { + "enabled": true, + "id": "cafe3763-3ff3-4dca-97cd-36a05bdbb97d", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "500" + }, + { + "enabled": true, + "id": "e685964c-b5ed-4445-b1e0-ea06b3d310cb", + "livelink": null, + "negated": null, + "term": "Lookback", + "type": "term", + "value": "20 Days" + } + ], + "favourite": false, + "hidden": false, + "id": "b3edf839-0b8c-47a2-8908-8a2f75555003", + "name": "Latest Comp", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "f04ac486-c89e-4ca8-bc79-eae79ba09806", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "d2c5cd06-1396-429b-9713-5696d6d05e19", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Lighting" + }, + { + "enabled": false, + "id": "891ba4ea-03a7-4ad7-837f-f7d1938ea41f", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "cb3fb3c2-b068-4e0e-ab6e-dc9b20c849ef", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "200" + }, + { + "enabled": true, + "id": "30e87e11-57b8-4b97-8a9c-eaa930959eb9", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + } + ], + "favourite": false, + "hidden": false, + "id": "6f9882e7-d69c-4d21-b6eb-30846a6e79b6", + "name": "Latest Lighting", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "dfe1827d-962d-49fc-917d-37d5d8e4466a", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "a2ab0218-72e3-49e4-bd4e-7ad42a92cbc6", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Creature" + }, + { + "enabled": true, + "id": "9bf09b0e-fcdf-4b61-affa-435d5060f4d4", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Creature FX" + }, + { + "enabled": false, + "id": "a0549d09-2d41-4119-958c-f83543b755d3", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Muscle" + }, + { + "enabled": false, + "id": "b0d66a80-0194-49e3-8dff-f471a93791c2", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Skin" + }, + { + "enabled": false, + "id": "36036401-6e64-4630-b25a-978f2f55e027", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "QC" + }, + { + "enabled": true, + "id": "0b72378a-df09-4082-adc2-3a67c00b9029", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "5472bc77-79f2-48da-a730-6b8602e0d32e", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "8" + } + ], + "favourite": false, + "hidden": false, + "id": "260a0c07-ed56-4788-8df4-8608d7db2153", + "name": "Latest CFX", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "19a21290-ebf0-4c8a-abd5-d1b3da698801", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "18356bb8-9368-4ead-a6a3-e822d85225f8", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "cb76870e-68f2-460c-be0a-0e9c371fb144", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": true, + "id": "8246888c-bf35-48ec-935a-5c1d3ed5c506", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "6b6ac37d-85bf-4dc8-a47c-e99e15fdfb7b", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "5cbd2086-9f9d-407a-afc5-ba0505cc467a", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "3f0030c1-d7f0-41e2-bba6-06d785a6d8bd", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "qc" + }, + { + "enabled": true, + "id": "8d14d0de-36d7-4cad-a442-6a3dd8a409a7", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "025c132c-696d-4df8-bd7b-79e2f9399511", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "c22c1a66-5d4c-451a-922c-828487c27284", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "pip" + } + ], + "favourite": false, + "hidden": false, + "id": "4aa46ffc-22a4-41e8-9679-483b1ef6a5f4", + "name": "Latest Anim", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "eaa74774-ba1b-4cf7-96a7-0f9a2788852c", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "7183c92f-0e2a-4051-9eca-a3e8cc5f437e", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "999f9729-39b7-4c98-a4b7-47762a508a58", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "layout_v" + }, + { + "enabled": true, + "id": "5883decc-737c-42b5-83e0-6239fc21a580", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "40" + } + ], + "favourite": false, + "hidden": false, + "id": "68be0ea3-ffeb-4383-943b-6d825883faf6", + "name": "Latest Layout", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "99173846-c872-4c66-b992-e6a75d1c79d5", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "e3f97f03-bc95-4482-80f1-b79c099991fa", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Environ" + } + ], + "favourite": false, + "hidden": false, + "id": "42482ae1-d564-462e-b55c-18099992a1d8", + "name": "Latest Env", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "92a551b8-cbb0-4ec0-9dff-f63e9ab9cbde", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "a11ac4c7-be46-40ff-ba0d-7793cc6b32dd", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "7193fca4-94a1-43a9-a2c0-1196a60f82ea", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": true, + "id": "2288d4b2-2980-48e9-958e-b6552530f847", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "f78cb36d-ffbe-4994-9962-8ceca70a4a49", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "46ca820e-b8de-4461-9a6c-58ebe5c37596", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "1ddcf915-4ef6-4818-b4c3-b5007a2bd553", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "anim_pip" + } + ], + "favourite": false, + "hidden": false, + "id": "026584d4-c8d6-4e7c-a428-8d3fadbe7978", + "name": "Latest Anim (PIP)", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "38202ab3-f59a-4e11-be77-753bd4a37595", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "99201b00-5f96-4951-9bb8-eecbc83fa185", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Body Track" + }, + { + "enabled": true, + "id": "45790202-4a2b-4fa9-9ba9-87848df3aa7e", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "cab9630c-0312-4990-9c2a-ad08448ff68e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "wireframe" + } + ], + "favourite": false, + "hidden": false, + "id": "eaa283b0-c0ea-4da7-ae46-98eed74401f0", + "name": "Latest Body Track", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "c68f3768-7bbc-44ee-9772-59673f3501d7", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "97b833bd-ef9c-45c6-a249-36210ad90107", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Camera Track" + }, + { + "enabled": true, + "id": "39607bfe-3129-4142-9094-4a3847c63144", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "24037645-2579-4770-8c2b-2635fd94d4ed", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "wireframe" + } + ], + "favourite": false, + "hidden": false, + "id": "9f33010a-4308-4b4e-9475-a16f646a621f", + "name": "Latest Camera Track", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "e6680ce0-9273-4b31-b8b3-49edfa244308", + "livelink": null, + "negated": null, + "term": "Sent To Client", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "d5e8315b-8581-484a-b0a2-d16559673812", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": false, + "hidden": false, + "id": "0e4d6729-6c9c-4a1f-97bd-f68541ad23c1", + "name": "Latest Client Version", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "41b952d2-9f2f-4189-8928-94fc8fa3a627", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "f82f418a-d20d-4c33-bffd-85d9766d115e", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Crowd" + } + ], + "favourite": false, + "hidden": false, + "id": "1441dcb7-87de-4301-92bb-f4e4dcceac30", + "name": "Latest Crowd", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "1a9fdf44-09de-4dc1-815e-b335d4a8b160", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "9aa612f7-fca7-43a1-a8f6-b68e66fd75e0", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "DMP" + } + ], + "favourite": false, + "hidden": false, + "id": "a393dea4-3b16-4ee3-9534-d4f33ba69957", + "name": "Latest DMP", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "9c6d7216-8d69-4029-963c-22e871506ca9", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": false, + "id": "7c8c1b40-c578-4d28-bb3b-56bbcc538fde", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "DMP" + }, + { + "enabled": true, + "id": "8e3be80f-e941-4954-ad34-2a46292bea41", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "edit_ref" + }, + { + "enabled": true, + "id": "539d412c-3a70-41e5-bdde-39a10862450d", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + } + ], + "favourite": false, + "hidden": false, + "id": "3da16559-12cb-4ff7-adc6-4c10de953174", + "name": "Latest Edit Ref", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "0e12b237-9e34-42e0-9cc2-4e417793ebd4", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "61e5617c-369e-4578-ac90-b78c64535d62", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Groom" + } + ], + "favourite": false, + "hidden": false, + "id": "28da1399-1344-4a90-9c71-d78320e606b3", + "name": "Latest Groom", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "21b3eac9-ea20-4cf0-88a8-7d3fa2dea3f1", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "fcb1b94a-c904-40d6-9b1c-96e37f661906", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "cca0b817-5c57-4639-a9da-0f1f8d5c4f2f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "c6ad13a7-1b01-4f77-8549-2b59ee9a362e", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "40" + } + ], + "favourite": false, + "hidden": false, + "id": "5842af55-2d57-4021-a033-afc1e6600c30", + "name": "Latest Layout - Shoco", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "7f20d116-4fba-4834-a99d-ce25ac6c8b18", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "cf1a4d07-f3c4-42b9-b371-2af72dfc9a9e", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Model" + }, + { + "enabled": false, + "id": "83833b5a-fde6-4dc5-bad9-a2078ea5659b", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "a28e0565-dcfb-4932-a50b-dcade80923ca", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": false, + "hidden": false, + "id": "554465fc-afff-40a8-a912-82e6cd04b9bb", + "name": "Latest Model", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "4e87630c-aecb-4cd6-a58d-503be957584b", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "03423fd9-b453-4704-bc41-ea8da2f01cc8", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Muscle" + }, + { + "enabled": false, + "id": "54fb907b-e401-418b-9d00-eab558936e99", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "65f79b89-3665-4d03-9adb-b9166bc05f92", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": false, + "hidden": false, + "id": "1ed047b6-a886-45b4-b31d-d9f568b1fa40", + "name": "Latest Muscle", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "497f86c1-eca1-4c21-9afc-059212353a09", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "6293ec2e-7fb8-4e81-8056-6eb60e5da780", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Postvis" + }, + { + "enabled": false, + "id": "a7822e4d-9516-4424-a218-6b23f5688bf1", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "7446ac61-0934-4000-b1ca-fb6a8d5ae289", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "5300ff77-b3d3-4e38-b13d-8c8a2b626713", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + } + ], + "favourite": false, + "hidden": false, + "id": "e8cb260b-24f9-46ce-ad7c-e657374bf647", + "name": "Latest Postvis", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "fd543359-33b2-46e1-872f-231da2bf5824", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "25344b82-556c-4ef0-8f19-f2a9a25dd60b", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Prep" + }, + { + "enabled": false, + "id": "b94249a3-9392-475f-9d1d-34d8b09da605", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "bf283d43-39ae-40b4-bc26-6e44b241f68a", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "456eac23-c52e-4d1f-96a6-03100944ebce", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "278223d4-3fb3-423b-9f98-90eba00be50d", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "6da4218f-4f1d-4ee3-ab97-1680006a308a", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + } + ], + "favourite": false, + "hidden": false, + "id": "c8bfd63e-4e7c-4392-8962-63dcecac65aa", + "name": "Latest Prep", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "36e2302f-45e0-4443-8adc-c6285878b2f2", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "a9567db6-9192-47b0-b87a-578dd653ce56", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Rig" + }, + { + "enabled": false, + "id": "f5f655c4-6cc6-4ba9-8a6c-2b9f4c09115a", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": false, + "id": "c5b3f77f-d789-46ae-8a0f-a2f2effc917d", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "cc112e97-2777-431d-bd66-5d5a463c46a1", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": false, + "id": "d93af687-ed57-4c52-bfd3-501020270363", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": false, + "id": "774177c3-a26d-460f-8cb9-27ca296fa5af", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + } + ], + "favourite": false, + "hidden": false, + "id": "bb3e9340-8df2-4686-aeac-e388a404c2ee", + "name": "Latest Rig", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "6c9f05a1-fe02-4ab4-9e60-f2a3fe20c72b", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "bd7b0a9a-3ef3-40cb-a74b-1260259c3481", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Roto" + }, + { + "enabled": false, + "id": "0d3483d4-aafb-48c2-a2d5-1b4b9859f291", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": false, + "id": "7e0cb65b-adbe-4b2a-b318-6e4b368f1927", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "aabe1dfe-140f-483a-8353-8c9e54f5529f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": false, + "id": "f9c18ab1-d4b7-4fe1-9734-68a8ae6dfd4d", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": false, + "id": "6b33b21b-c46f-4b34-8676-8096148444d8", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + } + ], + "favourite": false, + "hidden": false, + "id": "1b80f3e9-4599-4a39-8da6-f7e90a3ca112", + "name": "Latest Roto", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "fc711e88-113f-4dbe-b02a-53f3dbdbb739", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "dbd3b9d4-3480-4dd4-bc85-280eacd386b1", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "185e37b3-fc2e-4a14-b1d4-446acce299d1", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "7f0297e1-df00-4031-9759-1964e3a5f41a", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "771b228f-e1bd-4082-ba07-b075d30059a5", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "retime" + }, + { + "enabled": false, + "id": "e67fce4b-1481-4058-b997-55c89a937a6c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "repo" + }, + { + "enabled": true, + "id": "1bb64bf2-aa03-41ce-846c-78e7f7ddd18b", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "903c653a-9976-49f5-b228-ccc44eeb6b56", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "39826f88-3be2-4fd3-b411-d633e6c8bc45", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactsheet" + }, + { + "enabled": true, + "id": "eb2f2749-5313-4590-8a69-800568806f6a", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": false, + "hidden": false, + "id": "24bed354-e219-4a64-8fee-ca027c18d026", + "name": "Latest Retime", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "01b4d3e9-d50e-4116-9e59-71c9c416d012", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "c60e3035-cd08-406a-b33f-1314254dafde", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "c7d429af-ff99-4e55-84c0-8053b5493164", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "54ef60cd-da76-4e54-a84f-dbf92a257739", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "9046f764-bae2-408c-94eb-fb33db532adb", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "repo" + }, + { + "enabled": true, + "id": "6cc14bec-a9af-4675-80a9-c7bfaf0901d4", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "071dfec9-2f9d-4faf-91fa-e1ece3b457f5", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "9d5949fe-29f3-4c8f-b7a6-38414928c833", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactsheet" + }, + { + "enabled": true, + "id": "996f7fd4-cf13-4aa1-b4a9-1df32adb49c9", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": false, + "hidden": false, + "id": "8d823278-836a-410c-963e-e1a924fd48ee", + "name": "Latest Repo", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "7d96b141-ceb8-458b-9723-8198c84817c9", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": true, + "id": "e44e94a8-dc4d-419d-a48a-676900f368cb", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^S_" + }, + { + "enabled": true, + "id": "eb6be8a5-c90f-42d7-a3fa-74c7a13b64d7", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "ref" + }, + { + "enabled": true, + "id": "de4f29ad-f0d6-42a1-a0ec-d4b4c6f06595", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "_lr" + }, + { + "enabled": true, + "id": "2b152223-530a-44f2-9374-4931f12d9ba4", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "8e6d1e30-724b-41cb-bfbe-11fb9a2eaf90", + "livelink": null, + "negated": null, + "term": "Is Hero", + "type": "term", + "value": "True" + } + ], + "favourite": false, + "hidden": false, + "id": "3ed8e8ca-59c8-4d32-a6fc-bfd1de348fe9", + "name": "Latest Scan", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "cd2c2cfb-0ad1-4341-bb17-4b7231bdf967", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "e60a681f-feec-4fd2-9e24-44df12aac1b4", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": false, + "hidden": false, + "id": "e9bc12b5-7b6d-4741-ba3e-b38e4c68a1d7", + "name": "Latest Output", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "a9f4d623-6519-4adc-935e-557fe293c424", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witcam" + }, + { + "enabled": true, + "id": "f78ed0e1-f5cc-4b13-87d3-b9e4da5f3309", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "gesture" + }, + { + "enabled": true, + "id": "d6192abf-e5d1-4776-9e7a-a2d032c2d322", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "52cbf35c-1cf1-467c-81f9-368f26a16755", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "6f5a9847-99d3-46ec-bfc8-c987611cdcef", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "8" + } + ], + "favourite": false, + "hidden": false, + "id": "6c21b94b-293e-41dd-a14d-659b34e14d20", + "name": "Latest Reference", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "ed29e83b-177d-4852-89d9-c2f09581ab03", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "84e203e0-6500-49a0-9d13-15d3d6267c3f", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Retime Layout" + }, + { + "enabled": false, + "id": "d5d6c120-bb7c-427f-825d-4be29d0ecf36", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": false, + "id": "519debba-35a0-46b9-adc1-63cfdd5da63e", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "af82f042-349a-447e-aba0-8f1f3675fc74", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": false, + "id": "bb79b7af-0c8e-4f1e-92cd-5ab90ea3723c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": false, + "id": "78702b15-ae04-4246-954a-2384308d2a11", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + } + ], + "favourite": false, + "hidden": false, + "id": "865d48c3-3d17-4877-95c2-a15459e5a25a", + "name": "Latest Retime Layout", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "ab3fa8e1-0eb1-4a9c-91ef-e099399c2e21", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [], + "hidden": false, + "id": "60b20443-9e6d-4804-9525-494598f43ec5", + "name": "Depts", + "type": "group", + "update": false, + "userdata": "recent" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "6e9f05ca-1a75-4557-aa59-5fa2edf848da", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + }, + { + "enabled": true, + "id": "6aeb82d4-af79-4867-81ab-d30acc513d69", + "livelink": false, + "negated": null, + "term": "Project", + "type": "term", + "value": "LIBRARY" + } + ], + "favourite": false, + "hidden": true, + "id": "369ea49e-bee9-489f-b1e1-2ef5e8ae3a0a", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "73d17bcd-be46-4064-9c3f-2aae878178c9", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "a898121d-31d0-4273-b63f-6ef83efb33ee", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "58b6a7c5-da02-4f5a-894e-f552c22cd43f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "aircraft" + } + ], + "enabled": true, + "id": "ffb98d47-e5ac-4f1f-b6d2-5351f1ab2d95", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "05aeb62f-969d-4a16-887f-4e1c451d35b5", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "eed868d2-7c03-41ff-92b7-b11080609b27", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "aircraft" + } + ], + "enabled": true, + "id": "b288adca-ab9a-4fc8-8ff9-6d9c7f1cb865", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "1463b657-d633-465d-a49e-194c09306f94", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "cf776cb9-5461-4af6-aff5-2cce48178367", + "name": "aircraft", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "c96265f7-ecf5-4cd6-ada6-12a0aa2ccc8c", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "549b8292-40aa-472f-8c7c-488ccdd96fba", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "b8944498-1d1d-4d06-b906-8874b8937ca0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "anatomy" + } + ], + "enabled": true, + "id": "de884414-242b-439b-9101-2134ce8c3902", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "d0b9dcad-b414-40c6-bbc6-b9425bb2eba3", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "73c9ebf0-ea6e-41f2-9cb7-c2e07ce2a92e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "anatomy" + } + ], + "enabled": true, + "id": "d44d1685-b433-4ed6-b902-40507982fbd5", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "cca84780-17b8-4d9b-a585-72ca86167794", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "80bdc955-0fe4-4b3d-92a4-098e0ded56c3", + "name": "anatomy", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2531ad7f-2204-47c2-b6c3-3ef7bd426196", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "51d840e9-3a8b-4bba-8e48-c968e8890fb5", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "d4cb1e15-0d65-4cd0-aaa5-c6475fb93346", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "animal" + } + ], + "enabled": true, + "id": "63434854-5e03-4ab4-abf5-de2ac4ae1a1e", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "ce48dab1-0389-4071-a537-ff450b050222", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "52b0f0a7-0243-4c70-882d-058461fd40e4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "animal" + } + ], + "enabled": true, + "id": "e9b4ec5d-fd76-46b7-8f1c-3d3cda429fab", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "8d81e21b-4658-4cae-8b87-2ff1455c0def", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "b4dc460c-e04a-4abf-956e-d6cfbf3804f9", + "name": "animal", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "0f4735f2-905b-470a-8457-92ae3ed09010", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "72fd062a-b3d1-4eee-bdd9-77577316abcc", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "0a109f19-0fee-4a20-89ad-d60134d9b86f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "aquatic" + } + ], + "enabled": true, + "id": "5dc313e0-8d0a-4843-983d-c61087754091", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "7ba0068e-55dc-45ac-b61f-94affefaf2ea", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "0a7d1359-9cac-4e2a-a74a-5505b8537800", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "aquatic" + } + ], + "enabled": true, + "id": "46f79a71-d617-46fd-b418-040575ac2854", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "6bb30dd2-2a16-4c6c-b813-8b2fab4069e5", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "d3f83fc9-3b4d-49cc-a292-ca99b36818e9", + "name": "aquatic", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "fd913744-ac0e-4245-812e-90f41f127d00", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "f8c2eb55-5cf5-4dfa-8fc2-faa13d18ee91", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "d1cd325a-edf2-4032-997b-a9215ed2b357", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "ash" + } + ], + "enabled": true, + "id": "cdb8dba2-fd4d-454a-b7ea-cb7fc740fbee", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "2b5ff1d3-34c1-49fb-aa1e-361447661802", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "93ba1cbf-4537-4bd8-9c26-6ccb708d9d79", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "ash" + } + ], + "enabled": true, + "id": "4ce36c80-1372-4440-b83f-9358ebd92788", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "ad4819c1-d169-483d-9c07-3a344b92aca7", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "b9f9c31c-1d2b-4a39-bd92-0d63015e9266", + "name": "ash", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "9af80dd3-9fa2-46d1-a517-433a7e6ba514", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "44c1d4c5-14e2-481a-901c-d3288643dc05", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "43463161-31a0-47dc-987c-a83cc8338bb1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "astronomical" + } + ], + "enabled": true, + "id": "f3ca0aec-5307-4fae-b94f-0408d5801e77", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "c0e4c0b2-c498-45f2-8874-4c2219372b2c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "2d951e89-ba5d-456c-a0d7-554cea0b3eb7", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "astronomical" + } + ], + "enabled": true, + "id": "f44d0c36-a09a-4dcb-8f7e-1a624ead355e", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "3283961a-59e2-4189-bc5f-f07fa261126e", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "fb11f110-e1b5-4002-bc03-3a27cb80588b", + "name": "astronimical", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "06037f74-b925-49b1-ab5f-044c0036641b", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "33127ff8-8b72-46e3-aa46-363b7d00fd6e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "e6cd6dbe-6889-49be-9ac1-caf8a41b1ed1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "blood" + } + ], + "enabled": true, + "id": "8b87d9e7-5856-4b51-a895-47d03a03a9bb", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "d37f1be0-c978-4887-88eb-9e5e7540f254", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "59b19314-f70c-4844-b3b1-bb7e9d39014f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "blood" + } + ], + "enabled": true, + "id": "084b07ab-8eb9-41f6-9cc2-8b5c5df74d65", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "ae4bb3f7-a30e-4331-9385-fbb715326f84", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "0fae4d73-dc10-4d23-adf6-1ceef5e38fe0", + "name": "blood", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "759bf999-d2d9-4be6-b646-2ad228a75b53", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "20522f14-29f6-4d9b-a672-467f1c8fbece", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "e0e06d8c-7b47-4634-8123-3620dc1c13e2", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "boat" + } + ], + "enabled": true, + "id": "11bfe713-f849-4e7c-8b4a-99d7920976e7", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "48c05052-2cf7-46d1-8cec-2259d15b4a3d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "08039cec-8d4c-45e9-872f-2beca7c106fc", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "boat" + } + ], + "enabled": true, + "id": "2b0e923e-bee8-4a4d-80d9-f22b52c7473b", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "5a2936b0-befb-4726-b016-03ce97f7ed68", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "cbee839e-1cce-461d-be85-b0bc90f89072", + "name": "boat", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "fb267ad4-840e-45eb-b36b-58c9b05d7291", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "1954f69f-de5e-4ae5-bd08-d23e382fdae1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "e2f165ed-9f8c-4ac4-8252-d7c2471acb75", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "breath" + } + ], + "enabled": true, + "id": "c3bdd096-761b-445c-a229-0b249031d927", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "410ffc34-9310-47ac-9c53-2a52923d6537", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "589baaec-4589-40ee-a0d2-c8645a826a0d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "breath" + } + ], + "enabled": true, + "id": "a5bbb1dc-f9ad-4388-bfb3-f8d4694bb3d9", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "01fb001b-4ba7-468d-99b1-66fd629880d7", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "aa194790-1de6-48d7-92f5-5cc60c2a2312", + "name": "breath", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "654b8aea-b64a-458a-950e-8568ea0d1024", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "17b617e5-5be7-4c86-b71c-f4bad89b9d34", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "2cc9be8c-a545-498f-839c-84065f5c9865", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "bubbles" + } + ], + "enabled": true, + "id": "4a1f45fa-e1b3-4e48-91b9-77c5bd05424f", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "924b21d7-4f80-437c-aa5b-d67cbcd30630", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "f32806ce-c4f4-4513-a92a-7e03482e7f72", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "bubbles" + } + ], + "enabled": true, + "id": "647a92b6-a061-41bb-8be5-2d985f2a3de1", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "1e1377c2-5d87-47b7-90a5-cada9bb99735", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "9af98ed2-7831-4a6c-b772-1ff2e89650c5", + "name": "bubbles", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "b36a29a4-39e2-45a1-ab04-5aa5020aaa3e", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "2b4f3e7f-b085-4102-af4a-c5d7be8f7a96", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "af33aeff-4610-4b77-add3-e16f7aaf94f7", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "calibration" + } + ], + "enabled": true, + "id": "1df3d294-fcdb-485f-94a7-b2510f5ec0fb", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "5fe4e44b-895d-40e0-b247-03ca76c354f3", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "e1755caa-e2ed-497e-bbcb-d5c2ee27a179", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "calibration" + } + ], + "enabled": true, + "id": "a76a5e2d-bf20-4352-b957-33b04fe8bb15", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "7088b5bb-9bc8-4b4e-92d0-87d715f8a45b", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "87daccb5-fb1b-4202-a294-09fdb722609f", + "name": "calibration", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "1b1daaa1-3dd7-42b7-823c-ad0a499baa6f", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "92443d4d-3a60-45af-a5f4-d11a75b52b84", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "f1d6bedd-8820-4e9a-840f-95e054687b27", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "city" + } + ], + "enabled": true, + "id": "1e25c894-db19-47af-998a-9fc1c1c93b53", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "bcc26cfb-0b85-439b-90b3-06b3ec45dffa", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "f04b139d-23f0-438e-8985-57b0d0b0237f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "city" + } + ], + "enabled": true, + "id": "89e040cc-bdb6-415d-9583-5661dbad04eb", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "e5123830-3ccc-4069-8013-11c6c0a7b0b9", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "884fea52-cfa1-4edc-a2c6-baf4249eeab7", + "name": "city", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "fc70129c-5e19-4ca5-aac8-87083ffebb73", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "86009954-93d1-4e97-8838-41d5f772a44e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "cbad012e-c1b8-4cf2-b34a-eecb41534dd9", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "cloth" + } + ], + "enabled": true, + "id": "19d2de14-a397-4be4-bb4f-9d85f3f22318", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "e70942c2-cb27-41b8-a3a9-7f389450d25d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "c5e0f3c3-4bb2-403d-ab59-f5dbd4e9ae82", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "cloth" + } + ], + "enabled": true, + "id": "53a2f6dd-54a7-4aaf-86c3-f62a9edfed78", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "fe29a1b4-b197-4940-9607-0a823e5cac8a", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "f03b6cd8-6a85-472c-b118-9904164b8b8a", + "name": "cloth", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "fa1fd3ab-f044-4565-b9a6-58535273daa9", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "609f6af1-b806-4616-b169-1f7a1eb5abe9", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "a8d5f08a-57a4-4fa2-8172-c06bfb84d2a5", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "clouds" + } + ], + "enabled": true, + "id": "d69fc046-d175-49f4-83a3-bc5d753dd18b", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "b78c69d6-b8e0-422e-83ca-d6e2fb30bdf9", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "a4c93b82-3dd2-4b7f-8779-5f6cda32ecbb", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "clouds" + } + ], + "enabled": true, + "id": "404f56f3-db11-4fa0-a1aa-89646a923ca7", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "630f427a-39df-4892-b595-e69727afdcc8", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "26b9c28a-14a7-4f49-a05e-0e109048634f", + "name": "clouds", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "a409435a-47bd-4bf0-8382-1eeaedfacadb", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "b8a70c3a-039d-422e-aef8-155129e0c735", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "3a85ff62-9df8-46dd-9cb4-6a52b28a27a0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": " confetti" + } + ], + "enabled": true, + "id": "f2b1b526-022e-471d-a315-550a3e9d42b9", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "fedd1cef-6f10-4dfe-b11a-12d8b26afd1d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "161bfdbf-715e-4106-9290-f1ba65118610", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "confetti" + } + ], + "enabled": true, + "id": "e6238c03-fdb5-411b-b27d-c025555bc307", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "55c6d35f-5d11-4bf7-a800-01e0e90519b4", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "297feb3f-24b6-4830-b25c-95c4b6d83849", + "name": "confetti", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "8aeebd0d-4a7c-4e63-81ac-9b6ea68ced36", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "25b50537-d09a-4943-ad16-53ad477369ab", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "b9b29309-f0a5-4b89-8d38-892c65c762bf", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "crowds" + } + ], + "enabled": true, + "id": "28478db7-7180-4dd8-a173-d52386fb97fd", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "ebdcfab5-126d-47be-b7f4-143744abaa1d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "34d66e5e-fe5b-48d2-984f-2a3c5b7e4007", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "crowds" + } + ], + "enabled": true, + "id": "ab3a3d27-2528-420c-888f-bb35e5295d39", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "9419b1fd-6da8-4472-96af-f39943ec2450", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "67dbe637-b0db-48d1-a1d7-29bae0f17cbb", + "name": "crowds", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "1a71589f-6148-4166-97de-6d13f0317d33", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "b57ea152-7ae6-4946-9a12-7b4f41975d54", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "bb89a0a8-d487-4604-98f7-30e240e7f185", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "debris" + } + ], + "enabled": true, + "id": "2bb83094-10f2-4f4c-8224-399e1d2e91ee", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "119f75dd-778e-4dbc-9f4b-7330a699fb7d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "3e5512e2-88ff-446c-beaf-1ab76b1df9c9", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "debris" + } + ], + "enabled": true, + "id": "6ade8e9a-c05a-4b85-b758-9daaf6ae9ff6", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "a38be395-1fba-41b7-be8f-351632e0470e", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "5b10a8cd-7e76-4065-ad04-70edae73435a", + "name": "debris", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "7040c863-53bd-4367-b5c2-0373346d07ea", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "4bd3aa99-ff4d-445b-9a2a-4a8a0a60235d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "010cfad7-bc3c-45c6-89bd-1c8bcc091e15", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "doll" + } + ], + "enabled": true, + "id": "d6370978-f38e-4db4-8e24-ce981351b5ab", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "470cf3a3-df3c-44ca-9c88-7ec682023806", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "5e34b417-56d6-463b-998b-b5ac505171ce", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "doll" + } + ], + "enabled": true, + "id": "4ef43c49-6efe-4d2d-8a3f-2c2249923d5f", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "d0885084-e71f-402d-87a1-cead909128ba", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "9b2cbc69-e2a9-4b2c-a426-05d6cd6e88af", + "name": "doll", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "96885bc5-d7c9-4fd3-8244-bb70cadfb2ac", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "aec5d5c2-f716-4d8a-8f3a-b00579cde604", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "1aa42dbe-5daf-4026-8299-dd422c6bde3e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "drips" + } + ], + "enabled": true, + "id": "8e0d66e9-dca0-4330-a206-353bd3b3b92c", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "61d65d24-fbab-4d01-87fd-67b14f9480d6", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "fb3e1431-e95b-4f76-98df-4f257517fc33", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "drips" + } + ], + "enabled": true, + "id": "aa15f2cc-e5da-46ec-94dd-544c707ecf84", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "428ee523-0f86-4ea2-9413-f79f40b77dff", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "6b07c679-aed2-4c9a-8dff-603bafc79dc5", + "name": "drips", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "89e38f91-f759-4daf-a558-61f7d4c34be2", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "79aa5f81-5af0-431d-9614-bf12dfdaa048", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "acf86c26-ab24-4164-b041-c841d40398c8", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "dust" + } + ], + "enabled": true, + "id": "5ec81ca4-f077-4e2a-ba6b-6df5e59a1078", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "a46408b5-810d-42f5-9f3b-df3b60b7f5ea", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "9104787a-f554-4bfd-8715-79047ab405c3", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "dust" + } + ], + "enabled": true, + "id": "97297f69-78ea-41e8-bf8a-5b74747dfde3", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "858a3f48-d72a-4fd2-9f28-e3a40799eb0b", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "67ce7bec-4a81-4591-8ec3-f87eec5a781d", + "name": "dust", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "9b007f2f-bd80-49ef-a15a-fa326ea8f683", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "00595bb2-e6f8-4476-9c88-1e07750dc5bb", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "c7d3c843-8683-4b72-b7fd-af3e10d99a48", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "explosions" + } + ], + "enabled": true, + "id": "8d3ee267-756e-4e45-b133-5457d93092c1", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "4b1a277d-9af2-4794-a948-4551a79555ad", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "77a459a8-c971-43dc-bfc6-52a7e88a51b6", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "explosions" + } + ], + "enabled": true, + "id": "54843963-c18b-42a4-bf04-4da680a7945f", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "fc1c71d4-71fa-420d-93cb-5cc92ea331eb", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "75aeda2f-b1a4-40c8-9d4d-805c2e1be391", + "name": "explosions", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "ffa34850-43af-4f5d-8a92-b73f3dc4aa9c", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "5f7dee1b-9706-4787-a303-e58eea9ac561", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "12939853-20a9-4b76-a385-b36d3dca8f27", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "failingObjects" + } + ], + "enabled": true, + "id": "5bab8043-0a02-47c6-8b01-c3e6868b6aac", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "442a4a89-ed32-4d03-8b77-80401bc8335b", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "c0cc6988-828c-4e73-acf7-b9878290ef82", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "failingObjects" + } + ], + "enabled": true, + "id": "f50f9504-2157-4adc-a474-9ec1e5b2da6e", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "1e026564-d535-443a-a245-9dc5cb7fde9b", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "09e7debf-6567-45e7-92f9-baf304ce0426", + "name": "fallingObjects", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "acdcb31e-d3ad-45e8-b975-5bcbaf2b8124", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "307c5a15-7ccf-436c-bab9-4cfe6970befd", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "d1269b90-7d43-40e8-8b60-61cd83459164", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "feathers" + } + ], + "enabled": true, + "id": "10340dbe-c5da-4c09-a113-801190b89325", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "c965da58-c435-4129-9001-0708024992b9", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "f0734e68-fe3c-4a29-b28d-b679ee245650", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "feathers" + } + ], + "enabled": true, + "id": "83fb1efb-1093-483f-9a25-3b7b9b0ce053", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "0657d6e7-d40c-4981-be27-d9cfffcef920", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "1a4dfdee-b2cc-425f-94cd-19ab09b4c409", + "name": "feathers", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "c6cd6586-9a78-4dc0-96e9-2ac610e80e9d", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "a9f8817b-c29b-42b4-9595-a6adf1e4fd90", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "81173901-9c59-44e0-82e7-0aed6ee744b2", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "fence" + } + ], + "enabled": true, + "id": "c6df4d88-ae33-421a-9feb-4647f13f48d8", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "aa931cff-0211-4c01-b4c8-16e6db7c7611", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "14bb126f-ffff-410e-b9c6-637131b68de0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "fence" + } + ], + "enabled": true, + "id": "ef7334d1-fbf7-4405-86fc-267f7023de60", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "73a133da-103f-4bf1-b076-e40b801c1932", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "22e7da64-1758-42a6-b4b9-1199383ecc26", + "name": "fence", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "5967e0f4-b2b5-4816-bd36-bcec854dac16", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "262b93df-a5e3-4687-8c2f-84c4a81e7adb", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "b2d9b98d-3909-479b-843f-9974187515d8", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "field" + } + ], + "enabled": true, + "id": "50dce5c1-bbeb-4519-a92b-99c2cb7cdaf3", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "c649c65b-0417-49d9-8bb7-e51edb9c243d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "7c91c3c8-4122-4b29-b963-ad5b5f114a01", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "field" + } + ], + "enabled": true, + "id": "74639464-5376-41f1-aa95-00c4a7c83837", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "b9f133ca-5579-4530-8789-7f884598c78f", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "cfd9353a-0745-4f57-ac6f-a9a07f370bd0", + "name": "field", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "517ba2fe-1bb3-4d7d-928b-e654aae74718", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "1c5446d1-e0d0-439d-9f71-739258394967", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "22fed232-5853-4a54-9628-01466ad0123d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "flags" + } + ], + "enabled": true, + "id": "a25096b6-621d-4419-a2e3-0e9c1592ca00", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "836207e8-60ae-40d5-8ce7-f4d828c57efc", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "462d3c46-0dd2-47a3-b18f-c216e8ddf6d2", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "flags" + } + ], + "enabled": true, + "id": "6688f997-be05-44a4-ba0e-56f7041c5ed4", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "87a2f353-0478-413c-9737-4c44c9d43632", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "dd25e7d2-2edf-4d93-b62f-23b1f146fafe", + "name": "flags", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "c5944a67-c5c8-4ab8-a788-4f3fc376d382", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "4d3508d9-51b4-4bea-8a1b-b167a7a08c5d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "413ef013-4c86-4d34-bb2b-d008b888fd41", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "fire" + } + ], + "enabled": true, + "id": "4dcaa4ad-e9bf-4374-a437-b2116ffd5303", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "70542b65-3ba6-40d5-97a8-a2289fefc3ea", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "26f191d0-ea66-4b37-826b-85b91c22f490", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "fire" + } + ], + "enabled": true, + "id": "28b75356-7de2-4ba4-90dc-7feb87155c67", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "c646bee1-eb6f-4fd1-be2f-16469038cbcc", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "e605ea06-6003-46a9-97bb-3afd45e460e7", + "name": "fire", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "ab2e1df8-b462-406a-b8f8-0d49444fecbd", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "b8f81657-a866-4180-8f32-12318266ba69", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "ad40f08a-57c1-41f4-8e2a-4d1a64f165e9", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "fireworks" + } + ], + "enabled": true, + "id": "cae71a1b-ae0f-4597-970a-39b6886527dd", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "187d8cf1-42ba-4d7e-9a14-cfeaab782025", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "2d37d63c-2fff-4eb9-96f9-e69475e455e8", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "fireworks" + } + ], + "enabled": true, + "id": "6c1d1f8c-8fb5-45ad-ac2e-8429ed0c6cc8", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "4ff1497e-012e-4ae4-bb6a-fc04aba4aa19", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "99d43fe9-a45f-4ec1-88e4-d689cc262e97", + "name": "fireworks", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "c5b7de36-819c-493d-a2bd-b0d37e526c4c", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "634a3f95-0e49-4fb5-b4cf-ac412039b22d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "f844f584-f5f0-4287-8be4-5a5e5899e085", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "glass" + } + ], + "enabled": true, + "id": "3caee2fa-adde-43b1-be7f-8f77dcb8a1e2", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "d0ecf072-48b3-4593-a4d7-8f0288c139ac", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "a32141ac-7db8-4bb7-88af-59c33b103557", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "glass" + } + ], + "enabled": true, + "id": "fb7e2d74-db0d-4935-ba6e-ae05c0fa122c", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "a7bd3afd-aaf8-4ea2-b2e0-410fd01a5075", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "9fb44b6e-8711-4fdb-b2e0-1f4bbbed6f5b", + "name": "glass", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "f58e4771-9f7e-4e11-b3b5-444db4a27272", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "63882aa8-99e2-49fd-908b-478e3c842aee", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "527dbb65-4eb3-46ec-b2a3-34ffc3cba23d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "gore" + } + ], + "enabled": true, + "id": "d7ace605-a7e3-4717-8730-816a954394be", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "ca006f80-10f1-4477-af9d-e3af68ab828d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "44ab2b79-5cd3-4c3a-abff-b32e9d479c29", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "gore" + } + ], + "enabled": true, + "id": "b07f6849-775b-46df-bd29-faeb1d5aadd9", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "ac80257a-e47e-4908-ade6-106a37524a13", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "8e65ff13-2576-4dd0-90f3-e0bff4def8f6", + "name": "gore", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "39fd5b59-4889-4ffb-859c-8720ad092003", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "fb35c607-1a21-45c5-bcbc-48e70db41372", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "6c45956d-51f3-4243-966e-0ca8b19a4bb4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "grain" + } + ], + "enabled": true, + "id": "014d1c22-260b-4993-ad07-f5bcff611a5d", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "5caaa7b2-1fd6-459c-9ce0-4baef13ca0f4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "bf462f84-d60a-45c9-97a7-f6737cf257e8", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "grain" + } + ], + "enabled": true, + "id": "71496462-4092-4cfa-9f24-fb259e0ed19b", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "e6f49e9b-cdc2-4a37-ad4d-5c777b1dfc6d", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "c3e83651-72a8-4958-8775-4b65cb7cfda1", + "name": "grain", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "abd62de1-cdd8-4354-929e-5f4cedf836eb", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "2edb0871-6e00-462c-a3de-30b658da60c5", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "8f54563a-8ec4-405e-9a02-5b97e7670ef1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "heads" + } + ], + "enabled": true, + "id": "96f14767-1610-4872-950f-fb40e50d3841", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "fa252562-5e7f-43d7-bb5e-6e551342ab71", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "0cf684dd-154a-44f8-84fa-995d00e1cb01", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "heads" + } + ], + "enabled": true, + "id": "97df3881-75bf-4efd-9cf0-5228af783c0f", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "ff6c1aac-6621-45ec-a060-fe25dd11b826", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "55e35de7-642f-4fe9-8c49-b66aa15de45c", + "name": "heads", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "ebf3948d-e1b6-4c2c-9fbb-66d6d90ea20f", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "6440e65c-07ae-4957-b8db-2e7a7a7739f3", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "472ae047-7bb4-4c3a-a8dc-da0b90f59596", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "human" + } + ], + "enabled": true, + "id": "e60beeb3-5a28-4c32-86e4-9cf50ef880da", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "fb5b8b64-a533-4937-84ec-a4b82c0c234e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "64d7e334-2207-4a3e-b174-361ab4bb4a13", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "human" + } + ], + "enabled": true, + "id": "7867a402-35ca-4c54-8c8e-df4c1f5f0d23", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "65cfd462-b1de-4bc2-aa22-c80832ec395b", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "06fc4b55-a705-42c9-b46a-429576871e2c", + "name": "human", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "76fc1dd3-9368-4367-b69c-9661eb84df05", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "c818757c-aae2-40ee-bb79-8ab20aeffb9d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "fd1fd584-6dba-4e37-8bdc-e6f0172185bf", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "heathaze" + } + ], + "enabled": true, + "id": "a905eaab-4d5f-433a-bc79-05d644ef0cbb", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "a5edcb78-d047-481c-8f8d-960eec85fa00", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "0783c499-f86f-48de-bf02-94c9c487fbf0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "heathaze" + } + ], + "enabled": true, + "id": "21fa5b08-447a-4fd9-9e77-1c1d6aafec66", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "86fa3d8a-9070-4dbe-8fe1-b6bb5143208c", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "72168b63-516d-4081-9f23-e3d764d6b705", + "name": "heathaze", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "3ff3def4-a2b3-45ad-b1ae-a38b695e0c1d", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "dd642338-a9c4-4a4c-9de2-5bc95325144f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "be78deb7-6b80-4a55-9b07-20ae09ccc2dc", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "impacts" + } + ], + "enabled": true, + "id": "63886cf6-3074-4c4a-ae28-863a770007cb", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "f5152fc7-8f5c-454e-9480-70a36bbe1c1d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "0aac4f25-8f17-41ed-bdcc-c2e70fb59a78", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "impacts" + } + ], + "enabled": true, + "id": "59a063cc-cf8c-4793-88e0-2505676d39bc", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "b10835f7-4a9e-43b3-8434-6c145ccac39f", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "7303646e-f22f-4994-aa4f-beb2c83b75fc", + "name": "impacts", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "3934cc5c-0853-4536-8918-ed5a1f8d3926", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "f49db5a0-93b1-4349-b04e-f83841f130aa", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "080e41d5-0397-4178-86df-d24347c10251", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "ink" + } + ], + "enabled": true, + "id": "8659c3d8-3ec7-47ae-b314-f46cab7a2443", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "563f6b72-0d06-452e-ba86-276ca8bbb64a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "07dff590-8e79-4b83-bcab-d7f4f481b6ed", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "ink" + } + ], + "enabled": true, + "id": "58828b45-35d6-4f04-806d-f553de9041ec", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "28caccaa-9a26-4757-826a-28ef8a544e9d", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "73b6f48c-690c-4661-8732-e23a6e353f92", + "name": "ink", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "0a04d439-bd0b-42d1-94ca-26f61654b2ac", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "fb3a1bd8-5c08-4746-b5c4-3e467c9e954d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "c451cc08-4538-4d07-b547-3b3af90758fe", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "landscapes" + } + ], + "enabled": true, + "id": "fcaf3fd0-4c23-4db9-b5f1-7eb9aab553a5", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "1073033f-dc81-421c-ac46-3266b4125f6a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "48bb7b1a-d352-4912-9e7c-2db0a90d2896", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "landscapes" + } + ], + "enabled": true, + "id": "725537fb-93b5-4e98-b232-2c5117bb77d9", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "327264f6-850b-4c78-a693-63b3f1d31bde", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "53f54f75-ef66-4a13-8983-0f8591239bb4", + "name": "landscapes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "e91615e3-1106-43a7-850e-8a9e6ea0cfc6", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "533686d2-1984-41a7-8077-8f343e1caac9", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "fa0b4881-778f-4e21-b1aa-b26c9008000b", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "lens" + } + ], + "enabled": true, + "id": "7225f686-56fe-424b-9163-0d968e58c2fe", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "12771860-48d0-4db7-95da-a32c24a7f9a8", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "bc5c78b7-d5ce-4860-a902-5ce449b250fd", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "lens" + } + ], + "enabled": true, + "id": "53249d0c-fa55-49f0-a8d8-5de7c1af18f9", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "4380b0e9-dc16-4714-a976-ea8f04879847", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "ee88e33b-2951-4004-8cb9-37c11d17b3d4", + "name": "lens", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "0153e25d-2742-47af-b90f-c3bcab9018cf", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "f9d2a354-e441-4b02-a848-4e9cd686fde8", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "ef7fbb2a-917a-4e02-af3e-ca96488f26b9", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "light" + } + ], + "enabled": true, + "id": "f32bffea-32ff-4b4a-adde-06999c56684b", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "e062d234-1cdf-4b99-be30-9a6056e8f1bc", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "86f95ba7-2407-42ad-ae45-ab8f0f4bed13", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "lights" + } + ], + "enabled": true, + "id": "94c333b7-5752-4557-888d-5804a2206d46", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "0dfec027-d04b-4f55-8a82-a847e4c570fc", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "75a78925-0a66-46ac-b479-f112d6cf89b3", + "name": "lights", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "4dfd191c-e704-44e5-b68c-d7e6ed1e3dba", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "761f7094-e2ba-4e55-a9a0-39d44b8f1173", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "5223cca1-afc4-420f-bee6-eb0187f0e9c3", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "logos" + } + ], + "enabled": true, + "id": "8079921e-ed60-4f1a-ba57-b391321f4ee5", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "0d6d1cf9-423e-476c-bdec-59db8cba0095", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "e34317b8-df1f-4b69-9180-61bc1d458a62", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "logos" + } + ], + "enabled": true, + "id": "afb763e6-a252-411d-9543-3271b8bdaa68", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "18a8e5d8-b89f-4f8d-b53b-a7be8acdcb08", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "79f2980e-c4f4-4c72-b7e5-ab1963811faa", + "name": "logos", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2d669df7-d267-4d35-83be-9d261564a47b", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "22b04f37-33a7-44f0-ba98-02de81b0fa78", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "abb11c3f-96cd-48e8-92c2-b601d7f6d40e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "mist" + } + ], + "enabled": true, + "id": "f1b0a0cb-b959-42cd-83a4-5b0cc6e38787", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "a0d03b74-a92e-4e14-8d8d-a86ff9826b15", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "daa42128-a6f5-49aa-9d80-109715e4bfb5", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "mist" + } + ], + "enabled": true, + "id": "b8c22ed7-4290-4897-8a69-c2d89b1484ad", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "988ed908-97cd-41e8-b31c-06d4ec36f086", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "5168e78c-66d4-4a6a-a77c-8349c4d24477", + "name": "mist", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "f492897b-40b7-46b9-a305-0af2ec71f796", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "8a9aafb9-fb74-409f-83b9-fa12de00d1fa", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "cd0f79e1-3936-486e-80f9-ebdad0f5e287", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "mountains" + } + ], + "enabled": true, + "id": "b99b7380-73cf-48df-9a03-8406ee334efb", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "2d4a2bfc-6940-484b-a1c6-f5a62e2e1bca", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "24e303b9-a693-423e-9aa1-25c10f518976", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "mountains" + } + ], + "enabled": true, + "id": "f3fe9f44-6939-48cf-8b11-6b852e525a58", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "2a0386cd-4dcd-41f4-8326-0c9f551cd082", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "3b279dcd-5933-4e8e-923a-28a422bbef6f", + "name": "mountains", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "c6b544cc-d48c-4255-81f9-764ffe434472", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "f00ed448-c7f1-4910-aafe-d087f9169a00", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "0dcdf974-07e1-4701-8848-51ee2c692dfb", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "muzzleFlash" + } + ], + "enabled": true, + "id": "68aa3881-8db1-42a5-8fa6-e41e382c7a96", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "6d63f94c-2f7a-47d6-b165-005709e0a256", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "017c0f7d-cbac-4c11-a816-5589c0fa3d23", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "muzzleFlash" + } + ], + "enabled": true, + "id": "438f77f2-791d-47aa-b8fe-15790880a605", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "bbf87825-c7a2-4762-b3e2-a0a770b42869", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "e5c27cf0-bddd-47e1-a2cd-7c50c7de2b41", + "name": "muzzleFlash", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "d24ac458-f4c5-4d13-a8ff-34070693673a", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "0b648405-2f23-42e9-8701-0fe7b136900a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "adf81530-f9ee-496e-b2e2-b2a60194c23d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "neon" + } + ], + "enabled": true, + "id": "dd500414-141d-46a0-86b1-0ebf05854d11", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "311657a5-ad45-4b6f-96ab-8b5c9aaf3bcc", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "047c3010-5db4-4c7e-834d-543dd491640c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "neon" + } + ], + "enabled": true, + "id": "1586e28b-ee14-4854-b72b-fe8b8fe9ec88", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "2c6ea89f-37a5-4e13-8c65-1a7557320e6a", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "c7346476-1110-4f48-889d-2be371cacaa6", + "name": "neon", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "f9575d49-d13e-4446-b811-1ca8998c3a58", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "2133f9b4-02d1-4f2d-af08-1ea05b115046", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "4818d5c7-f681-4546-b447-56b9c28b4a15", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "overboard" + } + ], + "enabled": true, + "id": "3211d337-c912-4329-92ed-ac9f42ee7ec8", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "b0a43989-6535-4f57-b185-fb4d4d4f852c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "a07f202f-2caf-4684-a09d-5b14c614d387", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "overboard" + } + ], + "enabled": true, + "id": "1ba1a6fb-df92-47ca-896b-0eee43f4dab9", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "3be2e83e-e5a5-4867-833b-cee0884f91e0", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "bf2d8fee-b293-4375-969e-92b3e6015afe", + "name": "overboard", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "909b10d6-2f1a-4688-a354-4746b2359c34", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "571abf46-a7e0-4534-af64-94d9458209a5", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "eed02bd1-95f4-47ed-a27c-2b6722309fe2", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "paper" + } + ], + "enabled": true, + "id": "7c208b60-68be-4bb2-823a-24c0cb995bf0", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "62de12b8-cfc2-46e6-b4eb-ab3c2646529e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "6437ea9e-8271-4719-bbf5-5627e72b905d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "paper" + } + ], + "enabled": true, + "id": "e060f50a-cd28-4c64-989c-43dd44d729f2", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "0a1d1556-cf80-46aa-809c-a9fbc5b83527", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "1e6f0e86-25f4-4898-b78e-22fba2d0cfda", + "name": "paper", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "92997a23-4915-4b22-9e72-feb0771a5c7f", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "484ebe4b-115c-4fa7-882f-a82fb2fade27", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "9757be44-d329-4746-9be0-502ed50b0b8b", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "parachute" + } + ], + "enabled": true, + "id": "e21d976d-7bfc-4c44-94dd-14f8c6c952a9", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "cda2c24d-08c8-4e4f-9221-c07c1c835ee3", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "5312a8c4-7f1b-4a8c-9e23-7315477a3d33", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "parachute" + } + ], + "enabled": true, + "id": "2e10dbcd-2e85-4989-ab4d-43d704abd31c", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "e24d8fd7-964f-4eb0-a06e-df910e4c145b", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "c17072ce-8e48-4aa8-8861-e8d3ee96456a", + "name": "parachute", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "d272995b-285e-4274-bbcf-843eb11a68b3", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "e77db443-252a-4cab-9adc-b07e1b4ee50e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "9e3c5257-a93d-4712-be77-c51d399b78c3", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "plant" + } + ], + "enabled": true, + "id": "39d070ee-5801-4bf3-a89f-fc9665f67372", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "bad5dee2-4a71-4ce6-a564-88e0f1d74d7b", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "98abb8bd-5e65-4083-980a-cf79214f238d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "plant" + } + ], + "enabled": true, + "id": "104bc6ec-a929-458a-901f-81a2a34d533c", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "d773d5e2-21c3-4c6d-8067-b01286636567", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "1485c352-cb6b-4d0f-9da7-9acc0db17011", + "name": "plant", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "c882fd16-5f2b-4253-879d-a27af79c5276", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "064c219d-12a3-4a81-885d-1dbc24f2599b", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "c9ca453a-4ce9-4bff-b276-6bdb09e0f86f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "poo" + } + ], + "enabled": true, + "id": "49098dda-b1eb-4971-8f4b-a57e328f9888", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "ca0284fb-2a12-4f33-a71a-5cef0eb4b4de", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "7d68d9a6-cd3a-4d61-8959-d1aa5a99daae", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "poo" + } + ], + "enabled": true, + "id": "e8b978f3-7b36-47c8-97da-c6ba58af1e2f", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "4e651c59-6de2-442f-8289-6664e63662b5", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "3ea1a8e5-83cf-4a4b-9300-2e39e7ea6ff5", + "name": "poo", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2152cece-198b-42fb-9c33-50342ae5f4e1", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "4dec16cf-7d24-4eec-984a-b2769299e5b1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "fcf548f0-a0ee-4de8-b37d-169a328ade19", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "_rain" + } + ], + "enabled": true, + "id": "039cd984-8961-4818-a310-d076b49d008d", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "475d4e72-b34d-4752-a9fd-fd8064837662", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "3d868837-4829-4eea-87c1-16adf598fa5d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "_rain" + } + ], + "enabled": true, + "id": "4d6c4ef8-e00f-4bba-a167-9ab7ef2c4cf1", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "cc5c5b77-2128-4157-b365-e937353bf801", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "aa1289af-dd78-49cb-a1ae-24b9efe8d422", + "name": "rain", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "bd773adc-b8ef-4089-832c-42e6b32842e3", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "9f0614f7-e531-4f6b-9d17-4a5216c27486", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "a2b41617-7836-40f9-9308-335e06addb7c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "rivers" + } + ], + "enabled": true, + "id": "7fbdc477-12f1-4205-9a5b-bc6227044538", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "d7963147-a278-49ea-a338-590fe2779af9", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "d5b496bc-743e-4010-9b44-192462cf0490", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "rivers" + } + ], + "enabled": true, + "id": "58066891-5a75-4a9c-b838-ca49f13362f9", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "9deb8a74-7e03-4ac1-822f-fdfae1611042", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "68e29c75-a419-4bd1-b7c7-d36906854da9", + "name": "rivers", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "d68814ad-c198-4646-8229-e14edc985891", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "9cd27fde-a77e-416d-a9da-c0567cefe9b4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "8bf20b88-d687-49c0-b4ba-2c65fb3664f7", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "roads" + } + ], + "enabled": true, + "id": "130c2e14-dcd9-429d-8b8c-8190f804f05b", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "c861256c-9eb1-4f20-af81-4659da6a3d29", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "9ec67c8f-f881-476f-b981-206e84e58d5b", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "roads" + } + ], + "enabled": true, + "id": "93bc2f14-aefa-4a2a-8a20-5a019228cf7d", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "87f3e0bc-d152-4d1f-bf5d-2d66ae83afc1", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "d3c841a7-bb4d-4bfa-861f-f47cb352ede0", + "name": "roads", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "77c4f648-dc56-44b9-b472-77f62a551e82", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "f162e429-321e-4632-a517-6c19ab2980a1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "2a3839f5-5609-4451-a3e9-25ab0d39284d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "sand" + } + ], + "enabled": true, + "id": "33d9ba73-66f3-4294-843d-7af1bea37a34", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "c7fe921d-439f-4bca-9631-8bd0be69d3d6", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "c0f7cb3f-48d8-4b3a-b729-5730e5d0398c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "sand" + } + ], + "enabled": true, + "id": "e3f4143e-ee3b-4eac-97a5-e07becff54a5", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "7189bcd8-76ca-4baa-8a3a-99868bd326db", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "d89dea59-50c6-4099-a558-b82153bd2d37", + "name": "sand", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "145b296f-093b-43a7-9116-7c2f397a2242", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "123f80c0-f5dc-4eab-9b4c-59f771444f11", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "5f493036-0edd-4382-a900-4508b5e8f7df", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "sea" + } + ], + "enabled": true, + "id": "ea7becbb-7b2f-4489-a7d9-3202581f9783", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "3e6c5161-0aba-41df-a706-ca9b33c785a5", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "f9ffd325-2270-45fd-a7a0-94e495e8c1cd", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "sea" + } + ], + "enabled": true, + "id": "63fbc175-34ec-41e7-9e0e-28907fc2a436", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "df364836-7a23-47ad-b18b-2aa0a5e2edf1", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "b7564721-299e-4ba2-a019-6c99c00170a0", + "name": "sea", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "62f65e28-7e88-41d9-a11e-1a4739f44dcc", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "a861a844-9957-4340-9ed1-478dac44b9ce", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "73f35ac2-256f-4a91-8858-36fabe20ade2", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "sign" + } + ], + "enabled": true, + "id": "858c25c9-3d49-45c3-b2e2-3ab6498a9852", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "99afe186-b7d0-4f6a-8626-d2cc0e8c20e7", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "955c41fc-dceb-4208-99ad-7806318d2ab1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "sign" + } + ], + "enabled": true, + "id": "048212a1-90ef-4e73-82c2-8afc8515be5b", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "bcf29632-2145-4940-929e-3709dc7c2cf2", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "6484c8f4-f08a-446c-b7fe-170b276f6cb7", + "name": "sign", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "072efc75-884c-48a6-94dd-e0638747dcc5", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "d83f05cc-067a-42b0-8ee2-fdccd661825b", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "6683aedd-6262-4140-9b42-8472d0b948d5", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "sky" + } + ], + "enabled": true, + "id": "ec5d7d78-362d-4f83-8a3e-74951922e18e", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "239421cf-ea44-4a0d-b00a-f562a7764607", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "5848c853-2cde-4634-9fa8-3f58607dcc67", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "sky" + } + ], + "enabled": true, + "id": "8bdd92f2-806c-4eb1-b6a0-0c3e3f8bd0ef", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "b352a12e-65ac-4c94-b1ec-6acd28621c58", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "0b95411d-0d9d-4b0d-9915-86854c36a53c", + "name": "sky", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "dbd1e997-d39c-4a92-aaf5-81e2fad45358", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "7d38928c-eb02-4b17-b355-8f964f7ed1d3", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "e2c6862a-f437-4895-b830-b884f301d066", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "smoke" + } + ], + "enabled": true, + "id": "dbc02386-d897-4c5b-b19b-c1ede6e6e970", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "65f683f5-dc75-41ff-9bf9-6df9dadbdf6c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "42868bbe-9dbe-4b50-8dfe-d8ab5aab2a74", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "smoke" + } + ], + "enabled": true, + "id": "47c3d484-e19b-4bb1-b32e-34a5971a50d2", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "ea69ff39-17f6-42bd-8a31-daae5a29ddec", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "646a9628-8b1e-4b1f-b1d6-7cbf374776de", + "name": "smoke", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "d74549bd-5575-44e5-bd3b-641ea5ba8c6c", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "fffa35de-7921-4c5b-9684-9d7e05717ab3", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "9b3aa7f1-0dcb-4dd1-8cd1-418477ffd5da", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "snow" + } + ], + "enabled": true, + "id": "e9cff095-3e91-488c-9d4a-a4857a81721e", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "4ff12dee-2425-4dc2-b0b8-6b567b5fb737", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "d15306b2-9832-47e1-8555-5d9ac4ab2e05", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "snow" + } + ], + "enabled": true, + "id": "1615b44d-cb35-4f2a-b14c-e4a0a011111c", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "431dee5e-2122-4b4c-8591-43c301ef3a07", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "91df711c-e886-48ec-a65a-f0a49220c47d", + "name": "snow", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "dc73c3e0-6536-45bc-8140-98d0e4f1b4de", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "a41dc91d-65a4-4ffd-81cb-5f3ab8224bef", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "abf9653c-9baf-447e-ac48-922000912cdd", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "snow" + } + ], + "enabled": true, + "id": "d70999e7-fb63-4dad-9268-5c046d7aa344", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "3d5f34fb-64e8-4338-811d-7809273e9e17", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "68481e77-c10e-4efb-8048-643d0e1be190", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "snow" + } + ], + "enabled": true, + "id": "e54153c6-cf70-46be-803b-30acc1622913", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "d187df0f-4389-4f03-bc99-3fae4679729c", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "5b1e8767-017e-4758-b0bc-ace0e94f2189", + "name": "snow", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "23464310-f027-42d9-937c-5260752f2250", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "67ffbb6e-0c64-438a-a4e4-75e7f7844b43", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "c422b9d6-f443-401b-995a-0175d1691740", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "sparks" + } + ], + "enabled": true, + "id": "b92cb21a-05c9-436a-aaf3-827491de33d6", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "9ee085a7-3c1a-450f-be27-5a46ea01c830", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "0c4c5dcc-5c29-47f9-98e4-533b6defe90c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "sparks" + } + ], + "enabled": true, + "id": "1090a58c-1461-4a91-b985-c5368454864c", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "e5ad1488-14eb-4358-aed1-36184c1103bf", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "49b80bb4-352d-4412-b1a3-f6dc8c19848d", + "name": "sparks", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "6a1374e9-1bf4-4cd2-9f2a-2bba5797e04b", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "37cd6759-f402-4e97-b175-dfe440b96979", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "1a846900-bc31-4321-92c5-b1540a961045", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "steam" + } + ], + "enabled": true, + "id": "8f7c3661-4488-4b95-9874-5bd0cefaa0cc", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "c831f55a-2084-4be3-8f37-1346599c91db", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "2508b745-a25c-4e9f-8fba-6e67af81b8cf", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "steam" + } + ], + "enabled": true, + "id": "c546b6b2-76c3-4370-8019-648a8427b34f", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "ef8d6bfa-71d6-493a-bc6d-6ba7c4f2272b", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "d53639d3-3b12-453a-95ea-c2ef03f193e9", + "name": "steam", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "53cc9b14-5706-452e-8f70-a860d2cfeacc", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "a63c784a-ecbd-47a5-abe2-2739ddcc6524", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "5c9340ee-ae21-4090-89d4-1dd31eb156d8", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "structure" + } + ], + "enabled": true, + "id": "6986599e-7f97-4c6c-8df6-2f615acdc84f", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "00a416a4-be88-4d03-99d3-59b6b55cfb18", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "22f15958-5304-49ac-8295-0810faaa8f15", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "structure" + } + ], + "enabled": true, + "id": "4ae1970e-67c4-4c90-9ba5-a23c264eec19", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "f52a1138-ce5a-4d66-8630-d8dbc7d5b5c9", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "2000bd2d-3623-4180-8ff0-708e6368ce20", + "name": "structure", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "b8bdc1ab-6d57-464d-a566-f247439ce747", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "6021e037-f2cf-4cc1-b506-d5d065e96c60", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "d0eddfca-c6b2-4cdc-99a7-ee51aa22e057", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "tendons" + } + ], + "enabled": true, + "id": "38981cad-a6fd-4cee-a714-55b6284f15d9", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "53bfaff5-c4ba-4e7d-83c9-1dace4f4b164", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "6b87ccf0-e1f6-4e07-84bc-3401f7c02a32", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "tendons" + } + ], + "enabled": true, + "id": "61573fc7-fc82-4138-a9bf-7a00a391fda5", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "25ef0d96-ee0f-4126-ba9c-bd491a71d6cd", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "272111cf-d520-44ca-87a3-3f5b5b65b1b6", + "name": "tendons", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "f88ae053-a847-4e53-bf8c-802c9ed08826", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "a6652d03-89cc-4275-b859-8d172ccf0a5f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "803c1e4b-0e4d-45b4-9f8d-1e524c99fc2d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "underwat" + } + ], + "enabled": true, + "id": "9ab3df55-59bb-45b1-9949-87584c0be2b6", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "cfef2fcd-7290-4cb9-8872-565effa6af7f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "2e60d289-fc17-40cd-b596-90d90e3d902a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "underwat" + } + ], + "enabled": true, + "id": "53b031cf-331b-48ab-a8c8-e4507d7b6a09", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "c3208f69-4ada-422d-bd52-3fe528afc739", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "f745dbf3-d523-4d82-8332-3a68aa839394", + "name": "underwater", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "9b7eabea-6024-462f-8e39-c60f0add6181", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "3dc727f5-46a8-46b1-b8b3-a94bdf1e5527", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "e12159cc-082d-4b44-aea8-02f12a00cb16", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "wakes" + } + ], + "enabled": true, + "id": "d0f896a1-8076-4766-9996-9aadbbb40a52", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "3a13cbc4-6802-4114-99c7-6d530c7d81ed", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "301a1916-39a6-44f0-864d-687df37abc5c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "wakes" + } + ], + "enabled": true, + "id": "2d0c0691-0985-4c75-bbf0-361e385eddba", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "826d6035-3510-4cfd-a0b6-2a5e56580b6b", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "c8e5d8c5-e478-4893-aaf3-f59503f27371", + "name": "wakes", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "64c02464-e6b8-4173-9004-a5bfcfbec2de", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "93c41c1a-341b-4deb-9d4f-4856d7b913b2", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "b68c4f7f-4425-415e-895e-bec7f8db7e91", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "water" + } + ], + "enabled": true, + "id": "2c2586ba-d171-4ed8-a923-af6b9fb729b8", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "6318be73-4b06-4a89-8ce2-588c7a6e8983", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "b52e215c-730d-49ae-aa62-5528e805b360", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "water" + } + ], + "enabled": true, + "id": "735a265a-b93d-4a8c-8e3e-ffb671dc7640", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "5407c2ab-2304-4576-91bd-92fcb7e3c2f5", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "0e1d87b6-3b53-4d1c-81f4-d22a2f7e403f", + "name": "water", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "9dfded60-ba35-4168-8277-ca23d5e31c5b", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "8cef8d82-6d89-4410-8c15-268d81316ff5", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "c7e7727a-c10e-424c-b4a8-b224762e438d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "vegetation" + } + ], + "enabled": true, + "id": "9b064b50-02a9-45ab-8513-a77ed2625009", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "edc66d79-7667-4000-a172-6a674c2109bd", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "e77f5494-071b-45b8-bf1b-14898e739cd0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "vegetation" + } + ], + "enabled": true, + "id": "93ae023e-129d-4fec-84f5-6dc471086920", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "e76c8ebc-3ffd-4140-93e2-aa9b310c0d83", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "c5462169-1117-493b-ba07-1a87833bce70", + "name": "vegetation", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "5a6d3119-1c0a-440c-a667-892c27ecdb3b", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "4b3dce0f-62ed-4123-91a3-1bf254cdb8a5", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "535459d8-bec2-406d-b286-6bf816668cf0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "vehicle" + } + ], + "enabled": true, + "id": "dc9d6950-dc90-4f7e-a2a0-455063ce466f", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "040200b9-4534-4cb6-9ce7-0317b87b14c0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "216cd6e0-9b03-4b74-b0f0-311601fb05c0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "vehicle" + } + ], + "enabled": true, + "id": "8f6344ae-fcb6-4dba-a719-b4184983d4a7", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "c4311b70-dcc8-421c-a1cc-18c61c530aec", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "2fe9a682-2b05-4ad4-ac6a-766f84ae068c", + "name": "vehicle", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "8a7819bd-9420-4bb9-ac82-f3e49e40a2da", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "ac709790-791c-4e3e-9a9c-90915f37d763", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "ab87752c-02c7-4dae-bab2-9b6373e2b184", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "washin" + } + ], + "enabled": true, + "id": "328cc10b-8f4f-4832-b037-54c90350b033", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "490a6933-19c9-492a-aa28-535533221402", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "703dde00-96c4-4419-a4c6-97bb4fea59fd", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "washin" + } + ], + "enabled": true, + "id": "d7c96e92-6990-417a-9da2-4828d668dd20", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "ec118ef5-a94b-4232-94c0-392fb95ca155", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "b95c37e7-d676-4261-8f20-5927bf51f829", + "name": "washingLine", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "5e1049ea-d447-4d14-b49e-fc29c44a2026", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "fc2b2dfe-ca85-44f6-8eed-3c16a9a6f0cb", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "fbb3c086-51dd-4da4-a868-2abc39fa58f1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "water" + } + ], + "enabled": true, + "id": "0976056a-633f-48ca-b0d5-4d805f0ad251", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "ef6e8072-bd2b-4dd2-ba9a-ba0fc72ab9a1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "d0ce476f-b0fe-4cb1-bdb2-4298ad49e664", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "water" + } + ], + "enabled": true, + "id": "eee231d6-2910-4c0f-8e1e-95f4d0bdd9de", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "84004ad5-2c8c-4db1-9ca9-06320d84140d", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "f7ae5cbc-f71b-4ee7-b1c8-a9b7687cfdaf", + "name": "water", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "0c8cc080-4697-42cc-b669-0b05eb5bd747", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2500" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "961b414d-e407-4f1d-bbc7-076e0124af49", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "E_element" + }, + { + "enabled": true, + "id": "2dd82779-a05a-41e2-925c-e0928756751a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "waterfalls" + } + ], + "enabled": true, + "id": "94cdf8b6-65fa-4049-96d8-dfe82ccaaf73", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "a4d192a4-e779-4bac-a347-947ebbaef665", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "S_element" + }, + { + "enabled": true, + "id": "06253365-d044-4c94-9bd7-6190887386e1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "waterfalls" + } + ], + "enabled": true, + "id": "c641325b-f664-4b5e-98a9-c8117437d1de", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "5feb1af0-1e63-42cf-a46b-91ec6f47fd0a", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": false, + "hidden": false, + "id": "ddedf3fc-3c41-4067-bef1-bbf53e2890ec", + "name": "waterfalls", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "3289f724-751a-46ad-9e22-a9a8a5ccc8c2", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [], + "hidden": false, + "id": "198817b0-abd7-4e7b-baa5-5d88f8fa8ea9", + "name": "Library Elements", + "type": "group", + "update": false, + "userdata": "recent" + }, + { + "children": [ + { + "children": [], + "favourite": false, + "hidden": true, + "id": "57b98357-1036-4052-84c3-4aa003d93452", + "name": "OVERRIDE", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "708e077e-f9e5-443b-acd1-260260c4540a", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "2d266904-3b55-4ad1-8d3a-f0ed0e666ecc", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "710f2b58-cd91-4a6a-9b7e-44e5b3eb19c3", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "witness" + }, + { + "enabled": false, + "id": "67e18974-6e3c-469a-99c1-736c0fc0e15e", + "livelink": null, + "negated": false, + "term": "On Disk", + "type": "term", + "value": "${DNSITEDATA_SHORT_NAME}" + } + ], + "favourite": true, + "hidden": false, + "id": "e737a4f3-6fd2-4eff-b783-ac9a62549585", + "name": "Anything : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "4b4fe114-27e1-4298-8cae-8027a782ebf1", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": true, + "id": "2b448fed-af44-4a67-8155-07fe0477ae26", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "24a3ef97-8599-4eb7-8e9d-750d3c53f1c2", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "3994c48c-4f01-44ce-a4f1-adb937897dd3", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "qc" + }, + { + "enabled": true, + "id": "a2f251ef-5ce7-4703-93dd-46742d35d71b", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "582bc4ed-0b8b-4c0a-9a9c-63716f17540a", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "2372bf89-cb16-41da-823f-5f543dc8a79e", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "anim$" + }, + { + "enabled": true, + "id": "6278832c-0008-4ad2-80c3-765776007d9a", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "07d3e331-2f24-4c31-ae6c-fbacc286070f", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "pip" + } + ], + "favourite": true, + "hidden": false, + "id": "e5169cc2-b9e5-47d1-9c0e-5e5ae15efe4a", + "name": "Anim : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "cf8cd249-ab97-456e-9bb4-f977b842761c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "c0109d00-76b9-4f28-9e6e-1970c2352b3e", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "81ec1d03-ec6e-4a72-a41c-055ee7ecbd57", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": true, + "id": "8167981e-5097-4f8b-8825-4bc04b3fe6f7", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "82883f73-3955-479d-bd12-b1d1c033014f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "95c3e9b0-ec11-4cae-a9d7-f4334e47ec08", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "c621f93c-341b-4489-bdf4-8ba606342223", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "qc" + }, + { + "enabled": true, + "id": "ff28a97e-6cbf-4fc1-992c-d49e7df3c9a0", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "df7a3e80-368d-4151-aaef-d71c707242cf", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "e462b8cc-d72e-4836-affc-96e3b1469594", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "anim_pip" + } + ], + "favourite": false, + "hidden": false, + "id": "f007821c-d5cc-46a7-8a59-75b7678ec1ef", + "name": "Anim : PIP Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "e06bbcc5-c42d-4850-884c-84741cf10316", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "6347db7f-a413-4ac3-918c-1d57a75b5cef", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": false, + "id": "3355842e-2777-484a-9b35-7af3d9d22345", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "e5bb76c0-66fd-46ae-96d1-91249ae19b7c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "3c36c6dd-6e16-42e7-90f0-2d9fa81caa02", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "d00afa52-42fb-43dc-a308-0a71658ae75e", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "qc" + }, + { + "enabled": true, + "id": "60358730-15c8-4b52-8586-d6a1a0d50007", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "9f2c0052-6d41-43dc-a214-a5688aba25f5", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "83357398-1e1c-4c95-8da7-5378e5e4285f", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "anim_clean$" + }, + { + "enabled": true, + "id": "641f7192-398f-4b7d-b877-4f1d22120e35", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "anim$" + }, + { + "enabled": true, + "id": "7c354080-4b23-40fd-abfd-643bcb37eaf2", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "752e5163-937d-46c4-a374-00a66f182b7b", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "pip" + } + ], + "favourite": true, + "hidden": false, + "id": "d7afe7f2-dc08-46d8-ab2d-7659b300ed48", + "name": "Anim : WP Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "b08e928a-b714-4777-97c7-d1420e85c296", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "354f88d2-6b3b-427e-9514-6585f28fde8c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "gesture" + }, + { + "enabled": true, + "id": "c42de616-bec1-4b7a-8ec9-7eb4e1885849", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witcam" + }, + { + "enabled": true, + "id": "dc284bb4-2fed-433b-9e2e-6526eb5e1fe9", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "2731800e-bfb8-4d31-9640-9039c4878e3f", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "ecb98771-2dd3-400c-83ec-4065961a9225", + "livelink": false, + "negated": true, + "term": "Twig Type", + "type": "term", + "value": "audio" + } + ], + "favourite": true, + "hidden": false, + "id": "4956a7f5-084c-4c49-9cf8-f6fc2cb44b75", + "name": "Anim : Ref Latest ", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "5b350223-804a-4732-ad82-51cebe08ddc8", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": true, + "id": "a1328441-6802-4584-a5de-96669ad4223f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "2f273373-3b8d-4e2b-b77f-2267b8dcd00d", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "0e8a22be-d5d5-4187-9b81-d63f48cf166d", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "qc" + }, + { + "enabled": true, + "id": "5f83bc19-6b2c-4e81-9c4a-10d126b48bdf", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "d34f1b20-b9bf-4048-8420-72c4eeab4c05", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "f83867de-0df1-43b8-819e-1534bd548cbc", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + } + ], + "favourite": true, + "hidden": false, + "id": "93aa3a9b-d1bc-4107-b4f3-88b0df898320", + "name": "Anim : ShoCo/Slap Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "d7300a7d-7060-42f9-ab26-bf637f233735", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "1bde0bb7-f979-4171-b324-41aad1349705", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Body Track" + }, + { + "enabled": true, + "id": "d593f8fa-c489-442d-ba0a-f8308e605165", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "2" + }, + { + "enabled": true, + "id": "c2c4e37a-3a86-4c36-8a58-4ffa0dd407c8", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "wireframe" + }, + { + "enabled": true, + "id": "42de15bc-552f-482b-96b9-afd24b962df7", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "outline" + } + ], + "favourite": false, + "hidden": false, + "id": "f930558f-3bd9-4168-ba9a-c3440c4108bb", + "name": "Body Track : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "6673579b-44e7-4030-9271-32e9ea3255b9", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "c22d1db7-c864-4c9d-97c6-5058620a95a0", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Camera Track" + }, + { + "enabled": true, + "id": "53dfbf1a-c831-4bd8-811f-5f64b5b6b22e", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "147b76e7-c7c0-44ae-9e57-8317181a366f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "wireframe" + } + ], + "favourite": true, + "hidden": false, + "id": "b0984004-ff32-41d3-952d-0d91a9425c66", + "name": "Camera Track : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "fa31f32b-894f-463d-b64e-049598e11224", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "ea55c587-1457-4339-8c5e-67a63cecce66", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Creature" + }, + { + "enabled": true, + "id": "ae7b008a-8939-42b7-b82e-f9ca709cc31f", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Creature FX" + }, + { + "enabled": true, + "id": "1ad88a7f-8ef4-428d-ac46-31fe2b1a6282", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "7e85973b-7a40-4a41-ade1-682594232bd5", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "8" + } + ], + "favourite": false, + "hidden": false, + "id": "40a01d15-3fc7-4dd6-a2a4-1f1a6d990265", + "name": "CFX : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "8a181af7-6644-4eaf-9d9f-afb4d95d2319", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "8f2b5288-097a-4e5c-94b2-1e82a93a5562", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Comp" + }, + { + "enabled": true, + "id": "d701f1f4-50ba-45cc-84fd-8f2ae2620847", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "comp$" + }, + { + "enabled": true, + "id": "0a0a73b0-cc60-49e5-a460-4dfafcd16e3a", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "^O_" + }, + { + "enabled": true, + "id": "6dcbef74-38fe-476a-b0b0-b0de20af62e4", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "35f4a735-3704-4a3f-8d02-c7c2452f303b", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactsheet" + }, + { + "enabled": true, + "id": "dc846ceb-a34c-4751-851f-299e052572b2", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "movie_dneg" + }, + { + "enabled": false, + "id": "5fe9cf0d-b2f0-4489-9bcf-6ae8867ab356", + "livelink": false, + "negated": null, + "term": "Preferred Visual", + "type": "term", + "value": "review_proxy_1" + } + ], + "favourite": true, + "hidden": false, + "id": "e9d836f2-4553-4c8f-a9e8-1f73a1a2489b", + "name": "Comp : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "d3fa0460-581a-42e3-ad36-eafc4782c12b", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "87c29153-6e02-4659-8ef6-924ef6d23eb8", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Crowd" + } + ], + "favourite": false, + "hidden": false, + "id": "203f28ff-9a8e-4c8c-9e8a-c3e5b72531cf", + "name": "Crowd : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "a65226f5-31b0-4fc6-8104-dcde95b3eb9e", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + }, + { + "enabled": true, + "id": "55980725-5762-4fcf-b873-ab59a379e7aa", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "f72f8815-8b91-4cb4-a5af-1f8835b9a017", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Client Submit DESC" + } + ], + "favourite": true, + "hidden": false, + "id": "47169f5f-b26b-4230-804a-7217bc12f1af", + "name": "Client Send : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "3ff0e73f-ef59-4a13-8299-58efe8c9b269", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + }, + { + "enabled": true, + "id": "735ca25a-7ced-442d-997e-ca236ac0f98f", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "8d211397-12e0-48b3-917b-c4d3821435f5", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": true, + "id": "5e71a019-6284-449d-a696-81d1c32275dd", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "pip" + } + ], + "favourite": false, + "hidden": false, + "id": "99db88b8-bfb8-46a4-982e-55a14476b0cd", + "name": "Client Send : Anim Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "1a8600a5-cbc1-46fa-8cc6-1eb82e65891e", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + }, + { + "enabled": true, + "id": "ccb011a8-ccba-47f0-b1e4-fe0093cd6f9d", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "d39a7a65-4197-4ef4-b0b2-c03bed1cb46d", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Anim" + }, + { + "enabled": true, + "id": "da302da0-afc1-4a10-b479-d0ff004bc1e5", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "pip" + }, + { + "enabled": true, + "id": "2c0d6da0-2705-4ddb-9153-081fb7243171", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Awaiting Feedback" + }, + { + "enabled": true, + "id": "74b950bd-df16-422d-a0f8-c859ba686e48", + "livelink": null, + "negated": false, + "term": "Production Status", + "type": "term", + "value": "Awaiting Anim Dir Review" + } + ], + "favourite": true, + "hidden": false, + "id": "b44e688f-6f32-42c0-8dde-5c7ae8766121", + "name": "Awaiting Feedback", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "0a2c1570-1436-49bd-990f-ce4d4539e2c6", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Dailies" + }, + { + "enabled": true, + "id": "8acc2ec2-ff40-4304-a77f-07e5f23f804e", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "7d2ae786-3b14-4eba-80f9-3765228664c0", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + } + ], + "favourite": true, + "hidden": false, + "id": "9bc92d34-9724-465d-81ba-fe012e11022b", + "name": "Daily : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "93b19b55-42da-491a-bd70-73939a83d315", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "84976f9e-b090-4b93-a729-5d3f7ffc88f9", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "DMP" + } + ], + "favourite": false, + "hidden": false, + "id": "9ee7fddc-9627-4cda-bab3-c6729d168c62", + "name": "DMP : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "c232c855-9127-4e78-860d-2adb030bc66b", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": false, + "id": "1a908bd2-2567-4558-883b-98b33cc2a164", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + }, + { + "enabled": false, + "id": "a090487b-e3d3-4d56-b34a-32fadf213dcf", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "edit_ref" + }, + { + "enabled": true, + "id": "1cb13e06-5d65-4fa3-88d1-eeeb0ae7ebe8", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "6694348d-0ce5-4974-80b8-e1a7c2c7699d", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "edit_ref$" + }, + { + "enabled": true, + "id": "d29f5c25-a9d1-41c5-a157-90f6985ce01b", + "livelink": false, + "negated": true, + "term": "Twig Name", + "type": "term", + "value": "hero" + } + ], + "favourite": true, + "hidden": false, + "id": "32a3eb7d-410b-4fa5-9d7e-802c7e803fb5", + "name": "Edit Ref : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "9c501dcf-accd-4ce8-ab18-fa7e1cce9a61", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": false, + "id": "492a1c7a-79dd-4fa4-b3dc-aac442d2a69d", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Editorial" + }, + { + "enabled": false, + "id": "3ac3c00c-fece-4eca-98fa-1a14b389ede7", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "edit_ref" + }, + { + "enabled": true, + "id": "7e1dd710-9a49-460f-a2f9-8e5964775706", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "8e677818-6c5f-4fb1-90ee-a5b59ae0798e", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "edit_ref_storyboard$" + }, + { + "enabled": true, + "id": "ff6ec6c9-6bb7-4f1e-8e63-ab0e1debbb8f", + "livelink": false, + "negated": true, + "term": "Twig Name", + "type": "term", + "value": "hero" + } + ], + "favourite": false, + "hidden": false, + "id": "f0f106fe-c5da-4a07-971e-be90b1ebc8de", + "name": "Edit Ref : Storyboards Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "101f5bc3-e458-40d7-a04d-fadd91b9643d", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "d5c667a5-7471-40ed-9384-731d9b7625f1", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "2d9d8a89-10ce-4327-8598-c5a42b1dab97", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "heroplate" + } + ], + "favourite": false, + "hidden": false, + "id": "16ace936-4375-4713-b206-08a9d843415d", + "name": "Edit Ref : Hero Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "a8688abd-26c7-4ec4-8b84-f976c1bbe2a6", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "2f74489e-8eae-40d3-91af-b58fce6210cf", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Environ" + }, + { + "enabled": true, + "id": "92cfba35-1bfc-48fd-aafe-ecb9ed2fc680", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Envfinishing" + }, + { + "enabled": true, + "id": "eef4d869-f432-4775-90d1-02e3cbbf43ca", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Envsetup" + }, + { + "enabled": true, + "id": "b540d9b1-9123-4659-b1eb-13e64197b2ef", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Digimatte" + }, + { + "enabled": true, + "id": "134b27a7-6552-41ad-bda3-cb5ae717d691", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": true, + "hidden": false, + "id": "5581e165-8795-4f50-a403-afda1d3455e4", + "name": "Env : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "fcda83b2-d9ef-4cdf-846a-5d4672d7c1a9", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "d8ad6066-eee1-4ce0-a42d-ebdb5b4efaf5", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "FX" + }, + { + "enabled": true, + "id": "3a0be9ea-e4f2-4d40-93f6-561796236e46", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "8" + } + ], + "favourite": false, + "hidden": false, + "id": "866e9d49-201d-4992-a276-9e288f1f598e", + "name": "FX : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2a6aa1e9-34fb-487a-aff3-d5a2632e5864", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "937c537e-da4b-4d39-b608-75345297fcaf", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Groom" + } + ], + "favourite": false, + "hidden": false, + "id": "4a2bc4bf-2652-45a2-97f3-ed741264d259", + "name": "Groom : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "3ff59789-b179-4b4a-a67e-d31844737a9b", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Lighting" + }, + { + "enabled": true, + "id": "2477baee-0a63-4619-924f-6e3d5047859e", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "acd5b399-f091-43ce-9e25-0b55c4a8cb34", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "CG_" + }, + { + "enabled": true, + "id": "234fc97a-07c8-4622-b2c7-8fedf22d21f8", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "7ccc4592-ab36-46e2-8dc2-2c3423d304c0", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": false, + "hidden": false, + "id": "86519a7c-522b-4fe6-9796-a9e3fa385f9c", + "name": "Lighting : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "0850ee4a-14a8-4f36-a429-1c4ab0901048", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "2f5e7b94-09d8-460f-a0cd-2bbe61ff3a8a", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Lighting" + }, + { + "enabled": true, + "id": "41fbeb14-105c-4cda-9d31-95bb13aa9711", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "96e0914b-ba46-4ecb-85f3-ea362f316427", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "ce3214dc-88bd-47f7-a86f-465d507c26c7", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "09be9e48-48ac-431d-b9f9-99cfd0d02fb4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "comp" + }, + { + "enabled": true, + "id": "49c17798-eefd-4ddb-b684-a17afbd1846c", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + } + ], + "favourite": true, + "hidden": false, + "id": "1e35f643-a935-4bbe-9ba9-e447a154e0f7", + "name": "Lighting : Slap/Comp Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "12bcb422-20d6-4ecb-a715-76f75a3bc10d", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "efe1d694-46e6-4e5f-8738-a077646880dc", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Model" + }, + { + "enabled": false, + "id": "7918fde0-bc5f-49da-89d7-0023e0b9e1dc", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "f387806b-d59d-441b-87e2-5d9a7aaf6ef9", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": false, + "hidden": false, + "id": "46979be3-4902-4b0a-9758-b7e9f4e41c54", + "name": "Model : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "9e4ff350-5abb-492b-9068-67698611ba95", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "ff9f124c-a526-4a30-9a7b-f15723b61c89", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Muscle" + }, + { + "enabled": false, + "id": "1cebb740-5b3a-404c-a44f-83f0359b7134", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "d6a0da16-bbb2-44d2-ad6e-8ab30b7fcb7f", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + } + ], + "favourite": false, + "hidden": false, + "id": "26ee72e3-c352-4c86-aa68-a4b61fcb321a", + "name": "Muscle : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "ff925d54-737c-403a-bfde-133c2773325a", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "91690900-0bc1-4377-8608-5890d14affe5", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Postvis" + }, + { + "enabled": false, + "id": "42ddfc56-ebf3-4c38-a8a0-79b3bec6838c", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "32e53673-5b51-42f0-831b-d556bedcea96", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "fc72365c-fcb9-44a1-9ba6-f204d384de24", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + } + ], + "favourite": true, + "hidden": false, + "id": "da92ab1e-0075-45b8-b307-32555cacd778", + "name": "Postvis : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "31538ee9-cc2f-4ddd-bb5f-98f20f546e92", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "b8858d3a-7a4c-4cac-be0d-05539c53a099", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Prep" + }, + { + "enabled": false, + "id": "e3610f83-fa26-48f3-b67c-6a6a5a0fc048", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": true, + "id": "1b3d567e-ecad-42ae-8a98-2c6fe9ad48be", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "9b87b589-58d7-48c6-85ec-6749f5f79813", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "6f04c70f-daa5-4a87-91d0-7eb6e8e36759", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "7e6384ca-5ed6-4854-9270-a5d7c346c725", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + } + ], + "favourite": false, + "hidden": false, + "id": "5438e152-6258-438f-be8e-f132baa90b76", + "name": "Prep : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "d81630ee-988d-4114-9a62-369ecf9146ed", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "99999cb2-3945-49b9-bd88-19225516caa4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "qc" + }, + { + "enabled": true, + "id": "b14e634d-cd54-408c-8488-7afe201d55ee", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "0fc3f17e-5146-4a54-be32-d07d12ed6a1f", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + } + ], + "favourite": false, + "hidden": false, + "id": "31903ad1-8065-4b4e-9709-b5790ccee521", + "name": "QC : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2134a8ad-13b7-4be8-b818-fe4f0712b5a9", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "5334824a-77d1-4818-8eff-474c4b861526", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Rig" + }, + { + "enabled": false, + "id": "7e3efaa7-3ddd-4f05-8a93-71c7ef5497c0", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": false, + "id": "8515b4cd-df6e-45db-a7cf-a4fa60ca6e21", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "b66ac947-930c-4ad4-bb66-08bb2248cc8c", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": false, + "id": "ec4f7b22-c67f-4cd4-9902-bbbfef82559b", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": false, + "id": "f740e49d-9f26-442d-976e-affa5ce91b86", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + } + ], + "favourite": false, + "hidden": false, + "id": "96c7139d-5b4b-4bf7-bbde-c8bac7b1a63f", + "name": "Rig : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "bb7a6cc9-afa5-418c-929d-ecfb381e41e4", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "a39ce7f2-b8ca-4630-83b5-64fcd36d6028", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Roto" + }, + { + "enabled": false, + "id": "68b8aab5-a0bb-4cc2-b2b4-6d2f30b357ad", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contact" + }, + { + "enabled": false, + "id": "f419f766-0451-471e-96c6-3e17259ad39e", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "86d413af-d911-4900-91c0-3a576acdadb7", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": false, + "id": "456bf9b5-ec6d-47d4-9db7-52e8e4d6cfb9", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": false, + "id": "97524353-7b47-48cd-981c-de3e175f39a6", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + } + ], + "favourite": false, + "hidden": false, + "id": "54e007b7-ed7e-4a1f-9266-9b16c57ac2c6", + "name": "Roto : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "e9db9b48-fcc2-4e70-859b-34d21a3d23c2", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/2D" + }, + { + "enabled": true, + "id": "0195cbb1-568b-46fc-9cd7-7ed882bfb246", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "82e50e0e-33f4-45b5-86e1-ea5d65cc8597", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "62f4a429-6b80-42a3-9104-643d5368022f", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "6d550762-1e0b-45f1-8c81-b2d3f67b579b", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "retime" + }, + { + "enabled": true, + "id": "dea5cf5e-d396-45cc-93d5-6f77da157ec0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "repo" + }, + { + "enabled": true, + "id": "118d0611-9d4c-468d-8cfc-57aafb1aeef0", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/element" + }, + { + "enabled": true, + "id": "9fa30fc6-0634-44b8-8fef-53a808e66c5a", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "contactsheet" + }, + { + "enabled": true, + "id": "a7ea9d0d-0b5b-4f61-b28b-2dc2ac783c6e", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": false, + "id": "8d99370a-0ea4-4033-805e-bdc622d9db95", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": false, + "id": "73536377-33ef-4da7-aa7d-a0571b77724d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "rerack" + }, + { + "enabled": true, + "id": "df8a200a-ab06-4843-9889-889745f55df3", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + } + ], + "favourite": false, + "hidden": false, + "id": "8d148d39-390b-4c21-921a-3db09da2a79d", + "name": "Retimes & Repos : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "c8343366-48ec-4994-baa1-79c0323b38bf", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "3e102d9f-59e5-482b-999b-55ae84eac651", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "scan" + }, + { + "enabled": false, + "id": "6f6e86ed-3bfb-499c-b111-c9ba4108047c", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "^S_" + }, + { + "enabled": false, + "id": "5e8ceeb3-d504-435b-9cfa-663074b7ceab", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "ref" + }, + { + "enabled": false, + "id": "1ba38405-ec63-4911-bc0d-f4cde8d9ea6e", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "_lr" + }, + { + "enabled": true, + "id": "c9b3d03e-7dad-4e8f-8d1d-530502c6ef05", + "livelink": null, + "negated": null, + "term": "Is Hero", + "type": "term", + "value": "True" + } + ], + "favourite": true, + "hidden": false, + "id": "da1b53a9-256a-48af-aa79-c697e01334dd", + "name": "Scan : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "1d5de61e-7ac9-4b0b-92f7-98b6480bd49c", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "60381e6e-e234-4943-a66c-b86db6ca3539", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "3347568f-87aa-4114-98cc-c108bb41a071", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "c1dffcda-10ad-4cbb-886c-28dce8dcbfad", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "track" + }, + { + "enabled": true, + "id": "0739d0e3-4922-4234-a3a2-796a8a111a5b", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + } + ], + "favourite": true, + "hidden": false, + "id": "bc9e21c8-242f-433c-8794-d9526f10da71", + "name": "ShoCo : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "35b270fa-2175-4cb2-ae72-4bbd19973d1c", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "e292300f-c743-4335-a82f-dba568d1a10a", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "3778b77c-136d-49bf-8671-66b7950b0d4f", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "805f01f1-955b-47b7-b003-b3f5812b4314", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "track" + }, + { + "enabled": true, + "id": "39640921-4c55-424d-becb-80d88ee3b67c", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Sweatbox" + } + ], + "favourite": false, + "hidden": false, + "id": "4227ec33-c65e-4494-b863-5a2d7c810e55", + "name": "Sweatbox : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "8bef925e-c1c9-4754-849e-e17e4c4c3d73", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "children": [ + { + "children": [ + { + "enabled": true, + "id": "610710eb-e4f8-4464-a68d-ebc35d32ef60", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "CUT_" + }, + { + "enabled": true, + "id": "351e1bcd-83ca-40d0-ba78-5cb6ed686972", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witcam" + } + ], + "enabled": true, + "id": "65842ec1-788f-4da5-bd98-7ef8adef9eed", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + }, + { + "children": [ + { + "enabled": true, + "id": "6c352e4d-2d95-4100-8c32-b645b147119f", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "CUT_" + }, + { + "enabled": true, + "id": "18cff435-b9d6-447b-9cf0-54677d9b30f7", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "gesture" + } + ], + "enabled": true, + "id": "0788290a-ed0f-402e-928a-32d97e6ee153", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "And" + } + ], + "enabled": true, + "id": "1ccf83a9-9848-4846-beb3-efd4daca02a3", + "livelink": null, + "negated": null, + "term": "Operator", + "type": "term", + "value": "Or" + } + ], + "favourite": true, + "hidden": false, + "id": "9ad83481-bc8d-4b2f-957f-f65322827c89", + "name": "Witness Cam : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "61de34f7-cfc1-4800-a0eb-280cc035a8b0", + "livelink": true, + "negated": null, + "term": "Newer Version", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "ff3b982a-f6be-44b8-a0ce-e680d299d3f8", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "254b72e7-b524-4beb-8729-084bf2267d8a", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version ASC" + }, + { + "enabled": true, + "id": "b2392d1b-075c-4e3c-9059-55bc4333051d", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + } + ], + "favourite": true, + "hidden": false, + "id": "dbb3694a-d622-4ae4-90f8-e726f29f3e4d", + "name": "Next Version", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "17aea181-d659-4e39-a75f-c6365f134e34", + "livelink": true, + "negated": null, + "term": "Older Version", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "dd1e359d-417e-41ee-a644-0cf1ae6201b6", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "a3825d6d-9f56-4254-9ffc-7d280e6e294e", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "f4076b09-58ef-41f4-9152-4f1d77ba78e9", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + } + ], + "favourite": true, + "hidden": false, + "id": "40126a81-161b-4903-99e8-42c6eb608b0e", + "name": "Previous Version", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "49377c70-4b37-4bf8-8549-d5a91044ba69", + "livelink": true, + "negated": null, + "term": "Older Version", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "553ff425-6666-46a9-8af4-5d091df6ac74", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "188c42d5-de29-4d29-ada6-eab4d4127f93", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "f6642bd5-5af3-4d24-a6bf-1861e1bd6549", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "0c2c212f-8f75-4998-ab80-6a56b709e0a1", + "livelink": null, + "negated": null, + "term": "Sent To", + "type": "term", + "value": "Client" + } + ], + "favourite": true, + "hidden": false, + "id": "e5c36d93-b90f-496a-a322-5f888fc71404", + "name": "Client Send : Previous", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": false, + "id": "a158813f-c287-4c99-bd2d-9225a60314df", + "livelink": true, + "negated": null, + "term": "Newer Version", + "type": "term", + "value": "" + }, + { + "enabled": true, + "id": "047179cc-507f-4944-86e5-ddb8562c86c1", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "d4e58903-50ae-45a3-8e42-7f6b6883a960", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Version DESC" + }, + { + "enabled": true, + "id": "d5502347-a7c2-4023-874e-1a898ffecf19", + "livelink": true, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "" + } + ], + "favourite": true, + "hidden": false, + "id": "7d9f9ca7-4ecc-429d-86b8-eaf33559fa8a", + "name": "Highest Version", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "616540c6-7f24-467f-b0d0-2520bf9fcc4a", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "433e449f-776c-45ff-b442-3fb1a584b61e", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "layout_v" + }, + { + "enabled": true, + "id": "25510982-38e2-4589-9071-1918676f5f03", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "10de05ad-f8c6-4ce4-9735-587719212b73", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + } + ], + "favourite": true, + "hidden": false, + "id": "9a02a06e-6bd0-45ee-a827-bfbf8067b5f5", + "name": "Layout : Approved", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "fcaa078d-de49-4b09-ab81-a89247878b3b", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "e65b2e9b-517c-4428-939f-2cb8da7d6086", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "2a8cb708-4e95-4723-a182-e2cd588cc592", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "layout_v" + }, + { + "enabled": true, + "id": "b91a57b9-ad7d-4035-a72f-e49c97bacbf2", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "7d7cda57-d8de-4bbe-8808-1c9756d1d53d", + "livelink": null, + "negated": true, + "term": "Production Status", + "type": "term", + "value": "Declined" + } + ], + "favourite": true, + "hidden": false, + "id": "013aa112-0cdd-4970-a41b-ce6374c39e7c", + "name": "Layout : Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "814af553-3308-49f2-b399-70783432f37e", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "41633e62-edbe-47c2-b215-2f56e017c147", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "7db3a53c-8ba5-464c-b05e-6cf90387af7f", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "3d2dcffc-6b84-4047-8fab-3438a6f7b227", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "1f529f6c-fe3e-4d89-a366-ed8aa4ab686e", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + } + ], + "favourite": false, + "hidden": false, + "id": "461cbbb9-28f7-47dc-bf20-955d057bc1ba", + "name": "Layout : DeptSlap Approved", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "e928054a-826d-49e9-8c0a-667cad0a3ba6", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "2d3e3020-c115-4d02-b3b4-c43290ca02b8", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "e655cc48-00ff-4057-a098-a71235d243a6", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "3e7fb999-1440-44b3-83d2-747d719029ed", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "7b0ffc85-9bf5-4b52-9bbf-c97395aa689b", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "d454ceaf-2957-47fa-bdb6-46230220fef1", + "livelink": null, + "negated": true, + "term": "Production Status", + "type": "term", + "value": "Declined" + } + ], + "favourite": false, + "hidden": false, + "id": "d505b303-33ce-41b2-abdc-eebb7a06eb8a", + "name": "Layout : DeptSlap Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "79be5613-a264-4483-a0b7-9dfefa170bd3", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "3bd5566d-a0ef-44a5-982e-5221edf62a22", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "SHOCO" + }, + { + "enabled": true, + "id": "286b357e-4355-487f-b756-ccefa04bebf9", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "eb5520c4-f4ee-46ec-80e4-5aa3d5aa609f", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "36a3599d-c7c6-463e-ae22-7d498bc94b12", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + } + ], + "favourite": false, + "hidden": false, + "id": "034ad425-fc02-4397-a9ba-25f78dfe8608", + "name": "Layout : ShoCo Approved", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "448c0fa2-b5db-45ea-b111-b0eb59f80a68", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "3767ca95-5a98-4485-a2d5-5108a50ee62c", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": true, + "id": "c095c77c-a5fe-4eb8-ad6e-4b7a934b0c66", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "SHOCO" + }, + { + "enabled": true, + "id": "24b3ccfe-c113-43b4-8caa-facd410ff95e", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "fa448200-6c8c-446f-9e84-fa2489a1cafa", + "livelink": null, + "negated": null, + "term": "Disable Global", + "type": "term", + "value": "Twig Type" + }, + { + "enabled": true, + "id": "2bfbdfd5-fe0c-4391-a4af-b4ec5879dff4", + "livelink": null, + "negated": true, + "term": "Production Status", + "type": "term", + "value": "Declined" + } + ], + "favourite": false, + "hidden": false, + "id": "dc1d389d-784d-4606-aa95-b0e7b4f2e448", + "name": "Layout : ShoCo Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "f161fbd3-523a-4618-8ff9-358e56d06907", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": false, + "id": "06709619-b086-4efa-8d41-510858f5db3d", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "layout_v" + }, + { + "enabled": true, + "id": "991c4f89-dffb-4e06-88d8-4fe3174d4c90", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "c034a9e1-fb75-48c0-b40c-6f348deb5564", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "tech" + }, + { + "enabled": true, + "id": "be770491-eea3-43f0-afd0-1f15c7c7ff75", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "d799be43-0122-4e95-be9b-d09feb713c38", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + } + ], + "favourite": false, + "hidden": false, + "id": "f51f02c9-3c8f-49b6-9e27-e8f71bb12ee6", + "name": "Layout : Tech Approved", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "2971d9e2-f592-4322-898b-a0cfe6d90f64", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "99042280-c5f4-41ef-a9b1-940d68c4a022", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": false, + "id": "c5fc4b6a-e828-40e7-8066-4053a5096be0", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "layout_v" + }, + { + "enabled": true, + "id": "ad7ba233-ab7a-433f-b94a-911f66a4ce3b", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "2def23ab-2fef-46c6-b572-6d21fbd187a1", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "tech" + }, + { + "enabled": true, + "id": "3b8c4aa4-c6bd-4bdc-b195-a68c57618cb6", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "d7763c94-5619-4021-8163-14c441634576", + "livelink": null, + "negated": true, + "term": "Production Status", + "type": "term", + "value": "Declined" + } + ], + "favourite": false, + "hidden": false, + "id": "b97b871e-e60c-4cfd-9826-50d0d698a569", + "name": "Layout : Tech Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "16148e96-f7d5-4d1f-b02d-ab58deb9c336", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": false, + "id": "3488d345-7663-46fd-8473-f5cae0510ee4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "layout_v" + }, + { + "enabled": true, + "id": "f6fad910-00da-4d23-89e9-9a616d85adca", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "af492f4b-a4d5-4857-a854-5e653dd66ad4", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witnessPersp" + }, + { + "enabled": true, + "id": "12fd6a8d-c733-4ee4-9e17-7a47ad41a4f0", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "33c5b189-33fe-48fd-a902-6d899b9a98e5", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + } + ], + "favourite": false, + "hidden": false, + "id": "a70e4a97-83e4-440c-ad70-c3724b6c8768", + "name": "Layout : Witness Persp Approved", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "07c7455e-aff3-4d2e-a64b-c851b32efff5", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "12586ace-62df-4e80-8823-38410a0bda41", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": false, + "id": "a06dae52-0aba-4eb5-912a-965914741fee", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "layout_v" + }, + { + "enabled": true, + "id": "0b23c677-f334-4616-baee-931db6836a6a", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "4c0f5e87-357e-4552-b721-f189b404dc16", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witnessPersp" + }, + { + "enabled": true, + "id": "33f038ed-56d8-4c00-aaf0-2c5887829c55", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "861c1f7f-7bfb-4a5a-a317-279ba239a5b2", + "livelink": null, + "negated": true, + "term": "Production Status", + "type": "term", + "value": "Declined" + } + ], + "favourite": false, + "hidden": false, + "id": "dc40e1a1-4a73-433f-9c80-e045fbcbbcfe", + "name": "Layout : Witness Persp Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "a72eb390-8f2f-48fe-9476-9c4d76727805", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": false, + "id": "fc8576c2-b907-4697-a1ee-4aeaea667054", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "layout_v" + }, + { + "enabled": true, + "id": "7434309d-73b2-4e5b-bafd-d87c28aec054", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "495f68db-c087-4a53-8535-41d72c007c02", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witnessTop" + }, + { + "enabled": true, + "id": "db4baf36-78a9-4914-8f17-cd524f4cf8fc", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "4356a7fd-deac-4390-b8e0-08f20d257a55", + "livelink": null, + "negated": false, + "term": "Pipeline Status", + "type": "term", + "value": "Approved" + } + ], + "favourite": false, + "hidden": false, + "id": "4e149aaa-887c-45f1-ba24-6d9f03b9374f", + "name": "Layout : Witness Top Approved", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "13368e5c-f0ff-4521-85c2-35d7059229c2", + "livelink": null, + "negated": null, + "term": "Latest Version", + "type": "term", + "value": "True" + }, + { + "enabled": true, + "id": "2817e975-ecaa-493e-9911-797b85456f66", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": false, + "id": "d4a16fa0-edb1-499b-9224-af2d4fb3a4ee", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "layout_v" + }, + { + "enabled": true, + "id": "4e6fd7bf-65b0-4eee-bd7b-42809d54e9de", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "0daeca7e-2667-4973-bb39-847d4a7ed579", + "livelink": null, + "negated": false, + "term": "Filter", + "type": "term", + "value": "witnessTop" + }, + { + "enabled": true, + "id": "31fc7ebb-a56d-486e-9411-c746a5a989eb", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "6a7e4807-9df4-446b-b70c-f4aa2835a1f5", + "livelink": null, + "negated": true, + "term": "Production Status", + "type": "term", + "value": "Declined" + } + ], + "favourite": false, + "hidden": false, + "id": "034c4c35-df1e-47b4-bfcf-a2e1f35fa2d3", + "name": "Layout : Witness Top Latest", + "type": "preset", + "update": false, + "userdata": "" + }, + { + "children": [ + { + "enabled": true, + "id": "13ff5e06-f673-4aa2-9bc2-ab611fd249bc", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/out" + }, + { + "enabled": true, + "id": "e5ec61fb-e960-4b96-8672-89e8e41d6e75", + "livelink": false, + "negated": false, + "term": "Pipeline Step", + "type": "term", + "value": "Layout" + }, + { + "enabled": false, + "id": "9e8404f1-9af5-4a7d-87be-aa121618d476", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast" + }, + { + "enabled": true, + "id": "73da3de2-5b47-44d7-b771-a99302765360", + "livelink": false, + "negated": false, + "term": "Twig Type", + "type": "term", + "value": "render/playblast/working" + }, + { + "enabled": true, + "id": "19a62730-cc78-4e87-a6b0-e3c1fdf38978", + "livelink": null, + "negated": null, + "term": "Result Limit", + "type": "term", + "value": "1" + }, + { + "enabled": true, + "id": "545e0ae0-a54f-456e-a476-818a43abc55c", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "qc" + }, + { + "enabled": true, + "id": "cca2f1d3-e7cc-486d-bbfb-75ec6f2607b9", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "shoco" + }, + { + "enabled": true, + "id": "382e2d1d-bf8a-49fb-b910-3565f93fe114", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "slap" + }, + { + "enabled": true, + "id": "5c444d92-3ccc-460f-b5fa-0c91e9ae9ec2", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "anim_clean$" + }, + { + "enabled": true, + "id": "b15e3f4d-5169-4899-8bf4-279cd5029ced", + "livelink": false, + "negated": false, + "term": "Twig Name", + "type": "term", + "value": "anim$" + }, + { + "enabled": true, + "id": "5cdb50d8-2be7-41e2-97b2-289d57e714b5", + "livelink": null, + "negated": null, + "term": "Order By", + "type": "term", + "value": "Created DESC" + }, + { + "enabled": true, + "id": "a2be2be8-8446-4517-a93d-9f7beef66713", + "livelink": null, + "negated": true, + "term": "Filter", + "type": "term", + "value": "pip" + } + ], + "favourite": false, + "hidden": false, + "id": "9374edd9-cd6f-4fb5-8c3a-345d92cde406", + "name": "Layout : WP Latest", + "type": "preset", + "update": false, + "userdata": "" + } + ], + "id": "137aa66a-87e2-4c53-b304-44bd7ff9f755", + "type": "presets" + } + ], + "entity": "Versions", + "favourite": true, + "flags": [ + "Ignore Toolbar", + "Quick Load" + ], + "hidden": false, + "id": "ef787e88-1b8f-4d89-bbc7-3ecf85987792", + "name": "Quick Load", + "type": "group", + "update": false, + "userdata": "tree" + } + + + ], + "description": "Site presets.", + "path": "/plugin/data_source/shotbrowser/site_presets", + "value": null + }, + "user_presets": { + "context": [ + "PLUGIN" + ], + "datatype": "json", + "default_value": [], + "description": "User presets.", + "path": "/plugin/data_source/shotbrowser/user_presets", + "value": null + } + } + } + }, + "ui": { + "qml": { + "shotbrowser_settings": { + "context": [ + "QML_UI" + ], + "datatype": "json", + "default_value": {}, + "description": "Prefs relating to window position.", + "path": "/ui/qml/shotbrowser_settings", + "value": { + "__ignore__": true, + "height": 400, + "visibility": 0, + "width": 700, + "x": 100, + "y": 100 + } + } + } + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/CMakeLists.txt b/src/plugin/data_source/dneg/shotbrowser/src/CMakeLists.txt new file mode 100644 index 000000000..9a4eab9ca --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/CMakeLists.txt @@ -0,0 +1,70 @@ +project(shotbrowser VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt6 COMPONENTS Concurrent Core Quick Gui Qml Widgets OpenGL Multimedia REQUIRED) + +QT6_WRAP_CPP(SHOTBROWSER_UI_MOC_SRC "${CMAKE_CURRENT_SOURCE_DIR}/shotbrowser_engine_ui.hpp") +QT6_WRAP_CPP(RESULT_MODEL_UI_MOC_SRC "${CMAKE_CURRENT_SOURCE_DIR}/result_model_ui.hpp") +QT6_WRAP_CPP(MODEL_UI_MOC_SRC "${CMAKE_CURRENT_SOURCE_DIR}/model_ui.hpp") +QT6_WRAP_CPP(PRESET_MODEL_UI_MOC_SRC "${CMAKE_CURRENT_SOURCE_DIR}/preset_model_ui.hpp") + +set(SOURCES + shotbrowser_plugin.cpp + shotbrowser_engine_ui.cpp + shotbrowser_engine_request_ui.cpp + shotbrowser_engine_query_ui.cpp + preset_model_ui.cpp + model_ui.cpp + result_model_ui.cpp + async_request.cpp + worker.cpp + query_engine.cpp + action.cpp + get_actions.cpp + post_actions.cpp + put_actions.cpp + ${SHOTBROWSER_UI_MOC_SRC} + ${MODEL_UI_MOC_SRC} + ${PRESET_MODEL_UI_MOC_SRC} + ${RESULT_MODEL_UI_MOC_SRC} +) + +qt6_add_resources(SOURCES qml/ShotBrowser.1/shotbrowser.qrc) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +add_library(${PROJECT_NAME} SHARED ${SOURCES}) +add_library(xstudio::data_source::shotbrowser ALIAS ${PROJECT_NAME}) +default_plugin_options(${PROJECT_NAME}) + +target_link_libraries(${PROJECT_NAME} + PUBLIC + xstudio::module + xstudio::plugin_manager + xstudio::playlist + xstudio::ui::qml::helper + xstudio::shotgun_client + Qt6::Widgets + Qt6::Core + Qt6::Qml + Qt6::Quick + Qt6::Concurrent + Qt6::Multimedia +) + +set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) + +add_plugin_qml(${PROJECT_NAME} qml) + +# add_custom_target(COPY_SO) + +if (NOT WIN32) +add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy ${CMAKE_BINARY_DIR}/bin/plugin/libshotbrowser.so ${CMAKE_BINARY_DIR}/bin/plugin/qml/ShotBrowser.1/) +endif() + +# add_dependencies(${PROJECT_NAME} COPY_SO) diff --git a/src/plugin/data_source/dneg/shotbrowser/src/action.cpp b/src/plugin/data_source/dneg/shotbrowser/src/action.cpp new file mode 100644 index 000000000..9adbedc3d --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/action.cpp @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "xstudio/atoms.hpp" +#include "xstudio/utility/helpers.hpp" + +#include "shotbrowser_plugin.hpp" + +using namespace xstudio::data_source; +using namespace xstudio::shotbrowser; +using namespace xstudio::shotgun_client; +using namespace xstudio::utility; +using namespace xstudio; + +void ShotBrowser::use_action( + caf::typed_response_promise rp, const utility::JsonStore &action) { + + try { + auto operation = action.value("operation", ""); + + if (operation == "LoadPlaylist") { + scoped_actor sys{system()}; + auto session = request_receive( + *sys, + system().registry().template get(global_registry), + session::session_atom_v); + + mail(use_data_atom_v, action, session) + .request(caf::actor_cast(this), infinite) + .then( + [=](const UuidActor &) mutable { + rp.deliver(JsonStore(R"({"data": {"status": "successful"}})"_json)); + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(JsonStore(R"({"data": {"status": "successful"}})"_json)); + }); + } else if (operation == "RefreshPlaylist") { + refresh_playlist_versions( + rp, Uuid(action.at("playlist_uuid")), action.at("match_order")); + } else { + rp.deliver(make_error(xstudio_error::error, "Invalid operation.")); + } + } catch (const std::exception &err) { + rp.deliver( + make_error(xstudio_error::error, std::string("Invalid operation.\n") + err.what())); + } +} + +void ShotBrowser::use_action( + caf::typed_response_promise rp, + const utility::JsonStore &action, + const caf::actor &session) { + try { + auto operation = action.value("operation", ""); + + if (operation == "LoadPlaylist") { + load_playlist(rp, action.at("playlist_id").get(), session); + } else { + rp.deliver(make_error(xstudio_error::error, "Invalid operation.")); + } + } catch (const std::exception &err) { + rp.deliver( + make_error(xstudio_error::error, std::string("Invalid operation.\n") + err.what())); + } +} + +void ShotBrowser::use_action( + caf::typed_response_promise rp, + const caf::uri &uri, + const FrameRate &media_rate, + const bool create_playlist) { + // check protocol == shotgun.. + if (uri.scheme() != "shotgun") + return rp.deliver(UuidActorVector()); + + if (to_string(uri.authority()) == "load") { + // need type and id + auto query = uri.query(); + if (query.count("type") and query["type"] == "Version" and query.count("ids")) { + auto ids = split(query["ids"], '|'); + if (ids.empty()) + rp.deliver(UuidActorVector()); + + auto count = std::make_shared(ids.size()); + auto results = std::make_shared(); + + for (const auto i : ids) { + try { + auto type = query["type"]; + auto squery = R"({})"_json; + squery["id"] = i; + + mail( + shotgun_entity_filter_atom_v, + "Versions", + JsonStore(squery), + VersionFields, + std::vector(), + 1, + 4999) + .request( + caf::actor_cast(this), + std::chrono::seconds(static_cast(timeout_->value()))) + .then( + [=](const JsonStore &js) mutable { + // load version.. + mail( + playlist::add_media_atom_v, js, create_playlist, media_rate) + .request(caf::actor_cast(this), infinite) + .then( + [=](const UuidActorVector &uav) mutable { + (*count)--; + + for (const auto &ua : uav) + results->push_back(ua); + + if (not(*count)) + rp.deliver(*results); + }, + [=](const caf::error &err) mutable { + (*count)--; + spdlog::warn( + "{} {}", __PRETTY_FUNCTION__, to_string(err)); + if (not(*count)) + rp.deliver(*results); + }); + }, + [=](const caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } catch (const std::exception &err) { + (*count)--; + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + } else if (query.count("type") and query["type"] == "Playlist" and query.count("ids")) { + // will return an array of playlist actors.. + auto ids = split(query["ids"], '|'); + if (ids.empty()) + rp.deliver(UuidActorVector()); + + auto count = std::make_shared(ids.size()); + auto results = std::make_shared(); + + for (const auto i : ids) { + auto id = std::atoi(i.c_str()); + auto js = JsonStore(UseLoadPlaylist); + js["playlist_id"] = id; + mail(use_data_atom_v, js, caf::actor()) + .request(caf::actor_cast(this), infinite) + .then( + [=](const UuidActor &ua) mutable { + // process result to build playlist.. + (*count)--; + results->push_back(ua); + if (not(*count)) + rp.deliver(*results); + }, + [=](const caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + (*count)--; + if (not(*count)) + rp.deliver(*results); + }); + } + } else { + spdlog::warn("Invalid shotgun action {}, requires type, id", to_string(uri)); + rp.deliver(UuidActorVector()); + } + } else { + spdlog::warn( + "Invalid shotgun action {} {}", to_string(uri.authority()), to_string(uri)); + rp.deliver(UuidActorVector()); + } +} + + +void ShotBrowser::post_action( + caf::typed_response_promise rp, const utility::JsonStore &action) { + + try { + auto operation = action.value("operation", ""); + + if (operation == "RenameTag") { + rename_tag(rp, action.at("tag_id"), action.at("value")); + } else if (operation == "CreateTag") { + create_tag(rp, action.at("value")); + } else if (operation == "TagEntity") { + add_entity_tag( + rp, action.at("entity"), action.at("entity_id"), action.at("tag_id")); + } else if (operation == "UnTagEntity") { + remove_entity_tag( + rp, action.at("entity"), action.at("entity_id"), action.at("tag_id")); + } else if (operation == "CreatePlaylist") { + create_playlist(rp, action); + } else if (operation == "CreateNotes") { + create_playlist_notes( + rp, action.at("payload"), JsonStore(action.at("playlist_uuid"))); + } else { + rp.deliver(make_error(xstudio_error::error, "Invalid operation.")); + } + + } catch (const std::exception &err) { + rp.deliver( + make_error(xstudio_error::error, std::string("Invalid operation.\n") + err.what())); + } +} + +void ShotBrowser::get_action( + caf::typed_response_promise rp, const utility::JsonStore &action) { + + try { + auto operation = action.value("operation", ""); + + if (operation == "VersionIvyUuid") { + find_ivy_version( + rp, + action.at("ivy_uuid").get(), + action.at("job").get()); + } else if (operation == "GetShotFromId") { + find_shot(rp, action.at("shot_id").get()); + } else if (operation == "GetFields") { + get_fields( + rp, + action.at("id").get(), + action.at("entity").get(), + action.at("fields")); + } else if (operation == "VersionArtist") { + get_version_artist(rp, action.at("version_id").get()); + } else if (operation == "ExecutePreset") { + auto paths = std::vector(); + + if (not action.at("preset_paths").empty()) + paths = action.at("preset_paths"); + else if (not action.at("preset_path").empty()) + paths = {action.at("preset_path")}; + else if (not action.at("preset_id").is_null()) { + auto preset = + engine().user_presets().find_first("id", action.value("preset_id", Uuid())); + if (preset) + paths.push_back((*preset).to_string()); + } else if (not action.at("preset_ids").empty()) { + for (const auto &i : action.at("preset_ids")) { + auto preset = engine().user_presets().find_first("id", i.get()); + if (preset) + paths.push_back((*preset).to_string()); + } + } + + execute_preset( + rp, + paths, + action.at("project_id"), + action.at("context"), + action.at("metadata"), + action.at("env"), + action.at("custom_terms")); + + } else if (operation == "LinkMedia") { + link_media(rp, utility::Uuid(action.at("playlist_uuid"))); + } else if (operation == "AddShotgridMedia") { + add_shotgrid_media(rp, utility::Uuid(action.at("media_uuid"))); + } else if (operation == "DownloadShotgridMedia") { + download_shotgrid_media( + rp, + action.at("entity"), + action.at("entity_id"), + action.at("entity_name"), + action.at("project_name"), + action.at("parent_name")); + } else if (operation == "DownloadShotgridImage") { + download_shotgrid_image( + rp, + action.at("entity"), + action.at("entity_id"), + action.at("entity_name"), + action.at("project_name")); + } else if (operation == "GetData") { + get_data(rp, action.at("type"), action.at("project_id")); + } else if (operation == "Precache") { + get_precache(rp, action.at("project_id")); + } else if (operation == "MediaCount") { + get_valid_media_count(rp, utility::Uuid(action.at("playlist_uuid"))); + } else if (operation == "PrepareNotes") { + UuidVector media_uuids; + for (const auto &i : action.value("media_uuids", std::vector())) + media_uuids.push_back(Uuid(i)); + + prepare_playlist_notes( + rp, + utility::Uuid(action.at("playlist_uuid")), + media_uuids, + action.value("notify_owner", false), + action.value("notify_group_ids", std::vector()), + action.value("combine", false), + action.value("add_time", false), + action.value("add_playlist_name", false), + action.value("add_type", false), + action.value("anno_requires_note", true), + action.value("skip_already_published", false), + action.value("default_type", "")); + } else if (operation == "Query") { + execute_query(rp, action); + } else { + rp.deliver(make_error(xstudio_error::error, std::string("Invalid operation."))); + } + } catch (const std::exception &err) { + rp.deliver( + make_error(xstudio_error::error, std::string("Invalid operation.\n") + err.what())); + } +} + +void ShotBrowser::put_action( + caf::typed_response_promise rp, const xstudio::utility::JsonStore &action) { + + try { + auto operation = action.value("operation", ""); + + if (operation == "UpdatePlaylistVersions") { + update_playlist_versions(rp, Uuid(action["playlist_uuid"]), action["append"]); + } else { + rp.deliver(make_error(xstudio_error::error, "Invalid operation.")); + } + } catch (const std::exception &err) { + rp.deliver( + make_error(xstudio_error::error, std::string("Invalid operation.\n") + err.what())); + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/async_request.cpp b/src/plugin/data_source/dneg/shotbrowser/src/async_request.cpp new file mode 100644 index 000000000..b4caac604 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/async_request.cpp @@ -0,0 +1,82 @@ +#include + +#include "async_request.hpp" +#include "xstudio/ui/qml/job_control_ui.hpp" +#include "xstudio/ui/qml/helper_ui.hpp" +#include "xstudio/utility/json_store.hpp" +#include "xstudio/atoms.hpp" + +CAF_PUSH_WARNINGS +#include +#include +#include +// #include +CAF_POP_WARNINGS + +using namespace caf; +using namespace xstudio; +using namespace xstudio::utility; +using namespace xstudio::ui::qml; + +class ShotBrowserRequest : public ControllableJob { + public: + ShotBrowserRequest(const nlohmann::json request, const caf::actor backend) + : ControllableJob(), request_(std::move(request)), backend_(std::move(backend)) {} + + QString run(JobControl &cjc) override { + auto result = QString(); + try { + if (not cjc.shouldRun()) + throw std::runtime_error("Cancelled"); + + caf::actor_system &system_ = CafSystemObject::get_actor_system(); + scoped_actor sys{system_}; + + auto data = request_receive( + *sys, backend_, data_source::get_data_atom_v, JsonStore(request_)); + + result = QStringFromStd(data.dump()); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; + } + + private: + const nlohmann::json request_; + const caf::actor backend_; +}; + +ShotBrowserResponse::ShotBrowserResponse( + const QPersistentModelIndex index, + int role, + const nlohmann::json &request, + const caf::actor backend, + QThreadPool *pool) + : index_(std::move(index)), role_(role), backend_(std::move(backend)) { + // create a future.. + connect( + &watcher_, + &QFutureWatcher::finished, + this, + &ShotBrowserResponse::handleFinished); + + try { + QFuture future = + JobExecutor::run(new ShotBrowserRequest(request, backend_), pool); + + watcher_.setFuture(future); + } catch (...) { + deleteLater(); + } +} + +void ShotBrowserResponse::handleFinished() { + if (watcher_.future().resultCount()) { + auto result = watcher_.result(); + emit received(index_, role_, result); + + deleteLater(); + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/async_request.hpp b/src/plugin/data_source/dneg/shotbrowser/src/async_request.hpp new file mode 100644 index 000000000..6f6171b65 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/async_request.hpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +CAF_PUSH_WARNINGS +#include +#include +#include +CAF_POP_WARNINGS + +namespace xstudio::ui::qml { +using namespace caf; + + +class ShotBrowserResponse : public QObject { + Q_OBJECT + + signals: + // Search value, search role, search hint, set role, set value + void received(QPersistentModelIndex, int, QString); + + public: + ShotBrowserResponse( + const QPersistentModelIndex index, + int role, + const nlohmann::json &request, + const caf::actor backend, + QThreadPool *pool = QThreadPool::globalInstance()); + + private: + void handleFinished(); + + QFutureWatcher watcher_; + + const QPersistentModelIndex index_; + const nlohmann::json request_; + const int role_; + const caf::actor backend_; +}; + +} // namespace xstudio::ui::qml \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/definitions.hpp b/src/plugin/data_source/dneg/shotbrowser/src/definitions.hpp new file mode 100644 index 000000000..bf02c7c7e --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/definitions.hpp @@ -0,0 +1,480 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include "xstudio/utility/json_store.hpp" + +// Action templates + +// GET + +const auto GetVersionIvyUuid = + R"({"operation": "VersionIvyUuid", "job":null, "ivy_uuid": null})"_json; + +const auto GetShotFromId = R"({"operation": "GetShotFromId", "shot_id": null})"_json; + +const auto GetLinkMedia = R"({"operation": "LinkMedia", "playlist_uuid": null})"_json; + +const auto GetVersionArtist = R"({"operation": "VersionArtist", "version_id": null})"_json; + +const auto GetFields = + R"({"operation": "GetFields", "id": null, "entity": null, "fields": []})"_json; + +const auto GetExecutePreset = R"({ + "operation": "ExecutePreset", + "project_id": 0, + "query_ms": 0, + "preset_path": null, + "preset_paths": [], + "preset_id": null, + "preset_ids": [], + "metadata": {}, + "context": {}, + "env": {}, + "custom_terms": [] +})"_json; + +const auto GetValidMediaCount = R"({"operation": "MediaCount", "playlist_uuid": null})"_json; + +const auto GetShotgridMedia = R"({"operation": "AddShotgridMedia", "media_uuid": null})"_json; +const auto GetDownloadMedia = R"({ + "operation": "DownloadShotgridMedia", + "entity": "version", + "entity_id": 0, + "entity_name": "", + "project_name": "", + "parent_name": "" +})"_json; + +const auto GetDownloadImage = R"({ + "operation": "DownloadShotgridImage", + "entity": "", + "entity_id": 0, + "entity_name": "", + "project_name": "" +})"_json; + +const auto GetPrepareNotes = R"({ + "operation":"PrepareNotes", + "playlist_uuid": null, + "media_uuids": [], + "notify_owner": false, + "notify_group_ids": [], + "combine": false, + "add_time": false, + "add_playlist_name": false, + "add_type": false, + "anno_requires_note": true, + "skip_already_published": false, + "default_type": null +})"_json; + +const auto GetQueryResult = R"({ + "operation": "Query", + "context": null, + "project_id": 0, + "env": null, + "page": 1, + "execution_ms": 0, + "max_result": 4999, + "entity": null, + "fields": [], + "order": [], + "query": null, + "result": null +})"_json; + +const auto GetPrecache = R"({"operation": "Precache", "project_id": -1})"_json; + +const auto GetData = R"({"operation": "GetData", "type": "", "project_id": -1})"_json; + +// POST + +const auto PostRenameTag = R"({"operation": "RenameTag", "tag_id": null, "value": null})"_json; +const auto PostCreateTag = R"({"operation": "CreateTag", "value": null})"_json; + +const auto PostTagEntity = + R"({"operation": "TagEntity", "entity": null, "entity_id": null, "tag_id": null})"_json; + +const auto PostUnTagEntity = + R"({"operation": "UnTagEntity", "entity": null, "entity_id": null, "tag_id": null})"_json; + +const auto PostCreatePlaylist = + R"({"operation": "CreatePlaylist", "playlist_uuid": null, "project_id": null, "code": null, "location": null, "playlist_type": "Dailies"})"_json; + +const auto PostCreateNotes = + R"({"operation": "CreateNotes", "playlist_uuid": null, "payload": []})"_json; + +// PUT + +const auto PutUpdatePlaylistVersions = + R"({"operation": "UpdatePlaylistVersions", "append": false, "playlist_uuid": null})"_json; + +// USE + +const auto UseLoadPlaylist = R"({"operation": "LoadPlaylist", "playlist_id": 0})"_json; + +const auto UseRefreshPlaylist = + R"({"operation": "RefreshPlaylist", "playlist_uuid": null, "match_order": false})"_json; + +// const auto RefreshPlaylistNotesJSON = +// R"({"entity":"Playlist", "relationship": "Note", "playlist_uuid": null})"_json; + + +const auto PublishNoteTemplateJSON = R"( +{ + "bookmark_uuid": [], + "shot": "", + "payload": { + "project":{ "type": "Project", "id":0 }, + "note_links": [ + { "type": "Playlist", "id":0 }, + { "type": "Sequence", "id":0 }, + { "type": "Shot", "id":0 }, + { "type": "Version", "id":0 } + ], + + "addressings_to": [ + { "type": "HumanUser", "id": 0} + ], + + "addressings_cc": [ + ], + + "sg_note_type": null, + "sg_status_list":"opn", + "subject": null, + "content": null + } +} +)"_json; + +const auto locationsJSON = R"([ + {"name": "chn", "id": 4}, + {"name": "lon", "id": 1}, + {"name": "mtl", "id": 52}, + {"name": "mum", "id": 3}, + {"name": "syd", "id": 99}, + {"name": "van", "id": 2}])"_json; + +const auto VersionFields = std::vector( + {"id", + "code", + "created_at", + "created_by", + "cut_order", + "description", + "entity", + "frame_count", + "frame_range", + // "notes", + "project", + "sg_client_filename", + "sg_client_send_stage", + "sg_comp_in", + "sg_comp_out", + "sg_comp_range", + "sg_cut_in", + "sg_cut_order", + "sg_cut_out", + "sg_cut_range", + "sg_date_submitted_to_client", + "sg_dneg_version", + "sg_frames_have_slate", + "sg_ivy_dnuuid", + "sg_location", + "sg_movie_has_slate", + "sg_on_disk_chn", + "sg_on_disk_lon", + "sg_on_disk_mtl", + "sg_on_disk_mum", + "sg_on_disk_syd", + "sg_on_disk_van", + "sg_path_to_frames", + "sg_path_to_movie", + "sg_pipe_tag_3", + "sg_pipeline_step", + "sg_production_status", + "sg_project_name", + "sg_status_list", + "sg_submit_dailies", + "sg_submit_dailies_chn", + "sg_submit_dailies_mtl", + "sg_submit_dailies_mum", + "sg_submit_dailies_van", + "sg_transfix_map", + "sg_twig_name", + "sg_twig_type", + "sg_twig_type_code", + "tags", + "user", + "image"}); + +const auto ProjectFields = std::vector( + {"id", + "created_at", + "name", + "sg_description", + "sg_division", + "sg_type", + "sg_project_status", + "sg_status"}); + +const auto EpisodeFields = std::vector( + {"id", "project", "code", "sg_status_list", "sg_versions", "sg_sequences", "sg_shots"}); + +const auto NoteFields = std::vector( + {"id", + "created_by", + "created_at", + "client_note", + "sg_location", + "sg_note_type", + "sg_artist", + "sg_pipeline_step", + "subject", + "content", + "project", + "note_links", + "addressings_to", + "addressings_cc", + "attachments"}); + +const auto PlaylistFields = std::vector( + {"code", + "sg_location", + "updated_at", + "created_at", + "sg_date_and_time", + "sg_type", + "created_by", + "sg_department_unit"}); + +const auto SequenceFields = std::vector( + {"code", + "custom_entity20_sg_sequences_custom_entity20s", + "id", + "sg_dnuuid", + "sg_parent", + "sg_sequence_type", + "sg_status_list", + "shots", + "type"}); + +const auto AssetFields = std::vector( + {"code", "id", "sg_asset_name", "sg_asset_folder", "sg_status_list"}); + +const auto HumanUserFields = + std::vector({"name", "id", "login", "sg_department", "permission_rule_set"}); + +const auto SequenceShotFields = std::vector( + {"id", "code", "sg_dnuuid", "sg_shot_type", "sg_status_list", "sg_unit"}); + +const auto ShotFields = std::vector( + {"code", + "custom_entity20_sg_shots_custom_entity20s", + "id", + "project", + "sg_comp_range", + "sg_current_stage", + "sg_cut_range", + "sg_dnuuid", + "sg_shot_type", + "sg_status_list", + "sg_unit"}); + +const std::string shotbrowser_datasource_registry{"SHOTBROWSER"}; + +const auto ShotgunMetadataPath = std::string("/metadata/shotgun"); + +const auto TwigTypeCodes = xstudio::utility::JsonStore(R"([ + {"id": "anm", "name": "anim/dnanim"}, + {"id": "anmg", "name": "anim/group"}, + {"id": "pose", "name": "anim/pose"}, + {"id": "poseg", "name": "anim/posegroup"}, + {"id": "animcon", "name": "anim_concept"}, + {"id": "anno", "name": "annotation"}, + {"id": "aovc", "name": "aovconfig"}, + {"id": "apr", "name": "aov_presets"}, + {"id": "ably", "name": "assembly"}, + {"id": "asset", "name": "asset"}, + {"id": "assetl", "name": "assetl"}, + {"id": "acls", "name": "asset_class"}, + {"id": "alc", "name": "asset_library_config"}, + {"id": "abo", "name": "assisted_breakout"}, + {"id": "avpy", "name": "astrovalidate/check"}, + {"id": "avc", "name": "astrovalidate/checklist"}, + {"id": "ald", "name": "atmospheric_lookup_data"}, + {"id": "aud", "name": "audio"}, + {"id": "bsc", "name": "batch_script"}, + {"id": "buildcon", "name": "build_concept"}, + {"id": "imbl", "name": "bundle/image_map"}, + {"id": "texbl", "name": "bundle/texture"}, + {"id": "bch", "name": "cache/bgeo"}, + {"id": "fch", "name": "cache/fluid"}, + {"id": "gch", "name": "cache/geometry"}, + {"id": "houcache", "name": "cache/houdini"}, + {"id": "pch", "name": "cache/particle"}, + {"id": "vol", "name": "cache/volume"}, + {"id": "hcd", "name": "camera/chandata"}, + {"id": "cnv", "name": "camera/convergence"}, + {"id": "lnd", "name": "camera/lensdata"}, + {"id": "lnp", "name": "camera/lensprofile"}, + {"id": "cam", "name": "camera/mono"}, + {"id": "rtm", "name": "camera/retime"}, + {"id": "crig", "name": "camera/rig"}, + {"id": "camsheet", "name": "camera_sheet_ref"}, + {"id": "csht", "name": "charactersheet"}, + {"id": "cpk", "name": "charpik_pagedata"}, + {"id": "clrsl", "name": "clarisse/look"}, + {"id": "cdxc", "name": "codex_config"}, + {"id": "cpal", "name": "colourPalette"}, + {"id": "colsup", "name": "colour_setup"}, + {"id": "cpnt", "name": "component"}, + {"id": "artcon", "name": "concept_art"}, + {"id": "reicfg", "name": "config/rei"}, + {"id": "csc", "name": "contact_sheet_config"}, + {"id": "csp", "name": "contact_sheet_preset"}, + {"id": "cst", "name": "contact_sheet_template"}, + {"id": "convt", "name": "converter_template"}, + {"id": "crowda", "name": "crowd_actor"}, + {"id": "crowdc", "name": "crowd_cache"}, + {"id": "cdl", "name": "data/cdl"}, + {"id": "cut", "name": "data/clip/cut"}, + {"id": "edl", "name": "data/edl"}, + {"id": "lup", "name": "data/lineup"}, + {"id": "ref", "name": "data/ref"}, + {"id": "dspj", "name": "dossier_project"}, + {"id": "dvis", "name": "doublevision/scene"}, + {"id": "ecd", "name": "encoder_data"}, + {"id": "iss", "name": "framework/ivy/style"}, + {"id": "spt", "name": "framework/shotbuild/template"}, + {"id": "fbcv", "name": "furball/curve"}, + {"id": "fbgr", "name": "furball/groom"}, + {"id": "fbnt", "name": "furball/network"}, + {"id": "gsi", "name": "generics_instance"}, + {"id": "gss", "name": "generics_set"}, + {"id": "gst", "name": "generics_template"}, + {"id": "gft", "name": "giftwrap"}, + {"id": "grade", "name": "grade"}, + {"id": "llut", "name": "grade/looklut"}, + {"id": "artgfx", "name": "graphic_art"}, + {"id": "grm", "name": "groom"}, + {"id": "hbcfg", "name": "hotbuildconfig"}, + {"id": "hbcfgs", "name": "hotbuildconfig_set"}, + {"id": "hcpio", "name": "houdini_archive"}, + {"id": "ht", "name": "houdini_template"}, + {"id": "htp", "name": "houdini_template_params"}, + {"id": "idt", "name": "identity"}, + {"id": "art", "name": "image/artwork"}, + {"id": "ipg", "name": "image/imageplane"}, + {"id": "stb", "name": "image/storyboard"}, + {"id": "ibl", "name": "image_based_lighting"}, + {"id": "jgs", "name": "jigsaw"}, + {"id": "klr", "name": "katana/lightrig"}, + {"id": "klg", "name": "katana/livegroup"}, + {"id": "klf", "name": "katana/look"}, + {"id": "kr", "name": "katana/recipe"}, + {"id": "kla", "name": "katana_look_alias"}, + {"id": "kmac", "name": "katana_macro"}, + {"id": "lng", "name": "lensgrid"}, + {"id": "ladj", "name": "lighting_adjust"}, + {"id": "look", "name": "look"}, + {"id": "mtdd", "name": "material_data_driven"}, + {"id": "mtddcfg", "name": "material_data_driven_config"}, + {"id": "mtpc", "name": "material_plus_config"}, + {"id": "mtpg", "name": "material_plus_generator"}, + {"id": "mtpt", "name": "material_plus_template"}, + {"id": "mtpr", "name": "material_preset"}, + {"id": "moba", "name": "mob/actor"}, + {"id": "mobr", "name": "mob/rig"}, + {"id": "mobs", "name": "mob/sim"}, + {"id": "mcd", "name": "mocap/data"}, + {"id": "mcr", "name": "mocap/ref"}, + {"id": "mdl", "name": "model"}, + {"id": "mup", "name": "muppet"}, + {"id": "mupa", "name": "muppet/data"}, + {"id": "ndlr", "name": "noodle"}, + {"id": "nkc", "name": "nuke_config"}, + {"id": "ocean", "name": "ocean"}, + {"id": "omd", "name": "onset/metadata"}, + {"id": "otla", "name": "other/otlasset"}, + {"id": "omm", "name": "outsource/matchmove"}, + {"id": "apkg", "name": "package/asset"}, + {"id": "prm", "name": "params"}, + {"id": "psref", "name": "photoscan"}, + {"id": "pxt", "name": "pinocchio_extension"}, + {"id": "plt", "name": "plate"}, + {"id": "plook", "name": "preview_look"}, + {"id": "pbxt", "name": "procedural_build_extension"}, + {"id": "qcs", "name": "qcsheet"}, + {"id": "imageref", "name": "ref"}, + {"id": "osref", "name": "ref/onset"}, + {"id": "refbl", "name": "reference_bundle"}, + {"id": "render", "name": "render"}, + {"id": "2d", "name": "render/2D"}, + {"id": "cgr", "name": "render/cg"}, + {"id": "deepr", "name": "render/deep"}, + {"id": "elmr", "name": "render/element"}, + {"id": "foxr", "name": "render/forex"}, + {"id": "out", "name": "render/out"}, + {"id": "mov", "name": "render/playblast"}, + {"id": "movs", "name": "render/playblast/scene"}, + {"id": "wpb", "name": "render/playblast/working"}, + {"id": "scrr", "name": "render/scratch"}, + {"id": "testr", "name": "render/test"}, + {"id": "wrf", "name": "render/wireframe"}, + {"id": "wormr", "name": "render/worm"}, + {"id": "rpr", "name": "render_presets"}, + {"id": "repo2d", "name": "reposition_data_2d"}, + {"id": "zmdl", "name": "rexasset/model"}, + {"id": "rig", "name": "rig"}, + {"id": "lgtr", "name": "rig/light"}, + {"id": "rigs", "name": "rig_script"}, + {"id": "rigssn", "name": "rig_session"}, + {"id": "scan", "name": "scan"}, + {"id": "sctr", "name": "scatterer"}, + {"id": "sctrp", "name": "scatterer_preset"}, + {"id": "casc", "name": "scene/cascade"}, + {"id": "clrs", "name": "scene/clarisse"}, + {"id": "clwscn", "name": "scene/clarisse/working"}, + {"id": "hip", "name": "scene/houdini"}, + {"id": "scn", "name": "scene/maya"}, + {"id": "fxs", "name": "scene/maya/effects"}, + {"id": "gchs", "name": "scene/maya/geometry"}, + {"id": "lgt", "name": "scene/maya/lighting"}, + {"id": "ldv", "name": "scene/maya/lookdev"}, + {"id": "mod", "name": "scene/maya/model"}, + {"id": "mods", "name": "scene/maya/model/extended"}, + {"id": "mwscn", "name": "scene/maya/working"}, + {"id": "pycl", "name": "script/clarisse/python"}, + {"id": "otl", "name": "script/houdini/otl"}, + {"id": "pyh", "name": "script/houdini/python"}, + {"id": "mel", "name": "script/maya/mel"}, + {"id": "pym", "name": "script/maya/python"}, + {"id": "nkt", "name": "script/nuke/template"}, + {"id": "pcrn", "name": "script/popcorn"}, + {"id": "pys", "name": "script/python"}, + {"id": "artset", "name": "set_drawing"}, + {"id": "shot", "name": "shot"}, + {"id": "shotl", "name": "shot_layer"}, + {"id": "stig", "name": "stig"}, + {"id": "hdr", "name": "stig/hdr"}, + {"id": "sft", "name": "submission/subform/template"}, + {"id": "sbsd", "name": "substance_designer"}, + {"id": "sbsp", "name": "substance_painter"}, + {"id": "sprst", "name": "superset"}, + {"id": "surfs", "name": "surfacing_scene"}, + {"id": "nuketex", "name": "texture/nuke"}, + {"id": "texs", "name": "texture/sequence"}, + {"id": "texref", "name": "texture_ref"}, + {"id": "tvp", "name": "texture_viewport"}, + {"id": "tstl", "name": "tool_searcher_tool"}, + {"id": "veg", "name": "vegetation"}, + {"id": "vidref", "name": "video_ref"}, + {"id": "witvidref", "name": "video_ref_witness"}, + {"id": "wgt", "name": "weightmap"}, + {"id": "wsf", "name": "working_source_file"} +])"_json); diff --git a/src/plugin/data_source/dneg/shotbrowser/src/get_actions.cpp b/src/plugin/data_source/dneg/shotbrowser/src/get_actions.cpp new file mode 100644 index 000000000..5574abd3e --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/get_actions.cpp @@ -0,0 +1,2363 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include "xstudio/atoms.hpp" +#include "xstudio/media/media_actor.hpp" +#include "xstudio/shotgun_client/shotgun_client.hpp" +#include "xstudio/utility/helpers.hpp" + +#include "shotbrowser_plugin.hpp" + +using namespace xstudio; +using namespace xstudio::shotgun_client; +using namespace xstudio::shotbrowser; +using namespace xstudio::utility; +using namespace xstudio::data_source; + +void ShotBrowser::find_ivy_version( + caf::typed_response_promise rp, + const std::string &uuid, + const std::string &job) { + // find version from supplied details. + + auto version_filter = + FilterBy().And(Text("project.Project.name").is(job), Text("sg_ivy_dnuuid").is(uuid)); + + mail( + shotgun_entity_search_atom_v, + "Version", + JsonStore(version_filter), + concatenate_vector(VersionFields, version_fields_), + std::vector(), + 1, + 1) + .request(shotgun_, std::chrono::seconds(static_cast(timeout_->value()))) + .then( + [=](const JsonStore &jsn) mutable { + auto result = JsonStore(R"({"payload":[]})"_json); + if (jsn.count("data") and jsn.at("data").size()) { + result["payload"] = jsn.at("data")[0]; + } + rp.deliver(result); + }, + [=](error &err) mutable { + spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(JsonStore(R"({"payload":[]})"_json)); + }); +} + +void ShotBrowser::get_fields( + caf::typed_response_promise rp, + const int id, + const std::string &entity, + const std::vector &fields) { + mail(shotgun_entity_atom_v, entity, id, fields) + .request(shotgun_, std::chrono::seconds(static_cast(timeout_->value()))) + .then( + [=](const JsonStore &jsn) mutable { rp.deliver(jsn); }, + [=](error &err) mutable { + spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(JsonStore(R"({"data":{}})"_json)); + }); +} + + +void ShotBrowser::find_shot( + caf::typed_response_promise rp, const int shot_id) { + // find version from supplied details. + if (shot_cache_.count(shot_id)) + rp.deliver(shot_cache_.at(shot_id)); + else { + mail(shotgun_entity_atom_v, "Shot", shot_id, extend_fields("Shots", ShotFields)) + .request(shotgun_, std::chrono::seconds(static_cast(timeout_->value()))) + .then( + [=](const JsonStore &jsn) mutable { + shot_cache_[shot_id] = jsn; + rp.deliver(jsn); + }, + [=](error &err) mutable { + spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(JsonStore(R"({"data":{}})"_json)); + }); + } +} + +void ShotBrowser::link_media( + caf::typed_response_promise rp, const utility::Uuid &uuid) { + try { + // find playlist + scoped_actor sys{system()}; + + auto session = request_receive( + *sys, + system().registry().template get(global_registry), + session::session_atom_v); + + auto playlist = + request_receive(*sys, session, session::get_playlist_atom_v, uuid); + + // get media.. + auto media = + request_receive>(*sys, playlist, playlist::get_media_atom_v); + + // scan media for shotgun version / ivy uuid + if (not media.empty()) { + fan_out_request( + vector_to_caf_actor_vector(media), + infinite, + json_store::get_json_atom_v, + utility::Uuid(), + "", + true) + .then( + [=](std::vector> json) mutable { + // ivy uuid is stored on source not media.. balls. + auto left = std::make_shared(0); + auto invalid = std::make_shared(0); + for (const auto &i : json) { + try { + if (i.second.is_null() or + not i.second["metadata"].count("shotgun")) { + // request current media source metadata.. + scoped_actor sys{system()}; + auto source_meta = request_receive( + *sys, + i.first.actor(), + json_store::get_json_atom_v, + "/metadata/external/DNeg"); + // we has got it.. + auto ivy_uuid = source_meta.at("Ivy").at("dnuuid"); + auto job = source_meta.at("show"); + auto shot = source_meta.at("shot"); + (*left) += 1; + // spdlog::warn("{} {} {} {}", job, shot, ivy_uuid, *left); + // call back into self ? + // but we need to wait for the final result.. + // maybe in danger of deadlocks... + // now we need to query shotgun.. + // to try and find version from this information. + // this is then used to update the media actor. + auto jsre = JsonStore(GetVersionIvyUuid); + jsre["ivy_uuid"] = ivy_uuid; + jsre["job"] = job; + + mail(get_data_atom_v, jsre) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &ver) mutable { + // got ver from uuid + (*left)--; + if (ver["payload"].empty()) { + (*invalid)++; + } else { + // push version to media object + scoped_actor sys{system()}; + try { + request_receive( + *sys, + i.first.actor(), + json_store::set_json_atom_v, + utility::Uuid(), + JsonStore(ver["payload"]), + ShotgunMetadataPath + "/version"); + } catch (const std::exception &err) { + spdlog::warn( + "{} {}", + __PRETTY_FUNCTION__, + err.what()); + } + } + + if (not(*left)) { + JsonStore result( + R"({"result": {"valid":0, "invalid":0}})"_json); + result["result"]["valid"] = + json.size() - (*invalid); + result["result"]["invalid"] = (*invalid); + rp.deliver(result); + } + }, + [=](error &err) mutable { + spdlog::warn( + "{} {}", + __PRETTY_FUNCTION__, + to_string(err)); + (*left)--; + (*invalid)++; + if (not(*left)) { + JsonStore result( + R"({"result": {"valid":0, "invalid":0}})"_json); + result["result"]["valid"] = + json.size() - (*invalid); + result["result"]["invalid"] = (*invalid); + rp.deliver(result); + } + }); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + if (not(*left)) { + JsonStore result(R"({"result": {"valid":0, "invalid":0}})"_json); + result["result"]["valid"] = json.size(); + result["result"]["invalid"] = 0; + rp.deliver(result); + } + }, + [=](error &err) mutable { + spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); + }); + } else { + rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); + } + + + } catch (const std::exception &err) { + spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +void get_data( + caf::typed_response_promise rp, + const std::string &type, + const int project_id); + +void ShotBrowser::add_shotgrid_media( + caf::typed_response_promise rp, const utility::Uuid &uuid) { + try { + // find media + scoped_actor sys{system()}; + + auto session = request_receive( + *sys, + system().registry().template get(global_registry), + session::session_atom_v); + + auto media = + request_receive(*sys, session, playlist::get_media_atom_v, uuid); + + // get metadata, we need version id.. + auto media_metadata = request_receive( + *sys, + media, + json_store::get_json_atom_v, + utility::Uuid(), + "/metadata/shotgun/version"); + + // spdlog::warn("{}", media_metadata.dump(2)); + + auto name = media_metadata.at("attributes").at("code").template get(); + auto job = + media_metadata.at("attributes").at("sg_project_name").template get(); + auto shot = media_metadata.at("relationships") + .at("entity") + .at("data") + .at("name") + .template get(); + auto filepath = download_cache_.target_string() + "/" + name + "-" + job + "-" + shot + + ".dneg.webm"; + + + // check it doesn't already exist.. + if (fs::exists(filepath)) { + // create source and add to media + auto uuid = Uuid::generate(); + auto source = spawn( + "ShotGrid Preview", + utility::posix_path_to_uri(filepath), + FrameList(), + FrameRate(), + uuid); + mail(media::add_media_source_atom_v, UuidActor(uuid, source)) + .request(media, infinite) + .then( + [=](const Uuid &u) mutable { + auto jsn = JsonStore(R"({})"_json); + jsn["actor_uuid"] = uuid; + jsn["actor"] = actor_to_string(system(), source); + + rp.deliver(jsn); + }, + [=](error &err) mutable { + spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(JsonStore((R"({})"_json)["error"] = to_string(err))); + }); + } else { + mail( + shotgun_attachment_atom_v, + "version", + media_metadata.at("id").template get(), + "sg_uploaded_movie_webm") + .request(shotgun_, infinite) + .then( + [=](const std::string &data) mutable { + if (data.size() > 1024 * 15) { + // write to file + std::ofstream o(filepath); + try { + o.exceptions(std::ifstream::failbit | std::ifstream::badbit); + o << data << std::endl; + o.close(); + + // file written add to media as new source.. + auto uuid = Uuid::generate(); + auto source = spawn( + "ShotGrid Preview", + utility::posix_path_to_uri(filepath), + FrameList(), + FrameRate(), + uuid); + mail(media::add_media_source_atom_v, UuidActor(uuid, source)) + .request(media, infinite) + .then( + [=](const Uuid &u) mutable { + auto jsn = JsonStore(R"({})"_json); + jsn["actor_uuid"] = uuid; + jsn["actor"] = actor_to_string(system(), source); + + rp.deliver(jsn); + }, + [=](error &err) mutable { + spdlog::error( + "{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(JsonStore( + (R"({})"_json)["error"] = to_string(err))); + }); + + } catch (const std::exception &) { + // remove failed file + if (o.is_open()) { + o.close(); + fs::remove(filepath); + } + spdlog::warn("Failed to open file"); + } + } else { + rp.deliver( + JsonStore((R"({})"_json)["error"] = "Failed to download")); + } + }, + [=](error &err) mutable { + spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(JsonStore((R"({})"_json)["error"] = to_string(err))); + }); + } + // "content_type": "video/webm", + // "id": 88463162, + // "link_type": "upload", + // "name": "b'tmp_upload_webm_0okvakz6.webm'", + // "type": "Attachment", + // "url": "http://shotgun.dneg.com/file_serve/attachment/88463162" + + } catch (const std::exception &err) { + spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(JsonStore((R"({})"_json)["error"] = err.what())); + } +} + +// http://shotgun.dneg.com/thumbnail/full/Attachment/101464187 + + +// shotgun_image_atom, +// const std::string &entity, +// const int record_id, +// const bool thumbnail + +void ShotBrowser::download_shotgrid_image( + caf::typed_response_promise rp, + const std::string &entity, + const int entity_id, + const std::string &entity_name, + const std::string &project_name) { + try { + // find media + + auto filepath = download_cache_.target_string() + "/" + project_name + "-" + + std::to_string(entity_id) + "-" + entity_name; + + auto result = R"({})"_json; + result = "file:" + filepath; + + // check it doesn't already exist.. + if (fs::exists(filepath)) { + rp.deliver(JsonStore(result)); + } else { + mail(shotgun_image_atom_v, entity, entity_id, false) + .request(shotgun_, infinite) + .then( + [=](const std::string &data) mutable { + if (data.size() > 1024 * 15) { + // write to file + std::ofstream o(filepath); + try { + o.exceptions(std::ifstream::failbit | std::ifstream::badbit); + o << data << std::endl; + o.close(); + rp.deliver(JsonStore(result)); + + } catch (const std::exception &) { + // remove failed file + if (o.is_open()) { + o.close(); + fs::remove(filepath); + } + rp.deliver( + make_error(xstudio_error::error, "Failed to open file")); + } + } else { + rp.deliver(make_error(xstudio_error::error, "Failed to download")); + } + }, + [=](error &err) mutable { + spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(make_error(xstudio_error::error, to_string(err))); + }); + } + } catch (const std::exception &err) { + spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + + +void ShotBrowser::download_shotgrid_media( + caf::typed_response_promise rp, + const std::string &entity, + const int entity_id, + const std::string &entity_name, + const std::string &project_name, + const std::string &parent_name) { + try { + // find media + + auto filepath = download_cache_.target_string() + "/" + entity_name + "-" + + project_name + "-" + parent_name + ".dneg.webm"; + + auto result = R"({})"_json; + result = "file:" + filepath; + + // check it doesn't already exist.. + if (fs::exists(filepath)) { + rp.deliver(JsonStore(result)); + } else { + mail(shotgun_attachment_atom_v, entity, entity_id, "sg_uploaded_movie_webm") + .request(shotgun_, infinite) + .then( + [=](const std::string &data) mutable { + if (data.size() > 1024 * 15) { + // write to file + std::ofstream o(filepath); + try { + o.exceptions(std::ifstream::failbit | std::ifstream::badbit); + o << data << std::endl; + o.close(); + rp.deliver(JsonStore(result)); + + } catch (const std::exception &) { + // remove failed file + if (o.is_open()) { + o.close(); + fs::remove(filepath); + } + rp.deliver( + make_error(xstudio_error::error, "Failed to open file")); + } + } else { + rp.deliver(make_error(xstudio_error::error, "Failed to download")); + } + }, + [=](error &err) mutable { + spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(make_error(xstudio_error::error, to_string(err))); + }); + } + } catch (const std::exception &err) { + spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +void ShotBrowser::get_valid_media_count( + caf::typed_response_promise rp, const utility::Uuid &uuid) { + try { + // find playlist + scoped_actor sys{system()}; + + auto session = request_receive( + *sys, + system().registry().template get(global_registry), + session::session_atom_v); + + auto playlist = + request_receive(*sys, session, session::get_playlist_atom_v, uuid); + + // get media.. + auto media = + request_receive>(*sys, playlist, playlist::get_media_atom_v); + + if (not media.empty()) { + fan_out_request( + vector_to_caf_actor_vector(media), + infinite, + json_store::get_json_atom_v, + utility::Uuid(), + "") + .then( + [=](std::vector json) mutable { + int count = 0; + for (const auto &i : json) { + try { + if (i["metadata"].count("shotgun")) + count++; + } catch (...) { + } + } + + JsonStore result(R"({"result": {"valid":0, "invalid":0}})"_json); + result["result"]["valid"] = count; + result["result"]["invalid"] = json.size() - count; + rp.deliver(result); + }, + [=](error &err) mutable { + spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); + }); + } else { + rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); + } + } catch (const std::exception &err) { + spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +void ShotBrowser::prepare_playlist_notes( + caf::typed_response_promise rp, + const utility::Uuid &playlist_uuid, + const utility::UuidVector &media_uuids, + const bool notify_owner, + const std::vector notify_group_ids, + const bool combine, + const bool add_time, + const bool add_playlist_name, + const bool add_type, + const bool anno_requires_note, + const bool skip_already_pubished, + const std::string &default_type) { + + auto playlist_name = std::string(); + auto playlist_id = int(0); + auto payload = R"({"payload":[], "valid": 0, "invalid": 0})"_json; + + try { + scoped_actor sys{system()}; + + // get session + auto session = request_receive( + *sys, + system().registry().template get(global_registry), + session::session_atom_v); + + // get playlist + auto playlist = request_receive( + *sys, session, session::get_playlist_atom_v, playlist_uuid); + + // get shotgun info from playlist.. + try { + auto sgpl = request_receive( + *sys, playlist, json_store::get_json_atom_v, ShotgunMetadataPath + "/playlist"); + + playlist_name = sgpl.at("attributes").at("code").template get(); + playlist_id = sgpl.at("id").template get(); + + } catch (const std::exception &err) { + spdlog::info("No shotgun playlist information"); + } + + // get media for playlist. + auto media = + request_receive>(*sys, playlist, playlist::get_media_atom_v); + + // no media so no point.. + // nothing to publish. + if (media.empty()) + return rp.deliver(JsonStore(payload)); + + std::vector media_actors; + + if (not media_uuids.empty()) { + auto lookup = uuidactor_vect_to_map(media); + for (const auto &i : media_uuids) { + if (lookup.count(i)) + media_actors.push_back(lookup[i]); + } + } else { + media_actors = vector_to_caf_actor_vector(media); + } + + // get media shotgun json.. + // we can only publish notes for media that has version information + fan_out_request( + media_actors, + infinite, + json_store::get_json_atom_v, + utility::Uuid(), + ShotgunMetadataPath, + true) + .then( + [=](std::vector> version_meta) mutable { + auto result = JsonStore(payload); + + scoped_actor sys{system()}; + + std::map> media_map; + UuidVector valid_media; + + // get valid media. + // get all the shotgun info we need to publish + for (const auto &i : version_meta) { + try { + // spdlog::warn("{}", i.second.dump(2)); + const auto &version = i.second.at("version"); + auto jsn = JsonStore(PublishNoteTemplateJSON); + + // project + jsn["payload"]["project"]["id"] = version.at("relationships") + .at("project") + .at("data") + .at("id") + .get(); + + + // playlist link + jsn["payload"]["note_links"][0]["id"] = playlist_id; + + if (version.at("relationships") + .at("entity") + .at("data") + .value("type", "") == "Sequence") + // shot link + jsn["payload"]["note_links"][1]["id"] = + version.at("relationships") + .at("entity") + .at("data") + .value("id", 0); + else if ( + version.at("relationships") + .at("entity") + .at("data") + .value("type", "") == "Shot") + // sequence link + jsn["payload"]["note_links"][2]["id"] = + version.at("relationships") + .at("entity") + .at("data") + .value("id", 0); + + // version link + jsn["payload"]["note_links"][3]["id"] = version.value("id", 0); + + if (jsn["payload"]["note_links"][3]["id"].get() == 0) + jsn["payload"]["note_links"].erase(3); + if (jsn["payload"]["note_links"][2]["id"].get() == 0) + jsn["payload"]["note_links"].erase(2); + if (jsn["payload"]["note_links"][1]["id"].get() == 0) + jsn["payload"]["note_links"].erase(1); + if (jsn["payload"]["note_links"][0]["id"].get() == 0) + jsn["payload"]["note_links"].erase(0); + + // we don't pass these to shotgun.. + jsn["shot"] = version.at("relationships") + .at("entity") + .at("data") + .at("name") + .get(); + jsn["playlist_name"] = playlist_name; + + if (notify_owner) // 1068 + jsn["payload"]["addressings_to"][0]["id"] = + version.at("relationships") + .at("user") + .at("data") + .at("id") + .get(); + else + jsn["payload"].erase("addressings_to"); + + if (not notify_group_ids.empty()) { + auto grp = R"({ "type": "Group", "id": null})"_json; + for (const auto g : notify_group_ids) { + if (g <= 0) + continue; + + grp["id"] = g; + jsn["payload"]["addressings_cc"].push_back(grp); + } + } + + if (jsn["payload"]["addressings_cc"].empty()) + jsn["payload"].erase("addressings_cc"); + + + media_map[i.first.uuid()] = std::make_pair(i.first, jsn); + valid_media.push_back(i.first.uuid()); + } catch (const std::exception &err) { + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + // get bookmark manager. + auto bookmarks = request_receive( + *sys, session, bookmark::get_bookmark_atom_v); + + // // collect media notes if they have shotgun metadata on the media + auto existing_bookmarks = + request_receive>>( + *sys, bookmarks, bookmark::get_bookmarks_atom_v, valid_media); + + // get bookmark detail.. + for (const auto &i : existing_bookmarks) { + // grouped by media item. + // we may want to collapse to unique note_types + std::map>> + notes_by_type; + + for (const auto &j : i.second) { + try { + if (skip_already_pubished) { + auto already_published = false; + try { + // check for shotgun metadata on note. + request_receive( + *sys, + j.actor(), + json_store::get_json_atom_v, + ShotgunMetadataPath + "/note"); + already_published = true; + } catch (...) { + } + + if (already_published) + continue; + } + + auto detail = request_receive( + *sys, j.actor(), bookmark::bookmark_detail_atom_v); + // skip notes with no text unless annotated and + // only_with_annotation is true + auto has_note = detail.note_ and not(*(detail.note_)).empty(); + auto has_anno = + detail.has_annotation_ and *(detail.has_annotation_); + + // do not publish non-visible bookmarks (e.g. grades) + auto visible = detail.visible_ and *(detail.visible_); + if (not visible) + continue; + + if (not(has_note or (has_anno and not anno_requires_note))) + continue; + + auto [ua, jsn] = media_map[detail.owner_->uuid()]; + // push to shotgun client.. + jsn["bookmark_uuid"].push_back(j.uuid()); + if (not jsn.count("note_annotation")) + jsn["note_annotation"] = R"([])"_json; + + if (has_anno) { + auto title = std::string(fmt::format( + "{}_{}.jpg", + jsn["shot"], + detail.start_timecode_tc().total_frames())); + auto item = nlohmann::json(); + item["title"] = title; + item["bookmark_uuid"] = j.uuid(); + // requires media actor and first frame of annotation. + jsn["note_annotation"].push_back(item); + } + auto cat = detail.category_ ? *(detail.category_) : ""; + if (not default_type.empty()) + cat = default_type; + + jsn["payload"]["sg_note_type"] = cat; + jsn["payload"]["subject"] = + detail.subject_ ? *(detail.subject_) : ""; + // format note content + std::string content; + + if (add_time) + content += std::string("Frame : ") + + std::to_string( + detail.start_timecode_tc().total_frames()) + + " / " + detail.start_timecode() + " / " + + detail.duration_timecode() + "\n"; + + content += *(detail.note_); + + jsn["payload"]["content"] = content; + + // yeah this is a bit convoluted. + if (not notes_by_type.count(cat)) { + notes_by_type.insert(std::make_pair( + cat, + std::map>( + {{detail.start_frame(), {{jsn}}}}))); + } else { + if (notes_by_type[cat].count(detail.start_frame())) { + notes_by_type[cat][detail.start_frame()].push_back(jsn); + } else { + notes_by_type[cat].insert(std::make_pair( + detail.start_frame(), + std::vector({jsn}))); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + try { + auto merged = JsonStore(); + + // category + for (auto &k : notes_by_type) { + auto category = k.first; + // frame + for (const auto &j : k.second) { + // entry + for (const auto ¬epayload : j.second) { + // spdlog::warn("{}",notepayload.dump(2)); + + if (not merged.is_null() and + (not combine or + merged["payload"]["sg_note_type"] != + notepayload["payload"]["sg_note_type"])) { + // spdlog::warn("{}", merged.dump(2)); + result["payload"].push_back(merged); + merged = JsonStore(); + } + + if (merged.is_null()) { + merged = notepayload; + auto content = std::string(); + + if (add_playlist_name and + not merged["playlist_name"] + .get() + .empty()) + content += + "Playlist : " + + std::string(merged["playlist_name"]) + "\n"; + + if (add_type) + content += "Note Type : " + + merged["payload"]["sg_note_type"] + .get() + + "\n\n"; + else + content += "\n\n"; + + merged["payload"]["content"] = + content + + merged["payload"]["content"].get(); + + merged.erase("shot"); + merged.erase("playlist_name"); + } else { + merged["bookmark_uuid"].insert( + merged["bookmark_uuid"].end(), + notepayload["bookmark_uuid"].begin(), + notepayload["bookmark_uuid"].end()); + merged["payload"]["content"] = + merged["payload"]["content"] + .get() + + "\n\n" + + notepayload["payload"]["content"] + .get(); + merged["note_annotation"].insert( + merged["note_annotation"].end(), + notepayload["note_annotation"].begin(), + notepayload["note_annotation"].end()); + } + } + } + } + + if (not merged.is_null()) + result["payload"].push_back(merged); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + result["valid"] = result["payload"].size(); + + // spdlog::warn("{}", result.dump(2)); + rp.deliver(result); + }, + [=](error &err) mutable { + spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(JsonStore(payload)); + }); + + } catch (const std::exception &err) { + spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +void ShotBrowser::execute_query( + caf::typed_response_promise rp, const utility::JsonStore &action) { + + auto dispatched = utility::sysclock::now(); + + // spdlog::warn("{}",action.dump(2) ); + + mail( + shotgun_entity_search_atom_v, + action.at("entity").get(), + JsonStore(action.at("query")), + extend_fields( + action.at("entity").get(), + action.at("fields").get>()), + action.at("order").get>(), + action.at("page").get(), + action.at("max_result").get()) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + auto result = action; + result["execution_ms"] = std::chrono::duration_cast( + utility::sysclock::now() - dispatched) + .count(); + + // super special handling.. + + if (auto remove_id_jsn = action.value("context", R"({})"_json) + .value("exclude_self", R"(null)"_json); + not remove_id_jsn.is_null()) { + // look in results for matching record, if found remove it.. + // cast to int + try { + auto remove_id = 0; + if (remove_id_jsn.is_string()) + remove_id = std::stoi(remove_id_jsn.get()); + else + remove_id = remove_id_jsn.get(); + + auto pruned_data = data; + auto it = pruned_data["data"].begin(); + while (it != pruned_data["data"].end()) { + if ((*it)["id"].get() == remove_id) + it = pruned_data["data"].erase(it); + else + it++; + } + + result["result"] = pruned_data; + } catch (const std::exception &err) { + spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); + result["result"] = data; + } + } else { + result["result"] = data; + } + // spdlog::warn("{}", result.dump(2)); + rp.deliver(result); + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data( + caf::typed_response_promise rp, + const std::string &type, + const int project_id) { + + try { + auto cache_key = QueryEngine::cache_name(type, project_id); + // check it's not cached.. + auto cached = engine().get_cache(cache_key); + + if (cached) { + rp.deliver(std::move(*cached)); + } else { + // try and tigger request.. + if (type == "department") + get_data_department(rp, type); + else if (type == "project") + get_data_project(rp, type, expand_envvars(username_->value())); + else if (type == "location") + get_data_location(rp, type); + else if (type == "review_location") + get_data_review_location(rp, type); + else if (type == "playlist_type") + get_data_playlist_type(rp, type); + else if (type == "shot_status") + get_data_shot_status(rp, type); + else if (type == "note_type") + get_data_note_type(rp, type); + else if (type == "production_status") + get_data_production_status(rp, type); + else if (type == "pipeline_status") + get_data_pipeline_status(rp, type, "version", "Pipeline Status"); + else if (type == "sequence_status") + get_data_pipeline_status(rp, type, "sequence", "Sequence Status"); + else if (type == "user") + get_data_user(rp, type, project_id); + else if (type == "shot") + get_data_shot(rp, type, project_id); + else if (type == "sequence_shot") + get_data_shot_for_sequence(rp, type, project_id); + else if (type == "pipe_step") + get_pipe_step(rp); + else if (type == "sequence") + get_data_sequence(rp, type, project_id); + else if (type == "episode") + get_data_episode(rp, type, project_id); + else if (type == "playlist") + get_data_playlist(rp, type, project_id); + else if (type == "tree") + get_data_tree(rp, type, project_id); + else if (type == "unit") + get_data_unit(rp, type, project_id); + else if (type == "asset") + get_data_asset(rp, type, project_id); + else if (type == "group") + get_data_group(rp, type, project_id); + else if (type == "stage") + get_data_stage(rp, type, project_id); + else { + rp.deliver(make_error(xstudio_error::error, "Unknown type " + type)); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +void ShotBrowser::get_data_department( + caf::typed_response_promise rp, const std::string &type) { + + // ["sg_status_list", "is", "act"] + + auto filter = R"( + { + "logical_operator": "and", + "conditions": [ + ] + })"_json; + + mail( + shotgun_entity_search_atom_v, + "Departments", + JsonStore(filter), + std::vector({"name", "id"}), + std::vector({"name"}), + 1, + 4999) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto cache_key = QueryEngine::cache_name(type); + auto lookup_key = QueryEngine::cache_name("Department"); + + engine().set_cache(cache_key, data.at("data")); + engine().set_lookup(lookup_key, data.at("data"), engine().lookup()); + + rp.deliver(*engine().get_cache(cache_key)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_project( + caf::typed_response_promise rp, + const std::string &type, + const std::string &user) { + + auto filter = FilterBy().And( + StatusList("sg_status").is_not("Archive"), Text("sg_type").is_not("Template")); + + mail( + shotgun_entity_search_atom_v, + "Projects", + JsonStore(filter), + ProjectFields, + std::vector({"name"}), + 1, + 4999) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto cache_key = QueryEngine::cache_name(type); + auto lookup_key = QueryEngine::cache_name("Project"); + + engine().set_cache(cache_key, data.at("data")); + engine().set_lookup(lookup_key, data.at("data"), engine().lookup()); + + rp.deliver(*engine().get_cache(cache_key)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_location( + caf::typed_response_promise rp, const std::string &type) { + + mail(shotgun_schema_entity_fields_atom_v, "playlist", "sg_location", -1) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto cache_key = QueryEngine::cache_name(type); + auto field_data = QueryEngine::data_from_field(data.at("data")); + + engine().set_cache(cache_key, field_data); + // engine().set_lookup(QueryEngine::cache_name("Location"), field_data); + + rp.deliver(*engine().get_cache(cache_key)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_review_location( + caf::typed_response_promise rp, const std::string &type) { + + mail(shotgun_schema_entity_fields_atom_v, "playlist", "sg_review_location_1", -1) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto cache_key = QueryEngine::cache_name(type); + auto field_data = QueryEngine::data_from_field(data.at("data")); + + engine().set_cache(cache_key, field_data); + + rp.deliver(*engine().get_cache(cache_key)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_playlist_type( + caf::typed_response_promise rp, const std::string &type) { + + mail(shotgun_schema_entity_fields_atom_v, "playlist", "sg_type", -1) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto cache_key = QueryEngine::cache_name(type); + auto field_data = QueryEngine::data_from_field(data.at("data")); + + engine().set_cache(cache_key, field_data); + + rp.deliver(*engine().get_cache(cache_key)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_shot_status( + caf::typed_response_promise rp, const std::string &type) { + + mail(shotgun_schema_entity_fields_atom_v, "shot", "sg_status_list", -1) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto cache_key = QueryEngine::cache_name(type); + auto field_data = QueryEngine::data_from_field(data.at("data")); + + engine().set_cache(cache_key, field_data); + engine().set_lookup( + QueryEngine::cache_name("Exclude Shot Status"), + field_data, + engine().lookup()); + engine().set_lookup( + QueryEngine::cache_name("Shot Status"), + field_data, + engine().lookup()); + + rp.deliver(*engine().get_cache(cache_key)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_note_type( + caf::typed_response_promise rp, const std::string &type) { + + mail(shotgun_schema_entity_fields_atom_v, "note", "sg_note_type", -1) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto cache_key = QueryEngine::cache_name(type); + auto field_data = QueryEngine::data_from_field(data.at("data")); + + engine().set_cache(cache_key, field_data); + + rp.deliver(*engine().get_cache(cache_key)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_production_status( + caf::typed_response_promise rp, const std::string &type) { + + mail(shotgun_schema_entity_fields_atom_v, "version", "sg_production_status", -1) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto cache_key = QueryEngine::cache_name(type); + auto field_data = QueryEngine::data_from_field(data.at("data")); + + engine().set_lookup( + QueryEngine::cache_name("Production Status"), + field_data, + engine().lookup()); + engine().set_cache(cache_key, field_data); + + rp.deliver(*engine().get_cache(cache_key)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_pipeline_status( + caf::typed_response_promise rp, + const std::string &type, + const std::string &entity, + const std::string &cache_name) { + + mail(shotgun_schema_entity_fields_atom_v, entity, "sg_status_list", -1) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto cache_key = QueryEngine::cache_name(type); + auto field_data = QueryEngine::data_from_field(data.at("data")); + + engine().set_lookup( + QueryEngine::cache_name(cache_name), field_data, engine().lookup()); + engine().set_cache(cache_key, field_data); + + rp.deliver(*engine().get_cache(cache_key)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_pipe_step(caf::typed_response_promise rp) { + auto ps = engine().get_cache(QueryEngine::cache_name("Pipeline Step")); + if (ps) + rp.deliver(*ps); + else + rp.deliver(JsonStore(R"([{"name":"None"}])")); +} + + +void ShotBrowser::get_data_user( + caf::typed_response_promise rp, + const std::string &type, + const int project_id, + const int page, + const JsonStore &prev_data) { + + auto filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["sg_status_list", "is", "act"], + ["sg_location", "is_not", null] + ] + })"_json; + + if (project_id != -1) { + auto proj_filt = R"(["projects", "is", {"type":"Project", "id": -1}])"_json; + proj_filt[2]["id"] = project_id; + filter["conditions"].push_back(proj_filt); + } + + mail( + shotgun_entity_search_atom_v, + "HumanUsers", + JsonStore(filter), + HumanUserFields, + std::vector({"name"}), + page, + 4999) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto total = prev_data; + total.insert( + total.end(), data.at("data").begin(), data.at("data").end()); + + if (data.at("data").size() == 4999) { + get_data_user(rp, type, project_id, page + 1, total); + } else { + auto cache_key = QueryEngine::cache_name(type, project_id); + engine().set_lookup( + QueryEngine::cache_name("User", project_id), + total, + engine().lookup()); + engine().set_cache(cache_key, total); + + rp.deliver(*engine().get_cache(cache_key)); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_shot_for_sequence( + caf::typed_response_promise rp, + const std::string &type, + const int project_id, + const int page, + const JsonStore &prev_data) { + + auto filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["project", "is", {"type":"Project", "id":0}] + ] + })"_json; + + filter["conditions"][0][2]["id"] = project_id; + + mail( + shotgun_entity_search_atom_v, + "Shots", + JsonStore(filter), + SequenceShotFields, + std::vector({"id"}), + page, + 4999) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto total = prev_data; + total.insert( + total.end(), data.at("data").begin(), data.at("data").end()); + + if (data.at("data").size() == 4999) { + get_data_shot_for_sequence(rp, type, project_id, page + 1, total); + } else { + rp.deliver(total); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_shot( + caf::typed_response_promise rp, + const std::string &type, + const int project_id, + const int page, + const JsonStore &prev_data) { + + auto filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["project", "is", {"type":"Project", "id":0}], + ["sg_status_list", "not_in", ["na", "del"]] + ] + })"_json; + + filter["conditions"][0][2]["id"] = project_id; + + mail( + shotgun_entity_search_atom_v, + "Shots", + JsonStore(filter), + extend_fields("Shots", ShotFields), + std::vector({"code"}), + page, + 4999) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto total = prev_data; + total.insert( + total.end(), data.at("data").begin(), data.at("data").end()); + + if (data.at("data").size() == 4999) { + get_data_shot(rp, type, project_id, page + 1, total); + } else { + auto cache_key = QueryEngine::cache_name(type, project_id); + engine().set_lookup( + QueryEngine::cache_name("Shot", project_id), + total, + engine().lookup()); + engine().set_cache(cache_key, total); + + rp.deliver(*engine().get_cache(cache_key)); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_asset( + caf::typed_response_promise rp, + const std::string &type, + const int project_id, + const int page, + const JsonStore &prev_data) { + + auto filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["project", "is", {"type":"Project", "id":0}], + ["sg_asset_name", "is_not", null] + ] + })"_json; + // ["sg_status", "is_not", "arc"] + + filter["conditions"][0][2]["id"] = project_id; + + mail( + shotgun_entity_search_atom_v, + "Assets", + JsonStore(filter), + AssetFields, + std::vector({"sg_asset_name"}), + page, + 4999) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto total = prev_data; + total.insert( + total.end(), data.at("data").begin(), data.at("data").end()); + + if (data.at("data").size() == 4999) { + get_data_asset(rp, type, project_id, page + 1, total); + } else { + auto cache_key = QueryEngine::cache_name(type, project_id); + // we need to rewrite data as code is not unique + for (auto &i : total) + i["name"] = i["attributes"]["sg_asset_name"]; + + engine().set_lookup( + QueryEngine::cache_name("Asset", project_id), + total, + engine().lookup()); + engine().set_cache(cache_key, total); + + rp.deliver(*engine().get_cache(cache_key)); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_playlist( + caf::typed_response_promise rp, + const std::string &type, + const int project_id, + const int page, + const JsonStore &prev_data) { + + auto filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["project", "is", {"type":"Project", "id":0}], + ["versions", "is_not", null], + ["sg_status", "is_not", "arc"] + ] + })"_json; + + filter["conditions"][0][2]["id"] = project_id; + + mail( + shotgun_entity_search_atom_v, + "Playlists", + JsonStore(filter), + std::vector({"id", "code"}), + std::vector({"-created_at"}), + page, + 4999) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto total = prev_data; + total.insert( + total.end(), data.at("data").begin(), data.at("data").end()); + + if (data.at("data").size() == 4999) { + get_data_playlist(rp, type, project_id, page + 1, total); + } else { + auto cache_key = QueryEngine::cache_name(type, project_id); + engine().set_lookup( + QueryEngine::cache_name("Playlist", project_id), + total, + engine().lookup()); + engine().set_cache(cache_key, total); + + rp.deliver(*engine().get_cache(cache_key)); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_version_artist( + caf::typed_response_promise rp, const int version_id) { + + mail( + shotgun_entity_atom_v, + "Versions", + version_id, + std::vector({"code", "id", "user"})) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else + rp.deliver(JsonStore(data.at("data"))); + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_tree( + caf::typed_response_promise rp, + const std::string &type, + const int project_id) { + + auto getEpisodeData = GetData; + getEpisodeData["project_id"] = project_id; + getEpisodeData["type"] = "episode"; + + mail(get_data_atom_v, JsonStore(getEpisodeData)) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &episode_data) mutable { + auto getSequenceData = GetData; + getSequenceData["project_id"] = project_id; + getSequenceData["type"] = "sequence"; + + mail(get_data_atom_v, JsonStore(getSequenceData)) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &sequence_data) mutable { + auto data = R"([])"_json; + data.push_back(episode_data); + data.push_back(sequence_data); + rp.deliver(JsonStore(data)); + }, + [=](error &err) mutable { rp.deliver(err); }); + }, + [=](error &err) mutable { rp.deliver(err); }); +} + + +void ShotBrowser::get_data_unit( + caf::typed_response_promise rp, + const std::string &type, + const int project_id) { + + auto filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["project", "is", {"type":"Project", "id":0}], + ["sg_shots", "is_not", null] + ] + })"_json; + + filter["conditions"][0][2]["id"] = project_id; + + mail( + shotgun_entity_search_atom_v, + "CustomEntity24", + JsonStore(filter), + std::vector({"code", "id"}), + std::vector({"code"}), + 1, + 4999) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto cache_key = QueryEngine::cache_name(type, project_id); + + engine().set_lookup( + QueryEngine::cache_name("Unit", project_id), + data.at("data"), + engine().lookup()); + engine().set_cache(cache_key, data.at("data")); + + rp.deliver(*engine().get_cache(cache_key)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_stage( + caf::typed_response_promise rp, + const std::string &type, + const int project_id) { + + auto filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["project", "is", {"type":"Project", "id":0}] + ] + })"_json; + + filter["conditions"][0][2]["id"] = project_id; + + mail( + shotgun_entity_search_atom_v, + "CustomEntity34", + JsonStore(filter), + std::vector({"code", "id"}), + std::vector({"code"}), + 1, + 4999) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto cache_key = QueryEngine::cache_name(type, project_id); + + engine().set_lookup( + QueryEngine::cache_name("Stage", project_id), + data.at("data"), + engine().lookup()); + engine().set_cache(cache_key, data.at("data")); + + rp.deliver(*engine().get_cache(cache_key)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_group( + caf::typed_response_promise rp, + const std::string &type, + const int project_id) { + + mail(shotgun_groups_atom_v, project_id) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto cache_key = QueryEngine::cache_name(type, project_id); + engine().set_cache(cache_key, data.at("data")); + rp.deliver(*engine().get_cache(cache_key)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_sequence( + caf::typed_response_promise rp, + const std::string &type, + const int project_id, + const int page, + const JsonStore &prev_data) { + + auto filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["project", "is", {"type":"Project", "id":0}] + ] + })"_json; + // ["sg_status_list", "not_in", ["na","del"]] + + filter["conditions"][0][2]["id"] = project_id; + + mail( + shotgun_entity_search_atom_v, + "Sequences", + JsonStore(filter), + SequenceFields, + std::vector({"code"}), + page, + 4999) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto total = prev_data; + total.insert( + total.end(), data.at("data").begin(), data.at("data").end()); + + if (data.at("data").size() == 4999) { + get_data_sequence(rp, type, project_id, page + 1, total); + } else { + // request and purge deleted shots. + auto statusfilter = R"( + { + "logical_operator": "and", + "conditions": [ + ["project", "is", {"type":"Project", "id": 0}] + ] + })"_json; + statusfilter["conditions"][0][2]["id"] = project_id; + + // ["sg_status_list", "in", ["na","del"]] + + auto getShotData = GetData; + getShotData["project_id"] = project_id; + getShotData["type"] = "sequence_shot"; + + mail(get_data_atom_v, JsonStore(getShotData)) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &statusdata) mutable { + try { + // build set of deleted shot id's + std::map shot_data; + for (const auto &i : statusdata) + shot_data[i.at("id").get()] = i; + + if (not shot_data.empty()) { + // iterate over sequence -> shots and remove + // deleted + for (auto &i : total) { + auto &t = + i["relationships"]["shots"]["data"]; + for (auto it = t.begin(); it != t.end(); + ++it) { + if (shot_data.count(it->at("id"))) { + it->update(shot_data[it->at("id")]); + } + } + } + } + + // spdlog::warn("{}", total.dump(2)); + + auto cache_key = + QueryEngine::cache_name(type, project_id); + + engine().set_lookup( + QueryEngine::cache_name("Sequence", project_id), + total, + engine().lookup()); + engine().set_shot_sequence_lookup( + QueryEngine::cache_name( + "ShotSequence", project_id), + total, + engine().lookup()); + engine().set_cache(cache_key, total); + + rp.deliver(*engine().get_cache(cache_key)); + } catch (const std::exception &err) { + spdlog::warn( + "{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver( + make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::get_data_episode( + caf::typed_response_promise rp, + const std::string &type, + const int project_id, + const int page, + const JsonStore &prev_data) { + + auto filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["project", "is", {"type":"Project", "id":0}] + ] + })"_json; + // ["sg_status_list", "not_in", ["na","del"]] + + filter["conditions"][0][2]["id"] = project_id; + + mail( + shotgun_entity_search_atom_v, + "CustomEntity20", + JsonStore(filter), + EpisodeFields, + std::vector({"code"}), + page, + 4999) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &data) mutable { + try { + if (not data.count("data")) + rp.deliver(make_error(xstudio_error::error, data.dump(2))); + else { + auto total = prev_data; + total.insert( + total.end(), data.at("data").begin(), data.at("data").end()); + + if (data.at("data").size() == 4999) { + get_data_episode(rp, type, project_id, page + 1, total); + } else { + + auto cache_key = QueryEngine::cache_name(type, project_id); + + engine().set_lookup( + QueryEngine::cache_name("Episode", project_id), + total, + engine().lookup()); + engine().set_cache(cache_key, total); + + rp.deliver(*engine().get_cache(cache_key)); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(err); }); +} + +void ShotBrowser::execute_preset( + caf::typed_response_promise rp, + const std::vector &preset_paths, + const int _project_id, + const utility::JsonStore &context, + const utility::JsonStore &metadata, + const utility::JsonStore &env, + const utility::JsonStore &custom_terms) { + // is preset path valid ? + // we want the preset and it's group. + + // spdlog::warn("{}", __PRETTY_FUNCTION__); + auto project_id = _project_id; + + try { + auto presets = R"([])"_json; + auto preset_group = R"([])"_json; + auto group_detail = R"({"id": "", "flags":[]})"_json; + auto entity = std::string(); + + for (const auto &i : preset_paths) { + auto preset = engine().user_presets().at(json::json_pointer(i)); + if (entity.empty()) { + if (preset.value("type", "") == "group") { + entity = preset.at("entity"); + preset_group = preset.at("children").at(0).at("children"); + group_detail["id"] = preset.value("id", utility::Uuid()); + group_detail["flags"] = preset.value("flags", std::vector()); + } else { + auto tmp = engine().user_presets().at(json::json_pointer(i) + .parent_pointer() + .parent_pointer() + .parent_pointer() + .parent_pointer()); + + entity = tmp.at("entity"); + preset_group = tmp.at("children").at(0).at("children"); + group_detail["id"] = tmp.value("id", utility::Uuid()); + group_detail["flags"] = tmp.value("flags", std::vector()); + } + } + + if (preset.value("type", "") == "preset") + presets.push_back(preset.at("children")); + } + + // derive from metadata. + + if (not project_id) + project_id = engine().get_project_id(metadata, engine().cache()); + + // spdlog::warn("project_id {}", project_id); + // spdlog::warn("{}", preset_group.dump(2)); + // spdlog::warn("{}", presets.dump(2)); + + // we possibly need to precache lookups.. + if (not engine().precache_needed(project_id, engine().lookup()).empty()) { + // spdlog::warn("NEEDS PREECACHE {}", project_id); + // we need to trigger loading of lookups.. + auto req = JsonStore(GetPrecache); + req["project_id"] = project_id; + + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + // spdlog::warn("PRECACHED COMPLETE"); + execute_preset( + rp, preset_paths, project_id, context, metadata, env, custom_terms); + }, + [=](error &err) mutable { rp.deliver(err); }); + + } else { + auto request = QueryEngine::build_query( + project_id, + entity, + group_detail, + preset_group, + presets, + custom_terms, + context, + metadata, + env, + engine().lookup()); + + // spdlog::error("{}\n", request.dump(2)); + + execute_query(rp, request); + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +void ShotBrowser::get_precache( + caf::typed_response_promise rp, const int project_id) { + + // trigger precaching of lookup. + // we call into ourselves sigh.. + const auto &lookup = engine().lookup(); + const auto &cache = engine().cache(); + std::set need = engine().precache_needed(project_id, lookup); + + if (need.empty()) { + rp.deliver(utility::JsonStore()); + } else { + auto count = std::make_shared(need.size()); + + if (need.count("Project")) { + auto req = JsonStore(GetData); + req["type"] = "project"; + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(utility::JsonStore()); + }, + [=](error &err) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(err); + }); + } + + if (need.count("Department")) { + auto req = JsonStore(GetData); + req["type"] = "department"; + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(utility::JsonStore()); + }, + [=](error &err) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(err); + }); + } + + if (need.count("User")) { + auto req = JsonStore(GetData); + req["type"] = "user"; + req["project_id"] = project_id; + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(utility::JsonStore()); + }, + [=](error &err) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(err); + }); + } + + if (need.count("Pipeline Status")) { + auto req = JsonStore(GetData); + req["type"] = "pipeline_status"; + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(utility::JsonStore()); + }, + [=](error &err) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(err); + }); + } + + // if (need.count("Shot Pipeline Status")) { + // auto req = JsonStore(GetData); + // req["type"] = "shot_pipeline_status"; + // mail(get_data_atom_v, req) + // .request(caf::actor_cast(this), infinite).then( + // [=](const JsonStore &) mutable { + // (*count) = (*count) - 1; + // if (not(*count)) + // rp.deliver(utility::JsonStore()); + // }, + // [=](error &err) mutable { + // (*count) = (*count) - 1; + // if (not(*count)) + // rp.deliver(err); + // }); + // } + + if (need.count("Sequence Status")) { + auto req = JsonStore(GetData); + req["type"] = "sequence_status"; + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(utility::JsonStore()); + }, + [=](error &err) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(err); + }); + } + + if (need.count("Production Status")) { + auto req = JsonStore(GetData); + req["type"] = "production_status"; + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(utility::JsonStore()); + }, + [=](error &err) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(err); + }); + } + + if (need.count("Shot Status")) { + auto req = JsonStore(GetData); + req["type"] = "shot_status"; + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(utility::JsonStore()); + }, + [=](error &err) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(err); + }); + } + + if (need.count("Stage")) { + auto req = JsonStore(GetData); + req["type"] = "stage"; + req["project_id"] = project_id; + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(utility::JsonStore()); + }, + [=](error &err) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(err); + }); + } + + if (need.count("Unit")) { + auto req = JsonStore(GetData); + req["type"] = "unit"; + req["project_id"] = project_id; + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(utility::JsonStore()); + }, + [=](error &err) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(err); + }); + } + + if (need.count("Asset")) { + auto req = JsonStore(GetData); + req["type"] = "asset"; + req["project_id"] = project_id; + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(utility::JsonStore()); + }, + [=](error &err) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(err); + }); + } + + if (need.count("Playlist")) { + auto req = JsonStore(GetData); + req["type"] = "playlist"; + req["project_id"] = project_id; + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(utility::JsonStore()); + }, + [=](error &err) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(err); + }); + } + + if (need.count("Shot")) { + auto req = JsonStore(GetData); + req["type"] = "shot"; + req["project_id"] = project_id; + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(utility::JsonStore()); + }, + [=](error &err) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(err); + }); + } + + if (need.count("Episode")) { + auto req = JsonStore(GetData); + req["type"] = "episode"; + req["project_id"] = project_id; + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(utility::JsonStore()); + }, + [=](error &err) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(err); + }); + } + + if (need.count("Sequence")) { + auto req = JsonStore(GetData); + req["type"] = "sequence"; + req["project_id"] = project_id; + mail(get_data_atom_v, req) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(utility::JsonStore()); + }, + [=](error &err) mutable { + (*count) = (*count) - 1; + if (not(*count)) + rp.deliver(err); + }); + } + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/model_ui.cpp b/src/plugin/data_source/dneg/shotbrowser/src/model_ui.cpp new file mode 100644 index 000000000..c200cdf50 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/model_ui.cpp @@ -0,0 +1,665 @@ +// SPDX-License-Identifier: Apache-2.0 +// #include "shotgun_model_ui.hpp" +// #include "data_source_shotgun_ui.hpp" +// #include "../data_source_shotgun.hpp" +// #include "../data_source_shotgun_query_engine.hpp" + +#include "model_ui.hpp" + +// #include "xstudio/utility/string_helpers.hpp" +// #include "xstudio/ui/qml/helper_ui.hpp" +#include "xstudio/utility/json_store.hpp" +// #include "xstudio/atoms.hpp" + +#include + +using namespace xstudio; +using namespace xstudio::utility; +// using namespace xstudio::shotgun_client; +using namespace xstudio::ui::qml; +using namespace std::chrono_literals; +// using namespace xstudio::global_store; + +namespace { +void dumpNames(const nlohmann::json &jsn, const int depth) { + if (jsn.is_array()) { + for (const auto &item : jsn) { + dumpNames(item, depth); + } + } else { + spdlog::warn("{:>{}} {}", " ", depth * 4, jsn.value("name", "unnamed")); + if (jsn.count("children") and jsn.at("children").is_array()) { + for (const auto &item : jsn.at("children")) { + dumpNames(item, depth + 1); + } + } + } +} + +void add_subtype(std::set &types, const nlohmann::json &value) { + if (value != "No Type") + types.insert(value); +} +} // namespace + +nlohmann::json ShotBrowserSequenceModel::sortByNameAndType(const nlohmann::json &jsn) { + // this needs + auto result = jsn; + if (result.is_array()) { + std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) -> bool { + try { + if (a.at("type") == b.at("type")) { + if (a.at("subtype") == b.at("subtype")) + return a.at("name") < b.at("name"); + else + return a.at("subtype") < b.at("subtype"); + } + return a.at("type") < b.at("type"); + } catch (const std::exception &err) { + spdlog::warn("{}", err.what()); + } + return false; + }); + } + + for (auto &item : result) { + if (item.count("children")) { + item["children"] = sortByNameAndType(item.at("children")); + } + } + + return result; +} + +nlohmann::json +ShotBrowserSequenceModel::flatToAssetTree(const nlohmann::json &src, QStringList &_types) { + // manipulate data into tree structure. + // spdlog::warn("{}", src.size()); + auto result = R"([])"_json; + std::map assets; + auto done = false; + auto changed = true; + + try { + while (not done and changed) { + changed = false; + done = true; + for (const auto &i : src) { + if (not i.at("attributes").at("sg_asset_name").is_null()) { + const auto path = i.at("attributes").at("sg_asset_name").get(); + + if (not assets.count(path)) { + + auto parent = path.substr( + 0, + path.size() - path.find_last_of("/") == std::string::npos + ? 0 + : path.find_last_of("/")); + // add root level + if (path == parent) { + auto asset = i; + asset["name"] = asset.at("attributes").at("code"); + asset["children"] = json::array(); + + result.emplace_back(asset); + assets.insert(std::make_pair( + path, + nlohmann::json::json_pointer( + "/" + std::to_string(result.size() - 1)))); + changed = true; + } else { + // find parent.. + if (not assets.count(parent)) { + done = false; + } else { + auto asset = i; + asset["name"] = asset.at("attributes").at("code"); + asset["children"] = json::array(); + + result[assets[parent]]["children"].emplace_back(asset); + + assets.emplace(std::make_pair( + path, + assets[parent] / + nlohmann::json::json_pointer( + std::string("/children/") + + std::to_string( + result[assets[parent]]["children"].size() - + 1)))); + changed = true; + } + } + } + } + } + } + + // result = sortByNameAndType(result); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + // sort results.. + _types.clear(); + // for (auto i : types) { + // _types.push_back(QStringFromStd(i)); + // } + // spdlog::warn("{}", result.dump(2)); + + return result; +} + +nlohmann::json +ShotBrowserSequenceModel::flatToTree(const nlohmann::json &src, QStringList &_types) { + // manipulate data into tree structure. + // spdlog::warn("{}", src.size()); + const static auto sg_shot_type = json::json_pointer("/attributes/sg_shot_type"); + const static auto sg_sequence_type = json::json_pointer("/attributes/sg_sequence_type"); + + auto result = R"([])"_json; + std::map seqs; + + // spdlog::warn("{}", src.dump(2)); + + auto types = std::set(); + + try { + + if (src.is_array()) { + auto done = false; + + while (not done) { + done = true; + + for (auto seq : src.at(1)) { + try { + auto id = seq.at("id").get(); + // already logged ? + // if already there then skip + + if (not seqs.count(id)) { + seq["name"] = seq.at("attributes").at("code"); + seq["subtype"] = seq.at(sg_sequence_type).is_null() + ? "No Type" + : seq.at(sg_sequence_type); + add_subtype(types, seq["subtype"]); + + + auto parent_id = seq.at("relationships") + .at("sg_parent") + .at("data") + .at("id") + .get(); + + // no parent add to results. and store pointer to results entry. + if (parent_id == id) { + // spdlog::warn("new root level item {}", + // seq["name"].get()); + + auto &shots = seq["relationships"]["shots"]["data"]; + if (shots.is_array()) { + seq["children"] = seq["relationships"]["shots"]["data"]; + for (auto &i : seq["children"]) { + if (i.at("type") == "Shot" and not i.count("subtype")) { + i["subtype"] = i.at(sg_shot_type).is_null() + ? "No Type" + : i.at(sg_shot_type); + add_subtype(types, i["subtype"]); + } + } + // seq["children"] = + // sort_by(shots, + // nlohmann::json::json_pointer("/name")); + } else + seq["children"] = R"([])"_json; + + seq["parent_id"] = parent_id; + seq["relationships"].erase("shots"); + seq["relationships"].erase("sg_parent"); + + // store in result + // and pointer to entry. + result.emplace_back(seq); + + seqs.emplace(std::make_pair( + id, + nlohmann::json::json_pointer( + std::string("/") + std::to_string(result.size() - 1)))); + + done = false; + + } else if (seqs.count(parent_id)) { + // parent exists + // spdlog::warn("add to parent {} {}", parent_id, + // seq["name"].get()); + + auto parent_pointer = seqs[parent_id]; + + auto &shots = seq["relationships"]["shots"]["data"]; + if (shots.is_array()) { + seq["children"] = seq["relationships"]["shots"]["data"]; + for (auto &i : seq["children"]) { + if (i.at("type") == "Shot" and not i.count("subtype")) { + i["subtype"] = i.at(sg_shot_type).is_null() + ? "No Type" + : i.at(sg_shot_type); + add_subtype(types, i["subtype"]); + } + } + // sort_by(shots, nlohmann::json::json_pointer("/name")); + } else + seq["children"] = R"([])"_json; + + seq["parent_id"] = parent_id; + seq["relationships"].erase("shots"); + seq["relationships"].erase("sg_parent"); + + result[parent_pointer]["children"].emplace_back(seq); + // spdlog::warn("{}", result[parent_pointer].dump(2)); + + // add path to new entry.. + seqs.emplace(std::make_pair( + id, + parent_pointer / + nlohmann::json::json_pointer( + std::string("/children/") + + std::to_string( + result[parent_pointer]["children"].size() - + 1)))); + done = false; + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + } + + // un parented sequences + auto count = 0; + // unresolved.. + for (auto unseq : src.at(1)) { + try { + auto id = unseq.at("id").get(); + // already logged ? + if (not seqs.count(id)) { + unseq["name"] = unseq.at("attributes").at("code"); + unseq["subtype"] = unseq.at(sg_sequence_type).is_null() + ? "No Type" + : unseq.at(sg_sequence_type); + add_subtype(types, unseq["subtype"]); + + + auto parent_id = + unseq["relationships"]["sg_parent"]["data"]["id"].get(); + // no parent + auto &shots = unseq["relationships"]["shots"]["data"]; + + if (shots.is_array()) { + unseq["children"] = unseq["relationships"]["shots"]["data"]; + for (auto &i : unseq["children"]) { + if (i.at("type") == "Shot" and not i.count("subtype")) { + i["subtype"] = i.at(sg_shot_type).is_null() + ? "No Type" + : i.at(sg_shot_type); + add_subtype(types, i["subtype"]); + } + } + // sort_by(shots, nlohmann::json::json_pointer("/name")); + } else + unseq["children"] = R"([])"_json; + + unseq["parent_id"] = parent_id; + unseq["relationships"].erase("shots"); + unseq["relationships"].erase("sg_parent"); + result.emplace_back(unseq); + seqs.emplace(std::make_pair( + id, + nlohmann::json::json_pointer( + std::string("/") + std::to_string(result.size() - 1)))); + count++; + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + if (count) + spdlog::warn("{} unresolved sequences.", count); + } + + // result is now tree of sequence shots. + // add in any episodes.. + auto remove_seqs = std::vector(); + + for (auto ep : src.at(0)) { + // spdlog::warn("{}", ep.dump(2)); + ep["name"] = ep.at("attributes").at("code"); + auto parent_id = ep.at("id").get(); + ep["parent_id"] = parent_id; + ep["children"] = json::array(); + ep["type"] = "Episode"; + ep["subtype"] = "Episode"; + + // we now need to reparent any sequences. + for (const auto &i : ep.at("relationships").at("sg_sequences").at("data")) { + auto seqid = i.at("id").get(); + auto secit = seqs.find(seqid); + + if (secit != std::end(seqs)) { + // copy into our children + ep["children"].push_back(result.at(secit->second)); + + if (result.at(secit->second).at("parent_id") == seqid) { + // remove from results. + remove_seqs.push_back(seqid); + } + seqs.erase(secit); + } + } + + ep["children"] = sort_by(ep["children"], nlohmann::json::json_pointer("/name")); + + result.push_back(ep); + } + + for (const auto &id : remove_seqs) { + for (auto it = result.begin(); it != result.end(); ++it) { + if (it->at("id") == id) { + result.erase(it); + break; + } + } + } + + + result = sortByNameAndType(result); + // dumpNames(result, 0); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + // sort results.. + _types.clear(); + for (auto i : types) { + _types.push_back(QStringFromStd(i)); + } + // spdlog::warn("{}", result.dump(2)); + + return result; +} + + +QVariant ShotBrowserListModel::data(const QModelIndex &index, int role) const { + auto result = QVariant(); + const static auto sg_status_list = json::json_pointer("/attributes/sg_status_list"); + const static auto sg_description = json::json_pointer("/attributes/sg_description"); + const static auto sg_shot_type = json::json_pointer("/attributes/sg_shot_type"); + const static auto sg_sequence_type = json::json_pointer("/attributes/sg_sequence_type"); + const static auto sg_unit = json::json_pointer("/relationships/sg_unit/data/name"); + + try { + const auto &j = indexToData(index); + + switch (role) { + case JSONTreeModel::Roles::JSONRole: + result = QVariantMapFromJson(indexToFullData(index)); + break; + + case JSONTreeModel::Roles::JSONTextRole: + result = QString::fromStdString(indexToFullData(index).dump(2)); + break; + + case JSONTreeModel::Roles::idRole: + try { + if (j.at("id").is_number()) + result = j.at("id").get(); + else + result = QString::fromStdString(j.at("id").get()); + } catch (...) { + } + break; + + case Roles::typeRole: + if (j.contains("type") and j.at("type").is_string()) + result = QString::fromStdString(j.at("type").get()); + break; + + case Roles::descriptionRole: + if (j.contains(sg_description) and j.at(sg_description).is_string()) + result = QString::fromStdString(j.at(sg_description).get()); + break; + + case Roles::divisionRole: + if (j.contains("attributes") and j.at("attributes").contains("sg_division") and + j.at("attributes").at("sg_division").is_string()) + result = QString::fromStdString( + j.at("attributes").at("sg_division").get()); + break; + + case Roles::projectStatusRole: + if (j.contains("attributes") and + j.at("attributes").contains("sg_project_status") and + j.at("attributes").at("sg_project_status").is_string()) + result = QString::fromStdString( + j.at("attributes").at("sg_project_status").get()); + break; + + case Roles::createdRole: + if (j.contains("attributes") and j.at("attributes").contains("created_at") and + j.at("attributes").at("created_at").is_string()) + result = QDateTime::fromString( + QStringFromStd(j.at("attributes").at("created_at").get()), + Qt::ISODate); + break; + + case Roles::statusRole: + if (j.contains(sg_status_list)) + result = QString::fromStdString( + j.at(sg_status_list).is_null() ? "" + : j.at(sg_status_list).get()); + break; + + case Roles::unitRole: + if (j.contains(sg_unit)) + result = QString::fromStdString(j.at(sg_unit).get()); + break; + + case Roles::subtypeRole: + if (j.count("subtype")) + result = QString::fromStdString(j.at("subtype").get()); + break; + + case Roles::nameRole: + case Qt::DisplayRole: + try { + if (j.count("name")) + result = QString::fromStdString(j.at("name")); + else if (j.count("attributes") and j.at("attributes").count("code")) + result = QString::fromStdString(j.at("attributes").at("code")); + else if (j.count("attributes") and j.at("attributes").count("name")) + result = QString::fromStdString(j.at("attributes").at("name")); + } catch (...) { + } + break; + + default: + result = JSONTreeModel::data(index, role); + break; + } + } catch (const std::exception &err) { + spdlog::warn("{} {} {} {}", __PRETTY_FUNCTION__, err.what(), role, index.row()); + } + + return result; +} + +QVariant ShotBrowserFilterModel::get(const QModelIndex &item, const QString &role) const { + auto index = mapToSource(item); + return dynamic_cast(sourceModel())->get(index, role); +} + +QModelIndex ShotBrowserFilterModel::searchRecursive( + const QVariant &value, + const QString &role, + const QModelIndex &parent, + const int start, + const int depth) { + auto smodel = dynamic_cast(sourceModel()); + return mapFromSource( + smodel->searchRecursive(value, role, mapToSource(parent), start, depth)); +} + +QVariant +ShotBrowserSequenceFilterModel::get(const QModelIndex &item, const QString &role) const { + auto index = mapToSource(item); + return dynamic_cast(sourceModel())->get(index, role); +} + +QModelIndex ShotBrowserSequenceFilterModel::searchRecursive( + const QVariant &value, + const QString &role, + const QModelIndex &parent, + const int start, + const int depth) { + auto smodel = dynamic_cast(sourceModel()); + return mapFromSource( + smodel->searchRecursive(value, role, mapToSource(parent), start, depth)); +} + + +QVariant ShotBrowserSequenceModel::data(const QModelIndex &index, int role) const { + auto result = QVariant(); + const static auto sg_asset_name = json::json_pointer("/attributes/sg_asset_name"); + + try { + const auto &j = indexToData(index); + + switch (role) { + + case Roles::assetNameRole: + if (j.contains(sg_asset_name)) + result = QString::fromStdString(j.at(sg_asset_name).get()); + break; + + default: + result = ShotBrowserListModel::data(index, role); + break; + } + } catch (const std::exception &err) { + spdlog::warn("{} {} {} {}", __PRETTY_FUNCTION__, err.what(), role, index.row()); + } + + return result; +} + +void ShotBrowserSequenceFilterModel::setHideStatus(const QStringList &value) { + std::set new_value; + + for (const auto &i : value) + new_value.insert(i); + + if (new_value != hide_status_) { + hide_status_ = new_value; + emit hideStatusChanged(); + invalidateFilter(); + emit filterChanged(); + } +} + +QStringList ShotBrowserSequenceFilterModel::hideStatus() const { + auto result = QStringList(); + + for (const auto &i : hide_status_) + result.push_back(i); + + return result; +} + + +bool ShotBrowserSequenceFilterModel::filterAcceptsRow( + int source_row, const QModelIndex &source_parent) const { + auto result = true; + auto source_index = sourceModel()->index(source_row, 0, source_parent); + + if (not hide_status_.empty() and source_index.isValid() and + hide_status_.count( + source_index.data(ShotBrowserListModel::Roles::statusRole).toString())) + return false; + + if (not filter_unit_.empty() and sourceModel()) { + QModelIndex index = sourceModel()->index(source_row, 0, source_parent); + auto value = index.data(ShotBrowserListModel::Roles::unitRole); + auto no_unit = QVariant::fromValue(QString("No Unit")); + auto shot_type = QVariant::fromValue(QString("Shot")); + + for (const auto &i : filter_unit_) + if (i == value) + return false; + else if ( + i == no_unit and + source_index.data(ShotBrowserListModel::Roles::typeRole) == shot_type and + (value.isNull() or value.toString() == QString())) + return false; + } + + if (not filter_type_.empty() and sourceModel()) { + QModelIndex index = sourceModel()->index(source_row, 0, source_parent); + auto value = index.data(ShotBrowserListModel::Roles::subtypeRole); + + for (const auto &i : filter_type_) + if (i == value) + return false; + } + + if (hide_empty_ and source_index.isValid() and + source_index.data(ShotBrowserListModel::Roles::typeRole) != + QVariant::fromValue(QString("Shot"))) { + auto rc = sourceModel()->rowCount(source_index); + + // check all our children haven't been filtered.. + auto has_child = false; + for (auto i = 0; i < rc and not has_child; i++) + has_child = filterAcceptsRow(i, source_index); + + if (not has_child) + return false; + } + + return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); +} + +bool ShotBrowserFilterModel::filterAcceptsRow( + int source_row, const QModelIndex &source_parent) const { + // check level + if (not selection_filter_.empty() and sourceModel()) { + QModelIndex index = sourceModel()->index(source_row, 0, source_parent); + if (not selection_filter_.contains(index)) + return false; + } + + if (not filter_division_.empty() and sourceModel()) { + QModelIndex index = sourceModel()->index(source_row, 0, source_parent); + auto value = index.data(ShotBrowserListModel::Roles::divisionRole); + + if (not value.isNull()) { + for (const auto &i : filter_division_) + if (i == value) + return false; + } + } + + if (not filter_project_status_.empty() and sourceModel()) { + QModelIndex index = sourceModel()->index(source_row, 0, source_parent); + auto value = index.data(ShotBrowserListModel::Roles::projectStatusRole); + + if (not value.isNull()) { + for (const auto &i : filter_project_status_) + if (i == value) + return false; + } + } + + return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/model_ui.hpp b/src/plugin/data_source/dneg/shotbrowser/src/model_ui.hpp new file mode 100644 index 000000000..3cd90788a --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/model_ui.hpp @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +CAF_PUSH_WARNINGS +#include +#include +CAF_POP_WARNINGS + +#include "xstudio/ui/qml/helper_ui.hpp" +#include "xstudio/ui/qml/json_tree_model_ui.hpp" +#include "xstudio/shotgun_client/shotgun_client.hpp" +#include "xstudio/utility/json_store.hpp" + +namespace xstudio { +// using namespace shotgun_client; +namespace ui::qml { + + // dumping ground for termModels + class ShotBrowserListModel : public JSONTreeModel { + Q_OBJECT + + public: + const static inline std::vector RoleNames = { + "createdRole", + "descriptionRole", + "divisionRole", + "nameRole", + "projectStatusRole", + "sgTypeRole", + "statusRole", + "subtypeRole", + "typeRole", + "unitRole"}; + + enum Roles { + createdRole = JSONTreeModel::Roles::LASTROLE, + descriptionRole, + divisionRole, + nameRole, + projectStatusRole, + sgTypeRole, + statusRole, + subtypeRole, + typeRole, + unitRole, + LASTROLE + }; + + ShotBrowserListModel(QObject *parent = nullptr) : JSONTreeModel(parent) { + setRoleNames(RoleNames); + } + + [[nodiscard]] QVariant + data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + }; + + class ShotBrowserSequenceModel : public ShotBrowserListModel { + Q_OBJECT + + Q_PROPERTY(QStringList types READ types WRITE setTypes NOTIFY typesChanged) + + public: + const static inline std::vector RoleNames = {"assetNameRole"}; + + enum Roles { + assetNameRole = ShotBrowserListModel::Roles::LASTROLE, + }; + + ShotBrowserSequenceModel(QObject *parent = nullptr) : ShotBrowserListModel(parent) { + auto roles = ShotBrowserListModel::RoleNames; + roles.insert(roles.end(), RoleNames.begin(), RoleNames.end()); + setRoleNames(roles); + } + + + void setTypes(const QStringList &value) { + if (types_ != value) { + types_ = value; + emit typesChanged(); + } + } + + static nlohmann::json flatToTree(const nlohmann::json &src, QStringList &_types); + static nlohmann::json flatToAssetTree(const nlohmann::json &src, QStringList &_types); + + [[nodiscard]] QVariant + data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + [[nodiscard]] QStringList types() const { return types_; } + + signals: + void typesChanged(); + + private: + static nlohmann::json sortByNameAndType(const nlohmann::json &json); + QStringList types_{}; + }; + + class ShotBrowserSequenceFilterModel : public QSortFilterProxyModel { + Q_OBJECT + + Q_PROPERTY( + QStringList hideStatus READ hideStatus WRITE setHideStatus NOTIFY hideStatusChanged) + + Q_PROPERTY(bool hideEmpty READ hideEmpty WRITE setHideEmpty NOTIFY hideEmptyChanged) + + Q_PROPERTY(QVariantList unitFilter READ unitFilter WRITE setUnitFilter NOTIFY + unitFilterChanged) + + Q_PROPERTY(QVariantList typeFilter READ typeFilter WRITE setTypeFilter NOTIFY + typeFilterChanged) + + QML_NAMED_ELEMENT("ShotBrowserSequenceFilterModel") + + public: + ShotBrowserSequenceFilterModel(QObject *parent = nullptr) + : QSortFilterProxyModel(parent) {} + + Q_INVOKABLE [[nodiscard]] QVariant + get(const QModelIndex &item, const QString &role = "display") const; + + Q_INVOKABLE QModelIndex searchRecursive( + const QVariant &value, + const QString &role = "display", + const QModelIndex &parent = QModelIndex(), + const int start = 0, + const int depth = -1); + + [[nodiscard]] bool hideEmpty() const { return hide_empty_; } + [[nodiscard]] QStringList hideStatus() const; + [[nodiscard]] QVariantList unitFilter() const { return filter_unit_; } + [[nodiscard]] QVariantList typeFilter() const { return filter_type_; } + + void setHideStatus(const QStringList &value); + + void setHideEmpty(const bool value) { + if (hide_empty_ != value) { + hide_empty_ = value; + emit hideEmptyChanged(); + invalidateFilter(); + emit filterChanged(); + } + } + + void setUnitFilter(const QVariantList &filter) { + if (filter_unit_ != filter) { + filter_unit_ = filter; + emit unitFilterChanged(); + invalidateFilter(); + emit filterChanged(); + } + } + + void setTypeFilter(const QVariantList &filter) { + if (filter_type_ != filter) { + filter_type_ = filter; + emit typeFilterChanged(); + invalidateFilter(); + emit filterChanged(); + } + } + + + signals: + void hideStatusChanged(); + void unitFilterChanged(); + void typeFilterChanged(); + void hideEmptyChanged(); + void filterChanged(); + + protected: + [[nodiscard]] bool + filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + + + private: + std::set hide_status_; + QVariantList filter_unit_; + QVariantList filter_type_; + bool hide_empty_{false}; + }; + + + class ShotBrowserFilterModel : public QSortFilterProxyModel { + Q_OBJECT + + Q_PROPERTY(int length READ length NOTIFY lengthChanged) + Q_PROPERTY(int count READ length NOTIFY lengthChanged) + + Q_PROPERTY(QVariantList divisionFilter READ divisionFilter WRITE setDivisionFilter + NOTIFY divisionFilterChanged) + + Q_PROPERTY(QVariantList projectStatusFilter READ projectStatusFilter WRITE + setProjectStatusFilter NOTIFY projectStatusFilterChanged) + + Q_PROPERTY(QItemSelection selectionFilter READ selectionFilter WRITE setSelectionFilter + NOTIFY selectionFilterChanged) + + public: + ShotBrowserFilterModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) { + connect( + this, + &QAbstractListModel::rowsInserted, + this, + &ShotBrowserFilterModel::lengthChanged); + connect( + this, + &QAbstractListModel::modelReset, + this, + &ShotBrowserFilterModel::lengthChanged); + connect( + this, + &QAbstractListModel::rowsRemoved, + this, + &ShotBrowserFilterModel::lengthChanged); + } + + Q_INVOKABLE [[nodiscard]] QVariant + get(const QModelIndex &item, const QString &role = "display") const; + + [[nodiscard]] int length() const { return rowCount(); } + + Q_INVOKABLE QModelIndex searchRecursive( + const QVariant &value, + const QString &role = "display", + const QModelIndex &parent = QModelIndex(), + const int start = 0, + const int depth = -1); + + + [[nodiscard]] QItemSelection selectionFilter() const { return selection_filter_; } + [[nodiscard]] QVariantList divisionFilter() const { return filter_division_; } + [[nodiscard]] QVariantList projectStatusFilter() const { + return filter_project_status_; + } + + void setSelectionFilter(const QItemSelection &selection) { + if (selection_filter_ != selection) { + selection_filter_ = selection; + emit selectionFilterChanged(); + invalidateFilter(); + // setDynamicSortFilter(false); + // sort(0, sortOrder()); + // setDynamicSortFilter(true); + } + } + + void setDivisionFilter(const QVariantList &filter) { + if (filter_division_ != filter) { + filter_division_ = filter; + emit divisionFilterChanged(); + invalidateFilter(); + } + } + + void setProjectStatusFilter(const QVariantList &filter) { + if (filter_project_status_ != filter) { + filter_project_status_ = filter; + emit projectStatusFilterChanged(); + invalidateFilter(); + } + } + + signals: + void lengthChanged(); + void selectionFilterChanged(); + void divisionFilterChanged(); + void projectStatusFilterChanged(); + + protected: + [[nodiscard]] bool + filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + // [[nodiscard]] bool + // lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const; + + private: + QItemSelection selection_filter_; + QVariantList filter_division_; + QVariantList filter_project_status_; + }; + + +} // namespace ui::qml +} // namespace xstudio \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/post_actions.cpp b/src/plugin/data_source/dneg/shotbrowser/src/post_actions.cpp new file mode 100644 index 000000000..ac0ceca81 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/post_actions.cpp @@ -0,0 +1,475 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "xstudio/shotgun_client/shotgun_client.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/thumbnail/thumbnail.hpp" +#include "xstudio/utility/notification_handler.hpp" + +#include "shotbrowser_plugin.hpp" + +using namespace xstudio; +using namespace xstudio::shotgun_client; +using namespace xstudio::shotbrowser; +using namespace xstudio::utility; + + +void ShotBrowser::create_playlist_notes( + caf::typed_response_promise rp, + const utility::JsonStore ¬es, + const utility::Uuid &playlist_uuid) { + + const std::string ui(R"( + import xStudio 1.0 + import QtQuick + XsLabel { + anchors.fill: parent + font.pixelSize: XsStyle.popupControlFontSize*1.2 + verticalAlignment: Text.AlignVCenter + font.weight: Font.Bold + color: palette.highlight + text: "SG" + } + )"); + + try { + scoped_actor sys{system()}; + + // get session + auto session = request_receive( + *sys, + system().registry().template get(global_registry), + session::session_atom_v); + + auto bookmarks = + request_receive(*sys, session, bookmark::get_bookmark_atom_v); + + auto count = notes.size(); + auto failed = std::make_shared(0); + auto succeed = std::make_shared(0); + auto results = std::make_shared>(count); + + auto index = 0; + + // here we get the set of bookmark uuids for all the bookmarks that + // have annotations that are going to be part of the publish + std::map>> + annotated_bookmark_uuids; + for (const auto &j : notes) { + const auto &ann_uuids = j.at("note_annotation"); + for (const auto &anno_details : ann_uuids) { + auto bookmark_uuid = + anno_details.at("bookmark_uuid").template get(); + auto annotation_title = anno_details.at("title").template get(); + auto jpeg_buf = request_receive>( + *sys, + bookmarks, + bookmark::render_annotations_atom_v, + bookmark_uuid, + 2560 // QHD res for sharpness + ); + annotated_bookmark_uuids[bookmark_uuid] = + std::make_pair(annotation_title, jpeg_buf); + } + } + + for (const auto &j : notes) { + + // need to capture result to embed in playlist and add any media.. + // spdlog::warn("{}", j["payload"].dump(2)); + mail(shotgun_create_entity_atom_v, "notes", utility::JsonStore(j.at("payload"))) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &result) mutable { + (*results)[index] = result; + try { + if (result.count("errors")) + (*failed)++; + else { + + // get new playlist id.. + auto note_id = result.at("data").at("id").template get(); + + // attach any annotations that we generated to the note + for (const auto &ju : j.at("bookmark_uuid")) { + auto p = annotated_bookmark_uuids.find(ju.get()); + if (p == annotated_bookmark_uuids.end()) + continue; + mail( + shotgun_upload_atom_v, + "note", + note_id, + "", + p->second.first, // image title + p->second.second, // jpeg buf + "image/jpeg") + .request(shotgun_, infinite) + .then( + [=](const bool) {}, + [=](const error &err) mutable { + spdlog::warn( + "{} " + "Failed" + " uploa" + "d of " + "annota" + "tion " + "{}", + __PRETTY_FUNCTION__, + to_string(err)); + } + + ); + } + + // spdlog::warn("note {}", result.dump(2)); + // send json to note.. + + for (const auto &ju : j.at("bookmark_uuid")) { + anon_mail( + json_store::set_json_atom_v, + ju.get(), + utility::JsonStore(result.at("data")), + ShotgunMetadataPath + "/note") + .send(bookmarks); + + // add shotgun decorator to note. + anon_mail( + json_store::set_json_atom_v, + ju.get(), + JsonStore( + R"({"icon": "qrc:/shotbrowser_icons/shot_grid.svg", "tooltip": "Published to ShotGrid"})"_json), + "/ui/decorators/shotgrid") + .send(bookmarks); + } + + (*succeed)++; + } + } catch (const std::exception &err) { + (*failed)++; + spdlog::warn( + "{} {} {}", __PRETTY_FUNCTION__, err.what(), result.dump(2)); + } + + if (count == (*failed) + (*succeed)) { + auto jsn = JsonStore( + R"({"data": {"status": ""}, "failed": [], "succeed": [], "succeed_title": [], "failed_title": []})"_json); + for (const auto &r : (*results)) { + if (r.count("errors")) { + jsn["failed"].push_back(r); + jsn["failed_title"].push_back( + notes.at(index).at("payload").at("subject")); + } else { + jsn["succeed"].push_back(r); + jsn["succeed_title"].push_back( + r.at("data").at("attributes").at("subject")); + } + } + + jsn["data"]["status"] = std::string(fmt::format( + "Successfully published {} / {} notes.", + *succeed, + (*failed) + (*succeed))); + rp.deliver(jsn); + } + }, + [=](error &err) mutable { + spdlog::warn( + "Failed create note entity {} {}", + __PRETTY_FUNCTION__, + to_string(err)); + (*failed)++; + + if (count == (*failed) + (*succeed)) { + auto jsn = JsonStore( + R"({"data": {"status": ""}, "failed": [], "succeed": [], "succeed_title": [],"failed_title": []})"_json); + auto index = 0; + for (const auto &r : (*results)) { + if (r.count("errors")) { + jsn["failed"].push_back(r); + jsn["failed_title"].push_back( + notes.at(index).at("payload").at("subject")); + } else { + jsn["succeed"].push_back(r); + jsn["succeed_title"].push_back( + r.at("data").at("attributes").at("subject")); + } + index++; + } + + jsn["data"]["status"] = std::string(fmt::format( + "Successfully published {} / {} notes.", + *succeed, + (*failed) + (*succeed))); + rp.deliver(jsn); + } + }); + index++; + } + + } catch (const XStudioError &err) { + + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.caf_error().what()); + rp.deliver(make_error(xstudio_error::error, err.caf_error().what())); + + } catch (const std::exception &err) { + + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +void ShotBrowser::create_playlist( + caf::typed_response_promise rp, const utility::JsonStore &js) { + // src should be a playlist actor.. + // and we want to update it.. + // retrieve shotgun metadata from playlist, and media items. + auto notification_uuid = Uuid(); + auto playlist = caf::actor(); + + auto failed = [=](const caf::actor &dest, const Uuid &uuid) mutable { + if (dest and not uuid.is_null()) { + auto notify = Notification::WarnNotification("Publish Playlist Failed"); + notify.uuid(uuid); + anon_mail(utility::notification_atom_v, notify).send(dest); + } + }; + + try { + + scoped_actor sys{system()}; + + auto playlist_uuid = Uuid(js["playlist_uuid"]); + auto project_id = js["project_id"].template get(); + auto code = js["code"].template get(); + auto loc = js["location"].template get(); + auto playlist_type = js["playlist_type"].template get(); + + auto session = request_receive( + *sys, + system().registry().template get(global_registry), + session::session_atom_v); + + playlist = request_receive( + *sys, session, session::get_playlist_atom_v, playlist_uuid); + + auto jsn = R"({ + "project":{ "type": "Project", "id":null }, + "code": null, + "sg_location": "unknown", + "sg_type": "Dailies", + "sg_date_and_time": null + })"_json; + + jsn["project"]["id"] = project_id; + jsn["code"] = code; + jsn["sg_location"] = loc; + jsn["sg_type"] = playlist_type; + jsn["sg_date_and_time"] = date_time_and_zone(); + + // "2021-08-18T19:00:00Z" + + auto notify = Notification::ProcessingNotification("Publishing Playlist"); + notification_uuid = notify.uuid(); + anon_mail(utility::notification_atom_v, notify).send(playlist); + + + // need to capture result to embed in playlist and add any media.. + mail(shotgun_create_entity_atom_v, "playlists", utility::JsonStore(jsn)) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &result) mutable { + try { + // get new playlist id.. + auto playlist_id = result.at("data").at("id").template get(); + // update shotgun versions from our source playlist. + // return the result.. + update_playlist_versions( + rp, playlist_uuid, true, playlist_id, notification_uuid); + + } catch (const std::exception &err) { + failed(playlist, notification_uuid); + spdlog::warn("{} {}", __PRETTY_FUNCTION__, result.dump(2)); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { + failed(playlist, notification_uuid); + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + + } catch (const std::exception &err) { + failed(playlist, notification_uuid); + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +void ShotBrowser::rename_tag( + caf::typed_response_promise rp, + const int tag_id, + const std::string &value) { + + // as this is an update, we have to pull current list and then add / push it back.. (I + // THINK) + try { + auto payload = R"({"name": null})"_json; + payload["name"] = value; + + // send update request.. + mail( + shotgun_update_entity_atom_v, + "Tag", + tag_id, + utility::JsonStore(payload), + std::vector({"id"})) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &result) mutable { rp.deliver(result); }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + + } catch (const std::exception &err) { + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + + +void ShotBrowser::remove_entity_tag( + caf::typed_response_promise rp, + const std::string &entity, + const int entity_id, + const int tag_id) { + + // as this is an update, we have to pull current list and then add / push it back.. (I + // THINK) + try { + mail(shotgun_entity_atom_v, entity, entity_id, std::vector({"tags"})) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &result) mutable { + try { + auto current_tags = + result.at("data").at("relationships").at("tags").at("data"); + for (auto it = current_tags.begin(); it != current_tags.end(); ++it) { + if (it->at("id") == tag_id) { + current_tags.erase(it); + break; + } + } + + auto payload = R"({"tags": []})"_json; + payload["tags"] = current_tags; + + // send update request.. + mail( + shotgun_update_entity_atom_v, + entity, + entity_id, + utility::JsonStore(payload), + std::vector({"id", "code", "tags"})) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &result) mutable { rp.deliver(result); }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + + } catch (const std::exception &err) { + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + } catch (const std::exception &err) { + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +void ShotBrowser::create_tag( + caf::typed_response_promise rp, const std::string &value) { + + try { + auto jsn = R"({ + "name": null + })"_json; + + jsn["name"] = value; + + mail(shotgun_create_entity_atom_v, "tags", utility::JsonStore(jsn)) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &result) mutable { + try { + rp.deliver(result); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, result.dump(2)); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + + + } catch (const std::exception &err) { + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +void ShotBrowser::add_entity_tag( + caf::typed_response_promise rp, + const std::string &entity, + const int entity_id, + const int tag_id) { + + // as this is an update, we have to pull current list and then add / push it back.. (I + // THINK) + try { + mail(shotgun_entity_atom_v, entity, entity_id, std::vector({"tags"})) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &result) mutable { + try { + auto current_tags = + result.at("data").at("relationships").at("tags").at("data"); + auto new_tag = R"({"id":null, "type": "Tag"})"_json; + auto payload = R"({"tags": []})"_json; + + new_tag["id"] = tag_id; + current_tags.push_back(new_tag); + payload["tags"] = current_tags; + + // send update request.. + mail( + shotgun_update_entity_atom_v, + entity, + entity_id, + utility::JsonStore(payload), + std::vector({"id", "code", "tags"})) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &result) mutable { rp.deliver(result); }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + + } catch (const std::exception &err) { + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + } catch (const std::exception &err) { + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/preset_model_ui.cpp b/src/plugin/data_source/dneg/shotbrowser/src/preset_model_ui.cpp new file mode 100644 index 000000000..96a49e79d --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/preset_model_ui.cpp @@ -0,0 +1,1329 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "preset_model_ui.hpp" +#include "query_engine.hpp" +#include "model_ui.hpp" + +#include "xstudio/utility/string_helpers.hpp" +#include "xstudio/ui/qml/helper_ui.hpp" +#include "xstudio/global_store/global_store.hpp" +#include "xstudio/atoms.hpp" + +#include +#include +#include + +using namespace xstudio; +using namespace xstudio::utility; +using namespace xstudio::shotgun_client; +using namespace xstudio::ui::qml; +using namespace std::chrono_literals; +using namespace xstudio::global_store; + +namespace { +const static auto q_shot_history_scope = QStringFromStd("Shot History Scope"); +const static auto q_note_history_scope = QStringFromStd("Note History Scope"); +const static auto q_note_history_type = QStringFromStd("Note History Type"); +const static auto q_quick_load = QStringFromStd("Quick Load"); +}; // namespace + +ShotBrowserPresetModel::ShotBrowserPresetModel(QueryEngine &query_engine, QObject *parent) + : query_engine_(query_engine), JSONTreeModel(parent) { + setRoleNames(std::vector( + {"enabledRole", + "entityRole", + "favouriteRole", + "flagRole", + "groupFavouriteRole", + "groupFlagRole", + "groupIdRole", + "groupUserDataRole", + "hiddenRole", + "livelinkRole", + "nameRole", + "negatedRole", + "parentEnabledRole", + "termRole", + "typeRole", + "updateRole", + "userDataRole", + "valueRole"})); + + term_lists_ = new QQmlPropertyMap(this); + + for (const auto &i : ValidTerms.items()) { + auto terms = QStringList(); + for (const auto &j : i.value()) { + terms.push_back(QStringFromStd(j)); + } + term_lists_->insert(QStringFromStd(i.key()), QVariant::fromValue(terms)); + } + + connect(this, &JSONTreeModel::modelReset, this, &ShotBrowserPresetModel::modelModelReset); + + connect( + this, + &JSONTreeModel::rowsInserted, + this, + &ShotBrowserPresetModel::modelRowsInsertedRemoved); + + connect( + this, + &JSONTreeModel::rowsRemoved, + this, + &ShotBrowserPresetModel::modelRowsInsertedRemoved); + + connect(this, &JSONTreeModel::rowsMoved, this, &ShotBrowserPresetModel::modelRowsMoved); + + connect(this, &JSONTreeModel::dataChanged, this, &ShotBrowserPresetModel::modelDataChanged); +} +QStringList ShotBrowserPresetModel::groupFlags() const { + auto result = QStringList(); + + for (auto &i : GroupFlags) + result.append(QStringFromStd(i)); + + return result; +} + +void ShotBrowserPresetModel::modelDataChanged( + const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { + if (roles.contains(flagRole)) { + // could be optimised. + emit noteHistoryScopeChanged(); + emit noteHistoryTypeChanged(); + emit shotHistoryScopeChanged(); + emit quickLoadChanged(); + } else { + // build list of indexes changed + auto indexes = QItemSelection(topLeft, bottomRight).indexes(); + + for (auto &i : indexes) { + auto itype = StdFromQString(i.data(typeRole).toString()); + if (itype == "preset") { + auto group_i = i.parent().parent(); + auto flags = group_i.data(flagRole).toStringList(); + if (not reset_shot_history_scope_ and flags.contains(q_shot_history_scope)) { + reset_shot_history_scope_ = true; + emit shotHistoryScopeChanged(); + } + if (not reset_note_history_scope_ and flags.contains(q_note_history_scope)) { + reset_note_history_scope_ = true; + emit noteHistoryScopeChanged(); + } + if (not reset_note_history_type_ and flags.contains(q_note_history_type)) { + reset_note_history_type_ = true; + emit noteHistoryTypeChanged(); + } + if (not reset_quick_load_ and flags.contains(q_quick_load)) { + reset_quick_load_ = true; + emit quickLoadChanged(); + } + } else if ( + itype == "group" and roles.contains(favouriteRole) and + i.data(favouriteRole).toBool() == false) { + auto flags = i.data(flagRole).toStringList(); + if (flags.contains(q_shot_history_scope)) + emit shotHistoryScopeChanged(); + if (flags.contains(q_note_history_scope)) + emit noteHistoryScopeChanged(); + if (flags.contains(q_note_history_type)) + emit noteHistoryTypeChanged(); + if (flags.contains(q_quick_load)) + emit quickLoadChanged(); + } + } + } +} + +void ShotBrowserPresetModel::modelRowsMoved( + const QModelIndex &sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex &destinationParent, + int destinationRow) { + modelRowsInsertedRemoved(sourceParent, sourceStart, sourceEnd); +} + +void ShotBrowserPresetModel::modelRowsInsertedRemoved( + const QModelIndex &parent, int first, int last) { + // check inserted rows are of interest to our looks ups. + auto ptype = StdFromQString(parent.data(typeRole).toString()); + + if (ptype == "root" or ptype == "") { + // can be optimised.. + emit noteHistoryScopeChanged(); + emit noteHistoryTypeChanged(); + emit shotHistoryScopeChanged(); + emit quickLoadChanged(); + } else if (ptype == "presets") { + auto group_i = parent.parent(); + auto flags = group_i.data(flagRole).toStringList(); + + if (group_i.data(favouriteRole).toBool() == true and + group_i.data(favouriteRole).toBool() == false) { + if (flags.contains(q_shot_history_scope)) + emit shotHistoryScopeChanged(); + if (flags.contains(q_note_history_scope)) + emit noteHistoryScopeChanged(); + if (flags.contains(q_note_history_type)) + emit noteHistoryTypeChanged(); + if (flags.contains(q_quick_load)) + emit quickLoadChanged(); + } + } +} + +void ShotBrowserPresetModel::modelModelReset() { + emit noteHistoryScopeChanged(); + emit noteHistoryTypeChanged(); + emit shotHistoryScopeChanged(); + emit quickLoadChanged(); +} + +QVariantList ShotBrowserPresetModel::quickLoad() { + // scan structure and return QVariantList of hearted presets in groups with the flags + // containing "Shot History Scope" + const static auto qv_group = QVariant::fromValue(QStringFromStd("group")); + auto result = QVariantList(); + + if (reset_quick_load_) { + reset_quick_load_ = false; + QTimer::singleShot(100, this, &ShotBrowserPresetModel::quickLoadChanged); + } else { + try { + for (auto i = 0; i < rowCount(); i++) { + auto gi = index(i, 0); + if (gi.data(favouriteRole).toBool() == true and + gi.data(hiddenRole).toBool() == false and gi.data(typeRole) == qv_group) { + auto flags = gi.data(flagRole).toStringList(); + + if (flags.contains(q_quick_load)) { + // collect indexes of favourited presets.. + auto gpi = index(1, 0, gi); + for (auto j = 0; j < rowCount(gpi); j++) { + auto pi = index(j, 0, gpi); + if (pi.data(favouriteRole).toBool() == true and + pi.data(hiddenRole).toBool() == false) + result.push_back(QPersistentModelIndex(pi)); + } + } + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + return result; +} + +QVariantList ShotBrowserPresetModel::shotHistoryScope() { + // scan structure and return QVariantList of hearted presets in groups with the flags + // containing "Shot History Scope" + const static auto qv_group = QVariant::fromValue(QStringFromStd("group")); + auto result = QVariantList(); + + if (reset_shot_history_scope_) { + reset_shot_history_scope_ = false; + QTimer::singleShot(100, this, &ShotBrowserPresetModel::shotHistoryScopeChanged); + } else { + try { + + for (auto i = 0; i < rowCount(); i++) { + auto gi = index(i, 0); + if (gi.data(favouriteRole).toBool() == true and + gi.data(hiddenRole).toBool() == false and gi.data(typeRole) == qv_group) { + auto flags = gi.data(flagRole).toStringList(); + + if (flags.contains(q_shot_history_scope)) { + // collect indexes of favourited presets.. + auto gpi = index(1, 0, gi); + for (auto j = 0; j < rowCount(gpi); j++) { + auto pi = index(j, 0, gpi); + if (pi.data(favouriteRole).toBool() == true and + pi.data(hiddenRole).toBool() == false) + result.push_back(QPersistentModelIndex(pi)); + } + } + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + return result; +} + +QVariantList ShotBrowserPresetModel::noteHistoryScope() { + // scan structure and return QVariantList of hearted presets in groups with the flags + // containing "Shot History Scope" + const static auto qv_group = QVariant::fromValue(QStringFromStd("group")); + auto result = QVariantList(); + + if (reset_note_history_scope_) { + reset_note_history_scope_ = false; + QTimer::singleShot(100, this, &ShotBrowserPresetModel::noteHistoryScopeChanged); + } else { + try { + + for (auto i = 0; i < rowCount(); i++) { + auto gi = index(i, 0); + if (gi.data(favouriteRole).toBool() == true and + gi.data(hiddenRole).toBool() == false and gi.data(typeRole) == qv_group) { + auto flags = gi.data(flagRole).toStringList(); + + if (flags.contains(q_note_history_scope)) { + // collect indexes of favourited presets.. + auto gpi = index(1, 0, gi); + for (auto j = 0; j < rowCount(gpi); j++) { + auto pi = index(j, 0, gpi); + if (pi.data(favouriteRole).toBool() == true and + pi.data(hiddenRole).toBool() == false) + result.push_back(QPersistentModelIndex(pi)); + } + } + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + return result; +} + +QVariantList ShotBrowserPresetModel::noteHistoryType() { + // scan structure and return QVariantList of hearted presets in groups with the flags + // containing "Shot History Scope" + const static auto qv_group = QVariant::fromValue(QStringFromStd("group")); + auto result = QVariantList(); + + if (reset_note_history_type_) { + reset_note_history_type_ = false; + QTimer::singleShot(100, this, &ShotBrowserPresetModel::noteHistoryTypeChanged); + } else { + try { + + for (auto i = 0; i < rowCount(); i++) { + auto gi = index(i, 0); + if (gi.data(favouriteRole).toBool() == true and + gi.data(hiddenRole).toBool() == false and gi.data(typeRole) == qv_group) { + auto flags = gi.data(flagRole).toStringList(); + + if (flags.contains(q_note_history_type)) { + // collect indexes of favourited presets.. + auto gpi = index(1, 0, gi); + for (auto j = 0; j < rowCount(gpi); j++) { + auto pi = index(j, 0, gpi); + if (pi.data(favouriteRole).toBool() == true and + pi.data(hiddenRole).toBool() == false) + result.push_back(QPersistentModelIndex(pi)); + } + } + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + return result; +} + + +QStringList ShotBrowserPresetModel::entities() const { + auto result = QStringList(); + + for (const auto &i : ValidEntities) { + result.push_back(QStringFromStd(i)); + } + + return result; +} + +QVariant ShotBrowserPresetModel::copy(const QModelIndexList &indexes) const { + auto result = QVariant(); + + if (indexes.size() == 1) { + auto data = mapFromValue(indexes.at(0).data(JSONRole)); + QueryEngine::regenerate_ids(data); + + result = mapFromValue(data); + } else if (indexes.size() > 1) { + auto data = R"([])"_json; + for (const auto &i : indexes) { + auto d = mapFromValue(i.data(JSONRole)); + QueryEngine::regenerate_ids(d); + data.push_back(d); + } + result = mapFromValue(data); + } + return result; +} + +bool ShotBrowserPresetModel::paste( + const QVariant &qdata, const int row, const QModelIndex &parent) { + auto result = true; + + try { + auto items = R"([])"_json; + auto data = mapFromValue(qdata); + auto parent_type = StdFromQString(parent.data(typeRole).toString()); + auto current_row = row; + auto adjusted_parent = parent; + + if (not data.is_array()) { + items.push_back(data); + } else { + items = data; + } + + for (auto &i : items) { + // pasting preset into group, adjust parent.. + if (parent_type == "" and + (not i.count("type") or i.at("type") == "system" or i.at("type") == "preset")) { + adjusted_parent = index(1, 0, parent); + parent_type = StdFromQString(adjusted_parent.data(typeRole).toString()); + current_row = rowCount(adjusted_parent); + } + + if (parent_type == "presets" and + (not i.count("type") or i.at("type") == "system")) { + // old style preset. + try { + auto tmppreset = PresetTemplate; + tmppreset["name"] = i.at("name"); + for (const auto &j : i.at("queries")) { + auto term = TermTemplate; + term["value"] = j.at("value"); + term["term"] = j.at("term"); + + if (TermProperties.count(j["term"])) + term.update(TermProperties[j["term"]]); + + if (j.count("enabled")) + term["enabled"] = j.at("enabled"); + + if (j.count("livelink")) + term["livelink"] = j.at("livelink"); + + if (j.count("negated")) + term["negated"] = j.at("negated"); + tmppreset["children"].push_back(term); + } + QueryEngine::regenerate_ids(tmppreset); + insertRows(current_row, 1, adjusted_parent, tmppreset); + current_row++; + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } else if (parent_type == "presets" and i.at("type") == "preset") { + QueryEngine::regenerate_ids(i); + insertRows(current_row, 1, adjusted_parent, i); + current_row++; + } else if (parent_type == "preset" and i.at("type") == "term") { + QueryEngine::regenerate_ids(i); + insertRows(current_row, 1, adjusted_parent, i); + current_row++; + } else if (parent_type == "" and i.at("type") == "group") { + QueryEngine::regenerate_ids(i); + insertRows(current_row, 1, parent, i); + current_row++; + } else { + result = false; + } + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + result = false; + } + + return result; +} + + +void ShotBrowserPresetModel::resetPresets(const QModelIndexList &indexes) { + for (const auto &i : indexes) { + resetPreset(i); + } +} + +void ShotBrowserPresetModel::resetPreset(const QModelIndex &index) { + auto updated = index.data(Roles::updateRole); + if (!updated.isNull() and updated.toBool() == true) { + auto sp = query_engine_.find_by_id( + UuidFromQUuid(index.data(JSONTreeModel::Roles::idRole).toUuid()), + query_engine_.system_presets().as_json()); + if (sp) { + // here be dragons.. + auto model = const_cast(index.model()); + model->setData(index, mapFromValue(*sp), JSONTreeModel::Roles::JSONRole); + } + } +} + +QModelIndex ShotBrowserPresetModel::duplicate(const QModelIndex &index) { + auto result = QModelIndex(); + + auto data = mapFromValue(index.data(JSONRole)); + QueryEngine::regenerate_ids(data); + + if (insertRows(index.row() + 1, 1, index.parent(), data)) { + result = ShotBrowserPresetModel::index(index.row() + 1, 0, index.parent()); + } + + if (index.data(typeRole) != QVariant::fromValue(QString("term"))) + setData(result, result.data(nameRole).toString() + " - Copy", nameRole); + + return result; +} + +QModelIndex ShotBrowserPresetModel::getPresetGroup(const QModelIndex &index) const { + const auto group = QVariant::fromValue(QString("group")); + auto result = QModelIndex(); + auto i = index; + + while (i.isValid()) { + if (i.data(typeRole) == group) { + result = i; + break; + } + i = i.parent(); + } + + return result; +} + + +QVariant ShotBrowserPresetModel::data(const QModelIndex &index, int role) const { + auto result = QVariant(); + + if (not index.isValid()) + return result; + + try { + const auto &j = indexToData(index); + + try { + + switch (role) { + case JSONTreeModel::Roles::JSONRole: + result = QVariantMapFromJson(indexToFullData(index)); + break; + + case JSONTreeModel::Roles::JSONTextRole: + result = QString::fromStdString(indexToFullData(index).dump(2)); + break; + + case JSONTreeModel::Roles::JSONPathRole: + result = QString::fromStdString(getIndexPath(index).to_string()); + break; + + case Roles::enabledRole: + result = j.at("enabled").get(); + break; + + case Roles::parentEnabledRole: { + auto p = index.parent(); + result = true; + while (p.isValid() and p.data(typeRole) == QVariant("term") and + p.data(termRole) == QVariant("Operator")) { + if (not p.data(enabledRole).toBool()) { + result = false; + break; + } + p = p.parent(); + } + } break; + + case Roles::favouriteRole: + if (j.count("favourite") and not j.at("favourite").is_null()) + result = j.at("favourite").get(); + break; + + case Roles::groupFavouriteRole: + if (j.value("type", "") == "preset") + result = index.parent().parent().data(Roles::favouriteRole).toBool(); + break; + + case Roles::hiddenRole: + // check parent hidden + if (j.value("type", "") == "preset" and + index.parent().parent().data(Roles::hiddenRole).toBool()) + result = true; + else if (j.count("hidden") and not j.at("hidden").is_null()) + result = j.at("hidden").get(); + else + result = false; + break; + + case Roles::updateRole: + if (j.count("update") and not j.at("update").is_null()) + result = j.at("update").get(); + break; + + case Roles::userDataRole: + result = QString::fromStdString(j.value("userdata", "")); + break; + + case Roles::groupFlagRole: + if (j.at("type") == "group") + result = mapFromValue(j.value("flags", R"([])"_json)); + else + result = getPresetGroup(index).data(Roles::flagRole); + break; + + case Roles::flagRole: + result = mapFromValue(j.value("flags", R"([])"_json)); + break; + + case Roles::groupIdRole: + if (j.at("type") == "group") + result = JSONTreeModel::data(index, JSONTreeModel::Roles::idRole); + else + result = getPresetGroup(index).data(JSONTreeModel::Roles::idRole); + break; + + case Roles::groupUserDataRole: + if (j.at("type") == "group") + result = QString::fromStdString(j.value("userdata", "")); + else + result = getPresetGroup(index).data(role); + break; + + case Roles::termRole: + result = QString::fromStdString(j.at("term")); + break; + + case Roles::valueRole: + result = QString::fromStdString(j.at("value")); + break; + + case Roles::entityRole: + if (j.count("entity")) + result = QString::fromStdString(j.at("entity")); + else { + // find group entity.. + auto p = index.parent(); + while (p.isValid() and result.isNull()) { + result = p.data(Roles::entityRole); + p = p.parent(); + } + } + + break; + + case Roles::livelinkRole: + if (j.count("livelink") and not j.at("livelink").is_null()) + result = j.at("livelink").get(); + break; + + case Roles::negatedRole: + if (j.count("negated") and not j.at("negated").is_null()) + result = j.at("negated").get(); + break; + + case JSONTreeModel::Roles::idRole: + result = QString::fromStdString(to_string(j.at("id").get())); + break; + + case Roles::nameRole: + case Qt::DisplayRole: + result = QString::fromStdString(j.at("name")); + break; + + case Roles::typeRole: + result = QString::fromStdString(j.at("type")); + break; + + default: + result = JSONTreeModel::data(index, role); + break; + } + + } catch (const std::exception &err) { + + spdlog::warn( + "{} {} {} {} {} {}", + __PRETTY_FUNCTION__, + err.what(), + role, + index.row(), + index.internalId(), + j.dump(2)); + } + } catch (const std::exception &err) { + + spdlog::warn( + "{} {} {} {} {}", + __PRETTY_FUNCTION__, + err.what(), + role, + index.row(), + index.internalId()); + } + return result; +} + +void ShotBrowserPresetModel::setModelPathData(const std::string &path, const JsonStore &data) { + // don't propagate + if (path == "") + setModelDataBase(data, false); + else { + } +} + +bool ShotBrowserPresetModel::setData( + const QModelIndex &index, const QVariant &value, int role) { + bool result = false; + bool preset_changed = false; + + QVector roles({role}); + + try { + // nlohmann::json &j = indexToData(index); + switch (role) { + + case Roles::enabledRole: + result = baseSetData(index, value, "enabled", QVector({role}), true); + if (index.data(typeRole) == QVariant("term") and + index.data(termRole) == QVariant("Operator")) { + + std::function changedChild = + [&](const QModelIndex &parent) { + for (auto i = 0; i < rowCount(parent); i++) { + auto child = ShotBrowserPresetModel::index(i, 0, parent); + if (child.isValid() and child.data(typeRole) == QVariant("term") and + child.data(termRole) == QVariant("Operator")) + changedChild(child); + } + + emit dataChanged( + ShotBrowserPresetModel::index(0, 0, parent), + ShotBrowserPresetModel::index(rowCount(parent) - 1, 0, parent), + QVector({parentEnabledRole})); + }; + + changedChild(index); + } + preset_changed = true; + // if changed mark update field. + if (result) + markedAsUpdated(index.parent()); + break; + + case Roles::favouriteRole: + result = baseSetData(index, value, "favourite", QVector({role}), true); + if (result) { + if (index.data(typeRole).toString() == "group") { + auto presets = ShotBrowserPresetModel::index(1, 0, index); + emit dataChanged( + ShotBrowserPresetModel::index(0, 0, presets), + ShotBrowserPresetModel::index(rowCount(presets) - 1, 0, presets), + QVector({groupFavouriteRole})); + } + } + break; + + case Roles::hiddenRole: + result = baseSetData(index, value, "hidden", QVector({role}), true); + + // if changed mark update field. + if (result) { + emit presetHidden(index, value.toBool()); + markedAsUpdated(index.parent()); + + if (index.data(typeRole).toString() == "group") { + auto presets = ShotBrowserPresetModel::index(1, 0, index); + emit dataChanged( + ShotBrowserPresetModel::index(0, 0, presets), + ShotBrowserPresetModel::index(rowCount(presets) - 1, 0, presets), + roles); + } + } + break; + + case Roles::negatedRole: + result = baseSetData(index, value, "negated", QVector({role}), true); + preset_changed = true; + // if changed mark update field. + if (result) + markedAsUpdated(index.parent()); + break; + + case Roles::livelinkRole: + result = baseSetData(index, value, "livelink", QVector({role}), true); + // if changed mark update field. + preset_changed = true; + if (result) + markedAsUpdated(index.parent()); + break; + + case Roles::flagRole: + result = baseSetData(index, value, "flags", QVector({role}), true); + // if changed mark update field. + preset_changed = true; + if (result) { + if (index.data(typeRole).toString() == "group") { + auto presets = ShotBrowserPresetModel::index(1, 0, index); + emit dataChanged( + ShotBrowserPresetModel::index(0, 0, presets), + ShotBrowserPresetModel::index(rowCount(presets) - 1, 0, presets), + QVector({groupFlagRole})); + } + + markedAsUpdated(index.parent()); + } + break; + + case Roles::termRole: + result = baseSetData(index, value, "term", QVector({role}), true); + // if changed mark update field. + preset_changed = true; + if (result) + markedAsUpdated(index.parent()); + break; + + case Roles::valueRole: + // spdlog::warn("Roles::valueRole {}", mapFromValue(value).dump(2)); + result = baseSetData(index, value, "value", QVector({role}), true); + preset_changed = true; + + // if changed mark update field. + if (result) + markedAsUpdated(index.parent()); + break; + + case Roles::userDataRole: + result = baseSetData(index, value, "userdata", QVector({role}), true); + break; + + case Roles::nameRole: + result = baseSetData(index, value, "name", QVector({role}), true); + // if changed mark update field. + if (result) + markedAsUpdated(index.parent()); + break; + + default: + result = JSONTreeModel::setData(index, value, role); + break; + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + if (result and preset_changed) { + emit presetChanged(index.parent()); + } + + return result; +} + +bool ShotBrowserPresetModel::removeRows(int row, int count, const QModelIndex &parent) { + if (parent.isValid() and parent.data(typeRole).toString() == QString("preset")) + emit presetChanged(parent); + + return JSONTreeModel::removeRows(row, count, parent); +} + +bool ShotBrowserPresetModel::receiveEvent(const utility::JsonStore &event_pair) { + // is it an event for me ? + // spdlog::warn("{}", event_pair.dump(2)); + + auto result = false; + try { + const auto &event = event_pair.at("redo"); + + auto type = event.value("type", ""); + auto id = event.value("id", Uuid()); + + // ignore reflected events + if (id != model_id_) { + if (type == "set") { + // find index.. + auto path = nlohmann::json::json_pointer(event.value("parent", "")); + path /= "children"; + path /= std::to_string(event.value("row", 0)); + auto index = getPathIndex(path); + if (index.isValid()) { + auto data = event.value("data", nlohmann::json::object()); + + // we can't know the role.. + for (const auto &i : data.items()) { + auto roles = QVector(); + + try { + roles.push_back(role_map_.at(i.key())); + } catch (...) { + } + + result |= + baseSetData(index, mapFromValue(i.value()), i.key(), roles, false); + } + } + } else { + result = JSONTreeModel::receiveEvent(event_pair); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; +} + +QModelIndex ShotBrowserPresetModel::insertGroup(const QString &entity, const int row) { + auto result = QModelIndex(); + auto parent = getPathIndex(nlohmann::json::json_pointer("")); + auto real_row = (row == -1 ? rowCount(parent) : row); + + auto data = GroupTemplate; + // data["id"] = Uuid::generate(); + data["entity"] = StdFromQString(entity); + // data["children"][0]["id"] = Uuid::generate(); + // data["children"][1]["id"] = Uuid::generate(); + data["name"] = "New Group"; + + QueryEngine::regenerate_ids(data); + + if (insertRows(real_row, 1, parent, data)) { + result = index(real_row, 0, parent); + } + + return result; +} + +QModelIndex ShotBrowserPresetModel::insertPreset(const int row, const QModelIndex &parent) { + auto result = QModelIndex(); + + auto data = PresetTemplate; + data["name"] = "New Preset"; + QueryEngine::regenerate_ids(data); + + if (insertRows(row, 1, parent, data)) { + result = index(row, 0, parent); + } + + return result; +} + +QModelIndex ShotBrowserPresetModel::insertTerm( + const QString &term, const int row, const QModelIndex &parent) { + auto result = QModelIndex(); + + auto data = TermTemplate; + data["term"] = StdFromQString(term); + + if (TermProperties.count(data["term"])) { + data.update(TermProperties[data["term"]]); + } + + QueryEngine::regenerate_ids(data); + + if (insertRows(row, 1, parent, data)) { + result = index(row, 0, parent); + markedAsUpdated(parent); + } + + emit presetChanged(parent); + + return result; +} + +QModelIndex ShotBrowserPresetModel::insertOperatorTerm( + const bool anding, const int row, const QModelIndex &parent) { + auto result = QModelIndex(); + + auto data = OperatorTermTemplate; + data["value"] = anding ? "And" : "Or"; + QueryEngine::regenerate_ids(data); + + if (insertRows(row, 1, parent, data)) { + result = index(row, 0, parent); + markedAsUpdated(parent); + } + + return result; +} + +void ShotBrowserPresetModel::markedAsUpdated(const QModelIndex &parent) { + auto preset_index = parent; + while (preset_index.isValid() and preset_index.data(Roles::typeRole).toString() != "preset") + preset_index = preset_index.parent(); + + if (preset_index.isValid() and not preset_index.data(Roles::updateRole).isNull()) + baseSetData( + preset_index, + QVariant::fromValue(true), + "update", + QVector({Roles::updateRole}), + true); +} + +QObject *ShotBrowserPresetModel::termModel( + const QString &qterm, const QString &entity, const int project_id) { + QObject *result = nullptr; + + auto term = StdFromQString(qterm); + + if ((term == "Author" or term == "Recipient")) + term = "User"; + else if (term == "Disable Global") + term += "-" + StdFromQString(entity); + + const auto key = QueryEngine::cache_name_auto(term, project_id); + const auto qkey = QStringFromStd(key); + + if (TermHasNoModel.count(key)) { + // create empty model. + if (not term_models_.contains(qkey)) { + auto model = new ShotBrowserListModel(this); + model->setModelData(R"([{"name": ""}])"_json); + term_models_[qkey] = model; + } + } else { + auto cache = query_engine_.get_cache(key); + if (not cache) { + cache = query_engine_.get_lookup(key); + } + + if (cache) { + if (not term_models_.contains(qkey)) { + auto model = new ShotBrowserListModel(this); + if (cache->is_object()) { + auto data = R"([])"_json; + + auto item = R"({"name": null, "id":null})"_json; + + for (const auto i : cache->items()) { + // don't expose ids if they are numbers + if (not i.value().at("id").is_string() and + std::to_string(i.value().at("id").get()) == i.key()) + continue; + item["name"] = i.key(); + item["id"] = i.value().at("id"); + data.push_back(item); + } + + // try and trim results to something sensible. + // order by id desc + if (term == "Playlist" and data.size() > 4000) { + std::map pl_map; + for (const auto &i : data) + pl_map.insert(std::make_pair( + i.at("id").get(), i.at("name").get())); + data.clear(); + size_t icount = 0; + for (auto it = pl_map.rbegin(); it != pl_map.rend() and icount < 4000; + ++it, icount++) { + item["id"] = it->first; + item["name"] = it->second; + data.push_back(item); + } + } + model->setModelData(data); + } else if (cache->is_array()) { + model->setModelData(*cache); + } + + term_models_[qkey] = model; + } else { + // check row count match.. ? + auto model = dynamic_cast(term_models_[qkey]); + if (cache->is_array() and + static_cast(cache->size()) != model->rowCount()) { + model->setModelData(*cache); + } else if ( + cache->is_object() and + static_cast(cache->size()) != model->rowCount() and + not(model->rowCount() == 4000 and term == "Playlist")) { + + // pruning playlist might mess this up.. watch out.. + auto data = R"([])"_json; + + auto item = R"({"name": null, "id":null})"_json; + for (const auto i : cache->items()) { + // don't expose ids if they are numbers + if (not i.value().at("id").is_string() and + std::to_string(i.value().at("id").get()) == i.key()) + continue; + item["name"] = i.key(); + item["id"] = i.value().at("id"); + data.push_back(item); + } + + // try and trim results to something sensible. + // order by id desc + if (term == "Playlist" and data.size() > 4000) { + std::map pl_map; + for (const auto &i : data) + pl_map.insert(std::make_pair( + i.at("id").get(), i.at("name").get())); + data.clear(); + size_t icount = 0; + for (auto it = pl_map.rbegin(); it != pl_map.rend() and icount < 4000; + ++it, icount++) { + item["id"] = it->first; + item["name"] = it->second; + data.push_back(item); + } + } + + model->setModelData(data); + } + } + } else { + // return empty model... ? + // spdlog::warn("missing cache {}", key); + auto model = new ShotBrowserListModel(this); + model->setModelData(R"([])"_json); + term_models_[qkey] = model; + } + } + + if (term_models_.contains(qkey)) { + result = *(term_models_.find(qkey)); + } + + return result; +} + +void ShotBrowserPresetModel::updateTermModel(const std::string &key, const bool cache) { + const auto qkey = QStringFromStd(key); + // if in model, refresh data. + if (term_models_.contains(qkey)) { + auto data = query_engine_.get_cache(key); + if (not data) + data = query_engine_.get_lookup(key); + + if (data) { + auto model = dynamic_cast(term_models_[qkey]); + if (data->is_array() and static_cast(data->size()) != model->rowCount()) { + model->setModelData(*data); + } else if ( + data->is_object() and static_cast(data->size()) != model->rowCount()) { + auto tmp = R"([])"_json; + + for (const auto i : data->items()) { + auto item = R"({"name": null, "id":null})"_json; + item["name"] = i.key(); + item["id"] = i.value().at("id"); + tmp.push_back(item); + } + model->setModelData(tmp); + } + } + } +} + +QFuture ShotBrowserPresetModel::exportAsSystemPresetsFuture( + const QUrl &qpath, const QModelIndex &index) const { + return QtConcurrent::run([=]() { + auto path = uri_to_posix_path(UriFromQUrl(qpath)); + if (fs::path(path).extension() != ".json") + path += ".json"; + + try { + // get json from index or the lot + auto data = R"({"type": "root", "children": []})"_json; + + if (not index.isValid()) { + data = modelData(); + } else { + data["children"].push_back(indexToFullData(index)); + } + + data = QueryEngine::validate_presets(data, false, JsonStore(), 0, true); + + std::ofstream myfile; + myfile.open(path); + myfile << data.at("children").dump(2) << std::endl; + myfile.close(); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + throw; + } + + return QStringFromStd(std::string("Presets exported to ") + path); + }); +} + +QFuture +ShotBrowserPresetModel::backupPresetsFuture(const QUrl &qpath, const QModelIndex &index) const { + return QtConcurrent::run([=]() { + auto path = uri_to_posix_path(UriFromQUrl(qpath)); + if (fs::path(path).extension() != ".json") + path += ".json"; + + try { + // get json from index or the lot + auto data = R"({"type": "root", "children": []})"_json; + + if (not index.isValid()) { + data = modelData(); + } else { + data["children"].push_back(indexToFullData(index)); + } + + std::ofstream myfile; + myfile.open(path); + myfile << data.at("children").dump(2) << std::endl; + myfile.close(); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + throw; + } + + return QStringFromStd(std::string("Presets Backed up to ") + path); + }); +} + +QFuture ShotBrowserPresetModel::restorePresetsFuture(const QUrl &qpath) { + return QtConcurrent::run([=]() { + auto path = uri_to_posix_path(UriFromQUrl(qpath)); + if (fs::path(path).extension() != ".json") + path += ".json"; + + try { + // // get json from index or the lot + // auto data = R"({"type": "root", "children": []})"_json; + + // if (not index.isValid()) { + // data = modelData(); + // } else { + // data["children"].push_back(indexToFullData(index)); + // } + + std::ifstream myfile; + std::stringstream filedata; + + myfile.open(path); + + myfile >> filedata.rdbuf(); + + auto restored = RootTemplate; + restored["id"] = Uuid::generate(); + restored["children"] = nlohmann::json::parse(filedata.str()); + + setModelDataBase(restored, true); + myfile.close(); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + throw; + } + + return QStringFromStd(std::string("Presets restored from ") + path); + }); +} + +void ShotBrowserPresetFilterModel::setShowHidden(const bool value) { + if (value != show_hidden_) { + show_hidden_ = value; + emit showHiddenChanged(); + invalidateFilter(); + } +} + +void ShotBrowserPresetFilterModel::setOnlyShowFavourite(const bool value) { + if (value != only_show_favourite_) { + only_show_favourite_ = value; + emit onlyShowFavouriteChanged(); + invalidateFilter(); + } +} + +void ShotBrowserPresetFilterModel::setOnlyShowPresets(const bool value) { + if (value != only_show_presets_) { + only_show_presets_ = value; + emit onlyShowPresetsChanged(); + invalidateFilter(); + } +} + +void ShotBrowserPresetFilterModel::setIgnoreToolbar(const bool value) { + if (value != ignore_tool_bar_) { + ignore_tool_bar_ = value; + emit ignoreToolbarChanged(); + invalidateFilter(); + } +} + +void ShotBrowserPresetFilterModel::setFilterGroupUserData(const QVariant &filter) { + if (filter_group_user_data_ != filter) { + filter_group_user_data_ = filter; + emit filterGroupUserDataChanged(); + invalidateFilter(); + } +} + +void ShotBrowserPresetFilterModel::setFilterUserData(const QVariant &filter) { + if (filter != filter_user_data_) { + filter_user_data_ = filter; + emit filterUserDataChanged(); + invalidateFilter(); + } +} + +bool ShotBrowserPresetFilterModel::filterAcceptsRow( + int source_row, const QModelIndex &source_parent) const { + const static auto grp = QVariant::fromValue(QString("group")); + const static auto preset = QVariant::fromValue(QString("preset")); + const static auto presets = QVariant::fromValue(QString("presets")); + const static auto hidden = QVariant::fromValue(true); + const static auto not_favourite = QVariant::fromValue(false); + + auto accept = true; + + auto source_index = sourceModel()->index(source_row, 0, source_parent); + + if (source_index.isValid()) { + auto type = source_index.data(ShotBrowserPresetModel::Roles::typeRole); + auto favoured = QVariant(); + auto group_favoured = QVariant(); + if (only_show_favourite_) { + favoured = source_index.data(ShotBrowserPresetModel::Roles::favouriteRole); + group_favoured = + source_index.data(ShotBrowserPresetModel::Roles::groupFavouriteRole); + } + + if (only_show_presets_ and type != preset) + accept = false; + else if ( + not filter_group_user_data_.isNull() and + not source_index.data(ShotBrowserPresetModel::Roles::groupUserDataRole).isNull() and + source_index.data(ShotBrowserPresetModel::Roles::groupUserDataRole) != + filter_group_user_data_) + accept = false; + else if ( + not show_hidden_ and + source_index.data(ShotBrowserPresetModel::Roles::hiddenRole) == hidden) + accept = false; + else if ( + only_show_favourite_ and + ((type == preset and not favoured.isNull() and + (favoured == not_favourite or group_favoured == not_favourite)) or + (type == grp and favoured == not_favourite))) + accept = false; + else if ( + not filter_user_data_.isNull() and type == preset and + source_index.data(ShotBrowserPresetModel::Roles::userDataRole).toString() != + filter_user_data_.toString()) + accept = false; + else if ( + ignore_tool_bar_ and source_index.data(ShotBrowserPresetModel::Roles::groupFlagRole) + .toStringList() + .contains("Ignore Toolbar")) + accept = false; + } + + return accept; +} + +bool ShotBrowserPresetTreeFilterModel::filterAcceptsRow( + int source_row, const QModelIndex &source_parent) const { + + return true; +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/preset_model_ui.hpp b/src/plugin/data_source/dneg/shotbrowser/src/preset_model_ui.hpp new file mode 100644 index 000000000..b66a324d1 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/preset_model_ui.hpp @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +CAF_PUSH_WARNINGS +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +CAF_POP_WARNINGS + +#include "xstudio/ui/qml/helper_ui.hpp" +#include "xstudio/ui/qml/json_tree_model_ui.hpp" +#include "xstudio/shotgun_client/shotgun_client.hpp" +#include "xstudio/utility/json_store.hpp" + +#include "query_engine.hpp" + +namespace xstudio { +using namespace shotgun_client; + +namespace ui::qml { + + + class ShotBrowserPresetModel : public JSONTreeModel { + Q_PROPERTY(QStringList entities READ entities NOTIFY entitiesChanged) + Q_PROPERTY(QQmlPropertyMap *termLists READ termLists NOTIFY termListsChanged) + + Q_PROPERTY(QVariantList quickLoad READ quickLoad NOTIFY quickLoadChanged) + + Q_PROPERTY( + QVariantList shotHistoryScope READ shotHistoryScope NOTIFY shotHistoryScopeChanged) + + Q_PROPERTY( + QVariantList noteHistoryScope READ noteHistoryScope NOTIFY noteHistoryScopeChanged) + Q_PROPERTY( + QVariantList noteHistoryType READ noteHistoryType NOTIFY noteHistoryTypeChanged) + + Q_PROPERTY(QStringList groupFlags READ groupFlags NOTIFY groupFlagsChanged) + + Q_OBJECT + public: + enum Roles { + enabledRole = JSONTreeModel::Roles::LASTROLE, + entityRole, + favouriteRole, + flagRole, + groupFavouriteRole, + groupFlagRole, + groupIdRole, + groupUserDataRole, + hiddenRole, + livelinkRole, + nameRole, + negatedRole, + parentEnabledRole, + termRole, + typeRole, + updateRole, + userDataRole, + valueRole + }; + + ShotBrowserPresetModel(QueryEngine &query_engine, QObject *parent = nullptr); + + [[nodiscard]] QVariant + data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + bool setData( + const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + void setModelPathData(const std::string &path, const utility::JsonStore &data); + + bool receiveEvent(const utility::JsonStore &event) override; + + Q_INVOKABLE QModelIndex insertGroup(const QString &entity, const int row); + Q_INVOKABLE QModelIndex insertPreset(const int row, const QModelIndex &parent); + Q_INVOKABLE QModelIndex + insertTerm(const QString &term, const int row, const QModelIndex &parent); + + Q_INVOKABLE QVariantList noteHistoryScope(); + Q_INVOKABLE QVariantList noteHistoryType(); + Q_INVOKABLE QVariantList shotHistoryScope(); + Q_INVOKABLE QVariantList quickLoad(); + Q_INVOKABLE QStringList groupFlags() const; + + Q_INVOKABLE QModelIndex + insertOperatorTerm(const bool anding, const int row, const QModelIndex &parent); + + [[nodiscard]] QStringList entities() const; + + QQmlPropertyMap *termLists() const { return term_lists_; } + + Q_INVOKABLE QObject * + termModel(const QString &term, const QString &entity = "", const int project_id = -1); + + Q_INVOKABLE bool + removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + + Q_INVOKABLE void resetPresets(const QModelIndexList &indexes); + Q_INVOKABLE void resetPreset(const QModelIndex &index); + Q_INVOKABLE QModelIndex duplicate(const QModelIndex &index); + Q_INVOKABLE QVariant copy(const QModelIndexList &indexes) const; + Q_INVOKABLE bool paste(const QVariant &data, const int row, const QModelIndex &parent); + + Q_INVOKABLE QModelIndex getPresetGroup(const QModelIndex &index) const; + + Q_INVOKABLE QFuture exportAsSystemPresetsFuture( + const QUrl &path, const QModelIndex &index = QModelIndex()) const; + Q_INVOKABLE QString exportAsSystemPresets( + const QUrl &path, const QModelIndex &index = QModelIndex()) const { + return exportAsSystemPresetsFuture(path, index).result(); + } + + Q_INVOKABLE QFuture + backupPresetsFuture(const QUrl &path, const QModelIndex &index = QModelIndex()) const; + Q_INVOKABLE QString + backupPresets(const QUrl &path, const QModelIndex &index = QModelIndex()) const { + return backupPresetsFuture(path, index).result(); + } + + Q_INVOKABLE QFuture restorePresetsFuture(const QUrl &path); + Q_INVOKABLE QString restorePresets(const QUrl &path) { + return restorePresetsFuture(path).result(); + } + + void updateTermModel(const std::string &key, const bool cache); + + void modelModelReset(); + void modelRowsInsertedRemoved(const QModelIndex &parent, int first, int last); + void modelRowsMoved( + const QModelIndex &sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex &destinationParent, + int destinationRow); + void modelDataChanged( + const QModelIndex &topLeft, + const QModelIndex &bottomRight, + const QVector &roles = QVector()); + + signals: + void entitiesChanged(); + void termListsChanged(); + void presetChanged(QModelIndex); + void presetHidden(QModelIndex, bool); + void shotHistoryScopeChanged(); + void noteHistoryScopeChanged(); + void noteHistoryTypeChanged(); + void quickLoadChanged(); + void groupFlagsChanged(); + + private: + void markedAsUpdated(const QModelIndex &parent); + + // need flag role ? + + const std::map role_map_ = { + {"update", Roles::updateRole}, + {"hidden", Roles::hiddenRole}, + {"enabled", Roles::enabledRole}, + {"favourite", Roles::favouriteRole}, + {"term", Roles::termRole}, + {"value", Roles::valueRole}, + {"userdata", Roles::userDataRole}, + {"userdata", Roles::groupUserDataRole}, + {"name", Roles::nameRole}, + {"flags", Roles::flagRole}, + {"negated", Roles::negatedRole}}; + QQmlPropertyMap *term_lists_{nullptr}; + + QMap term_models_; + QueryEngine &query_engine_; + bool reset_note_history_scope_{false}; + bool reset_note_history_type_{false}; + bool reset_shot_history_scope_{false}; + bool reset_quick_load_{false}; + }; + + class ShotBrowserPresetTreeFilterModel : public QSortFilterProxyModel { + Q_OBJECT + QML_NAMED_ELEMENT("ShotBrowserSequenceFilterModel") + + public: + ShotBrowserPresetTreeFilterModel(QObject *parent = nullptr) + : QSortFilterProxyModel(parent) { + setDynamicSortFilter(true); + } + + protected: + [[nodiscard]] bool + filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + }; + + class ShotBrowserPresetFilterModel : public QSortFilterProxyModel { + Q_OBJECT + + Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged) + Q_PROPERTY(bool onlyShowFavourite READ onlyShowFavourite WRITE setOnlyShowFavourite + NOTIFY onlyShowFavouriteChanged) + + Q_PROPERTY(bool onlyShowPresets READ onlyShowPresets WRITE setOnlyShowPresets NOTIFY + onlyShowPresetsChanged) + + Q_PROPERTY(bool ignoreToolbar READ ignoreToolbar WRITE setIgnoreToolbar NOTIFY + ignoreToolbarChanged) + + Q_PROPERTY(QVariant filterUserData READ filterUserData WRITE setFilterUserData NOTIFY + filterUserDataChanged) + + Q_PROPERTY(QVariant filterGroupUserData READ filterGroupUserData WRITE + setFilterGroupUserData NOTIFY filterGroupUserDataChanged) + + + public: + ShotBrowserPresetFilterModel(QObject *parent = nullptr) + : QSortFilterProxyModel(parent) { + setDynamicSortFilter(true); + } + + [[nodiscard]] bool showHidden() const { return show_hidden_; } + [[nodiscard]] bool onlyShowFavourite() const { return only_show_favourite_; } + [[nodiscard]] bool onlyShowPresets() const { return only_show_presets_; } + [[nodiscard]] bool ignoreToolbar() const { return ignore_tool_bar_; } + [[nodiscard]] QVariant filterUserData() const { return filter_user_data_; } + [[nodiscard]] QVariant filterGroupUserData() const { return filter_group_user_data_; } + + Q_INVOKABLE void setFilterGroupUserData(const QVariant &filter); + Q_INVOKABLE void setFilterUserData(const QVariant &filter); + Q_INVOKABLE void setShowHidden(const bool value); + Q_INVOKABLE void setOnlyShowFavourite(const bool value); + Q_INVOKABLE void setOnlyShowPresets(const bool value); + Q_INVOKABLE void setIgnoreToolbar(const bool value); + + signals: + void ignoreToolbarChanged(); + void showHiddenChanged(); + void onlyShowPresetsChanged(); + void onlyShowFavouriteChanged(); + void filterUserDataChanged(); + void filterGroupUserDataChanged(); + + protected: + [[nodiscard]] bool + filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + + private: + QVariant filter_group_user_data_; + bool show_hidden_{false}; + bool only_show_favourite_{false}; + bool only_show_presets_{false}; + bool ignore_tool_bar_{false}; + QVariant filter_user_data_; + }; +} // namespace ui::qml +} // namespace xstudio diff --git a/src/plugin/data_source/dneg/shotbrowser/src/put_actions.cpp b/src/plugin/data_source/dneg/shotbrowser/src/put_actions.cpp new file mode 100644 index 000000000..bd1380abf --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/put_actions.cpp @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "xstudio/shotgun_client/shotgun_client.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/notification_handler.hpp" + +#include "shotbrowser_plugin.hpp" + +using namespace xstudio; +using namespace xstudio::shotgun_client; +using namespace xstudio::shotbrowser; +using namespace xstudio::utility; + +void ShotBrowser::update_playlist_versions( + caf::typed_response_promise rp, + const utility::Uuid &playlist_uuid, + const bool append, + const int playlist_id, + const utility::Uuid ¬ification_uuid_) { + // src should be a playlist actor.. + // and we want to update it.. + // retrieve shotgun metadata from playlist, and media items. + + auto playlist = caf::actor(); + auto notification_uuid = notification_uuid_; + + auto failed = [=](const caf::actor &dest, const Uuid &uuid) mutable { + if (dest and not uuid.is_null()) { + auto notify = Notification::WarnNotification("Update Playlist Failed"); + notify.uuid(uuid); + anon_mail(utility::notification_atom_v, notify).send(dest); + } + }; + + auto succeeded = [=](const caf::actor &dest, const Uuid &uuid) mutable { + if (dest and not uuid.is_null()) { + auto notify = Notification::InfoNotification( + "Update Playlist Succeeded", std::chrono::seconds(5)); + notify.uuid(uuid); + anon_mail(utility::notification_atom_v, notify).send(dest); + } + }; + + try { + + scoped_actor sys{system()}; + + auto session = request_receive( + *sys, + system().registry().template get(global_registry), + session::session_atom_v); + + playlist = request_receive( + *sys, session, session::get_playlist_atom_v, playlist_uuid); + + if (notification_uuid.is_null()) { + auto notify = Notification::ProcessingNotification("Updating Playlist"); + notification_uuid = notify.uuid(); + anon_mail(utility::notification_atom_v, notify).send(playlist); + } + + auto pl_id = playlist_id; + if (not pl_id) { + auto plsg = request_receive( + *sys, playlist, json_store::get_json_atom_v, ShotgunMetadataPath + "/playlist"); + + pl_id = plsg["id"].template get(); + } + + auto media = + request_receive>(*sys, playlist, playlist::get_media_atom_v); + + // foreach media actor get it's shogtun metadata. + auto jsn = R"({"versions":[]})"_json; + auto ver = R"({"id": 0, "type": "Version"})"_json; + std::map version_order_map; + + if (append) { + auto current_vers = request_receive_wait( + *sys, + shotgun_, + infinite, + shotgun_entity_atom_v, + "Playlists", + pl_id, + std::vector({"versions"})) + .at("data") + .at("relationships") + .at("versions") + .at("data"); + + for (const auto &i : current_vers) { + ver["id"] = i.at("id"); + jsn["versions"].push_back(ver); + + // we won't acutally use this, but will stop adding same version multiple times. + version_order_map[i.at("id").get()] = 0; + } + // { + // "id": 48906133, + // "name": "CG_ldev_pipe_lighting_sgco_test_L010_BEAUTY_v005", + // "type": "Version" + // }, + } + + // get media detail + int sort_order = 10; + for (const auto &i : media) { + try { + auto mjson = request_receive( + *sys, + i.actor(), + json_store::get_json_atom_v, + utility::Uuid(), + ShotgunMetadataPath + "/version"); + auto id = mjson["id"].template get(); + + if (not version_order_map.count(id)) { + ver["id"] = id; + jsn["versions"].push_back(ver); + version_order_map[id] = sort_order; + + sort_order += 10; + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + // update playlist with versions + mail(shotgun_update_entity_atom_v, "Playlists", pl_id, utility::JsonStore(jsn)) + .request(shotgun_, infinite) + .then( + [=](const JsonStore &result) mutable { + // spdlog::warn("{}", JsonStore(result["data"]).dump(2)); + // update playorder.. + // get PlaylistVersionConnections + if (not append) { + scoped_actor sys{system()}; + + auto order_filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["playlist", "is", {"type":"Playlist", "id":0}] + ] + })"_json; + + order_filter["conditions"][0][2]["id"] = pl_id; + + try { + auto order = request_receive( + *sys, + shotgun_, + shotgun_entity_search_atom_v, + "PlaylistVersionConnection", + JsonStore(order_filter), + std::vector({"sg_sort_order", "version"}), + std::vector({"sg_sort_order"}), + 1, + 4999); + // update all PlaylistVersionConnection's with new sort_order. + for (const auto &i : order["data"]) { + auto version_id = i.at("relationships") + .at("version") + .at("data") + .at("id") + .get(); + auto sort_order = + i.at("attributes").at("sg_sort_order").is_null() + ? 0 + : i.at("attributes").at("sg_sort_order").get(); + // spdlog::warn("{} {}", std::to_string(sort_order), + // std::to_string(version_order_map[version_id])); + if (sort_order != version_order_map[version_id]) { + auto so_jsn = R"({"sg_sort_order": 0})"_json; + so_jsn["sg_sort_order"] = version_order_map[version_id]; + try { + request_receive( + *sys, + shotgun_, + shotgun_update_entity_atom_v, + "PlaylistVersionConnection", + i.at("id").get(), + utility::JsonStore(so_jsn), + std::vector({"id"})); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + if (pl_id != playlist_id) { + anon_mail( + json_store::set_json_atom_v, + JsonStore(result["data"]), + ShotgunMetadataPath + "/playlist") + .send(playlist); + + anon_mail( + json_store::set_json_atom_v, + JsonStore( + R"({"icon": "qrc:/shotbrowser_icons/shot_grid.svg", "tooltip": "ShotGrid Playlist"})"_json), + "/ui/decorators/shotgrid") + .send(playlist); + } + succeeded(playlist, notification_uuid); + + rp.deliver(result); + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + failed(playlist, notification_uuid); + rp.deliver(err); + }); + + // need to update/add PlaylistVersionConnection's + // on creation the sort_order will be null. + // PlaylistVersionConnection are auto created when adding to playlist. (I think) + // so all we need to do is update.. + + + // get shotgun metadata + // get media actors. + // get media shotgun metadata. + } catch (const std::exception &err) { + failed(playlist, notification_uuid); + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/ShotBrowserHelpers.js b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/ShotBrowserHelpers.js new file mode 100644 index 000000000..38042140b --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/ShotBrowserHelpers.js @@ -0,0 +1,981 @@ +function getProjectIdFromList(indexlist) { + let result = 0 + if(indexlist.length) { + let m = indexlist[0].model + result = m.get(indexlist[0], "projectIdRole") + } + return result +} + +function getIds(indexlist) { + let result = [] + if(indexlist.length) { + let m = indexlist[0].model + + for(let i=0; i depth ;i++) { + let ind = selectionModel.model.index(i, 0, index.parent) + if(selectionModel.model.model.get(selectionModel.model.mapToModel(ind), "typeRole") == "Shot") + selection.push(ind) + } + selectionModel.select(index, ItemSelectionModel.Deselect) + selectionModel.select(helpers.createItemSelection(selection), ItemSelectionModel.Toggle) + // selectionModel.select(index, ItemSelectionModel.Toggle) +} + +function shiftSelectItem(selectionModel, index) { + let sel = selectionModel.selectedIndexes + if(sel.length) { + if(selectionModel.selectedIndexes[0].parent != index.parent) + selectItem(selectionModel, index) + else { + + // find last selected entry ? + let m = sel[sel.length-1] + let s = Math.min(index.row, m.row) + let e = Math.max(index.row, m.row) + let items = [] + + for(let i=s; i<=e; i++) { + items.push(selectionModel.model.index(i, 0, index.parent)) + } + selectionModel.select(helpers.createItemSelection(items), ItemSelectionModel.ClearAndSelect) + } + } else { + selectItem(selectionModel, index) + } +} + +function updateMetadata(enabled, mediaUuid) { + if(enabled && ShotBrowserEngine.liveLinkKey != mediaUuid) { + ShotBrowserEngine.liveLinkKey = mediaUuid + + let mindex = theSessionData.searchRecursive(mediaUuid, "actorUuidRole", theSessionData.index(0, 0)) + if(mindex.valid) { + Future.promise( + mindex.model.getJSONFuture(mindex, "", true) + ).then(function(json_string) { + ShotBrowserEngine.liveLinkKey = mediaUuid + // inject current sources. + + let jsn = JSON.parse(json_string) + + try { + let image_actor_idx = mindex.model.searchRecursive(mindex.model.get(mindex, "imageActorUuidRole"), "actorUuidRole", mindex) + let audio_actor_idx = mindex.model.searchRecursive(mindex.model.get(mindex, "audioActorUuidRole"), "actorUuidRole", mindex) + + if(image_actor_idx.valid) { + jsn["metadata"]["image_source"] = [image_actor_idx.model.get(image_actor_idx,"nameRole")] + } else { + jsn["metadata"]["image_source"] = ["movie_dneg"] + } + if(audio_actor_idx.valid) { + jsn["metadata"]["audio_source"] = [audio_actor_idx.model.get(audio_actor_idx,"nameRole")] + } else { + jsn["metadata"]["audio_source"] = ["movie_dneg"] + } + }catch(err){} + ShotBrowserEngine.liveLinkMetadata = JSON.stringify(jsn) + }) + return true + } else { + ShotBrowserEngine.liveLinkMetadata = "" + } + } + return false; +} + +function transfer(destination, indexes) { + let project = null + let source = null + + let uuids = [] + + indexes = mapIndexesToResultModel(indexes) + + if(indexes.length) { + let model = indexes[0].model + let sources = new Map() + + for(let i=0; i < indexes.length; i++) { + try { + if(project == null) + project = model.get(indexes[i],"projectRole") + + let dnuuid = model.get(indexes[i],"stalkUuidRole") + let source = model.get(indexes[i],"locationRole") + + if(!sources.has(source)) + sources.set(source, []) + + sources.get(source).push(dnuuid) + } catch (err) { + console.log(err) + } + } + + if(project == null) + project = helpers.getEnv("SHOW") + + if(project && destination && sources.size) { + sources.forEach(function (value, key, map) { + var fa = ShotBrowserEngine.requestFileTransferFuture(value, project, key, destination) + + Future.promise(fa).then( + function(result) {} + ) + }); + } + } +} + +function transferMedia(destination, indexes) { + let project = null + + if(indexes.length) { + let model = indexes[0].model + + let sources = new Map() + + for(let i=0; i < indexes.length; i++) { + try { + if(project == null) + project = JSON.parse(theSessionData.getJSON(indexes[i], "/metadata/shotgun/version/relationships/project/data/name")) + + let dnuuid = JSON.parse(theSessionData.getJSON(indexes[i], "/metadata/shotgun/version/attributes/sg_ivy_dnuuid")) + let source = JSON.parse(theSessionData.getJSON(indexes[i], "/metadata/shotgun/version/attributes/sg_location")) + + if(!sources.has(source)) + sources.set(source, []) + + sources.get(source).push(helpers.QVariantFromUuidString(dnuuid)) + } catch (err) { + console.log(err) + // failed try uri + // they need to use the transfertool + + // let ms = theSessionData.searchRecursive(theSessionData.get(indexes[i], "imageActorUuidRole"), "actorUuidRole", indexes[i]) + + // if(ms.valid) { + // let v = theSessionData.get(ms, "pathRole") + // if(v != undefined) + // path_items.push(v) + // } + } + } + + if(project == null) + project = helpers.getEnv("SHOW") + + if(project && destination && sources.size) { + sources.forEach(function (value, key, map) { + var fa = ShotBrowserEngine.requestFileTransferFuture(value, project, key, destination) + + Future.promise(fa).then( + function(result) {} + ) + }); + } + } +} + +function syncPlaylistFromShotGrid(playlistUuid, matchOrder=false, callback=null) { + Future.promise( + ShotBrowserEngine.refreshPlaylistVersionsFuture(playlistUuid, matchOrder) + ).then( + function(json_string) { + if(callback) { + callback(json_string) + } + }, + function() { + // dialogHelpers.errorDialogFunc("SG Playlist Reloaded", "Failed") + } + ) +} + + +function syncPlaylistToShotGrid(playlistUuid, append=false, callback=null) { + Future.promise( + ShotBrowserEngine.updatePlaylistVersionsFuture(playlistUuid, append) + ).then(function(json_string) { + if(callback) + callback(json_string) + }) +} + +function publishPlaylistToShotGrid(playlistUuid, projectId, name, location, playlistType, callback=null ) { + Future.promise( + ShotBrowserEngine.createPlaylistFuture(playlistUuid, projectId, name, location, playlistType) + ).then( + function(json_string) { + if(callback) + callback(json_string) + }, + function() { + + } + ) +} + +function getValidMediaCount(playlistUuid, callback=console.log) { + Future.promise( + ShotBrowserEngine.getPlaylistLinkMediaFuture(playlistUuid) + ).then(function(json_string) { + Future.promise( + ShotBrowserEngine.getPlaylistValidMediaCountFuture(playlistUuid) + ).then(function(json_string) { + callback(json_string) + }) + }) +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/ShotBrowserMediaListMenu.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/ShotBrowserMediaListMenu.qml new file mode 100644 index 000000000..b8adcfb28 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/ShotBrowserMediaListMenu.qml @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + +import xstudio.qml.models 1.0 +import xstudio.qml.viewport 1.0 +import ShotBrowser 1.0 +import xStudio 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.clipboard 1.0 +import QuickFuture 1.0 + +Item { + + Clipboard { + id: clipboard + } + + // Note: For each instance of the ShotBrowser panel, we will have an + // instance of THIS item. As such, the 'menu_model_name' needs to be + // unique for each instance, so it has its own model data in the backend + // from which the actual menu instance (of which there will also be + // multiple instances) is built. See ShotBrowserPanel + + // Create a menu 'Some Menu' with an item in it that says 'Do Something' + + XsHotkey { + id: reload_playlist + sequence: "Alt+r" + name: "Reload Playlist" + description: "Reload Playlist From ShotGrid Ordered" + onActivated: ShotBrowserHelpers.syncPlaylistFromShotGrid( + helpers.QUuidFromUuidString(inspectedMediaSetProperties.values.actorUuidRole), true + ) + } + + XsMenuModelItem { + text: "Pipeline" + menuItemType: "divider" + menuPath: "" + menuItemPosition: 200 + menuModelName: "media_list_menu_" + } + + XsMenuModelItem { + text: "In ShotGrid..." + (enabled ? "" : " (Production Only)") + menuPath: "Reveal Source" + enabled: ShotBrowserEngine.shotGridUserType != "User" + menuItemPosition: 2 + menuModelName: "media_list_menu_" + onActivated: ShotBrowserHelpers.revealMediaInShotgrid(menuContext.mediaSelection) + } + XsMenuModelItem { + text: "In Ivy..." + menuPath: "Reveal Source" + menuItemPosition: 3 + menuModelName: "media_list_menu_" + onActivated: ShotBrowserHelpers.revealMediaInIvy(menuContext.mediaSelection) + } + + XsMenuModelItem { + text: "Publish Media Notes..." + (enabled ? "" : " (Production Only)") + enabled: ShotBrowserEngine.shotGridUserType != "User" + menuPath: "" + menuItemPosition: 210 + menuModelName: "media_list_menu_" + onActivated: { + ShotBrowserEngine.connected = true + publish_notes.show() + publish_notes.publishFromMedia(menuContext.mediaSelection) + } + } + + // XsMenuModelItem { + // text: "Download Missing SG Previews" + // menuPath: "" + // menuItemPosition: 26.1 + // menuModelName: "media_list_menu_" + // onActivated: ShotBrowserHelpers.downloadMissingMovies(menuContext.mediaSelection) + // } + + XsMenuModelItem { + text: "Download SG Movie" + menuPath: "" + menuItemPosition: 260 + menuModelName: "media_list_menu_" + onActivated: ShotBrowserHelpers.downloadMovies(menuContext.mediaSelection) + } + + XsMenuModelItem { + text: "To Here" + menuItemPosition: 1 + menuPath: "Transfer" + menuModelName: "media_list_menu_" + onActivated: ShotBrowserHelpers.transferMedia(helpers.getEnv("DNSITEDATA_SHORT_NAME"), menuContext.mediaSelection) + } + + XsMenuModelItem { + menuItemType: "divider" + menuItemPosition: 1.5 + menuPath: "Transfer" + menuModelName: "media_list_menu_" + } + + XsMenuModelItem { + text: "To Chennai" + menuItemPosition: 2 + menuPath: "Transfer" + menuModelName: "media_list_menu_" + onActivated: ShotBrowserHelpers.transferMedia("chn", menuContext.mediaSelection) + } + XsMenuModelItem { + text: "To Montreal" + menuItemPosition: 3 + menuPath: "Transfer" + menuModelName: "media_list_menu_" + onActivated: ShotBrowserHelpers.transferMedia("mtl", menuContext.mediaSelection) + } + XsMenuModelItem { + text: "To Mumbai" + menuItemPosition: 4 + menuPath: "Transfer" + menuModelName: "media_list_menu_" + onActivated: ShotBrowserHelpers.transferMedia("mum", menuContext.mediaSelection) + } + XsMenuModelItem { + text: "To London" + menuItemPosition: 4 + menuPath: "Transfer" + menuModelName: "media_list_menu_" + onActivated: ShotBrowserHelpers.transferMedia("lon", menuContext.mediaSelection) + } + XsMenuModelItem { + text: "To Sydney" + menuItemPosition: 5 + menuPath: "Transfer" + menuModelName: "media_list_menu_" + onActivated: ShotBrowserHelpers.transferMedia("syd", menuContext.mediaSelection) + } + XsMenuModelItem { + text: "To Vancouver" + menuItemPosition: 6 + menuPath: "Transfer" + menuModelName: "media_list_menu_" + onActivated: ShotBrowserHelpers.transferMedia("van", menuContext.mediaSelection) + } + + XsMenuModelItem { + menuItemType: "divider" + menuItemPosition: 7 + menuPath: "Transfer" + menuModelName: "media_list_menu_" + } + XsMenuModelItem { + text: "Open Transfer Tool" + menuItemPosition: 8 + menuPath: "Transfer" + menuModelName: "media_list_menu_" + onActivated: { + let uuids = [] + if(menuContext.mediaSelection.length) { + // get stalk uuids.. + let m = menuContext.mediaSelection[0].model + for(let i =0; i< menuContext.mediaSelection.length; i++) { + let meta = JSON.parse(theSessionData.getJSON(menuContext.mediaSelection[i], "/metadata/shotgun/version/attributes/sg_ivy_dnuuid")) + if(meta) + uuids.push(meta) + } + } + + helpers.startDetachedProcess("dnenv-do", [helpers.getEnv("SHOW"), "--", "maketransfer"].concat(uuids)) + } + + Component.onCompleted: { + // we need this so the menu model knows where to insert the + // "Transfer" sub menu in the top level menu + setMenuPathPosition("Transfer", 220) + } + } + + XsMenuModelItem { + text: "Create SG Playlist..." + (enabled ? "" : " (Production Only)") + enabled: ShotBrowserEngine.shotGridUserType != "User" + menuPath: "Pipeline|Playlists" + menuItemPosition: 1 + menuModelName: "main menu bar" + onActivated: { + ShotBrowserEngine.connected = true + publish_to_dialog.show() + publish_to_dialog.playlistProperties = inspectedMediaSetProperties + } + Component.onCompleted: { + helpers.setMenuPathPosition("Pipeline", "main menu bar", 3.0) + } + + } + + XsMenuModelItem { + text: "Reload SG Playlist" + menuPath: "Pipeline|Playlists" + menuItemPosition: 2 + menuModelName: "main menu bar" + onActivated: ShotBrowserHelpers.syncPlaylistFromShotGrid( + helpers.QUuidFromUuidString(inspectedMediaSetProperties.values.actorUuidRole) + ) + } + + XsMenuModelItem { + text: "Reload SG Playlist (Ordered)" + // enabled: false + menuPath: "Pipeline|Playlists" + menuItemPosition: 2.5 + menuModelName: "main menu bar" + onActivated: ShotBrowserHelpers.syncPlaylistFromShotGrid( + helpers.QUuidFromUuidString(inspectedMediaSetProperties.values.actorUuidRole), + true + ) + } + + XsMenuModelItem { + text: "Push Media To SG Playlist" + (enabled ? "" : " (Production Only)") + enabled: ShotBrowserEngine.shotGridUserType != "User" + menuPath: "Pipeline|Playlists" + menuItemPosition: 3 + menuModelName: "main menu bar" + onActivated: { + ShotBrowserEngine.connected = true + sync_to_dialog.show() + sync_to_dialog.playlistProperties = inspectedMediaSetProperties + } + } + + XsMenuModelItem { + text: "Publish Playlist Notes" + (enabled ? "" : " (Production Only)") + enabled: ShotBrowserEngine.shotGridUserType != "User" + menuPath: "Pipeline|Notes" + menuItemPosition: 1 + menuModelName: "main menu bar" + onActivated: { + ShotBrowserEngine.connected = true + publish_notes.show() + publish_notes.publishFromPlaylist(helpers.QVariantFromUuidString(inspectedMediaSetProperties.values.actorUuidRole)) + } + } + + XsMenuModelItem { + text: "Publish Selected Media Notes" + (enabled ? "" : " (Production Only)") + enabled: ShotBrowserEngine.shotGridUserType != "User" + menuPath: "Pipeline|Notes" + menuItemPosition: 2 + menuModelName: "main menu bar" + onActivated: { + ShotBrowserEngine.connected = true + publish_notes.show() + publish_notes.publishFromMedia(mediaSelectionModel.selectedIndexes) + } + } + + + XsMenuModelItem { + menuItemType: "divider" + text: "Pipeline" + menuPath: "" + menuItemPosition: 10 + menuModelName: "playlist_context_menu" + } + + + XsMenuModelItem { + text: "Create SG Playlist..." + (enabled ? "" : " (Production Only)") + enabled: ShotBrowserEngine.shotGridUserType != "User" + menuPath: "Playlists" + menuItemPosition: 1 + menuModelName: "playlist_context_menu" + onActivated: { + ShotBrowserEngine.connected = true + publish_to_dialog.show() + publish_to_dialog.playlistProperties = inspectedMediaSetProperties + } + Component.onCompleted: { + setMenuPathPosition("Playlists", 10.1) + } + } + + XsMenuModelItem { + text: "Reload SG Playlist" + menuPath: "Playlists" + menuItemPosition: 2 + menuModelName: "playlist_context_menu" + onActivated: ShotBrowserHelpers.syncPlaylistFromShotGrid( + helpers.QUuidFromUuidString(inspectedMediaSetProperties.values.actorUuidRole) + ) + } + + XsMenuModelItem { + text: "Reload SG Playlist (Ordered)" + // enabled: false + menuPath: "Playlists" + menuItemPosition: 2.5 + menuModelName: "playlist_context_menu" + onActivated: ShotBrowserHelpers.syncPlaylistFromShotGrid( + helpers.QUuidFromUuidString(inspectedMediaSetProperties.values.actorUuidRole), + true + ) + hotkeyUuid: reload_playlist.uuid + } + + + XsMenuModelItem { + text: "Push Media To SG Playlist" + (enabled ? "" : " (Production Only)") + enabled: ShotBrowserEngine.shotGridUserType != "User" + menuPath: "Playlists" + menuItemPosition: 3 + menuModelName: "playlist_context_menu" + onActivated: { + ShotBrowserEngine.connected = true + sync_to_dialog.show() + sync_to_dialog.playlistProperties = inspectedMediaSetProperties + } + } + + XsMenuModelItem { + text: "Reveal In ShotGrid..." + (enabled ? "" : " (Production Only)") + enabled: ShotBrowserEngine.shotGridUserType != "User" + menuPath: "Playlists" + menuItemPosition: 4 + menuModelName: "playlist_context_menu" + onActivated: { + ShotBrowserEngine.connected = true + ShotBrowserHelpers.revealPlaylistInShotgrid(sessionSelectionModel.selectedIndexes) + } + } + + XsMenuModelItem { + text: "SG Playlist Link " + menuPath: "Copy To Clipboard" + menuItemPosition: 4 + menuModelName: "playlist_context_menu" + onActivated: { + ShotBrowserEngine.connected = true + clipboard.text = ShotBrowserHelpers.resolvePlaylistLink(sessionSelectionModel.selectedIndexes).join("\n") + } + } + + XsMenuModelItem { + text: "Publish Playlist Notes" + (enabled ? "" : " (Production Only)") + enabled: ShotBrowserEngine.shotGridUserType != "User" + menuPath: "Notes" + menuItemPosition: 1 + menuModelName: "playlist_context_menu" + onActivated: { + ShotBrowserEngine.connected = true + publish_notes.show() + publish_notes.publishFromPlaylist(helpers.QVariantFromUuidString(inspectedMediaSetProperties.values.actorUuidRole)) + } + Component.onCompleted: { + setMenuPathPosition("Notes", 10.2) + } + } + + XsSBPublishNotesDialog { + id: publish_notes + property real btnHeight: XsStyleSheet.widgetStdHeight + 4 + } + + XsSBSyncPlaylistToShotGridDialog { + id: sync_to_dialog + width: 350 + height: 150 + } + + XsSBPublishPlaylistToShotGridDialog { + id: publish_to_dialog + width: 500 + height: 350 + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/dialogs/XsSBPublishNotesDialog.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/dialogs/XsSBPublishNotesDialog.qml new file mode 100644 index 000000000..67017dab3 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/dialogs/XsSBPublishNotesDialog.qml @@ -0,0 +1,658 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick + +import QtQuick.Layouts + + + +import QuickFuture 1.0 + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 + +XsWindow{ + + title: "Publish "+notesType+" Notes" + property bool isPlaylistNotes: true + property string notesType: isPlaylistNotes? "Playlist": "Selected Media" + property string message: "No notes to publish." + + property real itemHeight: btnHeight + property real itemSpacing: 1 + + property int projectId: -1 + + property bool invalidPublish: false + + property alias notifyOwner: notifyCreatorCB.checked + property alias combineNotes: combineNotesCB.checked + property alias addFrameTimeCode: addFrameTimeCodeCB.checked + property alias addPlaylistName: addPlaylistNameCB.checked + property alias addNoteType: addNoteTypeCB.checked + property alias ignoreWithOnlyDrawing: ignoreWithOnlyDrawingCB.checked + property alias skipAlreadyPublished: skipAlreadyPublishedCB.checked + property string defaultType: typeRenameDiv.checked ? prefs.value.defaultType : "" + property var playlistUuid: null + property var mediaUuids: [] + + property var payload: null + property var payload_obj: null + + property int notesCount: (payload_obj ? payload_obj["payload"].length : 0) + + + onNotesCountChanged:{ + if(notesCount===1) message = "Ready to publish " +notesCount+" note." + else if(notesCount>1) message = "Ready to publish " +notesCount+" notes." + else message = "No notes to publish." + projectId + } + + onPayloadChanged: { + try { + payload_obj = JSON.parse(payload) + + // find project id.. + if(payload_obj && payload_obj["payload"].length) { + let id = payload_obj["payload"][0]["payload"]["project"]["id"] + projectDiv.currentIndex = projectDiv.model.search(id, "idRole").row + } + invalidPublish = false + } catch(err) { + if(isPlaylistNotes) { + message = "Invalid Playlist, must have ShotGrid Metadata." + } + invalidPublish = true + console.log(err) + } + } + + onPlaylistUuidChanged: { + // update playlist combo, if required. + for(let i = 0; i< theSessionData.playlists.length; i++) { + let ustr = helpers.QUuidToQString(playlistUuid) + if(theSessionData.playlists[i].uuid == ustr) { + if(playlistDiv.currentIndex != i) + playlistDiv.currentIndex = i + break + } + } + + updatePublish() + } + + + width: 400 + height: 660 + minimumWidth: 400 + // maximumWidth: width + minimumHeight: height + maximumHeight: height + // palette.base: XsStyleSheet.panelTitleBarColor + + XsSBPublishNotesFeedback { + id: publish_notes_feedback + property real btnHeight: XsStyleSheet.widgetStdHeight + 4 + } + + + Connections { + target: ShotBrowserEngine + function onReadyChanged() { + updatePublish() + prefs.updateWidgets() + } + } + + function updatePublish() { + if(visible) { + // console.log("playlistUuid", playlistUuid) + // console.log("notifyOwner", notifyOwner) + // console.log("combineNotes", combineNotes) + // console.log("addFrameTimeCode", addFrameTimeCode) + // console.log("addPlaylistName", addPlaylistName) + // console.log("addNoteType", addNoteType) + // console.log("ignoreWithOnlyDrawing", ignoreWithOnlyDrawing) + // console.log("skipAlreadyPublished", skipAlreadyPublished) + // console.log("defaultType", defaultType) + + if(playlistUuid) { + payload = ShotBrowserEngine.preparePlaylistNotes( + playlistUuid, + mediaUuids, + notifyOwner, + getNotifyGroups(), + combineNotes, + addFrameTimeCode, + addPlaylistName, + addNoteType, + ignoreWithOnlyDrawing, + skipAlreadyPublished, + defaultType + ) + } + } + } + + function publishNotes() { + + if(playlistUuid) { + payload = ShotBrowserEngine.preparePlaylistNotes( + playlistUuid, + mediaUuids, + notifyOwner, + getNotifyGroups(), + combineNotes, + addFrameTimeCode, + addPlaylistName, + addNoteType, + ignoreWithOnlyDrawing, + skipAlreadyPublished, + defaultType + ) + + Future.promise( + + ShotBrowserEngine.pushPlaylistNotesFuture(payload, playlistUuid) + + ).then(function(json_string) { + + try { + var json_obj = JSON.parse(json_string) + if (json_obj.hasOwnProperty("error")) { + message = JSON.stringify(json_obj.error) + if (json_obj.error.hasOwnProperty("message") && json_obj.error.hasOwnProperty("source")) { + message = "" + json_obj.error.source + " : " + json_obj.error.message + } + + message = "Publish notes has failed with the following error...\n\n"+message+"\n\nPlease retry first. Contact xSTUDIO support team with details of the error if it persists." + dialogHelpers.errorDialogFunc("Publish Shotgrid Notes Failed", message) + return + } + } catch (err) { + // unable to parse as json + } + + publish_notes_feedback.isPlaylistNotes = isPlaylistNotes + publish_notes_feedback.parseFeedback(json_string) + publish_notes_feedback.show() + }) + } + } + + + function getNotifyGroups() { + let result = [] + let email_group_names = [] + if(notifyGroupCB.checked) { + for(let i =0;i { + const idsSet = new Set(ids); + const newObj = {}; + for (const [key, val] of Object.entries(data)) { + if (!idsSet.has(key)) { + newObj[key] = val; + } + } + return newObj; + }; + + let tmp = JSON.parse(JSON.stringify(value)) + if(key != "") + tmp[key] = new_value + + if(!("settings" in tmp)) + tmp["settings"] = {} + + if(!("Default Profile" in tmp["settings"])) + tmp["settings"]["Default Profile"] = omit(tmp, ["settings","__ignore__"]) + + tmp["settings"][currentSetting] = omit(tmp, ["settings","__ignore__"]) + value = tmp + } + + function addSetting(name) { + currentSetting = name + storePreference() + } + + function createDefaultProfile() { + const omit = (data , ids) => { + const idsSet = new Set(ids); + const newObj = {}; + for (const [key, val] of Object.entries(data)) { + if (!idsSet.has(key)) { + newObj[key] = val; + } + } + return newObj; + }; + + let tmp = JSON.parse(JSON.stringify(value)) + let changed = false + if(!("settings" in tmp)) { + tmp["settings"] = {} + changed = true + } + + if(!("Default Profile" in tmp["settings"])) { + tmp["settings"]["Default Profile"] = omit(tmp, ["settings","__ignore__"]) + changed = true + } + + if(changed) + value = tmp + } + + function changeCurrentSetting(name) { + currentSetting = name + let tmp = JSON.parse(JSON.stringify(value)) + + for (const [key, val] of Object.entries(tmp["settings"][name])) { + tmp[key] = val + } + + value = tmp + updateWidgets() + } + + function removeSetting(name) { + changeCurrentSetting("Default Profile") + + let tmp = JSON.parse(JSON.stringify(value)) + delete tmp["settings"][name] + value = tmp + + storePreference() + } + + function updateWidgets() { + createDefaultProfile() + + typeRenameDiv.checked = value.defaultType != "" + typeRenameDiv.currentIndex = typeRenameDiv.valueDiv.find(value.defaultType) + + // select indexes.. + let hasGroups = (value.notifyGroups !== undefined && value.notifyGroups.length ? true : false) + if(hasGroups) + notifyGroupCB.valueDiv.selectFromNames(value.notifyGroups) + + notifyGroupCB.checked = hasGroups + } + } + + ColumnLayout { + anchors.fill: parent + spacing: itemSpacing + anchors.leftMargin: 20 + anchors.rightMargin: 20 + + XsTextWithComboBoxFullSize{ id: projectDiv + Layout.fillWidth: true + Layout.preferredHeight: itemHeight*1.5 + Layout.topMargin: itemHeight/2 + + enabled: false + + text: "Select project :" + model: ShotBrowserEngine.ready ? ShotBrowserEngine.presetsModel.termModel("Project") : [] + valueDiv.textRole: "nameRole" + onCurrentIndexChanged: { + if(currentIndex != -1) { + let pid = model.get(model.index(currentIndex,0), "idRole") + ShotBrowserEngine.cacheProject(pid) + projectId = pid + } + } + } + + XsTextWithComboBoxFullSize{ id: playlistDiv + Layout.fillWidth: true + Layout.preferredHeight: itemHeight*1.5 + Layout.topMargin: itemHeight/4 + + enabled: isPlaylistNotes + + text: "Select XSTUDIO playlist :" + valueDiv.textRole: "text" + model: theSessionData.playlists + onCurrentIndexChanged: { + if(currentIndex != -1) { + if(model[currentIndex].uuid != playlistUuid) + playlistUuid = model[currentIndex].uuid + } + else + playlistUuid = "" + } + } + + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: itemHeight*1.5 + Layout.topMargin: itemHeight/2 + Layout.bottomMargin: itemHeight/2 + spacing: 1 + + XsTextWithComboBoxFullSize{ id: settingsDiv + Layout.fillWidth: true + Layout.preferredHeight: itemHeight*1.5 + + valueDiv.textColorNormal: model[currentIndex] == "Default Profile" ? XsStyleSheet.secondaryTextColor : palette.text + + text: "Profiles :" + model: prefs.settings + onActivated: prefs.changeCurrentSetting(model[currentIndex]) + } + + XsPrimaryButton { + Layout.topMargin: itemHeight-2 + Layout.bottomMargin: 2 + Layout.preferredWidth: itemHeight + Layout.preferredHeight: itemHeight + imgSrc: "qrc:/icons/add.svg" + + function addSettingCallback(name, button) { + if(button == "Add Profile") { + prefs.addSetting(name) + settingsDiv.currentIndex = settingsDiv.model.length-1 + } + } + + onClicked: dialogHelpers.textInputDialog( + addSettingCallback, + "Add Profile", + "Enter a name for your profile.", + "", + ["Cancel", "Add Profile"]) + + } + + XsPrimaryButton { + Layout.topMargin: itemHeight-2 + Layout.bottomMargin: 2 + Layout.preferredWidth: itemHeight + Layout.preferredHeight: itemHeight + + imgSrc: "qrc:/icons/delete.svg" + enabled: prefs.currentSetting != "Default Profile" + onClicked: prefs.removeSetting(prefs.currentSetting) + } + } + + + XsTextWithCheckAndComboBoxes { id: typeRenameDiv + Layout.fillWidth: true + Layout.preferredHeight: itemHeight + Layout.topMargin: itemHeight + + + valueDiv.textRole: "nameRole" + text: "Rename all note types :" + + checked: false + currentIndex: -1 + model: ShotBrowserEngine.ready ? ShotBrowserEngine.presetsModel.termModel("Note Type") : [] + + onCurrentIndexChanged: { + if(!checked && currentIndex != -1) + currentIndex = -1 + } + + onActivated: { + if(index != -1) { + let dt = model.get(model.index(index, 0), "nameRole") + if(dt != prefs.value.defaultType) + prefs.storePreference("defaultType", dt) + } + } + + onCheckedChanged: { + if(!checked) { + prefs.storePreference("defaultType", "") + currentIndex = -1 + } + } + } + + XsTextWithCheckBox { id: notifyCreatorCB + Layout.fillWidth: true + Layout.preferredHeight: itemHeight + Layout.topMargin: itemHeight + + + text: "Notify version creator" + checked: prefs.value.notifyCreator + onCheckedChanged: { + if(prefs.value.notifyCreator != checked) { + updatePublish() + prefs.storePreference("notifyCreator", checked) + } + } + } + XsTextWithComboBoxMultiSelectable{ id: notifyGroupCB + Layout.fillWidth: true + Layout.preferredHeight: itemHeight + + z: 100 + text: "Notify :" + hintText: "Recipients" + checked: false + model: ShotBrowserFilterModel { + sourceModel: ShotBrowserEngine.ready ? ShotBrowserEngine.presetsModel.termModel("Group", "", projectId) : null + } + + onCheckedChanged: { + if(!checked) { + valueDiv.theSelectionModel.clearSelection() + } + } + + onCheckedIndexesChanged: { + let values = [] + for(let i =0;i \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/arrow_right.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/arrow_right.svg new file mode 100644 index 000000000..4bf73bb6d --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/arrow_right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/attach_file.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/attach_file.svg new file mode 100644 index 000000000..12cb1cb7f --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/attach_file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/bookmark_heart.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/bookmark_heart.svg new file mode 100644 index 000000000..35f782f55 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/bookmark_heart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/calendar_month.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/calendar_month.svg new file mode 100644 index 000000000..f0eb348d9 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/calendar_month.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/cloud.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/cloud.svg new file mode 100644 index 000000000..a36bddda9 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/cloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/drag_indicator.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/drag_indicator.svg new file mode 100644 index 000000000..e5221f8d5 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/drag_indicator.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/edit.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/edit.svg new file mode 100644 index 000000000..cb81b1130 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/equal.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/equal.svg new file mode 100644 index 000000000..a17ff4174 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/equal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/exclamation.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/exclamation.svg new file mode 100644 index 000000000..e43eebe05 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/exclamation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/favorite.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/favorite.svg new file mode 100644 index 000000000..a14e2c712 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/favorite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/globe.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/globe.svg new file mode 100644 index 000000000..8b82058dd --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/heart_plus.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/heart_plus.svg new file mode 100644 index 000000000..4ae0ecf09 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/heart_plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/key.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/key.svg new file mode 100644 index 000000000..8d4104d59 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/keyboard_return.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/keyboard_return.svg new file mode 100644 index 000000000..5c62f63ab --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/keyboard_return.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/left_panel_open.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/left_panel_open.svg new file mode 100644 index 000000000..99f82f08b --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/left_panel_open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/lock.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/lock.svg new file mode 100644 index 000000000..20b9e3984 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/lock_open.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/lock_open.svg new file mode 100644 index 000000000..824c70b7c --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/lock_open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/manage_accounts.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/manage_accounts.svg new file mode 100644 index 000000000..6cdc1eafa --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/manage_accounts.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/nature.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/nature.svg new file mode 100644 index 000000000..2d56c7b7e --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/nature.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/note_history.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/note_history.svg new file mode 100644 index 000000000..96238aae3 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/note_history.svg @@ -0,0 +1,70 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/park.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/park.svg new file mode 100644 index 000000000..24df1b0f0 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/park.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/right_panel_open.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/right_panel_open.svg new file mode 100644 index 000000000..498104e1e --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/right_panel_open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/schedule.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/schedule.svg new file mode 100644 index 000000000..19fb505d6 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/schedule.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/segment.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/segment.svg new file mode 100644 index 000000000..3cefe85f3 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/segment.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/settings.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/settings.svg new file mode 100644 index 000000000..328e2f081 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/shot_grid.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/shot_grid.svg new file mode 100644 index 000000000..744521bcb --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/shot_grid.svg @@ -0,0 +1,63 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/shot_history.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/shot_history.svg new file mode 100644 index 000000000..d8ea3a9e1 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/shot_history.svg @@ -0,0 +1,70 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/shotgun.png b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/shotgun.png new file mode 100644 index 000000000..c38d434c8 Binary files /dev/null and b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/shotgun.png differ diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/tree_plus.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/tree_plus.svg new file mode 100644 index 000000000..2b8042c4c --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/tree_plus.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/unfold_more.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/unfold_more.svg new file mode 100644 index 000000000..3e245d209 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/unfold_more.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/up_arrow.svg b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/up_arrow.svg new file mode 100644 index 000000000..b7a697339 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/icons/up_arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistory.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistory.qml new file mode 100644 index 000000000..20d2f8781 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistory.qml @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick + +import QtQuick.Layouts + + + + +import QuickFuture 1.0 +import QuickPromise 1.0 + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 +import ShotBrowser 1.0 + +Item{ + id: panel + anchors.fill: parent + + property bool isPanelEnabled: true + property var dataModel: results + + property color panelColor: XsStyleSheet.panelBgColor + + property var activeScopeIndex: helpers.qModelIndex() + property var activeTypeIndex: helpers.qModelIndex() + + // Track the uuid of the media that is currently visible in the Viewport + property var onScreenMediaUuid: currentPlayhead.mediaUuid + property var onScreenLogicalFrame: currentPlayhead.logicalFrame + + readonly property string panelType: "NotesHistory" + + property bool isPopout: typeof panels_layout_model_index == "undefined" + + property int queryCounter: 0 + property int queryRunning: 0 + + property bool isPaused: false + + onOnScreenMediaUuidChanged: { + if(visible) + updateTimer.start() + } + + property real panelPadding: XsStyleSheet.panelPadding + + XsPreference { + id: addAfterSelection + path: "/plugin/data_source/shotbrowser/add_after_selection" + } + + XsPreference { + id: pauseOnPlaying + path: "/plugin/data_source/shotbrowser/pause_update_on_playing" + } + + onOnScreenLogicalFrameChanged: { + if(visible) { + if(currentPlayhead.playing && !pauseOnPlaying.value) { + // no op + } else if(updateTimer.running) { + updateTimer.restart() + if(isPanelEnabled && !isPaused) { + isPaused = true + } + } + } + } + + Timer { + id: updateTimer + interval: 500 + running: false + repeat: false + onTriggered: { + isPaused = false + ShotBrowserHelpers.updateMetadata(isPanelEnabled, onScreenMediaUuid) + } + } + + ShotBrowserResultModel { + id: resultsBaseModel + } + + ShotBrowserResultFilterModel { + id: results + sourceModel: resultsBaseModel + } + + + // ShotBrowserResultModel { + // id: results + // } + + ItemSelectionModel { + id: resultsSelectionModel + model: results + } + + Connections { + target: ShotBrowserEngine + function onReadyChanged() { + setIndexesFromPreferences() + runQuery() + } + } + + function setIndexesFromPreferences() { + if(ShotBrowserEngine.ready && !activeScopeIndex.valid && prefs.scope) { + // from panel. + let i = getScopeIndex(prefs.scope) + if(i.valid && activeScopeIndex != i) + activeScopeIndex = i + } + if(ShotBrowserEngine.ready && !activeTypeIndex.valid && prefs.type) { + // from panel. + let i = getTypeIndex(prefs.type) + if(i.valid && activeTypeIndex != i) + activeTypeIndex = i + } + } + + // get presets node under group + function getScopeIndex(scope_name) { + for (const ind of ShotBrowserEngine.presetsModel.noteHistoryScope) { + if(ShotBrowserEngine.presetsModel.get(ind, "nameRole") == scope_name){ + return ind + } + } + return helpers.qModelIndex() + } + + function getTypeIndex(type_name) { + for (const ind of ShotBrowserEngine.presetsModel.noteHistoryType) { + if(ShotBrowserEngine.presetsModel.get(ind, "nameRole") == type_name){ + return ind + } + } + return helpers.qModelIndex() + } + + Item { + // Hold properties that we want to persist between sessions. + id: prefs + property string type: "" + property string scope: "" + property bool initialised: false + + XsStoredPanelProperties { + propertyNames: ["type", "scope"] + onPropertiesInitialised: { + prefs.initialised = true + setIndexesFromPreferences() + runQuery() + } + } + + } + + onActiveScopeIndexChanged: { + if(activeScopeIndex && activeScopeIndex.valid) { + let i = activeScopeIndex.model.get(activeScopeIndex, "nameRole") + prefs.scope = i + } + } + + onActiveTypeIndexChanged: { + if(activeTypeIndex && activeTypeIndex.valid) { + let i = activeTypeIndex.model.get(activeTypeIndex, "nameRole") + prefs.type = i + } + } + + + + Connections { + target: ShotBrowserEngine + function onLiveLinkMetadataChanged() { + runQuery() + } + } + + onIsPanelEnabledChanged: { + ShotBrowserHelpers.updateMetadata(isPanelEnabled, onScreenMediaUuid) + runQuery() + } + + function runQuery() { + if(panel.visible && ShotBrowserEngine.ready && isPanelEnabled && !isPaused && activeScopeIndex.valid && activeTypeIndex.valid) { + + if(onScreenMediaUuid == "{00000000-0000-0000-0000-000000000000}") { + resultsBaseModel.setResultDataJSON([]) + } else { + // make sure the results appear in sync. + queryCounter += 1 + queryRunning += 1 + let i = queryCounter + + let customContext = {} + customContext["preset_name"] = ShotBrowserEngine.presetsModel.get( + activeScopeIndex, + "nameRole" + ) + " - " + ShotBrowserEngine.presetsModel.get( + activeTypeIndex, + "nameRole" + ) + + Future.promise( + ShotBrowserEngine.executeQueryJSON( + [ + ShotBrowserEngine.presetsModel.get( + activeScopeIndex, + "jsonPathRole" + ), + ShotBrowserEngine.presetsModel.get( + activeTypeIndex, + "jsonPathRole" + ) + ], + {},[],customContext + ) + ).then(function(json_string) { + if(queryCounter == i) { + resultsSelectionModel.clear() + resultsBaseModel.setResultDataJSON([json_string]) + } + queryRunning -= 1 + + }, + function() { + resultsSelectionModel.clear() + resultsBaseModel.setResultDataJSON([]) + queryRunning -= 1 + }) + } + } + } + + function activateScope(clickedIndex){ + if(clickedIndex.valid) { + activeScopeIndex = clickedIndex + runQuery() + } + } + + function activateType(clickedIndex){ + if(clickedIndex.valid) { + activeTypeIndex = clickedIndex + runQuery() + } + } + + Component.onCompleted: { + if(visible) { + ShotBrowserEngine.connected = true + setIndexesFromPreferences() + ShotBrowserHelpers.updateMetadata(isPanelEnabled, onScreenMediaUuid) + isPaused = false + runQuery() + } + } + + onVisibleChanged: { + if(visible) { + ShotBrowserEngine.connected = true + setIndexesFromPreferences() + ShotBrowserHelpers.updateMetadata(isPanelEnabled, onScreenMediaUuid) + isPaused = false + runQuery() + } + } + + XsGradientRectangle{ id: backgroundDiv + anchors.fill: parent + } + + NotesHistoryResultPopup { + id: resultPopup + menu_model_name: "note_history_popup"+resultPopup + popupSelectionModel: resultsSelectionModel + } + + + ColumnLayout{ + anchors.fill: parent + anchors.margins: 4 + spacing: 4 + + NotesHistoryTitleDiv{ + titleButtonHeight: (XsStyleSheet.widgetStdHeight + 4) + Layout.fillWidth: true + Layout.minimumHeight: (titleButtonHeight * 2) + 1 + Layout.maximumHeight: (titleButtonHeight * 2) + 1 + } + + Rectangle{ + color: panelColor + Layout.fillWidth: true + Layout.fillHeight: true + + NotesHistoryListDiv{ + anchors.fill: parent + anchors.margins: 4 + } + } + + NotesHistoryActionDiv{ + Layout.fillWidth: true + Layout.maximumHeight: XsStyleSheet.widgetStdHeight + // nasty hack because QML crashes, if I try and do it properly. + parentWidth: parent.width - 4 + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryActionDiv.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryActionDiv.qml new file mode 100644 index 000000000..ccf8a3ccc --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryActionDiv.qml @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick + +import QtQuick.Layouts + +import xStudio 1.0 + + +import ShotBrowser 1.0 +import QuickFuture 1.0 +import QuickPromise 1.0 + +import xstudio.qml.helpers 1.0 + + +RowLayout{ + spacing: 2 + property int parentWidth: width + readonly property int cellWidth: parentWidth / children.length + + XsPrimaryButton{ + text: "Add" + clip:true + Layout.fillHeight: true + Layout.preferredWidth: cellWidth + onClicked: ShotBrowserHelpers.addToCurrent(resultsSelectionModel.selectedIndexes, true, addAfterSelection.value) + } + + XsPrimaryButton{ + text: "Replace" + Layout.fillHeight: true + Layout.preferredWidth: cellWidth + onClicked: ShotBrowserHelpers.replaceSelectedResults(resultsSelectionModel.selectedIndexes) + } + + XsPrimaryButton{ + text: "Compare" + Layout.fillHeight: true + Layout.preferredWidth: cellWidth + onClicked: ShotBrowserHelpers.compareSelectedResults(resultsSelectionModel.selectedIndexes) + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryListDelegate.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryListDelegate.qml new file mode 100644 index 000000000..f07c31b99 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryListDelegate.qml @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + + +import QtQuick.Layouts + +import QuickFuture 1.0 + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 + +Item{ id: thisItem + + property bool isActive: false + property bool isSelected: resultsSelectionModel.isSelected(delegateModel.modelIndex(index)) + + property real borderWidth: 1 + + // property real textSize: XsStyleSheet.fontSize + + // property color hintColor: XsStyleSheet.hintColor + // property color highlightColor: palette.highlight + // property color bgColorNormal: XsStyleSheet.widgetBgNormalColor + + property var delegateModel: null + property var popupMenu: null + + property bool isHovered: mArea.containsMouse || + sec1.isHovered || + sec2.isHovered + + required property string thumbRole + required property string noteTypeRole + required property string createdByRole + + required property string subjectRole + required property string contentRole + required property string versionNameRole + required property string artistRole + required property string projectRole + required property int idRole + required property var attachmentsRole + required property var addressingRole + required property var createdDateRole + required property int index + + signal showImages(items: var) + + Connections { + target: resultsSelectionModel + function onSelectionChanged(selected, deselected) { + isSelected = resultsSelectionModel.isSelected(delegateModel.modelIndex(index)) + } + } + + MouseArea{ id: mArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + propagateComposedEvents: false + + onReleased: { + if(!propagateComposedEvents) + propagateComposedEvents = true + } + + onPressed: (mouse)=>{ + // required for doubleclick to work + mouse.accepted = true + + if (mouse.button == Qt.RightButton){ + if(popupMenu.visible) popupMenu.visible = false + else{ + if(!isSelected) { + if(mouse.modifiers == Qt.NoModifier) { + ShotBrowserHelpers.selectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } else if(mouse.modifiers == Qt.ShiftModifier){ + ShotBrowserHelpers.shiftSelectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } else if(mouse.modifiers == Qt.ControlModifier) { + ShotBrowserHelpers.ctrlSelectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } + } + popupMenu.popupDelegateModel = delegateModel + let ppos = mapToItem(popupMenu.parent, mouseX, mouseY) + popupMenu.x = ppos.x + popupMenu.y = ppos.y + popupMenu.visible = true + } + } else if(mouse.modifiers == Qt.NoModifier) { + ShotBrowserHelpers.selectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } else if(mouse.modifiers == Qt.ShiftModifier){ + ShotBrowserHelpers.shiftSelectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } else if(mouse.modifiers == Qt.ControlModifier) { + ShotBrowserHelpers.ctrlSelectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } + } + + onDoubleClicked: (mouse)=> { + // need to know context, Which panel am I in. + ShotBrowserHelpers.addToCurrent([delegateModel.modelIndex(index)], panelType != "ShotBrowser", addAfterSelection.value) + } + + Rectangle{ + anchors.fill: parent + + color: isSelected? Qt.darker(palette.highlight, 5) : "transparent" + border.color: isHovered? palette.highlight : XsStyleSheet.widgetBgNormalColor + border.width: borderWidth + + // wierd workaround for flickable.. + + RowLayout{ + anchors.fill: parent + anchors.margins: borderWidth + spacing: 1 + + NotesHistorySection1{ id: sec1 + Layout.preferredWidth: 160 + Layout.fillHeight: true + } + NotesHistorySection2{ id: sec2 + Layout.fillWidth: true + Layout.minimumWidth: parent.width/2 + Layout.fillHeight: true + } + } + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryListDiv.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryListDiv.qml new file mode 100644 index 000000000..11ee58599 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryListDiv.qml @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic + + +import xStudio 1.0 +import ShotBrowser 1.0 + +XsListView{ + id: list + anchors.fill: parent + spacing: panelPadding + + property int rightSpacing: list.height < list.contentHeight ? 16 : 0 + Behavior on rightSpacing {NumberAnimation {duration: 150}} + + ScrollBar.vertical: XsScrollBar { + visible: list.height < list.contentHeight + parent: list.parent + anchors.top: list.top + anchors.right: list.right + anchors.bottom: list.bottom + x: -5 + } + + + XsSBImageViewer { + id: imagePlayer + anchors.fill: parent + visible: false + onEject: visible = false + } + + function viewImages(images) { + imagePlayer.images = images + imagePlayer.visible = true + } + + XsLabel { + text: "Select the 'Scope' and the 'Note Type' to view the Note History." + color: XsStyleSheet.hintColor + visible: !activeScopeIndex.valid || !activeTypeIndex.valid + + anchors.fill: parent + + font.pixelSize: XsStyleSheet.fontSize*1.2 + font.weight: Font.Medium + } + + XsLabel { + text: !queryRunning ? "No Results Found" : "" + // text: isPaused ? "Updates Paused" : !queryRunning ? "No Results Found" : "" + color: XsStyleSheet.hintColor + visible: dataModel && activeScopeIndex.valid && activeTypeIndex.valid && !dataModel.count //#TODO + + anchors.fill: parent + + font.pixelSize: XsStyleSheet.fontSize*1.2 + font.weight: Font.Medium + } + + model: DelegateModel { + id: chooserModel + model: dataModel + delegate: NotesHistoryListDelegate{ + width: list.width - rightSpacing + height: XsStyleSheet.widgetStdHeight * 8 + + delegateModel: chooserModel + popupMenu: resultPopup + onShowImages: (images) => list.viewImages(images) + } + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryResultPopup.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryResultPopup.qml new file mode 100644 index 000000000..7d41e0911 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryResultPopup.qml @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + + +import QtQuick.Layouts + + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.clipboard 1.0 +import QuickFuture 1.0 + +XsPopupMenu { + id: rightClickMenu + visible: false + + property var popupSelectionModel + property var popupDelegateModel + + menu_model_name: "notehistory_menu_"+rightClickMenu + + Clipboard { + id: clipboard + } + + XsMenuModelItem { + text: "Add To New Playlist" + menuItemPosition: 1 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: ShotBrowserHelpers.addToNewPlaylist(popupSelectionModel.selectedIndexes) + } + + XsMenuModelItem { + menuItemType: "divider" + menuItemPosition: 2 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + } + + XsMenuModelItem { + text: "Select All" + menuItemPosition: 3 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: popupSelectionModel.select( + helpers.createItemSelectionFromList(ShotBrowserHelpers.getAllIndexes(popupDelegateModel)), + ItemSelectionModel.Select + ) + } + XsMenuModelItem { + text: "Deselect All" + menuItemPosition: 4 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: popupSelectionModel.clear() + } + XsMenuModelItem { + text: "Invert Selection" + menuItemPosition: 5 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: popupSelectionModel.select( + helpers.createItemSelectionFromList(ShotBrowserHelpers.getAllIndexes(popupDelegateModel)), + ItemSelectionModel.Toggle + ) + } + XsMenuModelItem { + menuItemType: "divider" + menuItemPosition: 6 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + } + XsMenuModelItem { + text: "Reveal In ShotGrid..." + (enabled ? "" : " (Production Only)") + enabled: ShotBrowserEngine.shotGridUserType != "User" + menuItemPosition: 7 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: ShotBrowserHelpers.revealInShotgrid(popupSelectionModel.selectedIndexes) + } + XsMenuModelItem { + text: "Copy" + menuItemPosition: 8 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: clipboard.text = ShotBrowserHelpers.getNote(popupSelectionModel.selectedIndexes) + } + XsMenuModelItem { + text: "Copy JSON" + menuItemPosition: 9 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: clipboard.text = JSON.stringify(ShotBrowserHelpers.getJSON(popupSelectionModel.selectedIndexes)) + } + XsMenuModelItem { + text: "Copy DNUuid" + menuItemPosition: 10 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: clipboard.text = ShotBrowserHelpers.getDNUuid(popupSelectionModel.selectedIndexes).join("\n") + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryTitleDiv.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryTitleDiv.qml new file mode 100644 index 000000000..803c39479 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/NotesHistoryTitleDiv.qml @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick + +import QtQuick.Layouts + + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 + +RowLayout { + + readonly property real titleButtonSpacing: 1 + property real titleButtonHeight: XsStyleSheet.widgetStdHeight+4 + + XsPrimaryButton { + Layout.preferredWidth: 40 + Layout.fillHeight: true + + imgSrc: isPanelEnabled && !isPaused ? "qrc:///shotbrowser_icons/lock_open.svg" : "qrc:///shotbrowser_icons/lock.svg" + // text: isPanelEnabled? "ON" : "OFF" + isActive: !isPanelEnabled || isPaused + onClicked: isPanelEnabled = !isPanelEnabled + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + spacing: titleButtonSpacing + + RowLayout{ + Layout.fillWidth: true + Layout.preferredHeight: titleButtonHeight + // width: parent.width + // height: titleButtonHeight + spacing: 0 + + XsText{ id: scopeTxt + Layout.preferredWidth: (textWidth + panelPadding*3) + Layout.fillHeight: true + text: "Scope: " + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + enabled: isPanelEnabled && !isPaused + spacing: titleButtonSpacing + + Repeater { + model: ShotBrowserEngine.presetsModel.noteHistoryScope + + XsPrimaryButton{ + Layout.fillWidth: true + Layout.fillHeight: true + text: ShotBrowserEngine.presetsModel.get(modelData, "nameRole") + + isActive: activeScopeIndex == modelData + property bool isRunning: queryRunning && isActive + + onClicked: { + activateScope(modelData) + } + XsBusyIndicator{ + x: 4 + width: height + height: parent.height + running: visible + visible: isRunning + scale: 0.5 + } + } + } + } + } + + RowLayout{ + Layout.fillWidth: true + Layout.preferredHeight: titleButtonHeight + // width: parent.width + // height: titleButtonHeight + spacing: 0 + + XsText{ + Layout.preferredWidth: scopeTxt.width + Layout.fillHeight: true + text: "Type: " + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + enabled: isPanelEnabled && !isPaused + spacing: titleButtonSpacing + + Repeater { + model: ShotBrowserEngine.presetsModel.noteHistoryType + + XsPrimaryButton{ + Layout.fillWidth: true + Layout.fillHeight: true + text: ShotBrowserEngine.presetsModel.get(modelData, "nameRole") + + isActive: activeTypeIndex == modelData + property bool isRunning: queryRunning && isActive + + + onClicked: { + activateType(modelData) + } + XsBusyIndicator{ + x: 4 + width: height + height: parent.height + running: visible + visible: isRunning + scale: 0.5 + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/dataItems/NotesHistoryDetailRow.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/dataItems/NotesHistoryDetailRow.qml new file mode 100644 index 000000000..0d073c672 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/dataItems/NotesHistoryDetailRow.qml @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + +import QtQuick.Layouts + +import xStudio 1.0 + +RowLayout{ + id: control + property alias titleText: titleDiv.text + property alias valueText: valueDiv.text + property alias toolTipMArea: toolTipMArea + property alias valueDiv: valueDiv + property alias textColor: valueDiv.color + property string toolTip: valueDiv.text + + spacing: 0 + + XsText{ id: titleDiv + text: "Title :" + Layout.preferredWidth: parent.width/4 + Layout.maximumHeight: parent.height + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + horizontalAlignment: Text.AlignLeft + color: XsStyleSheet.hintColor + elide: Text.ElideRight + } + + XsText{ id: valueDiv + text: "Value" + Layout.fillWidth: true + Layout.preferredWidth: parent.width/1.5 + Layout.maximumHeight: parent.height + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + horizontalAlignment: Text.AlignRight + color: XsStyleSheet.hintColor + elide: Text.ElideRight + + MouseArea { id: toolTipMArea + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + acceptedButtons: Qt.NoButton + + XsToolTip{ + text: control.toolTip + visible: parent.containsMouse && parent.parent.truncated + width: parent.parent.textWidth == 0? 0 : 150 + x: 0 + } + } + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/dataItems/NotesHistoryTextRow.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/dataItems/NotesHistoryTextRow.qml new file mode 100644 index 000000000..c83e9aa33 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/dataItems/NotesHistoryTextRow.qml @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + +import QtQuick.Layouts + +import xStudio 1.0 + +Rectangle{ + property alias text: textDiv.text + property alias textDiv: textDiv + property alias textColor: textDiv.color + + color: XsStyleSheet.widgetBgNormalColor + + XsText{ id: textDiv //XsTextInput + text: "" + color: palette.text + font.bold: true + width : parent.width - panelPadding*2 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + // enabled: false + // readOnly: true + leftPadding: panelPadding + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/sections/NotesHistorySection1.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/sections/NotesHistorySection1.qml new file mode 100644 index 000000000..37c3b82b7 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/sections/NotesHistorySection1.qml @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects +import QuickFuture 1.0 + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 + +Rectangle{ + color: XsStyleSheet.widgetBgNormalColor + + property bool isHovered: thumbMArea.containsMouse || + dateDiv.toolTipMArea.containsMouse || + artistDiv.toolTipMArea.containsMouse || + timeDiv.toolTipMArea.containsMouse || + typeDiv.toolTipMArea.containsMouse || + fromDiv.toolTipMArea.containsMouse || + toDiv.toolTipMArea.containsMouse || + noThumbnailDisplayMArea.containsMouse + + MouseArea { id: thumbMArea + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + propagateComposedEvents: true + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: + { + top: 1 + bottom: 0 + left: 2 + right: 4 + } + spacing: 0 + + Item{ id: thumbnailDiv + Layout.fillWidth: true + Layout.fillHeight: true + // Layout.preferredHeight: itemHeight + // Layout.rowSpan: 2 + // Layout.columnSpan: 2 + + property bool failed: thumbRole == undefined + + Rectangle{ + width: parent.width + height: width / (16/9) - 10 + color: Qt.lighter(panelColor, 1.1) + visible: parent.failed + } + XsText{ id: noThumbnailDisplay + text: parent.failed? "No ShotGrid\nThumbnail":"Loading..." + width: thumbnail.width + height: thumbnail.height + anchors.centerIn: parent + wrapMode: Text.WordWrap + font.pixelSize: XsStyleSheet.fontSize + font.weight: Font.Black + opacity: parent.failed? 0.4 : 0.9 + color: XsStyleSheet.secondaryTextColor + + MouseArea { id: noThumbnailDisplayMArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + enabled: thumbRole + + XsToolTip{ + text: thumbRole==undefined? "" : thumbRole + visible: parent.containsMouse && thumbnailDiv.failed + x: 0 + } + } + } + XsImage{ id: thumbnail + visible: !parent.failed + width: parent.width + height: width / (16/9) + source: parent.failed || thumbRole==undefined? "" : thumbRole + fillMode: Image.PreserveAspectFit + anchors.centerIn: parent + cache: true + asynchronous: true + sourceSize.height: height + sourceSize.width: width + imgOverlayColor: "transparent" + + opacity: 0 + Behavior on opacity {NumberAnimation {duration: 150}} + + onStatusChanged: { + if (status == Image.Null) { parent.failed=true; opacity=0 } + else if (status == Image.Error) { parent.failed=true; opacity=0; noThumbnailDisplay.text= "Thumbnail Error" } + else if (status == Image.Ready) opacity=1 + else opacity=0 + } + } + + XsPrimaryButton { + imgSrc: "qrc:///shotbrowser_icons/attach_file.svg" + height: 20 + width: 20 + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 6 + background: Item{} + visible: attachmentsRole && attachmentsRole.length + imgOverlayColor: isHovered ? (pressed ? palette.highlight : palette.text) : XsStyleSheet.hintColor + onClicked: { + let images = [] + attachmentsRole.forEach(function (item, index) { + images.push([item.name, ShotBrowserEngine.downloadImage( + item.id, + "Attachment", + item.name, + projectRole)]) + + // Future.promise( + // helpers.openURLFuture("http://shotgun.dneg.com/thumbnail/full/"+item.type+"/"+item.id) + // ).then(function(result) { + // }) + }) + showImages(images) + } + + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowColor: "#010101" + shadowVerticalOffset: 2 + shadowHorizontalOffset: 2 + shadowScale: 1.5 + shadowBlur: 0.2 + } + + + } + } + Item{ id: emptyDiv + Layout.fillWidth: true + Layout.preferredHeight: 1 + } + + NotesHistoryDetailRow{ id: dateDiv + Layout.fillWidth: true + Layout.preferredHeight: XsStyleSheet.widgetStdHeight/1.5 + titleText: "Date :" + // valueText: "Nov 30 2023" + + property var dateFormatted: createdDateRole.toLocaleString().split(" ") + valueText: typeof dateFormatted !== 'undefined'? dateFormatted[1].substr(0,3)+" "+dateFormatted[2]+" "+dateFormatted[3] : "" + + toolTip: dateFormatted[0].substr(0,dateFormatted[0].length-1) + } + + NotesHistoryDetailRow{ id: timeDiv + Layout.fillWidth: true + Layout.preferredHeight: XsStyleSheet.widgetStdHeight/1.5 + titleText: "Time :" + // valueText: "10:01 AM IST" + + property var dateFormatted: createdDateRole.toLocaleString().split(" ") + property var timeFormatted: dateFormatted[4].split(":") + valueText: typeof timeFormatted !== 'undefined'? + typeof dateFormatted[6] !== 'undefined'? + timeFormatted[0]+":"+timeFormatted[1]+" "+dateFormatted[5]+" "+dateFormatted[6] : + timeFormatted[0]+":"+timeFormatted[1]+" "+dateFormatted[5] : "" + + } + + NotesHistoryDetailRow{ id: typeDiv + Layout.fillWidth: true + Layout.preferredHeight: XsStyleSheet.widgetStdHeight/1.5 + titleText: "Type :" + valueText: noteTypeRole ? noteTypeRole : "" + textColor: palette.text + } + + NotesHistoryDetailRow{ id: artistDiv + Layout.fillWidth: true + Layout.preferredHeight: XsStyleSheet.widgetStdHeight/1.5 + titleText: "Artist :" + valueText: artistRole ? artistRole : "" + } + + NotesHistoryDetailRow{ id: fromDiv + Layout.fillWidth: true + Layout.preferredHeight: XsStyleSheet.widgetStdHeight/1.5 + titleText: "From :" + valueText: createdByRole ? createdByRole : "" + } + + NotesHistoryDetailRow{ id: toDiv + Layout.fillWidth: true + Layout.preferredHeight: XsStyleSheet.widgetStdHeight/1.5 + titleText: "To :" + valueText: addressingRole ? addressingRole.join("\n") : "" + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/sections/NotesHistorySection2.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/sections/NotesHistorySection2.qml new file mode 100644 index 000000000..39985a451 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/notes_history/sections/NotesHistorySection2.qml @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + +import QtQuick.Layouts +import QtQuick.Controls.Basic + +import xStudio 1.0 +import ShotBrowser 1.0 + +Rectangle{ + color: "transparent" + + property bool isHovered: notesDiv.isHovered || toolTipMArea.containsMouse + + ColumnLayout { + anchors.fill: parent + spacing: 1 + + NotesHistoryTextRow{ id: subjectDiv + Layout.fillWidth: true + Layout.minimumHeight: XsStyleSheet.widgetStdHeight + text: versionNameRole + } + + NotesHistoryTextRow{ id: titleDiv + Layout.fillWidth: true + Layout.minimumHeight: XsStyleSheet.widgetStdHeight + text: subjectRole + textColor: XsStyleSheet.hintColor + } + + Rectangle{ id: notesDiv + Layout.fillWidth: true + Layout.fillHeight: true + color: XsStyleSheet.widgetBgNormalColor + + property bool isHovered: notesEdit.hovered || scrollView.hovered + + ScrollView{ id: scrollView + anchors.fill: parent + hoverEnabled: contentHeight > height || contentWidth > width + focusPolicy: Qt.ClickFocus + enabled: false + + TextArea{ id: notesEdit + enabled: false + readOnly: true + + text: contentRole + padding: panelPadding + wrapMode: TextEdit.Wrap + + XsToolTip{ + id: toolTip + text: parent.lineCount>15 ? parent.getFormattedText(0, parent.text.length) : parent.text + scaling: 0.8 + visible: toolTipMArea.containsMouse && (scrollView.contentHeight > scrollView.height || scrollView.contentWidth > scrollView.width) //parent.lineCount>7 + maxWidth: parent.width<200? parent.width+40 : parent.width + } + + } + } + MouseArea { + id: toolTipMArea + z: 20 + anchors.fill: scrollView + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + XsIcon{ + width: XsStyleSheet.secondaryButtonStdWidth + height: XsStyleSheet.secondaryButtonStdWidth + anchors.right: parent.right + anchors.rightMargin: 2 + anchors.bottom: parent.bottom + anchors.bottomMargin: 7 + imgOverlayColor: toolTipMArea.containsMouse? palette.highlight : XsStyleSheet.secondaryTextColor + source: "qrc:///shotbrowser_icons/arrow_right.svg" + visible: scrollView.contentWidth > scrollView.width + } + XsIcon{ + width: XsStyleSheet.secondaryButtonStdWidth + height: XsStyleSheet.secondaryButtonStdWidth + anchors.right: parent.right + anchors.rightMargin: 7 + anchors.bottom: parent.bottom + anchors.bottomMargin: 2 + imgOverlayColor: toolTipMArea.containsMouse? palette.highlight : XsStyleSheet.secondaryTextColor + source: "qrc:///shotbrowser_icons/arrow_right.svg" + visible: scrollView.contentHeight > scrollView.height + rotation: 90 + } + + } + } + +} + diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/qmldir b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/qmldir new file mode 100644 index 000000000..7598e7707 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/qmldir @@ -0,0 +1,64 @@ +module ShotBrowser + +ShotBrowserHelpers 1.0 ShotBrowserHelpers.js +ShotBrowserMediaListMenu 1.0 ShotBrowserMediaListMenu.qml + +XsSBPublishNotesFeedback.qml 1.0 dialogs/XsSBPublishNotesFeedback.qml +XsSBPublishNotesDialog 1.0 dialogs/XsSBPublishNotesDialog.qml +XsSBPublishPlaylistToShotGridDialog 1.0 dialogs/XsSBPublishPlaylistToShotGridDialog.qml +XsSBSyncPlaylistToShotGridDialog 1.0 dialogs/XsSBSyncPlaylistToShotGridDialog.qml + +ShotHistory 1.0 shot_history/ShotHistory.qml +ShotHistoryActionDiv 1.0 shot_history/ShotHistoryActionDiv.qml +ShotHistoryListDelegate 1.0 shot_history/ShotHistoryListDelegate.qml +ShotHistoryListDiv 1.0 shot_history/ShotHistoryListDiv.qml +ShotHistoryResultPopup 1.0 shot_history/ShotHistoryResultPopup.qml +ShotHistorySection1 1.0 shot_history/sections/ShotHistorySection1.qml +ShotHistorySection2 1.0 shot_history/sections/ShotHistorySection2.qml +ShotHistorySection3 1.0 shot_history/sections/ShotHistorySection3.qml +ShotHistoryTextRow 1.0 shot_history/dataItems/ShotHistoryTextRow.qml +ShotHistoryTitleDiv 1.0 shot_history/ShotHistoryTitleDiv.qml + +NotesHistory 1.0 notes_history/NotesHistory.qml +NotesHistoryActionDiv 1.0 notes_history/NotesHistoryActionDiv.qml +NotesHistoryDetailRow 1.0 notes_history/dataItems/NotesHistoryDetailRow.qml +NotesHistoryListDelegate 1.0 notes_history/NotesHistoryListDelegate.qml +NotesHistoryListDiv 1.0 notes_history/NotesHistoryListDiv.qml +NotesHistoryResultPopup 1.0 notes_history/NotesHistoryResultPopup.qml +NotesHistorySection1 1.0 notes_history/sections/NotesHistorySection1.qml +NotesHistorySection2 1.0 notes_history/sections/NotesHistorySection2.qml +NotesHistoryTextRow 1.0 notes_history/dataItems/NotesHistoryTextRow.qml +NotesHistoryTitleDiv 1.0 notes_history/NotesHistoryTitleDiv.qml + +ShotBrowserRoot 1.0 shot_browser/XsShotBrowser.qml +XsSBCountDisplay 1.0 shot_browser/widgets/XsSBCountDisplay.qml +XsSBMediaPlayer 1.0 shot_browser/widgets/XsSBMediaPlayer.qml +XsSBImageViewer 1.0 shot_browser/widgets/XsSBImageViewer.qml +XsSBL1Tools 1.0 shot_browser/left_sections/XsSBL1Tools.qml +XsSBL2V1Tree 1.0 shot_browser/left_sections/XsSBL2V1Tree.qml +XsSBL2V2Presets 1.0 shot_browser/left_sections/XsSBL2V2Presets.qml +XsSBL2Views 1.0 shot_browser/left_sections/XsSBL2Views.qml +XsSBL3Actions 1.0 shot_browser/left_sections/XsSBL3Actions.qml +XsSBLeftSection 1.0 shot_browser/XsSBLeftSection.qml +XsSBLoginDialog 1.0 shot_browser/left_sections/viewItems/XsSBLoginDialog.qml +XsSBPresetEditItem 1.0 shot_browser/left_sections/viewItems/XsSBPresetEditItem.qml +XsSBPresetEditNewItem 1.0 shot_browser/left_sections/viewItems/XsSBPresetEditNewItem.qml +XsSBPresetEditPopup 1.0 shot_browser/left_sections/viewItems/XsSBPresetEditPopup.qml +XsSBPresetsView 1.0 shot_browser/left_sections/viewItems/XsSBPresetsView.qml +XsSBPresetGroupDelegate 1.0 shot_browser/left_sections/viewItems/XsSBPresetGroupDelegate.qml +XsSBPresetDelegate 1.0 shot_browser/left_sections/viewItems/XsSBPresetDelegate.qml +XsSBR1Tools 1.0 shot_browser/right_sections/XsSBR1Tools.qml +XsSBR2Views 1.0 shot_browser/right_sections/XsSBR2Views.qml +XsSBR3Actions 1.0 shot_browser/right_sections/XsSBR3Actions.qml +XsSBRightSection 1.0 shot_browser/XsSBRightSection.qml +XsSBRPlaylistResultPopup 1.0 shot_browser/right_sections/XsSBRPlaylistResultPopup.qml +XsSBRPlaylistViewDelegate 1.0 shot_browser/right_sections/viewItems/XsSBRPlaylistViewDelegate.qml +XsSBTreeSearchButton 1.0 shot_browser/widgets/XsSBTreeSearchButton.qml +XsSBSequenceDelegate 1.0 shot_browser/left_sections/viewItems/XsSBSequenceDelegate.qml +XsSBShotDelegate 1.0 shot_browser/left_sections/viewItems/XsSBShotDelegate.qml + +plugin shotbrowser +classname ShotBrowserEngine +classname ShotBrowserResultModel +classname ShotBrowserSequenceFilterModel +classname ShotBrowserPresetTreeFilterModel \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/XsSBLeftSection.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/XsSBLeftSection.qml new file mode 100644 index 000000000..5dc0b4fc5 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/XsSBLeftSection.qml @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 +import ShotBrowser 1.0 + +Item{ + + XsGradientRectangle{ + anchors.fill: parent + } + + ColumnLayout { id: leftView + anchors.fill: parent + anchors.margins: panelPadding + spacing: panelPadding + + XsSBL1Tools{ + Layout.fillWidth: true + Layout.minimumHeight: btnHeight + Layout.maximumHeight: btnHeight + } + + XsSBL2Views{ id: viewDiv + Layout.fillWidth: true + Layout.fillHeight: true + } + + XsSBL3Actions{ + visible: currentCategory=="Tree" + Layout.fillWidth: true + Layout.preferredWidth: parent.width + Layout.preferredHeight: (XsStyleSheet.widgetStdHeight*2 + buttonSpacing*3) + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/XsSBRightSection.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/XsSBRightSection.qml new file mode 100644 index 000000000..fa7304a7d --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/XsSBRightSection.qml @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 +import ShotBrowser 1.0 + +ColumnLayout { + spacing: panelPadding + + XsSBR1Tools{ + Layout.fillWidth: true + } + + Rectangle{ + color: panelColor + Layout.fillWidth: true + Layout.fillHeight: true + + XsSBR2Views{ + anchors.fill: parent + anchors.margins: 4 + } + } + + XsSBR3Actions{ + Layout.fillWidth: true + Layout.minimumHeight: XsStyleSheet.widgetStdHeight + Layout.maximumHeight: XsStyleSheet.widgetStdHeight + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/XsShotBrowser.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/XsShotBrowser.qml new file mode 100644 index 000000000..a218a94ab --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/XsShotBrowser.qml @@ -0,0 +1,679 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic + +import QuickFuture 1.0 +import QuickPromise 1.0 + +import xStudio 1.0 +import xstudio.qml.bookmarks 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.clipboard 1.0 +import ShotBrowser 1.0 + +Item{ + id: panel + anchors.fill: parent + + property string resultViewTitle: "" + + property alias currentCategory: prefs.category + property bool isGroupedByLatest: false + + property real buttonSpacing: 1 + property real btnWidth: XsStyleSheet.primaryButtonStdWidth + property real btnHeight: XsStyleSheet.widgetStdHeight + 4 + property real panelPadding: XsStyleSheet.panelPadding + property color panelColor: XsStyleSheet.panelBgColor + + property bool queryRunning: queryRunningCount > 0 + + property bool assetMode: false + + property var onScreenMediaUuid: currentPlayhead.mediaUuid + property var onScreenLogicalFrame: currentPlayhead.logicalFrame + + property int projectId: -1 + property var projectIndex: null + property var sequenceBaseModel: null + property var assetBaseModel: null + property var currentPresetIndex: ShotBrowserEngine.presetsModel.index(-1,-1) + + property var categoryPreset: (new Map()) + + property bool sequenceTreeLiveLink: false + property bool sequenceTreeShowPresets: true + + property bool isPopout: typeof panels_layout_model_index == "undefined" + + property alias sortByNaturalOrder: resultsFilteredModel.sortByNaturalOrder + property alias sortByCreationDate: resultsFilteredModel.sortByCreationDate + property alias sortByShotName: resultsFilteredModel.sortByShotName + property alias sortInAscending: resultsFilteredModel.sortInAscending + property alias pipeStep: resultsFilteredModel.filterPipeStep + property alias nameFilter: resultsFilteredModel.filterName + + readonly property string panelType: "ShotBrowser" + + property string onDisk: "" + + property int queryCounter: 0 + property int queryRunningCount: 0 + + property bool isPaused: false + + onOnScreenMediaUuidChanged: {if(visible) updateTimer.start()} + + onCurrentPresetIndexChanged: { + if(currentPresetIndex.valid) + categoryPreset[currentCategory] = helpers.makePersistent(currentPresetIndex) + } + + onCurrentCategoryChanged: { + if(currentCategory in categoryPreset && categoryPreset[currentCategory].valid) { + activatePreset(categoryPreset[currentCategory]) + presetsSelectionModel.select(categoryPreset[currentCategory], ItemSelectionModel.ClearAndSelect) + } + } + + onOnScreenLogicalFrameChanged: { + if(updateTimer.running) + updateTimer.restart() + if(!isPaused && (currentCategory == "Menus" && currentPresetIndex.valid) || (currentCategory == "Tree" && sequenceTreeLiveLink)) { + isPaused = true + } + } + + // why was this commented out ? + MouseArea { + anchors.fill: parent + onClicked: forceActiveFocus(panel) + } + + Timer { + id: updateTimer + interval: 500 + running: false + repeat: false + onTriggered: { + isPaused = false + updateMetaData() + } + } + + Clipboard { + id: clipboard + } + + function setProjectIndex(force = false) { + if(ShotBrowserEngine.ready && (force || (projectIndex == null || !projectIndex.valid))) { + let pi = getProjectIndexFromName(projectPref.value) + if(pi.valid) + projectIndex = pi + } + } + + function updateMetaData() { + if(visible) { + if((currentCategory == "Menus" && currentPresetIndex.valid) || (currentCategory == "Tree" && sequenceTreeLiveLink)) + return ShotBrowserHelpers.updateMetadata(true, onScreenMediaUuid) + } + return false + } + + function updateTreeSelection() { + if(visible) { + if(currentCategory == "Tree" && sequenceTreeLiveLink) { + // update tree selection + let pname = ShotBrowserEngine.getProjectFromMetadata() + if(!projectIndex.valid || projectIndex.model.get(projectIndex,"nameRole") != pname) { + projectIndex = getProjectIndexFromName(pname) + } + let entity = ShotBrowserEngine.getEntityFromMetadata() + + if(entity != undefined) { + if(assetMode && entity.type == "Asset") { + let bi = assetBaseModel.searchRecursive(entity.id, "idRole") + if(bi.valid) { + let fi = assetFilterModel.mapFromSource(bi) + if(fi.valid) { + let parents = helpers.getParentIndexes([fi]) + for(let i = 0; i< parents.length; i++){ + if(parents[i].valid) { + // find in tree.. Order ? + let ti = assetTreeModel.mapFromModel(parents[i]) + if(ti.valid) { + assetTreeModel.expandRow(ti.row) + } + } + } + // should now be visible + let ti = assetTreeModel.mapFromModel(fi) + assetSelectionModel.select(ti, ItemSelectionModel.ClearAndSelect) + } + } + + } else if(!assetMode && entity.type != "Asset"){ + let bi = sequenceBaseModel.searchRecursive(entity.name) + if(bi.valid) { + let fi = sequenceFilterModel.mapFromSource(bi) + if(fi.valid) { + let parents = helpers.getParentIndexes([fi]) + for(let i = 0; i< parents.length; i++){ + if(parents[i].valid) { + // find in tree.. Order ? + let ti = sequenceTreeModel.mapFromModel(parents[i]) + if(ti.valid) { + sequenceTreeModel.expandRow(ti.row) + } + } + } + // should now be visible + let ti = sequenceTreeModel.mapFromModel(fi) + sequenceSelectionModel.select(ti, ItemSelectionModel.ClearAndSelect) + } + } + } + } + } + } + } + + onSequenceTreeLiveLinkChanged: { + if(sequenceTreeLiveLink) { + updateMetaData() + updateTreeSelection() + } + } + + Connections { + target: ShotBrowserEngine + function onLiveLinkMetadataChanged() { + if(panel.visible && !isPaused) { + updateTreeSelection() + + if(currentCategory == "Menus" && currentPresetIndex.valid) { + executeQuery() + } + } + } + } + + Connections { + target: ShotBrowserEngine + function onReadyChanged() {setProjectIndex()} + } + + Connections { + target: ShotBrowserEngine.presetsModel + function onPresetChanged(index) { + if(currentPresetIndex == index) { + executeQuery() + } + } + } + + function getProjectIndexFromId(project_id) { + let m = ShotBrowserEngine.presetsModel.termModel("Project") + return m.searchRecursive(project_id, "idRole") + } + + function getProjectIndexFromName(project_name) { + let m = ShotBrowserEngine.presetsModel.termModel("Project") + return m.searchRecursive(project_name, "nameRole") + } + + XsPreference { + id: projectPref + path: "/plugin/data_source/shotbrowser/browser/project" + onValueChanged: setProjectIndex(true) + } + + XsPreference { + id: addAfterSelection + path: "/plugin/data_source/shotbrowser/add_after_selection" + } + + Item { + // Hold properties that we want to persist between sessions. + id: prefs + property int resultPanelWidth: 600 + property int treePlusResultPanelWidth: 600 + property int treePanelWidth: 200 + property string category: "Tree" + property string quickLoad: "" + property bool hideEmpty: false + property bool showUnit: false + property bool showOnlyFavourites: false + property bool showStatus: false + property bool showType: false + property var hideStatus: ["omt", "na", "del", "omtnto", "omtnwd"] + property var filterProjects: [] + property var filterProjectStatus: [] + property var filterUnit: (new Map()) + property var filterType: [] + + onShowOnlyFavouritesChanged: { + treeModel.setOnlyShowFavourite(showOnlyFavourites) + menuModel.setOnlyShowFavourite(showOnlyFavourites) + recentModel.setOnlyShowFavourite(showOnlyFavourites) + } + + XsStoredPanelProperties { + + propertyNames: [ + "resultPanelWidth", + "treePlusResultPanelWidth", + "treePanelWidth", + "category", + "showOnlyFavourites", + "hideEmpty", + "showUnit", + "showType", + "hideStatus", + "showStatus", + "quickLoad", + "filterUnit", + "filterType", + "filterProjects", + "filterProjectStatus" + ] + } + + } + + ShotBrowserSequenceFilterModel { + id: sequenceFilterModel + sourceModel: sequenceBaseModel + hideStatus: prefs.hideStatus + hideEmpty: prefs.hideEmpty + typeFilter: prefs.filterType + // unitFilter: + onUnitFilterChanged: { + let tmp = prefs.filterUnit + tmp[projectIndex.model.get(projectIndex,"nameRole")] = unitFilter + prefs.filterUnit = tmp + } + } + + ShotBrowserSequenceFilterModel { + id: assetFilterModel + sourceModel: assetBaseModel + hideStatus: prefs.hideStatus + } + + QTreeModelToTableModel { + id: sequenceTreeModel + model: sequenceFilterModel + } + + QTreeModelToTableModel { + id: assetTreeModel + model: assetFilterModel + } + + ItemSelectionModel { + id: sequenceSelectionModel + model: sequenceTreeModel + onSelectionChanged: executeQuery() + } + + ItemSelectionModel { + id: assetSelectionModel + model: assetTreeModel + onSelectionChanged: executeQuery() + } + + ItemSelectionModel { + id: presetsExpandedModel + model: ShotBrowserEngine.presetsModel + } + + ItemSelectionModel { + id: presetsSelectionModel + model: ShotBrowserEngine.presetsModel + } + + ShotBrowserResultModel { + id: resultsBaseModel + isGrouped: true + } + + ShotBrowserResultFilterModel { + id: resultsFilteredModel + sourceModel: resultsBaseModel + } + + QTreeModelToTableModel { + id: results + model: resultsFilteredModel + } + + ItemSelectionModel { + id: resultsSelectionModel + model: results + } + + ShotHistoryResultPopup { + id: versionResultPopup + menu_model_name: "version_shot_browser_popup"+versionResultPopup + popupSelectionModel: resultsSelectionModel + } + + NotesHistoryResultPopup { + id: noteResultPopup + menu_model_name: "note_shot_browser_popup"+noteResultPopup + popupSelectionModel: resultsSelectionModel + } + + XsSBRPlaylistResultPopup { + id: playlistResultPopup + menu_model_name: "playlist_shot_browser_popup"+playlistResultPopup + popupSelectionModel: resultsSelectionModel + } + + ShotBrowserPresetFilterModel { + id: treeModel + showHidden: false + // onlyShowFavourite: true + filterGroupUserData: "tree" + sourceModel: ShotBrowserEngine.presetsModel + } + + ShotBrowserPresetFilterModel { + id: treeButtonModel + showHidden: false + onlyShowFavourite: true + onlyShowPresets: true + ignoreToolbar: true + filterGroupUserData: "tree" + sourceModel: buttonModelBase + + onModelReset: { + if(!currentPresetIndex.valid) { + let i = treeButtonModel.index(0,0) + if(i.valid) { + i = treeButtonModel.mapToSource(i) + currentPresetIndex = i.model.mapToModel(i) + } + } + } + onRowsInserted: { + if(!currentPresetIndex.valid) { + let i = treeButtonModel.index(0,0) + if(i.valid) { + i = treeButtonModel.mapToSource(i) + currentPresetIndex = i.model.mapToModel(i) + } + } + } + } + + ShotBrowserPresetFilterModel { + id: recentButtonModel + showHidden: false + onlyShowFavourite: true + onlyShowPresets: true + ignoreToolbar: true + filterGroupUserData: "recent" + sourceModel: buttonModelBase + } + + ShotBrowserPresetFilterModel { + id: menuButtonModel + showHidden: false + onlyShowFavourite: true + onlyShowPresets: true + ignoreToolbar: true + filterGroupUserData: "menu" + sourceModel: buttonModelBase + } + + QTreeModelToTableModel { + id: buttonModelBase + model: ShotBrowserEngine.presetsModel + onCountChanged: expandAll(2) + } + + ShotBrowserPresetFilterModel { + id: menuModel + showHidden: false + // onlyShowFavourite: true + filterGroupUserData: "menus" + sourceModel: ShotBrowserEngine.presetsModel + } + + ShotBrowserPresetFilterModel { + id: recentModel + showHidden: false + // onlyShowFavourite: true + filterGroupUserData: "recent" + sourceModel: ShotBrowserEngine.presetsModel + } + + onProjectIndexChanged: { + if(projectIndex && projectIndex.valid) { + let m = ShotBrowserEngine.presetsModel.termModel("Project") + let i = m.get(projectIndex, "idRole") + ShotBrowserEngine.cacheProject(i) + sequenceBaseModel = ShotBrowserEngine.sequenceTreeModel(i) + assetBaseModel = ShotBrowserEngine.assetTreeModel(i) + + projectPref.value = m.get(projectIndex, "nameRole") + projectId = i + } + } + + XsSplitView { + id: main_split + anchors.fill: parent + + property bool treePlusActive: currentCategory == "Tree" && sequenceTreeShowPresets + + readonly property int minimumResultWidth: 600 + readonly property int minimumPresetWidth: 330 + readonly property int minimumTreeWidth: 230 + + XsSBLeftSection{ id: leftSection + SplitView.fillHeight: true + // SplitView.fillWidth: true + SplitView.minimumWidth: ( + main_split.treePlusActive ? + prefs.treePanelWidth + 150 : + main_split.minimumPresetWidth + ) + SplitView.preferredWidth: (main_split.treePlusActive ? main_split.width - prefs.treePlusResultPanelWidth : main_split.width -prefs.resultPanelWidth) + } + + XsGradientRectangle{ + SplitView.fillHeight: true + SplitView.fillWidth: true + // SplitView.preferredWidth: (main_split.treePlusActive ? prefs.treePlusResultPanelWidth : prefs.resultPanelWidth) + SplitView.minimumWidth: main_split.minimumResultWidth + + onWidthChanged: { + if(SplitView.view.resizing) { + if(main_split.treePlusActive) { + prefs.treePlusResultPanelWidth = width + } else { + prefs.resultPanelWidth = width + } + } + } + + XsSBRightSection { + + anchors.fill: parent + anchors.margins: 4 + id: right_section + + } + } + } + + onOnDiskChanged: { + resultsFilteredModel.filterChn = (onDisk == "chn") + resultsFilteredModel.filterLon = (onDisk == "lon") + resultsFilteredModel.filterMtl = (onDisk == "mtl") + resultsFilteredModel.filterMum = (onDisk == "mum") + resultsFilteredModel.filterVan = (onDisk == "van") + resultsFilteredModel.filterSyd = (onDisk == "syd") + } + + function executeQuery() { + if(currentPresetIndex && currentPresetIndex.valid) { + let customContext = {} + customContext["project_name"] = projectPref.value + customContext["preset_name"] = ShotBrowserEngine.presetsModel.get(currentPresetIndex, "nameRole") + + resultsSelectionModel.clear() + + // pipeStep + if(currentCategory == "Menus") { + queryCounter += 1 + queryRunningCount += 1 + let i = queryCounter + + Future.promise( + ShotBrowserEngine.executeQueryJSON( + [ShotBrowserEngine.presetsModel.get(currentPresetIndex, "jsonPathRole")], {}, [], customContext) + ).then(function(json_string) { + // console.log(json_string) + if(queryCounter == i) { + resultsSelectionModel.clear() + resultsBaseModel.setResultDataJSON([json_string]) + } + queryRunningCount -= 1 + }, + function() { + resultsBaseModel.setResultDataJSON([]) + queryRunningCount -= 1 + }) + } else if(currentCategory == "Recent") { + queryCounter += 1 + queryRunningCount += 1 + + let i = queryCounter + + Future.promise( + ShotBrowserEngine.executeProjectQueryJSON( + [ShotBrowserEngine.presetsModel.get(currentPresetIndex, "jsonPathRole")], projectId, {}, [], customContext) + ).then(function(json_string) { + // console.log(json_string) + if(queryCounter == i) { + resultsSelectionModel.clear() + resultsBaseModel.setResultDataJSON([json_string]) + } + queryRunningCount -= 1 + }, + function() { + resultsBaseModel.setResultDataJSON([]) + queryRunningCount -= 1 + }) + } else { + let custom = [] + // convert to base model. + let seqsel = [] + if(assetMode) { + for(let i=0;i projectIndex = model.mapToSource(model.index(index, 0)) + onAccepted: { + projectIndex = model.mapToSource(model.index(currentIndex, 0)) + focus = false + } + + Connections { + target: panel + function onProjectIndexChanged() { + // console.log(projectIndex, projectIndex.row) + if(combo.currentIndex != combo.model.mapFromSource(projectIndex).row) { + combo.currentIndex = combo.model.mapFromSource(projectIndex).row + } + } + } + Component.onCompleted: { + if(ShotBrowserEngine.ready) + combo.model.sourceModel = ShotBrowserEngine.presetsModel.termModel("Project") + + if(projectIndex && projectIndex.valid && combo.currentIndex != model.mapFromSource(projectIndex).row) { + combo.currentIndex = model.mapFromSource(projectIndex).row + } + } + } + + XsPrimaryButton{ id: filterBtn + Layout.minimumWidth: butWidth + Layout.maximumWidth: butWidth + Layout.fillHeight: true + Layout.rightMargin: 4 + + imgSrc: "qrc:/icons/filter.svg" + isActive: combo.model && combo.model.divisionFilter.length + onClicked: { + // searchBtn.isExpanded = false + if(projectFilterPopup.visible) { + projectFilterPopup.visible = false + } else { + projectFilterPopup.showMenu( + filterBtn, + width/2, + height/2); + } + } + } + + XsPopupMenu { + id: projectFilterPopup + menu_model_name: "project_filter_popup" + visible: false + + closePolicy: filterBtn.hovered ? Popup.CloseOnEscape : Popup.CloseOnEscape | Popup.CloseOnPressOutside + XsMenuModelItem { + menuItemType: "divider" + text: "Hide Status" + menuItemPosition: 1 + menuPath: "" + menuModelName: projectFilterPopup.menu_model_name + } + + Repeater { + model: [ + {"name": "Awarded", "id": "awrd"}, + {"name": "Bidding", "id": "bld"}, + {"name": "Complete", "id": "cmpt"}, + {"name": "Error", "id": "err"}, + {"name": "In Progress", "id": "ip"}, + {"name": "N/A", "id": "na"}, + {"name": "On Hold", "id": "hld"}, + {"name": "Projected", "id": "prjd"}, + {"name": "Waiting To Start", "id": "wtg"} + ] + Item { + XsMenuModelItem { + text: modelData.name + menuItemType: "toggle" + menuPath: "" + menuItemPosition: index + 1 + menuModelName: projectFilterPopup.menu_model_name + isChecked: combo.model && combo.model.projectStatusFilter.includes(modelData.id) + onActivated: { + if(isChecked) { + prefs.filterProjectStatus = Array.from(combo.model.projectStatusFilter).filter(r => r !== modelData.id) + } else { + let tmp = combo.model.projectStatusFilter + tmp.push(modelData.id) + prefs.filterProjectStatus = tmp + } + } + } + } + } + + + XsMenuModelItem { + menuItemType: "divider" + text: "Hide Division" + menuItemPosition: 99 + menuPath: "" + menuModelName: projectFilterPopup.menu_model_name + } + + Repeater { + model: ["Feature Animation", "Visual Effects", "ReDefine", "TV", "dneg360"] + Item { + XsMenuModelItem { + text: modelData + menuItemType: "toggle" + menuPath: "" + menuItemPosition: index + 100 + menuModelName: projectFilterPopup.menu_model_name + isChecked: combo.model && combo.model.divisionFilter.includes(modelData) + onActivated: { + if(isChecked) { + prefs.filterProjects = Array.from(combo.model.divisionFilter).filter(r => r !== modelData) + } else { + let tmp = combo.model.divisionFilter + tmp.push(modelData) + prefs.filterProjects = tmp + } + } + } + } + } + } + + + XsPrimaryButton{ id: credentialsBtn + Layout.minimumWidth: butWidth + Layout.maximumWidth: butWidth + Layout.fillHeight: true + imgSrc: "qrc:///shotbrowser_icons/manage_accounts.svg" + isActive: loginDialog.visible + onClicked: { + showOrHideLoginDialog() + } + } + +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/XsSBL2V1Tree.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/XsSBL2V1Tree.qml new file mode 100644 index 000000000..eb8354ef9 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/XsSBL2V1Tree.qml @@ -0,0 +1,563 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic +import Qt.labs.qmlmodels + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 + + +Item{ + + XsGradientRectangle{ + anchors.fill: parent + } + + ColumnLayout { + anchors.fill: parent + anchors.rightMargin: panelPadding + spacing: panelPadding + + Rectangle{ + Layout.fillWidth: true + Layout.preferredHeight: 2 + color: panelColor + } + + RowLayout { id: headerDiv + Layout.fillWidth: true; + Layout.preferredHeight: btnHeight + spacing: buttonSpacing + z: 1 + + + XsComboBoxEditable{ id: searchBtn + Layout.fillWidth: true + Layout.minimumWidth: btnWidth + Layout.preferredWidth: btnWidth + Layout.preferredHeight: parent.height + property bool isExpanded: true + + model: assetMode ? assetListMode : seqListMode + textRole: "nameRole" + currentIndex: -1 + displayText: currentIndex ==-1 ? "Search..." : currentText + + onModelChanged: currentIndex = -1 + + ShotBrowserSequenceFilterModel { + id: seqListMode + sourceModel: ShotBrowserEngine.presetsModel.termModel("ShotSequenceList", "", projectId) + hideStatus: prefs.hideStatus + hideEmpty: prefs.hideEmpty + typeFilter: prefs.filterType + unitFilter: { + if( projectIndex && projectIndex.model.get(projectIndex,"nameRole") in prefs.filterUnit) + return prefs.filterUnit[projectIndex.model.get(projectIndex,"nameRole")] + return [] + } + } + + ShotBrowserSequenceFilterModel { + id: assetListMode + sourceModel: ShotBrowserEngine.presetsModel.termModel("AssetList", "", projectId) + hideStatus: prefs.hideStatus + } + + onAccepted: { + // try find first match? + if(currentIndex == -1) { + currentIndex = find(textField.text, Qt.MatchContains) + } + selectInTree() + } + + function selectInTree() { + // possibility of id collisions ? + if(currentIndex != -1) { + let index = model.index(currentIndex, 0) + let mid = index.model.get(index, "idRole") + let baseModel = sequenceBaseModel + let filterModel = sequenceFilterModel + let selectionModel = sequenceSelectionModel + let treemodel = sequenceTreeModel + + if(assetMode) { + baseModel = assetBaseModel + filterModel = assetFilterModel + selectionModel = assetSelectionModel + treemodel = assetTreeModel + } + + let bi = baseModel.searchRecursive(mid, "idRole") + if(bi.valid) { + let fi = filterModel.mapFromSource(bi) + if(fi.valid) { + let parents = helpers.getParentIndexes([fi]) + for(let i = 0; i< parents.length; i++){ + if(parents[i].valid) { + // find in tree.. Order ? + let ti = treemodel.mapFromModel(parents[i]) + if(ti.valid) { + treemodel.expandRow(ti.row) + } + } + } + // should now be visible + let ti = treemodel.mapFromModel(fi) + selectionModel.select(ti, ItemSelectionModel.ClearAndSelect) + } + } + } + } + + onActivated: selectInTree() + } + + + + // XsSBTreeSearchButton{ id: searchBtn + // Layout.fillWidth: isExpanded + // Layout.minimumWidth: btnWidth + // Layout.preferredWidth: btnWidth + // Layout.preferredHeight: parent.height + // isExpanded: false + // hint: "Search..." + // model: assetMode ? assetListMode : seqListMode + + // ShotBrowserFilterModel { + // id: seqListMode + // sourceModel: ShotBrowserEngine.presetsModel.termModel("ShotSequenceList", "", projectId) + // } + + // ShotBrowserFilterModel { + // id: assetListMode + // sourceModel: ShotBrowserEngine.presetsModel.termModel("AssetList", "", projectId) + // } + + // onIndexSelected: (index) => { + // // possibility of id collisions ? + // let mid = index.model.get(index, "idRole") + // let baseModel = sequenceBaseModel + // let filterModel = sequenceFilterModel + // let selectionModel = sequenceSelectionModel + // let treemodel = sequenceTreeModel + + // if(assetMode) { + // baseModel = assetBaseModel + // filterModel = assetFilterModel + // selectionModel = assetSelectionModel + // treemodel = assetTreeModel + // } + + // let bi = baseModel.searchRecursive(mid, "idRole") + // if(bi.valid) { + // let fi = filterModel.mapFromSource(bi) + // if(fi.valid) { + // let parents = helpers.getParentIndexes([fi]) + // for(let i = 0; i< parents.length; i++){ + // if(parents[i].valid) { + // // find in tree.. Order ? + // let ti = treemodel.mapFromModel(parents[i]) + // if(ti.valid) { + // treemodel.expandRow(ti.row) + // } + // } + // } + // // should now be visible + // let ti = treemodel.mapFromModel(fi) + // selectionModel.select(ti, ItemSelectionModel.ClearAndSelect) + // } + // } + // } + // } + Item{ + Layout.fillWidth: !searchBtn.isExpanded + Layout.preferredWidth: searchBtn.isExpanded? buttonSpacing : buttonSpacing*8 + Layout.preferredHeight: parent.height + } + XsButtonWithImageAndText{ id: liveLinkBtn + Layout.fillWidth: !searchBtn.isExpanded + Layout.minimumWidth: btnWidth + Layout.preferredWidth: searchBtn.isExpanded? btnWidth : btnWidth*2 + Layout.maximumWidth: btnWidth*2 + Layout.preferredHeight: parent.height + iconSrc: "qrc:/icons/link.svg" + iconText: "Link" + textDiv.visible: true + isActive: sequenceTreeLiveLink && !isPaused + onClicked: { + // searchBtn.isExpanded = false + sequenceTreeLiveLink = !sequenceTreeLiveLink + } + } + Item{ + Layout.fillWidth: !searchBtn.isExpanded + Layout.preferredWidth: searchBtn.isExpanded? buttonSpacing : buttonSpacing*8 + Layout.preferredHeight: parent.height + } + XsPrimaryButton{ id: seqBtn + Layout.preferredWidth: btnWidth + Layout.preferredHeight: parent.height + imgSrc: "qrc:/icons/movie.svg" + isActive: !assetMode + onClicked: assetMode = false + } + XsPrimaryButton{ id: assBtn + Layout.preferredWidth: btnWidth + Layout.preferredHeight: parent.height + Layout.rightMargin: 4 + imgSrc: "qrc:/icons/deployed_cube.svg" + isActive: assetMode + onClicked: assetMode = true + } + + XsPrimaryButton{ id: filterBtn + Layout.preferredWidth: btnWidth + Layout.preferredHeight: parent.height + imgSrc: "qrc:/icons/filter.svg" + isActive: sequenceFilterModel && sequenceFilterModel.hideStatus.length + onClicked: { + // searchBtn.isExpanded = false + if(shotFilterPopup.visible) { + shotFilterPopup.visible = false + } else { + shotFilterPopup.showMenu( + filterBtn, + width/2, + height/2); + } + } + } + } + + XsPopupMenu { + id: shotFilterPopup + menu_model_name: "shot_filter_popup" + visible: false + + closePolicy: filterBtn.hovered ? Popup.CloseOnEscape : Popup.CloseOnEscape | Popup.CloseOnPressOutside + + XsMenuModelItem { + text: "Show Status" + menuItemType: "toggle" + menuPath: "" + menuItemPosition: 0 + menuModelName: shotFilterPopup.menu_model_name + isChecked: prefs.showStatus + onActivated: prefs.showStatus = !prefs.showStatus + } + XsMenuModelItem { + text: "Show Unit" + menuItemType: "toggle" + menuPath: "" + menuItemPosition: 0.1 + menuModelName: shotFilterPopup.menu_model_name + isChecked: prefs.showUnit + onActivated: prefs.showUnit = !prefs.showUnit + } + + XsMenuModelItem { + text: "Show Type" + menuItemType: "toggle" + menuPath: "" + menuItemPosition: 0.2 + menuModelName: shotFilterPopup.menu_model_name + isChecked: prefs.showType + onActivated: prefs.showType = !prefs.showType + } + + XsMenuModelItem { + text: "Hide Empty" + menuItemType: "toggle" + menuPath: "" + menuItemPosition: 0.9 + menuModelName: shotFilterPopup.menu_model_name + isChecked: prefs.hideEmpty + onActivated: prefs.hideEmpty = !prefs.hideEmpty + } + + XsMenuModelItem { + text: "Hide Unit" + menuItemType: "divider" + menuPath: "" + menuItemPosition: 1 + menuModelName: shotFilterPopup.menu_model_name + } + + XsMenuModelItem { + text: "No Unit" + menuItemType: "toggle" + menuPath: "" + menuItemPosition: 10 + menuModelName: shotFilterPopup.menu_model_name + isChecked: sequenceFilterModel && sequenceFilterModel.unitFilter.includes("No Unit") + onActivated: { + if(isChecked) { + sequenceFilterModel.unitFilter = Array.from(sequenceFilterModel.unitFilter).filter(r => r !== "No Unit") + } else { + let tmp = sequenceFilterModel.unitFilter + tmp.push("No Unit") + sequenceFilterModel.unitFilter = tmp + } + } + } + + Repeater { + model: DelegateModel { + property var notifyUnitModel: ShotBrowserEngine.presetsModel.termModel("Unit", "Version", projectId) + onNotifyUnitModelChanged: { + if(sequenceFilterModel && projectIndex) { + if( projectIndex.model.get(projectIndex,"nameRole") in prefs.filterUnit) + sequenceFilterModel.unitFilter = prefs.filterUnit[projectIndex.model.get(projectIndex,"nameRole")] + else + sequenceFilterModel.unitFilter = [] + } + } + model: notifyUnitModel + delegate : + Item { + XsMenuModelItem { + text: nameRole + menuItemType: "toggle" + menuPath: "" + menuItemPosition: index + 10 + menuModelName: shotFilterPopup.menu_model_name + isChecked: sequenceFilterModel && sequenceFilterModel.unitFilter.includes(nameRole) + onActivated: { + if(isChecked) { + sequenceFilterModel.unitFilter = Array.from(sequenceFilterModel.unitFilter).filter(r => r !== nameRole) + } else { + let tmp = sequenceFilterModel.unitFilter + tmp.push(nameRole) + sequenceFilterModel.unitFilter = tmp + } + } + } + } + } + } + + XsMenuModelItem { + text: "Hide Type" + menuItemType: "divider" + menuPath: "" + menuItemPosition: 100 + menuModelName: shotFilterPopup.menu_model_name + } + + XsMenuModelItem { + text: "No Type" + menuItemType: "toggle" + menuPath: "" + menuItemPosition: 110 + menuModelName: shotFilterPopup.menu_model_name + isChecked: sequenceFilterModel && sequenceFilterModel.typeFilter.includes("No Type") + onActivated: { + if(isChecked) { + sequenceFilterModel.typeFilter = Array.from(sequenceFilterModel.typeFilter).filter(r => r !== "No Type") + } else { + let tmp = sequenceFilterModel.typeFilter + tmp.push("No Type") + sequenceFilterModel.typeFilter = tmp + prefs.filterType = sequenceFilterModel.typeFilter + } + } + } + + Repeater { + model: DelegateModel { + property var notifyTypeModel: sequenceFilterModel && sequenceFilterModel.sourceModel ? sequenceFilterModel.sourceModel.types : [] + onNotifyTypeModelChanged: { + if(sequenceFilterModel) + sequenceFilterModel.typeFilter = prefs.filterType + } + model: notifyTypeModel + delegate : + Item { + XsMenuModelItem { + text: modelData + menuItemType: "toggle" + menuPath: "" + menuItemPosition: index + 110 + menuModelName: shotFilterPopup.menu_model_name + isChecked: sequenceFilterModel && sequenceFilterModel.typeFilter.includes(modelData) + onActivated: { + if(isChecked) { + sequenceFilterModel.typeFilter = Array.from(sequenceFilterModel.typeFilter).filter(r => r !== modelData) + + } else { + let tmp = sequenceFilterModel.typeFilter + tmp.push(modelData) + sequenceFilterModel.typeFilter = tmp + } + prefs.filterType = sequenceFilterModel.typeFilter + } + } + } + } + } + + + XsMenuModelItem { + text: "Hide Status" + menuItemType: "divider" + menuPath: "" + menuItemPosition: 200 + menuModelName: shotFilterPopup.menu_model_name + } + + Repeater { + model: DelegateModel { + property var notifyModel: ShotBrowserEngine.presetsModel.termModel("Shot Status") + model: notifyModel + delegate : + Item { + XsMenuModelItem { + text: nameRole + menuItemType: "toggle" + menuPath: "" + menuItemPosition: index + 201 + menuModelName: shotFilterPopup.menu_model_name + isChecked: (prefs.hideStatus.includes(nameRole) || prefs.hideStatus.includes(idRole)) + onActivated: { + if(isChecked) { + prefs.hideStatus = Array.from(prefs.hideStatus).filter(r => r !== idRole && r !== nameRole) + } else { + let tmp = prefs.hideStatus + tmp.push(idRole) + tmp.push(nameRole) + prefs.hideStatus = tmp + } + } + } + } + } + } + } + + Rectangle{ + Layout.fillWidth: true; + Layout.fillHeight: true; + color: panelColor + visible: !assetMode + + XsListView { + id: sequenceTreeView + anchors.fill: parent + spacing: 1 + + ScrollBar.vertical: XsScrollBar{visible: sequenceTreeView.height < sequenceTreeView.contentHeight} + property int rightSpacing: sequenceTreeView.height < sequenceTreeView.contentHeight ? 10 : 0 + Behavior on rightSpacing {NumberAnimation {duration: 150}} + + model: sequenceTreeModel + + Connections { + target: sequenceSelectionModel + function onSelectionChanged(selected, deselected) { + if(selected.length){ + sequenceTreeView.positionViewAtIndex(selected[0].topLeft.row, ListView.Visible) + } + } + } + + delegate: DelegateChooser { + role: "typeRole" + + DelegateChoice { + roleValue: "Sequence"; + XsSBSequenceDelegate{ + width: sequenceTreeView.width - sequenceTreeView.rightSpacing + height: btnHeight-4 + delegateModel: sequenceTreeModel + selectionModel: sequenceSelectionModel + showStatus: prefs.showStatus + showType: prefs.showType + } + } + DelegateChoice { + roleValue: "Episode"; + XsSBSequenceDelegate{ + width: sequenceTreeView.width - sequenceTreeView.rightSpacing + height: btnHeight-4 + delegateModel: sequenceTreeModel + selectionModel: sequenceSelectionModel + showStatus: prefs.showStatus + showType: prefs.showType + } + } + DelegateChoice { + roleValue: "Asset"; + XsSBSequenceDelegate{ + width: sequenceTreeView.width - sequenceTreeView.rightSpacing + height: btnHeight-4 + delegateModel: sequenceTreeModel + selectionModel: sequenceSelectionModel + showStatus: prefs.showStatus + showType: prefs.showType + } + } + DelegateChoice { + roleValue: "Shot"; + XsSBShotDelegate{ + width: sequenceTreeView.width - sequenceTreeView.rightSpacing + height: btnHeight-4 + delegateModel: sequenceTreeModel + selectionModel: sequenceSelectionModel + showUnit: prefs.showUnit + showStatus: prefs.showStatus + showType: prefs.showType + } + } + } + } + } + Rectangle{ + Layout.fillWidth: true; + Layout.fillHeight: true; + color: panelColor + visible: assetMode + + XsListView { + id: assetTreeView + anchors.fill: parent + spacing: 1 + + ScrollBar.vertical: XsScrollBar{visible: assetTreeView.height < assetTreeView.contentHeight} + property int rightSpacing: assetTreeView.height < assetTreeView.contentHeight ? 10 : 0 + Behavior on rightSpacing {NumberAnimation {duration: 150}} + + model: assetTreeModel + + Connections { + target: assetSelectionModel + function onSelectionChanged(selected, deselected) { + if(selected.length){ + assetTreeView.positionViewAtIndex(selected[0].topLeft.row, ListView.Visible) + } + } + } + + delegate: DelegateChooser { + role: "typeRole" + + DelegateChoice { + roleValue: "Asset"; + XsSBSequenceDelegate{ + width: assetTreeView.width - assetTreeView.rightSpacing + height: btnHeight-4 + delegateModel: assetTreeModel + selectionModel: assetSelectionModel + showStatus: prefs.showStatus + showType: prefs.showType + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/XsSBL2V2Presets.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/XsSBL2V2Presets.qml new file mode 100644 index 000000000..630c23797 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/XsSBL2V2Presets.qml @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic + +import QuickFuture 1.0 +import QuickPromise 1.0 + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 + +XsGradientRectangle{ + id: presetView + + ColumnLayout { + anchors.fill: parent + anchors.leftMargin: (currentCategory == "Tree")? panelPadding : 0 + spacing: panelPadding + + Rectangle{ + Layout.fillWidth: true; + Layout.preferredHeight: 2; + color: panelColor + } + + RowLayout { id: headerDiv + Layout.fillWidth: true; + Layout.preferredHeight: btnHeight + spacing: buttonSpacing + + XsPrimaryButton{ id: addBtn + Layout.preferredWidth: btnWidth + Layout.preferredHeight: btnHeight + imgSrc: "qrc:/icons/add.svg" + onClicked: { + if(addMenu.visible) addMenu.visible = false + else{ + addMenu.showMenu( + addBtn, + width/2, + height/2); + } + } + } + + Item{ + Layout.fillWidth: true + Layout.preferredHeight: btnHeight + } + + XsPrimaryButton{ id: moreBtn + Layout.preferredWidth: btnWidth + Layout.preferredHeight: btnHeight + imgSrc: "qrc:/icons/more_vert.svg" + onClicked:{ + if(moreMenu.visible) moreMenu.visible = false + else{ + moreMenu.showMenu( + moreBtn, + width/2, + height/2); + } + } + } + } + + Rectangle{ + Layout.fillWidth: true; + Layout.fillHeight: true; + color: panelColor + + XsSBPresetsView{ + anchors.fill: parent + } + } + + } + + + function addGroupItem(type){ + + let m = ShotBrowserEngine.presetsModel + let i = m.insertGroup(type, -1) + + if(currentCategory == "Tree") { + m.set(i, "tree", "userDataRole") + // this shouldn't be required.. + treeModel.invalidate() + } + else if(currentCategory == "Menus") { + m.set(i, "menus", "userDataRole") + menuModel.invalidate() + } + else if(currentCategory == "Recent") { + m.set(i, "recent", "userDataRole") + recentModel.invalidate() + } + + } + + XsPopupMenu { + id: addMenu + visible: false + menu_model_name: "addMenu"+presetView + } + XsMenuModelItem { + property var type: "Versions" + text: type+" Group" + menuPath: "" + menuItemPosition: 1 + menuModelName: addMenu.menu_model_name + onActivated: { + addGroupItem(type) + } + } + XsMenuModelItem { + property var type: "Notes" + text: type+" Group" + menuPath: "" + menuItemPosition: 2 + menuModelName: addMenu.menu_model_name + onActivated: { + addGroupItem(type) + } + } + XsMenuModelItem { + property var type: "Playlists" + text: type+" Group" + menuPath: "" + menuItemPosition: 3 + menuModelName: addMenu.menu_model_name + onActivated: { + addGroupItem(type) + } + } + + + XsPopupMenu { + id: moreMenu + visible: false + menu_model_name: "moreMenu"+presetView + } + + XsMenuModelItem { + text: qsTr("Only Show Favourites") + menuPath: "" + menuItemType: "toggle" + menuItemPosition: 0.5 + menuModelName: moreMenu.menu_model_name + onActivated: prefs.showOnlyFavourites = !prefs.showOnlyFavourites + isChecked: prefs.showOnlyFavourites + } + + XsMenuModelItem { + text: "Undo" + menuPath: "" + menuItemPosition: 1 + menuModelName: moreMenu.menu_model_name + onActivated: ShotBrowserEngine.undo() + } + + XsMenuModelItem { + text: "Redo" + menuPath: "" + menuItemPosition: 2 + menuModelName: moreMenu.menu_model_name + onActivated: ShotBrowserEngine.redo() + } + + XsMenuModelItem { + menuItemType: "divider" + menuPath: "" + menuItemPosition: 3 + menuModelName: moreMenu.menu_model_name + } + + // XsMenuModelItem { + // menuItemType: "divider" + // menuPath: "" + // menuItemPosition: 3 + // menuModelName: moreMenu.menu_model_name + // } + XsMenuModelItem { + text: "Backup Presets..." + menuPath: "" + menuItemPosition: 3.5 + menuModelName: moreMenu.menu_model_name + onActivated: dialogHelpers.showFileDialog( + function(fileUrl, undefined, func) { + if(fileUrl) + Future.promise( + ShotBrowserEngine.presetsModel.backupPresetsFuture( + fileUrl + ) + ).then(function(string) { + dialogHelpers.errorDialogFunc("Backup Presets", "Backup Presets complete.\n\n" + string) + }, + function(err) { + dialogHelpers.errorDialogFunc("Backup Presets", "Backup Presets failed.\n\n" + err) + } + ) + }, + file_functions.defaultSessionFolder(), + "Backup Presets", + "json", + ["JSON (*.json)"], + false, + false + ) + } + + XsMenuModelItem { + text: "Restore Presets..." + menuPath: "" + menuItemPosition: 3.6 + menuModelName: moreMenu.menu_model_name + onActivated: dialogHelpers.showFileDialog( + function(fileUrl, button, func) { + if(fileUrl) + Future.promise( + ShotBrowserEngine.presetsModel.restorePresetsFuture( + fileUrl + ) + ).then(function(string) { + dialogHelpers.errorDialogFunc("Restore Presets", "Restore Presets complete.\n\n" + string) + }, + function(err) { + dialogHelpers.errorDialogFunc("Restore Presets", "Restore Presets failed.\n\n" + err) + } + ) + }, + file_functions.defaultSessionFolder(), + "Backup Presets", + "json", + ["JSON (*.json)"], + true, + false + ) + } + + XsMenuModelItem { + text: "Export As System Presets..." + menuPath: "" + menuItemPosition: 4 + menuModelName: moreMenu.menu_model_name + onActivated: dialogHelpers.showFileDialog( + function(fileUrl, undefined, func) { + if(fileUrl) + Future.promise( + ShotBrowserEngine.presetsModel.exportAsSystemPresetsFuture( + fileUrl + ) + ).then(function(string) { + dialogHelpers.errorDialogFunc("Export As System Presets", "Export As System Presets complete.\n\n" + string) + }, + function(err) { + dialogHelpers.errorDialogFunc("Export As System Presets", "Export As System Presets failed.\n\n" + err) + } + ) + }, + file_functions.defaultSessionFolder(), + "Export Presets", + "json", + ["JSON (*.json)"], + false, + false + ) + } + + + XsMenuModelItem { + text: "Reload All System Presets" + menuPath: "" + menuItemPosition: 5 + menuModelName: moreMenu.menu_model_name + onActivated: { + // set all indexs to visible. + let hidden = ShotBrowserEngine.presetsModel.searchRecursiveList( + true, "hiddenRole", + ShotBrowserEngine.presetsModel.index(-1,-1), + 0,-1 + ) + for(let i=0; i a.row - b.row) + for(let i=0; i < ordered.length; i++) { + if(ordered[i] == presetModelIndex()) + break + offset += 1 + } + + thisItem.y = draggingY + (offset * (btnHeight-1)) - parent.contentY + (btnHeight/2) + } + } + + onItemDraggingChanged: { + if(itemDragging) { + thisItem.x = height * 2 + oldY = mapToItem(thisItem.parent, 0, 0).y + oldParent = parent + thisItem.parent = thisItem.parent.parent + } else if(oldParent) { + parent = oldParent + thisItem.y = oldY + thisItem.x = height + oldParent = null + } + } + + Connections { + target: selectionModel + function onSelectionChanged(selected, deselected) { + isSelected = selectionModel.isSelected(presetModelIndex()) + } + } + + // TapHandler { + // acceptedModifiers: Qt.NoModifier + // onSingleTapped: { + // let g = mapToGlobal(0,0) + // control.tapped(Qt.LeftButton, g.x, g.y, Qt.NoModifier) + // } + // } + + // TapHandler { + // acceptedModifiers: Qt.ShiftModifier + // onSingleTapped: { + // let g = mapToGlobal(0,0) + // control.tapped(Qt.LeftButton, g.x, g.y, Qt.ShiftModifier) + // } + // } + + // TapHandler { + // acceptedModifiers: Qt.ControlModifier + // onSingleTapped: { + // let g = mapToGlobal(0,0) + // control.tapped(Qt.LeftButton, g.x, g.y, Qt.ControlModifier) + // } + // } + + Item { + id: dummy + } + + DragHandler { + cursorShape: Qt.PointingHandCursor + xAxis.enabled: false + target: dummy + + dragThreshold: 5 + + onTranslationChanged: { + let offset = Math.floor(translation.y / (btnHeight-1)) + let row_count = filterModelIndex().model.rowCount(filterModelIndex().parent) + + if(filterModelIndex().row + offset < 0) { + draggingOffset = -(filterModelIndex().row+1) + } + else if(filterModelIndex().row + offset > row_count-1){ + draggingOffset = (row_count - 1 - filterModelIndex().row) + } else { + draggingOffset = offset + } + } + onActiveChanged: { + if(active) { + // primary drag + draggingOffset = 0 + draggingY = mapToItem(thisItem.parent, 0, 0).y + isDragging = true + } else { + isDragging = false + if(draggingOffset) { + // need to move stuff.. + let pis = [] + let ordered = [].concat(selectionModel.selectedIndexes) + ordered.sort((a,b) => a.row - b.row) + for(let i = 0; i< ordered.length; i++) + pis.push(helpers.makePersistent(ordered[i])) + + // have list of persistent indexes. + let destRow = (presetModelIndex().row+draggingOffset) +1 + + + if(draggingOffset < 0) { + for(let i = 0;i < pis.length; i++) { + ShotBrowserEngine.presetsModel.moveRows( + pis[i].parent, + pis[i].row, + 1, + pis[i].parent, + destRow + i + ) + } + } else { + for(let i = 0;i < pis.length; i++) { + ShotBrowserEngine.presetsModel.moveRows( + pis[i].parent, + pis[i].row, + 1, + pis[i].parent, + destRow + ) + } + } + } + } + } + } + + MouseArea { + id: ma + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: (mouse) => { + if (mouse.button == Qt.RightButton && selectionModel.isSelected(presetModelIndex())) { + showPresetMenu(ma, mouseX, mouseY) //btnHeight - is the spacing at left for Presets apart from UIDataModels + } else { + if(mouse.modifiers == Qt.NoModifier) { + resultViewTitle = groupName+" : "+nameRole + selectionModel.select(presetModelIndex(), ItemSelectionModel.ClearAndSelect) + activatePreset(presetModelIndex()) + } else if(mouse.modifiers == Qt.ShiftModifier){ + ShotBrowserHelpers.shiftSelectItem(selectionModel, presetModelIndex()) + } else if(mouse.modifiers == Qt.ControlModifier) { + ShotBrowserHelpers.ctrlSelectItem(selectionModel, presetModelIndex()) + } + + if (mouse.button == Qt.RightButton) { + showPresetMenu(ma, mouseX, mouseY) //btnHeight - is the spacing at left for Presets apart from UIDataModels + } + } + } + } + + function presetModelIndex() { + try { + let fi = filterModelIndex() + return fi.model.mapToSource(fi) + } catch (err) { + return helpers.qModelIndex() + } + } + + function filterModelIndex() { + return delegateModel.mapRowToModel(index) + } + + function entityType() { + let i = presetModelIndex().parent.parent + return i.model.get(i,"entityRole") + } + + Rectangle{ id: selectedBgDiv + anchors.fill: parent + color: isSelected ? Qt.darker(palette.highlight, 2): "transparent" + // opacity: 0.6 + } + + Rectangle{ id: activeIndicatorDiv + anchors.bottom: parent.bottom + width: borderWidth*9 + height: parent.height + color: isActive ? bgColorPressed : "transparent" + } + + RowLayout{ + anchors.fill: parent + spacing: 1 + + XsText{ id: nameDiv + Layout.fillWidth: true + Layout.minimumWidth: 50 + Layout.preferredWidth: 100 + Layout.fillHeight: true + + text: isModified? nameRole+"*" : nameRole + font.weight: isSelected? Font.Bold : Font.Normal + horizontalAlignment: Text.AlignLeft + leftPadding: busyIndicator.width //25 + elide: Text.ElideRight + } + + XsSecondaryButton{ id: editBtn + Layout.topMargin: 1 + Layout.bottomMargin: 1 + Layout.preferredWidth: height + Layout.fillHeight: true + + visible: thisItem.hovered || editBtn.isActive + + imgSrc: "qrc:///shotbrowser_icons/edit.svg" + isActive: presetEditPopup.presetIndex == presetModelIndex() && presetEditPopup.visible + scale: 0.95 + + onClicked: { + openEditPopup() + } + } + + XsSecondaryButton{ id: moreBtn + Layout.topMargin: 1 + Layout.bottomMargin: 1 + Layout.preferredWidth: height + Layout.fillHeight: true + + visible: thisItem.hovered || moreBtn.isActive || editBtn.isActive + + imgSrc: "qrc:/icons/more_vert.svg" + scale: 0.95 + isActive: presetMenu.visible && presetMenu.presetModelIndex == presetModelIndex() + onClicked:{ + if(presetMenu.visible) { + presetMenu.visible = false + } + else{ + showPresetMenu(moreBtn, width/2, height/2) + } + } + } + + XsSecondaryButton{ id: favBtn + Layout.topMargin: 1 + Layout.bottomMargin: 1 + Layout.preferredWidth: height + Layout.fillHeight: true + + visible: thisItem.hovered || favouriteRole || editBtn.isActive + + showHoverOnActive: favouriteRole && !thisItem.hovered + isColoured: favouriteRole// && thisItem.hovered + imgSrc: "qrc:///shotbrowser_icons/favorite.svg" + scale: 0.95 + opacity: groupFavouriteRole ? 1.0 : 0.5 + onClicked: favouriteRole = !favouriteRole + } + + } + + + function openEditPopup(){ + + if( presetEditPopup.visible == true && presetEditPopup.entityName == nameRole) { + presetEditPopup.visible = false + return + } + + presetEditPopup.title = "Edit '"+nameRole+"' Preset" + presetEditPopup.entityName = nameRole + presetEditPopup.entityCategory = "Preset" + + if(!presetEditPopup.visible){ + presetEditPopup.x = appWindow.x + appWindow.width/3 + presetEditPopup.y = appWindow.y + appWindow.height/4 + } + + presetEditPopup.visible = true + presetEditPopup.presetIndex = helpers.makePersistent(presetModelIndex()) + presetEditPopup.entityType = entityType() + } + + function showPresetMenu(refItem, xpos, ypos){ + + presetMenu.presetModelIndex = presetModelIndex() + presetMenu.filterModelIndex = filterModelIndex() + if(!isSelected) { + selectionModel.select(presetModelIndex(), ItemSelectionModel.Select) + } + + presetMenu.showMenu( + refItem, + xpos, + ypos); + + } + + XsBusyIndicator{ id: busyIndicator + x: nameDiv.x + 4 + width: height + height: parent.height + running: visible + visible: isRunning + scale: 0.5 + } + +} + diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetEditItem.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetEditItem.qml new file mode 100644 index 000000000..bec79c13a --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetEditItem.qml @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.models 1.0 + +Rectangle{ + id: thisItem + + color: "transparent"// XsStyleSheet.widgetBgNormalColor + + property var delegateModel: null + property var termModel: [] + property var termValueRole: valueRole + // property var isSelected: false + property bool isSelected: termSelection.isSelected(helpers.makePersistent(delegateModel.notifyModel.mapRowToModel(index))) + + onTermValueRoleChanged: { + if(valueBox.count && valueBox.currentText != valueRole) { + let i = valueBox.find(valueRole) + if(i != -1) + valueBox.currentIndex = i + else { + valueBox.editText = valueRole + } + } + } + + Connections { + target: termSelection + function onSelectionChanged(selected, deselected) { + isSelected = termSelection.isSelected(helpers.makePersistent(delegateModel.notifyModel.mapRowToModel(index))) + } + } + + function setTermValue(value) { + valueRole = value + } + + Item{ id: rowItems + anchors.fill: parent + + Rectangle{ id: selectedBgDiv + anchors.fill: parent + color: isSelected ? Qt.darker(palette.highlight, 2): "transparent" + // opacity: 0.6 + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: (height+1) * delegateModel.notifyModel.depthAtRow(index) + spacing: 1 + + opacity: enabledRole && parentEnabledRole ? 1.0 : 0.5 + + MouseArea { + Layout.minimumWidth: 9 + Layout.maximumWidth: 9 + Layout.fillHeight: true + + cursorShape: Qt.PointingHandCursor + + onClicked: (mouse)=> { + if( mouse.modifiers == Qt.NoModifier ) { + termSelection.select(helpers.makePersistent(delegateModel.notifyModel.mapRowToModel(index)), ItemSelectionModel.ClearAndSelect) + } else if(mouse.modifiers == Qt.ShiftModifier ) { + if(termSelection.selectedIndexes.length) { + // find last selected entry ? + let m = termSelection.selectedIndexes[termSelection.selectedIndexes.length-1] + let s = Math.min(delegateModel.notifyModel.mapRowToModel(index).row, m.row) + let e = Math.max(delegateModel.notifyModel.mapRowToModel(index).row, m.row) + let items = [] + + for(let i=s; i<=e; i++) { + items.push(termSelection.model.index(i, 0, delegateModel.notifyModel.mapRowToModel(index).parent)) + } + termSelection.select(helpers.createItemSelection(items), ItemSelectionModel.ClearAndSelect) + } else { + termSelection.select(helpers.makePersistent(delegateModel.notifyModel.mapRowToModel(index)), ItemSelectionModel.ClearAndSelect) + } + } else if(mouse.modifiers == Qt.ControlModifier ) { + termSelection.select(helpers.makePersistent(delegateModel.notifyModel.mapRowToModel(index)), ItemSelectionModel.Toggle) + } + } + + Rectangle { + anchors.fill: parent + color: isSelected ? palette.highlight : XsStyleSheet.widgetBgNormalColor + } + } + + XsPrimaryButton { id: checkBox + Layout.minimumWidth: height + Layout.maximumWidth: height + Layout.fillHeight: true + isActive: enabledRole + onClicked: enabledRole = !enabledRole + imgSrc: enabledRole ? "qrc:/icons/check.svg" : "" + isActiveViaIndicator: true + activeIndicator.visible: false + } + + XsPrimaryButton{ id: expandButton + Layout.minimumWidth: height + Layout.maximumWidth: height + Layout.fillHeight: true + + imgSrc: "qrc:/icons/chevron_right.svg" + visible: termRole == "Operator" + isActive: {let i = delegateModel.notifyModel.isExpanded(index) ? true:false;i} + imageDiv.rotation: isActive ? 90 : 0 + + onClicked: { + if(isActive) + delegateModel.notifyModel.collapseRow(index) + else + delegateModel.notifyModel.expandRow(index) + isActive = !isActive + } + } + + XsPrimaryButton{ id: insertButton + Layout.minimumWidth: height + Layout.maximumWidth: height + Layout.fillHeight: true + imgSrc: "qrc:/icons/add.svg" + isActiveViaIndicator: false + visible: termRole == "Operator" + onClicked: { + termTypeMenu.termModelIndex = helpers.makePersistent(delegateModel.notifyModel.mapRowToModel(index)) + termTypeMenu.showMenu( + insertButton, + width/2, + height/2); + + if(expandButton.isActive == false) + expandButton.clicked() + } + } + + XsComboBox { + // Layout.fillWidth: true + Layout.preferredWidth: termWidth - (termRole == "Operator" ? (height+1)*2 : 0) + Layout.fillHeight: true + model: termModel + + currentIndex: this.count ? find(termRole) : -1 + + onActivated: (aindex) => { + if(termRole != textAt(aindex)) { + let ti = thisItem.delegateModel.notifyModel.mapRowToModel(index) + let r = ti.row+1 + + ShotBrowserEngine.presetsModel.insertTerm(textAt(aindex), ti.row, ti.parent) + ShotBrowserEngine.presetsModel.removeRows(r, 1, ti.parent) + } + } + } + + XsPrimaryButton{ id: equationBtn + property bool isLiveLink: livelinkRole != undefined && livelinkRole + property bool isNegate: negatedRole != undefined && negatedRole + property bool isEqual: !isLiveLink && !isNegate + property bool equalMenuEnabled: !isEqual + Layout.preferredWidth: height + Layout.fillHeight: true + + // anchors.fill: parent + imgSrc: + isLiveLink? "qrc:/icons/link.svg" : + isNegate? "qrc:/shotbrowser_icons/exclamation.svg" : + "qrc:/shotbrowser_icons/equal.svg" + isActiveViaIndicator: false + isActive: !isEqual + onClicked:{ + if(equationMenu.visible) + equationMenu.visible = false + else { + equationMenu.termModelIndex = delegateModel.notifyModel.mapRowToModel(index) + equationMenu.showMenu( + equationBtn, + width/2, + height/2); + } + } + } + + XsComboBoxEditable { + id: valueBox + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: btnWidth*4.2 + + model: ShotBrowserEngine.presetsModel.termModel(termRole, entityType, projectId) + enabled: !livelinkRole + textRole: "nameRole" + currentIndex: -1 + + onCountChanged: { + if(count && currentText != termValueRole) { + let i = find(termValueRole) + if(i != -1) { + currentIndex = i + } + else { + // inject value into model. + model.insertRowsData(0,1, model.index(-1,-1), {"name": termValueRole}) + currentIndex = 0 + } + } + } + + onAccepted: { + if (find(editText) === -1) { + setTermValue(editText) + model.insertRowsData(0, 1, model.index(-1,-1), {"name": editText}) + currentIndex = 0 + } else { + currentIndex = find(editText) + setTermValue(textAt(currentIndex)) + } + focus = false + } + + onActivated: (aindex) => { + setTermValue(textAt(aindex)) + } + } + XsPrimaryButton{ + Layout.preferredWidth: closeWidth + Layout.fillHeight: true + imgSrc: "qrc:/icons/close.svg" + onClicked: { + // map to real model.. + let i = delegateModel.model.mapRowToModel(index) + delegateModel.model.model.removeRows(i.row, 1, i.parent) + } + } + + XsPrimaryButton{ id: moreBtn + Layout.minimumWidth: height + Layout.maximumWidth: height + Layout.fillHeight: true + + imgSrc: "qrc:/icons/more_vert.svg" + // scale: 0.95 + isActive: termMenu.visible && isSelected + onClicked:{ + if(termMenu.visible) { + termMenu.visible = false + } + else{ + if(!isSelected) + termSelection.select(delegateModel.notifyModel.mapRowToModel(index), ItemSelectionModel.ClearAndSelect) + termMenu.showMenu(moreBtn, width/2, height/2) + } + } + } + } + } +} + diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetEditNewItem.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetEditNewItem.qml new file mode 100644 index 000000000..d7d8b259f --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetEditNewItem.qml @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 + +Rectangle { + id: control + property var termModel: null + color: XsStyleSheet.widgetBgNormalColor + + RowLayout { + anchors.fill: parent + spacing: 1 + + // Item{ + // Layout.maximumWidth: dragWidth + // Layout.minimumWidth: dragWidth + // Layout.fillHeight: true + // } + Item { + Layout.maximumWidth: 9+control.height+2 + Layout.minimumWidth: 9+control.height+2 + Layout.fillHeight: true + } + + XsComboBox { + Layout.maximumWidth: termWidth + Layout.minimumWidth: termWidth + Layout.preferredHeight: control.height + + model: termModel + displayText: currentIndex == -1? "Add Term..." : currentText + + currentIndex: -1 + onModelChanged: { + // console.log("new item", "onModelChanged", model) + currentIndex = -1 + } + + // don't use onCurrentIndex changed! As that'll get programatic changes as well. + onActivated: { + if(index != -1) { + let row = ShotBrowserEngine.presetsModel.rowCount(presetIndex) + let i = ShotBrowserEngine.presetsModel.insertTerm( + textAt(index), + row, + presetIndex + ) + + if(i.valid) { + let t = ShotBrowserEngine.presetsModel.get(i, "termRole") + let tm = ShotBrowserEngine.presetsModel.termModel(t, entityType, projectId) + if(tm.length && tm.get(tm.index(0,0), "nameRole") == "True") { + ShotBrowserEngine.presetsModel.set(i, "True", "valueRole") + } + } + currentIndex = -1 + } + } + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + } +} + diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetEditPopup.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetEditPopup.qml new file mode 100644 index 000000000..f6e72e8fa --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetEditPopup.qml @@ -0,0 +1,440 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.clipboard 1.0 + +XsWindow{ + id: presetEditPopup + + property var presetIndex: helpers.qModelIndex() + property string entityType: "Versions" + property string entityName: "" + property string entityCategory: "Group" + + property int itemHeight: 24 + + property int dragWidth: btnWidth/2 + property int termWidth: btnWidth*4 + property int modeWidth: itemHeight + property int closeWidth: btnWidth + + width: 460 + height: 40 + 200 + (nameDiv.height + coln.spacing*2) //+ presetList.height + // transientParent: parent + // remove focus from text widgets. + onClosing: coln.focus = true + + onPresetIndexChanged: { + if(presetIndex.valid) { + presetTermModel.model = null + presetTermModel.model = ShotBrowserEngine.presetsModel + presetTermModel.rootIndex = presetIndex + presetList.model.newTermParent = presetIndex + } + } + + QTreeModelToTableModel { + id: presetTermModel + model: ShotBrowserEngine.presetsModel + } + + ItemSelectionModel { + id: termSelection + model: ShotBrowserEngine.presetsModel + } + + ColumnLayout { id: coln + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + Item{ id: nameDiv + Layout.fillWidth: true + Layout.preferredHeight: itemHeight*1.2 + + RowLayout { + width: parent.width + height: parent.height + spacing: 1 + + Item{ + Layout.preferredWidth: dragWidth + Layout.fillHeight: true + } + Item { + Layout.preferredWidth: height + Layout.fillHeight: true + } + XsText{ + Layout.preferredWidth: termWidth + Layout.fillHeight: true + text: entityCategory+" name:" + } + Item { + Layout.preferredWidth: modeWidth + Layout.fillHeight: true + } + XsTextField{ id: nameEditDiv + Layout.fillWidth: true + Layout.preferredWidth: btnWidth*4.2 + Layout.fillHeight: true + text: entityName + placeholderText: entityName + onEditingFinished: { + if(ShotBrowserEngine.presetsModel.get(presetIndex.parent,"typeRole") == "presets") + ShotBrowserEngine.presetsModel.set(presetIndex, text, "nameRole") + else + ShotBrowserEngine.presetsModel.set(presetIndex.parent, text, "nameRole") + } + + background: + Rectangle{ + color: nameEditDiv.activeFocus? Qt.darker(palette.highlight, 1.5): nameEditDiv.hovered? Qt.lighter(palette.base, 2):Qt.lighter(palette.base, 1.5) + border.width: nameEditDiv.hovered || nameEditDiv.active? 1:0 + border.color: palette.highlight + opacity: enabled? 0.7 : 0.3 + } + } + Item{ + Layout.preferredWidth: closeWidth + itemHeight + 2 + Layout.fillHeight: true + } + XsPrimaryButton{ id: moreBtn + Layout.minimumWidth: height + Layout.maximumWidth: height + Layout.fillHeight: true + + imgSrc: "qrc:/icons/more_vert.svg" + // scale: 0.95 + isActive: termMenu.visible + onClicked:{ + if(termMenu.visible) + termMenu.visible = false + else + termMenu.showMenu(moreBtn, width/2, height/2) + } + } + } + } + + XsListView{ id: presetList + Layout.fillWidth: true + Layout.fillHeight: true + + property int rightSpacing: height < contentHeight ? 14 : 0 + Behavior on rightSpacing {NumberAnimation {duration: 150}} + + ScrollBar.vertical: XsScrollBar { + visible: presetList.height < presetList.contentHeight + parent: presetList + anchors.top: presetList.top + anchors.right: presetList.right + anchors.bottom: presetList.bottom + x: -5 + } + + model: DelegateModel { + id: presetDelegateModel + property var notifyModel: presetTermModel.rootIndex.valid ? presetTermModel : [] + model: notifyModel + + property var newTermParent: null + + delegate: XsSBPresetEditItem{ + width: presetList.width - presetList.rightSpacing + height: itemHeight + termModel: ShotBrowserEngine.presetsModel.termLists[entityType] + delegateModel: presetDelegateModel + } + } + + interactive: true + spacing: 1 + + footer: Item{ + width: presetList.width - presetList.rightSpacing + height: itemHeight + XsSBPresetEditNewItem{ + anchors.fill: parent + anchors.topMargin: 1 + termModel: ShotBrowserEngine.presetsModel.termLists[entityType] + } + } + } + + RowLayout{ + Layout.fillWidth: true + Layout.maximumHeight: itemHeight + Layout.minimumHeight: itemHeight + + Item{ + Layout.preferredWidth: presetEditPopup.width / 3 * 2 + Layout.fillHeight: true + } + + XsPrimaryButton{ + Layout.fillWidth: true + Layout.fillHeight: true + text: "Close" + onClicked: { + close() + } + } + } + } + + XsPopupMenu { + id: equationMenu + visible: false + menu_model_name: "equationMenu"+presetEditPopup + property var termModelIndex: helpers.qModelIndex() + + property var isLiveLink: ShotBrowserEngine.presetsModel.get(termModelIndex, "livelinkRole") + property var isNegate: ShotBrowserEngine.presetsModel.get(termModelIndex, "negatedRole") + property bool isEqual: !isLiveLink && !isNegate + + XsMenuModelItem { + text: "Equals" + menuPath: "" + menuModelName: equationMenu.menu_model_name + enabled: !equationMenu.isEqual + + onActivated: { + if(equationMenu.isNegate != undefined && equationMenu.isNegate) + ShotBrowserEngine.presetsModel.set(equationMenu.termModelIndex, false, "negatedRole") + if(equationMenu.isLiveLink != undefined && equationMenu.isLiveLink) + ShotBrowserEngine.presetsModel.set(equationMenu.termModelIndex, false, "livelinkRole") + } + } + XsMenuModelItem { + text: "Negates" + menuPath: "" + menuModelName: equationMenu.menu_model_name + enabled: equationMenu.isNegate != undefined && !equationMenu.isNegate + + onActivated: { + if(equationMenu.isLiveLink != undefined && equationMenu.isLiveLink) + ShotBrowserEngine.presetsModel.set(equationMenu.termModelIndex, false, "livelinkRole") + ShotBrowserEngine.presetsModel.set(equationMenu.termModelIndex, true, "negatedRole") + } + } + XsMenuModelItem { + text: "Live Link" + menuPath: "" + menuModelName: equationMenu.menu_model_name + enabled: equationMenu.isLiveLink != undefined && !equationMenu.isLiveLink + + onActivated: { + ShotBrowserEngine.presetsModel.set(equationMenu.termModelIndex, true, "livelinkRole") + + if(equationMenu.isNegate != undefined && equationMenu.isNegate) + ShotBrowserEngine.presetsModel.set(equationMenu.termModelIndex, false, "negatedRole") + } + } + } + + XsPopupMenu { + id: termTypeMenu + visible: false + menu_model_name: "termTypeMenu"+presetEditPopup + property var termModelIndex: helpers.qModelIndex() + + Repeater { + model: ShotBrowserEngine.presetsModel.termLists[entityType] + Item { + XsMenuModelItem { + menuItemType: "button" + text: modelData + menuPath: "" + menuItemPosition: index + menuModelName: termTypeMenu.menu_model_name + onActivated: { + let row = ShotBrowserEngine.presetsModel.rowCount(termTypeMenu.termModelIndex) + let i = ShotBrowserEngine.presetsModel.insertTerm( + modelData, + row, + termTypeMenu.termModelIndex + ) + + if(i.valid) { + let t = ShotBrowserEngine.presetsModel.get(i, "termRole") + let tm = ShotBrowserEngine.presetsModel.termModel(t, entityType, projectId) + if(tm.length && tm.get(tm.index(0,0), "nameRole") == "True") { + ShotBrowserEngine.presetsModel.set(i, "True", "valueRole") + } + } + } + } + } + } + } + + XsPopupMenu { + id: termMenu + visible: false + menu_model_name: "termMenu"+presetEditPopup + + Clipboard{ + id: clipboard + } + + XsMenuModelItem { + text: "Move Up" + menuPath: "" + menuModelName: termMenu.menu_model_name + menuItemPosition: 1 + enabled: { + let si = termSelection.selectedIndexes + for(let i = 0; i < si.length; i++) { + if(si[i].row) + return true + } + return false + } + onActivated: { + let ordered = [].concat(termSelection.selectedIndexes) + ordered.sort((a,b) => a.row - b.row) + + for(let i = 0; i < ordered.length; i++) { + if(ordered[i].row) + ShotBrowserEngine.presetsModel.moveRows(ordered[i].parent, ordered[i].row, 1, ordered[i].parent, ordered[i].row-1) + } + } + } + XsMenuModelItem { + text: "Move Down" + menuPath: "" + menuModelName: termMenu.menu_model_name + menuItemPosition: 2 + enabled: { + let si = termSelection.selectedIndexes + for(let i = 0; i < si.length; i++) { + let rc = ShotBrowserEngine.presetsModel.rowCount(si[i].parent)-1 + if(si[i].row < rc) + return true + } + return false + } + onActivated: { + let ordered = [].concat(termSelection.selectedIndexes) + ordered.sort((b,a) => a.row - b.row) + + for(let i = 0; i < ordered.length; i++) { + if(ordered[i].row < ShotBrowserEngine.presetsModel.rowCount(ordered[i].parent)-1) + ShotBrowserEngine.presetsModel.moveRows(ordered[i].parent, ordered[i].row, 1, ordered[i].parent, ordered[i].row+2) + } + } + } + XsMenuModelItem { + menuItemType: "divider" + menuPath: "" + menuItemPosition: 3 + menuModelName: termMenu.menu_model_name + } + + XsMenuModelItem { + text: "Duplicate" + menuPath: "" + menuItemPosition: 4 + menuModelName: termMenu.menu_model_name + onActivated: { + let si = termSelection.selectedIndexes + for(let i = 0; i < si.length; i++) { + ShotBrowserEngine.presetsModel.duplicate(si[i]) + } + } + } + + XsMenuModelItem { + text: "Copy Terms" + menuItemPosition: 4.5 + menuPath: "" + menuModelName: termMenu.menu_model_name + onActivated: clipboard.text = JSON.stringify(ShotBrowserEngine.presetsModel.copy(termSelection.selectedIndexes)) + } + + XsMenuModelItem { + text: "Paste Terms" + menuItemPosition: 4.6 + menuPath: "" + menuModelName: termMenu.menu_model_name + onActivated: { + if(termSelection.selectedIndexes.length) { + ShotBrowserEngine.presetsModel.paste( + JSON.parse(clipboard.text), + termSelection.selectedIndexes[0].row+1, + termSelection.selectedIndexes[0].parent + ) + } else { + ShotBrowserEngine.presetsModel.paste( + JSON.parse(clipboard.text), + ShotBrowserEngine.presetsModel.rowCount(presetTermModel.rootIndex), + presetTermModel.rootIndex + ) + } + } + } + + XsMenuModelItem { + text: "Paste Values From Clipboard" + menuPath: "" + menuModelName: termMenu.menu_model_name + menuItemPosition: 5 + onActivated: { + let si = termSelection.selectedIndexes + + let values = clipboard.text.split("\n") + if(values.length) { + let first = values[0].trim() + values = values.reverse() + + for(let i = 0;i b.row - a.row) + + for(let i = 0; i < ordered.length; i++) { + ShotBrowserEngine.presetsModel.removeRows(ordered[i].row, 1, ordered[i].parent) + } + termSelection.clear() + } + } + } + + +} + diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetGroupDelegate.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetGroupDelegate.qml new file mode 100644 index 000000000..15af5e5bc --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetGroupDelegate.qml @@ -0,0 +1,417 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQml.Models +import Qt.labs.qmlmodels + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import ShotBrowser 1.0 + + +MouseArea { + id: thisItem + + property var delegateModel: null + property var selectionModel: null + property var expandedModel: null + + property bool isUserGroup: updateRole == undefined + property bool isRunning: queryRunning && presetModelIndex() == currentPresetIndex + // property bool isActive: isSelected //#TODO + property bool isExpanded: expandedModel.isSelected(presetModelIndex()) + property bool isSelected: selectionModel.isSelected(presetModelIndex()) + + property bool isParent: true + property bool isIconVisible: false + property bool isMouseHovered: groupOnlyMArea.containsMouse || addBtn.hovered || editBtn.hovered || moreBtn.hovered || favBtn.hovered + + property color itemColorActive: palette.highlight + property color itemColorNormal: XsStyleSheet.widgetBgNormalColor //palette.base + + hoverEnabled: true + + property bool itemDragging: isDragging && isSelected + property int itemDraggingOffset: itemDragging ? draggingOffset : 0 + + property int oldY: 0 + property var oldParent: null + + x: 0 + + function presetModelIndex() { + try { + let fi = filterModelIndex() + return fi.model.mapToSource(fi) + } catch (err) { + return helpers.qModelIndex() + } + } + + function filterModelIndex() { + return delegateModel.mapRowToModel(index) + } + + function entityType() { + let i = presetModelIndex() + return i.model.get(i,"entityRole") + } + + onClicked: (mouse) => { + if(mouse.modifiers == Qt.NoModifier) { + selectionModel.select(presetModelIndex(), ItemSelectionModel.ClearAndSelect) + } else if(mouse.modifiers == Qt.ShiftModifier){ + ShotBrowserHelpers.shiftSelectItem(selectionModel, presetModelIndex()) + } else if(mouse.modifiers == Qt.ControlModifier) { + ShotBrowserHelpers.ctrlSelectItem(selectionModel, presetModelIndex()) + } + } + + onItemDraggingOffsetChanged: { + + if(itemDragging) { + let offset = itemDraggingOffset + + // calculate offset based on position in selection. + let ordered = [].concat(selectionModel.selectedIndexes) + ordered.sort((a,b) => a.row - b.row) + for(let i=0; i < ordered.length; i++) { + if(ordered[i] == presetModelIndex()) + break + offset += 1 + } + + thisItem.y = draggingY + (offset * (btnHeight-1)) - parent.contentY + (btnHeight/2) + } + } + + onItemDraggingChanged: { + if(itemDragging) { + thisItem.x = height + oldY = mapToItem(thisItem.parent, 0, 0).y + oldParent = parent + thisItem.parent = thisItem.parent.parent + } else if(oldParent) { + parent = oldParent + thisItem.y = oldY + thisItem.x = 0 + oldParent = null + } + } + + Item { + id: dummy + } + + DragHandler { + cursorShape: Qt.PointingHandCursor + xAxis.enabled: false + target: dummy + + dragThreshold: 5 + + onTranslationChanged: { + let offset = Math.floor(translation.y / (btnHeight-1)) + let row_count = filterModelIndex().model.rowCount(filterModelIndex().parent) + + if(filterModelIndex().row + offset < 0) { + draggingOffset = -(filterModelIndex().row+1) + } + else if(filterModelIndex().row + offset > row_count-1){ + draggingOffset = (row_count - 1 - filterModelIndex().row) + } else { + draggingOffset = offset + } + } + onActiveChanged: { + if(active) { + // collapse all + expandedModel.clear() + // primary drag + draggingOffset = 0 + draggingY = mapToItem(thisItem.parent, 0, 0).y + isDragging = true + } else { + isDragging = false + if(draggingOffset) { + // need to move stuff.. + let pis = [] + let ordered = [].concat(selectionModel.selectedIndexes) + ordered.sort((a,b) => a.row - b.row) + for(let i = 0; i< ordered.length; i++) + pis.push(helpers.makePersistent(ordered[i])) + + // have list of persistent indexes. + let destRow = presetModelIndex().model.rowCount(presetModelIndex().parent) + let filteredDestRow = index + draggingOffset + 1 + let modelDestIndex = filterModelIndex().model.index(filteredDestRow, 0, filterModelIndex().parent) + if(modelDestIndex.valid) { + destRow = modelDestIndex.model.mapToSource(modelDestIndex).row + } + + // destRow will not be what we think it is due to filtering! + // we need to real row based off the filtered row. + + if(draggingOffset < 0) { + for(let i = 0;i < pis.length; i++) { + ShotBrowserEngine.presetsModel.moveRows( + pis[i].parent, + pis[i].row, + 1, + pis[i].parent, + destRow + i + ) + } + } else { + for(let i = 0;i < pis.length; i++) { + ShotBrowserEngine.presetsModel.moveRows( + pis[i].parent, + pis[i].row, + 1, + pis[i].parent, + destRow + ) + } + } + } + } + } + } + + Connections { + target: expandedModel + function onSelectionChanged(selected, deselected) { + isExpanded = expandedModel.isSelected(presetModelIndex()) + } + } + + Connections { + target: selectionModel + function onSelectionChanged(selected, deselected) { + isSelected = selectionModel.isSelected(presetModelIndex()) + } + } + + Rectangle{ id: nodeDiv + + property bool isDivSelected: false + property int slNumber: index+1 + + color: isSelected ? Qt.darker(palette.highlight, 2) : + isUserGroup ? Qt.lighter(palette.base, 1.7) : Qt.lighter(palette.base, 1.5) //XsStyleSheet.widgetBgNormalColor + + opacity: hiddenRole ? 0.5 : 1.0 + + border.width: 1 + border.color: isMouseHovered? itemColorActive : itemColorNormal + + anchors.fill: parent + + MouseArea{ id: groupOnlyMArea + anchors.fill: row + hoverEnabled: true + propagateComposedEvents: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: (mouse)=>{ + + if (mouse.button == Qt.RightButton) { + if(groupMenu.visible) { + groupMenu.visible = false + } + else{ + showGroupMenu(groupOnlyMArea, mouseX, mouseY) + } + } + + mouse.accepted = false + } + } + RowLayout{ id: row + spacing: 1 + anchors.fill: parent + + XsPrimaryButton{ id: expandButton + Layout.minimumWidth: height + Layout.maximumWidth: height + Layout.fillHeight: true + + imgSrc: "qrc:/icons/chevron_right.svg" + isActive: isExpanded + enabled: isParent + opacity: enabled? 1 : 0.5 + imageDiv.rotation: isExpanded? 90 : 0 + + onClicked: { + let ind = presetModelIndex() + // some wierd interaction.. + // if(presetsTreeModel.isExpanded(index)) + // presetsTreeModel.collapseRow(index) + // else + // presetsTreeModel.expandRow(index) + + expandedModel.select(presetModelIndex(), ItemSelectionModel.Toggle) + if(expandedModel.model.rowCount(ind.model.index(1, 0, ind))) + expandedModel.select(ind.model.index(1, 0, ind), ItemSelectionModel.Select) + } + } + + + XsText{ id: groupNameDiv + Layout.fillWidth: true + Layout.minimumWidth: 30 + // Layout.preferredWidth: 100 + Layout.fillHeight: true + + text: nameRole + "..." + color: Qt.lighter(XsStyleSheet.hintColor, 1.2) + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + font.pixelSize: XsStyleSheet.fontSize*1.2 + font.weight: isSelected? Font.Bold : Font.Normal + leftPadding: 5 + + // anchors.verticalCenter: parent.verticalCenter + // anchors.left: parent.left + // width: parent.width + + elide: Text.ElideRight + } + + XsText{ + Layout.fillWidth: true + Layout.fillHeight: true + + Layout.minimumWidth: 15 + Layout.preferredWidth: 50 + Layout.maximumWidth: 50 + + visible: !isMouseHovered && !(addBtn.isActive || editBtn.isActive || moreBtn.isActive) + + text: entityRole + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + color: Qt.lighter(XsStyleSheet.panelBgColor, 2.2) + elide: Text.ElideRight + } + + Item{ + Layout.preferredWidth: 1 + Layout.fillHeight: true + } + + XsSecondaryButton{ id: addBtn + Layout.topMargin: 1 + Layout.bottomMargin: 1 + Layout.minimumWidth: height + Layout.maximumWidth: height + Layout.fillHeight: true + + visible: isMouseHovered || addBtn.isActive || editBtn.isActive + + imgSrc: "qrc:/icons/add.svg" + scale: 0.95 + + onClicked: { + let i = ShotBrowserEngine.presetsModel.index(1, 0, presetModelIndex()) + ShotBrowserEngine.presetsModel.insertPreset(ShotBrowserEngine.presetsModel.rowCount(i), i) + let ind = presetModelIndex() + expandedModel.select(presetModelIndex(), ItemSelectionModel.Select) + expandedModel.select(ind.model.index(1, 0, ind), ItemSelectionModel.Select) + } + } + + XsSecondaryButton{ id: editBtn + Layout.topMargin: 1 + Layout.bottomMargin: 1 + Layout.minimumWidth: height + Layout.maximumWidth: height + Layout.fillHeight: true + visible: isMouseHovered || editBtn.isActive + + imgSrc: "qrc:///shotbrowser_icons/edit.svg" + scale: 0.95 + isActive: presetEditPopup.presetIndex == ShotBrowserEngine.presetsModel.index(0, 0, presetModelIndex()) && presetEditPopup.visible + + onClicked: { + openEditPopup() + } + } + + XsSecondaryButton{ id: moreBtn + Layout.topMargin: 1 + Layout.bottomMargin: 1 + Layout.minimumWidth: height + Layout.maximumWidth: height + Layout.fillHeight: true + visible: isMouseHovered || moreBtn.isActive || editBtn.isActive + + imgSrc: "qrc:/icons/more_vert.svg" + scale: 0.95 + isActive: groupMenu.visible && groupMenu.presetModelIndex == presetModelIndex() + onClicked:{ + if(groupMenu.visible) { + groupMenu.visible = false + } + else{ + showGroupMenu(moreBtn, width/2, height/2) + } + } + } + XsSecondaryButton{ id: favBtn + Layout.topMargin: 1 + Layout.bottomMargin: 1 + Layout.minimumWidth: height + Layout.maximumWidth: height + Layout.fillHeight: true + + visible: isMouseHovered || favouriteRole || editBtn.isActive + + showHoverOnActive: favouriteRole && !isMouseHovered + isColoured: favouriteRole// && isMouseHovered + imgSrc: "qrc:///shotbrowser_icons/favorite.svg" + // isActive: favouriteRole + scale: 0.95 + onClicked: favouriteRole = !favouriteRole + } + } + } + + + + function openEditPopup(){ + + if( presetEditPopup.visible == true && presetEditPopup.entityName == nameRole) { + presetEditPopup.visible = false + return + } + + presetEditPopup.title = "Edit '"+nameRole+"' Group" + presetEditPopup.entityName = nameRole + presetEditPopup.entityCategory = "Group" + + if(!presetEditPopup.visible){ + presetEditPopup.x = appWindow.x + appWindow.width/3 + presetEditPopup.y = appWindow.y + appWindow.height/4 + } + + presetEditPopup.visible = true + presetEditPopup.presetIndex = helpers.makePersistent(ShotBrowserEngine.presetsModel.index(0, 0, presetModelIndex())) + presetEditPopup.entityType = entityType() + } + + + function showGroupMenu(refitem, xpos, ypos){ + + groupMenu.presetModelIndex = presetModelIndex() + groupMenu.filterModelIndex = filterModelIndex() + + groupMenu.showMenu( + refitem, + xpos, + ypos); + + } + +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetsView.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetsView.qml new file mode 100644 index 000000000..eaec21ce5 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBPresetsView.qml @@ -0,0 +1,411 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic +import Qt.labs.qmlmodels + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.models 1.0 +import QuickFuture 1.0 + +XsListView { + id: control + + spacing:1 + + property int rightSpacing: control.height < control.contentHeight ? 12 : 0 + Behavior on rightSpacing {NumberAnimation {duration: 150}} + + ScrollBar.vertical: XsScrollBar { + visible: control.height < control.contentHeight + parent: control.parent + anchors.top: control.top + anchors.right: control.right + anchors.bottom: control.bottom + x: -5 + } + + property bool isDragging: false + property int draggingOffset: 0 + property int draggingY: 0 + + model: presetsTreeModel + + delegate: DelegateChooser { + id: chooser + role: "typeRole" + + DelegateChoice { + roleValue: "group"; + XsSBPresetGroupDelegate{ + width: control.width - control.rightSpacing + height: btnHeight-2 + delegateModel: control.model + expandedModel: presetsExpandedModel + selectionModel: presetsSelectionModel + } + } + DelegateChoice { + roleValue: "preset"; + XsSBPresetDelegate{ + width: control.width - height - control.rightSpacing + height: btnHeight-2 + delegateModel: control.model + selectionModel: presetsSelectionModel + } + } + DelegateChoice { + roleValue: "presets"; + Rectangle { + visible: false + width: control.width - control.rightSpacing + height: -1 + } + } + DelegateChoice { + roleValue: "term"; + Rectangle { + visible: true + width: control.width - control.rightSpacing + height: btnHeight-2 + } + } + } + + Connections { + target: presetsExpandedModel + function onSelectionChanged(selected, deselected) { + // console.log("presetsExpandedModel onSelectionChanged") + // update tree model with expand + // map to from.. ack.. + for(let i = 0; i a.row - b.row) + + for(let i=0; i < indexs.length; i++) { + let mi = indexs[i].model.mapToSource(indexs[i]) + let p = mi.parent + let rpi = indexs[i].model.mapToSource( + indexs[i].model.index(indexs[i].row-1, 0, indexs[i].parent) + ) + ShotBrowserEngine.presetsModel.moveRows(p, mi.row, 1, p, rpi.row) + } + } + } + + + XsMenuModelItem { + text: "Move Down" + menuItemPosition: 7 + menuPath: "" + menuModelName: presetMenu.menu_model_name + enabled: presetMenu.filterModelIndex && presetMenu.filterModelIndex ? presetMenu.filterModelIndex.row != presetMenu.filterModelIndex.model.rowCount(presetMenu.filterModelIndex.parent) - 1 : false + onActivated: { + // because we use a view, the previous item in the base model isn't + // the previous in the view.. + let indexs = [] + for(let i=0;i b.row - a.row) + + for(let i=0; i < indexs.length; i++) { + let mi = indexs[i].model.mapToSource(indexs[i]) + let p = mi.parent + let rpi = indexs[i].model.mapToSource( + indexs[i].model.index(indexs[i].row+1, 0, indexs[i].parent) + ) + ShotBrowserEngine.presetsModel.moveRows(p, mi.row, 1, p, rpi.row+1) + } + } + } + + // XsMenuModelItem { + // text: "Reset Preset" + // menuPath: "" + // menuModelName: presetMenu.menu_model_name + // enabled: { + // if(presetMenu.presetModelIndex) { + // let v = presetMenu.presetModelIndex.model.get(presetMenu.presetModelIndex,"updateRole") + // return v != undefined ? v : false + // } + // return false + // } + // onActivated: ShotBrowserEngine.presetsModel.resetPresets([presetMenu.presetModelIndex]) + // } + } + + XsPopupMenu { + id: groupMenu + + property var presetModelIndex: null + property var filterModelIndex: null + property var flags: force_update ? updateFlags() : updateFlags() + property bool force_update: false + + visible: false + menu_model_name: "groupMenu"+control + + function updateFlags() { + if(groupMenu.presetModelIndex != null) { + let tmp = ShotBrowserEngine.presetsModel.get(groupMenu.presetModelIndex, "flagRole") + if(tmp != undefined) + return tmp + } + return [] + } + + Component.onCompleted: { + // make sure the 'Add' sub-menu appears in the correct place + helpers.setMenuPathPosition("Behaviour", groupMenu.menu_model_name, 5.1) + } + + XsMenuModelItem { + text: "Duplicate Group" + menuItemPosition: 1 + menuPath: "" + menuModelName: groupMenu.menu_model_name + onActivated: groupMenu.presetModelIndex.model.duplicate(groupMenu.presetModelIndex) + } + + XsMenuModelItem { + text: "Copy Group" + menuItemPosition: 2 + menuPath: "" + menuModelName: groupMenu.menu_model_name + onActivated: clipboard.text = JSON.stringify(ShotBrowserEngine.presetsModel.copy([groupMenu.presetModelIndex])) + } + XsMenuModelItem { + text: "Paste Group" + menuItemPosition: 3 + menuPath: "" + menuModelName: groupMenu.menu_model_name + onActivated: ShotBrowserEngine.presetsModel.paste( + JSON.parse(clipboard.text), + groupMenu.presetModelIndex.row+1, + groupMenu.presetModelIndex.parent + ) + } + XsMenuModelItem { + text: "Remove Group" + menuItemPosition: 4 + menuPath: "" + menuModelName: groupMenu.menu_model_name + onActivated: { + let m = groupMenu.presetModelIndex.model + let sys = m.get(groupMenu.presetModelIndex, "updateRole") + if(sys != undefined) { + m.set(groupMenu.presetModelIndex, true, "hiddenRole") + } else { + ShotBrowserEngine.presetsModel.removeRows(groupMenu.presetModelIndex.row, 1, groupMenu.presetModelIndex.parent) + } + } + } + XsMenuModelItem { + menuItemPosition: 5 + menuItemType: "divider" + menuPath: "" + menuModelName: groupMenu.menu_model_name + } + + Repeater { + model: ShotBrowserEngine.presetsModel.groupFlags + + Item { + XsMenuModelItem { + text: modelData + menuItemType: "toggle" + menuPath: "Behaviour" + menuItemPosition: index + 1 + menuModelName: groupMenu.menu_model_name + isChecked: groupMenu.flags.includes(modelData) + + onActivated: { + if(isChecked) { + ShotBrowserEngine.presetsModel.set(groupMenu.presetModelIndex, Array.from(groupMenu.flags).filter(r => r !== modelData), "flagRole") + } else { + let tmp = groupMenu.flags + tmp.push(modelData) + ShotBrowserEngine.presetsModel.set(groupMenu.presetModelIndex, tmp, "flagRole") + } + groupMenu.force_update = !groupMenu.force_update + } + } + } + } + + XsMenuModelItem { + text: "Paste Preset" + menuItemPosition: 6 + menuPath: "" + menuModelName: groupMenu.menu_model_name + onActivated: { + ShotBrowserEngine.presetsModel.paste( + JSON.parse(clipboard.text), + ShotBrowserEngine.presetsModel.rowCount(ShotBrowserEngine.presetsModel.index(1,0,groupMenu.presetModelIndex)), + ShotBrowserEngine.presetsModel.index(1,0,groupMenu.presetModelIndex) + ) + } + } + + XsMenuModelItem { + menuItemPosition: 7 + menuItemType: "divider" + menuPath: "" + menuModelName: groupMenu.menu_model_name + } + + XsMenuModelItem { + text: "Move Up" + menuItemPosition: 8 + menuPath: "" + menuModelName: groupMenu.menu_model_name + enabled: groupMenu.filterModelIndex && groupMenu.filterModelIndex.row + onActivated: { + + // argh this is horridly complex.. + // because we use a view, the previous item in the base model isn't + // the previous in the view.. + let p = groupMenu.presetModelIndex.parent + let rpi = groupMenu.filterModelIndex.model.mapToSource( + groupMenu.filterModelIndex.model.index(groupMenu.filterModelIndex.row-1,0,groupMenu.filterModelIndex.parent) + ) + + let oldy = control.contentY + ShotBrowserEngine.presetsModel.moveRows(p, groupMenu.presetModelIndex.row, 1, p, rpi.row) + control.contentY = oldy + + + } + } + + XsMenuModelItem { + text: "Move Down" + menuItemPosition: 9 + menuPath: "" + menuModelName: groupMenu.menu_model_name + + enabled: groupMenu.filterModelIndex ? groupMenu.filterModelIndex.row != groupMenu.filterModelIndex.model.rowCount(groupMenu.filterModelIndex.parent) - 1 : false + onActivated: { + let p = groupMenu.presetModelIndex.parent + let rpi = groupMenu.filterModelIndex.model.mapToSource( + groupMenu.filterModelIndex.model.index(groupMenu.filterModelIndex.row+1,0,groupMenu.filterModelIndex.parent) + ) + let oldy = control.contentY + ShotBrowserEngine.presetsModel.moveRows(p, groupMenu.presetModelIndex.row, 1, p, rpi.row+1) + control.contentY = oldy + } + } + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBSequenceDelegate.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBSequenceDelegate.qml new file mode 100644 index 000000000..fb677faa3 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBSequenceDelegate.qml @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQml.Models +import Qt.labs.qmlmodels + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 + +MouseArea { + hoverEnabled: true + + property bool isHovered: nodeDivMArea.containsMouse + property bool isExpanded: isParent && delegateModel.isExpanded(index) + property bool isSelected: selectionModel.isSelected(delegateModel.index(index, 0)) + + property bool isParent: delegateModel.model.rowCount(delegateModel.mapRowToModel(index)) + + property bool showStatus: false + property bool showType: false + + property var delegateModel: null + property var selectionModel: null + + onClicked: (mouse) => { + if(mouse.modifiers == Qt.NoModifier) { + ShotBrowserHelpers.selectItem(selectionModel, delegateModel.index(index, 0)) + } else if(mouse.modifiers == Qt.ShiftModifier){ + ShotBrowserHelpers.shiftSelectItem(selectionModel, delegateModel.index(index, 0)) + } else if(mouse.modifiers == Qt.ControlModifier) { + ShotBrowserHelpers.ctrlSelectItem(selectionModel, delegateModel.index(index, 0)) + } else if(mouse.modifiers == Qt.AltModifier) { + isExpanded = isParent + ShotBrowserHelpers.altSelectItem(selectionModel, delegateModel.index(index, 0)) + } + } + + Connections { + target: delegateModel.model + function onFilterChanged() { + isParent = delegateModel.model.rowCount(delegateModel.mapRowToModel(index)) + } + } + + Connections { + target: selectionModel + function onSelectionChanged(selected, deselected) { + isSelected = selectionModel.isSelected(delegateModel.index(index, 0)) + } + } + + Item { + anchors.fill: parent + anchors.leftMargin: delegateModel.depthAtRow(index) * height/1.2 + + MouseArea{ id: nodeDivMArea + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + onClicked: (mouse)=> { + mouse.accepted = false + } + } + + Rectangle{ id: nodeDiv + color: isSelected ? Qt.darker(palette.highlight, 2) : "transparent" + border.width: 1 + border.color: isHovered? palette.highlight : "transparent" + anchors.fill: parent + + RowLayout{ + spacing: buttonSpacing + anchors.fill: parent + + XsSecondaryButton{ id: expandButton + + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.preferredWidth: height/1.2 + Layout.preferredHeight: parent.height + + imgSrc: "qrc:/icons/chevron_right.svg" + isActive: isExpanded + enabled: isParent + opacity: enabled? 1 : 0.5 + imageDiv.rotation: (isExpanded)? 90:0 + + onClicked: { + if(isExpanded) { + delegateModel.collapseRow(index) + isExpanded = delegateModel.isExpanded(index) + } + else { + delegateModel.expandRow(index) + isExpanded = delegateModel.isExpanded(index) + } + } + } + + XsText{ + Layout.fillWidth: true + Layout.preferredHeight: parent.height + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + + opacity: ["na", "del", "omt", "omtnto", "omtnwd"].includes(statusRole) ? 0.5 : 1.0 + + color: "hld" == statusRole ? "red" : palette.text + + text: nameRole + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + font.pixelSize: XsStyleSheet.fontSize*1.2 + elide: Text.ElideRight + leftPadding: 2 + } + + XsText{ + Layout.preferredHeight: parent.height + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + opacity: 0.5 + visible: showType + + text: subtypeRole ? subtypeRole : "" + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + font.pixelSize: XsStyleSheet.fontSize*1.2 + elide: Text.ElideRight + rightPadding: 8 + } + + XsText{ + Layout.preferredHeight: parent.height + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + opacity: 0.5 + visible: showStatus + text: statusRole ? statusRole : "" + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + font.pixelSize: XsStyleSheet.fontSize*1.2 + elide: Text.ElideRight + rightPadding: 8 + } + } + } + } +} + diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBShotDelegate.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBShotDelegate.qml new file mode 100644 index 000000000..88952bc76 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/left_sections/viewItems/XsSBShotDelegate.qml @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQml.Models +import Qt.labs.qmlmodels + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 + +MouseArea { + hoverEnabled: true + + property bool isHovered: nodeDivMArea.containsMouse + property bool isSelected: selectionModel.isSelected(delegateModel.index(index, 0)) + property bool showUnit: false + property bool showStatus: false + property bool showType: false + + property var delegateModel: null + property var selectionModel: null + + onClicked: (mouse) => { + if(mouse.modifiers == Qt.NoModifier) { + ShotBrowserHelpers.selectItem(selectionModel, delegateModel.index(index, 0)) + } else if(mouse.modifiers == Qt.ShiftModifier){ + ShotBrowserHelpers.shiftSelectItem(selectionModel, delegateModel.index(index, 0)) + } else if(mouse.modifiers == Qt.ControlModifier) { + ShotBrowserHelpers.ctrlSelectItem(selectionModel, delegateModel.index(index, 0)) + } + } + + Connections { + target: selectionModel + function onSelectionChanged(selected, deselected) { + isSelected = selectionModel.isSelected(delegateModel.index(index, 0)) + } + } + + Rectangle { + id: treeNode + color: "transparent" + anchors.fill: parent + anchors.leftMargin: (delegateModel.depthAtRow(index)+1) * height/1.2 + + MouseArea{ id: nodeDivMArea + anchors.fill: nodeDiv + hoverEnabled: true + propagateComposedEvents: true + onClicked: (mouse)=> { + mouse.accepted = false + } + } + Rectangle{ id: nodeDiv + color: isSelected ? Qt.darker(palette.highlight, 2) : "transparent" + border.width: 1 + border.color: isHovered? palette.highlight : "transparent" + + anchors.fill: parent + + RowLayout{ + spacing: buttonSpacing + anchors.fill: parent + + XsText{ + Layout.fillWidth: true + Layout.preferredHeight: parent.height + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + + opacity: ["na", "del", "omt", "omtnto", "omtnwd"].includes(statusRole) ? 0.5 : 1.0 + + color: "hld" == statusRole ? "red" : palette.text + + text: nameRole + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + font.pixelSize: XsStyleSheet.fontSize*1.2 + elide: Text.ElideRight + leftPadding: 2 + } + XsText{ + Layout.preferredHeight: parent.height + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + opacity: 0.5 + visible: showUnit + + text: unitRole ? unitRole : "" + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + font.pixelSize: XsStyleSheet.fontSize*1.2 + elide: Text.ElideRight + rightPadding: 8 + } + + XsText{ + Layout.preferredHeight: parent.height + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + opacity: 0.5 + visible: showType + + text: subtypeRole ? subtypeRole : "" + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + font.pixelSize: XsStyleSheet.fontSize*1.2 + elide: Text.ElideRight + rightPadding: 8 + } + + XsText{ + Layout.preferredHeight: parent.height + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + opacity: 0.5 + visible: showStatus + + text: statusRole ? statusRole : "" + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + font.pixelSize: XsStyleSheet.fontSize*1.2 + elide: Text.ElideRight + rightPadding: 8 + } + } + } + } +} + diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBR1Tools.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBR1Tools.qml new file mode 100644 index 000000000..b8f1a3d1d --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBR1Tools.qml @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 +import ShotBrowser 1.0 + +ColumnLayout{ + id: toolDiv + spacing: panelPadding + + RowLayout{ + spacing: buttonSpacing*2 + Layout.fillWidth: true + Layout.minimumHeight: btnHeight + Layout.maximumHeight: btnHeight + + Repeater { + Layout.fillWidth: true + Layout.preferredHeight: btnHeight + model: DelegateModel { + id: delegate_model + property var notifyModel: currentCategory == "Tree" ? treeButtonModel : (currentCategory == "Recent" ? recentButtonModel : menuButtonModel) + model: notifyModel + delegate: XsPrimaryButton { + Layout.fillWidth: true + Layout.fillHeight: true + text: nameRole + toolTip: getPresetIndex().valid ? getPresetIndex().model.get(getPresetIndex().parent.parent, "nameRole") + " / " + nameRole : "" + + function getPresetIndex() { + let tindex = delegate_model.notifyModel.mapToSource(delegate_model.modelIndex(index)) + if(tindex.valid) + return tindex.model.mapToModel(tindex) + return tindex + } + property bool isRunning: queryRunning && isActive + + isActive: currentPresetIndex == getPresetIndex() + onClicked: { + activatePreset(getPresetIndex()) + presetsSelectionModel.select(getPresetIndex(), ItemSelectionModel.ClearAndSelect) + } + + XsBusyIndicator{ id: busyIndicator + x: 4 + width: height + height: parent.height + running: visible + visible: isRunning + scale: 0.5 + } + } + } + } + } + + Rectangle{ + Layout.fillWidth: true + Layout.preferredHeight: 2 + color: panelColor + } + + RowLayout{ + spacing: buttonSpacing + Layout.fillWidth: true + Layout.minimumHeight: btnHeight + Layout.maximumHeight: btnHeight + + XsSearchBar{ id: filterBtn + Layout.fillHeight: true + Layout.fillWidth: true + + placeholderText: "Filter..." + onTextChanged: nameFilter = text + onEditingCompleted: toolDiv.focus = true + + Connections { + target: panel + function onNameFilterChanged() { + filterBtn.text = nameFilter + } + } + } + + // XsTextInput{ id: filterBtn + // Layout.fillHeight: true + // Layout.fillWidth: true + + + // isExpanded: true + // hint: "Filter" + // onTextChanged: nameFilter = text + // onEditingCompleted: focus = false + + // Connections { + // target: panel + // function onNameFilterChanged() { + // filterBtn.text = nameFilter + // } + // } + // } + + Item { + Layout.fillWidth: true + } + + MouseArea { + Layout.preferredWidth: btnWidth*2.5 + Layout.fillHeight: true + + hoverEnabled: true + + XsSBCountDisplay{ + anchors.fill: parent + filteredCount: resultsFilteredModel.count + totalCount: resultsBaseModel.count == undefined ? "-" : resultsBaseModel.truncated ? resultsBaseModel.count + "+" : resultsBaseModel.count + } + + XsToolTip { + delay: 0 + visible: parent.containsMouse + text: "Execution time " + resultsBaseModel.executionMilliseconds + " ms." + } + } + + XsComboBoxEditable{ id: filterStep + Layout.minimumWidth: btnWidth*3 + Layout.preferredWidth: btnWidth*3 + Layout.fillHeight: true + model: ShotBrowserEngine.ready ? ShotBrowserEngine.presetsModel.termModel("Pipeline Step") : [] + textRole: "nameRole" + currentIndex: -1 + displayText: currentIndex ==-1 ? "Pipeline Step" : currentText + + onModelChanged: currentIndex = -1 + + onCurrentIndexChanged: { + if(currentIndex == -1) + pipeStep = "" + } + onAccepted: { + pipeStep = model.get(model.index(currentIndex, 0), "nameRole") + focus = false + } + + onActivated: pipeStep = model.get(model.index(currentIndex,0), "nameRole") + + Connections { + target: panel + function onPipeStepChanged() { + filterStep.currentIndex = filterStep.find(pipeStep) + } + } + } + XsComboBoxEditable{ id: filterOnDisk + Layout.minimumWidth: btnWidth*2 + Layout.preferredWidth: btnWidth*2 + + Layout.fillHeight: true + model: ShotBrowserEngine.ready ? ShotBrowserEngine.presetsModel.termModel("Site") : [] + currentIndex: -1 + textRole: "nameRole" + displayText: currentIndex==-1? "On Disk" : currentText + + onModelChanged: currentIndex = -1 + + onCurrentIndexChanged: { + if(currentIndex==-1) + onDisk = "" + } + + onAccepted: { + onDisk = model.get(model.index(currentIndex,0), "nameRole") + focus = false + } + + onActivated: onDisk = model.get(model.index(currentIndex,0), "nameRole") + + Connections { + target: panel + function onOnDiskChanged() { + filterOnDisk.currentIndex = filterOnDisk.find(onDisk) + } + } + + } + + + XsButtonWithImageAndText{ id: groupBtn + Layout.preferredWidth: btnWidth*2.2 + Layout.fillHeight: true + iconSrc: "qrc:///shotbrowser_icons/account_tree.svg" + iconText: "Group" + textDiv.visible: true + isActive: resultsBaseModel.isGrouped + visible: resultsBaseModel.canBeGrouped + onClicked: resultsBaseModel.isGrouped = !resultsBaseModel.isGrouped + } + + XsSortButton{ id: sortViaNaturalOrderBtn + Layout.preferredWidth: btnWidth + Layout.fillHeight: true + text: "ShotGrid Order" + isActive: sortByNaturalOrder + sortIconText: "SG" + isDescendingOrder: sortByNaturalOrder && !sortInAscending + + onClicked: { + if(sortByNaturalOrder && sortInAscending) sortInAscending = false + else sortInAscending = true + sortByNaturalOrder = true + } + } + + XsSortButton{ id: sortViaDateBtn + Layout.preferredWidth: btnWidth + Layout.fillHeight: true + text: "Creation Date" + isActive: sortByCreationDate + sortIconSrc: "qrc:///shotbrowser_icons/calendar_month.svg" + isDescendingOrder: sortByCreationDate && !sortInAscending + + onClicked: { + if(sortByCreationDate && sortInAscending) sortInAscending = false + else sortInAscending = true + sortByCreationDate = true + } + } + + XsSortButton{ id: sortViaShotBtn + Layout.preferredWidth: btnWidth + Layout.fillHeight: true + text: "Shot Name" + isActive: sortByShotName + sortIconText: "AZ" + isDescendingOrder: sortByShotName && !sortInAscending + + onClicked: { + if(sortByShotName && sortInAscending) sortInAscending = false + else sortInAscending = true + sortByShotName = true + } + } + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBR2Views.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBR2Views.qml new file mode 100644 index 000000000..70c0dab38 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBR2Views.qml @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic +import Qt.labs.qmlmodels + +import xStudio 1.0 +import ShotBrowser 1.0 + +XsListView{ id: listDiv + spacing: XsStyleSheet.panelPadding + + property int rightSpacing: listDiv.height < listDiv.contentHeight ? 16 : 0 + Behavior on rightSpacing {NumberAnimation {duration: 150}} + property bool isPlaylist: resultsBaseModel.entity == "Playlists" + + ScrollBar.vertical: XsScrollBar { + visible: listDiv.height < listDiv.contentHeight + parent: listDiv + anchors.top: listDiv.top + anchors.right: listDiv.right + anchors.bottom: listDiv.bottom + x: -5 + } + + XsLabel { + text: !queryRunning ? (presetsSelectionModel.hasSelection ? "No Results Found" : "Select a preset on left to view the results") : "" + color: XsStyleSheet.hintColor + visible: results.count == 0 + + anchors.fill: parent + + font.pixelSize: XsStyleSheet.fontSize*1.2 + font.weight: Font.Medium + } + + XsSBMediaPlayer { + id: mediaPlayer + anchors.fill: parent + visible: false + onEject: visible = false + z: 2 + } + + function playMovie(path) { + if(path != "") { + mediaPlayer.loadMedia(path) + mediaPlayer.visible = true + mediaPlayer.playToggle() + } + } + + XsSBImageViewer { + id: imagePlayer + anchors.fill: parent + visible: false + onEject: visible = false + } + + function viewImages(images) { + imagePlayer.images = images + imagePlayer.visible = true + } + + model: DelegateModel { id: chooserModel + property var notifyModel: results + onNotifyModelChanged: model = notifyModel + model: notifyModel + + delegate: DelegateChooser{ id: chooser + role: "typeRole" + + DelegateChoice{ + roleValue: "Version" + + ShotHistoryListDelegate{ + modelDepth: chooserModel.notifyModel.depthAtRow(index) + width: listDiv.width - rightSpacing + height: XsStyleSheet.widgetStdHeight*4 + delegateModel: chooserModel + popupMenu: versionResultPopup + groupingEnabled: resultsBaseModel.isGrouped + isPlaylist: listDiv.isPlaylist + onPlayMovie: (path) => listDiv.playMovie(path) + } + } + + DelegateChoice{ + roleValue: "Note" + + NotesHistoryListDelegate{ + width: listDiv.width - rightSpacing + height: XsStyleSheet.widgetStdHeight*8 + delegateModel: chooserModel + popupMenu: noteResultPopup + onShowImages: (images) => listDiv.viewImages(images) + } + } + + DelegateChoice{ + roleValue: "Playlist" + + XsSBRPlaylistViewDelegate{ + width: listDiv.width - rightSpacing + height: XsStyleSheet.widgetStdHeight*2 + delegateModel: chooserModel + popupMenu: playlistResultPopup + } + } + } + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBR3Actions.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBR3Actions.qml new file mode 100644 index 000000000..cd6cd8d6e --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBR3Actions.qml @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QuickFuture 1.0 +import QuickPromise 1.0 + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 + +RowLayout { + spacing: 2 + + XsPrimaryButton{ + Layout.fillHeight: true + Layout.fillWidth: true + + text: " Add " + onClicked: { + if(resultsBaseModel.groupDetail.flags.includes("Load Sequence")) + ShotBrowserHelpers.addSequencesToNewPlaylist(resultsSelectionModel.selectedIndexes) + else + ShotBrowserHelpers.addToCurrent(resultsSelectionModel.selectedIndexes, false, addAfterSelection.value) + } + } + + XsPrimaryButton{ + Layout.fillHeight: true + Layout.fillWidth: true + + text: "Replace" + onClicked: ShotBrowserHelpers.replaceSelectedResults(resultsSelectionModel.selectedIndexes) + enabled: ! resultsBaseModel.groupDetail.flags.includes("Load Sequence") + + } + + XsPrimaryButton{ + Layout.fillHeight: true + Layout.fillWidth: true + + text: "Compare" + enabled: ! resultsBaseModel.groupDetail.flags.includes("Load Sequence") + onClicked: ShotBrowserHelpers.compareSelectedResults(resultsSelectionModel.selectedIndexes) + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBRPlaylistResultPopup.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBRPlaylistResultPopup.qml new file mode 100644 index 000000000..97067160e --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/XsSBRPlaylistResultPopup.qml @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QuickFuture 1.0 + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.clipboard 1.0 + + +XsPopupMenu { + id: rightClickMenu + visible: false + + property var popupSelectionModel + property var popupDelegateModel + + menu_model_name: "playlisthistory_menu_"+rightClickMenu + + Clipboard { + id: clipboard + } + + XsMenuModelItem { + text: "Select All" + menuItemPosition: 1 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: popupSelectionModel.select( + helpers.createItemSelectionFromList(ShotBrowserHelpers.getAllIndexes(popupDelegateModel)), + ItemSelectionModel.Select + ) + } + XsMenuModelItem { + text: "Deselect All" + menuItemPosition: 2 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: popupSelectionModel.clear() + } + XsMenuModelItem { + text: "Invert Selection" + menuItemPosition: 3 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: popupSelectionModel.select( + helpers.createItemSelectionFromList(ShotBrowserHelpers.getAllIndexes(popupDelegateModel)), + ItemSelectionModel.Toggle + ) + } + XsMenuModelItem { + menuItemType: "divider" + menuItemPosition: 4 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + } + + XsMenuModelItem { + text: "Reveal In ShotGrid..." + (enabled ? "" : " (Production Only)") + enabled: ShotBrowserEngine.shotGridUserType != "User" + menuItemPosition: 5 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: ShotBrowserHelpers.revealInShotgrid(popupSelectionModel.selectedIndexes) + } + + XsMenuModelItem { + text: "Copy JSON" + menuItemPosition: 6.5 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: clipboard.text = JSON.stringify(ShotBrowserHelpers.getJSON(popupSelectionModel.selectedIndexes)) + } + XsMenuModelItem { + text: "Copy DNUuid" + menuItemPosition: 6.6 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: clipboard.text = ShotBrowserHelpers.getDNUuid(popupSelectionModel.selectedIndexes).join("\n") + } + +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/viewItems/XsSBRPlaylistViewDelegate.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/viewItems/XsSBRPlaylistViewDelegate.qml new file mode 100644 index 000000000..08303eab4 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/right_sections/viewItems/XsSBRPlaylistViewDelegate.qml @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QuickFuture 1.0 + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 + + +Rectangle { + property var delegateModel: null + + property int itemSpacing: 1 + property color itemColorActive: palette.highlight + property color itemColorNormal: "transparent" + + property var popupMenu: null + + property bool isMouseHovered: mArea.containsMouse || + versionArrowBtn.hovered || + notesBtn.hovered || + authorDisplay.isHovered || + typeDisplay.isHovered || + timeDisplay.isHovered || + dateDisplay.isHovered || + deptDisplay.isHovered || + nameDisplay.isHovered + + property bool isSelected: resultsSelectionModel.isSelected(delegateModel.modelIndex(index)) //delegateModel? selectionModel.isSelected(delegateModel.modelIndex(index)) : false + + color: isSelected? Qt.darker(itemColorActive, 2.75): XsStyleSheet.widgetBgNormalColor + + border.color: isMouseHovered? itemColorActive : "transparent" + + Connections { + target: resultsSelectionModel + function onSelectionChanged(selected, deselected) { + isSelected = resultsSelectionModel.isSelected(delegateModel.modelIndex(index)) + } + } + + MouseArea{ + id: mArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + + // wierd workaround for flickable.. + propagateComposedEvents: false + onReleased: { + if(!propagateComposedEvents) + propagateComposedEvents = true + } + + onDoubleClicked: ShotBrowserHelpers.loadShotgridPlaylists([delegateModel.modelIndex(index)]) + + onPressed: (mouse)=>{ + // required for doubleclick to work + mouse.accepted = true + + if (mouse.button == Qt.RightButton){ + if(popupMenu.visible) { + popupMenu.visible = false + } + else{ + if(!isSelected) { + if(mouse.modifiers == Qt.NoModifier) { + ShotBrowserHelpers.selectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } else if(mouse.modifiers == Qt.ShiftModifier){ + ShotBrowserHelpers.shiftSelectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } else if(mouse.modifiers == Qt.ControlModifier) { + ShotBrowserHelpers.ctrlSelectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } + } + popupMenu.popupDelegateModel = delegateModel + let ppos = mapToItem(popupMenu.parent, mouseX, mouseY) + popupMenu.x = ppos.x + popupMenu.y = ppos.y + popupMenu.visible = true + } + } + + else if(mouse.modifiers == Qt.NoModifier) { + ShotBrowserHelpers.selectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } else if(mouse.modifiers == Qt.ShiftModifier){ + ShotBrowserHelpers.shiftSelectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } else if(mouse.modifiers == Qt.ControlModifier) { + ShotBrowserHelpers.ctrlSelectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } + } + + GridLayout { + anchors.fill: parent + anchors.margins: panelPadding + rows: 2 + columns: 7 + rowSpacing: itemSpacing + columnSpacing: itemSpacing*3 + + XsPrimaryButton{ id: versionArrowBtn + Layout.preferredWidth: visible? 20 : 0 + Layout.fillHeight: true + Layout.rowSpan: 2 + + // height: visible? parent.itemHeight-listItemSpacing : 0 + imgSrc: "qrc:/icons/chevron_right.svg" + + imageDiv.rotation: isActive ? 90 : 0 + + property bool hasVersions: versionCountRole == 0 ? false : true + + visible: true + + enabled: hasVersions + isActiveViaIndicator: false + isActive: delegateModel.notifyModel.isExpanded(index) + onClicked: { + if(!isActive) { + delegateModel.notifyModel.expandRow(index) + isActive = delegateModel.notifyModel.isExpanded(index) + } else { + delegateModel.notifyModel.collapseRow(index) + isActive = delegateModel.notifyModel.isExpanded(index) + } + } + } + + + XsPrimaryButton{ id: notesBtn + Layout.preferredWidth: 20 + Layout.fillHeight: true + Layout.rowSpan: 2 + property bool hasNotes: noteCountRole <= 0 ? false : true + + text: "N" + font.weight: hasNotes? Font.Bold:Font.Normal + font.pixelSize: XsStyleSheet.fontSize*1.2 + // bgColorPressed: Qt.darker(palette.highlight, 2) + forcedBgColorNormal: hasNotes? palette.highlight : bgColorNormal + textDiv.color: hasNotes? palette.text : XsStyleSheet.hintColor + enabled: false //hasNotes + bgDiv.opacity: enabled? 1.0 : 0.5 + isActive: hasNotes + isActiveViaIndicator: false + isUnClickable: true + // onClicked: + } + + XsText{ + Layout.preferredWidth: parent.height //* 1.5 + Layout.fillHeight: true + Layout.rowSpan: 2 + text: versionCountRole == -1 ? "-" : versionCountRole + wrapMode: Text.WordWrap + elide: Text.ElideRight + // font.weight: text=="0"? Font.Normal : Font.Black + color: text=="0"? XsStyleSheet.hintColor : XsStyleSheet.secondaryTextColor + font.pixelSize: XsStyleSheet.fontSize*1.2 + horizontalAlignment: Text.AlignHCenter + } + + XsText{ id: nameDisplay + Layout.columnSpan: 3 + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + Layout.fillHeight: true + + text: nameRole + elide: Text.ElideRight + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignTop + topPadding: 2 + + property bool isHovered: nameTooltip.containsMouse + + MouseArea { id: nameTooltip + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + } + XsText{ id: deptDisplay + Layout.fillWidth: true + Layout.minimumWidth: 20 + Layout.preferredWidth: 60 + Layout.maximumWidth: 190 + Layout.fillHeight: true + Layout.columnSpan: 1 + Layout.alignment: Qt.AlignRight + + text: departmentRole ? departmentRole : "-" + elide: Text.ElideRight + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignTop + topPadding: 2 + rightPadding: 2 + + property bool isHovered: deptTooltip.containsMouse + + MouseArea { id: deptTooltip + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + } + XsText{ id: dateDisplay + Layout.columnSpan: 1 + Layout.fillWidth: true + Layout.minimumWidth: 40 + Layout.preferredWidth: 60 + Layout.maximumWidth: 92 + Layout.alignment: Qt.AlignHCenter + + property var dateFormatted: createdDateRole.toLocaleString().split(" ") + text: typeof dateFormatted !== 'undefined'? dateFormatted[1].substr(0,3)+" "+dateFormatted[2]+" "+dateFormatted[3] : "" + + elide: Text.ElideRight + color: XsStyleSheet.hintColor + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + bottomPadding: 2 + + property bool isHovered: dateTooltip.containsMouse + + MouseArea { id: dateTooltip + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + } + XsText{ id: timeDisplay + Layout.columnSpan: 1 + Layout.fillWidth: true + Layout.minimumWidth: 40 + Layout.preferredWidth: 60 + Layout.maximumWidth: 85 + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom + + property var dateFormatted: createdDateRole.toLocaleString().split(" ") + property var timeFormatted: dateFormatted[4].split(":") + text: typeof timeFormatted !== 'undefined'? + typeof dateFormatted[6] !== 'undefined'? + timeFormatted[0]+":"+timeFormatted[1]+" "+dateFormatted[5]+" "+dateFormatted[6] : + timeFormatted[0]+":"+timeFormatted[1]+" "+dateFormatted[5] : "" + + + elide: Text.ElideRight + color: XsStyleSheet.hintColor + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + bottomPadding: 2 + + property bool isHovered: timeTooltip.containsMouse + + MouseArea { id: timeTooltip + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + } + XsText{ id: typeDisplay + Layout.fillWidth: true + Layout.minimumWidth: text? 60 : 0 + // Layout.maximumWidth: 110 + Layout.alignment: Qt.AlignLeft + + text: playlistTypeRole ? playlistTypeRole : "-" + elide: Text.ElideRight + color: XsStyleSheet.hintColor + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignBottom + bottomPadding: 2 + + property bool isHovered: typeTooltip.containsMouse + + MouseArea { id: typeTooltip + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + } + XsText{ id: authorDisplay + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight + + text: authorRole ? authorRole : "-" //createdByRole + elide: Text.ElideRight + color: XsStyleSheet.hintColor + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignBottom + bottomPadding: 2 + rightPadding: 2 + + property bool isHovered: authorTooltip.containsMouse + + MouseArea { id: authorTooltip + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + } + } + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/widgets/XsSBCountDisplay.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/widgets/XsSBCountDisplay.qml new file mode 100644 index 000000000..8772ecd32 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/widgets/XsSBCountDisplay.qml @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 + + +Rectangle { id: widget + + property var filteredCount: "-" //resultsModel.count + property var totalCount: "-" //resultsModel.sourceModel.count + (resultsModel.sourceModel.truncated ? "+":"") + + property color bgColor: XsStyleSheet.panelBgColor + property color borderColor: XsStyleSheet.menuDividerColor //"transparent" + + implicitWidth: 100 + implicitHeight: 28 + border.color: borderColor + border.width: 1 + color: bgColor + + XsText{ + text: filteredCount + " / " + totalCount + width: parent.width + anchors.centerIn: parent + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/widgets/XsSBImageViewer.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/widgets/XsSBImageViewer.qml new file mode 100644 index 000000000..5e8e2db9c --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/widgets/XsSBImageViewer.qml @@ -0,0 +1,130 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic +import QtMultimedia + +import xStudio 1.0 + +MouseArea { + id: playerbox + property var images: [] + property int index: 0 + hoverEnabled: true + + onEntered: forceActiveFocus(playerbox) + + signal eject() + + onVisibleChanged: { + if(visible) + forceActiveFocus(playerbox) + } + + onImagesChanged: index = 0 + + onClicked: { + if(images.length) { + if(index == images.length-1) + index = 0 + else + index++ + } + } + + function next() { + if(images.length) { + if(index < images.length-1) + index++ + } + } + + function previous() { + if(images.length) { + if(index) + index-- + } + } + + function exitPlayback() { + playerbox.images = [] + eject() + } + + Keys.onEscapePressed: playerbox.exitPlayback() + Keys.onSpacePressed: playerbox.next() + + Rectangle { + anchors.fill: parent + color: "black" + + Image { + anchors.fill: parent + source: images.length ? images[index][1] : "" + fillMode: Image.PreserveAspectFit + } + + Rectangle { + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 4 + height: parent.height / 20 + color: "#AA000000" + + + RowLayout { + spacing:4 + anchors.fill: parent + + MouseArea { + Layout.fillHeight: true + Layout.preferredWidth: height + hoverEnabled: true + + onClicked: playerbox.exitPlayback() + + XsIcon{ + anchors.fill: parent + source: "qrc:/icons/close.svg" + imgOverlayColor: parent.containsMouse ? palette.highlight : XsStyleSheet.hintColor + } + } + + MouseArea { + Layout.fillHeight: true + Layout.preferredWidth: height + hoverEnabled: true + onClicked: index && playerbox.previous() + + XsIcon{ + anchors.fill: parent + rotation: 180 + source: "qrc:/icons/chevron_right.svg" + imgOverlayColor: index ? (parent.containsMouse ? palette.highlight : XsStyleSheet.hintColor) : XsStyleSheet.widgetBgNormalColor + } + } + + MouseArea { + Layout.fillHeight: true + Layout.preferredWidth: height + hoverEnabled: true + + onClicked: index < images.length-1 && playerbox.next() + + XsIcon{ + anchors.fill: parent + source: "qrc:/icons/chevron_right.svg" + imgOverlayColor: index < images.length-1 ? (parent.containsMouse ? palette.highlight : XsStyleSheet.hintColor) : XsStyleSheet.widgetBgNormalColor + } + } + + XsText { + Layout.fillHeight: true + Layout.fillWidth: true + + text: images.length ? images[index][0] : "No Images" + } + } + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/widgets/XsSBMediaPlayer.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/widgets/XsSBMediaPlayer.qml new file mode 100644 index 000000000..4010d2c44 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/widgets/XsSBMediaPlayer.qml @@ -0,0 +1,172 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic +import QtMultimedia + +import xStudio 1.0 + +MouseArea { + id: playerbox + hoverEnabled: true + + onEntered: forceActiveFocus(playerbox) + + function loadMedia(path) { + player.source = path + } + + signal eject() + + onVisibleChanged: { + if(visible) + forceActiveFocus(playerbox) + } + + onClicked: { + if(player.playing) + player.pause() + else + player.play() + } + + function playToggle() { + if(player.playing) { + player.pause() + } else { + player.play() + } + } + + function exitPlayback() { + player.stop() + player.source = "" + eject() + } + + function audioToggle() { + player.audioOutput.muted = !player.audioOutput.muted + } + + Keys.onEscapePressed: playerbox.exitPlayback() + Keys.onSpacePressed: playerbox.playToggle() + + Rectangle { + anchors.fill: parent + color: "black" + + VideoOutput { + id: output + anchors.fill: parent + } + + MediaPlayer { + id: player + property real rate: 1000.0 / 24.0 + loops: MediaPlayer.Infinite + videoOutput: output + audioOutput: AudioOutput { + volume: 0.5 + } + onMetaDataChanged: { + player.metaData.keys().forEach(function (item, index) { + if(player.metaData.metaDataKeyToString(item) == "Video frame rate") { + rate = 1000.0 / player.metaData.stringValue(item) + } + }) + } + } + + + Rectangle { + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 4 + height: parent.height / 20 + color: "#AA000000" + + RowLayout { + spacing:4 + anchors.fill: parent + + MouseArea { + Layout.fillHeight: true + Layout.preferredWidth: height + hoverEnabled: true + + onClicked: playerbox.exitPlayback() + + XsIcon { + anchors.fill: parent + source: "qrc:/icons/close.svg" + imgOverlayColor: parent.containsMouse ? palette.highlight : XsStyleSheet.hintColor + } + } + + MouseArea { + Layout.fillHeight: true + Layout.preferredWidth: height + hoverEnabled: true + + onClicked: playerbox.playToggle() + onEntered: forceActiveFocus(playerbox) + + XsIcon { + anchors.fill: parent + source: player.playing ? "qrc:/icons/play_arrow.svg" : "qrc:/icons/pause.svg" + imgOverlayColor: parent.containsMouse ? palette.highlight : XsStyleSheet.hintColor + } + } + + ScrollBar { + id: scroller + hoverEnabled: true + enabled: player.seekable + Layout.preferredHeight: parent.height/3 + Layout.fillWidth: true + orientation: Qt.Horizontal + policy: ScrollBar.AlwaysOn + position: player.duration ? Math.min(1.0,Math.max(0.0, (1.0/player.duration) * player.position)) : 0 + + size: player.duration ? (1.0/(player.duration/player.rate)): 1.0 + + onPositionChanged: { + if(pressed) + player.position = position * player.duration + } + + contentItem: Rectangle { + height: scroller.height + radius: height / 2 + color: scroller.pressed ? palette.highlight : (scroller.hovered ? palette.text : XsStyleSheet.hintColor) + } + + background: Rectangle { + anchors.fill: parent + radius: height / 2 + color: XsStyleSheet.widgetBgNormalColor + } + + Keys.onEscapePressed: playerbox.exitPlayback() + Keys.onSpacePressed: playerbox.playToggle() + } + + MouseArea { + Layout.fillHeight: true + Layout.preferredWidth: height + enabled: player.hasAudio + hoverEnabled: true + + onClicked: playerbox.audioToggle() + + XsIcon { + anchors.fill: parent + source: "qrc:/icons/volume_off.svg" + imgOverlayColor: player.hasAudio ? (parent.containsMouse ? palette.highlight : (player.audioOutput.muted ? palette.highlight : XsStyleSheet.hintColor)) : XsStyleSheet.widgetBgNormalColor + } + } + + } + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/widgets/XsSBTreeSearchButton.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/widgets/XsSBTreeSearchButton.qml new file mode 100644 index 000000000..7b3a1960c --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_browser/widgets/XsSBTreeSearchButton.qml @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic + +import QuickFuture 1.0 +import QuickPromise 1.0 + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 + +Item{ + id: button + + property bool isExpanded: false + property var model: null + + property color textColorActive: "white" + property color textColorNormal: "light grey" + property color itemColorActive: palette.highlight + property string hint: "" + + signal indexSelected(index: var) + + function selectionMade(index) { + indexSelected(index) + isExpanded = false + } + + onIsExpandedChanged: { + if(isExpanded) { + searchField.forceActiveFocus() + searchPop.open() + } else { + searchPop.close() + searchField.clearSearch() + searchField.focus = false + } + } + + XsPrimaryButton{ id: searchBtn + width: XsStyleSheet.primaryButtonStdWidth + height: parent.height + imgSrc: "qrc:/icons/search.svg" + text: "Search" + isActive: isExpanded + + onClicked: isExpanded = !isExpanded + } + + + Item{ + id: widget + visible: isExpanded + width: parent.width - searchBtn.width + height: parent.height // + combo.popupOptions.height + anchors.left: searchBtn.right + + XsSearchBar { id: searchField + width: parent.width + height: parent.height + + placeholderText: width? "Search...":"" + forcedHover: clearButton.hovered + + onAccepted: { + if(searchList.currentIndex!==-1) { + selectionMade(sequenceModelDelegate.modelIndex(searchList.currentIndex)) + model.setFilterWildcard("") + text = "" + } + } + + onTextEdited: { + model.filterCaseSensitivity = Qt.CaseInsensitive + model.setFilterWildcard(text) + searchList.currentIndex = 0 + } + + Keys.onPressed: (event)=> { + if (event.key == Qt.Key_Down) { + if(sequenceModelDelegate.count>0) { + if(searchList.currentIndex===-1) searchList.currentIndex=0 + else if(searchList.currentIndex!==-1) searchList.currentIndex+=1 + } + } + else if (event.key == Qt.Key_Up) { + if(sequenceModelDelegate.count>0 && searchList.currentIndex>-1) + searchList.currentIndex-=1 + } + } + } + XsSecondaryButton{ + id: clearButton + imgSrc: "qrc:/icons/close.svg" + visible: searchField.length !== 0 + width: visible? 16 : 0 + height: 16 + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: 6 + onClicked: { + clearSearch() + } + } + + } + + function clearSearch() + { + searchField.textEdited() + searchField.clear() + } + + DelegateModel { + id: sequenceModelDelegate + model: button.model + + delegate: Rectangle { + property bool isHovered: mouseArea.containsMouse + property var modelDelegate: sequenceModelDelegate + + width: searchList.width-2 + height: btnHeight/1.3 + color: searchList.currentIndex==index? palette.highlight : Qt.darker(palette.base, 1.5) + + XsText{ + text: nameRole + color: searchList.currentIndex==index? textColorActive: textColorNormal + elide: Text.ElideRight + width: parent.width - 2 + height: parent.height + ToolTip.text: text + ToolTip.visible: mouseArea.containsMouse && truncated + } + + MouseArea{id: mouseArea; anchors.fill: parent; hoverEnabled: true + onEntered: searchList.currentIndex = index + onClicked: selectionMade(modelDelegate.modelIndex(index)) + } + } + } + + Popup { + id: searchPop + + width: Math.max(searchField.width, 250) + height: Math.min(16, Math.max(5, sequenceModelDelegate.count+2)) * searchItemHeight + x: widget.x + searchField.x + y: widget.y + searchField.y + searchField.height + + property real searchItemHeight: btnHeight/1.3 + + closePolicy: searchBtn.hovered ? Popup.CloseOnEscape : Popup.CloseOnEscape | Popup.CloseOnPressOutside + + onClosed: button.isExpanded = false + + ListView { + id: searchList + clip:true + + anchors.fill: parent + anchors.margins: 1 + + model: sequenceModelDelegate + + orientation: ListView.Vertical + snapMode: ListView.SnapToItem + currentIndex: -1 + + ScrollBar.vertical: XsScrollBar { id: searchScrollBar; padding: 2} + + onContentHeightChanged: { + if(height < contentHeight) searchScrollBar.policy = ScrollBar.AlwaysOn + else searchScrollBar.policy = ScrollBar.AsNeeded + } + } + + background: Rectangle{ + anchors.fill: parent + color: Qt.darker(palette.base, 1.5) + border.color: Qt.lighter(XsStyleSheet.panelTabColor, 1.3) + border.width: 1 + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistory.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistory.qml new file mode 100644 index 000000000..9fb85e265 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistory.qml @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts + +import QuickFuture 1.0 +import QuickPromise 1.0 + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 +import ShotBrowser 1.0 + +Item{ + id: panel + anchors.fill: parent + + property bool isPanelEnabled: true + property var dataModel: results + + property real panelPadding: XsStyleSheet.panelPadding + property color panelColor: XsStyleSheet.panelBgColor + + property var activeScopeIndex: helpers.qModelIndex() + + // Track the uuid of the media that is currently visible in the Viewport + property var onScreenMediaUuid: currentPlayhead.mediaUuid + property var onScreenLogicalFrame: currentPlayhead.logicalFrame + + property int queryCounter: 0 + property int queryRunning: 0 + readonly property string panelType: "ShotHistory" + + property alias nameFilter: results.filterName + property string sentTo: "" + + property bool isPopout: typeof panels_layout_model_index == "undefined" + + // used ? + property real btnHeight: XsStyleSheet.widgetStdHeight + 4 + + property bool isPaused: false + + onOnScreenMediaUuidChanged: {if(visible) updateTimer.start()} + + onSentToChanged: { + runQuery() + } + + XsPreference { + id: addAfterSelection + path: "/plugin/data_source/shotbrowser/add_after_selection" + } + + XsPreference { + id: pauseOnPlaying + path: "/plugin/data_source/shotbrowser/pause_update_on_playing" + } + + onOnScreenLogicalFrameChanged: { + if(visible) { + if(currentPlayhead.playing && !pauseOnPlaying.value) { + // no op + } else if(updateTimer.running) { + updateTimer.restart() + if(isPanelEnabled && !isPaused) { + isPaused = true + } + } + } + } + + Timer { + id: updateTimer + interval: 500 + running: false + repeat: false + onTriggered: { + isPaused = false + ShotBrowserHelpers.updateMetadata(isPanelEnabled, onScreenMediaUuid) + } + } + + Connections { + target: ShotBrowserEngine + function onReadyChanged() { + setIndexFromPreference() + runQuery() + } + } + + function setIndexFromPreference() { + if(ShotBrowserEngine.ready && !activeScopeIndex.valid && prefs.scope) { + // from panel. + if(prefs.scope != undefined) { + let i = getScopeIndex(prefs.scope) + if(i.valid && activeScopeIndex != i) + activeScopeIndex = i + } + } + } + + function getScopeIndex(scope_name) { + for (const ind of ShotBrowserEngine.presetsModel.shotHistoryScope) { + if(ShotBrowserEngine.presetsModel.get(ind, "nameRole") == scope_name){ + return ind + } + } + return helpers.qModelIndex() + } + + onActiveScopeIndexChanged: { + if(activeScopeIndex && activeScopeIndex.valid) { + let m = activeScopeIndex.model + let i = m.get(activeScopeIndex, "nameRole") + prefs.scope = i + } + } + + Connections { + target: ShotBrowserEngine + function onLiveLinkMetadataChanged() { + runQuery() + } + } + + onIsPanelEnabledChanged: { + if(isPanelEnabled) { + ShotBrowserHelpers.updateMetadata(isPanelEnabled, onScreenMediaUuid) + runQuery() + } + } + + Component.onCompleted: { + if(visible) { + ShotBrowserEngine.connected = true + setIndexFromPreference() + ShotBrowserHelpers.updateMetadata(isPanelEnabled, onScreenMediaUuid) + isPaused = false + runQuery() + } else + setIndexFromPreference() + } + + onVisibleChanged: { + if(visible) { + ShotBrowserEngine.connected = true + setIndexFromPreference() + ShotBrowserHelpers.updateMetadata(isPanelEnabled, onScreenMediaUuid) + isPaused = false + runQuery() + } + } + + Item { + // Hold properties that we want to persist between sessions. + id: prefs + property string scope: "" + + XsStoredPanelProperties { + propertyNames: ["scope"] + onPropertiesInitialised: { + prefs.initialised = true + setIndexFromPreference() + runQuery() + } + } + property bool initialised: false + + } + + function runQuery() { + if(!isPaused && panel.visible && ShotBrowserEngine.ready && isPanelEnabled && activeScopeIndex.valid) { + // make sure the results appear in sync. + let custom = [] + if(sentTo != "" && sentTo != "Ignore") + custom.push({ + "enabled": true, + "type": "term", + "term": "Sent To", + "value": sentTo + }) + + if(onScreenMediaUuid == "{00000000-0000-0000-0000-000000000000}") { + resultsBaseModel.setResultDataJSON([]) + } else { + queryCounter += 1 + queryRunning += 1 + let i = queryCounter + + + let customContext = {} + customContext["preset_name"] = ShotBrowserEngine.presetsModel.get( + activeScopeIndex, + "nameRole" + ) + + Future.promise( + ShotBrowserEngine.executeQueryJSON( + [ShotBrowserEngine.presetsModel.get(activeScopeIndex, "jsonPathRole")], + {}, + custom, + customContext + ) + ).then( + function(json_string) { + if(queryCounter == i) { + resultsSelectionModel.clear() + resultsBaseModel.setResultDataJSON([json_string]) + } + queryRunning -= 1 + }, + function() { + resultsSelectionModel.clear() + resultsBaseModel.setResultDataJSON([]) + queryRunning -= 1 + } + ) + } + } + } + + function activateScope(clickedIndex){ + if(clickedIndex.valid) { + activeScopeIndex = clickedIndex + runQuery() + } + } + + XsGradientRectangle{ id: backgroundDiv + anchors.fill: parent + } + + ShotHistoryResultPopup { + id: resultPopup + menu_model_name: "shot_history_popup"+resultPopup + popupSelectionModel: resultsSelectionModel + } + + ShotBrowserResultModel { + id: resultsBaseModel + } + + ShotBrowserResultFilterModel { + id: results + sourceModel: resultsBaseModel + } + + + ItemSelectionModel { + id: resultsSelectionModel + model: results + } + + ColumnLayout{ + anchors.fill: parent + anchors.margins: 4 + spacing: 4 + + ShotHistoryTitleDiv{id: titleDiv + titleButtonHeight: (XsStyleSheet.widgetStdHeight + 4) + Layout.fillWidth: true + Layout.minimumHeight: titleButtonHeight*2 + Layout.maximumHeight: titleButtonHeight*2 + } + + Rectangle{ + color: panelColor + Layout.fillWidth: true + Layout.fillHeight: true + + ShotHistoryListDiv{ + anchors.fill: parent + anchors.margins: 4 + } + } + + ShotHistoryActionDiv{id: buttonsDiv + Layout.fillWidth: true + Layout.maximumHeight: XsStyleSheet.widgetStdHeight + parentWidth: parent.width - 4 + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryActionDiv.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryActionDiv.qml new file mode 100644 index 000000000..a3cda0fba --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryActionDiv.qml @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 +import ShotBrowser 1.0 +import QuickFuture 1.0 +import QuickPromise 1.0 +import xstudio.qml.helpers 1.0 + + +RowLayout{ + spacing: 2 + property int parentWidth: width + readonly property int cellWidth: parentWidth / children.length + + XsPrimaryButton{ + text: "Add" + Layout.fillHeight: true + Layout.preferredWidth: cellWidth + onClicked: ShotBrowserHelpers.addToCurrent(resultsSelectionModel.selectedIndexes, true, addAfterSelection.value) + } + XsPrimaryButton{ + text: "Replace" + Layout.fillHeight: true + Layout.preferredWidth: cellWidth + onClicked: ShotBrowserHelpers.replaceSelectedResults(resultsSelectionModel.selectedIndexes) + } + XsPrimaryButton{ + text: "Compare" + Layout.fillHeight: true + Layout.preferredWidth: cellWidth + onClicked: ShotBrowserHelpers.compareSelectedResults(resultsSelectionModel.selectedIndexes) + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryListDelegate.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryListDelegate.qml new file mode 100644 index 000000000..5f0c30f7d --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryListDelegate.qml @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts + +import QuickFuture 1.0 + +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 + +import xStudio 1.0 +import xstudio.qml.models 1.0 + + +Rectangle{ id: frame + // anchors.fill: parent + + color: isSelected? Qt.darker(palette.highlight, 5) : "transparent" + border.color: isHovered? palette.highlight : XsStyleSheet.widgetBgNormalColor + border.width: 1 + + property bool isActive: false + property bool isSelected: resultsSelectionModel.isSelected(delegateModel.modelIndex(index)) + + property int modelDepth: 0 + + property int itemSpacing: 1 + + property real textSize: XsStyleSheet.fontSize + + property var delegateModel: null + property var popupMenu: null + property bool groupingEnabled: false + + required property string nameRole + required property string frameRangeRole + required property string pipelineStepRole + required property string pipelineStatusFullRole + required property string authorRole + required property string thumbRole + required property string clientFilenameRole + required property string projectRole + required property int idRole + required property string entityRole + + required property int onSiteChn + required property int onSiteLon + required property int onSiteMtl + required property int onSiteMum + required property int onSiteSyd + required property int onSiteVan + required property int index + + required property int noteCountRole + + required property var submittedToDailiesRole + required property var dateSubmittedToClientRole + + required property var createdDateRole + + property bool isPlaylist: false + + property bool isHovered: mArea.containsMouse || versionArrowBtn.hovered || sec1.playerMA.containsMouse + + signal playMovie(path: var) + + Connections { + target: resultsSelectionModel + function onSelectionChanged(selected, deselected) { + isSelected = resultsSelectionModel.isSelected(delegateModel.modelIndex(index)) + } + } + + MouseArea{ id: mArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + + // wierd workaround for flickable.. + propagateComposedEvents: false + onReleased: { + if(!propagateComposedEvents) + propagateComposedEvents = true + } + + onDoubleClicked: (mouse)=> { + let m = ShotBrowserHelpers.mapIndexesToResultModel([delegateModel.modelIndex(index)])[0].model + if(m.groupDetail.flags.includes("Load Sequence")) + ShotBrowserHelpers.addSequencesToNewPlaylist([delegateModel.modelIndex(index)]) + else + ShotBrowserHelpers.addToCurrent([delegateModel.modelIndex(index)], panelType != "ShotBrowser", addAfterSelection.value) + } + + onPressed: (mouse)=> { + // required for doubleclick to work + mouse.accepted = true + + if (mouse.button == Qt.RightButton){ + if(popupMenu.visible) popupMenu.visible = false + else{ + if(!isSelected) { + if(mouse.modifiers == Qt.NoModifier) { + ShotBrowserHelpers.selectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } else if(mouse.modifiers == Qt.ShiftModifier){ + ShotBrowserHelpers.shiftSelectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } else if(mouse.modifiers == Qt.ControlModifier) { + ShotBrowserHelpers.ctrlSelectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } + } + popupMenu.popupDelegateModel = delegateModel + let ppos = mapToItem(popupMenu.parent, mouseX, mouseY) + popupMenu.x = ppos.x + popupMenu.y = ppos.y + popupMenu.visible = true + } + } + + else if(mouse.modifiers == Qt.NoModifier) { + ShotBrowserHelpers.selectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } else if(mouse.modifiers == Qt.ShiftModifier){ + ShotBrowserHelpers.shiftSelectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } else if(mouse.modifiers == Qt.ControlModifier) { + ShotBrowserHelpers.ctrlSelectItem(resultsSelectionModel, delegateModel.modelIndex(index)) + } + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: 1 + + XsPrimaryButton{ id: versionArrowBtn + Layout.minimumWidth: 20 + Layout.maximumWidth: 20 + Layout.fillHeight: true + + visible: groupingEnabled && !modelDepth + imgSrc: "qrc:/icons/chevron_right.svg" + imageDiv.rotation: isActive ? 90 : 0 + isActiveViaIndicator: false + enabled: groupingEnabled && delegateModel.notifyModel.hasChildren(index) + isActive: groupingEnabled && delegateModel.notifyModel.isExpanded(index) + + onClicked: { + if(!isActive) { + delegateModel.notifyModel.expandRow(index) + isActive = delegateModel.notifyModel.isExpanded(index) + } else { + delegateModel.notifyModel.collapseRow(index) + isActive = delegateModel.notifyModel.isExpanded(index) + } + } + } + + ColumnLayout{ id: col + // width: visible? 20 : 0 + Layout.fillHeight: true + Layout.fillWidth: true + Layout.leftMargin: !versionArrowBtn.visible && groupingEnabled ? 46 : isPlaylist ? 27 : 0 + + spacing: itemSpacing + + Rectangle{ id: shotTitle + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: XsStyleSheet.widgetStdHeight + color: XsStyleSheet.widgetBgNormalColor + + XsText{ + anchors.fill: parent + horizontalAlignment: Text.AlignLeft + text: nameRole + font.pixelSize: XsStyleSheet.fontSize * 1.1 + font.bold: true + leftPadding: panelPadding + } + XsText{ + anchors.fill: parent + horizontalAlignment: Text.AlignRight + text: clientFilenameRole ? clientFilenameRole : "" + font.pixelSize: XsStyleSheet.fontSize * 1.1 + font.bold: true + rightPadding: panelPadding + opacity: 0.5 + } + } + + RowLayout{ + Layout.fillWidth: true + Layout.preferredHeight: (XsStyleSheet.widgetStdHeight * rowCount) + (spacing * (rowCount-1)) + spacing: itemSpacing + x: spacing + + property int rowCount: 3 + + ShotHistorySection1{ id: sec1 + Layout.minimumWidth: 154 + Layout.preferredWidth: 154 + Layout.fillHeight: true + } + + ShotHistorySection2{ id: sec2 + Layout.fillWidth: true + Layout.minimumWidth: 115 + Layout.fillHeight: true + } + + ShotHistorySection3{ id: sec3 + Layout.minimumWidth: 157 + Layout.preferredWidth: 157 + Layout.fillHeight: true + } + } + } + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryListDiv.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryListDiv.qml new file mode 100644 index 000000000..d09a5b8b0 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryListDiv.qml @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic + +import xStudio 1.0 +import ShotBrowser 1.0 + +XsListView { id: list + spacing: panelPadding + property int rightSpacing: list.height < list.contentHeight ? 16 : 0 + Behavior on rightSpacing {NumberAnimation {duration: 150}} + + ScrollBar.vertical: XsScrollBar { + visible: list.height < list.contentHeight + parent: list.parent + anchors.top: list.top + anchors.right: list.right + anchors.bottom: list.bottom + x: -5 + } + + XsSBMediaPlayer { + id: mediaPlayer + anchors.fill: parent + visible: false + onEject: visible = false + } + + function playMovie(path) { + mediaPlayer.loadMedia(path) + mediaPlayer.visible = true + mediaPlayer.playToggle() + } + + + XsLabel { + text: "Select the 'Scope' to view the Shot History." + color: XsStyleSheet.hintColor + visible: !activeScopeIndex.valid //#TODO + + anchors.fill: parent + + font.pixelSize: XsStyleSheet.fontSize*1.2 + font.weight: Font.Medium + } + + XsLabel { + text: !queryRunning ? "No Results Found" : "" + // text: isPaused ? "Updates Paused" : !queryRunning ? "No Results Found" : "" + color: XsStyleSheet.hintColor + visible: dataModel && activeScopeIndex.valid && !dataModel.count //#TODO + + anchors.fill: parent + + font.pixelSize: XsStyleSheet.fontSize*1.2 + font.weight: Font.Medium + } + + model: DelegateModel { + id: chooserModel + model: dataModel + delegate: ShotHistoryListDelegate{ + width: list.width - rightSpacing + height: XsStyleSheet.widgetStdHeight * 4 + delegateModel: chooserModel + popupMenu: resultPopup + onPlayMovie: (path) => list.playMovie(path) + } + } +} + diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryResultPopup.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryResultPopup.qml new file mode 100644 index 000000000..2436defc3 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryResultPopup.qml @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.clipboard 1.0 +import QuickFuture 1.0 + + +XsPopupMenu { + + id: rightClickMenu + visible: false + + property var popupSelectionModel + property var popupDelegateModel + + menu_model_name: "shothistory_menu_"+rightClickMenu + + Clipboard { + id: clipboard + } + + XsMenuModelItem { + text: "Add To New Playlist" + menuItemPosition: 0.1 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: ShotBrowserHelpers.addToNewPlaylist(popupSelectionModel.selectedIndexes) + } + + XsMenuModelItem { + menuItemType: "divider" + menuItemPosition: 0.5 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + } + + XsMenuModelItem { + text: "Select All" + menuItemPosition: 1 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: popupSelectionModel.select( + helpers.createItemSelectionFromList(ShotBrowserHelpers.getAllIndexes(popupDelegateModel)), + ItemSelectionModel.Select + ) + } + XsMenuModelItem { + text: "Deselect All" + menuItemPosition: 2 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: popupSelectionModel.clear() + } + XsMenuModelItem { + text: "Invert Selection" + menuItemPosition: 3 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: popupSelectionModel.select( + helpers.createItemSelectionFromList(ShotBrowserHelpers.getAllIndexes(popupDelegateModel)), + ItemSelectionModel.Toggle + ) + } + XsMenuModelItem { + menuItemType: "divider" + menuItemPosition: 4 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + } + XsMenuModelItem { + text: "Reveal In ShotGrid..." + (enabled ? "" : " (Production Only)") + enabled: ShotBrowserEngine.shotGridUserType != "User" + menuItemPosition: 5 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: ShotBrowserHelpers.revealInShotgrid(popupSelectionModel.selectedIndexes) + } + XsMenuModelItem { + text: "Reveal In Ivy..." + menuItemPosition: 6 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: ShotBrowserHelpers.revealInIvy(popupSelectionModel.selectedIndexes) + } + + XsMenuModelItem { + text: "Copy JSON" + menuItemPosition: 6.5 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: clipboard.text = JSON.stringify(ShotBrowserHelpers.getJSON(popupSelectionModel.selectedIndexes)) + } + + XsMenuModelItem { + text: "Copy DNUuid" + menuItemPosition: 6.6 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + onActivated: clipboard.text = ShotBrowserHelpers.getDNUuid(popupSelectionModel.selectedIndexes).join("\n") + } + + XsMenuModelItem { + menuItemType: "divider" + menuItemPosition: 7 + menuPath: "" + menuModelName: rightClickMenu.menu_model_name + } + // XsMenuModelItem { + // text: "Transfer Selected" + // menuItemPosition: 10 + // menuPath: "" + // menuModelName: rightClickMenu.menu_model_name + // onActivated: {} + // } + + XsMenuModelItem { + text: "To Here" + menuItemPosition: 0.5 + menuPath: "Transfer Selected" + menuModelName: rightClickMenu.menu_model_name + onActivated: ShotBrowserHelpers.transfer(helpers.getEnv("DNSITEDATA_SHORT_NAME"), popupSelectionModel.selectedIndexes) + } + XsMenuModelItem { + menuItemType: "divider" + menuItemPosition: 0.6 + menuPath: "Transfer Selected" + menuModelName: rightClickMenu.menu_model_name + } + + XsMenuModelItem { + text: "To Chennai" + menuItemPosition: 1 + menuPath: "Transfer Selected" + menuModelName: rightClickMenu.menu_model_name + onActivated: ShotBrowserHelpers.transfer("chn", popupSelectionModel.selectedIndexes) + } + XsMenuModelItem { + text: "To London" + menuItemPosition: 2 + menuPath: "Transfer Selected" + menuModelName: rightClickMenu.menu_model_name + onActivated: ShotBrowserHelpers.transfer("lon", popupSelectionModel.selectedIndexes) + } + XsMenuModelItem { + text: "To Montreal" + menuItemPosition: 3 + menuPath: "Transfer Selected" + menuModelName: rightClickMenu.menu_model_name + onActivated: ShotBrowserHelpers.transfer("mtl", popupSelectionModel.selectedIndexes) + } + XsMenuModelItem { + text: "To Mumbai" + menuItemPosition: 4 + menuPath: "Transfer Selected" + menuModelName: rightClickMenu.menu_model_name + onActivated: ShotBrowserHelpers.transfer("mum", popupSelectionModel.selectedIndexes) + } + XsMenuModelItem { + text: "To Sydney" + menuItemPosition: 5 + menuPath: "Transfer Selected" + menuModelName: rightClickMenu.menu_model_name + onActivated: ShotBrowserHelpers.transfer("syd", popupSelectionModel.selectedIndexes) + } + XsMenuModelItem { + text: "To Vancouver" + menuItemPosition: 6 + menuPath: "Transfer Selected" + menuModelName: rightClickMenu.menu_model_name + onActivated: ShotBrowserHelpers.transfer("van", popupSelectionModel.selectedIndexes) + } + XsMenuModelItem { + menuItemType: "divider" + menuItemPosition: 7 + menuPath: "Transfer Selected" + menuModelName: rightClickMenu.menu_model_name + } + XsMenuModelItem { + text: "Open Transfer Tool" + menuItemPosition: 8 + menuPath: "Transfer Selected" + menuModelName: rightClickMenu.menu_model_name + onActivated: { + let uuids = [] + if(popupSelectionModel.selectedIndexes.length) { + let indexes = ShotBrowserHelpers.mapIndexesToResultModel(popupSelectionModel.selectedIndexes) + let m = indexes[0].model + for(let i = 0; i< indexes.length; i++) { + let uuid = m.get(indexes[i], "stalkUuidRole") + if(uuid) + uuids.push(helpers.QUuidToQString(uuid)) + } + } + helpers.startDetachedProcess("dnenv-do", [helpers.getEnv("SHOW"), "--", "maketransfer"].concat(uuids)) + } + + Component.onCompleted: { + // we need this so the menu model knows where to insert the + // "Transfer Selected" sub menu in the top level menu + setMenuPathPosition("Transfer Selected", 8.0) + } + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryTitleDiv.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryTitleDiv.qml new file mode 100644 index 000000000..a7fcfaa2f --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/ShotHistoryTitleDiv.qml @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 +import ShotBrowser 1.0 +import xstudio.qml.helpers 1.0 + +RowLayout {id: titleDiv + + property int titleButtonCount: 4 + property real titleButtonSpacing: 1 + property real titleButtonHeight: XsStyleSheet.widgetStdHeight+4 + + XsPrimaryButton{ id: updateScopeBtn + Layout.preferredWidth: 40 + Layout.fillHeight: true + + imgSrc: isPanelEnabled && !isPaused ? "qrc:///shotbrowser_icons/lock_open.svg" : "qrc:///shotbrowser_icons/lock.svg" + // text: isPanelEnabled? "ON" : "OFF" + isActive: !isPanelEnabled || isPaused + onClicked: isPanelEnabled = !isPanelEnabled + } + + ColumnLayout{ id: col + Layout.fillWidth: true + Layout.fillHeight: true + + spacing: titleButtonSpacing + + RowLayout{ + Layout.fillWidth: true + Layout.preferredHeight: titleButtonHeight + spacing: 0 + + + XsText{ id: scopeTxt + Layout.preferredWidth: (textWidth + panelPadding*3) + Layout.fillHeight: true + text: "Scope: " + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + enabled: isPanelEnabled && !isPaused + spacing: titleButtonSpacing + + Repeater { + model: ShotBrowserEngine.presetsModel.shotHistoryScope + + XsPrimaryButton{ + Layout.fillWidth: true + Layout.fillHeight: true + text: ShotBrowserEngine.presetsModel.get(modelData, "nameRole") + isActive: activeScopeIndex == modelData + property bool isRunning: queryRunning && isActive + + onClicked: { + activateScope(modelData) + } + XsBusyIndicator{ + x: 4 + width: height + height: parent.height + running: visible + visible: isRunning + scale: 0.5 + } + } + } + } + } + RowLayout{ + Layout.fillWidth: true + Layout.preferredHeight: titleButtonHeight + spacing: 0 + + XsSearchButton{ id: filterBtn + Layout.fillWidth: true + Layout.fillHeight: true + isExpanded: true + hint: "Filter" + buttonWidth: scopeTxt.width + enabled: isPanelEnabled && !isPaused + + onTextChanged: nameFilter = text + onEditingCompleted: forceActiveFocus(panel) + + Connections { + target: panel + function onNameFilterChanged() { + filterBtn.text = nameFilter + } + } + } + + XsComboBoxEditable{ id: filterSentTo + Layout.fillHeight: true + Layout.minimumWidth: titleButtonHeight * 3 + Layout.preferredWidth: titleButtonHeight * 3 + + enabled: isPanelEnabled && !isPaused + + model: ShotBrowserEngine.ready ? ShotBrowserEngine.presetsModel.termModel("Sent To") : [] + currentIndex: -1 + textRole: "nameRole" + displayText: currentIndex==-1? "Sent To" : currentText + + onModelChanged: currentIndex = -1 + + onCurrentIndexChanged: { + if(currentIndex==-1) + sentTo = "" + } + + onAccepted: { + sentTo = model.get(model.index(currentIndex,0), "nameRole") + } + + onActivated: sentTo = model.get(model.index(currentIndex,0), "nameRole") + + Connections { + target: panel + function onSentToChanged() { + filterSentTo.currentIndex = filterSentTo.find(sentTo) + } + } + } + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/dataItems/ShotHistoryTextRow.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/dataItems/ShotHistoryTextRow.qml new file mode 100644 index 000000000..b286e69d4 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/dataItems/ShotHistoryTextRow.qml @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 + +Rectangle{ + + property alias text: textDiv.text + property alias textDiv: textDiv + property alias textColor: textDiv.color + + color: XsStyleSheet.widgetBgNormalColor + + XsText{ id: textDiv + text: "" + color: XsStyleSheet.hintColor + // height: parent.height + anchors.verticalCenter: parent.verticalCenter + anchors.fill: parent + elide: Text.ElideRight + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/sections/ShotHistorySection1.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/sections/ShotHistorySection1.qml new file mode 100644 index 000000000..96b15fd8f --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/sections/ShotHistorySection1.qml @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects + +import xStudio 1.0 +import ShotBrowser 1.0 + + +Rectangle{ + color: XsStyleSheet.widgetBgNormalColor + property alias playerMA: playerMA + + RowLayout { + anchors.fill: parent + spacing: itemSpacing + + Rectangle{ id: indicators + Layout.preferredWidth: 20 + Layout.fillHeight: true + color: "transparent" + + Column{ + y: spacing + anchors.fill: parent + spacing: itemSpacing + + XsPrimaryButton{ id: notesIndicatorDisplay + property bool hasNotes: noteCountRole <= 0 ? false : true + text: "N" + width: 20 + height: parent.height/3 - itemSpacing + font.pixelSize: textSize*1.2 + font.weight: hasNotes? Font.Bold:Font.Medium + isUnClickable: true + isActiveViaIndicator: false + textDiv.color: hasNotes? palette.text : XsStyleSheet.hintColor + enabled: false + bgDiv.opacity: enabled? 1.0 : 0.5 + isActive: hasNotes + } + XsPrimaryButton{ id: dailiesIndicatorDisplay + property bool hasDailies: submittedToDailiesRole === undefined ? false :true + text: "D" //dalies + width: 20 + height: parent.height/3 - itemSpacing + font.pixelSize: textSize*1.2 + font.weight: hasDailies? Font.Bold:Font.Medium + isUnClickable: true + isActiveViaIndicator: false + textDiv.color: hasDailies? palette.text : XsStyleSheet.hintColor + enabled: false + bgDiv.opacity: enabled? 1.0 : 0.5 + isActive: hasDailies + } + XsPrimaryButton{ id: clientIndicatorDisplay + property bool hasClient: dateSubmittedToClientRole === undefined ? false : true + text: "C" //client + width: 20 + height: parent.height/3 - itemSpacing + font.pixelSize: textSize*1.2 + font.weight: hasClient? Font.Bold:Font.Medium + isUnClickable: true + isActiveViaIndicator: false + textDiv.color: hasClient? palette.text : XsStyleSheet.hintColor + enabled: false + bgDiv.opacity: enabled? 1.0 : 0.5 + isActive: hasClient + } + + } + + } + + Item{ id: thumbImage + Layout.fillWidth: true + Layout.fillHeight: true + + property bool failed: thumbRole == undefined + + Rectangle{ + width: parent.width + height: Math.min(width / (16/9), thumbImage.height) - 10 + color: Qt.lighter(panelColor, 1.1) + visible: parent.failed + anchors.verticalCenter: parent.verticalCenter + } + XsText{ id: noThumbnailDisplay + text: parent.failed? "No ShotGrid\nThumbnail":"Loading..." + width: thumbnail.width + height: thumbnail.height + anchors.centerIn: parent + wrapMode: Text.WordWrap + font.pixelSize: textSize*1.2 + font.weight: Font.Black + opacity: parent.failed? 0.4 : 0.9 + } + + XsImage{ id: thumbnail + visible: !parent.failed + width: parent.width + height: Math.min(width / (16/9), thumbImage.height) + sourceSize.height: height + sourceSize.width: width + source: parent.failed || thumbRole==undefined? "": thumbRole + fillMode: Image.PreserveAspectFit + anchors.verticalCenter: parent.verticalCenter + clip: true + cache: true + asynchronous: true + imgOverlayColor: "transparent" + + opacity: 0 + Behavior on opacity {NumberAnimation {duration: 150}} + + onStatusChanged: { + if (status == Image.Null) { parent.failed=true; opacity=0 } + else if (status == Image.Error) { parent.failed=true; opacity=0; noThumbnailDisplay.text= "Thumbnail\nError" } + else if (status == Image.Ready) opacity=1 + else opacity=0 + } + } + + + MouseArea { + id: playerMA + + property bool movieFailed: false + + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: 4 + height: parent.height / 3 + width: height + + enabled: !parent.failed + onClicked: { + let url = ShotBrowserEngine.downloadMedia(idRole, nameRole, projectRole, entityRole) + if(url != "") + playMovie(url) + else { + movieFailed = true + } + } + hoverEnabled: true + + Item { + anchors.fill: parent + visible: isHovered && playerMA.enabled + + // Note: we can't use 'icon' as source for MultiEffect as + // XsIcon already has a MultiEffect layer and this is + // buggy in QML + MultiEffect { + source: img + anchors.fill: icon + shadowBlur: 0.1 + shadowEnabled: true + shadowColor: "black" + shadowVerticalOffset: 2 + shadowHorizontalOffset: 2 + } + + Image { + + id: img + anchors.fill: parent + source: "qrc:/icons/play_circle.svg" + sourceSize.height: height + sourceSize.width: width + visible: false + } + + XsIcon{ + id: icon + anchors.fill: parent + source: "qrc:/icons/play_circle.svg" + imgOverlayColor: playerMA.movieFailed ? "red" : (playerMA.containsMouse ? palette.text : palette.highlight) + } + } + } + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/sections/ShotHistorySection2.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/sections/ShotHistorySection2.qml new file mode 100644 index 000000000..ddf494f0f --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/sections/ShotHistorySection2.qml @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 +import ShotBrowser 1.0 + +Rectangle{ + color: "transparent" + + ColumnLayout { + anchors.fill: parent + spacing: itemSpacing + + ShotHistoryTextRow{ id: authorDiv + Layout.fillWidth: true + Layout.fillHeight: true + + textDiv.leftPadding: panelPadding + textDiv.horizontalAlignment: Text.AlignLeft + + text: authorRole + textColor: palette.text + } + ShotHistoryTextRow{ id: dateDiv + Layout.fillWidth: true + Layout.minimumHeight: XsStyleSheet.widgetStdHeight + + textDiv.leftPadding: panelPadding + textDiv.horizontalAlignment: Text.AlignLeft + + property var dateFormatted: createdDateRole.toLocaleString().split(" ") + + text: typeof dateFormatted !== 'undefined'? dateFormatted[1].substr(0,3)+" "+dateFormatted[2]+" "+dateFormatted[3] : "" + } + ShotHistoryTextRow{ id: frameRangeDiv + Layout.fillWidth: true + Layout.minimumHeight: XsStyleSheet.widgetStdHeight + + textDiv.leftPadding: panelPadding + textDiv.horizontalAlignment: Text.AlignLeft + + text: frameRangeRole + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/sections/ShotHistorySection3.qml b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/sections/ShotHistorySection3.qml new file mode 100644 index 000000000..9ca77d6d0 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shot_history/sections/ShotHistorySection3.qml @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts + +import xStudio 1.0 +import ShotBrowser 1.0 + +Rectangle{ + color: "transparent" + + ColumnLayout { + anchors.fill: parent + spacing: itemSpacing + + ShotHistoryTextRow{ id: stepDiv + Layout.fillWidth: true + Layout.fillHeight: true + text: pipelineStepRole + textColor: palette.text + textDiv.width: width + } + ShotHistoryTextRow{ id: statusDiv + Layout.fillWidth: true + Layout.minimumHeight: XsStyleSheet.widgetStdHeight + text: pipelineStatusFullRole + textDiv.width: width + } + + Rectangle{ id: siteDiv + Layout.fillWidth: true + Layout.minimumHeight: XsStyleSheet.widgetStdHeight + color: XsStyleSheet.widgetBgNormalColor + + Grid{ id: siteGrid + width: parent.width - itemSpacing*(siteModel.count-1) + height: parent.height + anchors.verticalCenter: parent.verticalCenter + rows: 1 + columns: siteModel.count + spacing: itemSpacing + flow: Grid.LeftToRight + + Repeater{ + model: siteModel + + XsPrimaryButton{ + + property int onDisk: { + if(index==0) onSiteChn + else if(index==1) onSiteLon + else if(index==2) onSiteMtl + else if(index==3) onSiteMum + else if(index==4) onSiteSyd + else if(index==5) onSiteVan + else false + } + + property real desiredWidth: siteGrid.width/siteModel.count > 40? 40 : siteGrid.width/siteModel.count + + width: desiredWidth + height: siteGrid.height + + isUnClickable: true + enabled: false + text: siteName + textDiv.color: onDisk? palette.text : XsStyleSheet.hintColor + font.pixelSize: textSize/1.4 + font.weight: Font.Medium + + bgDiv.opacity: enabled? 1.0 : 0.5 + forcedBgColorNormal: onDisk ? onDisk == 1? "transparent" + : Qt.darker(siteColour, 1) + : Qt.lighter(panelColor, 1.1) + + Rectangle{ + width: parent.width + height: parent.height + anchors.bottom: parent.bottom + visible: parent.onDisk && parent.onDisk == 1 + opacity: parent.enabled? 1.0 : 0.5 + z:-1 + // border.width: itemSpacing + // border.color: siteColour + gradient: Gradient { + GradientStop { position: 0.4; color: Qt.lighter(panelColor, 1.1) } + GradientStop { position: 0.8; color: Qt.darker(siteColour, 1) } + GradientStop { position: 1.0; color: Qt.darker(siteColour, 1) } + } + } + + } + } + + ListModel{ + id: siteModel + ListElement{siteName:"chn"; siteColour:"#508f00"} + ListElement{siteName:"lon"; siteColour:"#2b7ffc"} + ListElement{siteName:"mtl"; siteColour:"#979700"} + ListElement{siteName:"mum"; siteColour:"#ef9526"} + ListElement{siteName:"syd"; siteColour:"#008a46"} + ListElement{siteName:"van"; siteColour:"#7a1a39"} + } + + } + } + } +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shotbrowser.qrc b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shotbrowser.qrc new file mode 100644 index 000000000..e718fa003 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/qml/ShotBrowser.1/shotbrowser.qrc @@ -0,0 +1,36 @@ + + + icons/keyboard_return.svg + icons/account_tree.svg + icons/arrow_right.svg + icons/attach_file.svg + icons/bookmark_heart.svg + icons/calendar_month.svg + icons/cloud.svg + icons/drag_indicator.svg + icons/edit.svg + icons/equal.svg + icons/exclamation.svg + icons/favorite.svg + icons/globe.svg + icons/heart_plus.svg + icons/key.svg + icons/left_panel_open.svg + icons/lock.svg + icons/lock_open.svg + icons/manage_accounts.svg + icons/nature.svg + icons/park.svg + icons/right_panel_open.svg + icons/schedule.svg + icons/segment.svg + icons/settings.svg + icons/tree_plus.svg + icons/unfold_more.svg + icons/up_arrow.svg + icons/note_history.svg + icons/shot_history.svg + icons/shot_grid.svg + icons/shotgun.png + + \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/query_engine.cpp b/src/plugin/data_source/dneg/shotbrowser/src/query_engine.cpp new file mode 100644 index 000000000..007afed0a --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/query_engine.cpp @@ -0,0 +1,2120 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "definitions.hpp" +#include "query_engine.hpp" +#include "xstudio/atoms.hpp" +#include "xstudio/shotgun_client/shotgun_client.hpp" +#include "xstudio/utility/string_helpers.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/uuid.hpp" + +using namespace xstudio; +using namespace xstudio::shotgun_client; +using namespace xstudio::utility; + +namespace { +std::chrono::system_clock::duration duration_since_midnight() { + auto now = std::chrono::system_clock::now(); + + time_t tnow = std::chrono::system_clock::to_time_t(now); + tm *date = std::localtime(&tnow); + date->tm_hour = 0; + date->tm_min = 0; + date->tm_sec = 0; + auto midnight = std::chrono::system_clock::from_time_t(std::mktime(date)); + + return now - midnight; +} +}; // namespace + +QueryEngine::QueryEngine() { + set_lookup(cache_name("Completion Location"), utility::JsonStore(locationsJSON), lookup_); + set_lookup(cache_name("Twig Type"), TwigTypeCodes, lookup_); + + set_cache(cache_name("Twig Type"), TwigTypeCodes); + set_cache(cache_name("Client Note"), BoolTermValues); + set_cache(cache_name("Completion Location"), locationsJSON); + set_cache(cache_name("Flag Media"), FlagTermValues); + set_cache(cache_name("Has Attachments"), BoolTermValues); + set_cache(cache_name("Has Contents"), BoolTermValues); + set_cache(cache_name("Has Notes"), BoolTermValues); + set_cache(cache_name("Is Hero"), BoolTermValues); + set_cache(cache_name("Viewable"), BoolTermValues); + set_cache(cache_name("Latest Version"), BoolTermValues); + set_cache(cache_name("Lookback"), LookbackTermValues); + set_cache(cache_name("On Disk"), OnDiskTermValues); + set_cache(cache_name("Order By"), OrderByTermValues); + set_cache(cache_name("Preferred Audio"), SourceTermValues); + set_cache(cache_name("Preferred Visual"), SourceTermValues); + set_cache(cache_name("Preferred Sequence"), SequenceTermValues); + set_cache(cache_name("Result Limit"), ResultLimitTermValues); + set_cache(cache_name("Sent To Client"), BoolTermValues); + set_cache(cache_name("Sent To"), SubmittedTermValues); + set_cache(cache_name("Sent To Dailies"), BoolTermValues); + set_cache(cache_name("Operator"), OperatorTermValues); + + for (const auto &i : ValidTerms.items()) { + auto key = std::string("Disable Global-") + i.key(); + auto values = R"([])"_json; + for (const auto &j : i.value()) { + auto v = R"({"name":null})"_json; + v["name"] = j; + values.emplace_back(v); + } + set_cache(cache_name(key), values); + } + + initialise_presets(); +} + + +std::optional +QueryEngine::find_by_id(const utility::Uuid &uuid, const utility::JsonStore &presets) { + if (presets.is_object()) { + if (presets.count("id") && presets.at("id").get() == uuid) + return presets; + else { + if (presets.count("children")) { + for (const auto &i : presets.at("children")) { + auto r = find_by_id(uuid, i); + if (r) + return *r; + } + } + } + } else if (presets.is_array()) { + for (const auto &i : presets) { + auto r = find_by_id(uuid, i); + if (r) + return *r; + } + } + + return {}; +} + + +template +T QueryEngine::to_value(const nlohmann::json &jsn, const std::string &key, const T &fallback) { + auto result = fallback; + + try { + if (jsn.count(key) and not jsn.at(key).is_null()) + result = jsn.at(key).get(); + } catch (...) { + } + + return result; +} + +void QueryEngine::initialise_presets() { + auto user_tmp = RootTemplate; + user_tmp["id"] = Uuid::generate(); + user_presets_ = JsonStoreSync(user_tmp); + + auto system_tmp = RootTemplate; + system_tmp["id"] = Uuid::generate(); + system_presets_ = JsonStoreSync(system_tmp); +} + +utility::JsonStore QueryEngine::validate_presets( + const utility::JsonStore &data, + const bool is_user, + const utility::JsonStore &parent, + const size_t index, + const bool export_as_system) { + auto result = R"({})"_json; + + auto type = data.value("type", std::string()); + auto ptype = parent.is_null() ? std::string() : parent.value("type", std::string()); + if (type == "root") { + result = RootTemplate; + result.update(data); + + if (not parent.is_null() and not export_as_system) { + spdlog::warn("{} parent of root should be null", __PRETTY_FUNCTION__); + } + + auto new_children = json::array(); + for (size_t i = 0; i < result["children"].size(); i++) { + auto child = + validate_presets(result["children"][i], is_user, data, i, export_as_system); + if (not child.is_null()) + new_children.push_back(child); + } + result["children"] = new_children; + } else if (type == "group") { + result = GroupTemplate; + result.update(data); + + if (ptype != "root" and not export_as_system) { + spdlog::warn( + "{} parent of group should be root {} {}", + __PRETTY_FUNCTION__, + data.value("id", ""), + ptype); + } + + if (result["children"].size() != 2 and not export_as_system) { + spdlog::warn( + "{} groups must have 2 children {} {}", + __PRETTY_FUNCTION__, + data.value("id", ""), + result["children"].size()); + } + + if (not is_user and (result["update"].is_null() or result["update"] != false)) { + if (not export_as_system) + spdlog::warn("Fix group update flag {}", result["name"].get()); + result["update"] = false; + } + + auto userdata = data.value("userdata", std::string()); + if (userdata != "recent" and userdata != "menus" and userdata != "tree" and + not export_as_system) + spdlog::warn( + "{} invalid group userdata {} {}", + __PRETTY_FUNCTION__, + data.value("id", ""), + userdata); + + // check for hidden systen preset == purge when export as system + if (export_as_system and not is_user and result["hidden"] == true) + result = JsonStore(); + else { + for (size_t i = 0; i < result["children"].size(); i++) + result["children"][i] = + validate_presets(result["children"][i], is_user, data, i, export_as_system); + } + } else if (type == "presets") { + result = data; + + if (ptype != "group" and not export_as_system) { + spdlog::warn( + "{} parent of presets should be group {} {}", + __PRETTY_FUNCTION__, + data.value("id", ""), + ptype); + } + + if (index != 1 and not export_as_system) { + spdlog::warn( + "{} index of presets must be 1 {} {} {}", + __PRETTY_FUNCTION__, + data.value("id", ""), + index, + ptype); + } + + auto new_children = json::array(); + for (size_t i = 0; i < result["children"].size(); i++) { + auto child = + validate_presets(result["children"][i], is_user, data, i, export_as_system); + if (not child.is_null()) + new_children.push_back(child); + } + result["children"] = new_children; + } else if (type == "term") { + result = TermTemplate; + result.update(data); + + if (ptype == "term" and parent.value("term", "") != "Operator") { + if (not export_as_system) + spdlog::warn( + "{} parent of term invalid {} {}", + __PRETTY_FUNCTION__, + data.value("id", ""), + ptype); + } else if (ptype != "preset" and ptype != "term") { + if (not export_as_system) + spdlog::warn( + "{} parent of term should be preset {} {}", + __PRETTY_FUNCTION__, + data.value("id", ""), + ptype); + } + + if (result.value("term", "") != "Operator") { + if (result.contains("children")) + result.erase("children"); + } else { + for (size_t i = 0; i < result["children"].size(); i++) + result["children"][i] = + validate_presets(result["children"][i], is_user, data, i, export_as_system); + } + + } else if (type == "preset") { + result = PresetTemplate; + result.update(data); + + if (ptype != "presets" and ptype != "group") { + if (not export_as_system) + spdlog::warn( + "{} parent of preset should be presets or group {} {}", + __PRETTY_FUNCTION__, + data.value("id", ""), + ptype); + } + + if (ptype == "group" and index != 0) { + if (not export_as_system) + spdlog::warn( + "{} index of group override must be 0 {} {} {}", + __PRETTY_FUNCTION__, + data.value("id", ""), + index, + ptype); + } + + // fix overrides. + if (ptype == "group") { + result.update(R"({"hidden": true, "favourite": false})"_json); + } + + if (not is_user and (result["update"].is_null() or result["update"] != false)) { + if (not export_as_system) + spdlog::warn("Fix preset update flag {}", result["name"].get()); + result["update"] = false; + } + + if (export_as_system and not is_user and result["hidden"] == true and ptype != "group") + result = JsonStore(); + else { + for (size_t i = 0; i < result["children"].size(); i++) + result["children"][i] = + validate_presets(result["children"][i], is_user, data, i, export_as_system); + } + + } else { + result = data; + } + + return result; +} + +void QueryEngine::set_presets( + const utility::JsonStore &user, const utility::JsonStore &system) { + // purge children + nlohmann::json user_tmp = user_presets_.as_json(); + user_tmp["children"] = user; + + nlohmann::json system_tmp = system_presets_.as_json(); + system_tmp["children"] = system; + + // fix presets.. + // recursively validate entries. + system_tmp = validate_presets(system_tmp); + + system_presets_.reset_data(system_tmp); + + // fix presets.. + user_tmp = validate_presets(user_tmp, true); + + // we need to merge system presets into user + merge_presets(user_tmp["children"], system_tmp["children"]); + + user_presets_.reset_data(user_tmp); +} + +// iterate over system preset groups. +void QueryEngine::merge_presets(nlohmann::json &destination, const nlohmann::json &source) { + for (const auto &i : source) { + if (i.value("type", "") == "group") + merge_group(destination, i); + else + spdlog::warn("{} Invalid group {}", __PRETTY_FUNCTION__, i.dump(2)); + } +} + +void QueryEngine::merge_group(nlohmann::json &destination, const nlohmann::json &source) { + // does group exist in user_presets.. + try { + bool group_exists = false; + auto group_id = source.value("id", Uuid()); + + for (auto &i : destination) { + if (i.value("type", "") == "group" and i.value("id", Uuid()) == group_id) { + group_exists = true; + + // update group name / entity? + + if (not i.value("update", false)) + i["flags"] = source.at("flags"); + + if (i.contains("update") and not i.at("update").is_null()) + i["name"] = source.at("name"); + + // validate group overrides + if (not i.at("children").at(0).value("update", false) and + preset_diff(i.at("children").at(0), source.at("children").at(0))) { + // spdlog::warn("overrides differ"); + // replace content.. as update flag not set by user change. + i["children"][0] = source.at("children").at(0); + } + + // validate group presets. add or update + std::set found_system_presets; + for (const auto &gp : source.at("children").at(1).at("children")) { + auto preset_id = gp.value("id", Uuid()); + found_system_presets.insert(preset_id); + bool preset_exists = false; + + for (auto &up : i["children"][1]["children"]) { + if (up.value("type", "") == "preset" and + up.value("id", Uuid()) == preset_id) { + preset_exists = true; + + // validate group preset + if (not up.value("update", false) and preset_diff(up, gp)) { + // replace content.. as update flag not set by user change. + up["name"] = gp.at("name"); + up["children"] = gp.at("children"); + } + } + } + + if (not preset_exists) + i["children"][1]["children"].push_back(gp); + } + + // do a reverse validation. + // if the user has a system preset that hasn't been modified and doesn't exist + // in this list.. but this will then break project presets.. + auto tmp = R"([])"_json; + + for (const auto &up : i["children"][1]["children"]) { + // spdlog::warn("checking {}", up.at("name").get()); + if (up.contains("update") and not up.at("update").is_null() and + up.at("update") == false) { + // is system preset, check it's in source. + if (found_system_presets.count(up.value("id", Uuid()))) + tmp.push_back(up); + else + spdlog::debug( + "Removing retired system preset {}", + up.at("name").get()); + } else { + // orpahaned system presets should change to user presets. + if (up.contains("update") and not up.at("update").is_null() and + up.at("update") == true and + not found_system_presets.count(up.value("id", Uuid()))) { + + auto tup = up; + tup["update"] = nullptr; + tmp.push_back(tup); + } else + tmp.push_back(up); + } + } + + + i["children"][1]["children"] = tmp; + } + } + + if (not group_exists) { + // spdlog::warn("Add new group {}", system_group.value("name", "")); + destination.push_back(source); + } + } catch (const std::exception &err) { + spdlog::warn( + "{} {} {} {}", + __PRETTY_FUNCTION__, + err.what(), + destination.dump(2), + source.dump(2)); + } +} + +std::set +QueryEngine::precache_needed(const int project_id, const utility::JsonStore &lookup) { + // check expected keys exist.. + auto result = std::set(); + + static const auto lookup_names = std::vector({ + "Project", + "Department", + "Pipeline Status", + "Production Status", + "Sequence Status", + "Shot Status", + }); + + static const auto lookup_project_names = std::vector( + {"Asset", "User", "Unit", "Stage", "Playlist", "Shot", "Sequence", "Episode"}); + + for (const auto &i : lookup_names) { + if (not lookup.count(cache_name(i))) + result.insert(i); + } + + if (project_id > 0) { + for (const auto &i : lookup_project_names) { + if (not lookup.count(cache_name(i, project_id))) + result.insert(i); + } + } + + return result; +} + +void QueryEngine::set_asset_list_cache( + const std::string &key, const utility::JsonStore &data, utility::JsonStore &cache) { + const static auto sg_status_list = json::json_pointer("/attributes/sg_status_list"); + + auto result = R"([])"_json; + + try { + auto row = R"({"id": null, "name": null, "type": null, "attributes": {}})"_json; + for (const auto &i : data) { + row["id"] = i.at("id"); + row["type"] = i.at("type"); + row["name"] = i.at(json::json_pointer("/attributes/sg_asset_name")); + row["attributes"]["sg_status_list"] = i.at(sg_status_list); + + result.push_back(row); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + cache[key] = result; +} + +void QueryEngine::set_shot_sequence_list_cache( + const std::string &key, const utility::JsonStore &data, utility::JsonStore &cache) { + const static auto sg_status_list = json::json_pointer("/attributes/sg_status_list"); + const static auto sg_unit = json::json_pointer("/relationships/sg_unit/data/name"); + const static auto sg_shot_type = json::json_pointer("/attributes/sg_shot_type"); + const static auto sg_sequence_type = json::json_pointer("/attributes/sg_sequence_type"); + + auto result = R"([])"_json; + + try { + auto row = R"({"id": null, "name": null, "type": null})"_json; + for (const auto &i : data[1]) { + row["id"] = i.at("id"); + row["type"] = i.at("type"); + row["name"] = i.at(json::json_pointer("/attributes/code")); + row["attributes"]["sg_status_list"] = i.at(sg_status_list); + row["subtype"] = + i.at(sg_sequence_type).is_null() ? "No Type" : i.at(sg_sequence_type); + + result.push_back(row); + + for (const auto &s : i.at(json::json_pointer("/relationships/shots/data"))) { + row["id"] = s.at("id"); + row["type"] = s.at("type"); + row["name"] = s.at("name"); + row["attributes"]["sg_status_list"] = s.at(sg_status_list); + if (s.contains(sg_unit)) + row["relationships"]["sg_unit"]["data"]["name"] = s.at(sg_unit); + else + row["relationships"]["sg_unit"]["data"]["name"] = ""; + + row["subtype"] = s.at(sg_shot_type).is_null() ? "No Type" : s.at(sg_shot_type); + + result.push_back(row); + } + } + for (const auto &i : data[0]) { + row["id"] = i.at("id"); + row["type"] = i.at("type"); + row["name"] = i.at(json::json_pointer("/attributes/code")); + row["attributes"]["sg_status_list"] = i.at(sg_status_list); + row["subtype"] = "Episode"; + result.push_back(row); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + // spdlog::warn("{} {}", key, result.dump(2)); + + cache[key] = result; +} + +void QueryEngine::set_shot_sequence_list_cache( + const std::string &key, const utility::JsonStore &data) { + set_shot_sequence_list_cache(key, data, cache_); + if (cache_changed_callback_) + cache_changed_callback_(key); +} + +void QueryEngine::set_asset_list_cache(const std::string &key, const utility::JsonStore &data) { + set_asset_list_cache(key, data, cache_); + if (cache_changed_callback_) + cache_changed_callback_(key); +} + +bool QueryEngine::preset_diff(const nlohmann::json &a, const nlohmann::json &b) { + auto result = true; + + try { // term count check (quick) + if (a.at("name") == b.at("name") and + a.at("children").size() == b.at("children").size()) { + // term comparison.. + bool mismatch = false; + + for (const auto &ta : a.at("children")) { + auto ta_id = ta.value("id", Uuid()); + mismatch = true; + + for (const auto &tb : b.at("children")) { + if (tb.value("id", Uuid()) == ta_id) { + if (ta.at("term") == tb.at("term") and + ta.at("negated") == tb.at("negated") and + ta.at("enabled") == tb.at("enabled") and + ta.at("livelink") == tb.at("livelink") and + (ta.at("value") == tb.at("value") or + (not ta.at("livelink").is_null() and ta.value("livelink", false)))) + mismatch = false; + + break; + } + } + + if (mismatch) + break; + } + + if (not mismatch) + result = false; + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; +} + +shotgun_client::FilterBy QueryEngine::terms_to_query( + const JsonStore &terms, + const int project_id, + const std::string &entity, + const utility::JsonStore &lookup, + const bool and_mode, + const bool initial) { + + auto result = FilterBy(BoolOperator::AND); + + if (initial) { + // add terms we always want. + result.emplace_back(Number("project.Project.id").is(project_id)); + + if (entity == "Playlists") { + result.emplace_back(Text("sg_status").is_not("arc")); + } else if (entity == "Versions") { + result.emplace_back(Text("sg_deleted").is_null()); + // result.emplace_back(FilterBy().Or( + // Text("sg_path_to_movie").is_not_null(), + // Text("sg_path_to_frames").is_not_null())); + } else if (entity == "Notes") { + } + } else if (not and_mode) { + result = FilterBy(BoolOperator::OR); + } + + for (const auto &i : terms) { + if (i.at("term") == "Operator") { + result.emplace_back(terms_to_query( + i.at("children"), project_id, entity, lookup, i.at("value") == "And", false)); + } else { + try { + add_term_to_filter(entity, i, project_id, lookup, &result); + } catch (const XStudioError &err) { + spdlog::debug("{} {} {}", __PRETTY_FUNCTION__, err.what(), i.dump(2)); + } catch (const std::exception &err) { + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), i.dump(2)); + } + } + } + + return result; +} + +utility::JsonStore QueryEngine::apply_livelinks( + const utility::JsonStore &terms, + const utility::JsonStore &metadata, + const utility::JsonStore &lookup) { + + auto result = utility::JsonStore(R"([])"_json); + + // apply livelinks.. + // update livelink data + for (auto &i : terms) { + auto t = i; + if (t.at("term") == "Operator") { + t["children"] = apply_livelinks(t["children"], metadata, lookup); + } else if (to_value(t, "livelink", false)) { + auto linkvalue = get_livelink_value(t.value("term", ""), metadata, lookup); + if (not linkvalue.is_null() and t["value"] != linkvalue) + t["value"] = linkvalue; + } + result.emplace_back(t); + } + + return result; +} + +utility::JsonStore QueryEngine::get_default_env() { + utility::JsonStore result(R"({})"_json); + + result["USER"] = get_login_name(); + result["USERFULLNAME"] = get_user_name(); + result["DNSITEDATA_SHORT_NAME"] = get_env("DNSITEDATA_SHORT_NAME", ""); + + return result; +} + +void QueryEngine::replace_with_env( + nlohmann::json &value, const utility::JsonStore &env, const utility::JsonStore &lookup) { + const static std::regex env_regex(R"(\$\{.+?\})"); + + if (value.is_string()) { + auto s = value.get(); + auto done = false; + auto env_end = std::sregex_iterator(); + + while (true) { + auto env_begin = std::sregex_iterator(s.begin(), s.end(), env_regex); + if (env_begin != env_end) { + std::smatch match = *env_begin; + auto key = match.str().substr(2, match.str().size() - 3); + replace_string_in_place( + s, match.str(), QueryEngine::to_value(env, key, std::string(""))); + } else + break; + } + if (value.get() != s) + value = s; + } else if (value.is_array()) { + // spdlog::warn("{}",value.dump(2)); + } +} + +utility::JsonStore QueryEngine::apply_env( + const utility::JsonStore &terms, + const utility::JsonStore &env, + const utility::JsonStore &lookup) { + + auto result = utility::JsonStore(R"([])"_json); + + // apply livelinks.. + // update livelink data + for (auto &i : terms) { + auto t = i; + if (t.at("term") == "Operator") { + t["children"] = apply_env(t["children"], env, lookup); + } else if (t.count("value") and not to_value(t, "livelink", false)) { + // check for env var in value field. + // substitue, need regexp search replace. + replace_with_env(t.at("value"), env, lookup); + } + result.emplace_back(t); + } + + return result; +} + +// handle expansion into OR/AND +utility::JsonStore QueryEngine::preprocess_terms( + const utility::JsonStore &terms, + const std::string &entity, + utility::JsonStore &query, + const utility::JsonStore &lookup, + const utility::JsonStore &metadata, + const bool and_mode, + const bool initial) { + auto result = utility::JsonStore(R"([])"_json); + + try { + + // special handling for top level. + std::map> dup_terms; + std::vector order_by; + + if (initial) { + query["entity"] = entity; + if (entity == "Versions") + query["fields"] = VersionFields; + else if (entity == "Notes") + query["fields"] = NoteFields; + else if (entity == "Playlists") + query["fields"] = PlaylistFields; + } + + // duplicate term if array. + auto expanded = R"([])"_json; + for (const auto &i : terms) { + if (i.at("value").is_array()) { + auto tmp = i; + for (const auto &ii : i.at("value")) { + tmp["value"] = ii; + expanded.push_back(tmp); + } + } else { + expanded.push_back(i); + } + } + + for (const auto &i : expanded) { + if (i.value("enabled", true)) { + auto term = i.value("term", ""); + if (term == "Operator") { + auto op = i; + op["children"] = preprocess_terms( + i.at("children"), + entity, + query, + lookup, + metadata, + i.value("value", "And") == "And", + false); + result.push_back(op); + } else if (term == "Disable Global") { + // ignored + } else if (term == "Result Limit") { + query["max_result"] = std::stoi(i.at("value").get()); + } else if (term == "Project") { + // need to also handle livelinked project... + if (not i.contains("livelink") or not i.value("livelink", false)) + query["project_id"] = + resolve_query_value("Project", i.at("value"), lookup).get(); + else { + auto linkvalue = get_livelink_value("Project", metadata, lookup); + if (not linkvalue.is_null() and i.at("value") != linkvalue) + query["project_id"] = + resolve_query_value("Project", linkvalue, lookup).get(); + } + } else if (term == "Preferred Visual") { + query["context"]["visual_source"].push_back( + i.at("value").get()); + } else if (term == "Preferred Audio") { + query["context"]["audio_source"].push_back( + i.at("value").get()); + } else if (term == "Preferred Sequence") { + query["context"]["sequence_source"].push_back( + i.at("value").get()); + } else if (term == "Exclude Self") { + query["context"]["exclude_self"] = + get_livelink_value("Id", metadata, lookup); + } else if (term == "Flag Media") { + auto flag_text = i.at("value").get(); + query["context"]["flag_text"] = flag_text; + if (flag_text == "Red") + query["context"]["flag_colour"] = "#FFFF0000"; + else if (flag_text == "Green") + query["context"]["flag_colour"] = "#FF00FF00"; + else if (flag_text == "Blue") + query["context"]["flag_colour"] = "#FF0000FF"; + else if (flag_text == "Yellow") + query["context"]["flag_colour"] = "#FFFFFF00"; + else if (flag_text == "Orange") + query["context"]["flag_colour"] = "#FFFFA500"; + else if (flag_text == "Purple") + query["context"]["flag_colour"] = "#FF800080"; + else if (flag_text == "Black") + query["context"]["flag_colour"] = "#FF000000"; + else if (flag_text == "White") + query["context"]["flag_colour"] = "#FFFFFFFF"; + } else if (term == "Order By") { + auto val = i.at("value").get(); + bool descending = false; + + if (ends_with(val, " ASC")) { + val = val.substr(0, val.size() - 4); + } else if (ends_with(val, " DESC")) { + val = val.substr(0, val.size() - 5); + descending = true; + } + + std::string field = ""; + // get sg term.. + if (entity == "Playlists") { + if (val == "Date And Time") + field = "sg_date_and_time"; + else if (val == "Created") + field = "created_at"; + else if (val == "Updated") + field = "updated_at"; + } else if (entity == "Versions") { + if (val == "Date And Time") + field = "created_at"; + else if (val == "Created") + field = "created_at"; + else if (val == "Updated") + field = "updated_at"; + else if (val == "Client Submit") + field = "sg_date_submitted_to_client"; + else if (val == "Version") + field = "sg_dneg_version"; + else if (val == "Pipeline Status") + field = "sg_status_list"; + } else if (entity == "Notes") { + if (val == "Created") + field = "created_at"; + else if (val == "Updated") + field = "updated_at"; + } + + if (not field.empty()) + order_by.push_back(descending ? "-" + field : field); + } else { + // add normal term to map. + auto key = std::string(to_value(i, "negated", false) ? "Not " : "") + term; + if (not dup_terms.count(key)) + dup_terms[key] = std::vector(); + + dup_terms[key].push_back(i); + } + } + } + + // we've got list of terms in multi map + // we now need to encapsulate in OR/AND groups. + for (const auto &i : dup_terms) { + if (i.second.size() == 1) + result.push_back(i.second.front()); + else { + if (i.first == "Operator" or not initial) { + for (const auto &j : i.second) + result.push_back(j); + } else { + auto mode = ""; + auto inverted = + starts_with(i.first, "Not ") or starts_with(i.first, "Exclude "); + if (and_mode) { + if (inverted) { + mode = "And"; + } else { + mode = "Or"; + } + } else { + // not sure if it should be inverted ? + if (inverted) { + mode = "And"; + } else { + mode = "Or"; + } + } + auto op = JsonStore(OperatorTermTemplate); + op["value"] = mode; + op["children"] = i.second; + result.emplace_back(op); + } + } + } + + if (initial) { + // set defaults if not specified + if (query["context"]["visual_source"].empty()) + query["context"]["visual_source"] = json::array({"SG Movie"}); + if (query["context"]["audio_source"].empty()) + query["context"]["audio_source"] = query["context"]["visual_source"]; + if (query["context"]["sequence_source"].empty()) + query["context"]["sequence_source"] = json::array({"otio_optimised", "otio"}); + + // set order by + if (order_by.empty()) { + order_by.emplace_back("-created_at"); + } + + query["order"] = order_by; + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + throw; + } + + return result; +} + +utility::JsonStore QueryEngine::build_query( + const int project_id, + const std::string &entity, + const utility::JsonStore &group_detail, + const utility::JsonStore &group_terms, + const utility::JsonStore &terms, + const utility::JsonStore &custom_terms, + const utility::JsonStore &context, + const utility::JsonStore &metadata, + const utility::JsonStore &env, + const utility::JsonStore &lookup) { + auto query = utility::JsonStore(GetQueryResult); + static const auto default_env = get_default_env(); + + query["group_detail"] = group_detail; + query["context"] = context; + query["env"] = default_env; + query["project_id"] = project_id; + if (env.is_object()) + query["env"].update(env); + + auto merged_preset = utility::JsonStore(R"([])"_json); + for (size_t i = 0; i < terms.size(); i++) + merged_preset = merge_query(merged_preset, terms.at(i), false); + + merged_preset = merge_query(merged_preset, group_terms, true); + + merged_preset = merge_query(merged_preset, custom_terms, false); + + merged_preset = apply_env(merged_preset, query["env"], lookup); + + merged_preset = apply_livelinks(merged_preset, metadata, lookup); + + // spdlog::warn("merged_preset\n{}", merged_preset.dump(2)); + + auto preprocessed = JsonStore(R"([])"_json); + preprocessed.push_back(OperatorTermTemplate); + preprocessed[0]["value"] = "And"; + preprocessed[0]["children"] = + preprocess_terms(merged_preset, entity, query, lookup, metadata, true, true); + + // spdlog::warn("preprocess_terms\n{}", preprocessed.dump(2)); + + try { + query["query"] = terms_to_query(preprocessed, query["project_id"], entity, lookup); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + throw; + } + + // spdlog::warn("terms_to_query {}", query.dump(2)); + + return query; +} + +utility::JsonStore QueryEngine::merge_query( + const utility::JsonStore &base, + const utility::JsonStore &override, + const bool ignore_duplicates) { + auto result = base; + + // we need to preprocess for Disable Global flags.. + auto disable_globals = std::set(); + + try { + for (const auto &i : result) { + if (i.at("enabled").get() and i.at("term") == "Disable Global") + disable_globals.insert(i.at("value").get()); + } + + // if term already exists in dst, then don't append. + if (ignore_duplicates) { + auto dup = std::set(); + for (const auto &i : result) + if (i.at("enabled").get()) + dup.insert(i.at("term").get()); + + for (const auto &i : override) { + auto term = i.at("term").get(); + if (not dup.count(term) and not disable_globals.count(term)) + result.push_back(i); + } + } else { + for (const auto &i : override) { + auto term = i.at("term").get(); + if (not disable_globals.count(term)) + result.push_back(i); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + throw; + } + + return result; +} + +Text QueryEngine::add_text_value( + const std::string &filter, const std::string &value, const bool negated) { + if (starts_with(value, "^") and ends_with(value, "$")) { + if (negated) + return Text(filter).is_not(value.substr(0, value.size() - 1).substr(1)); + + return Text(filter).is(value.substr(0, value.size() - 1).substr(1)); + } else if (ends_with(value, "$")) { + return Text(filter).ends_with(value.substr(0, value.size() - 1)); + } else if (starts_with(value, "^")) { + return Text(filter).starts_with(value.substr(1)); + } + if (negated) + return Text(filter).not_contains(value); + + return Text(filter).contains(value); +} + +void QueryEngine::add_playlist_term_to_filter( + const std::string &term, + const std::string &value, + const bool negated, + const int project_id, + const JsonStore &lookup, + FilterBy *qry) { + if (term == "Lookback") { + if (value == "Today") { + auto since_midnight = duration_since_midnight(); + auto hours = std::chrono::duration_cast(since_midnight).count(); + qry->push_back(DateTime("updated_at").in_last(hours, Period::HOUR)); + } else if (value == "1 Day") + qry->push_back(DateTime("updated_at").in_last(1, Period::DAY)); + else if (value == "3 Days") + qry->push_back(DateTime("updated_at").in_last(3, Period::DAY)); + else if (value == "7 Days") + qry->push_back(DateTime("updated_at").in_last(7, Period::DAY)); + else if (value == "20 Days") + qry->push_back(DateTime("updated_at").in_last(20, Period::DAY)); + else if (value == "30 Days") + qry->push_back(DateTime("updated_at").in_last(30, Period::DAY)); + else if (value == "30-60 Days") { + qry->push_back(DateTime("updated_at").not_in_last(30, Period::DAY)); + qry->push_back(DateTime("updated_at").in_last(60, Period::DAY)); + } else if (value == "60-90 Days") { + qry->push_back(DateTime("updated_at").not_in_last(60, Period::DAY)); + qry->push_back(DateTime("updated_at").in_last(90, Period::DAY)); + } else if (value == "100-150 Days") { + qry->push_back(DateTime("updated_at").not_in_last(100, Period::DAY)); + qry->push_back(DateTime("updated_at").in_last(150, Period::DAY)); + } else if (value == "Future Only") { + qry->push_back(DateTime("sg_date_and_time").in_next(30, Period::DAY)); + } else { + throw XStudioError("Invalid query term " + term + " " + value); + } + } else if (term == "Playlist Type") { + if (negated) + qry->push_back(Text("sg_type").is_not(value)); + else + qry->push_back(Text("sg_type").is(value)); + } else if (term == "Has Contents") { + if (value == "False") + qry->push_back(Text("versions").is_null()); + else if (value == "True") + qry->push_back(Text("versions").is_not_null()); + else + throw XStudioError("Invalid query term " + term + " " + value); + } else if (term == "Site") { + if (negated) + qry->push_back(Text("sg_location").is_not(value)); + else + qry->push_back(Text("sg_location").is(value)); + } else if (term == "Review Location") { + if (negated) + qry->push_back(Text("sg_review_location_1").is_not(value)); + else + qry->push_back(Text("sg_review_location_1").is(value)); + } else if (term == "Department") { + if (negated) + qry->push_back( + Number("sg_department_unit.Department.id") + .is_not(resolve_query_value(term, JsonStore(value), lookup).get())); + else + qry->push_back( + Number("sg_department_unit.Department.id") + .is(resolve_query_value(term, JsonStore(value), lookup).get())); + } else if (term == "Author") { + qry->push_back(Number("created_by.HumanUser.id") + .is(resolve_query_value(term, JsonStore(value), project_id, lookup) + .get())); + } else if (term == "Id") { + if (negated) + qry->push_back(Number("id").is_not(std::stoi(value))); + else + qry->push_back(Number("id").is(std::stoi(value))); + } else if (term == "Filter") { + qry->push_back(QueryEngine::add_text_value("code", value, negated)); + } else if (term == "Tag") { + qry->push_back(QueryEngine::add_text_value("tags.Tag.name", value, negated)); + } else if (term == "Has Notes") { + if (value == "False") + qry->push_back(Text("notes").is_null()); + else if (value == "True") + qry->push_back(Text("notes").is_not_null()); + else + throw XStudioError("Invalid query term " + term + " " + value); + } else if (term == "Unit") { + auto tmp = R"({"type": "CustomEntity24", "id":0})"_json; + tmp["id"] = resolve_query_value(term, JsonStore(value), project_id, lookup).get(); + if (negated) + qry->push_back(RelationType("sg_unit2").in({JsonStore(tmp)})); + else + qry->push_back(RelationType("sg_unit2").not_in({JsonStore(tmp)})); + } else { + spdlog::warn("{} Unhandled Term {}", __PRETTY_FUNCTION__, term); + } +} + +void QueryEngine::add_version_term_to_filter( + const std::string &term, + const std::string &value, + const bool negated, + const int project_id, + const JsonStore &lookup, + FilterBy *qry) { + if (term == "Lookback") { + if (value == "Today") { + auto since_midnight = duration_since_midnight(); + auto hours = std::chrono::duration_cast(since_midnight).count(); + qry->push_back(DateTime("created_at").in_last(hours, Period::HOUR)); + } else if (value == "1 Day") + qry->push_back(DateTime("created_at").in_last(1, Period::DAY)); + else if (value == "3 Days") + qry->push_back(DateTime("created_at").in_last(3, Period::DAY)); + else if (value == "7 Days") + qry->push_back(DateTime("created_at").in_last(7, Period::DAY)); + else if (value == "20 Days") + qry->push_back(DateTime("created_at").in_last(20, Period::DAY)); + else if (value == "30 Days") + qry->push_back(DateTime("created_at").in_last(30, Period::DAY)); + else if (value == "30-60 Days") { + qry->push_back(DateTime("created_at").not_in_last(30, Period::DAY)); + qry->push_back(DateTime("created_at").in_last(60, Period::DAY)); + } else if (value == "60-90 Days") { + qry->push_back(DateTime("created_at").not_in_last(60, Period::DAY)); + qry->push_back(DateTime("created_at").in_last(90, Period::DAY)); + } else if (value == "100-150 Days") { + qry->push_back(DateTime("created_at").not_in_last(100, Period::DAY)); + qry->push_back(DateTime("created_at").in_last(150, Period::DAY)); + } else + throw XStudioError("Invalid query term " + term + " " + value); + } else if (term == "Playlist") { + auto tmp = R"({"type": "Playlist", "id":0})"_json; + tmp["id"] = resolve_query_value(term, JsonStore(value), project_id, lookup).get(); + qry->push_back(RelationType("playlists").in({JsonStore(tmp)})); + } else if (term == "Author") { + qry->push_back(Number("created_by.HumanUser.id") + .is(resolve_query_value(term, JsonStore(value), project_id, lookup) + .get())); + } else if (term == "Older Version") { + qry->push_back(Number("sg_dneg_version").less_than(std::stoi(value))); + } else if (term == "Newer Version") { + qry->push_back(Number("sg_dneg_version").greater_than(std::stoi(value))); + } else if (term == "Id") { + if (negated) + qry->push_back(Number("id").is_not(std::stoi(value))); + else + qry->push_back(Number("id").is(std::stoi(value))); + } else if (term == "Site") { + if (negated) + qry->push_back(Text("sg_location").is_not(value)); + else + qry->push_back(Text("sg_location").is(value)); + } else if (term == "Stalk Uuid") { + qry->push_back(Text("sg_ivy_dnuuid").is(value)); + } else if (term == "On Disk") { + std::string prop = std::string("sg_on_disk_") + value; + if (negated) + qry->push_back(Text(prop).is("None")); + else + qry->push_back(FilterBy().Or(Text(prop).is("Full"), Text(prop).is("Partial"))); + } else if (term == "Pipeline Step") { + if (negated) { + if (value == "None") + qry->push_back(Text("sg_pipeline_step").is_not_null()); + else + qry->push_back(Text("sg_pipeline_step").is_not(value)); + } else { + if (value == "None") + qry->push_back(Text("sg_pipeline_step").is_null()); + else + qry->push_back(Text("sg_pipeline_step").is(value)); + } + } else if (term == "Pipeline Status") { + if (negated) + qry->push_back(Text("sg_status_list") + .is_not(resolve_query_value(term, JsonStore(value), lookup) + .get())); + else + qry->push_back(Text("sg_status_list") + .is(resolve_query_value(term, JsonStore(value), lookup) + .get())); + } else if (term == "Production Status") { + if (negated) + qry->push_back(Text("sg_production_status") + .is_not(resolve_query_value(term, JsonStore(value), lookup) + .get())); + else + qry->push_back(Text("sg_production_status") + .is(resolve_query_value(term, JsonStore(value), lookup) + .get())); + } else if (term == "Unit") { + auto tmp = R"({"type": "CustomEntity24", "id":0})"_json; + tmp["id"] = resolve_query_value(term, JsonStore(value), project_id, lookup).get(); + if (negated) + qry->push_back(RelationType("entity.Shot.sg_unit").is_not({JsonStore(tmp)})); + else + qry->push_back(RelationType("entity.Shot.sg_unit").is({JsonStore(tmp)})); + + } else if (term == "Stage") { + qry->push_back(RelationType("sg_client_send_stage").name_is(value)); + } else if (term == "Shot Status") { + if (negated) + qry->push_back(Text("entity.Shot.sg_status_list") + .is_not(resolve_query_value(term, JsonStore(value), lookup) + .get())); + else + qry->push_back(Text("entity.Shot.sg_status_list") + .is(resolve_query_value(term, JsonStore(value), lookup) + .get())); + } else if (term == "Exclude Shot Status") { + qry->push_back( + Text("entity.Shot.sg_status_list") + .is_not( + resolve_query_value(term, JsonStore(value), lookup).get())); + } else if (term == "Latest Version") { + if (value == "False") + qry->push_back(Text("sg_latest").is_null()); + else if (value == "True") + qry->push_back(Text("sg_latest").is("Yes")); + else + throw XStudioError("Invalid query term " + term + " " + value); + } else if (term == "Is Hero") { + if (value == "False") + qry->push_back(Checkbox("sg_is_hero").is(false)); + else if (value == "True") + qry->push_back(Checkbox("sg_is_hero").is(true)); + else + throw XStudioError("Invalid query term " + term + " " + value); + } else if (term == "Viewable") { + if (value == "False") + qry->push_back(FilterBy().And( + Text("sg_path_to_movie").is_null(), Text("sg_path_to_frames").is_null())); + else if (value == "True") + qry->push_back(FilterBy().Or( + Text("sg_path_to_movie").is_not_null(), + Text("sg_path_to_frames").is_not_null())); + else + throw XStudioError("Invalid query term " + term + " " + value); + } else if (term == "Entity") { + auto args = split(value, '-'); + if (args.size() == 2) { + auto rel = R"({"type": null, "id":null})"_json; + rel["type"] = args.at(0); + rel["id"] = std::stoi(args.at(1)); + qry->push_back(RelationType("entity").is(JsonStore(rel))); + } else if (args.size() == 3) { + auto rel = R"({"type": null, "id":null})"_json; + rel["type"] = args.at(0); + rel["id"] = std::stoi(args.at(1)); + qry->push_back(RelationType("entity").is(JsonStore(rel))); + qry->push_back(Text("sg_pipe_tag_3").is(args.at(2))); + } + } else if (term == "Shot") { + auto rel = R"({"type": "Shot", "id":0})"_json; + // if no match force failing query. or we'll get EVERYTHING + try { + rel["id"] = + resolve_query_value(term, JsonStore(value), project_id, lookup).get(); + } catch (...) { + } + qry->push_back(RelationType("entity").is(JsonStore(rel))); + } else if (term == "Asset") { + auto rel = R"({"type": "Asset", "id":0})"_json; + // if no match force failing query. or we'll get EVERYTHING + try { + rel["id"] = + resolve_query_value(term, JsonStore(value), project_id, lookup).get(); + } catch (...) { + } + qry->push_back(RelationType("entity").is(JsonStore(rel))); + } else if (term == "Episode") { + auto rel = R"({"type": "CustomEntity20", "id":0})"_json; + // if no match force failing query. or we'll get EVERYTHING + try { + rel["id"] = + resolve_query_value(term, JsonStore(value), project_id, lookup).get(); + } catch (...) { + } + qry->push_back(RelationType("entity").is(JsonStore(rel))); + } else if (term == "Shot Alternative") { + qry->push_back(QueryEngine::add_text_value("sg_pipe_tag_3", value, negated)); + } else if (term == "Sequence") { + try { + auto seq = R"({"type": "Sequence", "id":0})"_json; + auto seqs = std::vector(); + // if no match force failing query. or we'll get EVERYTHING + try { + seq["id"] = + resolve_query_value(term, JsonStore(value), project_id, lookup).get(); + seqs.push_back(seq); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + seqs.push_back(seq); + } + qry->push_back(RelationType("entity").in(seqs)); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + throw XStudioError("Invalid query term " + term + " " + value); + } + } else if (term == "Sent To") { + if (value == "Client") + qry->push_back(DateTime("sg_date_submitted_to_client").is_not_null()); + else if (value == "Dailies") { + qry->push_back(FilterBy().Or( + DateTime("sg_submit_dailies").is_not_null(), + DateTime("sg_submit_dailies_chn").is_not_null(), + DateTime("sg_submit_dailies_mtl").is_not_null(), + DateTime("sg_submit_dailies_van").is_not_null(), + DateTime("sg_submit_dailies_mum").is_not_null())); + } else if (value == "Any") { + qry->push_back(FilterBy().Or( + DateTime("sg_date_submitted_to_client").is_not_null(), + DateTime("sg_submit_dailies").is_not_null(), + DateTime("sg_submit_dailies_chn").is_not_null(), + DateTime("sg_submit_dailies_mtl").is_not_null(), + DateTime("sg_submit_dailies_van").is_not_null(), + DateTime("sg_submit_dailies_mum").is_not_null())); + } + } else if (term == "Sent To Client") { + if (value == "False") + qry->push_back(DateTime("sg_date_submitted_to_client").is_null()); + else if (value == "True") + qry->push_back(DateTime("sg_date_submitted_to_client").is_not_null()); + else + throw XStudioError("Invalid query term " + term + " " + value); + + + } else if (term == "Sent To Dailies") { + if (value == "False") + qry->push_back(FilterBy().And( + DateTime("sg_submit_dailies").is_null(), + DateTime("sg_submit_dailies_chn").is_null(), + DateTime("sg_submit_dailies_mtl").is_null(), + DateTime("sg_submit_dailies_van").is_null(), + DateTime("sg_submit_dailies_mum").is_null())); + else if (value == "True") + qry->push_back(FilterBy().Or( + DateTime("sg_submit_dailies").is_not_null(), + DateTime("sg_submit_dailies_chn").is_not_null(), + DateTime("sg_submit_dailies_mtl").is_not_null(), + DateTime("sg_submit_dailies_van").is_not_null(), + DateTime("sg_submit_dailies_mum").is_not_null())); + else + throw XStudioError("Invalid query term " + term + " " + value); + } else if (term == "Has Notes") { + if (value == "False") + qry->push_back(Text("notes").is_null()); + else if (value == "True") + qry->push_back(Text("notes").is_not_null()); + else + throw XStudioError("Invalid query term " + term + " " + value); + } else if (term == "Filter") { + qry->push_back(QueryEngine::add_text_value("code", value, negated)); + } else if (term == "Client Filename") { + qry->push_back(QueryEngine::add_text_value("sg_client_filename", value, negated)); + } else if (term == "Tag") { + qry->push_back( + QueryEngine::add_text_value("entity.Shot.tags.Tag.name", value, negated)); + } else if (term == "dnTag") { + qry->push_back(QueryEngine::add_text_value( + "entity.Shot.sg_dnbreakdown_tags.Tag.name", value, negated)); + } else if (term == "Reference Tag" or term == "Reference Tags") { + + if (value.find(',') != std::string::npos) { + // split ... + for (const auto &i : split(value, ',')) { + if (negated) + qry->push_back(RelationType("tags").name_not_contains(i + ".REFERENCE")); + else + qry->push_back(RelationType("tags").name_is(i + ".REFERENCE")); + } + } else { + if (negated) + qry->push_back(RelationType("tags").name_not_contains(value + ".REFERENCE")); + else + qry->push_back(RelationType("tags").name_is(value + ".REFERENCE")); + } + } else if (term == "Tag (Version)") { + qry->push_back(QueryEngine::add_text_value("tags.Tag.name", value, negated)); + } else if (term == "Twig Name") { + qry->push_back(QueryEngine::add_text_value("sg_twig_name", value, negated)); + } else if (term == "Twig Type") { + if (negated) + qry->push_back(Text("sg_twig_type_code") + .is_not(resolve_query_value(term, JsonStore(value), lookup) + .get())); + else + qry->push_back(Text("sg_twig_type_code") + .is(resolve_query_value(term, JsonStore(value), lookup) + .get())); + } else if (term == "Completion Location") { + auto rel = R"({"type": "CustomNonProjectEntity16", "id":0})"_json; + rel["id"] = resolve_query_value(term, JsonStore(value), lookup).get(); + if (negated) + qry->push_back( + RelationType("entity.Shot.sg_primary_shot_location").is_not(JsonStore(rel))); + else + qry->push_back( + RelationType("entity.Shot.sg_primary_shot_location").is(JsonStore(rel))); + + } else { + spdlog::warn("{} Unhandled Term {}", __PRETTY_FUNCTION__, term); + } +} + +void QueryEngine::add_note_term_to_filter( + const std::string &term, + const std::string &value, + const bool negated, + const int project_id, + const JsonStore &lookup, + FilterBy *qry) { + if (term == "Lookback") { + if (value == "Today") { + auto since_midnight = duration_since_midnight(); + auto hours = std::chrono::duration_cast(since_midnight).count(); + qry->push_back(DateTime("created_at").in_last(hours, Period::HOUR)); + } else if (value == "1 Day") + qry->push_back(DateTime("created_at").in_last(1, Period::DAY)); + else if (value == "3 Days") + qry->push_back(DateTime("created_at").in_last(3, Period::DAY)); + else if (value == "7 Days") + qry->push_back(DateTime("created_at").in_last(7, Period::DAY)); + else if (value == "20 Days") + qry->push_back(DateTime("created_at").in_last(20, Period::DAY)); + else if (value == "30 Days") + qry->push_back(DateTime("created_at").in_last(30, Period::DAY)); + else if (value == "30-60 Days") { + qry->push_back(DateTime("created_at").not_in_last(30, Period::DAY)); + qry->push_back(DateTime("created_at").in_last(60, Period::DAY)); + } else if (value == "60-90 Days") { + qry->push_back(DateTime("created_at").not_in_last(60, Period::DAY)); + qry->push_back(DateTime("created_at").in_last(90, Period::DAY)); + } else if (value == "100-150 Days") { + qry->push_back(DateTime("created_at").not_in_last(100, Period::DAY)); + qry->push_back(DateTime("created_at").in_last(150, Period::DAY)); + } else + throw XStudioError("Invalid query term " + term + " " + value); + } else if (term == "Filter") { + qry->push_back(QueryEngine::add_text_value("subject", value, negated)); + } else if (term == "Note Type") { + if (negated) + qry->push_back(Text("sg_note_type").is_not(value)); + else + qry->push_back(Text("sg_note_type").is(value)); + } else if (term == "Author") { + qry->push_back(Number("created_by.HumanUser.id") + .is(resolve_query_value(term, JsonStore(value), project_id, lookup) + .get())); + } else if (term == "Id") { + if (negated) + qry->push_back(Number("id").is_not(std::stoi(value))); + else + qry->push_back(Number("id").is(std::stoi(value))); + } else if (term == "Recipient") { + auto tmp = R"({"type": "HumanUser", "id":0})"_json; + tmp["id"] = resolve_query_value(term, JsonStore(value), project_id, lookup).get(); + qry->push_back(RelationType("addressings_to").in({JsonStore(tmp)})); + } else if (term == "Entity") { + auto args = split(value, '-'); + if (args.size() == 2) { + auto rel = R"([{"type": null, "id":null}])"_json; + rel[0]["type"] = args.at(0); + rel[0]["id"] = std::stoi(args.at(1)); + qry->push_back(RelationType("note_links").in(JsonStore(rel))); + } else if (args.size() == 3) { + auto rel = R"([{"type": null, "id":null}])"_json; + rel[0]["type"] = args.at(0); + rel[0]["id"] = std::stoi(args.at(1)); + qry->push_back(RelationType("note_links").in(JsonStore(rel))); + qry->push_back(Text("note_links.Version.sg_pipe_tag_3").is(args.at(2))); + } + } else if (term == "Shot Alternative") { + qry->push_back( + QueryEngine::add_text_value("note_links.Version.sg_pipe_tag_3", value, negated)); + } else if (term == "Shot") { + auto tmp = R"({"type": "Shot", "id":0})"_json; + // if no match force failing query. or we'll get EVERYTHING + try { + tmp["id"] = + resolve_query_value(term, JsonStore(value), project_id, lookup).get(); + } catch (...) { + } + + qry->push_back(RelationType("note_links").in({JsonStore(tmp)})); + } else if (term == "Sequence") { + try { + auto seq = R"({"type": "Sequence", "id":0})"_json; + auto seqs = std::vector(); + // if no match force failing query. or we'll get EVERYTHING + try { + seq["id"] = + resolve_query_value(term, JsonStore(value), project_id, lookup).get(); + seqs.push_back(seq); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + seqs.push_back(seq); + } + qry->push_back(RelationType("note_links").in(seqs)); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + throw XStudioError("Invalid query term " + term + " " + value); + } + } else if (term == "Playlist") { + auto tmp = R"({"type": "Playlist", "id":0})"_json; + tmp["id"] = resolve_query_value(term, JsonStore(value), project_id, lookup).get(); + qry->push_back(RelationType("note_links").in({JsonStore(tmp)})); + } else if (term == "Version Name") { + qry->push_back(QueryEngine::add_text_value("note_links.Version.code", value, negated)); + } else if (term == "Tag") { + qry->push_back(QueryEngine::add_text_value("tags.Tag.name", value, negated)); + } else if (term == "Has Attachments") { + if (value == "False") + qry->push_back(Text("attachments").is_null()); + else if (value == "True") + qry->push_back(Text("attachments").is_not_null()); + else + throw XStudioError("Invalid query term " + term + " " + value); + } else if (term == "Twig Type") { + if (negated) + qry->push_back(Text("note_links.Version.sg_twig_type_code") + .is_not(resolve_query_value(term, JsonStore(value), lookup) + .get())); + else + qry->push_back(Text("note_links.Version.sg_twig_type_code") + .is(resolve_query_value(term, JsonStore(value), lookup) + .get())); + } else if (term == "Twig Name") { + qry->push_back( + QueryEngine::add_text_value("note_links.Version.sg_twig_name", value, negated)); + } else if (term == "Client Note") { + if (value == "False") + qry->push_back(Checkbox("client_note").is(false)); + else if (value == "True") + qry->push_back(Checkbox("client_note").is(true)); + else + throw XStudioError("Invalid query term " + term + " " + value); + + } else if (term == "Pipeline Step") { + if (negated) { + if (value == "None") + qry->push_back(Text("sg_pipeline_step").is_not_null()); + else + qry->push_back(Text("sg_pipeline_step").is_not(value)); + } else { + if (value == "None") + qry->push_back(Text("sg_pipeline_step").is_null()); + else + qry->push_back(Text("sg_pipeline_step").is(value)); + } + } else if (term == "Older Version") { + qry->push_back( + Number("note_links.Version.sg_dneg_version").less_than(std::stoi(value))); + } else if (term == "Newer Version") { + qry->push_back( + Number("note_links.Version.sg_dneg_version").greater_than(std::stoi(value))); + } else { + spdlog::warn("{} Unhandled Term {}", __PRETTY_FUNCTION__, term); + } +} + +void QueryEngine::add_term_to_filter( + const std::string &entity, + const JsonStore &term, + const int project_id, + const JsonStore &lookup, + FilterBy *qry) { + auto name = term.value("term", ""); + auto val = term.value("value", ""); + auto live = to_value(term, "livelink", false); + auto negated = to_value(term, "negated", false); + + // kill queries with invalid shot live link. + if (val.empty() and live and (name == "Shot" or name == "Entity")) { + auto rel = R"({"type": "Shot", "id":0})"_json; + qry->push_back(RelationType("entity").is(JsonStore(rel))); + } + + if (val.empty()) { + throw XStudioError("Empty query value " + name); + } + + if (entity == "Playlists") + add_playlist_term_to_filter(name, val, negated, project_id, lookup, qry); + else if (entity == "Notes") + add_note_term_to_filter(name, val, negated, project_id, lookup, qry); + else if (entity == "Versions") + add_version_term_to_filter(name, val, negated, project_id, lookup, qry); + else + spdlog::warn("{} Unhandled Entity {}", __PRETTY_FUNCTION__, entity); +} + + +utility::JsonStore QueryEngine::resolve_attribute_value( + const std::string &type, + const utility::JsonStore &value, + const int project_id, + const utility::JsonStore &lookup) { + auto _type = type; + auto attr_value = utility::JsonStore(); + + if (_type == "Author" || _type == "Recipient") + _type = "User"; + + if (project_id != -1) + _type += "-" + std::to_string(project_id); + + try { + auto val = value.get(); + if (lookup.count(_type)) { + if (lookup.at(_type).count(val)) { + attr_value = lookup.at(_type).at(val); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {} {} {}", _type, __PRETTY_FUNCTION__, err.what(), value.dump(2)); + } + + return attr_value; +} + +// resolve value from look up +utility::JsonStore QueryEngine::resolve_query_value( + const std::string &type, + const utility::JsonStore &value, + const int project_id, + const utility::JsonStore &lookup) { + auto _type = type; + auto mapped_value = utility::JsonStore(); + + if (_type == "Author" || _type == "Recipient") + _type = "User"; + + if (project_id != -1) + _type += "-" + std::to_string(project_id); + try { + auto val = value.get(); + if (lookup.count(_type)) { + if (lookup.at(_type).count(val)) { + mapped_value = lookup.at(_type).at(val).at("id"); + } else { + spdlog::warn("Unmatched value {} {} {}", _type, val, __PRETTY_FUNCTION__); + } + } else { + spdlog::warn("Invalid type {} {}", _type, __PRETTY_FUNCTION__); + } + } catch (const std::exception &err) { + spdlog::warn("{} {} {} {}", _type, __PRETTY_FUNCTION__, err.what(), value.dump(2)); + } + + if (mapped_value.is_null()) { + if (type == "User" || type == "Author" || type == "Recipient") { + // usually because user if not assigned to project. + // force no match. + mapped_value = JsonStore(R"(0)"_json); + spdlog::warn( + "{} {} {} {}", + "QueryEngine", + type, + "not enabled for this Project", + value.dump(2)); + } else + throw XStudioError("Invalid term value " + value.dump()); + } + + return mapped_value; +} + +std::string QueryEngine::cache_name_auto(const std::string &type, const int project_id) { + auto _type = type; + + if (project_id != -1 and TermHasProjectKey.count(type)) + _type += "-" + std::to_string(project_id); + + return _type; +} + +std::optional QueryEngine::get_cache(const std::string &key) const { + if (cache_.count(key)) + return cache_.at(key); + + return {}; +} + +std::optional QueryEngine::get_lookup(const std::string &key) const { + if (lookup_.count(key)) + return lookup_.at(key); + + return {}; +} + +void QueryEngine::set_cache(const std::string &key, const utility::JsonStore &data) { + cache_[key] = data; + if (cache_changed_callback_) + cache_changed_callback_(key); +} + +void QueryEngine::set_shot_sequence_lookup( + const std::string &key, const utility::JsonStore &data) { + set_shot_sequence_lookup(key, data, lookup_); + if (lookup_changed_callback_) + lookup_changed_callback_(key); +} + +void QueryEngine::set_shot_sequence_lookup( + const std::string &key, const utility::JsonStore &data, utility::JsonStore &lookup) { + auto tmp = R"({})"_json; + + // build lookup + try { + auto seqmap = std::map(); + + for (const auto &i : data) { + auto value = R"({"id": null, "name": null, "parent_id": null})"_json; + value.at("name") = i.at(json::json_pointer("/attributes/code")); + value.at("id") = i.at("id"); + value.at("parent_id") = + i.at(json::json_pointer("/relationships/sg_parent/data/id")); + + seqmap[i.at("id")] = value; + } + + for (const auto &i : data) { + auto seqvalue = seqmap.at(i.at("id")); + auto seqs = R"([])"_json; + + while (seqvalue.at("parent_id") != seqvalue.at("id")) { + seqs.push_back(seqvalue); + seqvalue = seqmap.at(seqvalue.at("parent_id")); + } + seqs.push_back(seqvalue); + + // this is the direct parent of these shots.. + for (const auto &shot : i.at(json::json_pointer("/relationships/shots/data"))) { + tmp[std::to_string(shot.at("id").get())] = seqs; + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + lookup[key] = tmp; +} + +void QueryEngine::set_lookup(const std::string &key, const utility::JsonStore &data) { + set_lookup(key, data, lookup_); + if (lookup_changed_callback_) + lookup_changed_callback_(key); +} + +void QueryEngine::set_lookup( + const std::string &key, const utility::JsonStore &data, utility::JsonStore &lookup) { + auto tmp = R"({})"_json; + + auto entries = R"([])"_json; + try { + auto value = R"({"id": null, "name": null})"_json; + for (const auto &i : data) { + if (i.count("name")) + value.at("name") = i.at("name"); + else if (i.at("attributes").count("name")) + value.at("name") = i.at("attributes").at("name"); + else if (i.at("attributes").count("code")) + value.at("name") = i.at("attributes").at("code"); + else + continue; + + value.at("id") = i.at("id"); + + tmp[value.at("name")] = value; + if (value.at("id").is_string()) + tmp[value.at("id").get()] = value; + else + tmp[std::to_string(value.at("id").get())] = value; + } + } catch (const std::exception &err) { + spdlog::warn("{} {} {} {}", __PRETTY_FUNCTION__, err.what(), key, data.dump(2)); + } + + lookup[key] = tmp; +} + + +JsonStore QueryEngine::data_from_field(const JsonStore &data) { + auto result = R"([])"_json; + + std::map entries; + + for (const auto &i : data.at("properties").at("valid_values").at("value")) { + auto value = i.get(); + auto key = value; + if (data.at("properties").count("display_values") and + data.at("properties").at("display_values").at("value").count(value)) { + key = data.at("properties") + .at("display_values") + .at("value") + .at(value) + .get(); + } + + entries.insert(std::make_pair(key, value)); + } + + for (const auto &i : entries) { + auto field = R"({"id": null, "attributes": {"name": null}})"_json; + field["attributes"]["name"] = i.first; + field["id"] = i.second; + result.push_back(field); + } + + return JsonStore(result); +} + +std::vector QueryEngine::get_sequence_name( + const int project_id, const int shot_id, const utility::JsonStore &lookup) { + auto result = std::vector(); + auto key = cache_name("ShotSequence", project_id); + auto shot = std::to_string(shot_id); + + if (lookup.count(key)) { + if (lookup.at(key).count(shot)) { + for (const auto &seq : lookup.at(key).at(shot)) + result.push_back(seq.at("name")); + } + } + + return result; +} + +std::vector QueryEngine::get_asset_name( + const int project_id, const int asset_id, const utility::JsonStore &lookup) { + auto result = std::vector(); + auto key = cache_name("Asset", project_id); + auto asset = std::to_string(asset_id); + + if (lookup.count(key)) { + if (lookup.at(key).count(asset)) { + result.push_back(lookup.at(key).at(asset).at("name")); + } + } + + return result; +} + +std::string QueryEngine::get_version_name(const utility::JsonStore &metadata) { + auto name = std::string(); + + try { + std::vector locations( + {{"/metadata/shotgun/version/attributes/code"}, + {"/metadata/image_source_metadata/metadata/external/ivy/file/version/name"}, + {"/metadata/audio_source_metadata/metadata/external/ivy/file/version/name"}}); + for (const auto &i : locations) { + if (metadata.contains(json::json_pointer(i))) { + name = metadata.at(json::json_pointer(i)).get(); + break; + } + } + } catch (...) { + } + + return name; +} + +utility::JsonStore QueryEngine::get_livelink_value( + const std::string &term, + const utility::JsonStore &metadata, + const utility::JsonStore &lookup) { + + const static auto jp_sg_pipe_tag_3 = + json::json_pointer("/metadata/shotgun/version/attributes/sg_pipe_tag_3"); + const static auto sg_pipe_tag_3_ignore = + std::set({"NSFL", "LIBRARY", "TRAIN2", "SFL"}); + + auto result = JsonStore(); + + try { + if (term == "Preferred Audio") { + if (metadata.count("metadata")) + result = + metadata.at("metadata").value("audio_source", nlohmann::json("movie_dneg")); + else + result = json::array({"movie_dneg"}); + } else if (term == "Preferred Visual") { + if (metadata.count("metadata")) + result = + metadata.at("metadata").value("image_source", nlohmann::json("movie_dneg")); + else + result = json::array({"movie_dneg"}); + } else if (term == "Shot Alternative") { + auto sg_pipe_tag_3 = nlohmann::json(); + if (metadata.contains(jp_sg_pipe_tag_3)) + sg_pipe_tag_3 = metadata.at(jp_sg_pipe_tag_3); + + if (sg_pipe_tag_3.is_string() and not sg_pipe_tag_3.get().empty() and + not sg_pipe_tag_3_ignore.count(get_project_name(metadata))) { + // prune off postfix, and do match.. + auto tmp = sg_pipe_tag_3.get(); + if (not std::isdigit(tmp[tmp.size() - 1])) { + // prune off alternate + tmp = tmp.substr(0, tmp.size() - 1); + } + + result = nlohmann::json(std::string("^") + tmp); + } + + } else if (term == "Entity") { + if (metadata.contains(json::json_pointer("/metadata/shotgun/version"))) { + auto type = metadata.at(json::json_pointer( + "/metadata/shotgun/version/relationships/entity/data/type")); + auto id = metadata.at(json::json_pointer( + "/metadata/shotgun/version/relationships/entity/data/id")); + + auto sg_pipe_tag_3 = nlohmann::json(); + if (metadata.contains(jp_sg_pipe_tag_3)) + sg_pipe_tag_3 = metadata.at(jp_sg_pipe_tag_3); + + // check project ID as well ? + if (sg_pipe_tag_3.is_string() and + not sg_pipe_tag_3.get().empty() and + not sg_pipe_tag_3_ignore.count(get_project_name(metadata))) { + result = nlohmann::json( + type.get() + "-" + std::to_string(id.get()) + "-" + + sg_pipe_tag_3.get()); + } else { + result = nlohmann::json( + type.get() + "-" + std::to_string(id.get())); + } + + } else { + // try and derive from show/shot ? + const auto project = get_project_name(metadata); + const auto shot = get_shot_name(metadata); + if (not project.empty() and not shot.empty()) { + // Type is always Shot + // but we need the shot id. + auto shot_id = 0; + auto project_id = + resolve_query_value("Project", nlohmann::json(project), lookup) + .get(); + + // force fail if shot not found + try { + shot_id = resolve_query_value( + "Shot", nlohmann::json(shot), project_id, lookup) + .get(); + } catch (...) { + } + result = nlohmann::json("Shot-" + std::to_string(shot_id)); + } + } + } else if (metadata.contains(json::json_pointer("/metadata/shotgun/version"))) { + if (term == "Version Name") { + result = nlohmann::json(get_version_name(metadata)); + } else if (term == "Older Version" or term == "Newer Version") { + auto val = metadata + .at(json::json_pointer( + "/metadata/shotgun/version/attributes/sg_dneg_version")) + .get(); + result = nlohmann::json(std::to_string(val)); + } else if (term == "Id") { + result = nlohmann::json(std::to_string( + metadata.at(json::json_pointer("/metadata/shotgun/version/id")) + .get())); + } else if (term == "Author" or term == "Recipient") { + result = metadata.at(json::json_pointer( + "/metadata/shotgun/version/relationships/user/data/name")); + } else if (term == "Shot") { + result = nlohmann::json(get_shot_name(metadata)); + } else if (term == "Twig Name") { + result = nlohmann::json( + std::string("^") + + metadata + .at(json::json_pointer( + "/metadata/shotgun/version/attributes/sg_twig_name")) + .get() + + std::string("$")); + } else if (term == "Pipeline Step") { + result = metadata.at(json::json_pointer( + "/metadata/shotgun/version/attributes/sg_pipeline_step")); + } else if (term == "Project") { + result = nlohmann::json(get_project_id(metadata, lookup)); + } else if (term == "Twig Type") { + result = metadata.at( + json::json_pointer("/metadata/shotgun/version/attributes/sg_twig_type")); + } else if (term == "Episode") { + auto type = metadata.at(json::json_pointer( + "/metadata/shotgun/version/relationships/entity/data/type")); + if (type == "CustomEntity20") { + result = metadata.at(json::json_pointer( + "/metadata/shotgun/version/relationships/entity/data/name")); + } + } else if (term == "Asset") { + // name is not unique, so need to use id.. ACK.. + auto type = metadata.at(json::json_pointer( + "/metadata/shotgun/version/relationships/entity/data/type")); + if (type == "Asset") { + auto project_id = + metadata + .at(json::json_pointer( + "/metadata/shotgun/version/relationships/project/data/id")) + .get(); + auto asset_id = metadata.at(json::json_pointer( + "/metadata/shotgun/version/relationships/entity/data/id")); + // do lookup against assets. + auto asset_name = get_asset_name(project_id, asset_id, lookup); + + if (not asset_name.empty()) + result = nlohmann::json(asset_name); + } + } else if (term == "Sequence") { + auto type = metadata.at(json::json_pointer( + "/metadata/shotgun/version/relationships/entity/data/type")); + if (type == "Sequence") { + result = metadata.at(json::json_pointer( + "/metadata/shotgun/version/relationships/entity/data/name")); + } else { + auto project_id = + metadata + .at(json::json_pointer( + "/metadata/shotgun/version/relationships/project/data/id")) + .get(); + auto shot_id = + metadata + .at(json::json_pointer( + "/metadata/shotgun/version/relationships/entity/data/id")) + .get(); + + auto seq_name = get_sequence_name(project_id, shot_id, lookup); + if (not seq_name.empty()) + result = nlohmann::json(seq_name); + } + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {} {} {}", __PRETTY_FUNCTION__, err.what(), term, metadata.dump(2)); + } + + + return result; +} + +void QueryEngine::regenerate_ids(nlohmann::json &data) { + // iterate over all "id" keys reseting the uuids + if (data.is_array()) { + for (size_t i = 0; i < data.size(); i++) + regenerate_ids(data.at(i)); + + } else if (data.is_object()) { + if (data.count("id")) + data["id"] = Uuid::generate(); + + // duplicates are not system presets. + if (data.count("update")) + data["update"] = nullptr; + + if (data.count("flags")) { + // remove system flag.. + auto tmp = R"([])"_json; + for (const auto &i : data.at("flags")) { + if (i != "System Group") + tmp.push_back(i); + } + data["flags"] = tmp; + } + + if (data.count("children")) + regenerate_ids(data.at("children")); + } +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/query_engine.hpp b/src/plugin/data_source/dneg/shotbrowser/src/query_engine.hpp new file mode 100644 index 000000000..566f3c501 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/query_engine.hpp @@ -0,0 +1,730 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +#include "xstudio/utility/json_store.hpp" +#include "xstudio/utility/json_store_sync.hpp" +#include "xstudio/shotgun_client/shotgun_client.hpp" +#include "definitions.hpp" + +using namespace xstudio; + +const auto OperatorTermTemplate = R"({ + "id": null, + "type": "term", + "term": "Operator", + "value": "And", + "negated": false, + "enabled": true, + "children": [] +})"_json; + +// "dynamic": false, +const auto TermTemplate = R"({ + "id": null, + "type": "term", + "term": "", + "value": "", + "enabled": true, + "livelink": null, + "negated": null +})"_json; + +const auto PresetTemplate = R"({ + "id": null, + "type": "preset", + "name": "PRESET", + "hidden": false, + "favourite": false, + "update": null, + "userdata": "", + "children": [] +})"_json; + +const auto GroupTemplate = R"({ + "id": null, + "type": "group", + "name": "GROUP", + "hidden": false, + "favourite": true, + "entity": "", + "update": null, + "flags": [], + "userdata": "", + "children": [ + { + "id": null, + "type": "preset", + "name": "OVERRIDE", + "update": null, + "hidden": false, + "favourite": false, + "userdata": "", + "children": [] + }, + { + "id": null, + "type": "presets", + "children": [] + } + ] +})"_json; + +const auto RootTemplate = R"({ + "id": null, + "name": "root", + "type": "root", + "children": [] +})"_json; + +const auto ValidEntities = R"([ + "Playlists", + "Notes", + "Versions" +])"_json; + +const auto ValidTerms = R"_({ + "Playlists" : [ + "Operator", + "Author", + "Department", + "Disable Global", + "Exclude Shot Status", + "Filter", + "Flag Media", + "Has Contents", + "Has Notes", + "Id", + "Lookback", + "Order By", + "Playlist Type", + "Preferred Audio", + "Preferred Visual", + "Project", + "Review Location", + "Result Limit", + "Shot Status", + "Site", + "Tag", + "Unit" + ], + "Notes" : [ + "Operator", + "Author", + "Disable Global", + "Entity", + "Exclude Shot Status", + "Filter", + "Flag Media", + "Has Attachments", + "Id", + "Lookback", + "Newer Version", + "Note Type", + "Older Version", + "Order By", + "Pipeline Step", + "Playlist", + "Preferred Audio", + "Preferred Visual", + "Project", + "Recipient", + "Result Limit", + "Sequence", + "Shot Status", + "Shot", + "Shot Alternative", + "Tag", + "Twig Name", + "Twig Type", + "Version Name" + ], + "Versions" : [ + "Operator", + "Author", + "Asset", + "Client Filename", + "Completion Location", + "Disable Global", + "dnTag", + "Entity", + "Episode", + "Exclude Self", + "Exclude Shot Status", + "Filter", + "Flag Media", + "Has Notes", + "Id", + "Is Hero", + "Latest Version", + "Lookback", + "Newer Version", + "Older Version", + "On Disk", + "Order By", + "Pipeline Status", + "Pipeline Step", + "Playlist", + "Preferred Audio", + "Preferred Sequence", + "Preferred Visual", + "Production Status", + "Project", + "Reference Tag", + "Reference Tags", + "Result Limit", + "Sent To", + "Sent To Client", + "Sent To Dailies", + "Sequence", + "Shot Status", + "Shot", + "Shot Alternative", + "Stage", + "Stalk Uuid", + "Tag", + "Tag (Version)", + "Twig Name", + "Twig Type", + "Unit", + "Viewable" + ] +})_"_json; + +const auto OnDiskTermValues = R"([ + {"name": "chn"}, + {"name": "lon"}, + {"name": "mtl"}, + {"name": "mum"}, + {"name": "syd"}, + {"name": "van"} + ])"_json; + +const auto ResultLimitTermValues = R"([ + {"name": "1"}, + {"name": "2"}, + {"name": "4"}, + {"name": "8"}, + {"name": "10"}, + {"name": "20"}, + {"name": "40"}, + {"name": "100"}, + {"name": "200"}, + {"name": "500"}, + {"name": "1000"}, + {"name": "2500"}, + {"name": "4500"} + ])"_json; + +const auto LookbackTermValues = R"([ + {"name": "Today"}, + {"name": "1 Day"}, + {"name": "3 Days"}, + {"name": "7 Days"}, + {"name": "20 Days"}, + {"name": "30 Days"}, + {"name": "30-60 Days"}, + {"name": "60-90 Days"}, + {"name": "100-150 Days"}, + {"name": "Future Only"} + ])"_json; + +const auto OrderByTermValues = R"([ + {"name": "Date And Time ASC"}, + {"name": "Date And Time DESC"}, + {"name": "Created ASC"}, + {"name": "Created DESC"}, + {"name": "Updated ASC"}, + {"name": "Updated DESC"}, + {"name": "Client Submit ASC"}, + {"name": "Client Submit DESC"}, + {"name": "Version ASC"}, + {"name": "Version DESC"}, + {"name": "Pipeline Status ASC"}, + {"name": "Pipeline Status DESC"} + ])"_json; + +const auto BoolTermValues = R"([{"name": "True"},{"name": "False"}])"_json; +const auto SubmittedTermValues = + R"([{"name": "Any"}, {"name": "Client"}, {"name": "Dailies"}, {"name": "Ignore"}])"_json; +const auto OperatorTermValues = R"([{"name": "And"},{"name": "Or"}])"_json; + +// "Menu", +// "Tree", +// "Recent", + +const auto GroupFlags = R"([ + "Compare", + "Conform", + "Ignore Toolbar", + "Load Sequence", + "Note History Scope", + "Note History Type", + "Quick Load", + "Replace", + "Shot History Scope", + "System Group", + "View In Sequence" + ])"_json; + +const auto FlagTermValues = R"([ + {"name": "Red", "id":"#FFFF0000"}, + {"name": "Green", "id":"#FF00FF00"}, + {"name": "Blue", "id":"#FF0000FF"}, + {"name": "Yellow", "id":"#FFFFFF00"}, + {"name": "Orange", "id":"#FFFFA500"}, + {"name": "Purple", "id":"#FF800080"}, + {"name": "Black", "id":"#FF000000"}, + {"name": "White", "id":"#FFFFFFFF"} + ])"_json; + +const auto SequenceTermValues = R"([ + { "name": "dneg_shot_mixdown" }, + { "name": "extracted_otio" }, + { "name": "fcp" }, + { "name": "jsoncut" }, + { "name": "main" }, + { "name": "otio" }, + { "name": "otio_optimised" }, + { "name": "shot_mixdown" }, + { "name": "source" } + ])"_json; + +const auto SourceTermValues = R"([ + { "name": "SG Movie" }, + { "name": "SG Frames" }, + { "name": "main_proxy0" }, + { "name": "main_proxy1" }, + { "name": "main_proxy2" }, + { "name": "main_proxy3" }, + { "name": "movie_client" }, + { "name": "movie_diagnostic" }, + { "name": "movie_dneg" }, + { "name": "movie_dneg_editorial" }, + { "name": "movie_scqt" }, + { "name": "movie_scqt_prores" }, + { "name": "orig_proxy0" }, + { "name": "orig_proxy1" }, + { "name": "orig_proxy2" }, + { "name": "orig_proxy3" }, + { "name": "orig_main_proxy0" }, + { "name": "orig_main_proxy1" }, + { "name": "orig_main_proxy2" }, + { "name": "orig_main_proxy3" }, + { "name": "review_proxy_1" }, + { "name": "review_proxy_2" } + ])"_json; + +const auto TermProperties = R"_({ + "Asset": { "negated": false, "livelink": false }, + "Author": { "negated": null, "livelink": false }, + "Client Filename": { "negated": false, "livelink": null }, + "Completion Location": { "negated": false, "livelink": null }, + "Department": { "negated": false, "livelink": null }, + "Disable Global": { "negated": null, "livelink": null }, + "dnTag": { "negated": false, "livelink": null }, + "Entity": { "negated": null, "livelink": true }, + "Episode": { "negated": false, "livelink": false }, + "Exclude Self": { "negated": null, "livelink": true }, + "Exclude Shot Status": { "negated": null, "livelink": null }, + "Filter": { "negated": false, "livelink": null }, + "Flag Media": { "negated": null, "livelink": null }, + "Has Attachments": { "negated": null, "livelink": null }, + "Has Contents": { "negated": null, "livelink": null }, + "Has Notes": { "negated": null, "livelink": null }, + "Id": { "negated": false, "livelink": false }, + "Is Hero": { "negated": null, "livelink": null }, + "Latest Version": { "negated": null, "livelink": null }, + "Lookback": { "negated": null, "livelink": null }, + "Newer Version": { "negated": null, "livelink": false }, + "Note Type": { "negated": false, "livelink": null }, + "Older Version": { "negated": null, "livelink": false }, + "On Disk": { "negated": false, "livelink": null }, + "Operator": { "negated": null, "livelink": null, "children": [] }, + "Order By": { "negated": null, "livelink": null }, + "Pipeline Status": { "negated": false, "livelink": null }, + "Pipeline Step": { "negated": false, "livelink": false }, + "Playlist Type": { "negated": false, "livelink": null }, + "Playlist": { "negated": null, "livelink": null }, + "Preferred Audio": { "negated": null, "livelink": false }, + "Preferred Sequence": { "negated": null, "livelink": null }, + "Preferred Visual": { "negated": null, "livelink": false }, + "Production Status": { "negated": false, "livelink": null }, + "Project": { "negated": null, "livelink": false }, + "Recipient": { "negated": null, "livelink": false }, + "Reference Tag": { "negated": false, "livelink": null }, + "Reference Tags": { "negated": false, "livelink": null }, + "Result Limit": { "negated": null, "livelink": null }, + "Review Location": { "negated": null, "livelink": null }, + "Sent To Client": { "negated": null, "livelink": null }, + "Sent To Dailies": { "negated": null, "livelink": null }, + "Sent To": { "negated": null, "livelink": null }, + "Sequence": { "negated": false, "livelink": false }, + "Shot Status": { "negated": false, "livelink": null }, + "Shot": { "negated": false, "livelink": false }, + "Shot Alternative": { "negated": null, "livelink": true }, + "Site": { "negated": false, "livelink": null }, + "Stage": { "negated": null, "livelink": null }, + "Stalk Uuid": { "negated": null, "livelink": null }, + "Tag (Version)": { "negated": false, "livelink": null }, + "Tag": { "negated": false, "livelink": null }, + "Twig Name": { "negated": false, "livelink": false }, + "Twig Type": { "negated": false, "livelink": false }, + "Unit": { "negated": false, "livelink": null }, + "Viewable": { "negated": null, "livelink": null }, + "Version Name": { "negated": false, "livelink": false } +})_"_json; + +const std::set TermHasProjectKey = { + "Asset", "asset", "Author", "episode", + "Episode", "group", "Group", "playlist", + "Playlist", "Recipient", "sequence", "Sequence", + "shot", "Shot", "ShotSequence", "ShotSequenceList", + "AssetList", "stage", "Stage", "unit", + "Unit", "user", "User"}; + +const std::set TermHasNoModel = { + "Client Filename", + "dnTag", + "Entity", + "Exclude Self", + "Filter", + "Id", + "Newer Version", + "Older Version", + "Shot Alternative", + "Stalk Uuid", + "Tag (Version)", + "Tag", + "Twig Name", + "Version Name"}; + +typedef std::function CacheChangedFunc; + +class QueryEngine { + public: + QueryEngine(); + virtual ~QueryEngine() = default; + + static utility::JsonStore build_query( + const int project_id, + const std::string &entity, + const utility::JsonStore &group_detail, + const utility::JsonStore &group_terms, + const utility::JsonStore &terms, + const utility::JsonStore &custom_terms, + const utility::JsonStore &context, + const utility::JsonStore &metadata, + const utility::JsonStore &env, + const utility::JsonStore &lookup); + + static utility::JsonStore merge_query( + const utility::JsonStore &base, + const utility::JsonStore &override, + const bool ignore_duplicates = true); + + static shotgun_client::Text add_text_value( + const std::string &filter, const std::string &value, const bool negated = false); + + static void add_term_to_filter( + const std::string &entity, + const utility::JsonStore &term, + const int project_id, + const utility::JsonStore &lookup, + shotgun_client::FilterBy *qry); + + static utility::JsonStore resolve_query_value( + const std::string &type, + const utility::JsonStore &value, + const utility::JsonStore &lookup) { + return resolve_query_value(type, value, -1, lookup); + } + + static utility::JsonStore resolve_query_value( + const std::string &type, + const utility::JsonStore &value, + const int project_id, + const utility::JsonStore &lookup); + + static utility::JsonStore resolve_attribute_value( + const std::string &type, + const utility::JsonStore &value, + const int project_id, + const utility::JsonStore &lookup); + + static utility::JsonStore resolve_attribute_value( + const std::string &type, + const utility::JsonStore &value, + const utility::JsonStore &lookup) { + return resolve_attribute_value(type, value, -1, lookup); + } + + + static std::string cache_name(const std::string &type, const int project_id = -1) { + auto _type = type; + + if (project_id != -1) + _type += "-" + std::to_string(project_id); + + return _type; + } + + static utility::JsonStore validate_presets( + const utility::JsonStore &data, + const bool is_user = false, + const utility::JsonStore &parent = utility::JsonStore(), + const size_t index = 0, + const bool export_as_system = false); + + + static std::string cache_name_auto(const std::string &type, const int project_id); + static utility::JsonStore data_from_field(const utility::JsonStore &data); + + static std::vector get_sequence_name( + const int project_id, const int shot_id, const utility::JsonStore &lookup); + + static std::vector + get_asset_name(const int project_id, const int asset_id, const utility::JsonStore &lookup); + + static utility::JsonStore get_livelink_value( + const std::string &term, + const utility::JsonStore &metadata, + const utility::JsonStore &lookup); + + static std::string + get_shot_name(const utility::JsonStore &metadata, const bool strict = false) { + auto name = std::string(); + + try { + std::vector locations( + {{"/metadata/external/DNeg/shot"}, + {"/metadata/external/ivy/file/version/scope/name"}, + {"/metadata/shotgun/version/relationships/entity/data/name"}, + {"/metadata/shotgun/shot/attributes/code"}, + {"/metadata/image_source_metadata/metadata/external/DNeg/shot"}, + {"/metadata/audio_source_metadata/metadata/external/DNeg/shot"}, + {"/metadata/image_source_metadata/metadata/external/ivy/file/version/scope/" + "name"}, + {"/metadata/audio_source_metadata/metadata/external/ivy/file/version/scope/" + "name"}, + {"/metadata/image_source_metadata/colour_pipeline/ocio_context/SHOT"}}); + for (const auto &i : locations) { + if (metadata.contains(json::json_pointer(i))) { + if (strict and i == locations.at(2) and + metadata.value( + json::json_pointer( + "/metadata/shotgun/version/relationships/entity/data/type"), + std::string()) != "Shot") + continue; + name = metadata.at(json::json_pointer(i)).get(); + break; + } + } + } catch (...) { + } + + return name; + } + + static std::string get_version_name(const utility::JsonStore &metadata); + + static std::string get_project_name(const utility::JsonStore &metadata) { + auto project_name = std::string(); + + try { + std::vector project_name_locations( + {{"/metadata/external/DNeg/show"}, + {"/metadata/external/ivy/file/show"}, + {"/metadata/shotgun/version/relationships/project/data/name"}, + {"/metadata/shotgun/shot/relationships/project/data/name"}, + {"/metadata/image_source_metadata/metadata/external/DNeg/show"}, + {"/metadata/audio_source_metadata/metadata/external/DNeg/show"}, + {"/metadata/image_source_metadata/metadata/external/ivy/file/show"}, + {"/metadata/audio_source_metadata/metadata/external/ivy/file/show"}, + {"/metadata/image_source_metadata/colour_pipeline/ocio_context/SHOW"}}); + for (const auto &i : project_name_locations) { + if (metadata.contains(json::json_pointer(i))) { + project_name = metadata.at(json::json_pointer(i)).get(); + break; + } + } + } catch (...) { + } + + return project_name; + } + + int get_project_id(const utility::JsonStore &metadata) { + return get_project_id(metadata, cache_); + } + + static int + get_project_id(const utility::JsonStore &metadata, const utility::JsonStore &cache) { + auto project_id = 0; + + try { + std::vector project_id_locations( + {{"/metadata/shotgun/version/relationships/project/data/id"}, + {"/metadata/shotgun/shot/relationships/project/data/id"}}); + for (const auto &i : project_id_locations) { + if (metadata.contains(json::json_pointer(i))) { + project_id = metadata.at(json::json_pointer(i)).get(); + break; + } + } + + if (not project_id) { + // use project cache to get id from name. + auto name = get_project_name(metadata); + if (not name.empty()) { + auto key = QueryEngine::cache_name("project"); + if (cache.count(key)) { + for (const auto &i : cache.at(key)) { + if (i.at("attributes").at("name") == name) { + project_id = i.at("id").get(); + break; + } + } + } + } + } + } catch (...) { + } + + return project_id; + } + + static void set_lookup( + const std::string &key, const utility::JsonStore &data, utility::JsonStore &lookup); + + void set_lookup(const std::string &key, const utility::JsonStore &data); + + static void set_shot_sequence_lookup( + const std::string &key, const utility::JsonStore &data, utility::JsonStore &lookup); + + void set_shot_sequence_lookup(const std::string &key, const utility::JsonStore &data); + + static void set_shot_sequence_list_cache( + const std::string &key, const utility::JsonStore &data, utility::JsonStore &cache); + void set_shot_sequence_list_cache(const std::string &key, const utility::JsonStore &data); + + static void set_asset_list_cache( + const std::string &key, const utility::JsonStore &data, utility::JsonStore &cache); + void set_asset_list_cache(const std::string &key, const utility::JsonStore &data); + + std::optional get_cache(const std::string &key) const; + std::optional get_lookup(const std::string &key) const; + void set_cache(const std::string &key, const utility::JsonStore &data); + + utility::JsonStore &lookup() { return lookup_; } + utility::JsonStore &cache() { return cache_; } + utility::JsonStoreSync &user_presets() { return user_presets_; } + utility::JsonStoreSync &system_presets() { return system_presets_; } + + void initialise_presets(); + void set_presets(const utility::JsonStore &user, const utility::JsonStore &system); + void merge_presets(nlohmann::json &destination, const nlohmann::json &source); + + const utility::Uuid &user_uuid() const { return user_uuid_; } + const utility::Uuid &system_uuid() const { return system_uuid_; } + + template + static T to_value(const nlohmann::json &jsn, const std::string &key, const T &fallback); + + static std::set + precache_needed(const int project_id, const utility::JsonStore &lookup); + + static utility::JsonStore apply_livelinks( + const utility::JsonStore &terms, + const utility::JsonStore &metadata, + const utility::JsonStore &lookup); + + static void replace_with_env( + nlohmann::json &value, const utility::JsonStore &env, const utility::JsonStore &lookup); + + static utility::JsonStore apply_env( + const utility::JsonStore &terms, + const utility::JsonStore &env, + const utility::JsonStore &lookup); + + static utility::JsonStore get_default_env(); + + static void regenerate_ids(nlohmann::json &data); + + static std::optional + find_by_id(const utility::Uuid &uuid, const utility::JsonStore &presets); + + void bindCacheChangedFunc(CacheChangedFunc ccf) { + cache_changed_callback_ = [ccf](auto &&PH1) { + return ccf(std::forward(PH1)); + }; + } + + void bindLookupChangedFunc(CacheChangedFunc ccf) { + lookup_changed_callback_ = [ccf](auto &&PH1) { + return ccf(std::forward(PH1)); + }; + } + + private: + void merge_group(nlohmann::json &destination, const nlohmann::json &source); + + bool preset_diff(const nlohmann::json &a, const nlohmann::json &b); + + // handle expansion into OR/AND + static utility::JsonStore preprocess_terms( + const utility::JsonStore &terms, + const std::string &entity, + utility::JsonStore &query, + const utility::JsonStore &lookup, + const utility::JsonStore &metadata, + const bool and_mode = true, + const bool initial = true); + + static shotgun_client::FilterBy terms_to_query( + const utility::JsonStore &terms, + const int project_id, + const std::string &entity, + const utility::JsonStore &lookup, + const bool and_mode = true, + const bool initial = true); + + static void add_playlist_term_to_filter( + const std::string &term, + const std::string &value, + const bool negated, + const int project_id, + const utility::JsonStore &lookup, + shotgun_client::FilterBy *qry); + + static void add_version_term_to_filter( + const std::string &term, + const std::string &value, + const bool negated, + const int project_id, + const utility::JsonStore &lookup, + shotgun_client::FilterBy *qry); + + static void add_note_term_to_filter( + const std::string &term, + const std::string &value, + const bool negated, + const int project_id, + const utility::JsonStore &lookup, + shotgun_client::FilterBy *qry); + + utility::JsonStoreSync user_presets_; + utility::JsonStoreSync system_presets_; + + utility::JsonStore cache_; + utility::JsonStore lookup_; + + utility::Uuid user_uuid_ = utility::Uuid::generate(); + utility::Uuid system_uuid_ = utility::Uuid::generate(); + + CacheChangedFunc cache_changed_callback_{nullptr}; + CacheChangedFunc lookup_changed_callback_{nullptr}; +}; diff --git a/src/plugin/data_source/dneg/shotbrowser/src/result_model_ui.cpp b/src/plugin/data_source/dneg/shotbrowser/src/result_model_ui.cpp new file mode 100644 index 000000000..d46c0a921 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/result_model_ui.cpp @@ -0,0 +1,1095 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "result_model_ui.hpp" +#include "shotbrowser_engine_ui.hpp" + +#include "xstudio/utility/string_helpers.hpp" +#include "xstudio/ui/qml/helper_ui.hpp" + +#include +#include +#include + +using namespace std::chrono_literals; +using namespace xstudio::ui::qml; +using namespace xstudio::utility; +using namespace xstudio; + + +ShotBrowserResultModel::ShotBrowserResultModel(QObject *parent) : JSONTreeModel(parent) { + setRoleNames(std::vector( + {"addressingRole", + "artistRole", + "assetRole", + "attachmentsRole", + "authorRole", + "clientFilenameRole", + "clientNoteRole", + "contentRole", + "createdByRole", + "createdDateRole", + "dateSubmittedToClientRole", + "departmentRole", + "detailRole", + "entityRole", + "frameRangeRole", + "frameSequenceRole", + "linkedVersionsRole", + "locationRole", + "loginRole", + "movieRole", + "nameRole", + "noteCountRole", + "noteTypeRole", + "onSiteChn", + "onSiteLon", + "onSiteMtl", + "onSiteMum", + "onSiteSyd", + "onSiteVan", + "pipelineStatusFullRole", + "pipelineStatusRole", + "pipelineStepRole", + "playlistNameRole", + "playlistTypeRole", + "productionStatusRole", + "projectIdRole", + "projectRole", + "resultRowRole", + "sequenceRole", + "shotRole", + "siteRole", + "stageRole", + "stalkUuidRole", + "subjectRole", + "submittedToDailiesRole", + "tagRole", + "textFilterRole", + "thumbRole", + "twigNameRole", + "twigTypeRole", + "typeRole", + "URLRole", + "versionCountRole", + "versionNameRole", + "versionRole"})); +} + +void ShotBrowserResultModel::setEmpty() { + result_data_ = R"({})"_json; + setModelData(R"([])"_json); + setCanBeGrouped(false); +} + +int ShotBrowserResultModel::page() const { return getResultValue("/page", 0); } + +int ShotBrowserResultModel::maxResult() const { return getResultValue("/max_result", 0); } + +int ShotBrowserResultModel::executionMilliseconds() const { + return getResultValue("/execution_ms", 0); +} + +bool ShotBrowserResultModel::truncated() const { + return getResultValue("/context/truncated", false); +} + +void ShotBrowserResultModel::setIsGrouped(const bool value) { + if (can_be_grouped_ and value != is_grouped_) { + was_grouped_ = value; + is_grouped_ = value; + emit isGroupedChanged(); + + if (is_grouped_) + groupBy(); + else + unGroupBy(); + } +} + +void ShotBrowserResultModel::setCanBeGrouped(const bool value) { + if (value != can_be_grouped_) { + can_be_grouped_ = value; + emit canBeGroupedChanged(); + } + + if (can_be_grouped_ and was_grouped_) + is_grouped_ = true; + else + is_grouped_ = false; + + emit isGroupedChanged(); +} + + +QUuid ShotBrowserResultModel::presetId() const { + return QUuidFromUuid(getResultValue("/preset_id", Uuid())); +} + +QVariant ShotBrowserResultModel::groupDetail() const { + QVariant result; + try { + result = mapFromValue(result_data_.at(json::json_pointer("/group_detail"))); + } catch (...) { + result = mapFromValue(R"({"id":"", "flags":[]})"_json); + } + return result; +} + +QStringList ShotBrowserResultModel::audioSource() const { + auto result = QStringList(); + for (const auto &i : getResultValue("/context/audio_source", std::vector())) { + result.append(QStringFromStd(i)); + } + return result; +} + +QStringList ShotBrowserResultModel::visualSource() const { + auto result = QStringList(); + for (const auto &i : getResultValue("/context/visual_source", std::vector())) { + result.append(QStringFromStd(i)); + } + return result; +} + +QStringList ShotBrowserResultModel::sequenceSource() const { + auto result = QStringList(); + for (const auto &i : + getResultValue("/context/sequence_source", std::vector())) { + result.append(QStringFromStd(i)); + } + return result; +} + +QString ShotBrowserResultModel::entity() const { + return QStringFromStd(getResultValue("/entity", std::string(""))); +} + +QString ShotBrowserResultModel::flagColour() const { + return QStringFromStd(getResultValue("/context/flag_text", std::string(""))); +} + +QString ShotBrowserResultModel::flagText() const { + return QStringFromStd(getResultValue("/context/flag_colour", std::string(""))); +} + +QDateTime ShotBrowserResultModel::requestedAt() const { + return QDateTime::fromSecsSinceEpoch(getResultValue("/context/epoc", 0)); +} + +QVariant ShotBrowserResultModel::customContext() const { + QVariant result; + try { + result = mapFromValue(result_data_.at(json::json_pointer("/context/custom"))); + } catch (...) { + } + return result; +} + +QVariantMap ShotBrowserResultModel::env() const { + return QVariantMapFromJson(getResultValue("/env", R"({})"_json)); +} + +QVariantMap ShotBrowserResultModel::context() const { + return QVariantMapFromJson(getResultValue("/context", R"({})"_json)); +} + +QFuture ShotBrowserResultModel::setResultDataJSONFuture(const QVariant &qdata) { + auto data = mapFromValue(qdata); + + return QtConcurrent::run([=]() { + try { + + if (data.is_null()) + setEmpty(); + else { + auto results = R"([])"_json; + try { + auto have_set_result_data = false; + for (const auto &i : data) { + if (i.empty()) + continue; + if (not have_set_result_data) { + result_data_ = i; + have_set_result_data = true; + } + const auto &items = i.at("result").at("data"); + results.insert(results.end(), items.begin(), items.end()); + } + } catch (const std::exception &err) { + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), data.dump(2)); + } + + // inject helper order index.. + for (size_t i = 0; i < results.size(); i++) { + results[i]["result_row"] = i; + } + + setModelData(results); + if (entity() == "Versions") { + if (not can_be_grouped_) + setCanBeGrouped(true); + + if (is_grouped_) { + groupBy(); + } + } else + setCanBeGrouped(false); + } + } catch (const std::exception &err) { + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), data.dump(2)); + setEmpty(); + } + emit stateChanged(); + return true; + }); +} + + +// QFuture ShotBrowserResultModel::setResultDataFuture(const QStringList &data) { +// return QtConcurrent::run([=]() { +// try { +// if (data.isEmpty()) +// setEmpty(); +// else { +// auto results = R"([])"_json; +// try { +// auto have_set_result_data = false; +// for (const auto &i : data) { +// if (i.isEmpty()) +// continue; +// auto jsn_data = nlohmann::json::parse(StdFromQString(i)); +// if (not have_set_result_data) { +// result_data_ = jsn_data; +// have_set_result_data = true; +// } +// const auto &items = jsn_data.at("result").at("data"); + + +// results.insert(results.end(), items.begin(), items.end()); +// } +// } catch (const std::exception &err) { +// spdlog::warn( +// "{} {} {}", __PRETTY_FUNCTION__, err.what(), +// StdFromQString(data[0])); +// } + +// // inject helper order index.. +// for (size_t i = 0; i < results.size(); i++) { +// results[i]["result_row"] = i; +// } + +// setModelData(results); +// if (entity() == "Versions") { +// if (not can_be_grouped_) +// setCanBeGrouped(true); + +// if (is_grouped_) { +// groupBy(); +// } +// } else +// setCanBeGrouped(false); +// } +// } catch (const std::exception &err) { +// spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), +// StdFromQString(data[0])); setEmpty(); +// } +// emit stateChanged(); +// return true; +// }); +// } + + +QVariant ShotBrowserResultModel::data(const QModelIndex &index, int role) const { + auto result = QVariant(); + try { + const auto &j = indexToData(index); + switch (role) { + case submittedToDailiesRole: + if (not j.at("attributes").at("sg_submit_dailies").is_null()) + result = QDateTime::fromString( + QStringFromStd(j.at("attributes").at("sg_submit_dailies")), Qt::ISODate); + else if (not j.at("attributes").at("sg_submit_dailies_chn").is_null()) + result = QDateTime::fromString( + QStringFromStd(j.at("attributes").at("sg_submit_dailies_chn")), + Qt::ISODate); + else if (not j.at("attributes").at("sg_submit_dailies_mtl").is_null()) + result = QDateTime::fromString( + QStringFromStd(j.at("attributes").at("sg_submit_dailies_mtl")), + Qt::ISODate); + else if (not j.at("attributes").at("sg_submit_dailies_van").is_null()) + result = QDateTime::fromString( + QStringFromStd(j.at("attributes").at("sg_submit_dailies_van")), + Qt::ISODate); + else if (not j.at("attributes").at("sg_submit_dailies_mum").is_null()) + result = QDateTime::fromString( + QStringFromStd(j.at("attributes").at("sg_submit_dailies_mum")), + Qt::ISODate); + break; + + case Roles::departmentRole: + result = QString::fromStdString( + j.at("relationships").at("sg_department_unit").at("data").value("name", "")); + break; + + case Roles::pipelineStepRole: + result = QString::fromStdString(j.at("attributes").value("sg_pipeline_step", "")); + break; + case Roles::frameRangeRole: + result = QString::fromStdString(j.at("attributes").value("frame_range", "")); + break; + case Roles::versionRole: + result = j.at("attributes").value("sg_dneg_version", 0); + break; + + case Roles::pipelineStatusRole: + result = QString::fromStdString(j.at("attributes").value("sg_status_list", "")); + break; + + case Roles::stageRole: + result = QString::fromStdString( + j.at("relationships").at("sg_client_send_stage").at("data").value("name", "")); + break; + + case Roles::pipelineStatusFullRole: { + result = QString::fromStdString( + QueryEngine::resolve_attribute_value( + "Pipeline Status", + j.at("attributes").at("sg_status_list"), + ShotBrowserEngine::instance()->queryEngine().lookup()) + .at("name") + .get()); + } break; + + case Roles::productionStatusRole: + result = + QString::fromStdString(j.at("attributes").value("sg_production_status", "")); + break; + + case Roles::dateSubmittedToClientRole: + result = QDateTime::fromString( + QStringFromStd(j.at("attributes").value("sg_date_submitted_to_client", "")), + Qt::ISODate); + break; + + case Roles::addressingRole: { + auto tmp = QStringList(); + for (const auto &i : j.at("relationships").at("addressings_to").at("data")) + tmp.append(QStringFromStd(i.at("name").get())); + result = tmp; + } break; + + case Roles::noteTypeRole: + result = QString::fromStdString(j.at("attributes").value("sg_note_type", "")); + break; + + case Roles::resultRowRole: + result = j.value("result_row", -1); + break; + + case Roles::typeRole: + result = QString::fromStdString(j.value("type", "")); + break; + + case Roles::clientNoteRole: + result = j.at("attributes").value("client_note", false); + break; + + case Roles::clientFilenameRole: + result = QString::fromStdString(j.at("attributes").value("sg_client_filename", "")); + break; + + case Roles::textFilterRole: { + auto tmp = QStringList(); + auto troles = std::vector( + {nameRole, + clientFilenameRole, + artistRole, + authorRole, + departmentRole, + subjectRole, + playlistTypeRole, + noteTypeRole, + pipelineStatusFullRole}); + + for (auto r : troles) + if (auto txt = data(index, r); txt.canConvert()) + tmp.append(txt.toString()); + + result = tmp.join(" "); + } break; + + case Roles::subjectRole: + result = QString::fromStdString(j.at("attributes").value("subject", "")); + break; + + case Roles::contentRole: + result = QString::fromStdString(j.at("attributes").value("content", "")); + break; + + case Roles::linkedVersionsRole: { + auto v = QVariantList(); + for (const auto &i : j.at("relationships").at("note_links").at("data")) { + if (i.at("type").get() == "Version") { + v.append(QVariant::fromValue(i.at("id").get())); + break; + } + } + result = v; + } break; + + case Roles::versionNameRole: + for (const auto &i : j.at("relationships").at("note_links").at("data")) { + if (i.at("type").get() == "Version") { + result = QString::fromStdString(i.at("name").get()); + break; + } + } + break; + + case Roles::shotRole: + if (j.at("relationships").at("entity").at("data").at("type") == "Shot") + result = QString::fromStdString( + j.at("relationships").at("entity").at("data").at("name")); + break; + + case Roles::entityRole: + result = QString::fromStdString( + j.at("relationships").at("entity").at("data").at("name")); + break; + + case Roles::assetRole: + if (j.at("relationships").at("entity").at("data").at("type") == "Asset") + result = QString::fromStdString( + j.at("relationships").at("entity").at("data").at("name")); + break; + + case Roles::attachmentsRole: + result = mapFromValue(j.at("relationships").at("attachments").at("data")); + break; + + case Roles::movieRole: + result = QString::fromStdString(j.at("attributes").at("sg_path_to_movie")); + break; + + case Roles::sequenceRole: { + if (j.at("relationships").at("entity").at("data").value("type", "") == "Sequence") { + result = QString::fromStdString( + j.at("relationships").at("entity").at("data").value("name", "")); + } else { + auto name = QueryEngine::get_sequence_name( + j.at("relationships").at("project").at("data").value("id", 0), + j.at("relationships").at("entity").at("data").value("id", 0), + ShotBrowserEngine::instance()->queryEngine().lookup()); + if (not name.empty()) + result = QString::fromStdString(name.at(0)); + else + result = QString(); + } + } break; + + case Roles::frameSequenceRole: + result = QString::fromStdString(j.at("attributes").at("sg_path_to_frames")); + break; + + case Roles::noteCountRole: + try { + result = static_cast(j.at("relationships").at("notes").at("data").size()); + } catch (...) { + auto req = JsonStore(GetFields); + req["id"] = j.at("id"); + req["entity"] = j.at("type"); + req["fields"].push_back("notes"); + // send request. + ShotBrowserEngine::instance()->requestData(index, role, req); + result = static_cast(-1); + } + break; + + case Roles::versionCountRole: + try { + result = + static_cast(j.at("relationships").at("versions").at("data").size()); + } catch (...) { + auto req = JsonStore(GetFields); + req["id"] = j.at("id"); + req["entity"] = j.at("type"); + req["fields"].push_back("versions"); + // send request. + ShotBrowserEngine::instance()->requestData(index, role, req); + result = static_cast(-1); + } + break; + + case Roles::siteRole: + result = QString::fromStdString(j.at("attributes").value("sg_location", "")); + break; + + case Roles::onSiteMum: + try { + result = j.at("attributes").at("sg_on_disk_mum") == "Full" ? 2 + : j.at("attributes").at("sg_on_disk_mum") == "Partial" ? 1 + : 0; + } catch (...) { + result = 0; + } + break; + case Roles::onSiteMtl: + try { + result = j.at("attributes").at("sg_on_disk_mtl") == "Full" ? 2 + : j.at("attributes").at("sg_on_disk_mtl") == "Partial" ? 1 + : 0; + } catch (...) { + result = 0; + } + break; + case Roles::onSiteVan: + try { + result = j.at("attributes").at("sg_on_disk_van") == "Full" ? 2 + : j.at("attributes").at("sg_on_disk_van") == "Partial" ? 1 + : 0; + } catch (...) { + result = 0; + } + break; + case Roles::onSiteChn: + try { + result = j.at("attributes").at("sg_on_disk_chn") == "Full" ? 2 + : j.at("attributes").at("sg_on_disk_chn") == "Partial" ? 1 + : 0; + } catch (...) { + result = 0; + } + break; + case Roles::onSiteLon: + try { + result = j.at("attributes").at("sg_on_disk_lon") == "Full" ? 2 + : j.at("attributes").at("sg_on_disk_lon") == "Partial" ? 1 + : 0; + } catch (...) { + result = 0; + } + break; + + case Roles::onSiteSyd: + try { + result = j.at("attributes").at("sg_on_disk_syd") == "Full" ? 2 + : j.at("attributes").at("sg_on_disk_syd") == "Partial" ? 1 + : 0; + } catch (...) { + result = 0; + } + break; + + + case Roles::twigNameRole: + result = QString::fromStdString(j.at("attributes").value("sg_twig_name", "")); + break; + + case Roles::tagRole: { + auto tmp = QStringList(); + for (const auto &i : j.at("relationships").at("tags").at("data")) { + auto name = QStringFromStd(i.at("name").get()); + name.replace(QRegularExpression("\\.REFERENCE$"), ""); + tmp.append(name); + } + result = tmp; + } break; + + case Roles::twigTypeRole: + result = QString::fromStdString(j.at("attributes").value("sg_twig_type", "")); + break; + + case Roles::stalkUuidRole: + result = QUuidFromUuid(utility::Uuid(j.at("attributes").at("sg_ivy_dnuuid"))); + break; + + case Roles::URLRole: + result = QStringFromStd(std::string(fmt::format( + "http://shotgun/detail/{}/{}", + j.at("type").get(), + j.at("id").get()))); + break; + + case Roles::artistRole: { + auto name = std::string("pending"); + if (j.at("attributes").count("sg_artist") and + j.at("attributes").at("sg_artist").is_string()) + name = j.at("attributes").at("sg_artist").get(); + + if (name == "pending" or name == "") { + // request name from shot. + auto req = JsonStore(GetVersionArtist); + for (const auto &i : j.at("relationships").at("note_links").at("data")) { + if (i.at("type").get() == "Version") { + req["version_id"] = i.at("id"); + break; + } + } + if (req["version_id"].is_null()) { + name = "unknown"; + } else { + // send request. + ShotBrowserEngine::instance()->requestData(index, role, req); + } + } + result = QString::fromStdString(name); + } break; + + case Roles::nameRole: + case Qt::DisplayRole: + if (j.count("nameRole")) + result = QString::fromStdString(j.at("nameRole")); + else if (j.count("name")) + result = QString::fromStdString(j.at("name")); + else + result = QString::fromStdString( + j.at("attributes") + .value( + "name", + j.at("attributes") + .value("code", j.at("attributes").value("subject", "")))); + break; + + case JSONTreeModel::Roles::idRole: + try { + if (j.at("id").is_number()) + result = j.at("id").get(); + else + result = QString::fromStdString(j.at("id").get()); + } catch (...) { + } + break; + + case JSONTreeModel::Roles::JSONRole: + case Roles::detailRole: + result = QVariantMapFromJson(j); + break; + + case JSONTreeModel::Roles::JSONTextRole: + result = QString::fromStdString(j.dump(2)); + break; + + case Roles::loginRole: + result = QString::fromStdString(j.at("attributes").at("login").get()); + break; + + case Roles::locationRole: + result = + QString::fromStdString(j.at("attributes").at("sg_location").get()); + break; + + case Roles::playlistTypeRole: + result = + QString::fromStdString(j.at("attributes").at("sg_type").get()); + break; + + case Roles::createdByRole: + case Roles::authorRole: + result = QString::fromStdString( + j.at("relationships").at("created_by").at("data").value("name", "")); + break; + + case Roles::projectRole: + result = QString::fromStdString( + j.at("relationships").at("project").at("data").value("name", "")); + break; + + case Roles::projectIdRole: + result = j.at("relationships").at("project").at("data").value("id", 0); + break; + + case Roles::createdDateRole: + result = QDateTime::fromString( + QStringFromStd(j.at("attributes").value("created_at", "")), Qt::ISODate); + break; + + case Roles::thumbRole: + // result = "qrc:/feather_icons/film.svg"; + if (j.at("type") == "Playlist") { + result = QStringFromStd(fmt::format( + "image://shotgrid/thumbnail/{}/{}", j.value("type", ""), j.value("id", 0))); + } else if ( + j.at("attributes").count("image") and + not j.at("attributes").at("image").is_null()) + result = QStringFromStd(fmt::format( + "image://shotgrid/thumbnail/{}/{}", j.value("type", ""), j.value("id", 0))); + else { + for (const auto &i : j.at("relationships").at("note_links").at("data")) { + if (i.at("type") == "Version") { + result = QStringFromStd(fmt::format( + "image://shotgrid/thumbnail/{}/{}", + i.value("type", ""), + i.value("id", 0))); + break; + } else if (i.at("type") == "Shot") { + result = QStringFromStd(fmt::format( + "image://shotgrid/thumbnail/{}/{}", + i.value("type", ""), + i.value("id", 0))); + } else if (result.isNull() and i.at("type") == "Playlist") { + result = QStringFromStd(fmt::format( + "image://shotgrid/thumbnail/{}/{}", + i.value("type", ""), + i.value("id", 0))); + } + } + } + break; + + + break; + + default: + result = JSONTreeModel::data(index, role); + break; + } + } catch (const std::exception &err) { + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; +} + +void ShotBrowserResultModel::fetchMore(const QModelIndex &parent) { + try { + if (canFetchMore(parent)) { + auto type = mapFromValue(parent.data(Roles::typeRole)); + if (type == "Playlist") { + // issue async request for the children.. + auto future = ShotBrowserEngine::instance()->getPlaylistVersionsFuture( + mapFromValue(parent.data(JSONTreeModel::Roles::idRole))); + + auto jsn = nlohmann::json::parse(StdFromQString(future.result())); + auto tmp = R"({"children":null})"_json; + tmp["children"] = jsn.at("data").at("relationships").at("versions").at("data"); + setData(parent, mapFromValue(tmp), JSONTreeModel::Roles::childrenRole); + // spdlog::warn("{}", tmp["children"].dump(2)); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} + +bool ShotBrowserResultModel::setData( + const QModelIndex &index, const QVariant &value, int role) { + QVector roles({role}); + auto result = false; + + try { + auto &j = indexToData(index); + switch (role) { + + case Roles::artistRole: { + auto data = mapFromValue(value); + if (j["attributes"]["sg_artist"] != + data.at("relationships").at("user").at("data").at("name")) { + j["attributes"]["sg_artist"] = + data.at("relationships").at("user").at("data").at("name"); + result = true; + emit dataChanged(index, index, roles); + } + } break; + + case Roles::noteCountRole: { + auto data = mapFromValue(value); + auto jp = nlohmann::json::json_pointer("/relationships/notes/data"); + auto dp = nlohmann::json::json_pointer("/data/relationships/notes/data"); + + if (not j.contains(jp)) + j["relationships"]["notes"] = R"({"data":null})"_json; + + if (j.at(jp) != data.at(dp)) { + j["relationships"]["notes"]["data"] = data.at(dp); + result = true; + emit dataChanged(index, index, roles); + } + } break; + + case Roles::versionCountRole: { + auto data = mapFromValue(value); + auto jp = nlohmann::json::json_pointer("/relationships/versions/data"); + auto dp = nlohmann::json::json_pointer("/data/relationships/versions/data"); + + if (not j.contains(jp)) + j["relationships"]["versions"] = R"({"data":null})"_json; + + if (j.at(jp) != data.at(dp)) { + j["relationships"]["versions"]["data"] = data.at(dp); + + if (j["relationships"]["versions"]["data"].empty()) + j["children"] = R"([])"_json; + else + j["children"] = nullptr; + + result = true; + emit dataChanged(index, index, roles); + } + } break; + + default: + result = JSONTreeModel::setData(index, value, role); + break; + } + } catch (const std::exception &err) { + spdlog::warn( + "{} {} {} {}", + __PRETTY_FUNCTION__, + err.what(), + role, + StdFromQString(value.toString())); + } + + return result; +} + + +void ShotBrowserResultModel::groupBy() { + // reorder rows injecting into parent with version max for stalk. + // build lookup for items. + std::map> vmap; + + for (auto i = 0; i < rowCount(); i++) { + auto index = ShotBrowserResultModel::index(i, 0); + auto name = index.data(Roles::twigNameRole).toString(); + auto version = index.data(Roles::versionRole).toInt(); + + if (not vmap.count(name)) + vmap[name] = std::map(); + + vmap[name][version] = indexToTree(index); + } + + beginResetModel(); + + // iterate over map + for (const auto &[key, items] : vmap) { + if (items.size() == 1) { + continue; + } else { + // select largest version + auto pnode = items.crbegin()->second; + + // reparent others to this item.. + for (const auto &[cversion, cnode] : items) { + if (cnode == pnode) + continue; + + auto row_in_parent = cnode->index(); + moveNodes(cnode->parent(), row_in_parent, row_in_parent, pnode, 0); + } + } + } + + endResetModel(); +} + +void ShotBrowserResultModel::unGroupBy() { + // yay unparent.... + std::map vmap; + + // find children and add to map + for (auto i = 0; i < rowCount(); i++) { + auto pindex = ShotBrowserResultModel::index(i, 0); + + // has children + auto ccount = rowCount(pindex); + if (ccount) { + for (auto ii = 0; ii < ccount; ii++) { + auto cindex = ShotBrowserResultModel::index(ii, 0, pindex); + vmap[cindex.data(resultRowRole).toInt()] = indexToTree(cindex); + } + } + } + + beginResetModel(); + + // move children back to root + for (const auto &[crow, cnode] : vmap) { + auto row_in_parent = cnode->index(); + moveNodes( + cnode->parent(), row_in_parent, row_in_parent, cnode->parent()->parent(), crow); + } + + endResetModel(); +} + +void ShotBrowserResultFilterModel::setSortByNaturalOrder(const bool value) { + if (ordering_ != OM_NATURAL and value) { + ordering_ = OM_NATURAL; + emit sortByChanged(); + invalidate(); + } +} + +void ShotBrowserResultFilterModel::setSortByCreationDate(const bool value) { + if (ordering_ != OM_DATE and value) { + ordering_ = OM_DATE; + emit sortByChanged(); + invalidate(); + } +} + +void ShotBrowserResultFilterModel::setSortByShotName(const bool value) { + if (ordering_ != OM_SHOT and value) { + ordering_ = OM_SHOT; + emit sortByChanged(); + invalidate(); + } +} + +void ShotBrowserResultFilterModel::setSortInAscending(const bool value) { + if (sortInAscending_ != value) { + sortInAscending_ = value; + emit sortInAscendingChanged(); + invalidate(); + } +} + +bool ShotBrowserResultFilterModel::lessThan( + const QModelIndex &source_left, const QModelIndex &source_right) const { + auto result = false; + + switch (ordering_) { + case OM_SHOT: { + auto compare_shot = + source_left.data(ShotBrowserResultModel::Roles::shotRole) + .toString() + .compare(source_right.data(ShotBrowserResultModel::Roles::shotRole).toString()); + if (compare_shot == 0) { + result = source_left.data(ShotBrowserResultModel::Roles::nameRole).toString() < + source_right.data(ShotBrowserResultModel::Roles::nameRole).toString(); + } else + result = compare_shot < 0; + } break; + + case OM_DATE: { + result = source_left.data(ShotBrowserResultModel::Roles::createdDateRole).toDateTime() < + source_right.data(ShotBrowserResultModel::Roles::createdDateRole).toDateTime(); + } break; + + case OM_NATURAL: { + default: + result = source_left.row() < source_right.row(); + } break; + } + + if (sortInAscending_) + result = !result; + + return result; +} + +void ShotBrowserResultFilterModel::setFilterChn(const bool value) { + if (value != filterChn_) { + filterChn_ = value; + emit filterChnChanged(); + invalidateFilter(); + } +} +void ShotBrowserResultFilterModel::setFilterLon(const bool value) { + if (value != filterLon_) { + filterLon_ = value; + emit filterLonChanged(); + invalidateFilter(); + } +} +void ShotBrowserResultFilterModel::setFilterMtl(const bool value) { + if (value != filterMtl_) { + filterMtl_ = value; + emit filterMtlChanged(); + invalidateFilter(); + } +} +void ShotBrowserResultFilterModel::setFilterMum(const bool value) { + if (value != filterMum_) { + filterMum_ = value; + emit filterMumChanged(); + invalidateFilter(); + } +} +void ShotBrowserResultFilterModel::setFilterVan(const bool value) { + if (value != filterVan_) { + filterVan_ = value; + emit filterVanChanged(); + invalidateFilter(); + } +} +void ShotBrowserResultFilterModel::setFilterSyd(const bool value) { + if (value != filterSyd_) { + filterSyd_ = value; + emit filterSydChanged(); + invalidateFilter(); + } +} +void ShotBrowserResultFilterModel::setFilterPipeStep(const QString &value) { + if (value != filterPipeStep_) { + filterPipeStep_ = value; + emit filterPipeStepChanged(); + invalidateFilter(); + } +} +void ShotBrowserResultFilterModel::setFilterName(const QString &value) { + if (value != filterName_) { + filterName_ = value; + filterNameRE_.setPattern(value); + emit filterNameChanged(); + invalidateFilter(); + } +} + + +bool ShotBrowserResultFilterModel::filterAcceptsRow( + int source_row, const QModelIndex &source_parent) const { + auto result = true; + auto source_index = sourceModel()->index(source_row, 0, source_parent); + + if (source_index.isValid()) { + if (source_index.data(ShotBrowserResultModel::Roles::typeRole).toString() == + "Version") { + if (filterChn_ and + not source_index.data(ShotBrowserResultModel::Roles::onSiteChn).toInt()) + result = false; + else if ( + filterLon_ and + not source_index.data(ShotBrowserResultModel::Roles::onSiteLon).toInt()) + result = false; + else if ( + filterMtl_ and + not source_index.data(ShotBrowserResultModel::Roles::onSiteMtl).toInt()) + result = false; + else if ( + filterMum_ and + not source_index.data(ShotBrowserResultModel::Roles::onSiteMum).toInt()) + result = false; + else if ( + filterVan_ and + not source_index.data(ShotBrowserResultModel::Roles::onSiteVan).toInt()) + result = false; + else if ( + filterSyd_ and + not source_index.data(ShotBrowserResultModel::Roles::onSiteSyd).toInt()) + result = false; + else if ( + not filterPipeStep_.isEmpty() and + filterPipeStep_ != + source_index.data(ShotBrowserResultModel::Roles::pipelineStepRole) + .toString()) + result = false; + } + // only apply name filter to parent, not children. + + if (result and not filterName_.isEmpty() and not source_index.parent().isValid() and + (not filterNameRE_.isValid() or + not source_index.data(ShotBrowserResultModel::Roles::textFilterRole) + .toString() + .contains(filterNameRE_))) { + result = false; + } + } + + return result; +} + + +#include "result_model_ui.moc" diff --git a/src/plugin/data_source/dneg/shotbrowser/src/result_model_ui.hpp b/src/plugin/data_source/dneg/shotbrowser/src/result_model_ui.hpp new file mode 100644 index 000000000..7f1b8468e --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/result_model_ui.hpp @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include "xstudio/ui/qml/json_tree_model_ui.hpp" + + +CAF_PUSH_WARNINGS +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +CAF_POP_WARNINGS + +namespace xstudio::ui::qml { +using namespace std::chrono_literals; + +class ShotBrowserResultModel : public JSONTreeModel { + + Q_OBJECT + Q_PROPERTY(int length READ length NOTIFY lengthChanged) + Q_PROPERTY(bool truncated READ truncated NOTIFY stateChanged) + + Q_PROPERTY(int page READ page NOTIFY stateChanged) + Q_PROPERTY(int executionMilliseconds READ executionMilliseconds NOTIFY stateChanged) + Q_PROPERTY(int maxResult READ maxResult NOTIFY stateChanged) + + Q_PROPERTY(QUuid presetId READ presetId NOTIFY stateChanged) + Q_PROPERTY(QVariant groupDetail READ groupDetail NOTIFY stateChanged) + + Q_PROPERTY(QStringList audioSource READ audioSource NOTIFY stateChanged) + Q_PROPERTY(QStringList visualSource READ visualSource NOTIFY stateChanged) + Q_PROPERTY(QStringList sequenceSource READ sequenceSource NOTIFY stateChanged) + Q_PROPERTY(QString flagColour READ flagColour NOTIFY stateChanged) + Q_PROPERTY(QString flagText READ flagText NOTIFY stateChanged) + Q_PROPERTY(QString entity READ entity NOTIFY stateChanged) + Q_PROPERTY(QDateTime requestedAt READ requestedAt NOTIFY stateChanged) + Q_PROPERTY(QVariantMap env READ env NOTIFY stateChanged) + Q_PROPERTY(QVariantMap context READ context NOTIFY stateChanged) + Q_PROPERTY(QVariant customContext READ customContext NOTIFY stateChanged) + + Q_PROPERTY(bool isGrouped READ isGrouped WRITE setIsGrouped NOTIFY isGroupedChanged) + Q_PROPERTY(bool canBeGrouped READ canBeGrouped NOTIFY canBeGroupedChanged) + + + QML_NAMED_ELEMENT("ShotBrowserResultModel") + //![0] + public: + enum Roles { + addressingRole = JSONTreeModel::Roles::LASTROLE, + artistRole, + assetRole, + attachmentsRole, + authorRole, + clientFilenameRole, + clientNoteRole, + contentRole, + createdByRole, + createdDateRole, + dateSubmittedToClientRole, + departmentRole, + detailRole, + entityRole, + frameRangeRole, + frameSequenceRole, + linkedVersionsRole, + loginRole, + locationRole, + movieRole, + nameRole, + noteCountRole, + noteTypeRole, + onSiteChn, + onSiteLon, + onSiteMtl, + onSiteMum, + onSiteSyd, + onSiteVan, + pipelineStatusFullRole, + pipelineStatusRole, + pipelineStepRole, + playlistNameRole, + playlistTypeRole, + productionStatusRole, + projectIdRole, + projectRole, + resultRowRole, + sequenceRole, + shotRole, + siteRole, + stageRole, + stalkUuidRole, + subjectRole, + submittedToDailiesRole, + tagRole, + textFilterRole, + thumbRole, + twigNameRole, + twigTypeRole, + typeRole, + URLRole, + versionCountRole, + versionNameRole, + versionRole + }; + + explicit ShotBrowserResultModel(QObject *parent = nullptr); + ~ShotBrowserResultModel() override = default; + + [[nodiscard]] int length() const { return rowCount(); } + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + bool + setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + // Q_INVOKABLE bool setResultData(const QStringList &data) { + // return setResultDataFuture(data).result(); + // } + // Q_INVOKABLE QFuture setResultDataFuture(const QStringList &data); + + Q_INVOKABLE bool setResultDataJSON(const QVariant &data) { + return setResultDataJSONFuture(data).result(); + } + Q_INVOKABLE QFuture setResultDataJSONFuture(const QVariant &data); + + void fetchMore(const QModelIndex &parent) override; + + [[nodiscard]] bool truncated() const; + [[nodiscard]] int page() const; + [[nodiscard]] int maxResult() const; + [[nodiscard]] int executionMilliseconds() const; + + [[nodiscard]] QUuid presetId() const; + [[nodiscard]] QVariant groupDetail() const; + + [[nodiscard]] QStringList audioSource() const; + [[nodiscard]] QStringList visualSource() const; + [[nodiscard]] QStringList sequenceSource() const; + [[nodiscard]] QString flagColour() const; + [[nodiscard]] QString flagText() const; + [[nodiscard]] QString entity() const; + [[nodiscard]] QDateTime requestedAt() const; + [[nodiscard]] QVariantMap env() const; + [[nodiscard]] QVariantMap context() const; + [[nodiscard]] QVariant customContext() const; + + [[nodiscard]] bool isGrouped() const { return is_grouped_; } + [[nodiscard]] bool canBeGrouped() const { return can_be_grouped_; } + + void setIsGrouped(const bool value); + void setCanBeGrouped(const bool value); + + signals: + void lengthChanged(); + void truncatedChanged(); + void stateChanged(); + void isGroupedChanged(); + void canBeGroupedChanged(); + + private: + template T getResultValue(const std::string &path, const T value) const { + const auto jp = nlohmann::json::json_pointer(path); + T result = value; + if (result_data_.contains(jp) and not result_data_.at(jp).is_null()) + result = result_data_.at(jp); + + return result; + } + + void groupBy(); + void unGroupBy(); + + void setEmpty(); + + nlohmann::json result_data_ = R"({})"_json; + bool is_grouped_{false}; + bool can_be_grouped_{true}; + bool was_grouped_{false}; +}; + +class ShotBrowserResultFilterModel : public QSortFilterProxyModel { + Q_OBJECT + + Q_PROPERTY(int length READ length NOTIFY lengthChanged) + Q_PROPERTY(int count READ length NOTIFY lengthChanged) + + + Q_PROPERTY(bool sortByNaturalOrder READ sortByNaturalOrder WRITE setSortByNaturalOrder + NOTIFY sortByChanged) + Q_PROPERTY(bool sortByCreationDate READ sortByCreationDate WRITE setSortByCreationDate + NOTIFY sortByChanged) + Q_PROPERTY( + bool sortByShotName READ sortByShotName WRITE setSortByShotName NOTIFY sortByChanged) + Q_PROPERTY(bool sortInAscending READ sortInAscending WRITE setSortInAscending NOTIFY + sortInAscendingChanged) + + Q_PROPERTY(bool filterChn READ filterChn WRITE setFilterChn NOTIFY filterChnChanged) + Q_PROPERTY(bool filterLon READ filterLon WRITE setFilterLon NOTIFY filterLonChanged) + Q_PROPERTY(bool filterMtl READ filterMtl WRITE setFilterMtl NOTIFY filterMtlChanged) + Q_PROPERTY(bool filterMum READ filterMum WRITE setFilterMum NOTIFY filterMumChanged) + Q_PROPERTY(bool filterVan READ filterVan WRITE setFilterVan NOTIFY filterVanChanged) + Q_PROPERTY(bool filterSyd READ filterSyd WRITE setFilterSyd NOTIFY filterSydChanged) + + Q_PROPERTY(QString filterPipeStep READ filterPipeStep WRITE setFilterPipeStep NOTIFY + filterPipeStepChanged) + Q_PROPERTY(QString filterName READ filterName WRITE setFilterName NOTIFY filterNameChanged) + + QML_NAMED_ELEMENT("ShotBrowserResultFilterModel") + + public: + typedef enum { OM_NATURAL = 0, OM_DATE, OM_SHOT } OrderMode; + + + ShotBrowserResultFilterModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) { + setDynamicSortFilter(true); + sort(0); + + // not sure this stuff is working.. + connect( + this, + &QSortFilterProxyModel::rowsInserted, + this, + &ShotBrowserResultFilterModel::lengthChanged); + connect( + this, + &QSortFilterProxyModel::modelReset, + this, + &ShotBrowserResultFilterModel::lengthChanged); + connect( + this, + &QSortFilterProxyModel::rowsRemoved, + this, + &ShotBrowserResultFilterModel::lengthChanged); + connect( + this, + &QSortFilterProxyModel::rowsMoved, + this, + &ShotBrowserResultFilterModel::lengthChanged); + + filterNameRE_.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + + [[nodiscard]] bool sortByNaturalOrder() const { return ordering_ == OM_NATURAL; } + [[nodiscard]] bool sortByCreationDate() const { return ordering_ == OM_DATE; } + [[nodiscard]] bool sortByShotName() const { return ordering_ == OM_SHOT; } + [[nodiscard]] bool sortInAscending() const { return sortInAscending_; } + + [[nodiscard]] bool filterChn() const { return filterChn_; } + [[nodiscard]] bool filterLon() const { return filterLon_; } + [[nodiscard]] bool filterMtl() const { return filterMtl_; } + [[nodiscard]] bool filterMum() const { return filterMum_; } + [[nodiscard]] bool filterVan() const { return filterVan_; } + [[nodiscard]] bool filterSyd() const { return filterSyd_; } + + [[nodiscard]] QString filterPipeStep() const { return filterPipeStep_; } + [[nodiscard]] QString filterName() const { return filterName_; } + + [[nodiscard]] int length() const { return rowCount(); } + + void setSortByNaturalOrder(const bool value); + void setSortByCreationDate(const bool value); + void setSortByShotName(const bool value); + void setSortInAscending(const bool value); + + void setFilterChn(const bool value); + void setFilterLon(const bool value); + void setFilterMtl(const bool value); + void setFilterMum(const bool value); + void setFilterVan(const bool value); + void setFilterSyd(const bool value); + + void setFilterPipeStep(const QString &value); + void setFilterName(const QString &value); + + signals: + void sortByChanged(); + void sortInAscendingChanged(); + + void filterChnChanged(); + void filterLonChanged(); + void filterMtlChanged(); + void filterMumChanged(); + void filterVanChanged(); + void filterSydChanged(); + + void filterPipeStepChanged(); + void filterNameChanged(); + + void lengthChanged(); + + protected: + [[nodiscard]] bool + filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + [[nodiscard]] bool + lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; + + private: + bool sortInAscending_{false}; + + bool filterChn_{false}; + bool filterLon_{false}; + bool filterMtl_{false}; + bool filterMum_{false}; + bool filterVan_{false}; + bool filterSyd_{false}; + + QString filterPipeStep_{}; + QString filterName_{}; + QRegularExpression filterNameRE_{}; + + OrderMode ordering_{OM_NATURAL}; +}; + +} // namespace xstudio::ui::qml diff --git a/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_query_ui.cpp b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_query_ui.cpp new file mode 100644 index 000000000..6b294797c --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_query_ui.cpp @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "shotbrowser_engine_ui.hpp" +#include "model_ui.hpp" + +#include "definitions.hpp" +#include "query_engine.hpp" + +using namespace xstudio; +using namespace xstudio::utility; +using namespace xstudio::shotgun_client; +using namespace xstudio::ui::qml; +using namespace xstudio::global_store; +using namespace xstudio::data_source; + + +void ShotBrowserEngine::updateQueryValueCache( + const std::string &type, const utility::JsonStore &data, const int project_id) { + + if (type == "Sequence") { + query_engine_.set_shot_sequence_lookup( + QueryEngine::cache_name("ShotSequence", project_id), data); + } + + query_engine_.set_lookup(QueryEngine::cache_name(type, project_id), data); +} + +// merge global filters with Preset. +// Not sure if this should really happen here.. +// DST = PRESET src == Global + +QVariant ShotBrowserEngine::mergeQueries( + const QVariant &dst, const QVariant &src, const bool ignore_duplicates) const { + + JsonStore dst_qry; + JsonStore src_qry; + + try { + if (std::string(dst.typeName()) == "QJSValue") { + dst_qry = nlohmann::json::parse( + QJsonDocument::fromVariant(dst.value().toVariant()) + .toJson(QJsonDocument::Compact) + .constData()); + } else { + dst_qry = nlohmann::json::parse( + QJsonDocument::fromVariant(dst).toJson(QJsonDocument::Compact).constData()); + } + + if (std::string(src.typeName()) == "QJSValue") { + src_qry = nlohmann::json::parse( + QJsonDocument::fromVariant(src.value().toVariant()) + .toJson(QJsonDocument::Compact) + .constData()); + } else { + src_qry = nlohmann::json::parse( + QJsonDocument::fromVariant(src).toJson(QJsonDocument::Compact).constData()); + } + + auto merged = QueryEngine::merge_query( + dst_qry["queries"], src_qry.at("queries"), ignore_duplicates); + dst_qry["queries"] = merged; + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return QVariantMapFromJson(dst_qry); +} + +QFuture ShotBrowserEngine::executeQueryJSON( + const QStringList &preset_paths, + const QVariantMap &env, + const QVariantList &custom_terms, + const QVariantMap &customContext) { + + + // spdlog::warn("ShotBrowserEngine::executeQuery{}", live_link_metadata_.dump(2)); + // get project from metadata. + try { + auto project_id = + query_engine_.get_project_id(live_link_metadata_, query_engine_.cache()); + + // if (not project_id) + // throw std::runtime_error("Project metadata not found."); + + return executeProjectQueryJSON( + preset_paths, project_id, env, custom_terms, customContext); + } catch (const std::exception &err) { + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), live_link_metadata_.dump(2)); + return QtConcurrent::run([=]() { return QVariant(); }); + } +} + +QFuture ShotBrowserEngine::executeProjectQueryJSON( + const QStringList &preset_paths, + const int project_id, + const QVariantMap &env, + const QVariantList &custom_terms, + const QVariantMap &customContext) { + + // spdlog::warn("{} project_id - {}", __PRETTY_FUNCTION__, project_id); + cacheProject(project_id); + + return QtConcurrent::run([=]() { + if (backend_) { + scoped_actor sys{system()}; + + std::vector paths; + + for (const auto &i : preset_paths) + paths.emplace_back(StdFromQString(i)); + + auto request = JsonStore(GetExecutePreset); + request["project_id"] = project_id; + request["preset_paths"] = paths; + request["metadata"] = live_link_metadata_; + request["context"] = R"({ + "type": null, + "epoc": null, + "audio_source": [], + "visual_source": [], + "flag_text": "", + "flag_colour": "", + "truncated": false, + "custom": null + })"_json; + + try { + request["env"] = qvariant_to_json(env); + request["custom_terms"] = qvariant_to_json(custom_terms); + request["context"]["custom"] = qvariant_to_json(customContext); + + if (not request["context"]["custom"].contains("project_name")) { + request["context"]["custom"]["project_name"] = + query_engine_.get_project_name(live_link_metadata_); + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + request["context"]["epoc"] = utility::to_epoc_milliseconds(utility::clock::now()); + request["context"]["type"] = "result"; + + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, request.dump(2)); + try { + + auto data = request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, get_data_atom_v, request); + + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, data.dump(2)); + + if (data.at("result").count("errors")) { + spdlog::warn( + "{} {}", __PRETTY_FUNCTION__, data.at("result").at("errors").dump(2)); + return QVariant(mapFromValue(R"([])"_json)); + } + + if (data.at("result").at("data").is_array()) + data["context"]["truncated"] = + (static_cast(data.at("result").at("data").size()) == + data.at("max_result")); + + + return QVariant(mapFromValue(data)); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + // silence error.. + + if (starts_with(std::string(err.what()), "LiveLink ")) { + return QVariant(mapFromValue(request)); + } + + return QVariant(mapFromValue(R"([])"_json)); + } + } + return QVariant(mapFromValue(R"([])"_json)); + }); +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_request_ui.cpp b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_request_ui.cpp new file mode 100644 index 000000000..6d453c68a --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_request_ui.cpp @@ -0,0 +1,795 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "xstudio/atoms.hpp" + +#include "shotbrowser_engine_ui.hpp" +#include "model_ui.hpp" + +// #include "../data_source_shotgun.hpp" +#include "definitions.hpp" +#include "query_engine.hpp" + +using namespace xstudio; +using namespace xstudio::utility; +using namespace xstudio::shotgun_client; +using namespace xstudio::data_source; +using namespace xstudio::ui::qml; + +#define REQUEST_BEGIN() return QtConcurrent::run([=]() { \ + if (backend_) { \ + try { + +#define REQUEST_END() \ + } \ + catch (const XStudioError &err) { \ + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); \ + JsonStore error; \ + error["error"]["source"] = to_string(err.type()); \ + error["error"]["message"] = err.caf_error().what(); \ + return QStringFromStd(error.dump()); \ + } \ + catch (const std::exception &err) { \ + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); \ + JsonStore error; \ + error["error"]["source"] = "Exception"; \ + error["error"]["message"] = err.what(); \ + return QStringFromStd(error.dump()); \ + } \ + } \ + return QString(); \ + }); + + +QFuture ShotBrowserEngine::loadPresetModelFuture() { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + + auto uuids = request_receive(*sys, backend_, json_store::sync_atom_v); + + // get system presets + auto request = JsonStore(GetData); + request["type"] = "system_preset_model"; + auto data = R"({"uuid":null, "data":null })"_json; + data["uuid"] = uuids[1]; + data["data"] = + request_receive(*sys, backend_, json_store::sync_atom_v, uuids[1]); + anon_mail(shotgun_info_atom_v, request, JsonStore(data)).send(as_actor()); + + // get user presests + request = JsonStore(GetData); + request["type"] = "user_preset_model"; + data = R"({"uuid":null, "data":null })"_json; + data["uuid"] = uuids[0]; + data["data"] = + request_receive(*sys, backend_, json_store::sync_atom_v, uuids[0]); + anon_mail(shotgun_info_atom_v, request, JsonStore(data)).send(as_actor()); + + return QStringFromStd(data.dump()); + + REQUEST_END() +} + + +QFuture ShotBrowserEngine::getDataFuture(const QString &type, const int project_id) { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + + auto request = JsonStore(GetData); + request["type"] = StdFromQString(type); + request["project_id"] = project_id; + + auto data = request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, get_data_atom_v, request); + + anon_mail(shotgun_info_atom_v, request, data).send(as_actor()); + + return QStringFromStd(data.dump()); + + REQUEST_END() +} + + +QFuture ShotBrowserEngine::getProjectsFuture() { + return getDataFuture(QString("project")); +} + +QFuture ShotBrowserEngine::getSchemaFieldsFuture(const QString &type) { + return getDataFuture(type); +} + +// get json of versions data from shotgun. +QFuture +ShotBrowserEngine::getVersionsFuture(const int project_id, const QVariant &qids) { + + std::vector ids; + for (const auto &i : qids.toList()) + ids.push_back(i.toInt()); + + // return QtConcurrent::run([=, project_id = project_id]() { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + auto filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["project", "is", {"type":"Project", "id":0}], + ["id", "in", []] + ] + })"_json; + + filter["conditions"][0][2]["id"] = project_id; + for (const auto i : ids) + filter["conditions"][1][2].push_back(i); + + // we've got more that 5000 employees.... + auto data = request_receive_wait( + *sys, + backend_, + SHOTGRID_TIMEOUT, + shotgun_entity_search_atom_v, + "Versions", + JsonStore(filter), + VersionFields, + std::vector({"id"}), + 1, + 4999); + + if (not data.count("data")) + throw std::runtime_error(data.dump(2)); + + // reorder results based on request order.. + std::map result_items; + for (const auto &i : data["data"]) + result_items[i.at("id").get()] = i; + + data["data"].clear(); + for (const auto i : ids) { + if (result_items.count(i)) + data["data"].push_back(result_items[i]); + } + + return QStringFromStd(data.dump()); + + REQUEST_END() +} + + +QFuture ShotBrowserEngine::getPlaylistLinkMediaFuture(const QUuid &playlist) { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + auto req = JsonStore(GetLinkMedia); + req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); + + return QStringFromStd( + request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, data_source::get_data_atom_v, req) + .dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::getPlaylistValidMediaCountFuture(const QUuid &playlist) { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + auto req = JsonStore(GetValidMediaCount); + req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); + + return QStringFromStd( + request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, data_source::get_data_atom_v, req) + .dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::getGroupsFuture(const int project_id) { + return getDataFuture(QString("group"), project_id); +} + +QFuture ShotBrowserEngine::getSequencesFuture(const int project_id) { + return getDataFuture(QString("sequence"), project_id); +} + +QFuture ShotBrowserEngine::getPlaylistsFuture(const int project_id) { + return getDataFuture(QString("playlist"), project_id); +} + + +QFuture ShotBrowserEngine::getShotsFuture(const int project_id) { + return getDataFuture(QString("shot"), project_id); +} + +QFuture ShotBrowserEngine::getEpisodesFuture(const int project_id) { + return getDataFuture(QString("episode"), project_id); +} + + +QFuture ShotBrowserEngine::getUsersFuture(const int project_id) { + return getDataFuture(QString("user"), project_id); +} +QFuture ShotBrowserEngine::getPipeStepFuture() { + return getDataFuture(QString("pipe_step")); +} + +QFuture ShotBrowserEngine::getDepartmentsFuture() { + return getDataFuture(QString("department")); +} + +QFuture ShotBrowserEngine::getReferenceTagsFuture() { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + + auto filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["name", "ends_with", ".REFERENCE"] + ] + })"_json; + + // we've got more that 5000 employees.... + auto data = request_receive_wait( + *sys, + backend_, + SHOTGRID_TIMEOUT, + shotgun_entity_search_atom_v, + "Tags", + JsonStore(filter), + std::vector({"name", "id"}), + std::vector({"name"}), + 1, + 4999); + + if (not data.count("data")) + throw std::runtime_error(data.dump(2)); + + for (auto &i : data["data"]) { + auto str = i["attributes"]["name"].get(); + i["attributes"]["name"] = str.substr(0, str.size() - sizeof(".REFERENCE") + 1); + } + + anon_mail(shotgun_info_atom_v, JsonStore(R"({"type": "reference_tag"})"_json), data) + .send(as_actor()); + + return QStringFromStd(data.dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::getCustomEntity24Future(const int project_id) { + return getDataFuture(QString("unit"), project_id); +} + +QFuture ShotBrowserEngine::getAssetFuture(const int project_id) { + return getDataFuture(QString("asset"), project_id); +} + +QFuture ShotBrowserEngine::getTreeFuture(const int project_id) { + return getDataFuture(QString("tree"), project_id); +} + +QFuture ShotBrowserEngine::getAssetTreeFuture(const int project_id) { + return getDataFuture(QString("asset_tree"), project_id); +} + +QFuture ShotBrowserEngine::getStageFuture(const int project_id) { + return getDataFuture(QString("stage"), project_id); +} + +QFuture ShotBrowserEngine::addVersionsToPlaylistFuture( + const QVariantMap &versions, const QUuid &playlist, const QUuid &before) { + + REQUEST_BEGIN() + + scoped_actor sys{system()}; + auto media = request_receive( + *sys, + backend_, + playlist::add_media_atom_v, + JsonStore(mapFromValue(QVariant::fromValue(versions))), + UuidFromQUuid(playlist), + caf::actor(), + UuidFromQUuid(before)); + auto result = nlohmann::json::array(); + // return uuids.. + for (const auto &i : media) { + result.push_back(i.uuid()); + } + + return QStringFromStd(JsonStore(result).dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::addVersionToPlaylistFuture( + const QString &version, const QUuid &playlist, const QUuid &before) { + + REQUEST_BEGIN() + + scoped_actor sys{system()}; + auto media = request_receive( + *sys, + backend_, + playlist::add_media_atom_v, + JsonStore(nlohmann::json::parse(StdFromQString(version))), + UuidFromQUuid(playlist), + caf::actor(), + UuidFromQUuid(before)); + auto result = nlohmann::json::array(); + // return uuids.. + for (const auto &i : media) { + result.push_back(i.uuid()); + } + + return QStringFromStd(JsonStore(result).dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::updateEntityFuture( + const QString &entity, const int record_id, const QString &update_json) { + + REQUEST_BEGIN() + + scoped_actor sys{system()}; + + auto js = request_receive_wait( + *sys, + backend_, + SHOTGRID_TIMEOUT, + shotgun_update_entity_atom_v, + StdFromQString(entity), + record_id, + utility::JsonStore(nlohmann::json::parse(StdFromQString(update_json)))); + return QStringFromStd(js.dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::preparePlaylistNotesFuture( + const QUuid &playlist, + const QList &media, + const bool notify_owner, + const std::vector notify_group_ids, + const bool combine, + const bool add_time, + const bool add_playlist_name, + const bool add_type, + const bool anno_requires_note, + const bool skip_already_published, + const QString &defaultType) { + + return QtConcurrent::run([=]() { + try { + if (backend_) { + try { + scoped_actor sys{system()}; + auto req = JsonStore(GetPrepareNotes); + + for (const auto &i : media) + req["media_uuids"].push_back(to_string(UuidFromQUuid(i))); + + req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); + req["notify_owner"] = notify_owner; + req["notify_group_ids"] = notify_group_ids; + req["combine"] = combine; + req["add_time"] = add_time; + req["add_playlist_name"] = add_playlist_name; + req["add_type"] = add_type; + req["anno_requires_note"] = anno_requires_note; + req["skip_already_published"] = skip_already_published; + req["default_type"] = StdFromQString(defaultType); + + auto js = request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, data_source::get_data_atom_v, req); + return QStringFromStd(js.dump()); + } catch (const XStudioError &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + return QStringFromStd(err.what()); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + return QStringFromStd(err.what()); + } + } + } catch (const std::exception &err) { + spdlog::warn("erm {} {}", __PRETTY_FUNCTION__, err.what()); + return QStringFromStd(err.what()); + } + return QString(); + }); +} + + +QFuture +ShotBrowserEngine::pushPlaylistNotesFuture(const QString ¬es, const QUuid &playlist) { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + auto req = JsonStore(PostCreateNotes); + req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); + req["payload"] = JsonStore(nlohmann::json::parse(StdFromQString(notes))["payload"]); + + auto js = request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, data_source::post_data_atom_v, req); + + return QStringFromStd(js.dump()); + + REQUEST_END() +} + + +QFuture ShotBrowserEngine::createPlaylistFuture( + const QUuid &playlist, + const int project_id, + const QString &name, + const QString &location, + const QString &playlist_type) { + + REQUEST_BEGIN() + + scoped_actor sys{system()}; + auto req = JsonStore(PostCreatePlaylist); + req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); + req["project_id"] = project_id; + req["code"] = StdFromQString(name); + req["location"] = StdFromQString(location); + req["playlist_type"] = StdFromQString(playlist_type); + + auto js = request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, data_source::post_data_atom_v, req); + return QStringFromStd(js.dump()); + + REQUEST_END() +} + +QFuture +ShotBrowserEngine::updatePlaylistVersionsFuture(const QUuid &playlist, const bool append) { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + auto req = JsonStore(PutUpdatePlaylistVersions); + req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); + req["append"] = append; + auto js = request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, data_source::put_data_atom_v, req); + return QStringFromStd(js.dump()); + + REQUEST_END() +} + +// find playlist id from playlist +// request versions from shotgun +// add to playlist. +QFuture +ShotBrowserEngine::refreshPlaylistVersionsFuture(const QUuid &playlist, const bool matchOrder) { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + auto req = JsonStore(UseRefreshPlaylist); + req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); + req["match_order"] = matchOrder; + auto js = request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, data_source::use_data_atom_v, req); + return QStringFromStd(js.dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::getPlaylistNotesFuture(const int id) { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + + auto note_filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["note_links", "in", {"type":"Playlist", "id":0}] + ] + })"_json; + + note_filter["conditions"][0][2]["id"] = id; + + auto order = request_receive_wait( + *sys, + backend_, + SHOTGRID_TIMEOUT, + shotgun_entity_search_atom_v, + "Notes", + JsonStore(note_filter), + std::vector({"*"}), + std::vector(), + 1, + 4999); + + return QStringFromStd(order.dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::getPlaylistVersionsFuture(const int id) { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + + auto vers = request_receive_wait( + *sys, + backend_, + SHOTGRID_TIMEOUT, + shotgun_entity_atom_v, + "Playlists", + id, + std::vector()); + + // spdlog::warn("{}", vers.dump(2)); + + auto order_filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["playlist", "is", {"type":"Playlist", "id":0}] + ] + })"_json; + + order_filter["conditions"][0][2]["id"] = id; + + auto order = request_receive_wait( + *sys, + backend_, + SHOTGRID_TIMEOUT, + shotgun_entity_search_atom_v, + "PlaylistVersionConnection", + JsonStore(order_filter), + std::vector({"sg_sort_order", "version"}), + std::vector({"sg_sort_order"}), + 1, + 4999); + + // should be returned in the correct order.. + + // spdlog::warn("{}", order.dump(2)); + + std::vector version_ids; + for (const auto &i : order["data"]) + version_ids.emplace_back( + std::to_string(i["relationships"]["version"]["data"].at("id").get())); + + if (version_ids.empty()) + return QStringFromStd(vers.dump()); + + // expand version information.. + // get versions + auto query = R"({})"_json; + auto chunked_ids = split_vector(version_ids, 100); + auto data = R"([])"_json; + + for (const auto &chunk : chunked_ids) { + query["id"] = join_as_string(chunk, ","); + + auto js = request_receive_wait( + *sys, + backend_, + SHOTGRID_TIMEOUT, + shotgun_entity_filter_atom_v, + "Versions", + JsonStore(query), + VersionFields, + std::vector(), + 1, + 4999); + // reorder list based on playlist.. + // spdlog::warn("{}", js.dump(2)); + + for (const auto &i : chunk) { + for (auto &j : js["data"]) { + + // spdlog::warn("{} {}", std::to_string(j["id"].get()), i); + if (std::to_string(j["id"].get()) == i) { + data.push_back(j); + break; + } + } + } + } + + auto data_tmp = R"({"data":[]})"_json; + data_tmp["data"] = data; + + // spdlog::warn("{}", js.dump(2)); + + // add back in + vers["data"]["relationships"]["versions"] = data_tmp; + + // create playlist.. + return QStringFromStd(vers.dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::tagEntityFuture( + const QString &entity, const int record_id, const int tag_id) { + + REQUEST_BEGIN() + + scoped_actor sys{system()}; + + auto req = JsonStore(PostTagEntity); + + req["entity"] = StdFromQString(entity); + req["entity_id"] = record_id; + req["tag_id"] = tag_id; + + auto js = request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, data_source::post_data_atom_v, req); + + return QStringFromStd(js.dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::renameTagFuture(const int tag_id, const QString &text) { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + + auto req = JsonStore(); + + if (tag_id) { + req = JsonStore(PostRenameTag); + req["tag_id"] = tag_id; + req["value"] = StdFromQString(text); + } else { + req = JsonStore(PostCreateTag); + req["value"] = StdFromQString(text); + } + + auto js = request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, data_source::post_data_atom_v, req); + + // trigger update to get new tag. + getReferenceTagsFuture(); + + return QStringFromStd(js.dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::removeTagFuture(const int tag_id) { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + + auto js = request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, shotgun_delete_entity_atom_v, "Tag", tag_id); + + // trigger update to get new tag. + getReferenceTagsFuture(); + + return QStringFromStd(js.dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::untagEntityFuture( + const QString &entity, const int record_id, const int tag_id) { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + + auto req = JsonStore(PostUnTagEntity); + + req["entity"] = StdFromQString(entity); + req["entity_id"] = record_id; + req["tag_id"] = tag_id; + + auto js = request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, data_source::post_data_atom_v, req); + + return QStringFromStd(js.dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::createTagFuture(const QString &text) { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + auto req = JsonStore(PostCreateTag); + req["value"] = StdFromQString(text); + + auto js = request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, data_source::post_data_atom_v, req); + + // trigger update to get new tag. + getReferenceTagsFuture(); + + return QStringFromStd(js.dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::getEntityFuture(const QString &qentity, const int id) { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + auto entity = StdFromQString(qentity); + std::vector fields; + + if (entity == "Version") { + fields = VersionFields; + } + + return QStringFromStd( + request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, shotgun_entity_atom_v, entity, id, fields) + .dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::addDownloadToMediaFuture(const QUuid &media) { + REQUEST_BEGIN() + + scoped_actor sys{system()}; + auto req = JsonStore(GetShotgridMedia); + req["media_uuid"] = to_string(UuidFromQUuid(media)); + + return QStringFromStd( + request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, data_source::get_data_atom_v, req) + .dump()); + + REQUEST_END() +} + +QFuture ShotBrowserEngine::downloadMediaFuture( + const int entity_id, + const QString &entity_name, + const QString &project_name, + const QString &parent_name) { + return QtConcurrent::run([=]() { + if (backend_) { + scoped_actor sys{system()}; + auto req = JsonStore(GetDownloadMedia); + req["entity_id"] = entity_id; + req["entity_name"] = StdFromQString(entity_name); + req["project_name"] = StdFromQString(project_name); + req["parent_name"] = StdFromQString(parent_name); + + return QUrl(QStringFromStd( + request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, data_source::get_data_atom_v, req) + .get())); + } + return QUrl(); + }); +} + +QFuture ShotBrowserEngine::downloadImageFuture( + const int entity_id, + const QString &entity, + const QString &entity_name, + const QString &project_name) { + return QtConcurrent::run([=]() { + if (backend_) { + scoped_actor sys{system()}; + auto req = JsonStore(GetDownloadImage); + + req["entity_id"] = entity_id; + req["entity"] = StdFromQString(entity); + req["entity_name"] = StdFromQString(entity_name); + req["project_name"] = StdFromQString(project_name); + + return QUrl(QStringFromStd( + request_receive_wait( + *sys, backend_, SHOTGRID_TIMEOUT, data_source::get_data_atom_v, req) + .get())); + } + return QUrl(); + }); +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_ui.cpp b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_ui.cpp new file mode 100644 index 000000000..a35cb743f --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_ui.cpp @@ -0,0 +1,805 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "xstudio/atoms.hpp" + +#include "shotbrowser_engine_ui.hpp" +#include "shotgrid_provider_ui.hpp" +#include "model_ui.hpp" +#include "preset_model_ui.hpp" +#include "definitions.hpp" +#include "async_request.hpp" +#include "result_model_ui.hpp" + +#include +#include +#include + +using namespace std::chrono_literals; +using namespace xstudio::global_store; +using namespace xstudio::shotgun_client; +using namespace xstudio::data_source; +using namespace xstudio::ui::qml; +using namespace xstudio::utility; +using namespace xstudio; + +ShotBrowserEngine *ShotBrowserEngine::this_ = nullptr; + +ShotBrowserEngine::ShotBrowserEngine(QObject *parent) : QMLActor(parent) { + + presets_model_ = new ShotBrowserPresetModel(query_engine_, this); + presets_model_->bindEventFunc( + [this](auto &&PH1) { JSONTreeSendEventFunc(std::forward(PH1)); }); + + query_engine_.bindLookupChangedFunc([this](auto &&PH1) { + updateQueryEngineTermModel(std::forward(PH1), false); + }); + query_engine_.bindCacheChangedFunc([this](auto &&PH1) { + updateQueryEngineTermModel(std::forward(PH1), true); + }); + + updateQueryValueCache("Completion Location", locationsJSON); + + init(CafSystemObject::get_actor_system()); + set_backend(system().registry().template get(shotbrowser_datasource_registry)); +} + +void ShotBrowserEngine::updateQueryEngineTermModel(const std::string &key, const bool cache) { + presets_model_->updateTermModel(key, cache); +} + +void ShotBrowserEngine::checkReady() { + bool ready = true; + + if (not query_engine_.get_cache(QueryEngine::cache_name("Project"))) + ready = false; + + if (not query_engine_.get_lookup(QueryEngine::cache_name("User"))) + ready = false; + + if (not query_engine_.get_lookup(QueryEngine::cache_name("Department"))) + ready = false; + + if (not query_engine_.get_cache(QueryEngine::cache_name("Pipeline Step"))) + ready = false; + + if (not query_engine_.get_lookup(QueryEngine::cache_name("Site"))) + ready = false; + + if (not query_engine_.get_lookup(QueryEngine::cache_name("Review Location"))) + ready = false; + + if (not query_engine_.get_lookup(QueryEngine::cache_name("Shot Status"))) + ready = false; + + if (not query_engine_.get_lookup(QueryEngine::cache_name("Playlist Type"))) + ready = false; + + if (not query_engine_.get_lookup(QueryEngine::cache_name("Production Status"))) + ready = false; + + if (not query_engine_.get_lookup(QueryEngine::cache_name("Note Type"))) + ready = false; + + if (not query_engine_.get_lookup(QueryEngine::cache_name("Pipeline Status"))) + ready = false; + + if (not presets_model_->rowCount()) + ready = false; + + if (ready) { + ready_ = ready; + emit readyChanged(); + } +} + + +ShotBrowserEngine *ShotBrowserEngine::instance() { + // avoid creation of new instances + if (this_ == nullptr) { + this_ = new ShotBrowserEngine; + } + return this_; +} + +QObject *ShotBrowserEngine::qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine) { + Q_UNUSED(engine); + Q_UNUSED(scriptEngine); + // C++ and QML instance they are the same instance + return ShotBrowserEngine::instance(); +} + +QObject *ShotBrowserEngine::presetsModel() { return dynamic_cast(presets_model_); } + +void ShotBrowserEngine::JSONTreeSendEventFunc(const utility::JsonStore &event) { + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, event.dump(2)); + if (backend_) { + anon_mail(utility::event_atom_v, json_store::sync_atom_v, user_preset_event_id_, event) + .send(backend_); + } +} + +void ShotBrowserEngine::createGroupModel(const int project_id) { + const auto key = query_engine_.cache_name("Group", project_id); + + if (not query_engine_.get_cache(key)) { + query_engine_.set_cache(key, R"([])"_json); + getGroupsFuture(project_id); + } +} + +void ShotBrowserEngine::createUserCache(const int project_id) { + const auto key = query_engine_.cache_name("User", project_id); + + if (not query_engine_.get_cache(key)) { + query_engine_.set_cache(key, R"([])"_json); + getUsersFuture(project_id); + } +} + +void ShotBrowserEngine::createSequenceModels(const int project_id) { + if (not sequences_tree_map_.count(project_id)) { + sequences_tree_map_[project_id] = new ShotBrowserSequenceModel(this); + getSequencesFuture(project_id); + getTreeFuture(project_id); + } +} + +void ShotBrowserEngine::createAssetModels(const int project_id) { + if (not asset_tree_map_.count(project_id)) { + asset_tree_map_[project_id] = new ShotBrowserSequenceModel(this); + getAssetFuture(project_id); + } +} + +QObject *ShotBrowserEngine::sequenceTreeModel(const int project_id) { + createSequenceModels(project_id); + return sequences_tree_map_[project_id]; +} + +QObject *ShotBrowserEngine::assetTreeModel(const int project_id) { + createAssetModels(project_id); + return asset_tree_map_[project_id]; +} + +void ShotBrowserEngine::createShotCache(const int project_id) { + const auto key = query_engine_.cache_name("Shot", project_id); + + if (not query_engine_.get_lookup(key)) { + query_engine_.set_lookup(key, R"([])"_json); + getShotsFuture(project_id); + } +} + +void ShotBrowserEngine::createEpisodeCache(const int project_id) { + const auto key = query_engine_.cache_name("Episode", project_id); + + if (not query_engine_.get_lookup(key)) { + query_engine_.set_lookup(key, R"([])"_json); + getEpisodesFuture(project_id); + } +} + +void ShotBrowserEngine::createStageCache(const int project_id) { + const auto key = query_engine_.cache_name("Stage", project_id); + + if (not query_engine_.get_cache(key)) { + query_engine_.set_cache(key, R"([])"_json); + getStageFuture(project_id); + } +} + +void ShotBrowserEngine::createUnitCache(const int project_id) { + const auto key = query_engine_.cache_name("Unit", project_id); + + if (not query_engine_.get_cache(key)) { + query_engine_.set_cache(key, R"([])"_json); + getCustomEntity24Future(project_id); + } +} + +void ShotBrowserEngine::createAssetCache(const int project_id) { + const auto key = query_engine_.cache_name("Asset", project_id); + + if (not query_engine_.get_cache(key)) { + query_engine_.set_cache(key, R"([])"_json); + getAssetFuture(project_id); + } +} + +void ShotBrowserEngine::createPlaylistCache(const int project_id) { + const auto key = query_engine_.cache_name("Playlist", project_id); + + if (not query_engine_.get_lookup(key)) { + query_engine_.set_lookup(key, R"([])"_json); + getPlaylistsFuture(project_id); + } +} + +void ShotBrowserEngine::cacheProject(const int project_id) { + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, project_id); + if (not QueryEngine::precache_needed(project_id, query_engine_.lookup()).empty()) { + createGroupModel(project_id); + createUserCache(project_id); + createSequenceModels(project_id); + createShotCache(project_id); + createEpisodeCache(project_id); + createPlaylistCache(project_id); + createUnitCache(project_id); + createAssetCache(project_id); + createStageCache(project_id); + } +} + +QString ShotBrowserEngine::getProjectFromMetadata() const { + auto result = QString(); + try { + auto jp = + json::json_pointer("/metadata/shotgun/version/relationships/project/data/name"); + + if (live_link_metadata_.contains(jp)) { + result = QStringFromStd(live_link_metadata_.at(jp)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + return result; +} + +QVariant ShotBrowserEngine::getEntityFromMetadata() const { + auto result = QVariant(); + try { + static const auto jp = + json::json_pointer("/metadata/shotgun/version/relationships/entity/data"); + + if (live_link_metadata_.contains(jp)) { + result = mapFromValue(live_link_metadata_.at(jp)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + return result; +} + +void ShotBrowserEngine::setLiveLinkKey(const QVariant &key) { + if (live_link_key_ != key) { + live_link_key_ = key; + emit liveLinkKeyChanged(); + } +} + +void ShotBrowserEngine::setLiveLinkMetadata(QString &data) { + if (data == "null" or data == "") + data = "{}"; + + if (data != live_link_metadata_string_) { + try { + live_link_metadata_ = JsonStore(nlohmann::json::parse(StdFromQString(data))); + live_link_metadata_string_ = data; + + auto project = query_engine_.get_project_name(live_link_metadata_); + auto project_id = query_engine_.get_project_id(live_link_metadata_); + + // update model caches. + cacheProject(project_id); + + emit projectChanged(project_id, QStringFromStd(project)); + } catch (const std::exception &err) { + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), StdFromQString(data)); + live_link_metadata_ = JsonStore(R"({})"_json); + live_link_metadata_string_ = "{}"; + } + + emit liveLinkMetadataChanged(); + } +} + +JsonStore ShotBrowserEngine::getUserData() const { + JsonStore result; + + auto users = query_engine_.get_cache("User"); + if (users) { + auto login = get_login_name(); + try { + for (const auto &i : *users) { + if (i.at("attributes").at("login") == login) { + result = i; + break; + } + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + return result; +} + +QString ShotBrowserEngine::shotGridUserName() { + QString result = QStringFromStd(get_user_name()); + + auto jsn = getUserData(); + if (not jsn.is_null()) { + try { + result = QStringFromStd(jsn.at("attributes").at("name")); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + return result; +} + +QString ShotBrowserEngine::shotGridUserType() { + QString result = "User"; + + auto jsn = getUserData(); + if (not jsn.is_null()) { + try { + result = QStringFromStd( + jsn.at("relationships").at("permission_rule_set").at("data").at("name")); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + return QString("REPLACE_WITH_RESULT"); +} + + +void ShotBrowserEngine::init(caf::actor_system &system) { + QMLActor::init(system); + + // access global data store to retrieve stored filters. + // delay this just in case we're to early.. + anon_mail(shotgun_preferences_atom_v).delay(std::chrono::seconds(2)).send(as_actor()); + + // request data for presets. + + + set_message_handler([=](actor_companion *) -> message_handler { + return { + [=](shotgun_acquire_authentication_atom, const std::string &message) { + emit requestSecret(QStringFromStd(message)); + }, + [=](shotgun_preferences_atom) { + // loadPresets(); + loadPresetModelFuture(); + }, + + [=](shotgun_preferences_atom, const std::string &preset) { + // flushPreset(preset); + }, + // [=](utility::event_atom, + // put_data_atom, + // const std::string &path, + // const JsonStore &data) { + // spdlog::warn("setModelData {}", data.dump(2)); + // presets_model_->setModelPathData(path, data); + // }, + + [=](utility::event_atom, + json_store::sync_atom, + const Uuid &uuid, + const JsonStore &event) { + if (uuid == user_preset_event_id_) { + presets_model_->receiveEvent(event); + if (not ready_) + checkReady(); + } else if (uuid == system_preset_event_id_) { + // spdlog::warn("json_store::sync_atom {} system_preset_event_id_", + // event.dump(2)); + query_engine_.system_presets().process_event(event); + } + }, + + // catchall for dealing with results from shotgun + [=](shotgun_info_atom, const JsonStore &request, const JsonStore &data) { + try { + if (request.at("type") == "project") { + query_engine_.set_cache(query_engine_.cache_name("Project"), data); + if (not ready_) + checkReady(); + } else if (request.at("type") == "user_preset_model") { + user_preset_event_id_ = data.at("uuid"); + presets_model_->setModelPathData("", data.at("data")); + if (not ready_) + checkReady(); + } else if (request.at("type") == "system_preset_model") { + system_preset_event_id_ = data.at("uuid"); + query_engine_.system_presets().reset_data(data.at("data")); + } else if (request.at("type") == "user") { + const auto project_id = request.at("project_id").get(); + if (project_id != -1) { + updateQueryValueCache("User", data, project_id); + query_engine_.set_cache( + query_engine_.cache_name("User", project_id), data); + } else { + updateQueryValueCache("User", data); + query_engine_.set_cache(query_engine_.cache_name("User"), data); + if (not ready_) + checkReady(); + + // we can now return correct name + emit shotGridUserNameChanged(); + emit shotGridUserTypeChanged(); + } + } else if (request.at("type") == "department") { + updateQueryValueCache("Department", data); + if (not ready_) + checkReady(); + } else if (request.at("type") == "location") { + updateQueryValueCache("Site", data); + if (not ready_) + checkReady(); + } else if (request.at("type") == "review_location") { + updateQueryValueCache("Review Location", data); + if (not ready_) + checkReady(); + } else if (request.at("type") == "reference_tag") { + // qvariant_cast( + // term_models_->value("referenceTagModel")) + // ->setModelData(data.at("data")); + } else if (request.at("type") == "shot_status") { + updateQueryValueCache("Exclude Shot Status", data); + updateQueryValueCache("Shot Status", data); + if (not ready_) + checkReady(); + } else if (request.at("type") == "playlist_type") { + updateQueryValueCache("Playlist Type", data); + if (not ready_) + checkReady(); + } else if (request.at("type") == "unit") { + const auto project_id = request.at("project_id").get(); + updateQueryValueCache("Unit", data, project_id); + query_engine_.set_cache( + query_engine_.cache_name("Unit", project_id), data); + } else if (request.at("type") == "asset") { + const auto project_id = request.at("project_id").get(); + updateQueryValueCache("Asset", data, project_id); + query_engine_.set_cache( + query_engine_.cache_name("Asset", project_id), data); + + query_engine_.set_asset_list_cache( + QueryEngine::cache_name("AssetList", project_id), data); + + // if mode exists populate it.. + if (asset_tree_map_.count(project_id)) { + auto types = QStringList(); + asset_tree_map_[request.at("project_id")]->setModelData( + ShotBrowserSequenceModel::flatToAssetTree(data, types)); + asset_tree_map_[request.at("project_id")]->setTypes(types); + } + + } else if (request.at("type") == "stage") { + const auto project_id = request.at("project_id").get(); + updateQueryValueCache("Stage", data, project_id); + query_engine_.set_cache( + query_engine_.cache_name("Stage", project_id), data); + } else if (request.at("type") == "production_status") { + updateQueryValueCache("Production Status", data); + if (not ready_) + checkReady(); + } else if (request.at("type") == "pipe_step") { + query_engine_.set_cache( + query_engine_.cache_name("Pipeline Step"), data); + if (not ready_) + checkReady(); + } else if (request.at("type") == "note_type") { + updateQueryValueCache("Note Type", data); + if (not ready_) + checkReady(); + } else if (request.at("type") == "pipeline_status") { + updateQueryValueCache("Pipeline Status", data); + if (not ready_) + checkReady(); + } else if (request.at("type") == "sequence_status") { + updateQueryValueCache("Sequence Status", data); + if (not ready_) + checkReady(); + } else if (request.at("type") == "group") { + const auto project_id = request.at("project_id").get(); + query_engine_.set_cache( + query_engine_.cache_name("Group", project_id), data); + } else if (request.at("type") == "sequence") { + updateQueryValueCache( + "Sequence", data, request.at("project_id").get()); + } else if (request.at("type") == "tree") { + const auto project_id = request.at("project_id").get(); + auto types = QStringList(); + sequences_tree_map_[request.at("project_id")]->setModelData( + ShotBrowserSequenceModel::flatToTree(data, types)); + sequences_tree_map_[request.at("project_id")]->setTypes(types); + query_engine_.set_shot_sequence_list_cache( + QueryEngine::cache_name("ShotSequenceList", project_id), data); + } else if (request.at("type") == "shot") { + updateQueryValueCache( + "Shot", data, request.at("project_id").get()); + } else if (request.at("type") == "episode") { + updateQueryValueCache( + "Episode", data, request.at("project_id").get()); + } else if (request.at("type") == "playlist") { + updateQueryValueCache( + "Playlist", data, request.at("project_id").get()); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + }, + + // catchall for dealing with results from shotgun + [=](shotgun_info_atom, const JsonStore &request) {}, + + [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + }; + }); +} + +void ShotBrowserEngine::setConnected(const bool connected) { + if (connected_ != connected) { + connected_ = connected; + if (connected_) { + populateCaches(); + } + emit connectedChanged(); + } +} + +void ShotBrowserEngine::authenticate(const bool cancel) { + anon_mail(shotgun_acquire_authentication_atom_v, cancel).send(backend_); +} + +void ShotBrowserEngine::set_backend(caf::actor backend) { + backend_ = backend; + // this forces the use of futures to access datasource + // if this is not done the application will deadlock if the password + // is requested, as we'll already be in this class, + // this is because the UI runs in a single thread. + // I'm not aware of any solutions to this. + // we can use timeouts though.. + // but as we're accessing a remote service, we'll need to be careful.. + + scoped_actor sys{system()}; + + if (backend_events_) { + try { + request_receive( + *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); + } catch (const std::exception &) { + } + backend_events_ = caf::actor(); + } + + if (backend_) { + try { + backend_events_ = + request_receive(*sys, backend_, get_event_group_atom_v); + request_receive( + *sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); + } catch (const std::exception &) { + } + anon_mail(module::connect_to_ui_atom_v).send(backend_); + anon_mail(shotgun_authentication_source_atom_v, as_actor()).send(backend_); + } +} + +void ShotBrowserEngine::setBackendId(const QString &qid) { + caf::actor_id id = std::stoull(StdFromQString(qid).c_str()); + + if (id and id != backend_id_) { + auto actor = system().registry().template get(id); + if (actor) { + set_backend(actor); + backend_str_ = QStringFromStd(to_string(backend_)); + backend_id_ = backend_.id(); + emit backendChanged(); + emit backendIdChanged(); + } else { + spdlog::warn("{} Actor lookup failed", __PRETTY_FUNCTION__); + } + } +} + +void ShotBrowserEngine::populateCaches() { + getProjectsFuture(); + getUsersFuture(); + getPipeStepFuture(); + getDepartmentsFuture(); + getReferenceTagsFuture(); + + getSchemaFieldsFuture("location"); + getSchemaFieldsFuture("review_location"); + getSchemaFieldsFuture("playlist_type"); + getSchemaFieldsFuture("shot_status"); + getSchemaFieldsFuture("note_type"); + getSchemaFieldsFuture("production_status"); + getSchemaFieldsFuture("pipeline_status"); + getSchemaFieldsFuture("sequence_status"); +} + + +QFuture ShotBrowserEngine::requestFileTransferFuture( + const QVariantList &qitems, + const QString &project, + const QString &src_location, + const QString &dest_location) { + return QtConcurrent::run([=]() { + QString program = "dnenv-do"; + QString result; + + auto show_env = utility::get_env("SHOW"); + auto shot_env = utility::get_env("SHOT"); + + QStringList args = { + QStringFromStd(show_env ? *show_env : "NSFL"), + QStringFromStd(shot_env ? *shot_env : "ldev_pipe"), + "--", + "ft-cp", + // "-n", + "-debug", + "-show", + project, + "-e", + "production", + "--watchers", + QStringFromStd(get_login_name()), + "-priority", + "medium", + "-description", + "File transfer requested by xStudio"}; + + std::vector items; + for (const auto &i : qitems) { + if (i.userType() == QMetaType::QUrl) + items.emplace_back(uri_to_posix_path(UriFromQUrl(i.toUrl()))); + else + items.emplace_back(to_string(UuidFromQUuid(i.toUuid()))); + } + + if (src_location.isEmpty()) + args.push_back(QStringFromStd(join_as_string(items, ","))); + else + args.push_back(src_location + ":" + QStringFromStd(join_as_string(items, ","))); + + args.push_back(dest_location); + + qint64 pid; + QProcess::startDetached(program, args, "", &pid); + + return result; + }); +} + + +// void ShotBrowserEngine::updateModel(const QString &qname) { +// auto name = StdFromQString(qname); + +// if (name == "referenceTagModel") +// getReferenceTagsFuture(); +// } + +void ShotBrowserEngine::receivedDataSlot( + const QPersistentModelIndex &index, const int role, const QString &result) { + if (index.isValid()) { + // this might be bad.. + auto model = const_cast(index.model()); + model->setData( + index, mapFromValue(nlohmann::json::parse(StdFromQString(result))), role); + } +} + + +void ShotBrowserEngine::requestData( + const QPersistentModelIndex &index, const int role, const nlohmann::json &request) const { + + auto tmp = new ShotBrowserResponse(index, role, request, backend_); + + connect(tmp, &ShotBrowserResponse::received, this, &ShotBrowserEngine::receivedDataSlot); +} + +QFuture ShotBrowserEngine::undoFuture() { + return QtConcurrent::run([=]() { + auto result = false; + try { + scoped_actor sys{system()}; + result = request_receive( + *sys, + backend_, + history::undo_atom_v, + utility::sys_time_duration(std::chrono::milliseconds(500))); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; + }); +} + +QFuture ShotBrowserEngine::redoFuture() { + return QtConcurrent::run([=]() { + auto result = false; + try { + scoped_actor sys{system()}; + result = request_receive( + *sys, + backend_, + history::redo_atom_v, + utility::sys_time_duration(std::chrono::milliseconds(500))); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; + }); +} + +QFuture ShotBrowserEngine::getSequencePathFuture( + const QStringList &preferred, const QString &project, const QUuid &stalk) { + return QtConcurrent::run([=]() { + auto result = QUrl(); + + try { + scoped_actor sys{system()}; + + auto ivy = system().registry().template get("IVYDATASOURCE"); + + if (not ivy) + throw std::runtime_error("Failed to find Ivy Datasource"); + + auto jsn = request_receive( + *sys, + ivy, + data_source::get_data_atom_v, + StdFromQString(project), + UuidFromQUuid(stalk)); + + for (const auto &i : preferred) { + auto p_name = StdFromQString(i); + for (const auto &file : jsn.at("data").at("versions_by_id").at(0).at("files")) { + if (file.at("name") == p_name and fs::exists(forward_remap_file_path(file.at("path")))) { + result = QUrlFromUri(posix_path_to_uri(file.at("path"))); + break; + } + } + if (result != QUrl()) { + break; + } + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; + }); +} + + +//![plugin] +class ShotBrowserUIQml : public QQmlExtensionPlugin { + Q_OBJECT + Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid) + + public: + void registerTypes(const char *uri) override { + qmlRegisterType( + uri, 1, 0, "ShotBrowserSequenceFilterModel"); + qmlRegisterType( + uri, 1, 0, "ShotBrowserPresetTreeFilterModel"); + qmlRegisterType(uri, 1, 0, "ShotBrowserResultModel"); + qmlRegisterType(uri, 1, 0, "ShotBrowserFilterModel"); + qmlRegisterType( + uri, 1, 0, "ShotBrowserResultFilterModel"); + qmlRegisterType( + uri, 1, 0, "ShotBrowserPresetFilterModel"); + qmlRegisterSingletonType( + uri, 1, 0, "ShotBrowserEngine", &ShotBrowserEngine::qmlInstance); + } + void initializeEngine(QQmlEngine *engine, const char *uri) override { + engine->addImageProvider(QLatin1String("shotgrid"), new ShotgridProvider); + } +}; +//![plugin] + +#include "shotbrowser_engine_ui.moc" diff --git a/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_ui.hpp b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_ui.hpp new file mode 100644 index 000000000..d0c837c39 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_engine_ui.hpp @@ -0,0 +1,531 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include "xstudio/ui/qml/helper_ui.hpp" + +#include "query_engine.hpp" + +CAF_PUSH_WARNINGS +#include +#include +#include +#include +#include +#include +#include +#include +CAF_POP_WARNINGS + +namespace xstudio { +namespace ui { + namespace qml { + using namespace std::chrono_literals; + const auto SHOTGRID_TIMEOUT = 120s; + + class ShotBrowserSequenceFilterModel; + class ShotBrowserSequenceModel; + class ShotBrowserListModel; + class ShotBrowserFilterModel; + class ShotBrowserPresetModel; + + class ShotBrowserEngine : public QMLActor { + + Q_OBJECT + // Q_DISABLE_COPY(ShotBrowserEngine) + Q_PROPERTY(bool connected READ connected WRITE setConnected NOTIFY connectedChanged) + Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) + + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) + Q_PROPERTY(QString backend READ backend NOTIFY backendChanged) + + Q_PROPERTY( + QString shotGridUserName READ shotGridUserName NOTIFY shotGridUserNameChanged) + Q_PROPERTY( + QString shotGridUserType READ shotGridUserType NOTIFY shotGridUserTypeChanged) + + Q_PROPERTY(QObject *presetsModel READ presetsModel NOTIFY presetsModelChanged) + + Q_PROPERTY( + QString backendId READ backendId WRITE setBackendId NOTIFY backendIdChanged) + + Q_PROPERTY(QString liveLinkMetadata READ liveLinkMetadata WRITE setLiveLinkMetadata + NOTIFY liveLinkMetadataChanged) + Q_PROPERTY(QVariant liveLinkKey READ liveLinkKey WRITE setLiveLinkKey NOTIFY + liveLinkKeyChanged) + + QML_NAMED_ELEMENT("ShotBrowserEngine") + //![0] + public: + static ShotBrowserEngine *instance(); + static QObject *qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine); + ~ShotBrowserEngine() override = default; + + void init(caf::actor_system &system) override; + QString name() { return name_; } + QString backend() { return backend_str_; } + QString backendId() { return QStringFromStd(std::to_string(backend_id_)); } + + void set_backend(caf::actor backend); + void setBackendId(const QString &id); + + Q_INVOKABLE QObject *presetsModel(); + Q_INVOKABLE QObject *sequenceTreeModel(const int project_id); + Q_INVOKABLE QObject *assetTreeModel(const int project_id); + + Q_INVOKABLE QString shotGridUserName(); + Q_INVOKABLE QString shotGridUserType(); + + [[nodiscard]] bool connected() const { return connected_; } + + void setConnected(const bool connected); + + QString liveLinkMetadata() const { return live_link_metadata_string_; } + void setLiveLinkMetadata(QString &data); + + Q_INVOKABLE QString getProjectFromMetadata() const; + Q_INVOKABLE QVariant getEntityFromMetadata() const; + // Q_INVOKABLE QString getAssetFromMetadata() const; + + QVariant liveLinkKey() const { return live_link_key_; } + void setLiveLinkKey(const QVariant &key); + + Q_INVOKABLE bool undo() { return undoFuture().result(); }; + Q_INVOKABLE bool redo() { return redoFuture().result(); } + + Q_INVOKABLE QFuture undoFuture(); + Q_INVOKABLE QFuture redoFuture(); + + bool ready() const { return ready_; } + + QueryEngine &queryEngine() { return query_engine_; } + + private: + explicit ShotBrowserEngine(QObject *parent = nullptr); + void updateQueryEngineTermModel(const std::string &key, const bool cache); + + signals: + void liveLinkMetadataChanged(); + void liveLinkKeyChanged(); + void connectedChanged(); + void nameChanged(); + void requestSecret(const QString &message); + void backendChanged(); + void backendIdChanged(); + void systemInit(QObject *object); + + void presetModelsChanged(); + void presetsModelChanged(); + void readyChanged(); + void shotGridUserNameChanged(); + void shotGridUserTypeChanged(); + + void projectChanged(const int project_id, const QString &project_name); + + public slots: + void authenticate(const bool cancel = false); + void setName(const QString &name) { + if (name_ != name) { + name_ = name; + emit nameChanged(); + } + } + + utility::JsonStore getUserData() const; + + // void updateModel(const QString &name); + + QFuture getSequencePathFuture( + const QStringList &preferred, const QString &project, const QUuid &stalk); + QUrl getSequencePath( + const QStringList &preferred, const QString &project, const QUuid &stalk) { + return getSequencePathFuture(preferred, project, stalk).result(); + } + + QFuture getDataFuture(const QString &type, const int project_id = -1); + + QString getPlaylistVersions(const int id) { + return getPlaylistVersionsFuture(id).result(); + } + QFuture getPlaylistVersionsFuture(const int id); + + QString getPlaylistNotes(const int id) { + return getPlaylistNotesFuture(id).result(); + } + QFuture getPlaylistNotesFuture(const int id); + + QString addVersionToPlaylist( + const QString &version, const QUuid &playlist, const QUuid &before = QUuid()) { + return addVersionToPlaylistFuture(version, playlist, before).result(); + } + QFuture addVersionToPlaylistFuture( + const QString &version, const QUuid &playlist, const QUuid &before = QUuid()); + + QFuture addVersionsToPlaylistFuture( + const QVariantMap &versions, + const QUuid &playlist, + const QUuid &before = QUuid()); + + QString getProjects() { return getProjectsFuture().result(); } + QFuture getProjectsFuture(); + + QFuture getSchemaFieldsFuture(const QString &type); + + QFuture getGroupsFuture(const int project_id); + + QString getSequences(const int project_id) { + return getSequencesFuture(project_id).result(); + } + QFuture getSequencesFuture(const int project_id); + + QString getShots(const int project_id) { + return getShotsFuture(project_id).result(); + } + QFuture getShotsFuture(const int project_id); + + QString getEpisodes(const int project_id) { + return getEpisodesFuture(project_id).result(); + } + QFuture getEpisodesFuture(const int project_id); + + QString getUsers(const int project_id = -1) { + return getUsersFuture(project_id).result(); + } + QFuture getUsersFuture(const int project_id = -1); + + QString getPipeStep() { return getPipeStepFuture().result(); } + QFuture getPipeStepFuture(); + + QString getEntity(const QString &entity, const int id) { + return getEntityFuture(entity, id).result(); + } + QFuture getEntityFuture(const QString &entity, const int id); + + QString getReferenceTags() { return getReferenceTagsFuture().result(); } + QFuture getReferenceTagsFuture(); + + QString tagEntity(const QString &entity, const int record_id, const int tag_id) { + return tagEntityFuture(entity, record_id, tag_id).result(); + } + QFuture + tagEntityFuture(const QString &entity, const int record_id, const int tag_id); + + QString untagEntity(const QString &entity, const int record_id, const int tag_id) { + return untagEntityFuture(entity, record_id, tag_id).result(); + } + QFuture + untagEntityFuture(const QString &entity, const int record_id, const int tag_id); + + QString createTag(const QString &text) { return createTagFuture(text).result(); } + QFuture createTagFuture(const QString &text); + + QString renameTag(const int tag_id, const QString &text) { + return renameTagFuture(tag_id, text).result(); + } + QFuture renameTagFuture(const int tag_id, const QString &text); + + QString removeTag(const int tag_id) { return removeTagFuture(tag_id).result(); } + QFuture removeTagFuture(const int tag_id); + + QString updateEntity( + const QString &entity, const int record_id, const QString &update_json) { + return updateEntityFuture(entity, record_id, update_json).result(); + } + QFuture updateEntityFuture( + const QString &entity, const int record_id, const QString &update_json); + + QString updatePlaylistVersions(const QUuid &playlist, const bool append = false) { + return updatePlaylistVersionsFuture(playlist, append).result(); + } + QFuture + updatePlaylistVersionsFuture(const QVariant &playlist, const bool append = false) { + return updatePlaylistVersionsFuture(playlist.toUuid(), append); + } + QFuture + updatePlaylistVersionsFuture(const QUuid &playlist, const bool append = false); + + QString + refreshPlaylistVersions(const QUuid &playlist, const bool matchOrder = false) { + return refreshPlaylistVersionsFuture(playlist, matchOrder).result(); + } + // QFuture refreshPlaylistVersionsFuture( + // const QVariant &playlist, const bool matchOrder = false) { + // return refreshPlaylistVersionsFuture(playlist.toUuid(), matchOrder); + // } + QFuture + refreshPlaylistVersionsFuture(const QUuid &playlist, const bool matchOrder = false); + + // QString refreshPlaylistNotes(const QUuid &playlist) { + // return refreshPlaylistNotesFuture(playlist).result(); + // } + // QFuture refreshPlaylistNotesFuture(const QVariant &playlist) { + // return refreshPlaylistNotesFuture(playlist.toUuid()); + // } + // QFuture refreshPlaylistNotesFuture(const QUuid &playlist); + + QString createPlaylist( + const QUuid &playlist, + const int project_id, + const QString &name, + const QString &location, + const QString &playlist_type) { + + return createPlaylistFuture(playlist, project_id, name, location, playlist_type) + .result(); + } + QFuture createPlaylistFuture( + const QVariant &playlist, + const int project_id, + const QString &name, + const QString &location, + const QString &playlist_type) { + return createPlaylistFuture( + playlist.toUuid(), project_id, name, location, playlist_type); + } + QFuture createPlaylistFuture( + const QUuid &playlist, + const int project_id, + const QString &name, + const QString &location, + const QString &playlist_type); + + + QString getVersions(const int project_id, const QVariant &ids) { + return getVersionsFuture(project_id, ids).result(); + } + QFuture getVersionsFuture(const int project_id, const QVariant &ids); + + QString preparePlaylistNotes( + const QUuid &playlist, + const QList &media, + const bool notify_owner = false, + const std::vector notify_group_ids = {}, + const bool combine = false, + const bool add_time = false, + const bool add_playlist_name = false, + const bool add_type = false, + const bool anno_requires_note = true, + const bool skip_already_published = false, + const QString &defaultType = "") { + return preparePlaylistNotesFuture( + playlist, + media, + notify_owner, + notify_group_ids, + combine, + add_time, + add_playlist_name, + add_type, + anno_requires_note, + skip_already_published, + defaultType) + .result(); + } + QFuture preparePlaylistNotesFuture( + const QUuid &playlist, + const QList &media, + const bool notify_owner = false, + const std::vector notify_group_ids = {}, + const bool combine = false, + const bool add_time = false, + const bool add_playlist_name = false, + const bool add_type = false, + const bool anno_requires_note = true, + const bool skip_already_published = false, + const QString &defaultType = ""); + + QString pushPlaylistNotes(const QString ¬es, const QUuid &playlist) { + return pushPlaylistNotesFuture(notes, playlist).result(); + } + QFuture + pushPlaylistNotesFuture(const QString ¬es, const QUuid &playlist); + + QString getPlaylistValidMediaCount(const QUuid &playlist) { + return getPlaylistValidMediaCountFuture(playlist).result(); + } + QFuture getPlaylistValidMediaCountFuture(const QUuid &playlist); + + QString getPlaylistLinkMedia(const QUuid &playlist) { + return getPlaylistLinkMediaFuture(playlist).result(); + } + QFuture getPlaylistLinkMediaFuture(const QUuid &playlist); + + QString requestFileTransfer( + const QVariantList &items, + const QString &project, + const QString &src_location, + const QString &dest_location) { + return requestFileTransferFuture(items, project, src_location, dest_location) + .result(); + } + QFuture requestFileTransferFuture( + const QVariantList &items, + const QString &project, + const QString &src_location, + const QString &dest_location); + + void populateCaches(); + + // QFuture executeProjectQuery( + // const QStringList &preset_paths, + // const int project_id, + // const QVariantMap &env = QVariantMap(), + // const QVariantList &custom_terms = QVariantList(), + // const QVariantMap &customContext = QVariantMap()); + + // QFuture executeQuery( + // const QStringList &preset_paths, + // const QVariantMap &env = QVariantMap(), + // const QVariantList &custom_terms = QVariantList(), + // const QVariantMap &customContext = QVariantMap()); + + + QFuture executeProjectQueryJSON( + const QStringList &preset_paths, + const int project_id, + const QVariantMap &env = QVariantMap(), + const QVariantList &custom_terms = QVariantList(), + const QVariantMap &customContext = QVariantMap()); + + QFuture executeQueryJSON( + const QStringList &preset_paths, + const QVariantMap &env = QVariantMap(), + const QVariantList &custom_terms = QVariantList(), + const QVariantMap &customContext = QVariantMap()); + + QVariant mergeQueries( + const QVariant &dst, + const QVariant &src, + const bool ignore_duplicates = true) const; + + // value used by query, may map to id. + + void updateQueryValueCache( + const std::string &type, + const utility::JsonStore &data, + const int project_id = -1); + + QFuture addDownloadToMediaFuture(const QUuid &uuid); + QString addDownloadToMedia(const QUuid &uuid) { + return addDownloadToMediaFuture(uuid).result(); + } + + QFuture downloadMediaFuture( + const int entity_id, + const QString &entity_name, + const QString &project_name, + const QString &parent_name); + QUrl downloadMedia( + const int entity_id, + const QString &entity_name, + const QString &project_name, + const QString &parent_name) { + try { + return downloadMediaFuture( + entity_id, entity_name, project_name, parent_name) + .result(); + } catch (const std::exception &err) { + return QUrl(); + } + } + + QFuture downloadImageFuture( + const int entity_id, + const QString &entity, + const QString &entity_name, + const QString &project_name); + QUrl downloadImage( + const int entity_id, + const QString &entity, + const QString &entity_name, + const QString &project_name) { + try { + return downloadImageFuture(entity_id, entity, entity_name, project_name) + .result(); + } catch (const std::exception &err) { + spdlog::warn("{}", err.what()); + return QUrl(); + } + } + + void cacheProject(const int project_id); + + void requestData( + const QPersistentModelIndex &index, + const int role, + const nlohmann::json &request) const; + + private: + void JSONTreeSendEventFunc(const utility::JsonStore &event); + + void receivedDataSlot( + const QPersistentModelIndex &index, const int role, const QString &result); + + QFuture loadPresetModelFuture(); + + // unit + QFuture getCustomEntity24Future(const int project_id); + + QFuture getAssetFuture(const int project_id); + + QFuture getTreeFuture(const int project_id); + QFuture getAssetTreeFuture(const int project_id); + + void createUnitCache(const int project_id); + void createAssetCache(const int project_id); + + QFuture getStageFuture(const int project_id); + void createStageCache(const int project_id); + + QFuture getPlaylistsFuture(const int project_id); + void createPlaylistCache(const int project_id); + + void createShotCache(const int project_id); + void createEpisodeCache(const int project_id); + void createSequenceModels(const int project_id); + void createAssetModels(const int project_id); + void createGroupModel(const int project_id); + void createUserCache(const int project_id); + + QFuture getDepartmentsFuture(); + + void checkReady(); + + private: + bool connected_{false}; + static ShotBrowserEngine *this_; + caf::actor backend_; + caf::actor_id backend_id_{0}; + caf::actor backend_events_; + + QString backend_str_; + QString name_{"test"}; + + QMap sequences_tree_map_; + QMap asset_tree_map_; + + std::map preset_update_pending_; + int maximum_result_count_{2500}; + + QVariant live_link_key_; + QString live_link_metadata_string_; + utility::JsonStore live_link_metadata_; + + // map UI value to query value + QueryEngine query_engine_; + + ShotBrowserPresetModel *presets_model_{nullptr}; + + bool disable_flush_{true}; + + utility::Uuid user_preset_event_id_; + utility::Uuid system_preset_event_id_; + + bool ready_{false}; + }; + + } // namespace qml +} // namespace ui +} // namespace xstudio diff --git a/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_plugin.cpp b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_plugin.cpp new file mode 100644 index 000000000..54b687f59 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_plugin.cpp @@ -0,0 +1,2045 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "xstudio/global_store/global_store.hpp" +#include "xstudio/media/media_actor.hpp" +#include "xstudio/playlist/playlist_actor.hpp" +#include "xstudio/history/history_actor.hpp" +#include "xstudio/shotgun_client/shotgun_client.hpp" +#include "xstudio/shotgun_client/shotgun_client_actor.hpp" +#include "xstudio/utility/chrono.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/notification_handler.hpp" + +#include "shotbrowser_plugin.hpp" + +using namespace xstudio; +using namespace xstudio::utility; +using namespace xstudio::shotgun_client; +using namespace xstudio::shotbrowser; +using namespace xstudio::global_store; +using namespace xstudio::data_source; + +using namespace std::chrono_literals; + +#define JOB_DISPATCH_DELAY std::chrono::milliseconds(10) + +// Note the 'StandardPlugin' class is a Module AND a caf::event_based_actor. +// So we are a full actor we don't have to do that 'set_parent_actor_addr' call +ShotBrowser::ShotBrowser(caf::actor_config &cfg, const utility::JsonStore &init_settings) + : plugin::StandardPlugin(cfg, "ShotBrowser", init_settings) { + + // You might not want or need to use attributes in your plugin as you'll + // be doing a QAbstractItemModel ... however, they can be useful for + // creating menus and holding values with preferences etc + + add_attributes(); + + bind_attribute_changed_callback( + [this](auto &&PH1) { attribute_changed(std::forward(PH1)); }); + + // print_on_exit(this, "MediaHookActor"); + secret_source_ = actor_cast(this); + + shotgun_ = spawn(); + link_to(shotgun_); + + // we need to recieve authentication updates. + join_event_group(this, shotgun_); + + history_uuid_ = Uuid::generate(); + history_ = spawn>(history_uuid_); + link_to(history_); + + // disable history until we init + anon_mail(plugin_manager::enable_atom_v, false).send(history_); + + event_group_ = spawn(this); + link_to(event_group_); + // we are the source of the secret.. + anon_mail(shotgun_authentication_source_atom_v, actor_cast(this)) + .send(shotgun_); + + + try { + auto prefs = GlobalStoreHelper(system()); + JsonStore j; + join_broadcast(this, prefs.get_group(j)); + update_preferences(j); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + if (not disable_integration_) + system().registry().put( + shotbrowser_datasource_registry, caf::actor_cast(this)); + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + pool_ = caf::actor_pool::make( + system(), + worker_count_, + [&] { return system().template spawn(actor_cast(this)); }, + caf::actor_pool::round_robin()); + link_to(pool_); +#pragma GCC diagnostic pop + + + // set_parent_actor_addr(actor_cast(this)); + // anon_mail(// module::connect_to_ui_atom_v).delay(// + // std::chrono::milliseconds(500)).send(// caf::actor_cast(this)); + + // set json store as origin + // we rebroadcast all changes. + engine().user_presets().set_origin(true); + engine().user_presets().bind_send_event_func([&](auto &&PH1, auto &&PH2) { + auto event = JsonStore(std::forward(PH1)); + auto undo_redo = std::forward(PH2); + + // spdlog::warn("user_presets bind_send_event_func {} {}", event.dump(2), undo_redo); + + if (not undo_redo) + anon_mail(history::log_atom_v, sysclock::now(), event).send(history_); + + mail(utility::event_atom_v, json_store::sync_atom_v, engine().user_uuid(), event) + .send(event_group_); + }); + + engine().system_presets().set_origin(true); + engine().system_presets().bind_send_event_func([&](auto &&PH1, auto &&PH2) { + auto event = JsonStore(std::forward(PH1)); + mail(utility::event_atom_v, json_store::sync_atom_v, engine().system_uuid(), event) + .send(event_group_); + }); + + // this call is essential to set-up the base class + make_behavior(); + + // this call is required for attribute 'groups', menus etc. to be exposed + // in the Qt/QML layer ... if you're not using attrs or backend menu + // generation at all then you don't need this. + connect_to_ui(); + + // here's the code where we register our interface as an xSTUDIO 'panel' + // that can be selected in the UI + + + auto show_shotbrowser = register_hotkey( + int('S'), + ui::NoModifier, + "Show ShotBrowser panel", + "Shows or hides the pop-out Shotbrowser Plugin Panel"); + + register_ui_panel_qml( + "Shot Browser", + R"( + import ShotBrowser 1.0 + ShotBrowserRoot { + anchors.fill: parent + } + )", + 9.1f, // position in panels drop-down menu + "qrc:/icons/cloud-download.svg", + 5.0f, + show_shotbrowser); + + register_ui_panel_qml( + "Notes History", + R"( + import ShotBrowser 1.0 + NotesHistory { + anchors.fill: parent + } + )", + 9.2f, // position in panels drop-down menu + "qrc:/shotbrowser_icons/note_history.svg", + 6.0f); + + register_ui_panel_qml( + "Shot History", + R"( + import ShotBrowser 1.0 + ShotHistory { + anchors.fill: parent + } + )", + 9.3f, // position in panels drop-down menu + "qrc:/shotbrowser_icons/shot_history.svg", + 7.0f); + + // this causes a divider to be inserted in the panels drop down menu + register_ui_panel_qml("divider", "divider", 9.4f); + + // new method for instantiating a 'singleton' qml item which can + // do a one-time insertion of menu items into any menu model + register_singleton_qml( + R"( + import ShotBrowser 1.0 + ShotBrowserMediaListMenu {} + )"); +} + +void ShotBrowser::on_exit() { + // maybe on timer.. ? + for (auto &i : waiting_) + i.deliver(make_error(xstudio_error::error, "Password request cancelled.")); + waiting_.clear(); + system().registry().erase(shotbrowser_datasource_registry); +} + + +void ShotBrowser::set_authentication_method(const std::string &value) { + if (authentication_method_->value() != value) + authentication_method_->set_value(value); +} +void ShotBrowser::set_client_id(const std::string &value) { + if (client_id_->value() != value) + client_id_->set_value(value); +} +void ShotBrowser::set_client_secret(const std::string &value) { + if (client_secret_->value() != value) + client_secret_->set_value(value); +} +void ShotBrowser::set_username(const std::string &value) { + if (username_->value() != value) + username_->set_value(value); +} +void ShotBrowser::set_password(const std::string &value) { + if (password_->value() != value) + password_->set_value(value); +} +void ShotBrowser::set_session_token(const std::string &value) { + if (session_token_->value() != value) + session_token_->set_value(value); +} +void ShotBrowser::set_authenticated(const bool value) { + if (authenticated_->value() != value) + authenticated_->set_value(value); +} +void ShotBrowser::set_timeout(const int value) { + if (timeout_->value() != value) + timeout_->set_value(value); +} + +shotgun_client::AuthenticateShotgun ShotBrowser::get_authentication() const { + AuthenticateShotgun auth; + + auth.set_session_uuid(to_string(session_id_)); + + auth.set_authentication_method(authentication_method_->value()); + switch (*(auth.authentication_method())) { + case AM_SCRIPT: + auth.set_client_id(client_id_->value()); + auth.set_client_secret(client_secret_->value()); + break; + case AM_SESSION: + auth.set_session_token(session_token_->value()); + break; + case AM_LOGIN: + auth.set_username(expand_envvars(username_->value())); + auth.set_password(password_->value()); + break; + case AM_UNDEFINED: + default: + break; + } + + return auth; +} + +void ShotBrowser::add_attributes() { + + std::vector auth_method_names = { + "client_credentials", "password", "session_token"}; + + // module::QmlCodeAttribute *button = add_qml_code_attribute( + // "MyCode", + // R"( + // import Shotgun 1.0 + // ShotgunButton {} + // )"); + + // button->set_role_data(module::Attribute::ToolbarPosition, 1010.0); + // button->expose_in_ui_attrs_group("media_tools_buttons"); + + + authentication_method_ = add_string_choice_attribute( + "authentication_method", + "authentication_method", + "password", + auth_method_names, + auth_method_names); + + // playlist_notes_action_ = + // add_action_attribute("playlist_notes_to_shotgun", "playlist_notes_to_shotgun"); + // selected_notes_action_ = + // add_action_attribute("selected_notes_to_shotgun", "selected_notes_to_shotgun"); + + client_id_ = add_string_attribute("client_id", "client_id", ""); + client_secret_ = add_string_attribute("client_secret", "client_secret", ""); + username_ = add_string_attribute("username", "username", ""); + password_ = add_string_attribute("password", "password", ""); + session_token_ = add_string_attribute("session_token", "session_token", ""); + authenticated_ = add_boolean_attribute("authenticated", "authenticated", false); + // should be int.. + timeout_ = add_float_attribute("timeout", "timeout", 120.0, 10.0, 600.0, 1.0, 0); + + + // by setting static UUIDs on these module we only create them once in the UI + // playlist_notes_action_->set_role_data( + // module::Attribute::UuidRole, "92c780be-d0bc-462a-b09f-643e8986e2a1"); + // playlist_notes_action_->set_role_data( + // module::Attribute::Title, "Publish Playlist Notes..."); + // playlist_notes_action_->set_role_data( + // module::Attribute::UIDataModels, nlohmann::json{"shotgun_datasource_menu"}); + // playlist_notes_action_->set_role_data( + // module::Attribute::MenuPaths, std::vector({"publish_menu|ShotGrid"})); + + // selected_notes_action_->set_role_data( + // module::Attribute::UuidRole, "7583a4d0-35d8-4f00-bc32-ae8c2bddc30a"); + // selected_notes_action_->set_role_data( + // module::Attribute::Title, "Publish Selected Notes..."); + // selected_notes_action_->set_role_data( + // module::Attribute::UIDataModels, nlohmann::json{"shotgun_datasource_menu"}); + // selected_notes_action_->set_role_data( + // module::Attribute::MenuPaths, std::vector({"publish_menu|ShotGrid"})); + + authentication_method_->set_role_data( + module::Attribute::UuidRole, "e512646d-08c0-4049-91fb-04dac38f75f2"); + client_id_->set_role_data( + module::Attribute::UuidRole, "bb0a6e65-efd5-4115-89ca-b81652440b27"); + client_secret_->set_role_data( + module::Attribute::UuidRole, "89b549af-c44d-48ee-9874-f628ce4c2112"); + username_->set_role_data( + module::Attribute::UuidRole, "bad9c1a5-0f3f-4ccf-91e2-93817a04c6f1"); + password_->set_role_data( + module::Attribute::UuidRole, "ebf90da7-b2bb-46a3-8a85-dbd28c454ea6"); + session_token_->set_role_data( + module::Attribute::UuidRole, "c1f92a57-53c1-48f9-ac22-5387e06fee97"); + authenticated_->set_role_data( + module::Attribute::UuidRole, "cf9ed35c-e82c-4665-bedd-7785a767cff6"); + timeout_->set_role_data( + module::Attribute::UuidRole, "fff5d3e7-06c1-432b-b89d-f78f695f84e7"); + + authentication_method_->set_role_data( + module::Attribute::UIDataModels, nlohmann::json{"shotbrowser_datasource_preference"}); + client_id_->set_role_data( + module::Attribute::UIDataModels, nlohmann::json{"shotbrowser_datasource_preference"}); + client_secret_->set_role_data( + module::Attribute::UIDataModels, nlohmann::json{"shotbrowser_datasource_preference"}); + username_->set_role_data( + module::Attribute::UIDataModels, nlohmann::json{"shotbrowser_datasource_preference"}); + password_->set_role_data( + module::Attribute::UIDataModels, nlohmann::json{"shotbrowser_datasource_preference"}); + session_token_->set_role_data( + module::Attribute::UIDataModels, nlohmann::json{"shotbrowser_datasource_preference"}); + authenticated_->set_role_data( + module::Attribute::UIDataModels, nlohmann::json{"shotbrowser_datasource_preference"}); + timeout_->set_role_data( + module::Attribute::UIDataModels, nlohmann::json{"shotbrowser_datasource_preference"}); + + authentication_method_->set_role_data( + module::Attribute::ToolTip, "ShotGrid authentication method."); + + client_id_->set_role_data(module::Attribute::ToolTip, "ShotGrid script key."); + client_secret_->set_role_data(module::Attribute::ToolTip, "ShotGrid script secret."); + username_->set_role_data(module::Attribute::ToolTip, "ShotGrid username."); + password_->set_role_data(module::Attribute::ToolTip, "ShotGrid password."); + session_token_->set_role_data(module::Attribute::ToolTip, "ShotGrid session token."); + authenticated_->set_role_data(module::Attribute::ToolTip, "Authenticated."); + timeout_->set_role_data(module::Attribute::ToolTip, "ShotGrid server timeout."); +} + +void ShotBrowser::attribute_changed(const utility::Uuid &attr_uuid, const int /*role*/) { + // pass upto actor.. + call_attribute_changed(attr_uuid); +} + + +caf::message_handler ShotBrowser::message_handler_extensions() { + + // Here's where our own message handler is declared + + return caf::message_handler( + {make_get_event_group_handler(event_group_), + [=](utility::name_atom) -> std::string { return "ShotBrowser"; }, + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + + [=](utility::event_atom, + json_store::sync_atom, + const Uuid &uuid, + const JsonStore &event) { + if (uuid == engine().user_uuid()) { + // spdlog::warn("user json_store::sync_atom {}", event.dump(2)); + engine().user_presets().process_event(event, true, false, false); + + // trigger sync to preferences + if (not pending_preference_update_) { + pending_preference_update_ = true; + anon_mail(json_store::sync_atom_v, true) + .delay(std::chrono::seconds(1)) + .send(actor_cast(this)); + } + } else if (uuid == engine().system_uuid()) { + // spdlog::warn("system json_store::sync_atom {}", event.dump(2)); + engine().system_presets().process_event(event, true, false, false); + } + }, + + [=](json_store::sync_atom, const Uuid &uuid) -> JsonStore { + if (uuid == engine().user_uuid()) + return engine().user_presets().as_json(); + else if (uuid == engine().system_uuid()) + return engine().system_presets().as_json(); + + return JsonStore(); + }, + + [=](json_store::sync_atom, const bool) { + pending_preference_update_ = false; + auto prefs = GlobalStoreHelper(system()); + prefs.set_value( + engine().user_presets().as_json().at("children"), + "/plugin/data_source/shotbrowser/user_presets", + false); + prefs.save("PLUGIN"); + }, + + [=](json_store::sync_atom) -> UuidVector { + return UuidVector({engine().user_uuid(), engine().system_uuid()}); + }, + + [=](shotgun_projects_atom atom) { return mail(atom).delegate(shotgun_); }, + + [=](shotgun_groups_atom atom, const int project_id) { + return mail(atom, project_id).delegate(shotgun_); + }, + + [=](shotgun_schema_atom atom, const int project_id) { + return mail(atom, project_id).delegate(shotgun_); + }, + + [=](shotgun_authentication_source_atom, caf::actor source) { + secret_source_ = actor_cast(source); + }, + + [=](shotgun_authentication_source_atom) -> caf::actor { + return actor_cast(secret_source_); + }, + + [=](shotgun_update_entity_atom atom, + const std::string &entity, + const int record_id, + const JsonStore &body) { + return mail(atom, entity, record_id, body).delegate(shotgun_); + }, + + [=](shotgun_update_entity_atom atom, + const std::string &entity, + const int record_id, + const JsonStore &body, + const std::vector &fields) { + return mail(atom, entity, record_id, body, fields).delegate(shotgun_); + }, + + [=](shotgun_image_atom atom, + const std::string &entity, + const int record_id, + const bool thumbnail) { + return mail(atom, entity, record_id, thumbnail).delegate(shotgun_); + }, + + [=](shotgun_delete_entity_atom atom, const std::string &entity, const int record_id) { + return mail(atom, entity, record_id).delegate(shotgun_); + }, + + [=](shotgun_image_atom atom, + const std::string &entity, + const int record_id, + const bool thumbnail, + const bool as_buffer) { + return mail(atom, entity, record_id, thumbnail, as_buffer).delegate(shotgun_); + }, + + [=](shotgun_upload_atom atom, + const std::string &entity, + const int record_id, + const std::string &field, + const std::string &name, + const std::vector &data, + const std::string &content_type) { + return mail(atom, entity, record_id, field, name, data, content_type) + .delegate(shotgun_); + }, + + // just use the default with jsonstore ? + [=](put_data_atom, const utility::JsonStore &js) -> result { + auto rp = make_response_promise(); + put_action(rp, js); + return rp; + }, + + [=](data_source::use_data_atom, const caf::actor &media, const FrameRate &media_rate) + -> result { return UuidActorVector(); }, + + // no drop support.. + [=](data_source::use_data_atom, const JsonStore &, const FrameRate &, const bool) + -> UuidActorVector { return UuidActorVector(); }, + + // do we need the UI to have spun up before we can issue calls to shotgun... + // erm... + [=](use_data_atom atom, const caf::uri &uri) -> result { + auto rp = make_response_promise(); + use_action(rp, uri, FrameRate(), true); + return rp; + }, + + [=](use_data_atom, + const caf::uri &uri, + const FrameRate &media_rate, + const bool create_playlist) -> result { + auto rp = make_response_promise(); + use_action(rp, uri, media_rate, create_playlist); + return rp; + }, + + [=](use_data_atom, const utility::JsonStore &js, const caf::actor &session) + -> result { + auto rp = make_response_promise(); + use_action(rp, js, session); + return rp; + }, + + // just use the default with jsonstore ? + [=](use_data_atom, const utility::JsonStore &js) -> result { + auto rp = make_response_promise(); + use_action(rp, js); + return rp; + }, + + // just use the default with jsonstore ? + + [=](post_data_atom, const utility::JsonStore &js) -> result { + auto rp = make_response_promise(); + post_action(rp, js); + return rp; + }, + + [=](shotgun_entity_atom atom, + const std::string &entity, + const int record_id, + const std::vector &fields) { + return mail(atom, entity, record_id, extend_fields(entity, fields)) + .delegate(shotgun_); + }, + + [=](shotgun_entity_filter_atom atom, + const std::string &entity, + const JsonStore &filter, + const std::vector &fields, + const std::vector &sort) { + return mail(atom, entity, filter, extend_fields(entity, fields), sort) + .delegate(shotgun_); + }, + + [=](shotgun_entity_filter_atom atom, + const std::string &entity, + const JsonStore &filter, + const std::vector &fields, + const std::vector &sort, + const int page, + const int page_size) { + return mail( + atom, + entity, + filter, + extend_fields(entity, fields), + sort, + page, + page_size) + .delegate(shotgun_); + }, + + [=](shotgun_schema_entity_fields_atom atom, + const std::string &entity, + const std::string &field, + const int id) { return mail(atom, entity, field, id).delegate(shotgun_); }, + + [=](shotgun_entity_search_atom atom, + const std::string &entity, + const JsonStore &conditions, + const std::vector &fields, + const std::vector &sort, + const int page, + const int page_size) { + return mail( + atom, + entity, + conditions, + extend_fields(entity, fields), + sort, + page, + page_size) + .delegate(shotgun_); + }, + + [=](shotgun_text_search_atom atom, + const std::string &text, + const JsonStore &conditions, + const int page, + const int page_size) { + return mail(atom, text, conditions, page, page_size).delegate(shotgun_); + }, + + // can't reply via qt mixin.. this is a work around.. + [=](shotgun_acquire_authentication_atom, const bool cancelled) { + if (cancelled) { + set_authenticated(false); + for (auto &i : waiting_) + i.deliver( + make_error(xstudio_error::error, "Authentication request cancelled.")); + } else { + auto auth = get_authentication(); + if (waiting_.empty()) { + anon_mail(shotgun_authenticate_atom_v, auth).send(shotgun_); + } else { + for (auto &i : waiting_) + i.deliver(auth); + } + } + waiting_.clear(); + }, + + [=](shotgun_acquire_authentication_atom atom, + const std::string &message) -> result { + if (secret_source_ == actor_cast(this)) + return make_error(xstudio_error::error, "No authentication source."); + + auto rp = make_response_promise(); + waiting_.push_back(rp); + set_authenticated(false); + anon_mail(atom, message).send(actor_cast(secret_source_)); + return rp; + }, + + [=](utility::event_atom, + shotgun_acquire_token_atom, + const std::pair &tokens) { + auto prefs = GlobalStoreHelper(system()); + prefs.set_value( + tokens.second, + "/plugin/data_source/shotbrowser/authentication/refresh_token", + false); + prefs.save("PLUGIN"); + set_authenticated(true); + }, + + [=](playlist::add_media_atom, + const utility::JsonStore &data, + const utility::Uuid &playlist_uuid, + const caf::actor &playlist, + const utility::Uuid &before) -> result> { + auto rp = make_response_promise>(); + add_media_to_playlist( + rp, + data, + playlist ? false : true, + playlist_uuid, + playlist, + before, + FrameRate()); + return rp; + }, + + [=](playlist::add_media_atom, + const utility::JsonStore &data, + const bool create_playlist, + const FrameRate &media_rate) -> result> { + auto rp = make_response_promise>(); + add_media_to_playlist( + rp, data, create_playlist, Uuid(), caf::actor(), Uuid(), media_rate); + return rp; + }, + + [=](playlist::move_media_atom, caf::actor playlist, const JsonStore &sg_playlist) + -> result { + auto rp = make_response_promise(); + reorder_playlist(rp, playlist, sg_playlist); + return rp; + }, + + [=](playlist::add_media_atom) { + // this message handler is called in a loop until all build media + // tasks in the queue are exhausted + + bool is_ivy_build_task; + + auto build_media_task_data = get_next_build_task(is_ivy_build_task); + while (build_media_task_data) { + + if (is_ivy_build_task) { + + do_add_media_sources_from_ivy(build_media_task_data); + + } else { + + do_add_media_sources_from_shotgun(build_media_task_data); + } + + // N.B. we only get a new build task if the number of incomplete tasks + // already dispatched is less than the number of actors in our + // worker pool + build_media_task_data = get_next_build_task(is_ivy_build_task); + } + }, + + [=](get_data_atom, const utility::JsonStore &js) -> result { + auto rp = make_response_promise(); + get_action(rp, js); + return rp; + }, + + [=](history::history_atom) -> UuidActor { return UuidActor(history_uuid_, history_); }, + + // loop with timepoint + [=](history::undo_atom, const sys_time_point &key) -> result { + auto rp = make_response_promise(); + + mail(history::undo_atom_v, key) + .request(history_, infinite) + .then( + [=](const JsonStore &hist) mutable { + rp.delegate( + caf::actor_cast(this), history::undo_atom_v, hist); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + return rp; + }, + + // loop with timepoint + [=](history::redo_atom, const sys_time_point &key) -> result { + auto rp = make_response_promise(); + + mail(history::redo_atom_v, key) + .request(history_, infinite) + .then( + [=](const JsonStore &hist) mutable { + rp.delegate( + caf::actor_cast(this), history::redo_atom_v, hist); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + return rp; + }, + + [=](history::undo_atom, const utility::sys_time_duration &duration) -> result { + auto rp = make_response_promise(); + mail(history::undo_atom_v, duration) + .request(history_, infinite) + .then( + [=](const std::vector &hist) mutable { + auto count = std::make_shared(0); + for (const auto &h : hist) { + mail(history::undo_atom_v, h) + .request(caf::actor_cast(this), infinite) + .then( + [=](const bool) mutable { + (*count)++; + if (*count == hist.size()) + rp.deliver(true); + }, + [=](const error &err) mutable { + (*count)++; + if (*count == hist.size()) + rp.deliver(true); + }); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + return rp; + }, + + [=](history::redo_atom, const utility::sys_time_duration &duration) -> result { + auto rp = make_response_promise(); + mail(history::redo_atom_v, duration) + .request(history_, infinite) + .then( + [=](const std::vector &hist) mutable { + auto count = std::make_shared(0); + for (const auto &h : hist) { + mail(history::redo_atom_v, h) + .request(caf::actor_cast(this), infinite) + .then( + [=](const bool) mutable { + (*count)++; + if (*count == hist.size()) + rp.deliver(true); + }, + [=](const error &err) mutable { + (*count)++; + if (*count == hist.size()) + rp.deliver(true); + }); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + return rp; + }, + + [=](history::undo_atom) -> result { + auto rp = make_response_promise(); + mail(history::undo_atom_v) + .request(history_, infinite) + .then( + [=](const JsonStore &hist) mutable { + rp.delegate( + caf::actor_cast(this), history::undo_atom_v, hist); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + return rp; + }, + + [=](history::redo_atom) -> result { + auto rp = make_response_promise(); + mail(history::redo_atom_v) + .request(history_, infinite) + .then( + [=](const JsonStore &hist) mutable { + rp.delegate( + caf::actor_cast(this), history::redo_atom_v, hist); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + return rp; + }, + + [=](history::undo_atom, const JsonStore &hist) -> result { + auto rp = make_response_promise(); + + // spdlog::warn("undo {}", hist.dump(2)); + + // apply undo to state. + // don't store state change in undo redo... + + engine().user_presets().process_event(hist, false, true, true); + + + rp.deliver(true); + // base_.item().undo(hist); + + // auto inverted = R"([])"_json; + // for (const auto &i : hist) { + // auto ev = R"({})"_json; + // ev["redo"] = i.at("undo"); + // ev["undo"] = i.at("redo"); + // inverted.emplace_back(ev); + // } + + // mail(event_atom_v, item_atom_v, JsonStore(inverted), true).send(event_group_); + // if (not actors_.empty()) { + // // push to children.. + // fan_out_request( + // map_value_to_vec(actors_), infinite, history::undo_atom_v, hist) + // .await( + // [=](std::vector updated) mutable { + // anon_mail(link_media_atom_v, media_actors_).send(this); + // mail(// event_atom_v, + // item_atom_v, + // JsonStore(inverted), + // true).send(// event_group_); + // rp.deliver(true); + // }, + // [=](error &err) mutable { rp.deliver(std::move(err)); }); + // } else { + // mail(event_atom_v, item_atom_v, JsonStore(inverted), + // true).send(event_group_); rp.deliver(true); + // } + return rp; + }, + + [=](history::redo_atom, const JsonStore &hist) -> result { + auto rp = make_response_promise(); + // spdlog::warn("redo {}", hist.dump(2)); + + + engine().user_presets().process_event(hist, true, true, true); + + rp.deliver(true); + + // base_.item().redo(hist); + + // // mail(event_atom_v, item_atom_v, hist, true).send(event_group_); + + // if (not actors_.empty()) { + // // push to children.. + // fan_out_request( + // map_value_to_vec(actors_), infinite, history::redo_atom_v, hist) + // .await( + // [=](std::vector updated) mutable { + // rp.deliver(true); + // anon_mail(link_media_atom_v, media_actors_).send(this); + + // mail(event_atom_v, item_atom_v, hist, true).send(event_group_); + // }, + // [=](error &err) mutable { rp.deliver(std::move(err)); }); + // } else { + // mail(event_atom_v, item_atom_v, hist, true).send(event_group_); + // rp.deliver(true); + // } + + return rp; + }, + + [=](json_store::update_atom, + const JsonStore & /*change*/, + const std::string & /*path*/, + const JsonStore &full) { + return mail(json_store::update_atom_v, full) + .delegate(actor_cast(this)); + }, + + [=](json_store::update_atom, const JsonStore &js) { + try { + update_preferences(js); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + }}); +} + + +void ShotBrowser::attribute_changed(const utility::Uuid &attr_uuid) { + // properties changed somewhere. + // update loop ? + if (attr_uuid == authentication_method_->uuid()) { + auto prefs = GlobalStoreHelper(system()); + prefs.set_value( + authentication_method_->value(), + "/plugin/data_source/shotbrowser/authentication/grant_type"); + } + if (attr_uuid == client_id_->uuid()) { + auto prefs = GlobalStoreHelper(system()); + prefs.set_value( + client_id_->value(), "/plugin/data_source/shotbrowser/authentication/client_id"); + } + // if (attr_uuid == client_secret_->uuid()) { + // auto prefs = GlobalStoreHelper(system()); + // prefs.set_value(client_secret_->value(), + // "/plugin/data_source/shotbrowser/authentication/client_secret"); + // } + if (attr_uuid == timeout_->uuid()) { + auto prefs = GlobalStoreHelper(system()); + prefs.set_value(timeout_->value(), "/plugin/data_source/shotbrowser/server/timeout"); + } + + if (attr_uuid == username_->uuid()) { + auto prefs = GlobalStoreHelper(system()); + prefs.set_value( + username_->value(), "/plugin/data_source/shotbrowser/authentication/username"); + } + // if (attr_uuid == password_->uuid()) { + // auto prefs = GlobalStoreHelper(system()); + // prefs.set_value(password_->value(), + // "/plugin/data_source/shotbrowser/authentication/password"); + // } + if (attr_uuid == session_token_->uuid()) { + auto prefs = GlobalStoreHelper(system()); + prefs.set_value( + session_token_->value(), + "/plugin/data_source/shotbrowser/authentication/session_token"); + } +} + +void ShotBrowser::update_preferences(const JsonStore &js) { + try { + auto grant = preference_value( + js, "/plugin/data_source/shotbrowser/authentication/grant_type"); + + auto client_id = preference_value( + js, "/plugin/data_source/shotbrowser/authentication/client_id"); + auto client_secret = preference_value( + js, "/plugin/data_source/shotbrowser/authentication/client_secret"); + auto username = preference_value( + js, "/plugin/data_source/shotbrowser/authentication/username"); + auto password = preference_value( + js, "/plugin/data_source/shotbrowser/authentication/password"); + auto session_token = preference_value( + js, "/plugin/data_source/shotbrowser/authentication/session_token"); + + auto refresh_token = preference_value( + js, "/plugin/data_source/shotbrowser/authentication/refresh_token"); + + disable_integration_ = + preference_value(js, "/plugin/data_source/shotbrowser/disable_integration"); + + auto host = + preference_value(js, "/plugin/data_source/shotbrowser/server/host"); + auto port = preference_value(js, "/plugin/data_source/shotbrowser/server/port"); + auto protocol = preference_value( + js, "/plugin/data_source/shotbrowser/server/protocol"); + auto timeout = + preference_value(js, "/plugin/data_source/shotbrowser/server/timeout"); + + auto cache_dir = expand_envvars( + preference_value(js, "/plugin/data_source/shotbrowser/download/path")); + auto cache_size = + preference_value(js, "/plugin/data_source/shotbrowser/download/size"); + + download_cache_.prune_on_exit(true); + download_cache_.target(cache_dir, true); + download_cache_.max_size(cache_size * 1024 * 1024 * 1024); + + auto version_fields = + preference_value(js, "/plugin/data_source/shotbrowser/fields/version"); + auto note_fields = + preference_value(js, "/plugin/data_source/shotbrowser/fields/note"); + auto shot_fields = + preference_value(js, "/plugin/data_source/shotbrowser/fields/shot"); + auto playlist_fields = + preference_value(js, "/plugin/data_source/shotbrowser/fields/playlist"); + + version_fields_.clear(); + note_fields_.clear(); + shot_fields_.clear(); + playlist_fields_.clear(); + + version_fields_.insert( + version_fields_.end(), version_fields.begin(), version_fields.end()); + note_fields_.insert(note_fields_.end(), note_fields.begin(), note_fields.end()); + shot_fields_.insert(shot_fields_.end(), shot_fields.begin(), shot_fields.end()); + playlist_fields_.insert( + playlist_fields_.end(), playlist_fields.begin(), playlist_fields.end()); + + auto category = preference_value(js, "/core/bookmark/category"); + category_colours_.clear(); + if (category.is_array()) { + for (const auto &i : category) { + category_colours_[i.value("value", "default")] = i.value("colour", ""); + } + } + + auto pipe_step = + preference_value(js, "/plugin/data_source/shotbrowser/browser/pipestep"); + + if (auto ps = engine().get_cache(QueryEngine::cache_name("Pipeline Step")); not ps) { + engine().set_cache(QueryEngine::cache_name("Pipeline Step"), pipe_step); + } + + // no op ? + set_authentication_method(grant); + set_client_id(client_id); + set_client_secret(client_secret); + set_username(expand_envvars(username)); + set_password(password); + set_session_token(session_token); + set_timeout(timeout); + + // we ignore after initial setup.. + if (engine().user_presets().at("children").empty()) { + auto project_presets = preference_value( + js, "/plugin/data_source/shotbrowser/project_presets"); + auto site_presets = + preference_value(js, "/plugin/data_source/shotbrowser/site_presets"); + auto user_presets = + preference_value(js, "/plugin/data_source/shotbrowser/user_presets"); + + auto uorp = + preference_overridden_path(js, "/plugin/data_source/shotbrowser/user_presets"); + if (ends_with(uorp, "application-v2.json")) { + + auto prefs = GlobalStoreHelper(system()); + prefs.set_overridden_path( + replace_once(uorp, "/application-v2.json", "/plugin-v2.json"), + "/plugin/data_source/shotbrowser/user_presets", + false); + prefs.save("PLUGIN"); + } + + engine().merge_presets(site_presets, project_presets); + engine().set_presets(user_presets, site_presets); + + // turn on undo redo + anon_mail(plugin_manager::enable_atom_v, true).send(history_); + } + + // what hppens if we get a sequence of changes... should this be on a timed event ? + // watch out for multiple instances. + auto new_hash = std::hash{}( + grant + username + client_id + host + std::to_string(port) + protocol); + + if (new_hash != changed_hash_) { + changed_hash_ = new_hash; + // set server + anon_mail( + shotgun_host_atom_v, + std::string(fmt::format( + "{}://{}{}", protocol, host, (port ? ":" + std::to_string(port) : "")))) + .send(shotgun_); + + auto auth = get_authentication(); + if (not refresh_token.empty()) + auth.set_refresh_token(refresh_token); + + anon_mail(shotgun_credential_atom_v, auth).send(shotgun_); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} + +void ShotBrowser::refresh_playlist_versions( + caf::typed_response_promise rp, + const utility::Uuid &playlist_uuid, + const bool match_order) { + // grab playlist id, get versions compare/load into playlist + + auto notification_uuid = Uuid(); + auto playlist = caf::actor(); + + auto failed = [=](const caf::actor &dest, const Uuid &uuid) mutable { + if (dest and not uuid.is_null()) { + auto notify = Notification::WarnNotification("Reloading Playlist Failed"); + notify.uuid(uuid); + anon_mail(utility::notification_atom_v, notify).send(dest); + } + }; + + auto succeeded = [=](const caf::actor &dest, const Uuid &uuid) mutable { + if (dest and not uuid.is_null()) { + auto notify = Notification::InfoNotification( + "Reloading Playlist Succeeded", std::chrono::seconds(5)); + notify.uuid(uuid); + anon_mail(utility::notification_atom_v, notify).send(dest); + } + }; + + + try { + + scoped_actor sys{system()}; + + auto session = request_receive( + *sys, + system().registry().template get(global_registry), + session::session_atom_v); + + playlist = request_receive( + *sys, session, session::get_playlist_atom_v, playlist_uuid); + + if (playlist) { + auto notify = Notification::ProcessingNotification("Reloading Playlist"); + notification_uuid = notify.uuid(); + anon_mail(utility::notification_atom_v, notify).send(playlist); + } + + auto plsg = request_receive( + *sys, playlist, json_store::get_json_atom_v, ShotgunMetadataPath + "/playlist"); + + auto pl_id = plsg["id"].template get(); + + // this is a list of the media.. + auto media = + request_receive>(*sys, playlist, playlist::get_media_atom_v); + + // foreach media actor get it's shotgun metadata. + std::set current_version_ids; + + for (const auto &i : media) { + try { + auto mjson = request_receive( + *sys, + i.actor(), + json_store::get_json_atom_v, + utility::Uuid(), + ShotgunMetadataPath + "/version"); + current_version_ids.insert(mjson["id"].template get()); + } catch (const std::exception &err) { + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + // we got media shotgun ids, plus playlist id + // get current shotgun playlist/versions + mail(shotgun_entity_atom_v, "Playlists", pl_id, std::vector()) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &result) mutable { + try { + scoped_actor sys{system()}; + // update playlist + anon_mail( + json_store::set_json_atom_v, + JsonStore(result["data"]), + ShotgunMetadataPath + "/playlist") + .send(playlist); + + // gather versions, to get more detail.. + std::vector version_ids; + for (const auto &i : + result.at("data").at("relationships").at("versions").at("data")) { + if (not current_version_ids.count(i.at("id").template get())) + version_ids.emplace_back( + std::to_string(i.at("id").template get())); + } + + if (version_ids.empty()) { + if (match_order) { + // still need to match ordering.. + // which is in another table :( + anon_mail( + playlist::move_media_atom_v, + playlist, + JsonStore(result["data"])) + .send(caf::actor_cast(this)); + } + + succeeded(playlist, notification_uuid); + rp.deliver(result); + return; + } + + auto query = R"({})"_json; + query["id"] = join_as_string(version_ids, ","); + + // get details.. + mail( + shotgun_entity_filter_atom_v, + "Versions", + JsonStore(query), + VersionFields, + std::vector(), + 1, + 4999) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &result2) mutable { + try { + // got version details. + // we can now just call add versions to playlist.. + mail( + playlist::add_media_atom_v, + result2, + playlist_uuid, + playlist, + utility::Uuid()) + .request( + caf::actor_cast(this), infinite) + .then( + [=](const std::vector + &media) mutable { + if (match_order) + anon_mail( + playlist::move_media_atom_v, + playlist, + JsonStore(result["data"])) + .send(caf::actor_cast( + this)); + }, + [=](error &err) mutable { + spdlog::warn( + "{} {}", + __PRETTY_FUNCTION__, + to_string(err)); + }); + + // return this as the result. + rp.deliver(result); + succeeded(playlist, notification_uuid); + } catch (const std::exception &err) { + failed(playlist, notification_uuid); + rp.deliver( + make_error(xstudio_error::error, err.what())); + } + }, + + [=](error &err) mutable { + failed(playlist, notification_uuid); + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + } catch (const std::exception &err) { + failed(playlist, notification_uuid); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + failed(playlist, notification_uuid); + rp.deliver(err); + }); + + + } catch (const std::exception &err) { + failed(playlist, notification_uuid); + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +void ShotBrowser::add_media_to_playlist( + caf::typed_response_promise rp, + const utility::JsonStore &data, + const bool create_playlist, + utility::Uuid playlist_uuid, + caf::actor playlist, + const utility::Uuid &before, + const utility::FrameRate &media_rate_) { + // data can be in multiple forms.. + + auto sys = caf::scoped_actor(system()); + + nlohmann::json versions; + + try { + versions = data.at("data").at("relationships").at("versions").at("data"); + } catch (...) { + try { + versions = data.at("data"); + } catch (...) { + try { + versions = data.at("result").at("data"); + } catch (...) { + return rp.deliver(make_error(xstudio_error::error, "Invalid JSON")); + } + } + } + + if (versions.empty()) + return rp.deliver(std::vector()); + + // auto event_msg = std::shared_ptr(); + + + // get uuid for playlist + if (playlist and playlist_uuid.is_null()) { + try { + playlist_uuid = + request_receive(*sys, playlist, utility::uuid_atom_v); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + playlist = caf::actor(); + playlist_uuid = Uuid(); + } + } + + // get playlist for uuid + if (not playlist and not playlist_uuid.is_null()) { + try { + auto session = request_receive( + *sys, + system().registry().template get(global_registry), + session::session_atom_v); + + playlist = request_receive( + *sys, session, session::get_playlist_atom_v, playlist_uuid); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + playlist_uuid = utility::Uuid(); + } + } + + // create playlist.. + if (create_playlist and not playlist and playlist_uuid.is_null()) { + try { + auto session = request_receive( + *sys, + system().registry().template get(global_registry), + session::session_atom_v); + + auto tmp = request_receive>( + *sys, session, session::add_playlist_atom_v, "ShotGrid Media"); + + playlist_uuid = tmp.second.uuid(); + playlist = tmp.second.actor(); + } catch (const std::exception &err) { + spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + // if (not playlist_uuid.is_null()) { + // event_msg = std::make_shared( + // "Loading ShotGrid Playlist Media {}", + // 0, + // 0, + // versions.size(), // we increment progress once per version loaded - ivy leafs are + // // added after progress hits 100% + // std::set({playlist_uuid})); + // event::send_event(this, *event_msg); + // } + + try { + auto media_rate = media_rate_; + if (playlist) + media_rate = request_receive(*sys, playlist, session::media_rate_atom_v); + + std::string flag_text, flag_colour; + if (data.contains(json::json_pointer("/context/flag_text")) and + not data.at("context").value("flag_text", "").empty() and + not data.at("context").value("flag_colour", "").empty()) { + flag_colour = data.at("context").value("flag_colour", ""); + flag_text = data.at("context").value("flag_text", ""); + } + + std::vector visual_sources; + if (data.contains(json::json_pointer("/context/visual_source"))) { + auto tmp = data.at("context").value("visual_source", R"([])"_json); + for (const auto &i : tmp) + visual_sources.push_back(i); + } + + std::vector audio_sources; + if (data.contains(json::json_pointer("/context/audio_source"))) { + auto tmp = data.at("context").value("audio_source", R"([])"_json); + for (const auto &i : tmp) + audio_sources.push_back(i); + } + + // we need to ensure that media are added to playlist IN ORDER - this + // is a bit fiddly because media are created out of order by the worker + // pool so we use this utility::UuidList to ensure that the playlist builds + // with media in order + auto ordered_uuids = std::make_shared(); + auto result = std::make_shared(); + auto result_count = std::make_shared(0); + + // get a new media item created for each of the names in our list + for (const auto &i : versions) { + // create a task data item, with the raw shotgun data that + // can be used to build the media sources for each media + // item in the playlist + ordered_uuids->push_back(utility::Uuid::generate()); + build_playlist_media_tasks_.emplace_back(std::make_shared( + playlist, + ordered_uuids->back(), + i.at("attributes").at("code").get(), // name for the media + JsonStore(i), + media_rate, + visual_sources, + audio_sources, + ordered_uuids, + before, + flag_colour, + flag_text, + rp, + result, + result_count)); + } + + // this call starts the work of building the media and consuming + // the jobs in the 'build_playlist_media_tasks_' queue + mail(playlist::add_media_atom_v).send(this); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + // if (not playlist_uuid.is_null()) { + // event_msg->set_complete(); + // event::send_event(this, *event_msg); + // } + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +void ShotBrowser::load_playlist( + caf::typed_response_promise rp, + const int playlist_id, + const caf::actor &session) { + + // this is going to get nesty :() + + // get playlist from shotgun + mail(shotgun_entity_atom_v, "Playlists", playlist_id, std::vector()) + .request(caf::actor_cast(this), infinite) + .then( + [=](JsonStore pljs) mutable { + // got playlist. + // we can create an new xstudio playlist actor at this point.. + auto playlist = UuidActor(); + try { + if (session) { + scoped_actor sys{system()}; + + auto tmp = request_receive>( + *sys, + session, + session::add_playlist_atom_v, + pljs["data"]["attributes"]["code"].get(), + utility::Uuid(), + false); + + playlist = tmp.second; + + } else { + auto uuid = utility::Uuid::generate(); + auto tmp = spawn( + pljs["data"]["attributes"]["code"].get(), uuid); + playlist = UuidActor(uuid, tmp); + } + + // place holder for shotgun decorators. + anon_mail(json_store::set_json_atom_v, JsonStore(), "/metadata/shotgun") + .send(playlist.actor()); + // should really be driven from back end not UI.. + } catch (const std::exception &err) { + spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + + // get version order + auto order_filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["playlist", "is", {"type":"Playlist", "id":0}] + ] + })"_json; + + order_filter["conditions"][0][2]["id"] = playlist_id; + + mail( + shotgun_entity_search_atom_v, + "PlaylistVersionConnection", + JsonStore(order_filter), + std::vector({"sg_sort_order", "version"}), + std::vector({"sg_sort_order"}), + 1, + 4999) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &order) mutable { + std::vector version_ids; + for (const auto &i : order["data"]) + version_ids.emplace_back(std::to_string( + i["relationships"]["version"]["data"].at("id").get())); + + if (version_ids.empty()) + return rp.deliver( + make_error(xstudio_error::error, "No Versions found")); + + // get versions + auto query = R"({})"_json; + query["id"] = join_as_string(version_ids, ","); + + // get versions ordered by playlist. + mail( + shotgun_entity_filter_atom_v, + "Versions", + JsonStore(query), + VersionFields, + std::vector(), + 1, + 4999) + .request(caf::actor_cast(this), infinite) + .then( + [=](JsonStore &js) mutable { + // munge it.. + auto data = R"([])"_json; + + for (const auto &i : version_ids) { + for (auto &j : js["data"]) { + + // spdlog::warn("{} {}", + // std::to_string(j["id"].get()), i); + if (std::to_string(j["id"].get()) == i) { + data.push_back(j); + break; + } + } + } + + js["data"] = data; + + // add back in + pljs["data"]["relationships"]["versions"] = js; + + // spdlog::warn("{}",pljs.dump(2)); + // now we have a playlist json struct with the versions + // corrrecly ordered, set metadata on playlist.. + anon_mail( + json_store::set_json_atom_v, + JsonStore(pljs["data"]), + ShotgunMetadataPath + "/playlist") + .send(playlist.actor()); + + anon_mail( + json_store::set_json_atom_v, + JsonStore( + R"({"icon": "qrc:/shotbrowser_icons/shot_grid.svg", "tooltip": "ShotGrid Playlist"})"_json), + "/ui/decorators/shotgrid") + .send(playlist.actor()); + + anon_mail( + playlist::add_media_atom_v, + pljs, + playlist.uuid(), + playlist.actor(), + utility::Uuid()) + .send(caf::actor_cast(this)); + + rp.deliver(playlist); + }, + [=](error &err) mutable { + spdlog::error( + "{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver( + make_error(xstudio_error::error, to_string(err))); + }); + }, + [=](error &err) mutable { + spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(make_error(xstudio_error::error, to_string(err))); + }); + }, + [=](error &err) mutable { + spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(make_error(xstudio_error::error, to_string(err))); + }); +} + +std::shared_ptr +ShotBrowser::get_next_build_task(bool &is_ivy_build_task) { + + std::shared_ptr job_info; + // if we already have popped N jobs off the queue that haven't completed + // and N >= worker_count_ we don't pop a job off and instead return a null + // + if (build_tasks_in_flight_ < worker_count_) { + if (!build_playlist_media_tasks_.empty()) { + is_ivy_build_task = false; + job_info = build_playlist_media_tasks_.front(); + build_playlist_media_tasks_.pop_front(); + } else if (!extend_media_with_ivy_tasks_.empty()) { + is_ivy_build_task = true; + job_info = extend_media_with_ivy_tasks_.front(); + extend_media_with_ivy_tasks_.pop_front(); + } + } + return job_info; +} + +void ShotBrowser::do_add_media_sources_from_shotgun( + std::shared_ptr build_media_task_data) { + + // now 'build' the MediaActor via our worker pool to create + // MediaSources and add them + build_tasks_in_flight_++; + + // spawn a media actor + build_media_task_data->media_actor_ = spawn( + build_media_task_data->media_name_, + build_media_task_data->media_uuid_, + UuidActorVector()); + + auto ua = + UuidActor(build_media_task_data->media_uuid_, build_media_task_data->media_actor_); + + // this is called when we get a result back - keeps track of the number + // of jobs being processed and sends a message to self to continue working + // through the queue + auto continue_processing_job_queue = [=]() { + build_tasks_in_flight_--; + mail(playlist::add_media_atom_v).delay(JOB_DISPATCH_DELAY).send(this); + // if (build_media_task_data->event_msg_) { + // build_media_task_data->event_msg_->increment_progress(); + // event::send_event(this, *(build_media_task_data->event_msg_)); + // } + }; + + // now we get our worker pool to build media sources and add them to the + // parent MediaActor using the shotgun query data + mail( + playlist::add_media_atom_v, + build_media_task_data->media_actor_, + build_media_task_data->sg_data_, + build_media_task_data->media_rate_) + .request(pool_, caf::infinite) + .then( + + [=](bool) { + // media sources were constructed successfully - now we can add to + // the playlist, we pass in the overall ordered list of uuids that + // we are building so the playlist can ensure everything is added + // in order, even if they aren't created in the correct order + if (build_media_task_data->playlist_actor_) { + mail( + playlist::add_media_atom_v, + ua, + *(build_media_task_data->ordererd_uuids_), + build_media_task_data->before_) + .request(build_media_task_data->playlist_actor_, caf::infinite) + .then( + + [=](const UuidActor &) { + if (!build_media_task_data->flag_colour_.empty()) { + anon_mail( + playlist::reflag_container_atom_v, + std::make_tuple( + std::optional( + build_media_task_data->flag_colour_), + std::optional( + build_media_task_data->flag_text_))) + .send(build_media_task_data->media_actor_); + } + + extend_media_with_ivy_tasks_.emplace_back( + build_media_task_data); + continue_processing_job_queue(); + }, + [=](const caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + continue_processing_job_queue(); + }); + } else { + // not adding to playlist.. + if (!build_media_task_data->flag_colour_.empty()) { + anon_mail( + playlist::reflag_container_atom_v, + std::make_tuple( + std::optional(build_media_task_data->flag_colour_), + std::optional(build_media_task_data->flag_text_))) + .send(build_media_task_data->media_actor_); + } + + extend_media_with_ivy_tasks_.emplace_back(build_media_task_data); + continue_processing_job_queue(); + } + }, + [=](const caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + continue_processing_job_queue(); + }); +} + +void ShotBrowser::do_add_media_sources_from_ivy( + std::shared_ptr ivy_media_task_data) { + + auto ivy = system().registry().template get("IVYDATASOURCE"); + build_tasks_in_flight_++; + + // this is called when we get a result back - keeps track of the number + // of jobs being processed and sends a message to self to continue working + // through the queue + auto continue_processing_job_queue = [=]() { + build_tasks_in_flight_--; + mail(playlist::add_media_atom_v).delay(JOB_DISPATCH_DELAY).send(this); + /* Commented out bevause we're not including ivy leaf addition + in progress indicator now. + if (ivy_media_task_data->event_msg) { + ivy_media_task_data->event_msg->increment_progress(); + event::send_event(this, *(ivy_media_task_data->event_msg)); + }*/ + }; + + + auto good_sources = std::make_shared(); + auto count = std::make_shared(0); + + // this function adds the sources that are 'good' (i.e. were able + // to acquire MediaDetail) to the MediaActor - we only call it + // when we've fully 'built' each MediaSourceActor in our 'sources' + // list -0 see the request/then handler below where it is used + auto finalise = [=]() { + mail(media::add_media_source_atom_v, *good_sources) + .request(ivy_media_task_data->media_actor_, infinite) + .then( + [=](const bool) { + // media sources all in media actor. + // we can now select the ones we want.. + // + // get list of valid sources for media.. + mail(media::get_media_source_names_atom_v, media::MT_IMAGE) + .request(ivy_media_task_data->media_actor_, infinite) + .then( + [=](const std::vector> + &names) { + auto name = std::string(); + + for (const auto &pref : + ivy_media_task_data->preferred_visual_sources_) { + for (const auto &i : names) { + if (i.second == pref) { + name = pref; + break; + } + } + if (not name.empty()) + break; + } + + if (name.empty()) { + for (const auto &i : names) { + if (i.second == "movie_dneg") { + name = i.second; + break; + } + } + } + + if (name.empty()) { + for (const auto &i : names) { + if (i.second == "SG Movie") { + name = i.second; + break; + } + } + } + + if (not name.empty()) + anon_mail( + playhead::media_source_atom_v, + name, + media::MT_IMAGE, + true) + .send(ivy_media_task_data->media_actor_); + }, + [=](error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + + mail(media::get_media_source_names_atom_v, media::MT_AUDIO) + .request(ivy_media_task_data->media_actor_, infinite) + .then( + [=](const std::vector> + &names) { + auto name = std::string(); + + for (const auto &pref : + ivy_media_task_data->preferred_audio_sources_) { + for (const auto &i : names) { + if (i.second == pref) { + name = pref; + break; + } + } + if (not name.empty()) + break; + } + + if (name.empty()) { + for (const auto &i : names) { + if (i.second == "movie_dneg") { + name = i.second; + break; + } + } + } + + if (name.empty()) { + for (const auto &i : names) { + if (i.second == "SG Movie") { + name = i.second; + break; + } + } + } + + if (not name.empty()) + anon_mail( + playhead::media_source_atom_v, + name, + media::MT_AUDIO, + true) + .send(ivy_media_task_data->media_actor_); + }, + [=](error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + continue_processing_job_queue(); + }, + [=](error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + continue_processing_job_queue(); + }); + }; + + // here we get the ivy data source to fetch sources (ivy leafs) using the + // ivy dnuuid for the MediaActor already created from shotgun data + // pass media actor for metadata + + try { + mail( + use_data_atom_v, + ivy_media_task_data->sg_data_.at("attributes") + .at("sg_project_name") + .get(), + utility::Uuid(ivy_media_task_data->sg_data_.at("attributes") + .at("sg_ivy_dnuuid") + .get()), + ivy_media_task_data->media_actor_, + ivy_media_task_data->media_rate_) + .request(ivy, infinite) + .then( + [=](const utility::UuidActorVector &sources) { + // we want to make sure the 'MediaDetail' has been fetched on the + // sources before adding to the parent MediaActor - this means we + // don't build up a massive queue of IO heavy MediaDetail fetches + // but instead deal with them sequentially as each media item is + // added to the playlist + + if (sources.empty()) { + finalise(); + } else { + *count = sources.size(); + } + + for (auto source : sources) { + + // we need to get each source to get its detail to ensure that + // it is readable/valid + mail( + media::acquire_media_detail_atom_v, + ivy_media_task_data->media_rate_) + .request(source.actor(), infinite) + .then( + [=](bool got_media_detail) mutable { + if (got_media_detail) + good_sources->push_back(source); + else + send_exit( + source.actor(), caf::exit_reason::user_shutdown); + + (*count)--; + if (!(*count)) + finalise(); + }, + [=](error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + + // kill bad source. + send_exit(source.actor(), caf::exit_reason::user_shutdown); + + (*count)--; + if (!(*count)) + finalise(); + }); + } + }, + + [=](error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + continue_processing_job_queue(); + }); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + continue_processing_job_queue(); + } +} + +std::vector ShotBrowser::extend_fields( + const std::string &entity, const std::vector &fields) const { + + if (entity == "Notes" and fields == NoteFields) + return concatenate_vector(fields, note_fields_); + else if (entity == "Versions" and fields == VersionFields) + return concatenate_vector(fields, version_fields_); + else if (entity == "Shots" and fields == ShotFields) + return concatenate_vector(fields, shot_fields_); + else if (entity == "Playlists" and fields == PlaylistFields) + return concatenate_vector(fields, playlist_fields_); + + return fields; +} + +void ShotBrowser::reorder_playlist( + caf::typed_response_promise rp, + const caf::actor &playlist, + const utility::JsonStore &sg_playlist) { + // get version order + const static auto id_jpointer = + nlohmann::json::json_pointer("/relationships/version/data/id"); + + auto order_filter = R"( + { + "logical_operator": "and", + "conditions": [ + ["playlist", "is", {"type":"Playlist", "id":0}] + ] + })"_json; + + order_filter["conditions"][0][2]["id"] = sg_playlist.at("id"); + + mail( + shotgun_entity_search_atom_v, + "PlaylistVersionConnection", + JsonStore(order_filter), + std::vector({"sg_sort_order", "version"}), + std::vector({"sg_sort_order"}), + 1, + 4999) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &order) mutable { + try { + auto version_ids = std::vector(); + + for (const auto &i : order.at("data")) + version_ids.push_back(i.at(id_jpointer).get()); + + // get media from playlist.. + scoped_actor sys{system()}; + auto media = request_receive>( + *sys, playlist, playlist::get_media_atom_v); + + auto media_id = std::map(); + auto unused_media = std::list(); + + // get media current version id.. + for (const auto &i : media) { + unused_media.push_back(i.uuid()); + try { + auto mvid = request_receive( + *sys, + i.actor(), + json_store::get_json_atom_v, + utility::Uuid(), + ShotgunMetadataPath + "/version/id"); + media_id[mvid.get()] = i.uuid(); + } catch (const std::exception &) { + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + // build list of media uuids, ordered by version_ids. + auto new_media_order = std::vector(); + + for (const auto &i : version_ids) + if (media_id.count(i)) { + new_media_order.push_back(media_id.at(i)); + unused_media.erase(std::find( + unused_media.begin(), unused_media.end(), media_id.at(i))); + } + + new_media_order.insert( + new_media_order.end(), unused_media.begin(), unused_media.end()); + + // update playlist. + mail(playlist::move_media_atom_v, new_media_order, Uuid()) + .request(playlist, infinite) + .then( + [=](const bool) mutable { rp.deliver(true); }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); +} + +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>( + {std::make_shared>( + ShotBrowser::PLUGIN_UUID, + "ShotBrowser", + plugin_manager::PluginFlags::PF_DATA_SOURCE, + true, // resident + "Al Crate", + "DNEG Shotgrid & Ivy Integration Plugin")})); +} +} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_plugin.hpp b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_plugin.hpp new file mode 100644 index 000000000..f21ad9998 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/shotbrowser_plugin.hpp @@ -0,0 +1,416 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/plugin_manager/plugin_base.hpp" +#include "xstudio/utility/frame_rate.hpp" +#include "xstudio/utility/json_store.hpp" +#include "xstudio/utility/managed_dir.hpp" + +#include "query_engine.hpp" +#include "worker.hpp" + +namespace xstudio { +namespace shotbrowser { + + class ShotBrowser : public plugin::StandardPlugin { + public: + inline static const utility::Uuid PLUGIN_UUID = + utility::Uuid("8c0f06a8-cf43-44f3-94e2-0428bf7d150c"); + + ShotBrowser(caf::actor_config &cfg, const utility::JsonStore &init_settings); + virtual ~ShotBrowser() = default; + + QueryEngine &engine() { return engine_; } + + void update_preferences(const utility::JsonStore &js); + void on_exit() override; + + protected: + caf::message_handler message_handler_extensions() override; + + // void attribute_changed( + // const utility::Uuid &attribute_uuid, const int /*role*/ + // ) override; + + // void register_hotkeys() override; + // void hotkey_pressed(const utility::Uuid &uuid, const std::string &context) override; + // void hotkey_released(const utility::Uuid &uuid, const std::string &context) override; + + // void images_going_on_screen( + // const std::vector &images, + // const std::string viewport_name, + // const bool playhead_playing) override; + + // void menu_item_activated(const utility::Uuid &menu_item_uuid) override; + + shotgun_client::AuthenticateShotgun get_authentication() const; + + void set_authentication_method(const std::string &value); + void set_client_id(const std::string &value); + void set_client_secret(const std::string &value); + void set_username(const std::string &value); + void set_password(const std::string &value); + void set_session_token(const std::string &value); + void set_authenticated(const bool value); + void set_timeout(const int value); + + void bind_attribute_changed_callback( + std::function fn) { + attribute_changed_callback_ = [fn](auto &&PH1) { + return fn(std::forward(PH1)); + }; + } + + void attribute_changed(const utility::Uuid &attr_uuid, const int /*role*/) override; + + void call_attribute_changed(const utility::Uuid &attr_uuid) { + if (attribute_changed_callback_) + attribute_changed_callback_(attr_uuid); + } + + private: + void add_attributes(); + void attribute_changed(const utility::Uuid &attr_uuid); + + void update_playlist_versions( + caf::typed_response_promise rp, + const utility::Uuid &playlist_uuid, + const bool ordered = true, + const int playlist_id = 0, + const utility::Uuid ¬ify_uuid = utility::Uuid()); + void refresh_playlist_versions( + caf::typed_response_promise rp, + const utility::Uuid &playlist_uuid, + const bool match_order = false); + // void refresh_playlist_notes(caf::typed_response_promise rp, const + // utility::Uuid &playlist_uuid); + + void create_playlist( + caf::typed_response_promise rp, const utility::JsonStore &js); + + void create_tag( + caf::typed_response_promise rp, const std::string &value); + + void rename_tag( + caf::typed_response_promise rp, + const int tag_id, + const std::string &value); + + void add_entity_tag( + caf::typed_response_promise rp, + const std::string &entity, + const int entity_id, + const int tag_id); + + void remove_entity_tag( + caf::typed_response_promise rp, + const std::string &entity, + const int entity_id, + const int tag_id); + + void prepare_playlist_notes( + caf::typed_response_promise rp, + const utility::Uuid &playlist_uuid, + const utility::UuidVector &media_uuids = {}, + const bool notify_owner = false, + const std::vector notify_group_ids = {}, + const bool combine = false, + const bool add_time = false, + const bool add_playlist_name = false, + const bool add_type = false, + const bool anno_requires_note = true, + const bool skip_already_pubished = false, + const std::string &default_type = ""); + + void create_playlist_notes( + caf::typed_response_promise rp, + const utility::JsonStore ¬es, + const utility::Uuid &playlist_uuid); + void load_playlist( + caf::typed_response_promise rp, + const int playlist_id, + const caf::actor &session); + + // void update_playlist_notes(caf::typed_response_promise rp, const + // utility::Uuid &playlist_uuid, const utility::JsonStore &js); void + // add_media_to_playlist(caf::typed_response_promise> + // rp, + // const utility::JsonStore &data, + // const utility::Uuid &playlist_uuid, + // const utility::Uuid &before + // ); + + void reorder_playlist( + caf::typed_response_promise rp, + const caf::actor &playlist, + const utility::JsonStore &sg_playlist); + + void add_media_to_playlist( + caf::typed_response_promise> rp, + const utility::JsonStore &data, + const bool create_playlist, + utility::Uuid playlist_uuid, + caf::actor playlist, + const utility::Uuid &before, + const utility::FrameRate &media_rate); + void get_valid_media_count( + caf::typed_response_promise rp, const utility::Uuid &uuid); + void link_media( + caf::typed_response_promise rp, const utility::Uuid &uuid); + + void add_shotgrid_media( + caf::typed_response_promise rp, const utility::Uuid &uuid); + + void download_shotgrid_media( + caf::typed_response_promise rp, + const std::string &entity, + const int entity_id, + const std::string &entity_name, + const std::string &project_name, + const std::string &parent_name); + + void download_shotgrid_image( + caf::typed_response_promise rp, + const std::string &entity, + const int entity_id, + const std::string &entity_name, + const std::string &project_name); + + void get_data( + caf::typed_response_promise rp, + const std::string &type, + const int project_id); + + void + get_precache(caf::typed_response_promise rp, const int project_id); + + void get_data_department( + caf::typed_response_promise rp, const std::string &type); + + void get_data_project( + caf::typed_response_promise rp, + const std::string &type, + const std::string &user = ""); + + void get_data_location( + caf::typed_response_promise rp, const std::string &type); + + void get_data_review_location( + caf::typed_response_promise rp, const std::string &type); + + void get_data_playlist_type( + caf::typed_response_promise rp, const std::string &type); + + void get_data_shot_status( + caf::typed_response_promise rp, const std::string &type); + + void get_data_note_type( + caf::typed_response_promise rp, const std::string &type); + + void get_data_production_status( + caf::typed_response_promise rp, const std::string &type); + + void get_data_pipeline_status( + caf::typed_response_promise rp, + const std::string &type, + const std::string &entity, + const std::string &cache_name); + + void get_version_artist( + caf::typed_response_promise rp, const int version_id); + + void get_pipe_step(caf::typed_response_promise rp); + + void get_data_unit( + caf::typed_response_promise rp, + const std::string &type, + const int project_id); + + void get_data_asset( + caf::typed_response_promise rp, + const std::string &type, + const int project_id, + const int page = 1, + const utility::JsonStore &prev_data = utility::JsonStore(R"([])"_json)); + + void get_data_tree( + caf::typed_response_promise rp, + const std::string &type, + const int project_id); + + void get_data_stage( + caf::typed_response_promise rp, + const std::string &type, + const int project_id); + + void get_data_group( + caf::typed_response_promise rp, + const std::string &type, + const int project_id); + + void get_data_user( + caf::typed_response_promise rp, + const std::string &type, + const int project_id = -1, + const int page = 1, + const utility::JsonStore &prev_data = utility::JsonStore(R"([])"_json)); + + void get_data_shot( + caf::typed_response_promise rp, + const std::string &type, + const int project_id, + const int page = 1, + const utility::JsonStore &prev_data = utility::JsonStore(R"([])"_json)); + + void get_data_shot_for_sequence( + caf::typed_response_promise rp, + const std::string &type, + const int project_id, + const int page = 1, + const utility::JsonStore &prev_data = utility::JsonStore(R"([])"_json)); + + void get_data_sequence( + caf::typed_response_promise rp, + const std::string &type, + const int project_id, + const int page = 1, + const utility::JsonStore &prev_data = utility::JsonStore(R"([])"_json)); + + void get_data_episode( + caf::typed_response_promise rp, + const std::string &type, + const int project_id, + const int page = 1, + const utility::JsonStore &prev_data = utility::JsonStore(R"([])"_json)); + + void get_data_playlist( + caf::typed_response_promise rp, + const std::string &type, + const int project_id, + const int page = 1, + const utility::JsonStore &prev_data = utility::JsonStore(R"([])"_json)); + + void find_ivy_version( + caf::typed_response_promise rp, + const std::string &uuid, + const std::string &job); + void find_shot(caf::typed_response_promise rp, const int shot_id); + + void get_fields( + caf::typed_response_promise rp, + const int id, + const std::string &entity, + const std::vector &fields); + + std::shared_ptr get_next_build_task(bool &is_ivy_build_task); + void do_add_media_sources_from_shotgun(std::shared_ptr); + void do_add_media_sources_from_ivy(std::shared_ptr); + + void execute_query( + caf::typed_response_promise rp, + const utility::JsonStore &action); + + void put_action( + caf::typed_response_promise rp, + const utility::JsonStore &action); + + void use_action( + caf::typed_response_promise rp, + const utility::JsonStore &action); + + void use_action( + caf::typed_response_promise rp, + const utility::JsonStore &action, + const caf::actor &session); + + void use_action( + caf::typed_response_promise rp, + const caf::uri &uri, + const utility::FrameRate &media_rate, + const bool create_playlist); + + void get_action( + caf::typed_response_promise rp, + const utility::JsonStore &action); + + void post_action( + caf::typed_response_promise rp, + const utility::JsonStore &action); + + void execute_preset( + caf::typed_response_promise rp, + const std::vector &preset_paths, + const int project_id, + const utility::JsonStore &context, + const utility::JsonStore &metadata, + const utility::JsonStore &env, + const utility::JsonStore &custom_terms); + + std::vector + extend_fields(const std::string &entity, const std::vector &fields) const; + + + private: + module::StringChoiceAttribute *authentication_method_; + module::StringAttribute *client_id_; + module::StringAttribute *client_secret_; + module::StringAttribute *username_; + module::StringAttribute *password_; + module::StringAttribute *session_token_; + module::BooleanAttribute *authenticated_; + module::FloatAttribute *timeout_; + + bool disable_integration_{false}; + + // module::ActionAttribute *playlist_notes_action_; + // module::ActionAttribute *selected_notes_action_; + + // void setup_menus(); + + // Example attributes .. you might not need to use these + // module::StringChoiceAttribute *some_multichoice_attribute_{nullptr}; + // module::IntegerAttribute *some_integer_attribute_{nullptr}; + // module::BooleanAttribute *some_bool_attribute_{nullptr}; + // module::ColourAttribute *some_colour_attribute_{nullptr}; + + // utility::Uuid demo_hotkey_; + // utility::Uuid my_menu_item_; + + utility::Uuid session_id_; + std::function attribute_changed_callback_; + + QueryEngine engine_; + + caf::actor shotgun_; + caf::actor pool_; + + utility::Uuid history_uuid_; + caf::actor history_; + + caf::actor event_group_; + + size_t changed_hash_{0}; + caf::actor_addr secret_source_; + std::vector> waiting_; + utility::Uuid uuid_ = {utility::Uuid::generate()}; + std::map category_colours_; + + std::deque> build_playlist_media_tasks_; + std::deque> extend_media_with_ivy_tasks_; + int build_tasks_in_flight_ = {0}; + int worker_count_ = {8}; + + std::map shot_cache_; + + utility::ManagedDir download_cache_; + + bool pending_preference_update_ = {false}; + + std::vector version_fields_; + std::vector shot_fields_; + std::vector note_fields_; + std::vector playlist_fields_; + }; + +} // namespace shotbrowser +} // namespace xstudio \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotbrowser/src/shotgrid_provider_ui.hpp b/src/plugin/data_source/dneg/shotbrowser/src/shotgrid_provider_ui.hpp new file mode 100644 index 000000000..67391bff8 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/shotgrid_provider_ui.hpp @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include "xstudio/atoms.hpp" +#include "xstudio/thumbnail/thumbnail.hpp" +#include "xstudio/ui/qml/helper_ui.hpp" +#include "xstudio/ui/qml/job_control_ui.hpp" +#include "xstudio/utility/logging.hpp" +#include "definitions.hpp" + +CAF_PUSH_WARNINGS +#include +#include +#include +#include +#include +#include +#include +#include +#include +CAF_POP_WARNINGS + +using namespace xstudio::thumbnail; +using namespace xstudio::ui::qml; +using namespace xstudio::utility; +using namespace xstudio; + +// QFututre futureValue = TaskExecutor::run(new SomeControllableTask(inputForThatTask)); +// Then she may cancel it by calling futureValue.cancel(), bearing in mind that cancellation is +// graceful and not immediate. + +class ShotgridThumbnailReader : public ControllableJob> { + public: + ShotgridThumbnailReader(const QString id, const QSize requestedSize) + : ControllableJob(), id_(std::move(id)), requestedSize_(std::move(requestedSize)) {} + + std::pair run(JobControl &cjc) override { + + + int width = 100; + int height = 100; + auto actual_width = requestedSize_.width() > 0 ? requestedSize_.width() : width; + auto actual_height = requestedSize_.height() > 0 ? requestedSize_.height() : height; + std::string error; + + try { + + if (not cjc.shouldRun()) + throw std::runtime_error("Cancelled"); + + caf::actor_system &system_ = CafSystemObject::get_actor_system(); + auto shotgrid = system_.registry().get(shotbrowser_datasource_registry); + auto thumbnail_cache = + system_.registry().get(thumbnail_manager_registry); + + if (not shotgrid) + throw std::runtime_error("ShotGrid not available"); + + scoped_actor sys{system_}; + + auto key = StdFromQString(id_); + ThumbnailBufferPtr tbp; + + if (thumbnail_cache) { + try { + tbp = request_receive( + *sys, + thumbnail_cache, + media_reader::get_thumbnail_atom_v, + key, + static_cast(std::max(requestedSize_.width(), 128))); + } catch (...) { + } + } + + if (not cjc.shouldRun()) + throw std::runtime_error("Cancelled"); + + if (not tbp or tbp->empty()) { + auto mode = StdFromQString(id_.section('/', 0, 0)); + auto entity = StdFromQString(id_.section('/', 1, 1)); + auto id = std::atoi(StdFromQString(id_.section('/', 2, 2)).c_str()); + + tbp = request_receive( + *sys, + shotgrid, + shotgun_client::shotgun_image_atom_v, + entity, + id, + (mode == "thumbnail" ? true : false), + true); + if (thumbnail_cache) + anon_mail( + media_cache::store_atom_v, + key, + static_cast(std::max(requestedSize_.width(), 128)), + tbp) + .send(thumbnail_cache); + } + + if (not cjc.shouldRun()) + throw std::runtime_error("Cancelled"); + + // set size based on + actual_width = requestedSize_.width() > 0 ? requestedSize_.width() : tbp->width(); + actual_height = + requestedSize_.height() > 0 ? requestedSize_.height() : tbp->height(); + + return std::make_pair( + QImage( + (uchar *)&(tbp->data()[0]), + tbp->width(), + tbp->height(), + 3 * tbp->width(), + QImage::Format_RGB888) + .scaled(actual_width, actual_height, Qt::KeepAspectRatio), + QString()); + } catch (const std::exception &err) { + spdlog::debug("{} {} {}", __PRETTY_FUNCTION__, StdFromQString(id_), err.what()); + if (cjc.shouldRun()) + error = err.what(); + } + + return std::make_pair(QImage(), QStringFromStd(error)); + } + + private: + const QString id_; + const QSize requestedSize_; +}; + + +class ShotgridResponse : public QQuickImageResponse { + public: + ShotgridResponse( + const QString &id, + const QSize &requestedSize, + QThreadPool *pool, + QMap &bad_thumbs) + : id_(id), bad_thumbs_(bad_thumbs) { + // spdlog::warn("{}", StdFromQString(id)); + if (bad_thumbs_.contains(id_) and + bad_thumbs_[id_].secsTo(QDateTime::currentDateTime()) < 60 * 20) { + error_ = "Thumbnail does not exist."; + emit finished(); + } else { + + // create a future.. + connect( + &watcher_, + &QFutureWatcher>::finished, + this, + &ShotgridResponse::handleFinished); + connect( + &watcher_, + &QFutureWatcher>::canceled, + this, + &ShotgridResponse::handleCanceled); + + // Start the computation. + QFuture> future = + JobExecutor::run(new ShotgridThumbnailReader(id, requestedSize), pool); + + watcher_.setFuture(future); + } + } + [[nodiscard]] QString errorString() const override { return error_; } + + void handleFinished() { + if (watcher_.future().resultCount()) { + // spdlog::warn("finished {}", StdFromQString(id_)); + auto [i, e] = watcher_.result(); + + if (not e.isEmpty()) { + error_ = "Thumbnail does not exist."; + bad_thumbs_.insert(id_, QDateTime::currentDateTime()); + } else { + bad_thumbs_.remove(id_); + m_image = i; + } + emit finished(); + } else { + // spdlog::warn("finished no result CANCELED {}", StdFromQString(id_)); + emit finished(); + } + } + + void handleCanceled() { + // spdlog::warn("canceled {}", StdFromQString(id_)); + emit finished(); + } + + void cancel() override { + // spdlog::warn("canceling {}", StdFromQString(id_)); + watcher_.cancel(); + } + + void handleDone(QImage image) { + bad_thumbs_.remove(id_); + m_image = image; + emit finished(); + } + + void handleFailed(QString error) { + error_ = "Thumbnail does not exist."; + emit finished(); + bad_thumbs_.insert(id_, QDateTime::currentDateTime()); + } + + [[nodiscard]] QQuickTextureFactory *textureFactory() const override { + return QQuickTextureFactory::textureFactoryForImage(m_image); + } + + QImage m_image; + QString error_; + + QString id_; + QMap &bad_thumbs_; + QFutureWatcher> watcher_; +}; + +class ShotgridProvider : public QQuickAsyncImageProvider { + public: + QQuickImageResponse * + requestImageResponse(const QString &id, const QSize &requestedSize) override { + auto response = new ShotgridResponse(id, requestedSize, &pool, bad_thumbs_); + return response; + } + + private: + QThreadPool pool; + QMap bad_thumbs_; +}; diff --git a/src/plugin/data_source/dneg/shotbrowser/src/worker.cpp b/src/plugin/data_source/dneg/shotbrowser/src/worker.cpp new file mode 100644 index 000000000..68012e475 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/worker.cpp @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "worker.hpp" +#include "definitions.hpp" + +#include "xstudio/atoms.hpp" +#include "xstudio/media/media_actor.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/json_store.hpp" +#include "xstudio/utility/uuid.hpp" + +using namespace xstudio; +using namespace xstudio::utility; + +void MediaWorker::add_media_step_1( + caf::typed_response_promise rp, + caf::actor media, + const JsonStore &jsn, + const FrameRate &media_rate) { + mail(media::add_media_source_atom_v, jsn, media_rate, true) + .request(actor_cast(this), infinite) + .then( + [=](const UuidActor &movie_source) mutable { + add_media_step_2(rp, media, jsn, media_rate, movie_source); + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); +} + +void MediaWorker::add_media_step_2( + caf::typed_response_promise rp, + caf::actor media, + const JsonStore &jsn, + const FrameRate &media_rate, + const UuidActor &movie_source) { + // now get image.. + mail(media::add_media_source_atom_v, jsn, media_rate) + .request(actor_cast(this), infinite) + .then( + [=](const UuidActor &image_source) mutable { + // check to see if what we've got.. + // failed... + if (movie_source.uuid().is_null() and image_source.uuid().is_null()) { + // spdlog::warn("{} No valid sources", __PRETTY_FUNCTION__); + add_media_step_3(rp, media, jsn, UuidActorVector()); + } else { + try { + UuidActorVector srcs; + + if (not movie_source.uuid().is_null()) + srcs.push_back(movie_source); + if (not image_source.uuid().is_null()) + srcs.push_back(image_source); + + + add_media_step_3(rp, media, jsn, srcs); + + } catch (const std::exception &err) { + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), jsn.dump(2)); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); +} + +void MediaWorker::add_media_step_3( + caf::typed_response_promise rp, + caf::actor media, + const JsonStore &jsn, + const UuidActorVector &srcs) { + mail(media::add_media_source_atom_v, srcs) + .request(media, infinite) + .then( + [=](const bool) mutable { + rp.deliver(true); + // push metadata to media actor. + anon_mail( + json_store::set_json_atom_v, + utility::Uuid(), + jsn, + ShotgunMetadataPath + "/version") + .send(media); + + anon_mail( + json_store::set_json_atom_v, + utility::Uuid(), + JsonStore( + R"({"icon": "qrc:/shotbrowser_icons/shot_grid.svg", "tooltip": "ShotGrid Version"})"_json), + "/ui/decorators/shotgrid") + .send(media); + + // dispatch delayed shot data. + try { + auto shotreq = JsonStore(GetShotFromId); + shotreq["shot_id"] = + jsn.at("relationships").at("entity").at("data").value("id", 0); + + mail(data_source::get_data_atom_v, shotreq) + .request(caf::actor_cast(data_source_), infinite) + .then( + [=](const JsonStore &jsn) mutable { + try { + if (jsn.count("data")) + anon_mail( + json_store::set_json_atom_v, + utility::Uuid(), + JsonStore(jsn.at("data")), + ShotgunMetadataPath + "/shot") + .send(media); + } catch (const std::exception &err) { + spdlog::warn("A {} {}", __PRETTY_FUNCTION__, err.what()); + } + }, + [=](const error &err) mutable { + spdlog::warn("B {} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } catch (const std::exception &err) { + spdlog::warn("C {} {}", __PRETTY_FUNCTION__, err.what()); + } + }, + [=](error &err) mutable { + spdlog::warn("D {} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(err); + }); +} + + +MediaWorker::MediaWorker(caf::actor_config &cfg, const caf::actor_addr source) + : data_source_(std::move(source)), caf::event_based_actor(cfg) { + + // for each input we spawn one media item with upto two media sources. + + + behavior_.assign( + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + // movie + [=](media::add_media_source_atom, + const JsonStore &jsn, + const FrameRate &media_rate, + const bool /*movie*/) -> result { + auto rp = make_response_promise(); + try { + if (not jsn.at("attributes").at("sg_path_to_movie").is_null()) { + // spdlog::info("{}", i["attributes"]["sg_path_to_movie"]); + // prescan movie for duration.. + // it may contain slate, which needs trimming.. + // SLOW do we want to be offsetting the movie ? + // if we keep this code is needs threading.. + auto uri = posix_path_to_uri(jsn["attributes"]["sg_path_to_movie"]); + const auto source_uuid = Uuid::generate(); + auto source = spawn( + "SG Movie", uri, media_rate, source_uuid); + + mail(media::acquire_media_detail_atom_v, media_rate) + .request(source, infinite) + .then( + [=](bool) mutable { rp.deliver(UuidActor(source_uuid, source)); }, + [=](error &err) mutable { + // even though there is an error, we want the broken media + // source added so the user can see it in the UI (and its error + // state) + rp.deliver(UuidActor(source_uuid, source)); + }); + + } else { + rp.deliver(UuidActor()); + } + } catch (const std::exception &err) { + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), jsn.dump(2)); + rp.deliver(UuidActor()); + } + return rp; + }, + + // frames + [=](media::add_media_source_atom, + const JsonStore &jsn, + const FrameRate &media_rate) -> result { + auto rp = make_response_promise(); + try { + if (not jsn.at("attributes").at("sg_path_to_frames").is_null()) { + FrameList frame_list; + caf::uri uri; + + if (jsn.at("attributes").at("frame_range").is_null()) { + // no frame range specified.. + // try and aquire from filesystem.. + uri = parse_cli_posix_path( + jsn.at("attributes").at("sg_path_to_frames"), frame_list, true); + } else { + frame_list = FrameList( + jsn.at("attributes").at("frame_range").template get()); + uri = parse_cli_posix_path( + jsn.at("attributes").at("sg_path_to_frames"), frame_list, false); + } + + const auto source_uuid = Uuid::generate(); + auto source = + frame_list.empty() + ? spawn( + "SG Frames", uri, media_rate, source_uuid) + : spawn( + "SG Frames", uri, frame_list, media_rate, source_uuid); + + mail(media::acquire_media_detail_atom_v, media_rate) + .request(source, infinite) + .then( + [=](bool) mutable { rp.deliver(UuidActor(source_uuid, source)); }, + [=](error &err) mutable { + // even though there is an error, we want the broken media + // source added so the user can see it in the UI (and its error + // state) + rp.deliver(UuidActor(source_uuid, source)); + }); + } else { + rp.deliver(UuidActor()); + } + } catch (const std::exception &err) { + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), jsn.dump(2)); + rp.deliver(UuidActor()); + } + + return rp; + }, + + [=](playlist::add_media_atom, + caf::actor media, + JsonStore jsn, + const FrameRate &media_rate) -> result { + auto rp = make_response_promise(); + + try { + // do stupid stuff, because data integrity is for losers. + // if we've got a movie in the sg_frames property, swap them over. + if (jsn.at("attributes").at("sg_path_to_movie").is_null() and + not jsn.at("attributes").at("sg_path_to_frames").is_null() and + jsn.at("attributes") + .at("sg_path_to_frames") + .template get() + .find_first_of('#') == std::string::npos) { + // movie in image sequence.. + jsn["attributes"]["sg_path_to_movie"] = + jsn.at("attributes").at("sg_path_to_frames"); + jsn["attributes"]["sg_path_to_frames"] = nullptr; + } + + // request movie .. THESE MUST NOT RETURN error on fail. + add_media_step_1(rp, media, jsn, media_rate); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } + return rp; + }); +} diff --git a/src/plugin/data_source/dneg/shotbrowser/src/worker.hpp b/src/plugin/data_source/dneg/shotbrowser/src/worker.hpp new file mode 100644 index 000000000..a3ad15049 --- /dev/null +++ b/src/plugin/data_source/dneg/shotbrowser/src/worker.hpp @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include "xstudio/utility/frame_rate.hpp" +#include "xstudio/utility/json_store.hpp" +#include "xstudio/utility/uuid.hpp" + +using namespace xstudio; + +class BuildPlaylistMediaJob { + + public: + BuildPlaylistMediaJob( + caf::actor playlist_actor, + const utility::Uuid &media_uuid, + const std::string media_name, + utility::JsonStore sg_data, + utility::FrameRate media_rate, + std::vector preferred_visual_sources, + std::vector preferred_audio_sources, + std::shared_ptr ordererd_uuids, + utility::Uuid before, + std::string flag_colour, + std::string flag_text, + caf::typed_response_promise response_promise, + std::shared_ptr result, + std::shared_ptr result_count) + : playlist_actor_(std::move(playlist_actor)), + media_uuid_(media_uuid), + media_name_(media_name), + sg_data_(sg_data), + media_rate_(media_rate), + preferred_visual_sources_(std::move(preferred_visual_sources)), + preferred_audio_sources_(std::move(preferred_audio_sources)), + ordererd_uuids_(std::move(ordererd_uuids)), + before_(std::move(before)), + flag_colour_(std::move(flag_colour)), + flag_text_(std::move(flag_text)), + response_promise_(std::move(response_promise)), + result_(std::move(result)), + result_count_(result_count) { + // increment a shared counter - the counter is shared between + // all the indiviaual Media creation jobs in a single build playlist + // task + (*result_count)++; + } + + BuildPlaylistMediaJob(const BuildPlaylistMediaJob &o) = default; + BuildPlaylistMediaJob() = default; + + ~BuildPlaylistMediaJob() { + // this gets destroyed when the job is done with. + if (media_actor_) { + result_->push_back(utility::UuidActor(media_uuid_, media_actor_)); + } + // decrement the counter + (*result_count_)--; + + if (!(*result_count_)) { + // counter has dropped to zero, all jobs within a single build playlist + // tas are done. Our 'result' member here is in the order that the + // media items were created (asynchronously), rather than the order + // of the final playlist ... so we need to reorder our 'result' to + // match the ordering in the playlist + utility::UuidActorVector reordered; + reordered.reserve(result_->size()); + for (const auto &uuid : (*ordererd_uuids_)) { + for (auto uai = result_->begin(); uai != result_->end(); uai++) { + if ((*uai).uuid() == uuid) { + reordered.push_back(*uai); + result_->erase(uai); + break; + } + } + } + response_promise_.deliver(reordered); + } + } + + caf::actor playlist_actor_; + utility::Uuid media_uuid_; + std::string media_name_; + utility::JsonStore sg_data_; + utility::FrameRate media_rate_; + std::vector preferred_visual_sources_; + std::vector preferred_audio_sources_; + std::shared_ptr ordererd_uuids_; + utility::Uuid before_; + std::string flag_colour_; + std::string flag_text_; + caf::typed_response_promise response_promise_; + std::shared_ptr result_; + std::shared_ptr result_count_; + caf::actor media_actor_; +}; + +class MediaWorker : public caf::event_based_actor { + public: + MediaWorker(caf::actor_config &cfg, const caf::actor_addr source); + ~MediaWorker() override = default; + + const char *name() const override { return NAME.c_str(); } + + private: + inline static const std::string NAME = "MediaWorker"; + caf::behavior make_behavior() override { return behavior_; } + + void add_media_step_1( + caf::typed_response_promise rp, + caf::actor media, + const utility::JsonStore &jsn, + const utility::FrameRate &media_rate); + void add_media_step_2( + caf::typed_response_promise rp, + caf::actor media, + const utility::JsonStore &jsn, + const utility::FrameRate &media_rate, + const utility::UuidActor &movie_source); + void add_media_step_3( + caf::typed_response_promise rp, + caf::actor media, + const utility::JsonStore &jsn, + const utility::UuidActorVector &srcs); + + private: + caf::behavior behavior_; + caf::actor_addr data_source_; +}; \ No newline at end of file diff --git a/src/plugin/data_source/dneg/ivy/src/qml/CMakeLists.txt b/src/plugin/data_source/dneg/shotbrowser/test/CMakeLists.txt similarity index 100% rename from src/plugin/data_source/dneg/ivy/src/qml/CMakeLists.txt rename to src/plugin/data_source/dneg/shotbrowser/test/CMakeLists.txt diff --git a/src/plugin/data_source/dneg/shotgun/src/CMakeLists.txt b/src/plugin/data_source/dneg/shotgun/src/CMakeLists.txt deleted file mode 100644 index 283af285d..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -SET(LINK_DEPS - xstudio::data_source - xstudio::shotgun_client - xstudio::playlist - xstudio::utility - xstudio::event - xstudio::module - xstudio::media -) - -create_plugin_with_alias(data_source_shotgun xstudio::data_source::shotgun 0.1.0 "${LINK_DEPS}") - -add_subdirectory(qml) diff --git a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.cpp b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.cpp deleted file mode 100644 index 3896e9335..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.cpp +++ /dev/null @@ -1,3416 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include "data_source_shotgun.tcc" -#include -#include - -#include "data_source_shotgun.hpp" -#include "xstudio/atoms.hpp" -#include "xstudio/bookmark/bookmark.hpp" -#include "xstudio/event/event.hpp" -#include "xstudio/global_store/global_store.hpp" -#include "xstudio/media/media_actor.hpp" -#include "xstudio/playlist/playlist_actor.hpp" -#include "xstudio/shotgun_client/shotgun_client.hpp" -#include "xstudio/shotgun_client/shotgun_client_actor.hpp" -#include "xstudio/tag/tag.hpp" -#include "xstudio/thumbnail/thumbnail.hpp" -#include "xstudio/utility/chrono.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/uuid.hpp" - -using namespace xstudio; -using namespace xstudio::shotgun_client; -using namespace xstudio::utility; -using namespace xstudio::global_store; -using namespace std::chrono_literals; - -/*CAF_BEGIN_TYPE_ID_BLOCK(shotgun, xstudio::shotgun_client::shotgun_client_error) -CAF_ADD_ATOM(shotgun, xstudio::shotgun_client, test_atom) -CAF_END_TYPE_ID_BLOCK(shotgun)*/ - -// Datasource should support a common subset of operations that apply to multiple datasources. -// not idea what they are though. -// get and put should try and map from this to the relevant sources. - -// shotgun piggy backs on the shotgun client actor, so most of the work is done in the actor -// class. because shotgun is very flexible, it's hard to write helpers, as entities/properties -// are entirely configurable. but we also don't want to put all the logic into the frontend. as -// python module may want access to this logic. - -// This value helps tune the rate that jobs to build media are processed, if it -// is zero xstudio tends to get overwhelmed when building large playlists, increasing -// the value means xstudio stays interactive at the cost of slowing the overall -#define JOB_DISPATCH_DELAY std::chrono::milliseconds(10) - -class BuildPlaylistMediaJob { - - public: - BuildPlaylistMediaJob( - caf::actor playlist_actor, - const utility::Uuid &media_uuid, - const std::string media_name, - utility::JsonStore sg_data, - utility::FrameRate media_rate, - std::string preferred_visual_source, - std::string preferred_audio_source, - std::shared_ptr event, - std::shared_ptr ordererd_uuids, - utility::Uuid before, - std::string flag_colour, - std::string flag_text, - caf::typed_response_promise response_promise, - std::shared_ptr result, - std::shared_ptr result_count) - : playlist_actor_(std::move(playlist_actor)), - media_uuid_(media_uuid), - media_name_(media_name), - sg_data_(sg_data), - media_rate_(media_rate), - preferred_visual_source_(std::move(preferred_visual_source)), - preferred_audio_source_(std::move(preferred_audio_source)), - event_msg_(std::move(event)), - ordererd_uuids_(std::move(ordererd_uuids)), - before_(std::move(before)), - flag_colour_(std::move(flag_colour)), - flag_text_(std::move(flag_text)), - response_promise_(std::move(response_promise)), - result_(std::move(result)), - result_count_(result_count) { - // increment a shared counter - the counter is shared between - // all the indiviaual Media creation jobs in a single build playlist - // task - (*result_count)++; - } - - BuildPlaylistMediaJob(const BuildPlaylistMediaJob &o) = default; - BuildPlaylistMediaJob() = default; - - ~BuildPlaylistMediaJob() { - // this gets destroyed when the job is done with. - if (media_actor_) { - result_->push_back(UuidActor(media_uuid_, media_actor_)); - } - // decrement the counter - (*result_count_)--; - - if (!(*result_count_)) { - // counter has dropped to zero, all jobs within a single build playlist - // tas are done. Our 'result' member here is in the order that the - // media items were created (asynchronously), rather than the order - // of the final playlist ... so we need to reorder our 'result' to - // match the ordering in the playlist - UuidActorVector reordered; - reordered.reserve(result_->size()); - for (const auto &uuid : (*ordererd_uuids_)) { - for (auto uai = result_->begin(); uai != result_->end(); uai++) { - if ((*uai).uuid() == uuid) { - reordered.push_back(*uai); - result_->erase(uai); - break; - } - } - } - response_promise_.deliver(reordered); - } - } - - caf::actor playlist_actor_; - utility::Uuid media_uuid_; - std::string media_name_; - utility::JsonStore sg_data_; - utility::FrameRate media_rate_; - std::string preferred_visual_source_; - std::string preferred_audio_source_; - std::shared_ptr event_msg_; - std::shared_ptr ordererd_uuids_; - utility::Uuid before_; - std::string flag_colour_; - std::string flag_text_; - caf::typed_response_promise response_promise_; - std::shared_ptr result_; - std::shared_ptr result_count_; - caf::actor media_actor_; -}; - -class ShotgunMediaWorker : public caf::event_based_actor { - public: - ShotgunMediaWorker(caf::actor_config &cfg, const caf::actor_addr source); - ~ShotgunMediaWorker() override = default; - - const char *name() const override { return NAME.c_str(); } - - private: - inline static const std::string NAME = "ShotgunMediaWorker"; - caf::behavior make_behavior() override { return behavior_; } - - void add_media_step_1( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const FrameRate &media_rate); - void add_media_step_2( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const FrameRate &media_rate, - const UuidActor &movie_source); - void add_media_step_3( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const UuidActorVector &srcs); - - private: - caf::behavior behavior_; - caf::actor_addr data_source_; -}; - -void ShotgunMediaWorker::add_media_step_1( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const FrameRate &media_rate) { - request( - actor_cast(this), - infinite, - media::add_media_source_atom_v, - jsn, - media_rate, - true) - .then( - [=](const UuidActor &movie_source) mutable { - add_media_step_2(rp, media, jsn, media_rate, movie_source); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); -} - -void ShotgunMediaWorker::add_media_step_2( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const FrameRate &media_rate, - const UuidActor &movie_source) { - // now get image.. - request( - actor_cast(this), infinite, media::add_media_source_atom_v, jsn, media_rate) - .then( - [=](const UuidActor &image_source) mutable { - // check to see if what we've got.. - // failed... - if (movie_source.uuid().is_null() and image_source.uuid().is_null()) { - spdlog::warn("{} No valid sources {}", __PRETTY_FUNCTION__, jsn.dump(2)); - rp.deliver(false); - } else { - try { - UuidActorVector srcs; - - if (not movie_source.uuid().is_null()) - srcs.push_back(movie_source); - if (not image_source.uuid().is_null()) - srcs.push_back(image_source); - - - add_media_step_3(rp, media, jsn, srcs); - - } catch (const std::exception &err) { - spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), jsn.dump(2)); - rp.deliver(make_error(xstudio_error::error, err.what())); - } - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); -} - -void ShotgunMediaWorker::add_media_step_3( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const UuidActorVector &srcs) { - request(media, infinite, media::add_media_source_atom_v, srcs) - .then( - [=](const bool) mutable { - rp.deliver(true); - // push metadata to media actor. - anon_send( - media, - json_store::set_json_atom_v, - utility::Uuid(), - jsn, - ShotgunMetadataPath + "/version"); - - // dispatch delayed shot data. - try { - auto shotreq = JsonStore(GetShotFromIdJSON); - shotreq["shot_id"] = - jsn.at("relationships").at("entity").at("data").value("id", 0); - - request( - caf::actor_cast(data_source_), - infinite, - get_data_atom_v, - shotreq) - .then( - [=](const JsonStore &jsn) mutable { - try { - if (jsn.count("data")) - anon_send( - media, - json_store::set_json_atom_v, - utility::Uuid(), - JsonStore(jsn.at("data")), - ShotgunMetadataPath + "/shot"); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - }, - [=](const error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - }, - [=](error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); -} - - -ShotgunMediaWorker::ShotgunMediaWorker(caf::actor_config &cfg, const caf::actor_addr source) - : data_source_(std::move(source)), caf::event_based_actor(cfg) { - - // for each input we spawn one media item with upto two media sources. - - - behavior_.assign( - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - // movie - [=](media::add_media_source_atom, - const JsonStore &jsn, - const FrameRate &media_rate, - const bool /*movie*/) -> result { - auto rp = make_response_promise(); - try { - if (not jsn.at("attributes").at("sg_path_to_movie").is_null()) { - // spdlog::info("{}", i["attributes"]["sg_path_to_movie"]); - // prescan movie for duration.. - // it may contain slate, which needs trimming.. - // SLOW do we want to be offsetting the movie ? - // if we keep this code is needs threading.. - auto uri = posix_path_to_uri(jsn["attributes"]["sg_path_to_movie"]); - const auto source_uuid = Uuid::generate(); - auto source = spawn( - "SG Movie", uri, media_rate, source_uuid); - - request(source, infinite, media::acquire_media_detail_atom_v, media_rate) - .then( - [=](bool) mutable { rp.deliver(UuidActor(source_uuid, source)); }, - [=](error &err) mutable { - // even though there is an error, we want the broken media - // source added so the user can see it in the UI (and its error - // state) - rp.deliver(UuidActor(source_uuid, source)); - }); - - } else { - rp.deliver(UuidActor()); - } - } catch (const std::exception &err) { - spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), jsn.dump(2)); - rp.deliver(UuidActor()); - } - return rp; - }, - - // frames - [=](media::add_media_source_atom, - const JsonStore &jsn, - const FrameRate &media_rate) -> result { - auto rp = make_response_promise(); - try { - if (not jsn.at("attributes").at("sg_path_to_frames").is_null()) { - FrameList frame_list; - caf::uri uri; - - if (jsn.at("attributes").at("frame_range").is_null()) { - // no frame range specified.. - // try and aquire from filesystem.. - uri = parse_cli_posix_path( - jsn.at("attributes").at("sg_path_to_frames"), frame_list, true); - } else { - frame_list = FrameList( - jsn.at("attributes").at("frame_range").template get()); - uri = parse_cli_posix_path( - jsn.at("attributes").at("sg_path_to_frames"), frame_list, false); - } - - const auto source_uuid = Uuid::generate(); - auto source = - frame_list.empty() - ? spawn( - "SG Frames", uri, media_rate, source_uuid) - : spawn( - "SG Frames", uri, frame_list, media_rate, source_uuid); - - request(source, infinite, media::acquire_media_detail_atom_v, media_rate) - .then( - [=](bool) mutable { rp.deliver(UuidActor(source_uuid, source)); }, - [=](error &err) mutable { - // even though there is an error, we want the broken media - // source added so the user can see it in the UI (and its error - // state) - rp.deliver(UuidActor(source_uuid, source)); - }); - } else { - rp.deliver(UuidActor()); - } - } catch (const std::exception &err) { - spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), jsn.dump(2)); - rp.deliver(UuidActor()); - } - - return rp; - }, - - [=](playlist::add_media_atom, - caf::actor media, - JsonStore jsn, - const FrameRate &media_rate) -> result { - auto rp = make_response_promise(); - - try { - // do stupid stuff, because data integrity is for losers. - // if we've got a movie in the sg_frames property, swap them over. - if (jsn.at("attributes").at("sg_path_to_movie").is_null() and - not jsn.at("attributes").at("sg_path_to_frames").is_null() and - jsn.at("attributes") - .at("sg_path_to_frames") - .template get() - .find_first_of('#') == std::string::npos) { - // movie in image sequence.. - jsn["attributes"]["sg_path_to_movie"] = - jsn.at("attributes").at("sg_path_to_frames"); - jsn["attributes"]["sg_path_to_frames"] = nullptr; - } - - // request movie .. THESE MUST NOT RETURN error on fail. - add_media_step_1(rp, media, jsn, media_rate); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } - return rp; - }); -} - - -void ShotgunDataSource::set_authentication_method(const std::string &value) { - if (authentication_method_->value() != value) - authentication_method_->set_value(value); -} -void ShotgunDataSource::set_client_id(const std::string &value) { - if (client_id_->value() != value) - client_id_->set_value(value); -} -void ShotgunDataSource::set_client_secret(const std::string &value) { - if (client_secret_->value() != value) - client_secret_->set_value(value); -} -void ShotgunDataSource::set_username(const std::string &value) { - if (username_->value() != value) - username_->set_value(value); -} -void ShotgunDataSource::set_password(const std::string &value) { - if (password_->value() != value) - password_->set_value(value); -} -void ShotgunDataSource::set_session_token(const std::string &value) { - if (session_token_->value() != value) - session_token_->set_value(value); -} -void ShotgunDataSource::set_authenticated(const bool value) { - if (authenticated_->value() != value) - authenticated_->set_value(value); -} -void ShotgunDataSource::set_timeout(const int value) { - if (timeout_->value() != value) - timeout_->set_value(value); -} - -shotgun_client::AuthenticateShotgun ShotgunDataSource::get_authentication() const { - AuthenticateShotgun auth; - - auth.set_session_uuid(to_string(session_id_)); - - auth.set_authentication_method(authentication_method_->value()); - switch (*(auth.authentication_method())) { - case AM_SCRIPT: - auth.set_client_id(client_id_->value()); - auth.set_client_secret(client_secret_->value()); - break; - case AM_SESSION: - auth.set_session_token(session_token_->value()); - break; - case AM_LOGIN: - auth.set_username(expand_envvars(username_->value())); - auth.set_password(password_->value()); - break; - case AM_UNDEFINED: - default: - break; - } - - return auth; -} - -void ShotgunDataSource::add_attributes() { - - std::vector auth_method_names = { - "client_credentials", "password", "session_token"}; - - module::QmlCodeAttribute *button = add_qml_code_attribute( - "MyCode", - R"( -import Shotgun 1.0 -ShotgunButton {} -)"); - - button->set_role_data(module::Attribute::ToolbarPosition, 1010.0); - button->expose_in_ui_attrs_group("media_tools_buttons"); - - - authentication_method_ = add_string_choice_attribute( - "authentication_method", - "authentication_method", - "password", - auth_method_names, - auth_method_names); - - playlist_notes_action_ = - add_action_attribute("playlist_notes_to_shotgun", "playlist_notes_to_shotgun"); - selected_notes_action_ = - add_action_attribute("selected_notes_to_shotgun", "selected_notes_to_shotgun"); - - client_id_ = add_string_attribute("client_id", "client_id", ""); - client_secret_ = add_string_attribute("client_secret", "client_secret", ""); - username_ = add_string_attribute("username", "username", ""); - password_ = add_string_attribute("password", "password", ""); - session_token_ = add_string_attribute("session_token", "session_token", ""); - - authenticated_ = add_boolean_attribute("authenticated", "authenticated", false); - - // should be int.. - timeout_ = add_float_attribute("timeout", "timeout", 120.0, 10.0, 600.0, 1.0, 0); - - - // by setting static UUIDs on these module we only create them once in the UI - playlist_notes_action_->set_role_data( - module::Attribute::UuidRole, "92c780be-d0bc-462a-b09f-643e8986e2a1"); - playlist_notes_action_->set_role_data( - module::Attribute::Title, "Publish Playlist Notes..."); - playlist_notes_action_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_menu"}); - playlist_notes_action_->set_role_data( - module::Attribute::MenuPaths, std::vector({"publish_menu|Shotgun"})); - - selected_notes_action_->set_role_data( - module::Attribute::UuidRole, "7583a4d0-35d8-4f00-bc32-ae8c2bddc30a"); - selected_notes_action_->set_role_data( - module::Attribute::Title, "Publish Selected Notes..."); - selected_notes_action_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_menu"}); - selected_notes_action_->set_role_data( - module::Attribute::MenuPaths, std::vector({"publish_menu|Shotgun"})); - - authentication_method_->set_role_data( - module::Attribute::UuidRole, "ea7c47b8-a851-4f44-b9f1-3f5b38c11d96"); - client_id_->set_role_data( - module::Attribute::UuidRole, "31925e29-674f-4f03-a861-502a2bc92f78"); - client_secret_->set_role_data( - module::Attribute::UuidRole, "05d18793-ef4c-4753-8b55-1d98788eb727"); - username_->set_role_data( - module::Attribute::UuidRole, "a012c508-a8a7-4438-97ff-05fc707331d0"); - password_->set_role_data( - module::Attribute::UuidRole, "55982b32-3273-4f1c-8164-251d8af83365"); - session_token_->set_role_data( - module::Attribute::UuidRole, "d6fac6a6-a6c9-4ac3-b961-499d9862a886"); - authenticated_->set_role_data( - module::Attribute::UuidRole, "ce708287-222f-46b6-820c-f6dfda592ba9"); - timeout_->set_role_data( - module::Attribute::UuidRole, "9947a178-b5bb-4370-905e-c6687b2d7f41"); - - authentication_method_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - client_id_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - client_secret_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - username_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - password_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - session_token_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - authenticated_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - timeout_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - - authentication_method_->set_role_data( - module::Attribute::ToolTip, "Shotgun authentication method."); - - client_id_->set_role_data(module::Attribute::ToolTip, "Shotgun script key."); - client_secret_->set_role_data(module::Attribute::ToolTip, "Shotgun script secret."); - username_->set_role_data(module::Attribute::ToolTip, "Shotgun username."); - password_->set_role_data(module::Attribute::ToolTip, "Shotgun password."); - session_token_->set_role_data(module::Attribute::ToolTip, "Shotgun session token."); - authenticated_->set_role_data(module::Attribute::ToolTip, "Authenticated."); - timeout_->set_role_data(module::Attribute::ToolTip, "Shotgun server timeout."); -} - -void ShotgunDataSource::attribute_changed(const utility::Uuid &attr_uuid, const int /*role*/) { - // pass upto actor.. - call_attribute_changed(attr_uuid); -} - -template -void ShotgunDataSourceActor::attribute_changed(const utility::Uuid &attr_uuid) { - // properties changed somewhere. - // update loop ? - if (attr_uuid == data_source_.authentication_method_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.authentication_method_->value(), - "/plugin/data_source/shotgun/authentication/grant_type"); - } - if (attr_uuid == data_source_.client_id_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.client_id_->value(), - "/plugin/data_source/shotgun/authentication/client_id"); - } - // if (attr_uuid == data_source_.client_secret_->uuid()) { - // auto prefs = GlobalStoreHelper(system()); - // prefs.set_value(data_source_.client_secret_->value(), - // "/plugin/data_source/shotgun/authentication/client_secret"); - // } - if (attr_uuid == data_source_.timeout_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.timeout_->value(), "/plugin/data_source/shotgun/server/timeout"); - } - - if (attr_uuid == data_source_.username_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.username_->value(), - "/plugin/data_source/shotgun/authentication/username"); - } - // if (attr_uuid == data_source_.password_->uuid()) { - // auto prefs = GlobalStoreHelper(system()); - // prefs.set_value(data_source_.password_->value(), - // "/plugin/data_source/shotgun/authentication/password"); - // } - if (attr_uuid == data_source_.session_token_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.session_token_->value(), - "/plugin/data_source/shotgun/authentication/session_token"); - } -} - - -template -ShotgunDataSourceActor::ShotgunDataSourceActor( - caf::actor_config &cfg, const utility::JsonStore &) - : caf::event_based_actor(cfg) { - - data_source_.bind_attribute_changed_callback( - [this](auto &&PH1) { attribute_changed(std::forward(PH1)); }); - - spdlog::debug("Created ShotgunDataSourceActor {}", name()); - // print_on_exit(this, "MediaHookActor"); - secret_source_ = actor_cast(this); - - shotgun_ = spawn(); - link_to(shotgun_); - - // we need to recieve authentication updates. - join_event_group(this, shotgun_); - - // we are the source of the secret.. - anon_send(shotgun_, shotgun_authentication_source_atom_v, actor_cast(this)); - - system().registry().put(shotgun_datasource_registry, caf::actor_cast(this)); - - try { - auto prefs = GlobalStoreHelper(system()); - JsonStore j; - join_broadcast(this, prefs.get_group(j)); - update_preferences(j); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - pool_ = caf::actor_pool::make( - system().dummy_execution_unit(), - worker_count_, - [&] { - return system().template spawn( - actor_cast(this)); - }, - caf::actor_pool::round_robin()); - link_to(pool_); - - // data_source_.connect_to_ui(); coz async - data_source_.set_parent_actor_addr(actor_cast(this)); - delayed_anon_send( - caf::actor_cast(this), - std::chrono::milliseconds(500), - module::connect_to_ui_atom_v); - - behavior_.assign( - [=](utility::name_atom) -> std::string { return name(); }, - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - - [=](data_source::use_data_atom, const caf::actor &media, const FrameRate &media_rate) - -> result { return UuidActorVector(); }, - - // no drop support.. - [=](data_source::use_data_atom, const JsonStore &, const FrameRate &, const bool) - -> UuidActorVector { return UuidActorVector(); }, - - [=](data_source::use_data_atom, - const std::string &project, - const utility::Uuid &stalk_dnuuid) { - auto jsre = JsonStore(GetVersionIvyUuidJSON); - jsre["ivy_uuid"] = to_string(stalk_dnuuid); - jsre["job"] = project; - delegate(caf::actor_cast(this), get_data_atom_v, jsre); - }, - - [=](shotgun_projects_atom atom) { delegate(shotgun_, atom); }, - - [=](shotgun_groups_atom atom, const int project_id) { - delegate(shotgun_, atom, project_id); - }, - - [=](shotgun_schema_atom atom, const int project_id) { - delegate(shotgun_, atom, project_id); - }, - - [=](shotgun_authentication_source_atom, caf::actor source) { - secret_source_ = actor_cast(source); - }, - - [=](shotgun_authentication_source_atom) -> caf::actor { - return actor_cast(secret_source_); - }, - - [=](shotgun_update_entity_atom atom, - const std::string &entity, - const int record_id, - const JsonStore &body) { delegate(shotgun_, atom, entity, record_id, body); }, - - [=](shotgun_image_atom atom, - const std::string &entity, - const int record_id, - const bool thumbnail) { delegate(shotgun_, atom, entity, record_id, thumbnail); }, - - [=](shotgun_image_atom atom, - const std::string &entity, - const int record_id, - const bool thumbnail, - const bool as_buffer) { - delegate(shotgun_, atom, entity, record_id, thumbnail, as_buffer); - }, - - [=](shotgun_upload_atom atom, - const std::string &entity, - const int record_id, - const std::string &field, - const std::string &name, - const std::vector &data, - const std::string &content_type) { - delegate(shotgun_, atom, entity, record_id, field, name, data, content_type); - }, - - // just use the default with jsonstore ? - [=](put_data_atom, const utility::JsonStore &js) -> result { - try { - if (js["entity"] == "Playlist" and js["relationship"] == "Version") { - auto rp = make_response_promise(); - update_playlist_versions(rp, Uuid(js["playlist_uuid"])); - return rp; - } - } catch (const std::exception &err) { - return make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what()); - } - - return make_error(xstudio_error::error, "Invalid operation."); - }, - - // do we need the UI to have spun up before we can issue calls to shotgun... - // erm... - [=](use_data_atom atom, const caf::uri &uri) { - delegate(actor_cast(this), atom, uri, FrameRate()); - }, - [=](use_data_atom, - const caf::uri &uri, - const FrameRate &media_rate) -> result { - // check protocol == shotgun.. - if (uri.scheme() != "shotgun") - return UuidActorVector(); - - if (to_string(uri.authority()) == "load") { - // need type and id - auto query = uri.query(); - if (query.count("type") and query["type"] == "Version" and query.count("ids")) { - auto ids = split(query["ids"], '|'); - if (ids.empty()) - return UuidActorVector(); - - auto count = std::make_shared(ids.size()); - auto rp = make_response_promise(); - auto results = std::make_shared(); - - for (const auto i : ids) { - try { - auto type = query["type"]; - auto squery = R"({})"_json; - squery["id"] = i; - - request( - caf::actor_cast(this), - std::chrono::seconds( - static_cast(data_source_.timeout_->value())), - shotgun_entity_filter_atom_v, - "Versions", - JsonStore(squery), - VersionFields, - std::vector(), - 1, - 4999) - .then( - [=](const JsonStore &js) mutable { - // load version.. - request( - caf::actor_cast(this), - infinite, - playlist::add_media_atom_v, - js, - utility::Uuid(), - caf::actor(), - utility::Uuid()) - .then( - [=](const UuidActorVector &uav) mutable { - (*count)--; - - for (const auto &ua : uav) - results->push_back(ua); - - if (not(*count)) - rp.deliver(*results); - }, - [=](const caf::error &err) mutable { - (*count)--; - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - if (not(*count)) - rp.deliver(*results); - }); - }, - [=](const caf::error &err) mutable { - spdlog::warn( - "{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - } catch (const std::exception &err) { - (*count)--; - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - return rp; - } else if ( - query.count("type") and query["type"] == "Playlist" and - query.count("ids")) { - // will return an array of playlist actors.. - auto ids = split(query["ids"], '|'); - if (ids.empty()) - return UuidActorVector(); - - auto rp = make_response_promise(); - auto count = std::make_shared(ids.size()); - auto results = std::make_shared(); - - for (const auto i : ids) { - auto id = std::atoi(i.c_str()); - auto js = JsonStore(LoadPlaylistJSON); - js["playlist_id"] = id; - request( - caf::actor_cast(this), - infinite, - use_data_atom_v, - js, - caf::actor()) - .then( - [=](const UuidActor &ua) mutable { - // process result to build playlist.. - (*count)--; - results->push_back(ua); - if (not(*count)) - rp.deliver(*results); - }, - [=](const caf::error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - (*count)--; - if (not(*count)) - rp.deliver(*results); - }); - } - } else { - spdlog::warn( - "Invalid shotgun action {}, requires type, id", to_string(uri)); - } - } else { - spdlog::warn( - "Invalid shotgun action {} {}", to_string(uri.authority()), to_string(uri)); - } - - return UuidActorVector(); - }, - - [=](use_data_atom, - const utility::JsonStore &js, - const caf::actor &session) -> result { - try { - if (js.at("entity") == "Playlist" and js.count("playlist_id")) { - auto rp = make_response_promise(); - load_playlist(rp, js.at("playlist_id").get(), session); - return rp; - } - } catch (const std::exception &err) { - return make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what()); - } - return make_error(xstudio_error::error, "Invalid operation."); - }, - - // just use the default with jsonstore ? - [=](use_data_atom, const utility::JsonStore &js) -> result { - try { - if (js.at("entity") == "Playlist" and js.count("playlist_id")) { - scoped_actor sys{system()}; - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto rp = make_response_promise(); - request( - caf::actor_cast(this), - infinite, - use_data_atom_v, - js, - session) - .then( - [=](const UuidActor &) mutable { - rp.deliver( - JsonStore(R"({"data": {"status": "successful"}})"_json)); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver( - JsonStore(R"({"data": {"status": "successful"}})"_json)); - }); - - return rp; - } else if ( - js.at("entity") == "Playlist" and js.at("relationship") == "Version") { - auto rp = make_response_promise(); - refresh_playlist_versions(rp, Uuid(js.at("playlist_uuid"))); - return rp; - } - } catch (const std::exception &err) { - return make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what()); - } - return make_error(xstudio_error::error, "Invalid operation."); - }, - - // just use the default with jsonstore ? - - [=](post_data_atom, const utility::JsonStore &js) -> result { - try { - if (js["entity"] == "Note") { - auto rp = make_response_promise(); - create_playlist_notes(rp, js["payload"], JsonStore(js["playlist_uuid"])); - return rp; - } - if (js["entity"] == "Playlist") { - auto rp = make_response_promise(); - create_playlist(rp, js); - return rp; - } - } catch (const std::exception &err) { - return make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what()); - } - - return make_error(xstudio_error::error, "Invalid operation."); - }, - - [=](shotgun_entity_atom atom, - const std::string &entity, - const int record_id, - const std::vector &fields) { - delegate(shotgun_, atom, entity, record_id, fields); - }, - - [=](shotgun_entity_filter_atom atom, - const std::string &entity, - const JsonStore &filter, - const std::vector &fields, - const std::vector &sort) { - delegate(shotgun_, atom, entity, filter, fields, sort); - }, - - [=](shotgun_entity_filter_atom atom, - const std::string &entity, - const JsonStore &filter, - const std::vector &fields, - const std::vector &sort, - const int page, - const int page_size) { - delegate(shotgun_, atom, entity, filter, fields, sort, page, page_size); - }, - - [=](shotgun_schema_entity_fields_atom atom, - const std::string &entity, - const std::string &field, - const int id) { delegate(shotgun_, atom, entity, field, id); }, - - [=](shotgun_entity_search_atom atom, - const std::string &entity, - const JsonStore &conditions, - const std::vector &fields, - const std::vector &sort, - const int page, - const int page_size) { - delegate(shotgun_, atom, entity, conditions, fields, sort, page, page_size); - }, - - [=](shotgun_text_search_atom atom, - const std::string &text, - const JsonStore &conditions, - const int page, - const int page_size) { - delegate(shotgun_, atom, text, conditions, page, page_size); - }, - - // can't reply via qt mixin.. this is a work around.. - [=](shotgun_acquire_authentication_atom, const bool cancelled) { - if (cancelled) { - data_source_.set_authenticated(false); - for (auto &i : waiting_) - i.deliver( - make_error(xstudio_error::error, "Authentication request cancelled.")); - } else { - auto auth = data_source_.get_authentication(); - if (waiting_.empty()) { - anon_send(shotgun_, shotgun_authenticate_atom_v, auth); - } else { - for (auto &i : waiting_) - i.deliver(auth); - } - } - waiting_.clear(); - }, - - [=](shotgun_acquire_authentication_atom atom, - const std::string &message) -> result { - if (secret_source_ == actor_cast(this)) - return make_error(xstudio_error::error, "No authentication source."); - - auto rp = make_response_promise(); - waiting_.push_back(rp); - data_source_.set_authenticated(false); - anon_send(actor_cast(secret_source_), atom, message); - return rp; - }, - - [=](utility::event_atom, - shotgun_acquire_token_atom, - const std::pair &tokens) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - tokens.second, - "/plugin/data_source/shotgun/authentication/refresh_token", - false); - prefs.save("APPLICATION"); - data_source_.set_authenticated(true); - }, - - [=](playlist::add_media_atom, - const utility::JsonStore &data, - const utility::Uuid &playlist_uuid, - const caf::actor &playlist, - const utility::Uuid &before) -> result> { - auto rp = make_response_promise>(); - add_media_to_playlist(rp, data, playlist_uuid, playlist, before); - return rp; - }, - - [=](playlist::add_media_atom) { - // this message handler is called in a loop until all build media - // tasks in the queue are exhausted - - bool is_ivy_build_task; - - auto build_media_task_data = get_next_build_task(is_ivy_build_task); - while (build_media_task_data) { - - if (is_ivy_build_task) { - - do_add_media_sources_from_ivy(build_media_task_data); - - } else { - - do_add_media_sources_from_shotgun(build_media_task_data); - } - - // N.B. we only get a new build task if the number of incomplete tasks - // already dispatched is less than the number of actors in our - // worker pool - build_media_task_data = get_next_build_task(is_ivy_build_task); - } - }, - - /*[=](playlist::add_media_atom, - caf::actor playlist, - JsonStore versions_to_add, - const utility::Uuid job_uuid, - const utility::Uuid before, - const FrameRate media_rate, - const bool only_movies) { - - if (versions_to_add.empty()) { - // versions_to_add is now empty - deliver on the job uuid - job_response_promise_[job_uuid].deliver(job_result_[job_uuid]); - job_result_.erase(job_result_.find(job_uuid)); - job_response_promise_.erase(job_response_promise_.find(job_uuid)); - return; - } - - while (job_inflight_[job_uuid] < 10) { - // the version to add (first item in 'versions') - JsonStore version(versions_to_add.begin().value()); - // erase it from 'versions', which is used to recursively calling this message - handler versions_to_add.erase(versions_to_add.begin()); job_inflight_[job_uuid]++; - request(pool_, caf::infinite, playlist::add_media_atom_v, version, media_rate, - only_movies).then( - [=](const UuidActor &res) mutable { - request( - playlist, - infinite, - playlist::add_media_atom_v, - res, - before).then( - [=](const UuidActor& r) mutable { - job_result_[job_uuid].push_back(r); - job_inflight_[job_uuid]--; - // continue processing this job - }, - [=](const caf::error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - job_inflight_[job_uuid]--; - }); - }, - [=](const caf::error &err) mutable { - job_inflight_[job_uuid]--; - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - if (versions_to_add.empty()) break; - } - send(this, playlist::add_media_atom_v, playlist, versions_to_add, job_uuid, before, - media_rate, only_movies); - - },*/ - - // not used. - [=](get_data_atom, - const std::string &entity, - const utility::JsonStore &conditions) -> result { - auto rp = make_response_promise(); - request( - shotgun_, - infinite, - shotgun_entity_search_atom_v, - entity, - conditions, - VersionFields, - std::vector(), - 1, - 30) - .then( - [=](const JsonStore &proj) mutable { rp.deliver(proj); }, - [=](error &err) mutable { rp.deliver(err); }); - return rp; - }, - - [=](get_data_atom, const utility::JsonStore &js) -> result { - auto rp = make_response_promise(); - try { - if (js.at("operation") == "VersionFromIvy") { - find_ivy_version( - rp, - js.at("ivy_uuid").get(), - js.at("job").get()); - } else if (js.at("operation") == "GetShotFromId") { - find_shot(rp, js.at("shot_id").get()); - } else if (js.at("operation") == "LinkMedia") { - link_media(rp, utility::Uuid(js.at("playlist_uuid"))); - } else if (js.at("operation") == "DownloadMedia") { - download_media(rp, utility::Uuid(js.at("media_uuid"))); - } else if (js.at("operation") == "MediaCount") { - get_valid_media_count(rp, utility::Uuid(js.at("playlist_uuid"))); - } else if (js.at("operation") == "PrepareNotes") { - UuidVector media_uuids; - for (const auto &i : js.value("media_uuids", std::vector())) - media_uuids.push_back(Uuid(i)); - - prepare_playlist_notes( - rp, - utility::Uuid(js.at("playlist_uuid")), - media_uuids, - js.value("notify_owner", false), - js.value("notify_group_ids", std::vector()), - js.value("combine", false), - js.value("add_time", false), - js.value("add_playlist_name", false), - js.value("add_type", false), - js.value("anno_requires_note", true), - js.value("skip_already_published", false), - js.value("default_type", "")); - } else { - rp.deliver( - make_error(xstudio_error::error, std::string("Invalid operation."))); - } - } catch (const std::exception &err) { - rp.deliver(make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what())); - } - return rp; - }, - - [=](json_store::update_atom, - const JsonStore & /*change*/, - const std::string & /*path*/, - const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); - }, - - [=](json_store::update_atom, const JsonStore &js) { - try { - update_preferences(js); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - }); -} - -template void ShotgunDataSourceActor::on_exit() { - // maybe on timer.. ? - for (auto &i : waiting_) - i.deliver(make_error(xstudio_error::error, "Password request cancelled.")); - waiting_.clear(); - system().registry().erase(shotgun_datasource_registry); -} - -template void ShotgunDataSourceActor::update_preferences(const JsonStore &js) { - try { - auto grant = preference_value( - js, "/plugin/data_source/shotgun/authentication/grant_type"); - - auto client_id = preference_value( - js, "/plugin/data_source/shotgun/authentication/client_id"); - auto client_secret = preference_value( - js, "/plugin/data_source/shotgun/authentication/client_secret"); - auto username = preference_value( - js, "/plugin/data_source/shotgun/authentication/username"); - auto password = preference_value( - js, "/plugin/data_source/shotgun/authentication/password"); - auto session_token = preference_value( - js, "/plugin/data_source/shotgun/authentication/session_token"); - - auto refresh_token = preference_value( - js, "/plugin/data_source/shotgun/authentication/refresh_token"); - - auto host = - preference_value(js, "/plugin/data_source/shotgun/server/host"); - auto port = preference_value(js, "/plugin/data_source/shotgun/server/port"); - auto protocol = - preference_value(js, "/plugin/data_source/shotgun/server/protocol"); - auto timeout = preference_value(js, "/plugin/data_source/shotgun/server/timeout"); - - - auto cache_dir = expand_envvars( - preference_value(js, "/plugin/data_source/shotgun/download/path")); - auto cache_size = - preference_value(js, "/plugin/data_source/shotgun/download/size"); - - download_cache_.prune_on_exit(true); - download_cache_.target(cache_dir, true); - download_cache_.max_size(cache_size * 1024 * 1024 * 1024); - - auto category = preference_value(js, "/core/bookmark/category"); - category_colours_.clear(); - if (category.is_array()) { - for (const auto &i : category) { - category_colours_[i.value("value", "default")] = i.value("colour", ""); - } - } - - // no op ? - data_source_.set_authentication_method(grant); - data_source_.set_client_id(client_id); - data_source_.set_client_secret(client_secret); - data_source_.set_username(expand_envvars(username)); - data_source_.set_password(password); - data_source_.set_session_token(session_token); - data_source_.set_timeout(timeout); - - // what hppens if we get a sequence of changes... should this be on a timed event ? - // watch out for multiple instances. - auto new_hash = std::hash{}( - grant + username + client_id + host + std::to_string(port) + protocol); - - if (new_hash != changed_hash_) { - changed_hash_ = new_hash; - // set server - anon_send( - shotgun_, - shotgun_host_atom_v, - std::string(fmt::format( - "{}://{}{}", protocol, host, (port ? ":" + std::to_string(port) : "")))); - - auto auth = data_source_.get_authentication(); - if (not refresh_token.empty()) - auth.set_refresh_token(refresh_token); - - anon_send(shotgun_, shotgun_credential_atom_v, auth); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } -} - -template -void ShotgunDataSourceActor::update_playlist_versions( - caf::typed_response_promise rp, - const utility::Uuid &playlist_uuid, - const int playlist_id) { - // src should be a playlist actor.. - // and we want to update it.. - // retrieve shotgun metadata from playlist, and media items. - try { - - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - auto pl_id = playlist_id; - if (not pl_id) { - auto plsg = request_receive( - *sys, playlist, json_store::get_json_atom_v, ShotgunMetadataPath + "/playlist"); - - pl_id = plsg["id"].template get(); - } - - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - // foreach medai actor get it's shogtun metadata. - auto jsn = R"({"versions":[]})"_json; - auto ver = R"({"id": 0, "type": "Version"})"_json; - - std::map version_order_map; - // get media detail - int sort_order = 1; - for (const auto &i : media) { - try { - auto mjson = request_receive( - *sys, - i.actor(), - json_store::get_json_atom_v, - utility::Uuid(), - ShotgunMetadataPath + "/version"); - auto id = mjson["id"].template get(); - ver["id"] = id; - jsn["versions"].push_back(ver); - version_order_map[id] = sort_order; - - sort_order++; - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - // update playlist - request( - shotgun_, - infinite, - shotgun_update_entity_atom_v, - "Playlists", - pl_id, - utility::JsonStore(jsn)) - .then( - [=](const JsonStore &result) mutable { - // spdlog::warn("{}", JsonStore(result["data"]).dump(2)); - // update playorder.. - // get PlaylistVersionConnections - scoped_actor sys{system()}; - - auto order_filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["playlist", "is", {"type":"Playlist", "id":0}] - ] - })"_json; - - order_filter["conditions"][0][2]["id"] = pl_id; - - try { - auto order = request_receive( - *sys, - shotgun_, - shotgun_entity_search_atom_v, - "PlaylistVersionConnection", - JsonStore(order_filter), - std::vector({"sg_sort_order", "version"}), - std::vector({"sg_sort_order"}), - 1, - 4999); - // update all PlaylistVersionConnection's with new sort_order. - for (const auto &i : order["data"]) { - auto version_id = i.at("relationships") - .at("version") - .at("data") - .at("id") - .get(); - auto sort_order = - i.at("attributes").at("sg_sort_order").is_null() - ? 0 - : i.at("attributes").at("sg_sort_order").get(); - // spdlog::warn("{} {}", std::to_string(sort_order), - // std::to_string(version_order_map[version_id])); - if (sort_order != version_order_map[version_id]) { - auto so_jsn = R"({"sg_sort_order": 0})"_json; - so_jsn["sg_sort_order"] = version_order_map[version_id]; - try { - request_receive( - *sys, - shotgun_, - shotgun_update_entity_atom_v, - "PlaylistVersionConnection", - i.at("id").get(), - utility::JsonStore(so_jsn)); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - - if (pl_id != playlist_id) - anon_send( - playlist, - json_store::set_json_atom_v, - JsonStore(result["data"]), - ShotgunMetadataPath + "/playlist"); - rp.deliver(result); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - - // need to update/add PlaylistVersionConnection's - // on creation the sort_order will be null. - // PlaylistVersionConnection are auto created when adding to playlist. (I think) - // so all we need to do is update.. - - - // get shotgun metadata - // get media actors. - // get media shotgun metadata. - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::refresh_playlist_versions( - caf::typed_response_promise rp, const utility::Uuid &playlist_uuid) { - // grab playlist id, get versions compare/load into playlist - try { - - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - - auto plsg = request_receive( - *sys, playlist, json_store::get_json_atom_v, ShotgunMetadataPath + "/playlist"); - - auto pl_id = plsg["id"].template get(); - - // this is a list of the media.. - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - - // foreach media actor get it's shogtun metadata. - std::set current_version_ids; - - for (const auto &i : media) { - try { - auto mjson = request_receive( - *sys, - i.actor(), - json_store::get_json_atom_v, - utility::Uuid(), - ShotgunMetadataPath + "/version"); - current_version_ids.insert(mjson["id"].template get()); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - // we got media shotgun ids, plus playlist id - // get current shotgun playlist/versions - request( - caf::actor_cast(this), - infinite, - shotgun_entity_atom_v, - "Playlists", - pl_id, - std::vector()) - .then( - [=](const JsonStore &result) mutable { - try { - scoped_actor sys{system()}; - // update playlist - anon_send( - playlist, - json_store::set_json_atom_v, - JsonStore(result["data"]), - ShotgunMetadataPath + "/playlist"); - - // gather versions, to get more detail.. - std::vector version_ids; - for (const auto &i : - result.at("data").at("relationships").at("versions").at("data")) { - if (not current_version_ids.count(i.at("id").template get())) - version_ids.emplace_back( - std::to_string(i.at("id").template get())); - } - - if (version_ids.empty()) { - rp.deliver(result); - return; - } - - auto query = R"({})"_json; - query["id"] = join_as_string(version_ids, ","); - - // get details.. - request( - caf::actor_cast(this), - infinite, - shotgun_entity_filter_atom_v, - "Versions", - JsonStore(query), - VersionFields, - std::vector(), - 1, - 1000) - .then( - [=](const JsonStore &result2) mutable { - try { - // got version details. - // we can now just call add versions to playlist.. - anon_send( - caf::actor_cast(this), - playlist::add_media_atom_v, - result2, - playlist_uuid, - playlist, - utility::Uuid()); - - // return this as the result. - rp.deliver(result); - - } catch (const std::exception &err) { - rp.deliver( - make_error(xstudio_error::error, err.what())); - } - }, - - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - - - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::create_playlist( - caf::typed_response_promise rp, const utility::JsonStore &js) { - // src should be a playlist actor.. - // and we want to update it.. - // retrieve shotgun metadata from playlist, and media items. - try { - - scoped_actor sys{system()}; - - auto playlist_uuid = Uuid(js["playlist_uuid"]); - auto project_id = js["project_id"].template get(); - auto code = js["code"].template get(); - auto loc = js["location"].template get(); - auto playlist_type = js["playlist_type"].template get(); - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - auto jsn = R"({ - "project":{ "type": "Project", "id":null }, - "code": null, - "sg_location": "unknown", - "sg_type": "Dailies", - "sg_date_and_time": null - })"_json; - - jsn["project"]["id"] = project_id; - jsn["code"] = code; - jsn["sg_location"] = loc; - jsn["sg_type"] = playlist_type; - jsn["sg_date_and_time"] = date_time_and_zone(); - - // "2021-08-18T19:00:00Z" - - // need to capture result to embed in playlist and add any media.. - request( - shotgun_, - infinite, - shotgun_create_entity_atom_v, - "playlists", - utility::JsonStore(jsn)) - .then( - [=](const JsonStore &result) mutable { - try { - // get new playlist id.. - auto playlist_id = result.at("data").at("id").template get(); - // update shotgun versions from our source playlist. - // return the result.. - update_playlist_versions(rp, playlist_uuid, playlist_id); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, result.dump(2)); - rp.deliver(make_error(xstudio_error::error, err.what())); - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - - -template -void ShotgunDataSourceActor::add_media_to_playlist( - caf::typed_response_promise rp, - const utility::JsonStore &data, - utility::Uuid playlist_uuid, - caf::actor playlist, - const utility::Uuid &before) { - // data can be in multiple forms.. - - auto sys = caf::scoped_actor(system()); - - nlohmann::json versions; - try { - versions = data.at("data").at("relationships").at("versions").at("data"); - } catch (...) { - try { - versions = data.at("data"); - } catch (...) { - return rp.deliver(make_error(xstudio_error::error, "Invalid JSON")); - ; - } - } - - if (versions.empty()) - return rp.deliver(std::vector()); - - auto event_msg = std::shared_ptr(); - - - // get uuid for playlist - if (playlist and playlist_uuid.is_null()) { - try { - playlist_uuid = - request_receive(*sys, playlist, utility::uuid_atom_v); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - playlist = caf::actor(); - } - } - - // get playlist for uuid - if (not playlist and not playlist_uuid.is_null()) { - try { - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - playlist_uuid = utility::Uuid(); - } - } - - // create playlist.. - if (not playlist and playlist_uuid.is_null()) { - try { - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - playlist_uuid = utility::Uuid::generate(); - playlist = spawn("Shotgun Media", playlist_uuid, session); - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - if (not playlist_uuid.is_null()) { - event_msg = std::make_shared( - "Loading Shotgun Playlist Media {}", - 0, - 0, - versions.size(), // we increment progress once per version loaded - ivy leafs are - // added after progress hits 100% - std::set({playlist_uuid})); - event::send_event(this, *event_msg); - } - - try { - auto media_rate = - request_receive(*sys, playlist, session::media_rate_atom_v); - - std::string flag_text, flag_colour; - if (not data.value("flag_text", "").empty() and - not data.value("flag_colour", "").empty()) { - flag_colour = data.value("flag_colour", ""); - flag_text = data.value("flag_text", ""); - } - - // we need to ensure that media are added to playlist IN ORDER - this - // is a bit fiddly because media are created out of order by the worker - // pool so we use this utility::UuidList to ensure that the playlist builds - // with media in order - auto ordered_uuids = std::make_shared(); - auto result = std::make_shared(); - auto result_count = std::make_shared(0); - - // get a new media item created for each of the names in our list - for (const auto &i : versions) { - - std::string name(i.at("attributes").at("code")); - - // create a task data item, with the raw shotgun data that - // can be used to build the media sources for each media - // item in the playlist - ordered_uuids->push_back(utility::Uuid::generate()); - build_playlist_media_tasks_.emplace_back(std::make_shared( - playlist, - ordered_uuids->back(), - name, // name for the media - JsonStore(i), - media_rate, - data.value("preferred_visual_source", ""), - data.value("preferred_audio_source", ""), - event_msg, - ordered_uuids, - before, - flag_colour, - flag_text, - rp, - result, - result_count)); - } - - // this call starts the work of building the media and consuming - // the jobs in the 'build_playlist_media_tasks_' queue - send(this, playlist::add_media_atom_v); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - if (not playlist_uuid.is_null()) { - event_msg->set_complete(); - event::send_event(this, *event_msg); - } - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::get_valid_media_count( - caf::typed_response_promise rp, const utility::Uuid &uuid) { - try { - // find playlist - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = - request_receive(*sys, session, session::get_playlist_atom_v, uuid); - - // get media.. - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - if (not media.empty()) { - fan_out_request( - vector_to_caf_actor_vector(media), - infinite, - json_store::get_json_atom_v, - utility::Uuid(), - "") - .then( - [=](std::vector json) mutable { - int count = 0; - for (const auto &i : json) { - try { - if (i["metadata"].count("shotgun")) - count++; - } catch (...) { - } - } - - JsonStore result(R"({"result": {"valid":0, "invalid":0}})"_json); - result["result"]["valid"] = count; - result["result"]["invalid"] = json.size() - count; - rp.deliver(result); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); - }); - } else { - rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); - } - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - - -template -void ShotgunDataSourceActor::download_media( - caf::typed_response_promise rp, const utility::Uuid &uuid) { - try { - // find media - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto media = - request_receive(*sys, session, playlist::get_media_atom_v, uuid); - - // get metadata, we need version id.. - auto media_metadata = request_receive( - *sys, - media, - json_store::get_json_atom_v, - utility::Uuid(), - "/metadata/shotgun/version"); - - // spdlog::warn("{}", media_metadata.dump(2)); - - auto name = media_metadata.at("attributes").at("code").template get(); - auto job = - media_metadata.at("attributes").at("sg_project_name").template get(); - auto shot = media_metadata.at("relationships") - .at("entity") - .at("data") - .at("name") - .template get(); - auto filepath = download_cache_.target_string() + "/" + name + "-" + job + "-" + shot + - ".dneg.webm"; - - - // check it doesn't already exist.. - if (fs::exists(filepath)) { - // create source and add to media - auto uuid = Uuid::generate(); - auto source = spawn( - "Shotgun Preview", - utility::posix_path_to_uri(filepath), - FrameList(), - FrameRate(), - uuid); - request(media, infinite, media::add_media_source_atom_v, UuidActor(uuid, source)) - .then( - [=](const Uuid &u) mutable { - auto jsn = JsonStore(R"({})"_json); - jsn["actor_uuid"] = uuid; - jsn["actor"] = actor_to_string(system(), source); - - rp.deliver(jsn); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore((R"({})"_json)["error"] = to_string(err))); - }); - } else { - request( - shotgun_, - infinite, - shotgun_attachment_atom_v, - "version", - media_metadata.at("id").template get(), - "sg_uploaded_movie_webm") - .then( - [=](const std::string &data) mutable { - if (data.size() > 1024 * 15) { - // write to file - std::ofstream o(filepath); - try { - o.exceptions(std::ifstream::failbit | std::ifstream::badbit); - o << data << std::endl; - o.close(); - - // file written add to media as new source.. - auto uuid = Uuid::generate(); - auto source = spawn( - "Shotgun Preview", - utility::posix_path_to_uri(filepath), - FrameList(), - FrameRate(), - uuid); - request( - media, - infinite, - media::add_media_source_atom_v, - UuidActor(uuid, source)) - .then( - [=](const Uuid &u) mutable { - auto jsn = JsonStore(R"({})"_json); - jsn["actor_uuid"] = uuid; - jsn["actor"] = actor_to_string(system(), source); - - rp.deliver(jsn); - }, - [=](error &err) mutable { - spdlog::error( - "{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore( - (R"({})"_json)["error"] = to_string(err))); - }); - - } catch (const std::exception &) { - // remove failed file - if (o.is_open()) { - o.close(); - fs::remove(filepath); - } - spdlog::warn("Failed to open file"); - } - } else { - rp.deliver( - JsonStore((R"({})"_json)["error"] = "Failed to download")); - } - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore((R"({})"_json)["error"] = to_string(err))); - }); - } - // "content_type": "video/webm", - // "id": 88463162, - // "link_type": "upload", - // "name": "b'tmp_upload_webm_0okvakz6.webm'", - // "type": "Attachment", - // "url": "http://shotgun.dneg.com/file_serve/attachment/88463162" - - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(JsonStore((R"({})"_json)["error"] = err.what())); - } -} - -template -void ShotgunDataSourceActor::link_media( - caf::typed_response_promise rp, const utility::Uuid &uuid) { - try { - // find playlist - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = - request_receive(*sys, session, session::get_playlist_atom_v, uuid); - - // get media.. - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - // scan media for shotgun version / ivy uuid - if (not media.empty()) { - fan_out_request( - vector_to_caf_actor_vector(media), - infinite, - json_store::get_json_atom_v, - utility::Uuid(), - "", - true) - .then( - [=](std::vector> json) mutable { - // ivy uuid is stored on source not media.. balls. - auto left = std::make_shared(0); - auto invalid = std::make_shared(0); - for (const auto &i : json) { - try { - if (i.second.is_null() or - not i.second["metadata"].count("shotgun")) { - // request current media source metadata.. - scoped_actor sys{system()}; - auto source_meta = request_receive( - *sys, - i.first.actor(), - json_store::get_json_atom_v, - "/metadata/external/DNeg"); - // we has got it.. - auto ivy_uuid = source_meta.at("Ivy").at("dnuuid"); - auto job = source_meta.at("show"); - auto shot = source_meta.at("shot"); - (*left) += 1; - // spdlog::warn("{} {} {} {}", job, shot, ivy_uuid, *left); - // call back into self ? - // but we need to wait for the final result.. - // maybe in danger of deadlocks... - // now we need to query shotgun.. - // to try and find version from this information. - // this is then used to update the media actor. - auto jsre = JsonStore(GetVersionIvyUuidJSON); - jsre["ivy_uuid"] = ivy_uuid; - jsre["job"] = job; - - request( - caf::actor_cast(this), - infinite, - get_data_atom_v, - jsre) - .then( - [=](const JsonStore &ver) mutable { - // got ver from uuid - (*left)--; - if (ver["payload"].empty()) { - (*invalid)++; - } else { - // push version to media object - scoped_actor sys{system()}; - try { - request_receive( - *sys, - i.first.actor(), - json_store::set_json_atom_v, - utility::Uuid(), - JsonStore(ver["payload"]), - ShotgunMetadataPath + "/version"); - } catch (const std::exception &err) { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - err.what()); - } - } - - if (not(*left)) { - JsonStore result( - R"({"result": {"valid":0, "invalid":0}})"_json); - result["result"]["valid"] = - json.size() - (*invalid); - result["result"]["invalid"] = (*invalid); - rp.deliver(result); - } - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - (*left)--; - (*invalid)++; - if (not(*left)) { - JsonStore result( - R"({"result": {"valid":0, "invalid":0}})"_json); - result["result"]["valid"] = - json.size() - (*invalid); - result["result"]["invalid"] = (*invalid); - rp.deliver(result); - } - }); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - if (not(*left)) { - JsonStore result(R"({"result": {"valid":0, "invalid":0}})"_json); - result["result"]["valid"] = json.size(); - result["result"]["invalid"] = 0; - rp.deliver(result); - } - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); - }); - } else { - rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); - } - - - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::find_ivy_version( - caf::typed_response_promise rp, - const std::string &uuid, - const std::string &job) { - // find version from supplied details. - - auto version_filter = - FilterBy().And(Text("project.Project.name").is(job), Text("sg_ivy_dnuuid").is(uuid)); - - request( - shotgun_, - std::chrono::seconds(static_cast(data_source_.timeout_->value())), - shotgun_entity_search_atom_v, - "Version", - JsonStore(version_filter), - VersionFields, - std::vector(), - 1, - 1) - .then( - [=](const JsonStore &jsn) mutable { - auto result = JsonStore(R"({"payload":[]})"_json); - if (jsn.count("data") and jsn.at("data").size()) { - result["payload"] = jsn.at("data")[0]; - } - rp.deliver(result); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(R"({"payload":[]})"_json)); - }); -} - -template -void ShotgunDataSourceActor::find_shot( - caf::typed_response_promise rp, const int shot_id) { - // find version from supplied details. - if (shot_cache_.count(shot_id)) - rp.deliver(shot_cache_.at(shot_id)); - - request( - shotgun_, - std::chrono::seconds(static_cast(data_source_.timeout_->value())), - shotgun_entity_atom_v, - "Shot", - shot_id, - ShotFields) - .then( - [=](const JsonStore &jsn) mutable { - shot_cache_[shot_id] = jsn; - rp.deliver(jsn); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(R"({"data":{}})"_json)); - }); -} - -template -void ShotgunDataSourceActor::prepare_playlist_notes( - caf::typed_response_promise rp, - const utility::Uuid &playlist_uuid, - const utility::UuidVector &media_uuids, - const bool notify_owner, - const std::vector notify_group_ids, - const bool combine, - const bool add_time, - const bool add_playlist_name, - const bool add_type, - const bool anno_requires_note, - const bool skip_already_pubished, - const std::string &default_type) { - - auto playlist_name = std::string(); - auto playlist_id = int(0); - auto payload = R"({"payload":[], "valid": 0, "invalid": 0})"_json; - - try { - scoped_actor sys{system()}; - - // get session - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - // get playlist - auto playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - // get shotgun info from playlist.. - try { - auto sgpl = request_receive( - *sys, playlist, json_store::get_json_atom_v, ShotgunMetadataPath + "/playlist"); - - playlist_name = sgpl.at("attributes").at("code").template get(); - playlist_id = sgpl.at("id").template get(); - - } catch (const std::exception &err) { - spdlog::info("No shotgun playlist information"); - } - - // get media for playlist. - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - // no media so no point.. - // nothing to publish. - if (media.empty()) - return rp.deliver(JsonStore(payload)); - - std::vector media_actors; - - if (not media_uuids.empty()) { - auto lookup = uuidactor_vect_to_map(media); - for (const auto &i : media_uuids) { - if (lookup.count(i)) - media_actors.push_back(lookup[i]); - } - } else { - media_actors = vector_to_caf_actor_vector(media); - } - - // get media shotgun json.. - // we can only publish notes for media that has version information - fan_out_request( - media_actors, - infinite, - json_store::get_json_atom_v, - utility::Uuid(), - ShotgunMetadataPath, - true) - .then( - [=](std::vector> version_meta) mutable { - auto result = JsonStore(payload); - - scoped_actor sys{system()}; - - std::map> media_map; - UuidVector valid_media; - - // get valid media. - // get all the shotgun info we need to publish - for (const auto &i : version_meta) { - try { - // spdlog::warn("{}", i.second.dump(2)); - const auto &version = i.second.at("version"); - auto jsn = JsonStore(PublishNoteTemplateJSON); - - // project - jsn["payload"]["project"]["id"] = version.at("relationships") - .at("project") - .at("data") - .at("id") - .get(); - - - // playlist link - jsn["payload"]["note_links"][0]["id"] = playlist_id; - - if (version.at("relationships") - .at("entity") - .at("data") - .value("type", "") == "Sequence") - // shot link - jsn["payload"]["note_links"][1]["id"] = - version.at("relationships") - .at("entity") - .at("data") - .value("id", 0); - else if ( - version.at("relationships") - .at("entity") - .at("data") - .value("type", "") == "Shot") - // sequence link - jsn["payload"]["note_links"][2]["id"] = - version.at("relationships") - .at("entity") - .at("data") - .value("id", 0); - - // version link - jsn["payload"]["note_links"][3]["id"] = version.value("id", 0); - - if (jsn["payload"]["note_links"][3]["id"].get() == 0) - jsn["payload"]["note_links"].erase(3); - if (jsn["payload"]["note_links"][2]["id"].get() == 0) - jsn["payload"]["note_links"].erase(2); - if (jsn["payload"]["note_links"][1]["id"].get() == 0) - jsn["payload"]["note_links"].erase(1); - if (jsn["payload"]["note_links"][0]["id"].get() == 0) - jsn["payload"]["note_links"].erase(0); - - // we don't pass these to shotgun.. - jsn["shot"] = version.at("relationships") - .at("entity") - .at("data") - .at("name") - .get(); - jsn["playlist_name"] = playlist_name; - - if (notify_owner) // 1068 - jsn["payload"]["addressings_to"][0]["id"] = - version.at("relationships") - .at("user") - .at("data") - .at("id") - .get(); - else - jsn["payload"].erase("addressings_to"); - - if (not notify_group_ids.empty()) { - auto grp = R"({ "type": "Group", "id": null})"_json; - for (const auto g : notify_group_ids) { - if (g <= 0) - continue; - - grp["id"] = g; - jsn["payload"]["addressings_cc"].push_back(grp); - } - } - - if (jsn["payload"]["addressings_cc"].empty()) - jsn["payload"].erase("addressings_cc"); - - - media_map[i.first.uuid()] = std::make_pair(i.first, jsn); - valid_media.push_back(i.first.uuid()); - } catch (const std::exception &err) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - // get bookmark manager. - auto bookmarks = request_receive( - *sys, session, bookmark::get_bookmark_atom_v); - - // // collect media notes if they have shotgun metadata on the media - auto existing_bookmarks = - request_receive>>( - *sys, bookmarks, bookmark::get_bookmarks_atom_v, valid_media); - - // get bookmark detail.. - for (const auto &i : existing_bookmarks) { - // grouped by media item. - // we may want to collapse to unique note_types - std::map>> - notes_by_type; - - for (const auto &j : i.second) { - try { - if (skip_already_pubished) { - auto already_published = false; - try { - // check for shotgun metadata on note. - request_receive( - *sys, - j.actor(), - json_store::get_json_atom_v, - ShotgunMetadataPath + "/note"); - already_published = true; - } catch (...) { - } - - if (already_published) - continue; - } - - auto detail = request_receive( - *sys, j.actor(), bookmark::bookmark_detail_atom_v); - // skip notes with no text unless annotated and - // only_with_annotation is true - auto has_note = detail.note_ and not(*(detail.note_)).empty(); - auto has_anno = - detail.has_annotation_ and *(detail.has_annotation_); - - if (not(has_note or (has_anno and not anno_requires_note))) - continue; - - auto [ua, jsn] = media_map[detail.owner_->uuid()]; - // push to shotgun client.. - jsn["bookmark_uuid"] = j.uuid(); - if (not jsn.count("has_annotation")) - jsn["has_annotation"] = R"([])"_json; - - if (has_anno) { - auto item = - R"({"media_uuid": null, "media_name": null, "media_frame": 0, "timecode_frame": 0})"_json; - item["media_uuid"] = i.first; - item["media_name"] = jsn["shot"]; - item["media_frame"] = detail.start_frame(); - item["timecode_frame"] = - detail.start_timecode_tc().total_frames(); - // requires media actor and first frame of annotation. - jsn["has_annotation"].push_back(item); - } - auto cat = detail.category_ ? *(detail.category_) : ""; - if (not default_type.empty()) - cat = default_type; - - jsn["payload"]["sg_note_type"] = cat; - jsn["payload"]["subject"] = - detail.subject_ ? *(detail.subject_) : ""; - // format note content - std::string content; - - if (add_time) - content += std::string("Frame : ") + - std::to_string( - detail.start_timecode_tc().total_frames()) + - " / " + detail.start_timecode() + " / " + - detail.duration_timecode() + "\n"; - - content += *(detail.note_); - - jsn["payload"]["content"] = content; - - // yeah this is a bit convoluted. - if (not notes_by_type.count(cat)) { - notes_by_type.insert(std::make_pair( - cat, - std::map>( - {{detail.start_frame(), {{jsn}}}}))); - } else { - if (notes_by_type[cat].count(detail.start_frame())) { - notes_by_type[cat][detail.start_frame()].push_back(jsn); - } else { - notes_by_type[cat].insert(std::make_pair( - detail.start_frame(), - std::vector({jsn}))); - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - try { - auto merged = JsonStore(); - - // category - for (auto &k : notes_by_type) { - auto category = k.first; - // frame - for (const auto &j : k.second) { - // entry - for (const auto ¬epayload : j.second) { - // spdlog::warn("{}",notepayload.dump(2)); - - if (not merged.is_null() and - (not combine or - merged["payload"]["sg_note_type"] != - notepayload["payload"]["sg_note_type"])) { - // spdlog::warn("{}", merged.dump(2)); - result["payload"].push_back(merged); - merged = JsonStore(); - } - - if (merged.is_null()) { - merged = notepayload; - auto content = std::string(); - if (add_playlist_name and - not merged["playlist_name"] - .get() - .empty()) - content += - "Playlist : " + - std::string(merged["playlist_name"]) + "\n"; - if (add_type) - content += "Note Type : " + - merged["payload"]["sg_note_type"] - .get() + - "\n\n"; - else - content += "\n\n"; - - merged["payload"]["content"] = - content + - merged["payload"]["content"].get(); - - merged.erase("shot"); - merged.erase("playlist_name"); - } else { - merged["payload"]["content"] = - merged["payload"]["content"] - .get() + - "\n\n" + - notepayload["payload"]["content"] - .get(); - merged["has_annotation"].insert( - merged["has_annotation"].end(), - notepayload["has_annotation"].begin(), - notepayload["has_annotation"].end()); - } - } - } - } - - if (not merged.is_null()) - result["payload"].push_back(merged); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - result["valid"] = result["payload"].size(); - - // spdlog::warn("{}", result.dump(2)); - rp.deliver(result); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(payload)); - }); - - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::create_playlist_notes( - caf::typed_response_promise rp, - const utility::JsonStore ¬es, - const utility::Uuid &playlist_uuid) { - - const std::string ui(R"( - import xStudio 1.0 - import QtQuick 2.14 - XsLabel { - anchors.fill: parent - font.pixelSize: XsStyle.popupControlFontSize*1.2 - verticalAlignment: Text.AlignVCenter - font.weight: Font.Bold - color: palette.highlight - text: "SG" - } - )"); - - try { - scoped_actor sys{system()}; - - // get session - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto bookmarks = - request_receive(*sys, session, bookmark::get_bookmark_atom_v); - - auto tags = request_receive(*sys, session, xstudio::tag::get_tag_atom_v); - - auto count = std::make_shared(notes.size()); - auto failed = std::make_shared(0); - auto succeed = std::make_shared(0); - - auto offscreen_renderer = - system().registry().template get(offscreen_viewport_registry); - auto thumbnail_manager = - system().registry().template get(thumbnail_manager_registry); - - for (const auto &j : notes) { - // need to capture result to embed in playlist and add any media.. - // spdlog::warn("{}", j["payload"].dump(2)); - request( - shotgun_, - infinite, - shotgun_create_entity_atom_v, - "notes", - utility::JsonStore(j["payload"])) - .then( - [=](const JsonStore &result) mutable { - (*count)--; - try { - // "errors": [ - // { - // "status": null - // } - // ] - if (not result.at("errors")[0].at("status").is_null()) - throw std::runtime_error(result["errors"].dump(2)); - - // get new playlist id.. - auto note_id = result.at("data").at("id").template get(); - // we have a note... - if (not j["has_annotation"].empty()) { - for (const auto &anno : j["has_annotation"]) { - request( - session, - infinite, - playlist::get_media_atom_v, - utility::Uuid(anno["media_uuid"])) - .then( - [=](const caf::actor &media_actor) mutable { - // spdlog::warn("render annotation {}", - // anno["media_frame"].get()); - request( - offscreen_renderer, - infinite, - ui::viewport:: - render_viewport_to_image_atom_v, - media_actor, - anno["media_frame"].get(), - thumbnail::THUMBNAIL_FORMAT::TF_RGB24, - 0, - true, - true) - .then( - [=](const thumbnail::ThumbnailBufferPtr - &tnail) { - // got buffer. convert to jpg.. - request( - thumbnail_manager, - infinite, - media_reader:: - get_thumbnail_atom_v, - tnail) - .then( - [=](const std::vector< - std::byte> - &jpgbuf) mutable { - // final step... - auto title = std:: - string(fmt::format( - "{}_{}.jpg", - anno["media_" - "name"] - .get< - std:: - string>(), - anno["timecode_" - "frame"] - .get< - int>())); - request( - shotgun_, - infinite, - shotgun_upload_atom_v, - "note", - note_id, - "", - title, - jpgbuf, - "image/jpeg") - .then( - [=](const bool) { - }, - [=](const error & - err) mutable { - spdlog::warn( - "{} " - "Failed" - " uploa" - "d of " - "annota" - "tion " - "{}", - __PRETTY_FUNCTION__, - to_string( - err)); - } - - ); - }, - [=](const error - &err) mutable { - spdlog::warn( - "{} Failed jpeg " - "conversion {}", - __PRETTY_FUNCTION__, - to_string(err)); - }); - }, - [=](const error &err) mutable { - spdlog::warn( - "{} Failed render annotation " - "{}", - __PRETTY_FUNCTION__, - to_string(err)); - }); - }, - [=](const error &err) mutable { - spdlog::warn( - "{} Failed get media {}", - __PRETTY_FUNCTION__, - to_string(err)); - }); - } - } - - // spdlog::warn("note {}", result.dump(2)); - // send json to note.. - anon_send( - bookmarks, - json_store::set_json_atom_v, - utility::Uuid(j["bookmark_uuid"]), - utility::JsonStore(result.at("data")), - ShotgunMetadataPath + "/note"); - - xstudio::tag::Tag t; - t.set_type("Decorator"); - t.set_data(ui); - t.set_link(utility::Uuid(j["bookmark_uuid"])); - t.set_unique(to_string(t.link()) + t.type() + t.data()); - - anon_send(tags, xstudio::tag::add_tag_atom_v, t); - - // update shotgun versions from our source playlist. - // return the result.. - // update_playlist_versions(rp, playlist_uuid, playlist_id); - (*succeed)++; - } catch (const std::exception &err) { - (*failed)++; - spdlog::warn( - "{} {} {}", __PRETTY_FUNCTION__, err.what(), result.dump(2)); - } - - if (not(*count)) { - auto jsn = JsonStore(R"({"data": {"status": ""}})"_json); - jsn["data"]["status"] = std::string(fmt::format( - "Successfully published {} / {} notes.", - *succeed, - (*failed) + (*succeed))); - rp.deliver(jsn); - } - }, - [=](error &err) mutable { - spdlog::warn( - "Failed create note entity {} {}", - __PRETTY_FUNCTION__, - to_string(err)); - (*count)--; - (*failed)++; - - if (not(*count)) { - auto jsn = JsonStore(R"({"data": {"status": ""}})"_json); - jsn["data"]["status"] = std::string(fmt::format( - "Successfully published {} / {} notes.", - *succeed, - (*failed) + (*succeed))); - rp.deliver(jsn); - } - }); - } - - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -// template void -// ShotgunDataSourceActor::refresh_playlist_notes(caf::typed_response_promise -// rp, const utility::Uuid &playlist_uuid) { -// try { -// // find playlist -// scoped_actor sys{system()}; - -// // get session -// auto session = request_receive( -// *sys, -// system().registry().template get(global_registry), -// session::session_atom_v); - -// // get playlist -// auto playlist = request_receive( -// *sys, -// session, -// session::get_playlist_atom_v, -// playlist_uuid); - - -// // get media.. -// auto media = request_receive>( -// *sys, -// playlist, -// playlist::get_media_atom_v); - -// // no media so no point.. -// if(media.empty()) { -// rp.deliver(JsonStore(R"({"data": {"status": "successful"}})"_json)); -// return; -// } - -// auto bookmarks = request_receive( -// *sys, -// session, -// bookmark::get_bookmark_atom_v); - -// // get shotgun playlist id.. -// auto sgpl = request_receive( -// *sys, playlist, json_store::get_json_atom_v, ShotgunMetadataPath+"/playlist"); -// auto sgpl_id = sgpl["id"].template get(); - -// // get shotgun data.. -// // this calls ourself so need dispatch.. -// auto note_filter = R"( -// { -// "logical_operator": "and", -// "conditions": [ -// ["note_links", "in", {"type":"Playlist", "id":0}] -// ] -// })"_json; - -// note_filter["conditions"][0][2]["id"] = sgpl_id; - -// request(caf::actor_cast(this), -// SHOTGUN_TIMEOUT, -// shotgun_entity_search_atom_v, "Notes", -// JsonStore(note_filter), -// std::vector({"*"}), -// std::vector(), -// 1, 4999).then( -// [=](const JsonStore &jsn) mutable { -// // get metadata to see if they are tagged with version id's -// fan_out_request( -// vpair_second_to_v(media), infinite, json_store::get_json_atom_v, -// utility::Uuid(), ShotgunMetadataPath, true) .then( -// [=](std::vector> vmeta) mutable { -// std::map media_map; -// std::map ver_media_map; -// std::map ver_note_map; - -// // map shot gun versions to media actors. -// for(const auto &i: vmeta){ -// auto ver_id = i.second.value("version", -// R"({})"_json).value("id", 0); -// // spdlog::warn("{} {} {}", ver_id, -// to_string(i.first.first), i.second.dump(2)); if(ver_id){ -// ver_media_map[ver_id] = i.first; -// // add media to map -// media_map[i.first.first] = i.first.second; -// } -// } - -// // map notes to versions, maybe more than one note. -// for(const auto &i : jsn["data"]) { -// for(const auto &j : -// i["relationships"]["note_links"]["data"] ){ -// auto ver_id = j.value("id", 0); -// if(j.value("type", "") == "Version") { -// if(ver_media_map.count(ver_id)) { -// if(not ver_note_map.count(ver_id)) { -// ver_note_map[ver_id] = R"([])"_json; -// } -// ver_note_map[ver_id].push_back(i); -// // spdlog::warn("pushed to {}", ver_id); -// } else { -// // spdlog::warn("No match {}", j.dump(2)); -// } -// break; -// } -// } -// } - -// scoped_actor sys{system()}; -// // collect all note metadata on all media. -// // even if the note exists it's state might have changed.. -// // so we always update. -// auto existing_bookmarks = -// request_receive>>( -// *sys, bookmarks, bookmark::get_bookmarks_atom_v, -// map_key_to_vec(media_map) -// ); - -// // get metadata from existing bookmarks.. -// // do group query on bookmark json.. -// UuidVector meta_bookmarks; -// for(const auto &i: existing_bookmarks) { -// for(const auto &j: i.second) { -// meta_bookmarks.push_back(j.first); -// } -// } -// std::set existing_notes; -// auto bookmark_json = -// request_receive>>(*sys, bookmarks, json_store::get_json_atom_v, -// meta_bookmarks, ShotgunMetadataPath+"/note/id"); for(const -// auto &i: bookmark_json) { -// existing_notes.insert(i.second.get()); -// // spdlog::warn("bookmark sg js {} {}", -// i.second.dump(2),to_string(i.first.first)); -// } - - -// // Create new notes and link to media -// for(const auto &i: ver_note_map) { -// for(const auto &j: i.second) { -// if(existing_notes.count(j["id"].get())) { -// // spdlog::warn("Existing note skipping {}", -// j["id"].get()); continue; -// } - -// // spdlog::warn("{}", j.dump(2)); -// // create bookmark -// auto ba = request_receive( -// *sys, bookmarks, bookmark::add_bookmark_atom_v, -// ver_media_map[i.first] -// ); -// // set json data -// anon_send(ba.second, json_store::set_json_atom_v, -// JsonStore(j), ShotgunMetadataPath+"/note"); - -// bookmark::BookmarkDetail detail; -// try { -// detail.author_ = -// j["relationships"]["created_by"]["data"].value("name","Anonymous"); -// } catch(...){ -// detail.author_ = "Anonymous"; -// } -// detail.category_ = -// j["attributes"].value("sg_note_type", -// "default"); detail.colour_ = -// category_colours_.count(*(detail.category_)) ? -// category_colours_[*(detail.category_)] : ""; -// detail.subject_ = -// j["attributes"].value("subject", ""); -// detail.note_ = -// j["attributes"].value("content", ""); -// detail.created_ = -// to_sys_time_point(j["attributes"].value("created_at", -// "1972-03-19T00:00:00Z")); - -// // set detail -// anon_send(ba.second, -// bookmark::bookmark_detail_atom_v, detail); - -// // spdlog::warn("{} {} {}", i.first, -// j.value("id",0), -// j["attributes"].value("created_at", -// "")); -// } -// } - -// rp.deliver(JsonStore(R"({"data": {"status": -// "successful"}})"_json)); -// }, -// [=](error &err) mutable { -// spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); -// rp.deliver(JsonStore(R"({"data": {"status": -// "unsuccessful"}})"_json)); -// }); - -// }, -// [=](error &err) mutable { -// spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); -// rp.deliver(JsonStore(R"({"data": {"status": "unsuccessful"}})"_json)); -// } -// ); - - -// } catch(const std::exception &err) { -// spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); -// rp.deliver(make_error(xstudio_error::error, err.what())); -// } -// } - -template -void ShotgunDataSourceActor::load_playlist( - caf::typed_response_promise rp, - const int playlist_id, - const caf::actor &session) { - - // this is going to get nesty :() - - // get playlist from shotgun - request( - caf::actor_cast(this), - infinite, - shotgun_entity_atom_v, - "Playlists", - playlist_id, - std::vector()) - .then( - [=](JsonStore pljs) mutable { - // got playlist. - // we can create an new xstudio playlist actor at this point.. - auto playlist = UuidActor(); - try { - if (session) { - scoped_actor sys{system()}; - - auto tmp = request_receive>( - *sys, - session, - session::add_playlist_atom_v, - pljs["data"]["attributes"]["code"].get(), - utility::Uuid(), - false); - - playlist = tmp.second; - - } else { - auto uuid = utility::Uuid::generate(); - auto tmp = spawn( - pljs["data"]["attributes"]["code"].get(), uuid); - playlist = UuidActor(uuid, tmp); - } - - // place holder for shotgun decorators. - anon_send( - playlist.actor(), - json_store::set_json_atom_v, - JsonStore(), - "/metadata/shotgun"); - // should really be driven from back end not UI.. - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } - - // get version order - auto order_filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["playlist", "is", {"type":"Playlist", "id":0}] - ] - })"_json; - - order_filter["conditions"][0][2]["id"] = playlist_id; - - request( - caf::actor_cast(this), - infinite, - shotgun_entity_search_atom_v, - "PlaylistVersionConnection", - JsonStore(order_filter), - std::vector({"sg_sort_order", "version"}), - std::vector({"sg_sort_order"}), - 1, - 4999) - .then( - [=](const JsonStore &order) mutable { - std::vector version_ids; - for (const auto &i : order["data"]) - version_ids.emplace_back(std::to_string( - i["relationships"]["version"]["data"].at("id").get())); - - if (version_ids.empty()) - return rp.deliver( - make_error(xstudio_error::error, "No Versions found")); - - // get versions - auto query = R"({})"_json; - query["id"] = join_as_string(version_ids, ","); - - // get versions ordered by playlist. - request( - caf::actor_cast(this), - infinite, - shotgun_entity_filter_atom_v, - "Versions", - JsonStore(query), - VersionFields, - std::vector(), - 1, - 4999) - .then( - [=](JsonStore &js) mutable { - // munge it.. - auto data = R"([])"_json; - - for (const auto &i : version_ids) { - for (auto &j : js["data"]) { - - // spdlog::warn("{} {}", - // std::to_string(j["id"].get()), i); - if (std::to_string(j["id"].get()) == i) { - data.push_back(j); - break; - } - } - } - - js["data"] = data; - - // add back in - pljs["data"]["relationships"]["versions"] = js; - - // spdlog::warn("{}",pljs.dump(2)); - // now we have a playlist json struct with the versions - // corrrecly ordered, set metadata on playlist.. - anon_send( - playlist.actor(), - json_store::set_json_atom_v, - JsonStore(pljs["data"]), - ShotgunMetadataPath + "/playlist"); - - // addDecorator(playlist.uuid) - // addMenusFull(playlist.uuid) - - anon_send( - caf::actor_cast(this), - playlist::add_media_atom_v, - pljs, - playlist.uuid(), - playlist.actor(), - utility::Uuid()); - - rp.deliver(playlist); - }, - [=](error &err) mutable { - spdlog::error( - "{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver( - make_error(xstudio_error::error, to_string(err))); - }); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(make_error(xstudio_error::error, to_string(err))); - }); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(make_error(xstudio_error::error, to_string(err))); - }); -} - -template -std::shared_ptr -ShotgunDataSourceActor::get_next_build_task(bool &is_ivy_build_task) { - - std::shared_ptr job_info; - // if we already have popped N jobs off the queue that haven't completed - // and N >= worker_count_ we don't pop a job off and instead return a null - // - if (build_tasks_in_flight_ < worker_count_) { - if (!build_playlist_media_tasks_.empty()) { - is_ivy_build_task = false; - job_info = build_playlist_media_tasks_.front(); - build_playlist_media_tasks_.pop_front(); - } else if (!extend_media_with_ivy_tasks_.empty()) { - is_ivy_build_task = true; - job_info = extend_media_with_ivy_tasks_.front(); - extend_media_with_ivy_tasks_.pop_front(); - } - } - return job_info; -} - -template -void ShotgunDataSourceActor::do_add_media_sources_from_shotgun( - std::shared_ptr build_media_task_data) { - - // now 'build' the MediaActor via our worker pool to create - // MediaSources and add them - build_tasks_in_flight_++; - - // spawn a media actor - build_media_task_data->media_actor_ = spawn( - build_media_task_data->media_name_, - build_media_task_data->media_uuid_, - UuidActorVector()); - UuidActor ua(build_media_task_data->media_uuid_, build_media_task_data->media_actor_); - - // this is called when we get a result back - keeps track of the number - // of jobs being processed and sends a message to self to continue working - // through the queue - auto continue_processing_job_queue = [=]() { - build_tasks_in_flight_--; - delayed_send(this, JOB_DISPATCH_DELAY, playlist::add_media_atom_v); - if (build_media_task_data->event_msg_) { - build_media_task_data->event_msg_->increment_progress(); - event::send_event(this, *(build_media_task_data->event_msg_)); - } - }; - - // now we get our worker pool to build media sources and add them to the - // parent MediaActor using the shotgun query data - request( - pool_, - caf::infinite, - playlist::add_media_atom_v, - build_media_task_data->media_actor_, - build_media_task_data->sg_data_, - build_media_task_data->media_rate_) - .then( - - [=](bool) { - // media sources were constructed successfully - now we can add to - // the playlist, we pass in the overall ordered list of uuids that - // we are building so the playlist can ensure everything is added - // in order, even if they aren't created in the correct order - request( - build_media_task_data->playlist_actor_, - caf::infinite, - playlist::add_media_atom_v, - ua, - *(build_media_task_data->ordererd_uuids_), - build_media_task_data->before_) - .then( - - [=](const UuidActor &) { - if (!build_media_task_data->flag_colour_.empty()) { - anon_send( - build_media_task_data->media_actor_, - playlist::reflag_container_atom_v, - std::make_tuple( - std::optional( - build_media_task_data->flag_colour_), - std::optional( - build_media_task_data->flag_text_))); - } - - extend_media_with_ivy_tasks_.emplace_back(build_media_task_data); - continue_processing_job_queue(); - }, - [=](const caf::error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - continue_processing_job_queue(); - }); - }, - [=](const caf::error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - continue_processing_job_queue(); - }); -} - -template -void ShotgunDataSourceActor::do_add_media_sources_from_ivy( - std::shared_ptr ivy_media_task_data) { - - auto ivy = system().registry().template get("IVYDATASOURCE"); - build_tasks_in_flight_++; - - // this is called when we get a result back - keeps track of the number - // of jobs being processed and sends a message to self to continue working - // through the queue - auto continue_processing_job_queue = [=]() { - build_tasks_in_flight_--; - delayed_send(this, JOB_DISPATCH_DELAY, playlist::add_media_atom_v); - /* Commented out bevause we're not including ivy leaf addition - in progress indicator now. - if (ivy_media_task_data->event_msg) { - ivy_media_task_data->event_msg->increment_progress(); - event::send_event(this, *(ivy_media_task_data->event_msg)); - }*/ - }; - - - auto good_sources = std::make_shared(); - auto count = std::make_shared(0); - - // this function adds the sources that are 'good' (i.e. were able - // to acquire MediaDetail) to the MediaActor - we only call it - // when we've fully 'built' each MediaSourceActor in our 'sources' - // list -0 see the request/then handler below where it is used - auto finalise = [=]() { - request( - ivy_media_task_data->media_actor_, - infinite, - media::add_media_source_atom_v, - *good_sources) - .then( - [=](const bool) { - // media sources all in media actor. - // we can now select the ones we want.. - anon_send( - ivy_media_task_data->media_actor_, - playhead::media_source_atom_v, - ivy_media_task_data->preferred_visual_source_, - media::MT_IMAGE, - true); - - anon_send( - ivy_media_task_data->media_actor_, - playhead::media_source_atom_v, - ivy_media_task_data->preferred_audio_source_, - media::MT_AUDIO, - true); - - continue_processing_job_queue(); - }, - [=](error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - continue_processing_job_queue(); - }); - }; - - // here we get the ivy data source to fetch sources (ivy leafs) using the - // ivy dnuuid for the MediaActor already created from shotgun data - request( - ivy, - infinite, - use_data_atom_v, - ivy_media_task_data->sg_data_.at("attributes").at("sg_project_name").get(), - utility::Uuid(ivy_media_task_data->sg_data_.at("attributes") - .at("sg_ivy_dnuuid") - .get()), - ivy_media_task_data->media_rate_) - .then( - [=](const utility::UuidActorVector &sources) { - // we want to make sure the 'MediaDetail' has been fetched on the - // sources before adding to the parent MediaActor - this means we - // don't build up a massive queue of IO heavy MediaDetail fetches - // but instead deal with them sequentially as each media item is - // added to the playlist - - if (sources.empty()) { - finalise(); - } else { - *count = sources.size(); - } - - for (auto source : sources) { - - // we need to get each source to get its detail to ensure that - // it is readable/valid - request( - source.actor(), - infinite, - media::acquire_media_detail_atom_v, - ivy_media_task_data->media_rate_) - .then( - [=](bool got_media_detail) mutable { - if (got_media_detail) - good_sources->push_back(source); - else - send_exit(source.actor(), caf::exit_reason::user_shutdown); - - (*count)--; - if (!(*count)) - finalise(); - }, - [=](error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - - // kill bad source. - send_exit(source.actor(), caf::exit_reason::user_shutdown); - - (*count)--; - if (!(*count)) - finalise(); - }); - } - }, - - [=](error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - continue_processing_job_queue(); - }); -} - -extern "C" { -plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { - return new plugin_manager::PluginFactoryCollection( - std::vector>( - {std::make_shared>>( - Uuid("33201f8d-db32-4278-9c40-8c068372a304"), - "Shotgun", - "DNeg", - "Shotgun Data Source", - semver::version("1.0.0"), - "import Shotgun 1.0; ShotgunRoot {}" - // "import Shotgun 1.0; ShotgunMenu {}" - )})); -} -} diff --git a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.hpp b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.hpp deleted file mode 100644 index 35b6556f1..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.hpp +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include - -#include "xstudio/data_source/data_source.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/frame_rate.hpp" -#include "xstudio/utility/managed_dir.hpp" -#include "xstudio/module/module.hpp" - -#include "data_source_shotgun_base.hpp" - -using namespace xstudio; -using namespace xstudio::data_source; - -namespace xstudio::shotgun_client { -class AuthenticateShotgun; -} - -class BuildPlaylistMediaJob; - -template class ShotgunDataSourceActor : public caf::event_based_actor { - public: - ShotgunDataSourceActor( - caf::actor_config &cfg, const utility::JsonStore & = utility::JsonStore()); - - caf::behavior make_behavior() override { - return data_source_.message_handler().or_else(behavior_); - } - void update_preferences(const utility::JsonStore &js); - void on_exit() override; - - private: - void attribute_changed(const utility::Uuid &attr_uuid); - void update_playlist_versions( - caf::typed_response_promise rp, - const utility::Uuid &playlist_uuid, - const int playlist_id = 0); - void refresh_playlist_versions( - caf::typed_response_promise rp, const utility::Uuid &playlist_uuid); - // void refresh_playlist_notes(caf::typed_response_promise rp, const - // utility::Uuid &playlist_uuid); - void create_playlist( - caf::typed_response_promise rp, const utility::JsonStore &js); - - void - create_tag(caf::typed_response_promise rp, const std::string &value); - - void rename_tag( - caf::typed_response_promise rp, - const int tag_id, - const std::string &value); - - void add_entity_tag( - caf::typed_response_promise rp, - const std::string &entity, - const int entity_id, - const int tag_id); - - void remove_entity_tag( - caf::typed_response_promise rp, - const std::string &entity, - const int entity_id, - const int tag_id); - - void prepare_playlist_notes( - caf::typed_response_promise rp, - const utility::Uuid &playlist_uuid, - const utility::UuidVector &media_uuids = {}, - const bool notify_owner = false, - const std::vector notify_group_ids = {}, - const bool combine = false, - const bool add_time = false, - const bool add_playlist_name = false, - const bool add_type = false, - const bool anno_requires_note = true, - const bool skip_already_pubished = false, - const std::string &default_type = ""); - - void create_playlist_notes( - caf::typed_response_promise rp, - const utility::JsonStore ¬es, - const utility::Uuid &playlist_uuid); - void load_playlist( - caf::typed_response_promise rp, - const int playlist_id, - const caf::actor &session); - - // void update_playlist_notes(caf::typed_response_promise rp, const - // utility::Uuid &playlist_uuid, const utility::JsonStore &js); void - // add_media_to_playlist(caf::typed_response_promise> rp, - // const utility::JsonStore &data, - // const utility::Uuid &playlist_uuid, - // const utility::Uuid &before - // ); - void add_media_to_playlist( - caf::typed_response_promise> rp, - const utility::JsonStore &data, - utility::Uuid playlist_uuid, - caf::actor playlist, - const utility::Uuid &before); - void get_valid_media_count( - caf::typed_response_promise rp, const utility::Uuid &uuid); - void - link_media(caf::typed_response_promise rp, const utility::Uuid &uuid); - - void download_media( - caf::typed_response_promise rp, const utility::Uuid &uuid); - - void find_ivy_version( - caf::typed_response_promise rp, - const std::string &uuid, - const std::string &job); - void find_shot(caf::typed_response_promise rp, const int shot_id); - - std::shared_ptr get_next_build_task(bool &is_ivy_build_task); - void do_add_media_sources_from_shotgun(std::shared_ptr); - void do_add_media_sources_from_ivy(std::shared_ptr); - - void execute_query( - caf::typed_response_promise rp, const utility::JsonStore &action); - - void put_action( - caf::typed_response_promise rp, const utility::JsonStore &action); - - void use_action( - caf::typed_response_promise rp, const utility::JsonStore &action); - - void use_action( - caf::typed_response_promise rp, - const utility::JsonStore &action, - const caf::actor &session); - - void use_action( - caf::typed_response_promise rp, - const caf::uri &uri, - const utility::FrameRate &media_rate); - - void get_action( - caf::typed_response_promise rp, const utility::JsonStore &action); - - void post_action( - caf::typed_response_promise rp, const utility::JsonStore &action); - - - private: - caf::behavior behavior_; - T data_source_; - caf::actor shotgun_; - caf::actor pool_; - size_t changed_hash_{0}; - caf::actor_addr secret_source_; - std::vector> waiting_; - utility::Uuid uuid_ = {utility::Uuid::generate()}; - std::map category_colours_; - - std::deque> build_playlist_media_tasks_; - std::deque> extend_media_with_ivy_tasks_; - int build_tasks_in_flight_ = {0}; - int worker_count_ = {8}; - - bool disable_integration_ {false}; - - std::map shot_cache_; - - utility::ManagedDir download_cache_; -}; diff --git a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.tcc b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.tcc deleted file mode 100644 index d5f87bf20..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun.tcc +++ /dev/null @@ -1,1188 +0,0 @@ - -#include -#include - -#include "data_source_shotgun.hpp" -#include "data_source_shotgun_worker.hpp" -#include "data_source_shotgun_definitions.hpp" - -#include "xstudio/atoms.hpp" -#include "xstudio/bookmark/bookmark.hpp" -#include "xstudio/event/event.hpp" -#include "xstudio/global_store/global_store.hpp" -#include "xstudio/media/media_actor.hpp" -#include "xstudio/playlist/playlist_actor.hpp" -#include "xstudio/shotgun_client/shotgun_client.hpp" -#include "xstudio/shotgun_client/shotgun_client_actor.hpp" -#include "xstudio/tag/tag.hpp" -#include "xstudio/thumbnail/thumbnail.hpp" -#include "xstudio/utility/chrono.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/uuid.hpp" - -using namespace xstudio; -using namespace xstudio::shotgun_client; -using namespace xstudio::utility; -using namespace xstudio::global_store; -using namespace std::chrono_literals; - - -/*CAF_BEGIN_TYPE_ID_BLOCK(shotgun, xstudio::shotgun_client::shotgun_client_error) -CAF_ADD_ATOM(shotgun, xstudio::shotgun_client, test_atom) -CAF_END_TYPE_ID_BLOCK(shotgun)*/ - -// Datasource should support a common subset of operations that apply to multiple datasources. -// not idea what they are though. -// get and put should try and map from this to the relevant sources. - -// shotgun piggy backs on the shotgun client actor, so most of the work is done in the actor -// class. because shotgun is very flexible, it's hard to write helpers, as entities/properties -// are entirely configurable. but we also don't want to put all the logic into the frontend. as -// python module may want access to this logic. - -// This value helps tune the rate that jobs to build media are processed, if it -// is zero xstudio tends to get overwhelmed when building large playlists, increasing -// the value means xstudio stays interactive at the cost of slowing the overall -#define JOB_DISPATCH_DELAY std::chrono::milliseconds(10) - -#include "data_source_shotgun_action.tcc" -#include "data_source_shotgun_get_actions.tcc" -#include "data_source_shotgun_put_actions.tcc" -#include "data_source_shotgun_post_actions.tcc" - -template -void ShotgunDataSourceActor::attribute_changed(const utility::Uuid &attr_uuid) { - // properties changed somewhere. - // update loop ? - if (attr_uuid == data_source_.authentication_method_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.authentication_method_->value(), - "/plugin/data_source/shotgun/authentication/grant_type"); - } - if (attr_uuid == data_source_.client_id_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.client_id_->value(), - "/plugin/data_source/shotgun/authentication/client_id"); - } - // if (attr_uuid == data_source_.client_secret_->uuid()) { - // auto prefs = GlobalStoreHelper(system()); - // prefs.set_value(data_source_.client_secret_->value(), - // "/plugin/data_source/shotgun/authentication/client_secret"); - // } - if (attr_uuid == data_source_.timeout_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.timeout_->value(), "/plugin/data_source/shotgun/server/timeout"); - } - - if (attr_uuid == data_source_.username_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.username_->value(), - "/plugin/data_source/shotgun/authentication/username"); - } - // if (attr_uuid == data_source_.password_->uuid()) { - // auto prefs = GlobalStoreHelper(system()); - // prefs.set_value(data_source_.password_->value(), - // "/plugin/data_source/shotgun/authentication/password"); - // } - if (attr_uuid == data_source_.session_token_->uuid()) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - data_source_.session_token_->value(), - "/plugin/data_source/shotgun/authentication/session_token"); - } -} - - -template -ShotgunDataSourceActor::ShotgunDataSourceActor( - caf::actor_config &cfg, const utility::JsonStore &) - : caf::event_based_actor(cfg) { - - data_source_.bind_attribute_changed_callback( - [this](auto &&PH1) { attribute_changed(std::forward(PH1)); }); - - spdlog::debug("Created ShotgunDataSourceActor {}", name()); - // print_on_exit(this, "MediaHookActor"); - secret_source_ = actor_cast(this); - - shotgun_ = spawn(); - link_to(shotgun_); - - // we need to recieve authentication updates. - join_event_group(this, shotgun_); - - // we are the source of the secret.. - anon_send(shotgun_, shotgun_authentication_source_atom_v, actor_cast(this)); - - - try { - auto prefs = GlobalStoreHelper(system()); - JsonStore j; - join_broadcast(this, prefs.get_group(j)); - update_preferences(j); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - if(not disable_integration_) - system().registry().put(shotgun_datasource_registry, caf::actor_cast(this)); - - pool_ = caf::actor_pool::make( - system().dummy_execution_unit(), - worker_count_, - [&] { - return system().template spawn( - actor_cast(this)); - }, - caf::actor_pool::round_robin()); - link_to(pool_); - - // data_source_.connect_to_ui(); coz async - data_source_.set_parent_actor_addr(actor_cast(this)); - delayed_anon_send( - caf::actor_cast(this), - std::chrono::milliseconds(500), - module::connect_to_ui_atom_v); - - behavior_.assign( - [=](utility::name_atom) -> std::string { return name(); }, - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - - [=](shotgun_projects_atom atom) { delegate(shotgun_, atom); }, - - [=](shotgun_groups_atom atom, const int project_id) { - delegate(shotgun_, atom, project_id); - }, - - [=](shotgun_schema_atom atom, const int project_id) { - delegate(shotgun_, atom, project_id); - }, - - [=](shotgun_authentication_source_atom, caf::actor source) { - secret_source_ = actor_cast(source); - }, - - [=](shotgun_authentication_source_atom) -> caf::actor { - return actor_cast(secret_source_); - }, - - [=](shotgun_update_entity_atom atom, - const std::string &entity, - const int record_id, - const JsonStore &body) { delegate(shotgun_, atom, entity, record_id, body); }, - - [=](shotgun_image_atom atom, - const std::string &entity, - const int record_id, - const bool thumbnail) { delegate(shotgun_, atom, entity, record_id, thumbnail); }, - - [=](shotgun_delete_entity_atom atom, const std::string &entity, const int record_id) { - delegate(shotgun_, atom, entity, record_id); - }, - - [=](shotgun_image_atom atom, - const std::string &entity, - const int record_id, - const bool thumbnail, - const bool as_buffer) { - delegate(shotgun_, atom, entity, record_id, thumbnail, as_buffer); - }, - - [=](shotgun_upload_atom atom, - const std::string &entity, - const int record_id, - const std::string &field, - const std::string &name, - const std::vector &data, - const std::string &content_type) { - delegate(shotgun_, atom, entity, record_id, field, name, data, content_type); - }, - - // just use the default with jsonstore ? - [=](put_data_atom, const utility::JsonStore &js) -> result { - auto rp = make_response_promise(); - put_action(rp, js); - return rp; - }, - - [=](data_source::use_data_atom, const caf::actor &media, const FrameRate &media_rate) - -> result { return UuidActorVector(); }, - - // no drop support.. - [=](data_source::use_data_atom, const JsonStore &, const FrameRate &, const bool) - -> UuidActorVector { return UuidActorVector(); }, - - // do we need the UI to have spun up before we can issue calls to shotgun... - // erm... - [=](use_data_atom atom, const caf::uri &uri) -> result { - auto rp = make_response_promise(); - use_action(rp, uri, FrameRate()); - return rp; - }, - - [=](use_data_atom, - const caf::uri &uri, - const FrameRate &media_rate) -> result { - auto rp = make_response_promise(); - use_action(rp, uri, media_rate); - return rp; - }, - - [=](use_data_atom, - const utility::JsonStore &js, - const caf::actor &session) -> result { - auto rp = make_response_promise(); - use_action(rp, js, session); - return rp; - }, - - // just use the default with jsonstore ? - [=](use_data_atom, const utility::JsonStore &js) -> result { - auto rp = make_response_promise(); - use_action(rp, js); - return rp; - }, - - // just use the default with jsonstore ? - - [=](post_data_atom, const utility::JsonStore &js) -> result { - auto rp = make_response_promise(); - post_action(rp, js); - return rp; - }, - - [=](shotgun_entity_atom atom, - const std::string &entity, - const int record_id, - const std::vector &fields) { - delegate(shotgun_, atom, entity, record_id, fields); - }, - - [=](shotgun_entity_filter_atom atom, - const std::string &entity, - const JsonStore &filter, - const std::vector &fields, - const std::vector &sort) { - delegate(shotgun_, atom, entity, filter, fields, sort); - }, - - [=](shotgun_entity_filter_atom atom, - const std::string &entity, - const JsonStore &filter, - const std::vector &fields, - const std::vector &sort, - const int page, - const int page_size) { - delegate(shotgun_, atom, entity, filter, fields, sort, page, page_size); - }, - - [=](shotgun_schema_entity_fields_atom atom, - const std::string &entity, - const std::string &field, - const int id) { delegate(shotgun_, atom, entity, field, id); }, - - [=](shotgun_entity_search_atom atom, - const std::string &entity, - const JsonStore &conditions, - const std::vector &fields, - const std::vector &sort, - const int page, - const int page_size) { - delegate(shotgun_, atom, entity, conditions, fields, sort, page, page_size); - }, - - [=](shotgun_text_search_atom atom, - const std::string &text, - const JsonStore &conditions, - const int page, - const int page_size) { - delegate(shotgun_, atom, text, conditions, page, page_size); - }, - - // can't reply via qt mixin.. this is a work around.. - [=](shotgun_acquire_authentication_atom, const bool cancelled) { - if (cancelled) { - data_source_.set_authenticated(false); - for (auto &i : waiting_) - i.deliver( - make_error(xstudio_error::error, "Authentication request cancelled.")); - } else { - auto auth = data_source_.get_authentication(); - if (waiting_.empty()) { - anon_send(shotgun_, shotgun_authenticate_atom_v, auth); - } else { - for (auto &i : waiting_) - i.deliver(auth); - } - } - waiting_.clear(); - }, - - [=](shotgun_acquire_authentication_atom atom, - const std::string &message) -> result { - if (secret_source_ == actor_cast(this)) - return make_error(xstudio_error::error, "No authentication source."); - - auto rp = make_response_promise(); - waiting_.push_back(rp); - data_source_.set_authenticated(false); - anon_send(actor_cast(secret_source_), atom, message); - return rp; - }, - - [=](utility::event_atom, - shotgun_acquire_token_atom, - const std::pair &tokens) { - auto prefs = GlobalStoreHelper(system()); - prefs.set_value( - tokens.second, - "/plugin/data_source/shotgun/authentication/refresh_token", - false); - prefs.save("APPLICATION"); - data_source_.set_authenticated(true); - }, - - [=](playlist::add_media_atom, - const utility::JsonStore &data, - const utility::Uuid &playlist_uuid, - const caf::actor &playlist, - const utility::Uuid &before) -> result> { - auto rp = make_response_promise>(); - add_media_to_playlist(rp, data, playlist_uuid, playlist, before); - return rp; - }, - - [=](playlist::add_media_atom) { - // this message handler is called in a loop until all build media - // tasks in the queue are exhausted - - bool is_ivy_build_task; - - auto build_media_task_data = get_next_build_task(is_ivy_build_task); - while (build_media_task_data) { - - if (is_ivy_build_task) { - - do_add_media_sources_from_ivy(build_media_task_data); - - } else { - - do_add_media_sources_from_shotgun(build_media_task_data); - } - - // N.B. we only get a new build task if the number of incomplete tasks - // already dispatched is less than the number of actors in our - // worker pool - build_media_task_data = get_next_build_task(is_ivy_build_task); - } - }, - - [=](get_data_atom, const utility::JsonStore &js) -> result { - auto rp = make_response_promise(); - get_action(rp, js); - return rp; - }, - - [=](json_store::update_atom, - const JsonStore & /*change*/, - const std::string & /*path*/, - const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); - }, - - [=](json_store::update_atom, const JsonStore &js) { - try { - update_preferences(js); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - }); -} - -template void ShotgunDataSourceActor::on_exit() { - // maybe on timer.. ? - for (auto &i : waiting_) - i.deliver(make_error(xstudio_error::error, "Password request cancelled.")); - waiting_.clear(); - system().registry().erase(shotgun_datasource_registry); -} - -template void ShotgunDataSourceActor::update_preferences(const JsonStore &js) { - try { - auto grant = preference_value( - js, "/plugin/data_source/shotgun/authentication/grant_type"); - - auto client_id = preference_value( - js, "/plugin/data_source/shotgun/authentication/client_id"); - auto client_secret = preference_value( - js, "/plugin/data_source/shotgun/authentication/client_secret"); - auto username = preference_value( - js, "/plugin/data_source/shotgun/authentication/username"); - auto password = preference_value( - js, "/plugin/data_source/shotgun/authentication/password"); - auto session_token = preference_value( - js, "/plugin/data_source/shotgun/authentication/session_token"); - - auto refresh_token = preference_value( - js, "/plugin/data_source/shotgun/authentication/refresh_token"); - - auto host = - preference_value(js, "/plugin/data_source/shotgun/server/host"); - auto port = preference_value(js, "/plugin/data_source/shotgun/server/port"); - auto protocol = - preference_value(js, "/plugin/data_source/shotgun/server/protocol"); - auto timeout = preference_value(js, "/plugin/data_source/shotgun/server/timeout"); - - - auto cache_dir = expand_envvars( - preference_value(js, "/plugin/data_source/shotgun/download/path")); - auto cache_size = - preference_value(js, "/plugin/data_source/shotgun/download/size"); - - auto disable_integration = - preference_value(js, "/plugin/data_source/shotgun/disable_integration"); - - if(disable_integration_ != disable_integration) { - disable_integration_ = disable_integration; - if(disable_integration_) - system().registry().erase(shotgun_datasource_registry); - else - system().registry().put(shotgun_datasource_registry, caf::actor_cast(this)); - } - - download_cache_.prune_on_exit(true); - download_cache_.target(cache_dir, true); - download_cache_.max_size(cache_size * 1024 * 1024 * 1024); - - auto category = preference_value(js, "/core/bookmark/category"); - category_colours_.clear(); - if (category.is_array()) { - for (const auto &i : category) { - category_colours_[i.value("value", "default")] = i.value("colour", ""); - } - } - - // no op ? - data_source_.set_authentication_method(grant); - data_source_.set_client_id(client_id); - data_source_.set_client_secret(client_secret); - data_source_.set_username(expand_envvars(username)); - data_source_.set_password(password); - data_source_.set_session_token(session_token); - data_source_.set_timeout(timeout); - - // what hppens if we get a sequence of changes... should this be on a timed event ? - // watch out for multiple instances. - auto new_hash = std::hash{}( - grant + username + client_id + host + std::to_string(port) + protocol); - - if (new_hash != changed_hash_) { - changed_hash_ = new_hash; - // set server - anon_send( - shotgun_, - shotgun_host_atom_v, - std::string(fmt::format( - "{}://{}{}", protocol, host, (port ? ":" + std::to_string(port) : "")))); - - auto auth = data_source_.get_authentication(); - if (not refresh_token.empty()) - auth.set_refresh_token(refresh_token); - - anon_send(shotgun_, shotgun_credential_atom_v, auth); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } -} - -template -void ShotgunDataSourceActor::refresh_playlist_versions( - caf::typed_response_promise rp, const utility::Uuid &playlist_uuid) { - // grab playlist id, get versions compare/load into playlist - try { - - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - - auto plsg = request_receive( - *sys, playlist, json_store::get_json_atom_v, ShotgunMetadataPath + "/playlist"); - - auto pl_id = plsg["id"].template get(); - - // this is a list of the media.. - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - - // foreach media actor get it's shogtun metadata. - std::set current_version_ids; - - for (const auto &i : media) { - try { - auto mjson = request_receive( - *sys, - i.actor(), - json_store::get_json_atom_v, - utility::Uuid(), - ShotgunMetadataPath + "/version"); - current_version_ids.insert(mjson["id"].template get()); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - // we got media shotgun ids, plus playlist id - // get current shotgun playlist/versions - request( - caf::actor_cast(this), - infinite, - shotgun_entity_atom_v, - "Playlists", - pl_id, - std::vector()) - .then( - [=](const JsonStore &result) mutable { - try { - scoped_actor sys{system()}; - // update playlist - anon_send( - playlist, - json_store::set_json_atom_v, - JsonStore(result["data"]), - ShotgunMetadataPath + "/playlist"); - - // gather versions, to get more detail.. - std::vector version_ids; - for (const auto &i : - result.at("data").at("relationships").at("versions").at("data")) { - if (not current_version_ids.count(i.at("id").template get())) - version_ids.emplace_back( - std::to_string(i.at("id").template get())); - } - - if (version_ids.empty()) { - rp.deliver(result); - return; - } - - auto query = R"({})"_json; - query["id"] = join_as_string(version_ids, ","); - - // get details.. - request( - caf::actor_cast(this), - infinite, - shotgun_entity_filter_atom_v, - "Versions", - JsonStore(query), - VersionFields, - std::vector(), - 1, - 1000) - .then( - [=](const JsonStore &result2) mutable { - try { - // got version details. - // we can now just call add versions to playlist.. - anon_send( - caf::actor_cast(this), - playlist::add_media_atom_v, - result2, - playlist_uuid, - playlist, - utility::Uuid()); - - // return this as the result. - rp.deliver(result); - - } catch (const std::exception &err) { - rp.deliver( - make_error(xstudio_error::error, err.what())); - } - }, - - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - - - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::add_media_to_playlist( - caf::typed_response_promise rp, - const utility::JsonStore &data, - utility::Uuid playlist_uuid, - caf::actor playlist, - const utility::Uuid &before) { - // data can be in multiple forms.. - - auto sys = caf::scoped_actor(system()); - - nlohmann::json versions; - - try { - versions = data.at("data").at("relationships").at("versions").at("data"); - } catch (...) { - try { - versions = data.at("data"); - } catch (...) { - try { - versions = data.at("result").at("data"); - } catch (...) { - return rp.deliver(make_error(xstudio_error::error, "Invalid JSON")); - } - } - } - - if (versions.empty()) - return rp.deliver(std::vector()); - - auto event_msg = std::shared_ptr(); - - - // get uuid for playlist - if (playlist and playlist_uuid.is_null()) { - try { - playlist_uuid = - request_receive(*sys, playlist, utility::uuid_atom_v); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - playlist = caf::actor(); - } - } - - // get playlist for uuid - if (not playlist and not playlist_uuid.is_null()) { - try { - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - playlist_uuid = utility::Uuid(); - } - } - - // create playlist.. - if (not playlist and playlist_uuid.is_null()) { - try { - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - playlist_uuid = utility::Uuid::generate(); - playlist = spawn("ShotGrid Media", playlist_uuid, session); - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - if (not playlist_uuid.is_null()) { - event_msg = std::make_shared( - "Loading ShotGrid Playlist Media {}", - 0, - 0, - versions.size(), // we increment progress once per version loaded - ivy leafs are - // added after progress hits 100% - std::set({playlist_uuid})); - event::send_event(this, *event_msg); - } - - try { - auto media_rate = - request_receive(*sys, playlist, session::media_rate_atom_v); - - std::string flag_text, flag_colour; - if (data.contains(json::json_pointer("/context/flag_text")) and not data.at("context").value("flag_text", "").empty() and - not data.at("context").value("flag_colour", "").empty()) { - flag_colour = data.at("context").value("flag_colour", ""); - flag_text = data.at("context").value("flag_text", ""); - } - - std::string visual_source; - if (data.contains(json::json_pointer("/context/visual_source"))) { - visual_source = data.at("context").value("visual_source", ""); - } - - std::string audio_source; - if (data.contains(json::json_pointer("/context/audio_source"))) { - audio_source = data.at("context").value("audio_source", ""); - } - - // we need to ensure that media are added to playlist IN ORDER - this - // is a bit fiddly because media are created out of order by the worker - // pool so we use this utility::UuidList to ensure that the playlist builds - // with media in order - auto ordered_uuids = std::make_shared(); - auto result = std::make_shared(); - auto result_count = std::make_shared(0); - - // get a new media item created for each of the names in our list - for (const auto &i : versions) { - - std::string name(i.at("attributes").at("code")); - - // create a task data item, with the raw shotgun data that - // can be used to build the media sources for each media - // item in the playlist - ordered_uuids->push_back(utility::Uuid::generate()); - build_playlist_media_tasks_.emplace_back(std::make_shared( - playlist, - ordered_uuids->back(), - name, // name for the media - JsonStore(i), - media_rate, - visual_source, - audio_source, - event_msg, - ordered_uuids, - before, - flag_colour, - flag_text, - rp, - result, - result_count)); - } - - // this call starts the work of building the media and consuming - // the jobs in the 'build_playlist_media_tasks_' queue - send(this, playlist::add_media_atom_v); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - if (not playlist_uuid.is_null()) { - event_msg->set_complete(); - event::send_event(this, *event_msg); - } - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::load_playlist( - caf::typed_response_promise rp, - const int playlist_id, - const caf::actor &session) { - - // this is going to get nesty :() - - // get playlist from shotgun - request( - caf::actor_cast(this), - infinite, - shotgun_entity_atom_v, - "Playlists", - playlist_id, - std::vector()) - .then( - [=](JsonStore pljs) mutable { - // got playlist. - // we can create an new xstudio playlist actor at this point.. - auto playlist = UuidActor(); - try { - if (session) { - scoped_actor sys{system()}; - - auto tmp = request_receive>( - *sys, - session, - session::add_playlist_atom_v, - pljs["data"]["attributes"]["code"].get(), - utility::Uuid(), - false); - - playlist = tmp.second; - - } else { - auto uuid = utility::Uuid::generate(); - auto tmp = spawn( - pljs["data"]["attributes"]["code"].get(), uuid); - playlist = UuidActor(uuid, tmp); - } - - // place holder for shotgun decorators. - anon_send( - playlist.actor(), - json_store::set_json_atom_v, - JsonStore(), - "/metadata/shotgun"); - // should really be driven from back end not UI.. - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } - - // get version order - auto order_filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["playlist", "is", {"type":"Playlist", "id":0}] - ] - })"_json; - - order_filter["conditions"][0][2]["id"] = playlist_id; - - request( - caf::actor_cast(this), - infinite, - shotgun_entity_search_atom_v, - "PlaylistVersionConnection", - JsonStore(order_filter), - std::vector({"sg_sort_order", "version"}), - std::vector({"sg_sort_order"}), - 1, - 4999) - .then( - [=](const JsonStore &order) mutable { - std::vector version_ids; - for (const auto &i : order["data"]) - version_ids.emplace_back(std::to_string( - i["relationships"]["version"]["data"].at("id").get())); - - if (version_ids.empty()) - return rp.deliver( - make_error(xstudio_error::error, "No Versions found")); - - // get versions - auto query = R"({})"_json; - query["id"] = join_as_string(version_ids, ","); - - // get versions ordered by playlist. - request( - caf::actor_cast(this), - infinite, - shotgun_entity_filter_atom_v, - "Versions", - JsonStore(query), - VersionFields, - std::vector(), - 1, - 4999) - .then( - [=](JsonStore &js) mutable { - // munge it.. - auto data = R"([])"_json; - - for (const auto &i : version_ids) { - for (auto &j : js["data"]) { - - // spdlog::warn("{} {}", - // std::to_string(j["id"].get()), i); - if (std::to_string(j["id"].get()) == i) { - data.push_back(j); - break; - } - } - } - - js["data"] = data; - - // add back in - pljs["data"]["relationships"]["versions"] = js; - - // spdlog::warn("{}",pljs.dump(2)); - // now we have a playlist json struct with the versions - // corrrecly ordered, set metadata on playlist.. - anon_send( - playlist.actor(), - json_store::set_json_atom_v, - JsonStore(pljs["data"]), - ShotgunMetadataPath + "/playlist"); - - // addDecorator(playlist.uuid) - // addMenusFull(playlist.uuid) - - anon_send( - caf::actor_cast(this), - playlist::add_media_atom_v, - pljs, - playlist.uuid(), - playlist.actor(), - utility::Uuid()); - - rp.deliver(playlist); - }, - [=](error &err) mutable { - spdlog::error( - "{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver( - make_error(xstudio_error::error, to_string(err))); - }); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(make_error(xstudio_error::error, to_string(err))); - }); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(make_error(xstudio_error::error, to_string(err))); - }); -} - -template -std::shared_ptr -ShotgunDataSourceActor::get_next_build_task(bool &is_ivy_build_task) { - - std::shared_ptr job_info; - // if we already have popped N jobs off the queue that haven't completed - // and N >= worker_count_ we don't pop a job off and instead return a null - // - if (build_tasks_in_flight_ < worker_count_) { - if (!build_playlist_media_tasks_.empty()) { - is_ivy_build_task = false; - job_info = build_playlist_media_tasks_.front(); - build_playlist_media_tasks_.pop_front(); - } else if (!extend_media_with_ivy_tasks_.empty()) { - is_ivy_build_task = true; - job_info = extend_media_with_ivy_tasks_.front(); - extend_media_with_ivy_tasks_.pop_front(); - } - } - return job_info; -} - -template -void ShotgunDataSourceActor::do_add_media_sources_from_shotgun( - std::shared_ptr build_media_task_data) { - - // now 'build' the MediaActor via our worker pool to create - // MediaSources and add them - build_tasks_in_flight_++; - - // spawn a media actor - build_media_task_data->media_actor_ = spawn( - build_media_task_data->media_name_, - build_media_task_data->media_uuid_, - UuidActorVector()); - UuidActor ua(build_media_task_data->media_uuid_, build_media_task_data->media_actor_); - - // this is called when we get a result back - keeps track of the number - // of jobs being processed and sends a message to self to continue working - // through the queue - auto continue_processing_job_queue = [=]() { - build_tasks_in_flight_--; - delayed_send(this, JOB_DISPATCH_DELAY, playlist::add_media_atom_v); - if (build_media_task_data->event_msg_) { - build_media_task_data->event_msg_->increment_progress(); - event::send_event(this, *(build_media_task_data->event_msg_)); - } - }; - - // now we get our worker pool to build media sources and add them to the - // parent MediaActor using the shotgun query data - request( - pool_, - caf::infinite, - playlist::add_media_atom_v, - build_media_task_data->media_actor_, - build_media_task_data->sg_data_, - build_media_task_data->media_rate_) - .then( - - [=](bool) { - // media sources were constructed successfully - now we can add to - // the playlist, we pass in the overall ordered list of uuids that - // we are building so the playlist can ensure everything is added - // in order, even if they aren't created in the correct order - request( - build_media_task_data->playlist_actor_, - caf::infinite, - playlist::add_media_atom_v, - ua, - *(build_media_task_data->ordererd_uuids_), - build_media_task_data->before_) - .then( - - [=](const UuidActor &) { - if (!build_media_task_data->flag_colour_.empty()) { - anon_send( - build_media_task_data->media_actor_, - playlist::reflag_container_atom_v, - std::make_tuple( - std::optional( - build_media_task_data->flag_colour_), - std::optional( - build_media_task_data->flag_text_))); - } - - extend_media_with_ivy_tasks_.emplace_back(build_media_task_data); - continue_processing_job_queue(); - }, - [=](const caf::error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - continue_processing_job_queue(); - }); - }, - [=](const caf::error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - continue_processing_job_queue(); - }); -} - -template -void ShotgunDataSourceActor::do_add_media_sources_from_ivy( - std::shared_ptr ivy_media_task_data) { - - auto ivy = system().registry().template get("IVYDATASOURCE"); - build_tasks_in_flight_++; - - // this is called when we get a result back - keeps track of the number - // of jobs being processed and sends a message to self to continue working - // through the queue - auto continue_processing_job_queue = [=]() { - build_tasks_in_flight_--; - delayed_send(this, JOB_DISPATCH_DELAY, playlist::add_media_atom_v); - /* Commented out bevause we're not including ivy leaf addition - in progress indicator now. - if (ivy_media_task_data->event_msg) { - ivy_media_task_data->event_msg->increment_progress(); - event::send_event(this, *(ivy_media_task_data->event_msg)); - }*/ - }; - - - auto good_sources = std::make_shared(); - auto count = std::make_shared(0); - - // this function adds the sources that are 'good' (i.e. were able - // to acquire MediaDetail) to the MediaActor - we only call it - // when we've fully 'built' each MediaSourceActor in our 'sources' - // list -0 see the request/then handler below where it is used - auto finalise = [=]() { - request( - ivy_media_task_data->media_actor_, - infinite, - media::add_media_source_atom_v, - *good_sources) - .then( - [=](const bool) { - // media sources all in media actor. - // we can now select the ones we want.. - anon_send( - ivy_media_task_data->media_actor_, - playhead::media_source_atom_v, - ivy_media_task_data->preferred_visual_source_, - media::MT_IMAGE, - true); - - anon_send( - ivy_media_task_data->media_actor_, - playhead::media_source_atom_v, - ivy_media_task_data->preferred_audio_source_, - media::MT_AUDIO, - true); - - continue_processing_job_queue(); - }, - [=](error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - continue_processing_job_queue(); - }); - }; - - // here we get the ivy data source to fetch sources (ivy leafs) using the - // ivy dnuuid for the MediaActor already created from shotgun data - try { - request( - ivy, - infinite, - use_data_atom_v, - ivy_media_task_data->sg_data_.at("attributes") - .at("sg_project_name") - .get(), - utility::Uuid(ivy_media_task_data->sg_data_.at("attributes") - .at("sg_ivy_dnuuid") - .get()), - ivy_media_task_data->media_rate_) - .then( - [=](const utility::UuidActorVector &sources) { - // we want to make sure the 'MediaDetail' has been fetched on the - // sources before adding to the parent MediaActor - this means we - // don't build up a massive queue of IO heavy MediaDetail fetches - // but instead deal with them sequentially as each media item is - // added to the playlist - - if (sources.empty()) { - finalise(); - } else { - *count = sources.size(); - } - - for (auto source : sources) { - - // we need to get each source to get its detail to ensure that - // it is readable/valid - request( - source.actor(), - infinite, - media::acquire_media_detail_atom_v, - ivy_media_task_data->media_rate_) - .then( - [=](bool got_media_detail) mutable { - if (got_media_detail) - good_sources->push_back(source); - else - send_exit( - source.actor(), caf::exit_reason::user_shutdown); - - (*count)--; - if (!(*count)) - finalise(); - }, - [=](error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - - // kill bad source. - send_exit(source.actor(), caf::exit_reason::user_shutdown); - - (*count)--; - if (!(*count)) - finalise(); - }); - } - }, - - [=](error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - continue_processing_job_queue(); - }); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - continue_processing_job_queue(); - } -} - diff --git a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_action.tcc b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_action.tcc deleted file mode 100644 index 7d4ec2fb8..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_action.tcc +++ /dev/null @@ -1,292 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -template -void ShotgunDataSourceActor::use_action( - caf::typed_response_promise rp, - const utility::JsonStore &action) { - - try { - auto operation = action.value("operation", ""); - - if (operation == "LoadPlaylist") { - scoped_actor sys{system()}; - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - request( - caf::actor_cast(this), - infinite, - use_data_atom_v, - action, - session) - .then( - [=](const UuidActor &) mutable { - rp.deliver( - JsonStore(R"({"data": {"status": "successful"}})"_json)); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver( - JsonStore(R"({"data": {"status": "successful"}})"_json)); - }); - } else if (operation == "RefreshPlaylist") { - refresh_playlist_versions(rp, Uuid(action.at("playlist_uuid"))); - } else { - rp.deliver(make_error(xstudio_error::error, "Invalid operation.")); - - } - } catch (const std::exception &err) { - rp.deliver( make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what())); - } -} - -template -void ShotgunDataSourceActor::use_action( - caf::typed_response_promise rp, - const utility::JsonStore &action, const caf::actor &session -) { - try { - auto operation = action.value("operation", ""); - - if (operation == "LoadPlaylist") { - load_playlist(rp, action.at("playlist_id").get(), session); - } else { - rp.deliver(make_error(xstudio_error::error, "Invalid operation.")); - } - } catch (const std::exception &err) { - rp.deliver(make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what())); - } -} - -template -void ShotgunDataSourceActor::use_action( - caf::typed_response_promise rp, - const caf::uri &uri, const FrameRate &media_rate -) { - // check protocol == shotgun.. - if (uri.scheme() != "shotgun") - return rp.deliver(UuidActorVector()); - - if (to_string(uri.authority()) == "load") { - // need type and id - auto query = uri.query(); - if (query.count("type") and query["type"] == "Version" and query.count("ids")) { - auto ids = split(query["ids"], '|'); - if (ids.empty()) - rp.deliver( UuidActorVector() ); - - auto count = std::make_shared(ids.size()); - auto results = std::make_shared(); - - for (const auto i : ids) { - try { - auto type = query["type"]; - auto squery = R"({})"_json; - squery["id"] = i; - - request( - caf::actor_cast(this), - std::chrono::seconds( - static_cast(data_source_.timeout_->value())), - shotgun_entity_filter_atom_v, - "Versions", - JsonStore(squery), - VersionFields, - std::vector(), - 1, - 4999) - .then( - [=](const JsonStore &js) mutable { - // load version.. - request( - caf::actor_cast(this), - infinite, - playlist::add_media_atom_v, - js, - utility::Uuid(), - caf::actor(), - utility::Uuid()) - .then( - [=](const UuidActorVector &uav) mutable { - (*count)--; - - for (const auto &ua : uav) - results->push_back(ua); - - if (not(*count)) - rp.deliver(*results); - }, - [=](const caf::error &err) mutable { - (*count)--; - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - if (not(*count)) - rp.deliver(*results); - }); - }, - [=](const caf::error &err) mutable { - spdlog::warn( - "{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - } catch (const std::exception &err) { - (*count)--; - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - } else if ( - query.count("type") and query["type"] == "Playlist" and - query.count("ids")) { - // will return an array of playlist actors.. - auto ids = split(query["ids"], '|'); - if (ids.empty()) - rp.deliver( UuidActorVector()); - - auto count = std::make_shared(ids.size()); - auto results = std::make_shared(); - - for (const auto i : ids) { - auto id = std::atoi(i.c_str()); - auto js = JsonStore(UseLoadPlaylist); - js["playlist_id"] = id; - request( - caf::actor_cast(this), - infinite, - use_data_atom_v, - js, - caf::actor()) - .then( - [=](const UuidActor &ua) mutable { - // process result to build playlist.. - (*count)--; - results->push_back(ua); - if (not(*count)) - rp.deliver(*results); - }, - [=](const caf::error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - (*count)--; - if (not(*count)) - rp.deliver(*results); - }); - } - } else { - spdlog::warn( - "Invalid shotgun action {}, requires type, id", to_string(uri)); - rp.deliver(UuidActorVector()); - } - } else { - spdlog::warn( - "Invalid shotgun action {} {}", to_string(uri.authority()), to_string(uri)); - rp.deliver(UuidActorVector()); - } -} - - -template -void ShotgunDataSourceActor::post_action( - caf::typed_response_promise rp, - const utility::JsonStore &action) { - - try { - auto operation = action.value("operation", ""); - - if(operation == "RenameTag") { - rename_tag(rp, action.at("tag_id"), action.at("value")); - } else if(operation == "CreateTag") { - create_tag(rp, action.at("value")); - } else if(operation == "TagEntity") { - add_entity_tag( - rp, action.at("entity"), action.at("entity_id"), action.at("tag_id")); - } else if(operation == "UnTagEntity") { - remove_entity_tag( - rp, action.at("entity"), action.at("entity_id"), action.at("tag_id")); - } else if(operation == "CreatePlaylist") { - create_playlist(rp, action); - } else if(operation == "CreateNotes") { - create_playlist_notes(rp, action.at("payload"), JsonStore(action.at("playlist_uuid"))); - } else { - rp.deliver(make_error(xstudio_error::error, "Invalid operation.")); - } - - } catch (const std::exception &err) { - rp.deliver( make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what())); - } -} - -template -void ShotgunDataSourceActor::get_action( - caf::typed_response_promise rp, - const utility::JsonStore &action) { - - try { - auto operation = action.value("operation", ""); - - if (operation == "VersionIvyUuid") { - find_ivy_version( - rp, - action.at("ivy_uuid").get(), - action.at("job").get()); - } else if (operation == "GetShotFromId") { - find_shot(rp, action.at("shot_id").get()); - } else if (operation == "LinkMedia") { - link_media(rp, utility::Uuid(action.at("playlist_uuid"))); - } else if (operation == "DownloadMedia") { - download_media(rp, utility::Uuid(action.at("media_uuid"))); - } else if (operation == "MediaCount") { - get_valid_media_count(rp, utility::Uuid(action.at("playlist_uuid"))); - } else if (operation == "PrepareNotes") { - UuidVector media_uuids; - for (const auto &i : action.value("media_uuids", std::vector())) - media_uuids.push_back(Uuid(i)); - - prepare_playlist_notes( - rp, - utility::Uuid(action.at("playlist_uuid")), - media_uuids, - action.value("notify_owner", false), - action.value("notify_group_ids", std::vector()), - action.value("combine", false), - action.value("add_time", false), - action.value("add_playlist_name", false), - action.value("add_type", false), - action.value("anno_requires_note", true), - action.value("skip_already_published", false), - action.value("default_type", "")); - } else if (operation == "Query") { - execute_query(rp, action); - } else { - rp.deliver( - make_error(xstudio_error::error, std::string("Invalid operation."))); - } - } catch (const std::exception &err) { - rp.deliver(make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what())); - } -} - -template -void ShotgunDataSourceActor::put_action( - caf::typed_response_promise rp, - const xstudio::utility::JsonStore &action) { - - try { - auto operation = action.value("operation", ""); - - if (operation == "UpdatePlaylistVersions") { - update_playlist_versions(rp, Uuid(action["playlist_uuid"])); - } else { - rp.deliver(make_error(xstudio_error::error, "Invalid operation.")); - } - } catch (const std::exception &err) { - rp.deliver(make_error( - xstudio_error::error, std::string("Invalid operation.\n") + err.what())); - } -} - diff --git a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_base.cpp b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_base.cpp deleted file mode 100644 index 137a7ab53..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_base.cpp +++ /dev/null @@ -1,179 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include "data_source_shotgun.hpp" - -#include "xstudio/shotgun_client/shotgun_client.hpp" -#include "xstudio/utility/helpers.hpp" - -using namespace xstudio::shotgun_client; -using namespace xstudio::utility; -using namespace xstudio; - -void ShotgunDataSource::set_authentication_method(const std::string &value) { - if (authentication_method_->value() != value) - authentication_method_->set_value(value); -} -void ShotgunDataSource::set_client_id(const std::string &value) { - if (client_id_->value() != value) - client_id_->set_value(value); -} -void ShotgunDataSource::set_client_secret(const std::string &value) { - if (client_secret_->value() != value) - client_secret_->set_value(value); -} -void ShotgunDataSource::set_username(const std::string &value) { - if (username_->value() != value) - username_->set_value(value); -} -void ShotgunDataSource::set_password(const std::string &value) { - if (password_->value() != value) - password_->set_value(value); -} -void ShotgunDataSource::set_session_token(const std::string &value) { - if (session_token_->value() != value) - session_token_->set_value(value); -} -void ShotgunDataSource::set_authenticated(const bool value) { - if (authenticated_->value() != value) - authenticated_->set_value(value); -} -void ShotgunDataSource::set_timeout(const int value) { - if (timeout_->value() != value) - timeout_->set_value(value); -} - -shotgun_client::AuthenticateShotgun ShotgunDataSource::get_authentication() const { - AuthenticateShotgun auth; - - auth.set_session_uuid(to_string(session_id_)); - - auth.set_authentication_method(authentication_method_->value()); - switch (*(auth.authentication_method())) { - case AM_SCRIPT: - auth.set_client_id(client_id_->value()); - auth.set_client_secret(client_secret_->value()); - break; - case AM_SESSION: - auth.set_session_token(session_token_->value()); - break; - case AM_LOGIN: - auth.set_username(expand_envvars(username_->value())); - auth.set_password(password_->value()); - break; - case AM_UNDEFINED: - default: - break; - } - - return auth; -} - -void ShotgunDataSource::add_attributes() { - - std::vector auth_method_names = { - "client_credentials", "password", "session_token"}; - - module::QmlCodeAttribute *button = add_qml_code_attribute( - "MyCode", - R"( -import Shotgun 1.0 -ShotgunButton {} -)"); - - button->set_role_data(module::Attribute::ToolbarPosition, 1010.0); - button->expose_in_ui_attrs_group("media_tools_buttons"); - - - authentication_method_ = add_string_choice_attribute( - "authentication_method", - "authentication_method", - "password", - auth_method_names, - auth_method_names); - - playlist_notes_action_ = - add_action_attribute("playlist_notes_to_shotgun", "playlist_notes_to_shotgun"); - selected_notes_action_ = - add_action_attribute("selected_notes_to_shotgun", "selected_notes_to_shotgun"); - - client_id_ = add_string_attribute("client_id", "client_id", ""); - client_secret_ = add_string_attribute("client_secret", "client_secret", ""); - username_ = add_string_attribute("username", "username", ""); - password_ = add_string_attribute("password", "password", ""); - session_token_ = add_string_attribute("session_token", "session_token", ""); - - authenticated_ = add_boolean_attribute("authenticated", "authenticated", false); - - // should be int.. - timeout_ = add_float_attribute("timeout", "timeout", 120.0, 10.0, 600.0, 1.0, 0); - - - // by setting static UUIDs on these module we only create them once in the UI - playlist_notes_action_->set_role_data( - module::Attribute::UuidRole, "92c780be-d0bc-462a-b09f-643e8986e2a1"); - playlist_notes_action_->set_role_data( - module::Attribute::Title, "Publish Playlist Notes..."); - playlist_notes_action_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_menu"}); - playlist_notes_action_->set_role_data( - module::Attribute::MenuPaths, std::vector({"publish_menu|ShotGrid"})); - - selected_notes_action_->set_role_data( - module::Attribute::UuidRole, "7583a4d0-35d8-4f00-bc32-ae8c2bddc30a"); - selected_notes_action_->set_role_data( - module::Attribute::Title, "Publish Selected Notes..."); - selected_notes_action_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_menu"}); - selected_notes_action_->set_role_data( - module::Attribute::MenuPaths, std::vector({"publish_menu|ShotGrid"})); - - authentication_method_->set_role_data( - module::Attribute::UuidRole, "ea7c47b8-a851-4f44-b9f1-3f5b38c11d96"); - client_id_->set_role_data( - module::Attribute::UuidRole, "31925e29-674f-4f03-a861-502a2bc92f78"); - client_secret_->set_role_data( - module::Attribute::UuidRole, "05d18793-ef4c-4753-8b55-1d98788eb727"); - username_->set_role_data( - module::Attribute::UuidRole, "a012c508-a8a7-4438-97ff-05fc707331d0"); - password_->set_role_data( - module::Attribute::UuidRole, "55982b32-3273-4f1c-8164-251d8af83365"); - session_token_->set_role_data( - module::Attribute::UuidRole, "d6fac6a6-a6c9-4ac3-b961-499d9862a886"); - authenticated_->set_role_data( - module::Attribute::UuidRole, "ce708287-222f-46b6-820c-f6dfda592ba9"); - timeout_->set_role_data( - module::Attribute::UuidRole, "9947a178-b5bb-4370-905e-c6687b2d7f41"); - - authentication_method_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - client_id_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - client_secret_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - username_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - password_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - session_token_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - authenticated_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - timeout_->set_role_data( - module::Attribute::Groups, nlohmann::json{"shotgun_datasource_preference"}); - - authentication_method_->set_role_data( - module::Attribute::ToolTip, "ShotGrid authentication method."); - - client_id_->set_role_data(module::Attribute::ToolTip, "ShotGrid script key."); - client_secret_->set_role_data(module::Attribute::ToolTip, "ShotGrid script secret."); - username_->set_role_data(module::Attribute::ToolTip, "ShotGrid username."); - password_->set_role_data(module::Attribute::ToolTip, "ShotGrid password."); - session_token_->set_role_data(module::Attribute::ToolTip, "ShotGrid session token."); - authenticated_->set_role_data(module::Attribute::ToolTip, "Authenticated."); - timeout_->set_role_data(module::Attribute::ToolTip, "ShotGrid server timeout."); -} - -void ShotgunDataSource::attribute_changed(const utility::Uuid &attr_uuid, const int /*role*/) { - // pass upto actor.. - call_attribute_changed(attr_uuid); -} diff --git a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_base.hpp b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_base.hpp deleted file mode 100644 index 9ca44a1fe..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_base.hpp +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include - -#include "xstudio/data_source/data_source.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/module/module.hpp" - -using namespace xstudio; -using namespace xstudio::data_source; - -namespace xstudio::shotgun_client { -class AuthenticateShotgun; -} - -class ShotgunDataSource : public DataSource, public module::Module { - public: - ShotgunDataSource() : DataSource("Shotgun"), module::Module("ShotgunDataSource") { - add_attributes(); - } - ~ShotgunDataSource() override = default; - - // handled directly in actor. - utility::JsonStore get_data(const utility::JsonStore &) override { - return utility::JsonStore(); - } - utility::JsonStore put_data(const utility::JsonStore &) override { - return utility::JsonStore(); - } - utility::JsonStore post_data(const utility::JsonStore &) override { - return utility::JsonStore(); - } - utility::JsonStore use_data(const utility::JsonStore &) override { - return utility::JsonStore(); - } - - void set_authentication_method(const std::string &value); - void set_client_id(const std::string &value); - void set_client_secret(const std::string &value); - void set_username(const std::string &value); - void set_password(const std::string &value); - void set_session_token(const std::string &value); - void set_authenticated(const bool value); - void set_timeout(const int value); - - utility::Uuid session_id_; - - module::StringChoiceAttribute *authentication_method_; - module::StringAttribute *client_id_; - module::StringAttribute *client_secret_; - module::StringAttribute *username_; - module::StringAttribute *password_; - module::StringAttribute *session_token_; - module::BooleanAttribute *authenticated_; - module::FloatAttribute *timeout_; - - module::ActionAttribute *playlist_notes_action_; - module::ActionAttribute *selected_notes_action_; - - shotgun_client::AuthenticateShotgun get_authentication() const; - - void - bind_attribute_changed_callback(std::function fn) { - attribute_changed_callback_ = [fn](auto &&PH1) { - return fn(std::forward(PH1)); - }; - } - using module::Module::connect_to_ui; - - protected: - // void hotkey_pressed(const utility::Uuid &hotkey_uuid, const std::string &context) - // override; - - void attribute_changed(const utility::Uuid &attr_uuid, const int /*role*/) override; - - - void call_attribute_changed(const utility::Uuid &attr_uuid) { - if (attribute_changed_callback_) - attribute_changed_callback_(attr_uuid); - } - - - private: - std::function attribute_changed_callback_; - - void add_attributes(); -}; diff --git a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_definitions.hpp b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_definitions.hpp deleted file mode 100644 index 637c16fa9..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_definitions.hpp +++ /dev/null @@ -1,391 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include - -#include "xstudio/utility/json_store.hpp" - -// Action templates - -// GET - -const auto GetVersionIvyUuid = - R"({"operation": "VersionIvyUuid", "job":null, "ivy_uuid": null})"_json; - -const auto GetShotFromId = R"({"operation": "GetShotFromId", "shot_id": null})"_json; - -const auto GetLinkMedia = R"({"operation": "LinkMedia", "playlist_uuid": null})"_json; - -const auto GetValidMediaCount = R"({"operation": "MediaCount", "playlist_uuid": null})"_json; - -const auto GetDownloadMedia = R"({"operation": "DownloadMedia", "media_uuid": null})"_json; - -const auto GetPrepareNotes = R"({ - "operation":"PrepareNotes", - "playlist_uuid": null, - "media_uuids": [], - "notify_owner": false, - "notify_group_ids": [], - "combine": false, - "add_time": false, - "add_playlist_name": false, - "add_type": false, - "anno_requires_note": true, - "skip_already_published": false, - "default_type": null -})"_json; - -const auto GetQueryResult = R"({ - "operation": "Query", - "context": null, - "page": 1, - "max_result": 4999, - "entity": null, - "fields": [], - "order": [], - "query": null, - "result": null -})"_json; - -// POST - -const auto PostRenameTag = R"({"operation": "RenameTag", "tag_id": null, "value": null})"_json; -const auto PostCreateTag = R"({"operation": "CreateTag", "value": null})"_json; - -const auto PostTagEntity = - R"({"operation": "TagEntity", "entity": null, "entity_id": null, "tag_id": null})"_json; - -const auto PostUnTagEntity = - R"({"operation": "UnTagEntity", "entity": null, "entity_id": null, "tag_id": null})"_json; - -const auto PostCreatePlaylist = - R"({"operation": "CreatePlaylist", "playlist_uuid": null, "project_id": null, "code": null, "location": null, "playlist_type": "Dailies"})"_json; - -const auto PostCreateNotes = - R"({"operation": "CreateNotes", "playlist_uuid": null, "payload": []})"_json; - -// PUT - -const auto PutUpdatePlaylistVersions = - R"({"operation": "UpdatePlaylistVersions", "playlist_uuid": null})"_json; - -// USE - -const auto UseLoadPlaylist = R"({"operation": "LoadPlaylist", "playlist_id": 0})"_json; - -const auto UseRefreshPlaylist = - R"({"operation": "RefreshPlaylist", "playlist_uuid": null})"_json; - -// const auto RefreshPlaylistNotesJSON = -// R"({"entity":"Playlist", "relationship": "Note", "playlist_uuid": null})"_json; - - -const auto PublishNoteTemplateJSON = R"( -{ - "bookmark_uuid": "", - "shot": "", - "payload": { - "project":{ "type": "Project", "id":0 }, - "note_links": [ - { "type": "Playlist", "id":0 }, - { "type": "Sequence", "id":0 }, - { "type": "Shot", "id":0 }, - { "type": "Version", "id":0 } - ], - - "addressings_to": [ - { "type": "HumanUser", "id": 0} - ], - - "addressings_cc": [ - ], - - "sg_note_type": null, - "sg_status_list":"opn", - "subject": null, - "content": null - } -} -)"_json; - -const auto locationsJSON = R"([ - {"name": "chn", "id": 4}, - {"name": "lon", "id": 1}, - {"name": "mtl", "id": 52}, - {"name": "mum", "id": 3}, - {"name": "syd", "id": 99}, - {"name": "van", "id": 2}])"_json; - -const auto VersionFields = std::vector( - {"id", - "created_by", - "sg_pipeline_step", - "sg_path_to_frames", - "sg_dneg_version", - "sg_twig_name", - "sg_on_disk_mum", - "sg_on_disk_mtl", - "sg_on_disk_van", - "sg_on_disk_chn", - "sg_on_disk_lon", - "sg_on_disk_syd", - "sg_production_status", - "sg_status_list", - "sg_date_submitted_to_client", - "sg_ivy_dnuuid", - "frame_range", - "code", - "tags", - "sg_path_to_movie", - "frame_count", - "entity", - "project", - "created_at", - "notes", - "sg_twig_type_code", - "user", - "sg_cut_range", - "sg_comp_range", - "sg_project_name", - "sg_twig_type", - "sg_cut_order", - "cut_order", - "sg_cut_in", - "sg_comp_in", - "sg_cut_out", - "sg_comp_out", - "sg_frames_have_slate", - "sg_movie_has_slate", - "sg_submit_dailies", - "sg_submit_dailies_chn", - "sg_submit_dailies_mtl", - "sg_submit_dailies_van", - "sg_submit_dailies_mum", - "image"}); - -const auto NoteFields = std::vector( - {"id", - "created_by", - "created_at", - "client_note", - "sg_location", - "sg_note_type", - "sg_artist", - "sg_pipeline_step", - "subject", - "content", - "project", - "note_links", - "addressings_to", - "addressings_cc", - "attachments"}); - -const auto PlaylistFields = std::vector( - {"code", - "versions", - "sg_location", - "updated_at", - "created_at", - "sg_date_and_time", - "sg_type", - "created_by", - "sg_department_unit", - "notes"}); - -const auto ShotFields = - std::vector({"id", "code", "sg_comp_range", "sg_cut_range", "project"}); - -const std::string shotgun_datasource_registry{"SHOTGUNDATASOURCE"}; - -const auto ShotgunMetadataPath = std::string("/metadata/shotgun"); - -const auto TwigTypeCodes = xstudio::utility::JsonStore(R"([ - {"id": "anm", "name": "anim/dnanim"}, - {"id": "anmg", "name": "anim/group"}, - {"id": "pose", "name": "anim/pose"}, - {"id": "poseg", "name": "anim/posegroup"}, - {"id": "animcon", "name": "anim_concept"}, - {"id": "anno", "name": "annotation"}, - {"id": "aovc", "name": "aovconfig"}, - {"id": "apr", "name": "aov_presets"}, - {"id": "ably", "name": "assembly"}, - {"id": "asset", "name": "asset"}, - {"id": "assetl", "name": "assetl"}, - {"id": "acls", "name": "asset_class"}, - {"id": "alc", "name": "asset_library_config"}, - {"id": "abo", "name": "assisted_breakout"}, - {"id": "avpy", "name": "astrovalidate/check"}, - {"id": "avc", "name": "astrovalidate/checklist"}, - {"id": "ald", "name": "atmospheric_lookup_data"}, - {"id": "aud", "name": "audio"}, - {"id": "bsc", "name": "batch_script"}, - {"id": "buildcon", "name": "build_concept"}, - {"id": "imbl", "name": "bundle/image_map"}, - {"id": "texbl", "name": "bundle/texture"}, - {"id": "bch", "name": "cache/bgeo"}, - {"id": "fch", "name": "cache/fluid"}, - {"id": "gch", "name": "cache/geometry"}, - {"id": "houcache", "name": "cache/houdini"}, - {"id": "pch", "name": "cache/particle"}, - {"id": "vol", "name": "cache/volume"}, - {"id": "hcd", "name": "camera/chandata"}, - {"id": "cnv", "name": "camera/convergence"}, - {"id": "lnd", "name": "camera/lensdata"}, - {"id": "lnp", "name": "camera/lensprofile"}, - {"id": "cam", "name": "camera/mono"}, - {"id": "rtm", "name": "camera/retime"}, - {"id": "crig", "name": "camera/rig"}, - {"id": "camsheet", "name": "camera_sheet_ref"}, - {"id": "csht", "name": "charactersheet"}, - {"id": "cpk", "name": "charpik_pagedata"}, - {"id": "clrsl", "name": "clarisse/look"}, - {"id": "cdxc", "name": "codex_config"}, - {"id": "cpal", "name": "colourPalette"}, - {"id": "colsup", "name": "colour_setup"}, - {"id": "cpnt", "name": "component"}, - {"id": "artcon", "name": "concept_art"}, - {"id": "reicfg", "name": "config/rei"}, - {"id": "csc", "name": "contact_sheet_config"}, - {"id": "csp", "name": "contact_sheet_preset"}, - {"id": "cst", "name": "contact_sheet_template"}, - {"id": "convt", "name": "converter_template"}, - {"id": "crowda", "name": "crowd_actor"}, - {"id": "crowdc", "name": "crowd_cache"}, - {"id": "cdl", "name": "data/cdl"}, - {"id": "cut", "name": "data/clip/cut"}, - {"id": "edl", "name": "data/edl"}, - {"id": "lup", "name": "data/lineup"}, - {"id": "ref", "name": "data/ref"}, - {"id": "dspj", "name": "dossier_project"}, - {"id": "dvis", "name": "doublevision/scene"}, - {"id": "ecd", "name": "encoder_data"}, - {"id": "iss", "name": "framework/ivy/style"}, - {"id": "spt", "name": "framework/shotbuild/template"}, - {"id": "fbcv", "name": "furball/curve"}, - {"id": "fbgr", "name": "furball/groom"}, - {"id": "fbnt", "name": "furball/network"}, - {"id": "gsi", "name": "generics_instance"}, - {"id": "gss", "name": "generics_set"}, - {"id": "gst", "name": "generics_template"}, - {"id": "gft", "name": "giftwrap"}, - {"id": "grade", "name": "grade"}, - {"id": "llut", "name": "grade/looklut"}, - {"id": "artgfx", "name": "graphic_art"}, - {"id": "grm", "name": "groom"}, - {"id": "hbcfg", "name": "hotbuildconfig"}, - {"id": "hbcfgs", "name": "hotbuildconfig_set"}, - {"id": "hcpio", "name": "houdini_archive"}, - {"id": "ht", "name": "houdini_template"}, - {"id": "htp", "name": "houdini_template_params"}, - {"id": "idt", "name": "identity"}, - {"id": "art", "name": "image/artwork"}, - {"id": "ipg", "name": "image/imageplane"}, - {"id": "stb", "name": "image/storyboard"}, - {"id": "ibl", "name": "image_based_lighting"}, - {"id": "jgs", "name": "jigsaw"}, - {"id": "klr", "name": "katana/lightrig"}, - {"id": "klg", "name": "katana/livegroup"}, - {"id": "klf", "name": "katana/look"}, - {"id": "kr", "name": "katana/recipe"}, - {"id": "kla", "name": "katana_look_alias"}, - {"id": "kmac", "name": "katana_macro"}, - {"id": "lng", "name": "lensgrid"}, - {"id": "ladj", "name": "lighting_adjust"}, - {"id": "look", "name": "look"}, - {"id": "mtdd", "name": "material_data_driven"}, - {"id": "mtddcfg", "name": "material_data_driven_config"}, - {"id": "mtpc", "name": "material_plus_config"}, - {"id": "mtpg", "name": "material_plus_generator"}, - {"id": "mtpt", "name": "material_plus_template"}, - {"id": "mtpr", "name": "material_preset"}, - {"id": "moba", "name": "mob/actor"}, - {"id": "mobr", "name": "mob/rig"}, - {"id": "mobs", "name": "mob/sim"}, - {"id": "mcd", "name": "mocap/data"}, - {"id": "mcr", "name": "mocap/ref"}, - {"id": "mdl", "name": "model"}, - {"id": "mup", "name": "muppet"}, - {"id": "mupa", "name": "muppet/data"}, - {"id": "ndlr", "name": "noodle"}, - {"id": "nkc", "name": "nuke_config"}, - {"id": "ocean", "name": "ocean"}, - {"id": "omd", "name": "onset/metadata"}, - {"id": "otla", "name": "other/otlasset"}, - {"id": "omm", "name": "outsource/matchmove"}, - {"id": "apkg", "name": "package/asset"}, - {"id": "prm", "name": "params"}, - {"id": "psref", "name": "photoscan"}, - {"id": "pxt", "name": "pinocchio_extension"}, - {"id": "plt", "name": "plate"}, - {"id": "plook", "name": "preview_look"}, - {"id": "pbxt", "name": "procedural_build_extension"}, - {"id": "qcs", "name": "qcsheet"}, - {"id": "imageref", "name": "ref"}, - {"id": "osref", "name": "ref/onset"}, - {"id": "refbl", "name": "reference_bundle"}, - {"id": "render", "name": "render"}, - {"id": "2d", "name": "render/2D"}, - {"id": "cgr", "name": "render/cg"}, - {"id": "deepr", "name": "render/deep"}, - {"id": "elmr", "name": "render/element"}, - {"id": "foxr", "name": "render/forex"}, - {"id": "out", "name": "render/out"}, - {"id": "mov", "name": "render/playblast"}, - {"id": "movs", "name": "render/playblast/scene"}, - {"id": "wpb", "name": "render/playblast/working"}, - {"id": "scrr", "name": "render/scratch"}, - {"id": "testr", "name": "render/test"}, - {"id": "wrf", "name": "render/wireframe"}, - {"id": "wormr", "name": "render/worm"}, - {"id": "rpr", "name": "render_presets"}, - {"id": "repo2d", "name": "reposition_data_2d"}, - {"id": "zmdl", "name": "rexasset/model"}, - {"id": "rig", "name": "rig"}, - {"id": "lgtr", "name": "rig/light"}, - {"id": "rigs", "name": "rig_script"}, - {"id": "rigssn", "name": "rig_session"}, - {"id": "scan", "name": "scan"}, - {"id": "sctr", "name": "scatterer"}, - {"id": "sctrp", "name": "scatterer_preset"}, - {"id": "casc", "name": "scene/cascade"}, - {"id": "clrs", "name": "scene/clarisse"}, - {"id": "clwscn", "name": "scene/clarisse/working"}, - {"id": "hip", "name": "scene/houdini"}, - {"id": "scn", "name": "scene/maya"}, - {"id": "fxs", "name": "scene/maya/effects"}, - {"id": "gchs", "name": "scene/maya/geometry"}, - {"id": "lgt", "name": "scene/maya/lighting"}, - {"id": "ldv", "name": "scene/maya/lookdev"}, - {"id": "mod", "name": "scene/maya/model"}, - {"id": "mods", "name": "scene/maya/model/extended"}, - {"id": "mwscn", "name": "scene/maya/working"}, - {"id": "pycl", "name": "script/clarisse/python"}, - {"id": "otl", "name": "script/houdini/otl"}, - {"id": "pyh", "name": "script/houdini/python"}, - {"id": "mel", "name": "script/maya/mel"}, - {"id": "pym", "name": "script/maya/python"}, - {"id": "nkt", "name": "script/nuke/template"}, - {"id": "pcrn", "name": "script/popcorn"}, - {"id": "pys", "name": "script/python"}, - {"id": "artset", "name": "set_drawing"}, - {"id": "shot", "name": "shot"}, - {"id": "shotl", "name": "shot_layer"}, - {"id": "stig", "name": "stig"}, - {"id": "hdr", "name": "stig/hdr"}, - {"id": "sft", "name": "submission/subform/template"}, - {"id": "sbsd", "name": "substance_designer"}, - {"id": "sbsp", "name": "substance_painter"}, - {"id": "sprst", "name": "superset"}, - {"id": "surfs", "name": "surfacing_scene"}, - {"id": "nuketex", "name": "texture/nuke"}, - {"id": "texs", "name": "texture/sequence"}, - {"id": "texref", "name": "texture_ref"}, - {"id": "tvp", "name": "texture_viewport"}, - {"id": "tstl", "name": "tool_searcher_tool"}, - {"id": "veg", "name": "vegetation"}, - {"id": "vidref", "name": "video_ref"}, - {"id": "witvidref", "name": "video_ref_witness"}, - {"id": "wgt", "name": "weightmap"}, - {"id": "wsf", "name": "working_source_file"} -])"_json); diff --git a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_get_actions.tcc b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_get_actions.tcc deleted file mode 100644 index d0648d03c..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_get_actions.tcc +++ /dev/null @@ -1,787 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -template -void ShotgunDataSourceActor::find_ivy_version( - caf::typed_response_promise rp, - const std::string &uuid, - const std::string &job) { - // find version from supplied details. - - auto version_filter = - FilterBy().And(Text("project.Project.name").is(job), Text("sg_ivy_dnuuid").is(uuid)); - - request( - shotgun_, - std::chrono::seconds(static_cast(data_source_.timeout_->value())), - shotgun_entity_search_atom_v, - "Version", - JsonStore(version_filter), - VersionFields, - std::vector(), - 1, - 1) - .then( - [=](const JsonStore &jsn) mutable { - auto result = JsonStore(R"({"payload":[]})"_json); - if (jsn.count("data") and jsn.at("data").size()) { - result["payload"] = jsn.at("data")[0]; - } - rp.deliver(result); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(R"({"payload":[]})"_json)); - }); -} - -template -void ShotgunDataSourceActor::find_shot( - caf::typed_response_promise rp, const int shot_id) { - // find version from supplied details. - if (shot_cache_.count(shot_id)) - rp.deliver(shot_cache_.at(shot_id)); - else { - request( - shotgun_, - std::chrono::seconds(static_cast(data_source_.timeout_->value())), - shotgun_entity_atom_v, - "Shot", - shot_id, - ShotFields) - .then( - [=](const JsonStore &jsn) mutable { - shot_cache_[shot_id] = jsn; - rp.deliver(jsn); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(R"({"data":{}})"_json)); - }); - } -} - -template -void ShotgunDataSourceActor::link_media( - caf::typed_response_promise rp, const utility::Uuid &uuid) { - try { - // find playlist - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = - request_receive(*sys, session, session::get_playlist_atom_v, uuid); - - // get media.. - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - // scan media for shotgun version / ivy uuid - if (not media.empty()) { - fan_out_request( - vector_to_caf_actor_vector(media), - infinite, - json_store::get_json_atom_v, - utility::Uuid(), - "", - true) - .then( - [=](std::vector> json) mutable { - // ivy uuid is stored on source not media.. balls. - auto left = std::make_shared(0); - auto invalid = std::make_shared(0); - for (const auto &i : json) { - try { - if (i.second.is_null() or - not i.second["metadata"].count("shotgun")) { - // request current media source metadata.. - scoped_actor sys{system()}; - auto source_meta = request_receive( - *sys, - i.first.actor(), - json_store::get_json_atom_v, - "/metadata/external/DNeg"); - // we has got it.. - auto ivy_uuid = source_meta.at("Ivy").at("dnuuid"); - auto job = source_meta.at("show"); - auto shot = source_meta.at("shot"); - (*left) += 1; - // spdlog::warn("{} {} {} {}", job, shot, ivy_uuid, *left); - // call back into self ? - // but we need to wait for the final result.. - // maybe in danger of deadlocks... - // now we need to query shotgun.. - // to try and find version from this information. - // this is then used to update the media actor. - auto jsre = JsonStore(GetVersionIvyUuid); - jsre["ivy_uuid"] = ivy_uuid; - jsre["job"] = job; - - request( - caf::actor_cast(this), - infinite, - get_data_atom_v, - jsre) - .then( - [=](const JsonStore &ver) mutable { - // got ver from uuid - (*left)--; - if (ver["payload"].empty()) { - (*invalid)++; - } else { - // push version to media object - scoped_actor sys{system()}; - try { - request_receive( - *sys, - i.first.actor(), - json_store::set_json_atom_v, - utility::Uuid(), - JsonStore(ver["payload"]), - ShotgunMetadataPath + "/version"); - } catch (const std::exception &err) { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - err.what()); - } - } - - if (not(*left)) { - JsonStore result( - R"({"result": {"valid":0, "invalid":0}})"_json); - result["result"]["valid"] = - json.size() - (*invalid); - result["result"]["invalid"] = (*invalid); - rp.deliver(result); - } - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - (*left)--; - (*invalid)++; - if (not(*left)) { - JsonStore result( - R"({"result": {"valid":0, "invalid":0}})"_json); - result["result"]["valid"] = - json.size() - (*invalid); - result["result"]["invalid"] = (*invalid); - rp.deliver(result); - } - }); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - if (not(*left)) { - JsonStore result(R"({"result": {"valid":0, "invalid":0}})"_json); - result["result"]["valid"] = json.size(); - result["result"]["invalid"] = 0; - rp.deliver(result); - } - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); - }); - } else { - rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); - } - - - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::download_media( - caf::typed_response_promise rp, const utility::Uuid &uuid) { - try { - // find media - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto media = - request_receive(*sys, session, playlist::get_media_atom_v, uuid); - - // get metadata, we need version id.. - auto media_metadata = request_receive( - *sys, - media, - json_store::get_json_atom_v, - utility::Uuid(), - "/metadata/shotgun/version"); - - // spdlog::warn("{}", media_metadata.dump(2)); - - auto name = media_metadata.at("attributes").at("code").template get(); - auto job = - media_metadata.at("attributes").at("sg_project_name").template get(); - auto shot = media_metadata.at("relationships") - .at("entity") - .at("data") - .at("name") - .template get(); - auto filepath = download_cache_.target_string() + "/" + name + "-" + job + "-" + shot + - ".dneg.webm"; - - - // check it doesn't already exist.. - if (fs::exists(filepath)) { - // create source and add to media - auto uuid = Uuid::generate(); - auto source = spawn( - "ShotGrid Preview", - utility::posix_path_to_uri(filepath), - FrameList(), - FrameRate(), - uuid); - request(media, infinite, media::add_media_source_atom_v, UuidActor(uuid, source)) - .then( - [=](const Uuid &u) mutable { - auto jsn = JsonStore(R"({})"_json); - jsn["actor_uuid"] = uuid; - jsn["actor"] = actor_to_string(system(), source); - - rp.deliver(jsn); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore((R"({})"_json)["error"] = to_string(err))); - }); - } else { - request( - shotgun_, - infinite, - shotgun_attachment_atom_v, - "version", - media_metadata.at("id").template get(), - "sg_uploaded_movie_webm") - .then( - [=](const std::string &data) mutable { - if (data.size() > 1024 * 15) { - // write to file - std::ofstream o(filepath); - try { - o.exceptions(std::ifstream::failbit | std::ifstream::badbit); - o << data << std::endl; - o.close(); - - // file written add to media as new source.. - auto uuid = Uuid::generate(); - auto source = spawn( - "ShotGrid Preview", - utility::posix_path_to_uri(filepath), - FrameList(), - FrameRate(), - uuid); - request( - media, - infinite, - media::add_media_source_atom_v, - UuidActor(uuid, source)) - .then( - [=](const Uuid &u) mutable { - auto jsn = JsonStore(R"({})"_json); - jsn["actor_uuid"] = uuid; - jsn["actor"] = actor_to_string(system(), source); - - rp.deliver(jsn); - }, - [=](error &err) mutable { - spdlog::error( - "{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore( - (R"({})"_json)["error"] = to_string(err))); - }); - - } catch (const std::exception &) { - // remove failed file - if (o.is_open()) { - o.close(); - fs::remove(filepath); - } - spdlog::warn("Failed to open file"); - } - } else { - rp.deliver( - JsonStore((R"({})"_json)["error"] = "Failed to download")); - } - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore((R"({})"_json)["error"] = to_string(err))); - }); - } - // "content_type": "video/webm", - // "id": 88463162, - // "link_type": "upload", - // "name": "b'tmp_upload_webm_0okvakz6.webm'", - // "type": "Attachment", - // "url": "http://shotgun.dneg.com/file_serve/attachment/88463162" - - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(JsonStore((R"({})"_json)["error"] = err.what())); - } -} - -template -void ShotgunDataSourceActor::get_valid_media_count( - caf::typed_response_promise rp, const utility::Uuid &uuid) { - try { - // find playlist - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = - request_receive(*sys, session, session::get_playlist_atom_v, uuid); - - // get media.. - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - if (not media.empty()) { - fan_out_request( - vector_to_caf_actor_vector(media), - infinite, - json_store::get_json_atom_v, - utility::Uuid(), - "") - .then( - [=](std::vector json) mutable { - int count = 0; - for (const auto &i : json) { - try { - if (i["metadata"].count("shotgun")) - count++; - } catch (...) { - } - } - - JsonStore result(R"({"result": {"valid":0, "invalid":0}})"_json); - result["result"]["valid"] = count; - result["result"]["invalid"] = json.size() - count; - rp.deliver(result); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); - }); - } else { - rp.deliver(JsonStore(R"({"result": {"valid":0, "invalid":0}})"_json)); - } - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::prepare_playlist_notes( - caf::typed_response_promise rp, - const utility::Uuid &playlist_uuid, - const utility::UuidVector &media_uuids, - const bool notify_owner, - const std::vector notify_group_ids, - const bool combine, - const bool add_time, - const bool add_playlist_name, - const bool add_type, - const bool anno_requires_note, - const bool skip_already_pubished, - const std::string &default_type) { - - auto playlist_name = std::string(); - auto playlist_id = int(0); - auto payload = R"({"payload":[], "valid": 0, "invalid": 0})"_json; - - try { - scoped_actor sys{system()}; - - // get session - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - // get playlist - auto playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - // get shotgun info from playlist.. - try { - auto sgpl = request_receive( - *sys, playlist, json_store::get_json_atom_v, ShotgunMetadataPath + "/playlist"); - - playlist_name = sgpl.at("attributes").at("code").template get(); - playlist_id = sgpl.at("id").template get(); - - } catch (const std::exception &err) { - spdlog::info("No shotgun playlist information"); - } - - // get media for playlist. - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - // no media so no point.. - // nothing to publish. - if (media.empty()) - return rp.deliver(JsonStore(payload)); - - std::vector media_actors; - - if (not media_uuids.empty()) { - auto lookup = uuidactor_vect_to_map(media); - for (const auto &i : media_uuids) { - if (lookup.count(i)) - media_actors.push_back(lookup[i]); - } - } else { - media_actors = vector_to_caf_actor_vector(media); - } - - // get media shotgun json.. - // we can only publish notes for media that has version information - fan_out_request( - media_actors, - infinite, - json_store::get_json_atom_v, - utility::Uuid(), - ShotgunMetadataPath, - true) - .then( - [=](std::vector> version_meta) mutable { - auto result = JsonStore(payload); - - scoped_actor sys{system()}; - - std::map> media_map; - UuidVector valid_media; - - // get valid media. - // get all the shotgun info we need to publish - for (const auto &i : version_meta) { - try { - // spdlog::warn("{}", i.second.dump(2)); - const auto &version = i.second.at("version"); - auto jsn = JsonStore(PublishNoteTemplateJSON); - - // project - jsn["payload"]["project"]["id"] = version.at("relationships") - .at("project") - .at("data") - .at("id") - .get(); - - - // playlist link - jsn["payload"]["note_links"][0]["id"] = playlist_id; - - if (version.at("relationships") - .at("entity") - .at("data") - .value("type", "") == "Sequence") - // shot link - jsn["payload"]["note_links"][1]["id"] = - version.at("relationships") - .at("entity") - .at("data") - .value("id", 0); - else if ( - version.at("relationships") - .at("entity") - .at("data") - .value("type", "") == "Shot") - // sequence link - jsn["payload"]["note_links"][2]["id"] = - version.at("relationships") - .at("entity") - .at("data") - .value("id", 0); - - // version link - jsn["payload"]["note_links"][3]["id"] = version.value("id", 0); - - if (jsn["payload"]["note_links"][3]["id"].get() == 0) - jsn["payload"]["note_links"].erase(3); - if (jsn["payload"]["note_links"][2]["id"].get() == 0) - jsn["payload"]["note_links"].erase(2); - if (jsn["payload"]["note_links"][1]["id"].get() == 0) - jsn["payload"]["note_links"].erase(1); - if (jsn["payload"]["note_links"][0]["id"].get() == 0) - jsn["payload"]["note_links"].erase(0); - - // we don't pass these to shotgun.. - jsn["shot"] = version.at("relationships") - .at("entity") - .at("data") - .at("name") - .get(); - jsn["playlist_name"] = playlist_name; - - if (notify_owner) // 1068 - jsn["payload"]["addressings_to"][0]["id"] = - version.at("relationships") - .at("user") - .at("data") - .at("id") - .get(); - else - jsn["payload"].erase("addressings_to"); - - if (not notify_group_ids.empty()) { - auto grp = R"({ "type": "Group", "id": null})"_json; - for (const auto g : notify_group_ids) { - if (g <= 0) - continue; - - grp["id"] = g; - jsn["payload"]["addressings_cc"].push_back(grp); - } - } - - if (jsn["payload"]["addressings_cc"].empty()) - jsn["payload"].erase("addressings_cc"); - - - media_map[i.first.uuid()] = std::make_pair(i.first, jsn); - valid_media.push_back(i.first.uuid()); - } catch (const std::exception &err) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - // get bookmark manager. - auto bookmarks = request_receive( - *sys, session, bookmark::get_bookmark_atom_v); - - // // collect media notes if they have shotgun metadata on the media - auto existing_bookmarks = - request_receive>>( - *sys, bookmarks, bookmark::get_bookmarks_atom_v, valid_media); - - // get bookmark detail.. - for (const auto &i : existing_bookmarks) { - // grouped by media item. - // we may want to collapse to unique note_types - std::map>> - notes_by_type; - - for (const auto &j : i.second) { - try { - if (skip_already_pubished) { - auto already_published = false; - try { - // check for shotgun metadata on note. - request_receive( - *sys, - j.actor(), - json_store::get_json_atom_v, - ShotgunMetadataPath + "/note"); - already_published = true; - } catch (...) { - } - - if (already_published) - continue; - } - - auto detail = request_receive( - *sys, j.actor(), bookmark::bookmark_detail_atom_v); - // skip notes with no text unless annotated and - // only_with_annotation is true - auto has_note = detail.note_ and not(*(detail.note_)).empty(); - auto has_anno = - detail.has_annotation_ and *(detail.has_annotation_); - - // do not publish non-visible bookmarks (e.g. grades) - auto visible = - detail.visible_ and *(detail.visible_); - if (not visible) continue; - - if (not(has_note or (has_anno and not anno_requires_note))) - continue; - - auto [ua, jsn] = media_map[detail.owner_->uuid()]; - // push to shotgun client.. - jsn["bookmark_uuid"] = j.uuid(); - if (not jsn.count("has_annotation")) - jsn["has_annotation"] = R"([])"_json; - - if (has_anno) { - auto item = - R"({"media_uuid": null, "media_name": null, "media_frame": 0, "timecode_frame": 0})"_json; - item["media_uuid"] = i.first; - item["media_name"] = jsn["shot"]; - item["media_frame"] = detail.start_frame(); - item["timecode_frame"] = - detail.start_timecode_tc().total_frames(); - // requires media actor and first frame of annotation. - jsn["has_annotation"].push_back(item); - } - auto cat = detail.category_ ? *(detail.category_) : ""; - if (not default_type.empty()) - cat = default_type; - - jsn["payload"]["sg_note_type"] = cat; - jsn["payload"]["subject"] = - detail.subject_ ? *(detail.subject_) : ""; - // format note content - std::string content; - - if (add_time) - content += std::string("Frame : ") + - std::to_string( - detail.start_timecode_tc().total_frames()) + - " / " + detail.start_timecode() + " / " + - detail.duration_timecode() + "\n"; - - content += *(detail.note_); - - jsn["payload"]["content"] = content; - - // yeah this is a bit convoluted. - if (not notes_by_type.count(cat)) { - notes_by_type.insert(std::make_pair( - cat, - std::map>( - {{detail.start_frame(), {{jsn}}}}))); - } else { - if (notes_by_type[cat].count(detail.start_frame())) { - notes_by_type[cat][detail.start_frame()].push_back(jsn); - } else { - notes_by_type[cat].insert(std::make_pair( - detail.start_frame(), - std::vector({jsn}))); - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - try { - auto merged = JsonStore(); - - // category - for (auto &k : notes_by_type) { - auto category = k.first; - // frame - for (const auto &j : k.second) { - // entry - for (const auto ¬epayload : j.second) { - // spdlog::warn("{}",notepayload.dump(2)); - - if (not merged.is_null() and - (not combine or - merged["payload"]["sg_note_type"] != - notepayload["payload"]["sg_note_type"])) { - // spdlog::warn("{}", merged.dump(2)); - result["payload"].push_back(merged); - merged = JsonStore(); - } - - if (merged.is_null()) { - merged = notepayload; - auto content = std::string(); - if (add_playlist_name and - not merged["playlist_name"] - .get() - .empty()) - content += - "Playlist : " + - std::string(merged["playlist_name"]) + "\n"; - if (add_type) - content += "Note Type : " + - merged["payload"]["sg_note_type"] - .get() + - "\n\n"; - else - content += "\n\n"; - - merged["payload"]["content"] = - content + - merged["payload"]["content"].get(); - - merged.erase("shot"); - merged.erase("playlist_name"); - } else { - merged["payload"]["content"] = - merged["payload"]["content"] - .get() + - "\n\n" + - notepayload["payload"]["content"] - .get(); - merged["has_annotation"].insert( - merged["has_annotation"].end(), - notepayload["has_annotation"].begin(), - notepayload["has_annotation"].end()); - } - } - } - } - - if (not merged.is_null()) - result["payload"].push_back(merged); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - result["valid"] = result["payload"].size(); - - // spdlog::warn("{}", result.dump(2)); - rp.deliver(result); - }, - [=](error &err) mutable { - spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(JsonStore(payload)); - }); - - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::execute_query(caf::typed_response_promise rp, const utility::JsonStore &action) { - - request(shotgun_, infinite, shotgun_entity_search_atom_v, - action.at("entity").get(), - JsonStore(action.at("query")), - action.at("fields").get>(), - action.at("order").get>(), - action.at("page").get(), - action.at("max_result").get() - ).then( - [=](const JsonStore &data) mutable { - auto result = action; - result["result"] = data; - rp.deliver(result); - }, - [=](error &err) mutable { - rp.deliver(err); - } - ); -} diff --git a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_post_actions.tcc b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_post_actions.tcc deleted file mode 100644 index 969e1f8be..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_post_actions.tcc +++ /dev/null @@ -1,498 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -template -void ShotgunDataSourceActor::create_playlist_notes( - caf::typed_response_promise rp, - const utility::JsonStore ¬es, - const utility::Uuid &playlist_uuid) { - - const std::string ui(R"( - import xStudio 1.0 - import QtQuick 2.14 - XsLabel { - anchors.fill: parent - font.pixelSize: XsStyle.popupControlFontSize*1.2 - verticalAlignment: Text.AlignVCenter - font.weight: Font.Bold - color: palette.highlight - text: "SG" - } - )"); - - try { - scoped_actor sys{system()}; - - // get session - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto bookmarks = - request_receive(*sys, session, bookmark::get_bookmark_atom_v); - - auto tags = request_receive(*sys, session, xstudio::tag::get_tag_atom_v); - - auto count = std::make_shared(notes.size()); - auto failed = std::make_shared(0); - auto succeed = std::make_shared(0); - - auto offscreen_renderer = - system().registry().template get(offscreen_viewport_registry); - auto thumbnail_manager = - system().registry().template get(thumbnail_manager_registry); - - for (const auto &j : notes) { - // need to capture result to embed in playlist and add any media.. - // spdlog::warn("{}", j["payload"].dump(2)); - request( - shotgun_, - infinite, - shotgun_create_entity_atom_v, - "notes", - utility::JsonStore(j["payload"])) - .then( - [=](const JsonStore &result) mutable { - (*count)--; - try { - // "errors": [ - // { - // "status": null - // } - // ] - if (not result.at("errors")[0].at("status").is_null()) - throw std::runtime_error(result["errors"].dump(2)); - - // get new playlist id.. - auto note_id = result.at("data").at("id").template get(); - // we have a note... - if (not j["has_annotation"].empty()) { - for (const auto &anno : j["has_annotation"]) { - request( - session, - infinite, - playlist::get_media_atom_v, - utility::Uuid(anno["media_uuid"])) - .then( - [=](const caf::actor &media_actor) mutable { - // spdlog::warn("render annotation {}", - // anno["media_frame"].get()); - request( - offscreen_renderer, - infinite, - ui::viewport:: - render_viewport_to_image_atom_v, - media_actor, - anno["media_frame"].get(), - thumbnail::THUMBNAIL_FORMAT::TF_RGB24, - 0, - true, - true) - .then( - [=](const thumbnail::ThumbnailBufferPtr - &tnail) { - // got buffer. convert to jpg.. - request( - thumbnail_manager, - infinite, - media_reader:: - get_thumbnail_atom_v, - tnail) - .then( - [=](const std::vector< - std::byte> - &jpgbuf) mutable { - // final step... - auto title = std:: - string(fmt::format( - "{}_{}.jpg", - anno["media_" - "name"] - .get< - std:: - string>(), - anno["timecode_" - "frame"] - .get< - int>())); - request( - shotgun_, - infinite, - shotgun_upload_atom_v, - "note", - note_id, - "", - title, - jpgbuf, - "image/jpeg") - .then( - [=](const bool) { - }, - [=](const error & - err) mutable { - spdlog::warn( - "{} " - "Failed" - " uploa" - "d of " - "annota" - "tion " - "{}", - __PRETTY_FUNCTION__, - to_string( - err)); - } - - ); - }, - [=](const error - &err) mutable { - spdlog::warn( - "{} Failed jpeg " - "conversion {}", - __PRETTY_FUNCTION__, - to_string(err)); - }); - }, - [=](const error &err) mutable { - spdlog::warn( - "{} Failed render annotation " - "{}", - __PRETTY_FUNCTION__, - to_string(err)); - }); - }, - [=](const error &err) mutable { - spdlog::warn( - "{} Failed get media {}", - __PRETTY_FUNCTION__, - to_string(err)); - }); - } - } - - // spdlog::warn("note {}", result.dump(2)); - // send json to note.. - anon_send( - bookmarks, - json_store::set_json_atom_v, - utility::Uuid(j["bookmark_uuid"]), - utility::JsonStore(result.at("data")), - ShotgunMetadataPath + "/note"); - - xstudio::tag::Tag t; - t.set_type("Decorator"); - t.set_data(ui); - t.set_link(utility::Uuid(j["bookmark_uuid"])); - t.set_unique(to_string(t.link()) + t.type() + t.data()); - - anon_send(tags, xstudio::tag::add_tag_atom_v, t); - - // update shotgun versions from our source playlist. - // return the result.. - // update_playlist_versions(rp, playlist_uuid, playlist_id); - (*succeed)++; - } catch (const std::exception &err) { - (*failed)++; - spdlog::warn( - "{} {} {}", __PRETTY_FUNCTION__, err.what(), result.dump(2)); - } - - if (not(*count)) { - auto jsn = JsonStore(R"({"data": {"status": ""}})"_json); - jsn["data"]["status"] = std::string(fmt::format( - "Successfully published {} / {} notes.", - *succeed, - (*failed) + (*succeed))); - rp.deliver(jsn); - } - }, - [=](error &err) mutable { - spdlog::warn( - "Failed create note entity {} {}", - __PRETTY_FUNCTION__, - to_string(err)); - (*count)--; - (*failed)++; - - if (not(*count)) { - auto jsn = JsonStore(R"({"data": {"status": ""}})"_json); - jsn["data"]["status"] = std::string(fmt::format( - "Successfully published {} / {} notes.", - *succeed, - (*failed) + (*succeed))); - rp.deliver(jsn); - } - }); - } - - } catch (const std::exception &err) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::create_playlist( - caf::typed_response_promise rp, const utility::JsonStore &js) { - // src should be a playlist actor.. - // and we want to update it.. - // retrieve shotgun metadata from playlist, and media items. - try { - - scoped_actor sys{system()}; - - auto playlist_uuid = Uuid(js["playlist_uuid"]); - auto project_id = js["project_id"].template get(); - auto code = js["code"].template get(); - auto loc = js["location"].template get(); - auto playlist_type = js["playlist_type"].template get(); - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - auto jsn = R"({ - "project":{ "type": "Project", "id":null }, - "code": null, - "sg_location": "unknown", - "sg_type": "Dailies", - "sg_date_and_time": null - })"_json; - - jsn["project"]["id"] = project_id; - jsn["code"] = code; - jsn["sg_location"] = loc; - jsn["sg_type"] = playlist_type; - jsn["sg_date_and_time"] = date_time_and_zone(); - - // "2021-08-18T19:00:00Z" - - // need to capture result to embed in playlist and add any media.. - request( - shotgun_, - infinite, - shotgun_create_entity_atom_v, - "playlists", - utility::JsonStore(jsn)) - .then( - [=](const JsonStore &result) mutable { - try { - // get new playlist id.. - auto playlist_id = result.at("data").at("id").template get(); - // update shotgun versions from our source playlist. - // return the result.. - update_playlist_versions(rp, playlist_uuid, playlist_id); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, result.dump(2)); - rp.deliver(make_error(xstudio_error::error, err.what())); - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::rename_tag( - caf::typed_response_promise rp, - const int tag_id, - const std::string &value) { - - // as this is an update, we have to pull current list and then add / push it back.. (I - // THINK) - try { - - scoped_actor sys{system()}; - - auto payload = R"({"name": null})"_json; - payload["name"] = value; - - // send update request.. - request( - shotgun_, - infinite, - shotgun_update_entity_atom_v, - "Tag", - tag_id, - utility::JsonStore(payload), - std::vector({"id"})) - .then( - [=](const JsonStore &result) mutable { rp.deliver(result); }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - - -template -void ShotgunDataSourceActor::remove_entity_tag( - caf::typed_response_promise rp, - const std::string &entity, - const int entity_id, - const int tag_id) { - - // as this is an update, we have to pull current list and then add / push it back.. (I - // THINK) - try { - - scoped_actor sys{system()}; - request( - caf::actor_cast(this), - infinite, - shotgun_entity_atom_v, - entity, - entity_id, - std::vector({"tags"})) - .then( - [=](const JsonStore &result) mutable { - try { - auto current_tags = - result.at("data").at("relationships").at("tags").at("data"); - for (auto it = current_tags.begin(); it != current_tags.end(); ++it) { - if (it->at("id") == tag_id) { - current_tags.erase(it); - break; - } - } - - auto payload = R"({"tags": []})"_json; - payload["tags"] = current_tags; - - // send update request.. - request( - shotgun_, - infinite, - shotgun_update_entity_atom_v, - entity, - entity_id, - utility::JsonStore(payload), - std::vector({"id", "code", "tags"})) - .then( - [=](const JsonStore &result) mutable { rp.deliver(result); }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::create_tag( - caf::typed_response_promise rp, const std::string &value) { - - try { - scoped_actor sys{system()}; - - auto jsn = R"({ - "name": null - })"_json; - - jsn["name"] = value; - - request( - shotgun_, infinite, shotgun_create_entity_atom_v, "tags", utility::JsonStore(jsn)) - .then( - [=](const JsonStore &result) mutable { - try { - rp.deliver(result); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, result.dump(2)); - rp.deliver(make_error(xstudio_error::error, err.what())); - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - - - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} - -template -void ShotgunDataSourceActor::add_entity_tag( - caf::typed_response_promise rp, - const std::string &entity, - const int entity_id, - const int tag_id) { - - // as this is an update, we have to pull current list and then add / push it back.. (I - // THINK) - try { - - scoped_actor sys{system()}; - request( - caf::actor_cast(this), - infinite, - shotgun_entity_atom_v, - entity, - entity_id, - std::vector({"tags"})) - .then( - [=](const JsonStore &result) mutable { - try { - auto current_tags = - result.at("data").at("relationships").at("tags").at("data"); - auto new_tag = R"({"id":null, "type": "Tag"})"_json; - auto payload = R"({"tags": []})"_json; - - new_tag["id"] = tag_id; - current_tags.push_back(new_tag); - payload["tags"] = current_tags; - - // send update request.. - request( - shotgun_, - infinite, - shotgun_update_entity_atom_v, - entity, - entity_id, - utility::JsonStore(payload), - std::vector({"id", "code", "tags"})) - .then( - [=](const JsonStore &result) mutable { rp.deliver(result); }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} diff --git a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_put_actions.tcc b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_put_actions.tcc deleted file mode 100644 index d2c67ca05..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_put_actions.tcc +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -template -void ShotgunDataSourceActor::update_playlist_versions( - caf::typed_response_promise rp, - const utility::Uuid &playlist_uuid, - const int playlist_id) { - // src should be a playlist actor.. - // and we want to update it.. - // retrieve shotgun metadata from playlist, and media items. - try { - - scoped_actor sys{system()}; - - auto session = request_receive( - *sys, - system().registry().template get(global_registry), - session::session_atom_v); - - auto playlist = request_receive( - *sys, session, session::get_playlist_atom_v, playlist_uuid); - - auto pl_id = playlist_id; - if (not pl_id) { - auto plsg = request_receive( - *sys, playlist, json_store::get_json_atom_v, ShotgunMetadataPath + "/playlist"); - - pl_id = plsg["id"].template get(); - } - - auto media = - request_receive>(*sys, playlist, playlist::get_media_atom_v); - - // foreach medai actor get it's shogtun metadata. - auto jsn = R"({"versions":[]})"_json; - auto ver = R"({"id": 0, "type": "Version"})"_json; - - std::map version_order_map; - // get media detail - int sort_order = 1; - for (const auto &i : media) { - try { - auto mjson = request_receive( - *sys, - i.actor(), - json_store::get_json_atom_v, - utility::Uuid(), - ShotgunMetadataPath + "/version"); - auto id = mjson["id"].template get(); - ver["id"] = id; - jsn["versions"].push_back(ver); - version_order_map[id] = sort_order; - - sort_order++; - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - // update playlist - request( - shotgun_, - infinite, - shotgun_update_entity_atom_v, - "Playlists", - pl_id, - utility::JsonStore(jsn)) - .then( - [=](const JsonStore &result) mutable { - // spdlog::warn("{}", JsonStore(result["data"]).dump(2)); - // update playorder.. - // get PlaylistVersionConnections - scoped_actor sys{system()}; - - auto order_filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["playlist", "is", {"type":"Playlist", "id":0}] - ] - })"_json; - - order_filter["conditions"][0][2]["id"] = pl_id; - - try { - auto order = request_receive( - *sys, - shotgun_, - shotgun_entity_search_atom_v, - "PlaylistVersionConnection", - JsonStore(order_filter), - std::vector({"sg_sort_order", "version"}), - std::vector({"sg_sort_order"}), - 1, - 4999); - // update all PlaylistVersionConnection's with new sort_order. - for (const auto &i : order["data"]) { - auto version_id = i.at("relationships") - .at("version") - .at("data") - .at("id") - .get(); - auto sort_order = - i.at("attributes").at("sg_sort_order").is_null() - ? 0 - : i.at("attributes").at("sg_sort_order").get(); - // spdlog::warn("{} {}", std::to_string(sort_order), - // std::to_string(version_order_map[version_id])); - if (sort_order != version_order_map[version_id]) { - auto so_jsn = R"({"sg_sort_order": 0})"_json; - so_jsn["sg_sort_order"] = version_order_map[version_id]; - try { - request_receive( - *sys, - shotgun_, - shotgun_update_entity_atom_v, - "PlaylistVersionConnection", - i.at("id").get(), - utility::JsonStore(so_jsn), - std::vector({"id"})); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - - if (pl_id != playlist_id) - anon_send( - playlist, - json_store::set_json_atom_v, - JsonStore(result["data"]), - ShotgunMetadataPath + "/playlist"); - rp.deliver(result); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - - // need to update/add PlaylistVersionConnection's - // on creation the sort_order will be null. - // PlaylistVersionConnection are auto created when adding to playlist. (I think) - // so all we need to do is update.. - - - // get shotgun metadata - // get media actors. - // get media shotgun metadata. - } catch (const std::exception &err) { - rp.deliver(make_error(xstudio_error::error, err.what())); - } -} diff --git a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_query_engine.cpp b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_query_engine.cpp deleted file mode 100644 index 701256d29..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_query_engine.cpp +++ /dev/null @@ -1,229 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include "data_source_shotgun_definitions.hpp" -#include "data_source_shotgun_query_engine.hpp" -#include "xstudio/shotgun_client/shotgun_client.hpp" -#include "xstudio/utility/string_helpers.hpp" - -using namespace xstudio; -using namespace xstudio::shotgun_client; -using namespace xstudio::utility; - -utility::JsonStore QueryEngine::build_query( - const int project_id, - const std::string &entity, - const utility::JsonStore &group_terms, - const utility::JsonStore &terms, - const utility::JsonStore &context, - const utility::JsonStore &lookup) { - auto query = utility::JsonStore(GetQueryResult); - FilterBy filter; - - query["entity"] = entity; - - query["context"] = context; - // R"({ - // "type": null, - // "epoc": null, - // "audio_source": "", - // "visual_source": "", - // "flag_text": "", - // "flag_colour": "", - // "truncated": false - // })"_json; - - if (entity == "Versions") - query["fields"] = VersionFields; - else if (entity == "Notes") - query["fields"] = NoteFields; - else if (entity == "Playlists") - query["fields"] = PlaylistFields; - - auto merged_preset = merge_query(terms, group_terms); - - FilterBy qry; - - try { - - std::multimap qry_terms; - std::vector order_by; - - // collect terms in map - for (const auto &i : merged_preset) { - if (i.at("enabled").get()) { - // filter out order by and max count.. - if (i.at("term") == "Disable Global") { - // filtered out - } else if (i.at("term") == "Result Limit") { - query["max_result"] = std::stoi(i.at("value").get()); - } else if (i.at("term") == "Preferred Visual") { - query["context"]["visual_source"] = i.at("value").get(); - } else if (i.at("term") == "Preferred Audio") { - query["context"]["audio_source"] = i.at("value").get(); - } else if (i.at("term") == "Flag Media") { - auto flag_text = i.at("value").get(); - query["context"]["flag_text"] = flag_text; - if (flag_text == "Red") - query["context"]["flag_colour"] = "#FFFF0000"; - else if (flag_text == "Green") - query["context"]["flag_colour"] = "#FF00FF00"; - else if (flag_text == "Blue") - query["context"]["flag_colour"] = "#FF0000FF"; - else if (flag_text == "Yellow") - query["context"]["flag_colour"] = "#FFFFFF00"; - else if (flag_text == "Orange") - query["context"]["flag_colour"] = "#FFFFA500"; - else if (flag_text == "Purple") - query["context"]["flag_colour"] = "#FF800080"; - else if (flag_text == "Black") - query["context"]["flag_colour"] = "#FF000000"; - else if (flag_text == "White") - query["context"]["flag_colour"] = "#FFFFFFFF"; - } else if (i.at("term") == "Order By") { - auto val = i.at("value").get(); - bool descending = false; - - if (ends_with(val, " ASC")) { - val = val.substr(0, val.size() - 4); - } else if (ends_with(val, " DESC")) { - val = val.substr(0, val.size() - 5); - descending = true; - } - - std::string field = ""; - // get sg term.. - if (entity == "Playlists") { - if (val == "Date And Time") - field = "sg_date_and_time"; - else if (val == "Created") - field = "created_at"; - else if (val == "Updated") - field = "updated_at"; - } else if (entity == "Versions") { - if (val == "Date And Time") - field = "created_at"; - else if (val == "Created") - field = "created_at"; - else if (val == "Updated") - field = "updated_at"; - else if (val == "Client Submit") - field = "sg_date_submitted_to_client"; - else if (val == "Version") - field = "sg_dneg_version"; - } else if (entity == "Notes") { - if (val == "Created") - field = "created_at"; - else if (val == "Updated") - field = "updated_at"; - } - - if (not field.empty()) - order_by.push_back(descending ? "-" + field : field); - } else { - // add normal term to map. - qry_terms.insert(std::make_pair( - std::string(i.value("negated", false) ? "Not " : "") + - i.at("term").get(), - i)); - } - } - } - // set defaults if not specified - if (query["context"]["visual_source"].empty()) - query["context"]["visual_source"] = "SG Movie"; - if (query["context"]["audio_source"].empty()) - query["context"]["audio_source"] = query["context"]["visual_source"]; - - // set order by - if (order_by.empty()) { - order_by.emplace_back("-created_at"); - } - query["order"] = order_by; - - // add terms we always want. - qry.push_back(Number("project.Project.id").is(project_id)); - - if (context == "Playlists") { - } else if (entity == "Versions") { - qry.push_back(Text("sg_deleted").is_null()); - qry.push_back(FilterBy().Or( - Text("sg_path_to_movie").is_not_null(), - Text("sg_path_to_frames").is_not_null())); - } else if (entity == "Notes") { - } - - // create OR group for multiples of same term. - std::string key; - FilterBy *dest = &qry; - for (const auto &i : qry_terms) { - if (key != i.first) { - key = i.first; - // multiple identical terms OR / AND them.. - if (qry_terms.count(key) > 1) { - if (starts_with(key, "Not ") or starts_with(key, "Exclude ")) - qry.push_back(FilterBy(BoolOperator::AND)); - else - qry.push_back(FilterBy(BoolOperator::OR)); - dest = &std::get(qry.back()); - } else { - dest = &qry; - } - } - try { - // addTerm(project_id, context, dest, i.second); - } catch (const std::exception &err) { - // spdlog::warn("{}", err.what()); - // bad term.. we ignore them.. - - // if(i.second.value("livelink", false)) - // throw XStudioError(std::string("LiveLink ") + err.what()); - - // throw; - } - } - - query["query"] = qry; - - } catch (const std::exception &err) { - throw; - } - - return query; -} - -utility::JsonStore QueryEngine::merge_query( - const utility::JsonStore &base, - const utility::JsonStore &override, - const bool ignore_duplicates) { - auto result = base; - - // we need to preprocess for Disable Global flags.. - auto disable_globals = std::set(); - - for (const auto &i : result) { - if (i.at("enabled").get() and i.at("term") == "Disable Global") - disable_globals.insert(i.at("value").get()); - } - - // if term already exists in dst, then don't append. - if (ignore_duplicates) { - auto dup = std::set(); - for (const auto &i : result) - if (i.at("enabled").get()) - dup.insert(i.at("term").get()); - - for (const auto &i : override) { - auto term = i.at("term").get(); - if (not dup.count(term) and not disable_globals.count(term)) - result.push_back(i); - } - } else { - for (const auto &i : override) { - auto term = i.at("term").get(); - if (not disable_globals.count(term)) - result.push_back(i); - } - } - - return result; -} diff --git a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_query_engine.hpp b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_query_engine.hpp deleted file mode 100644 index dcce88255..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_query_engine.hpp +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include - -#include "xstudio/utility/json_store.hpp" - -using namespace xstudio; - -const auto TermTemplate = R"({ - "id": null, - "type": "term", - "term": "", - "value": "", - "dynamic": false, - "enabled": true, - "livelink": null, - "negated": null -})"_json; - -const auto PresetTemplate = R"({ - "id": null, - "type": "preset", - "name": "PRESET", - "children": [] -})"_json; - -const auto GroupTemplate = R"({ - "id": null, - "type": "group", - "name": "GROUP", - "entity": "", - "children": [ - null, - { - "id": null, - "type": "presets", - "children": [] - } - ] -})"_json; - -const auto RootTemplate = R"({ - "id": null, - "name": "root", - "type": "root", - "children": [] -})"_json; - - -class QueryEngine { - public: - QueryEngine() = default; - virtual ~QueryEngine() = default; - - static utility::JsonStore build_query( - const int project_id, - const std::string &entity, - const utility::JsonStore &group_terms, - const utility::JsonStore &terms, - const utility::JsonStore &context, - const utility::JsonStore &lookup); - - static utility::JsonStore merge_query( - const utility::JsonStore &base, - const utility::JsonStore &override, - const bool ignore_duplicates = true); - - private: - utility::JsonStore data_; -}; \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_worker.cpp b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_worker.cpp deleted file mode 100644 index 4a8dd4bc6..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_worker.cpp +++ /dev/null @@ -1,259 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include "data_source_shotgun_worker.hpp" -#include "data_source_shotgun_definitions.hpp" - -#include "xstudio/atoms.hpp" -#include "xstudio/event/event.hpp" -#include "xstudio/media/media_actor.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/uuid.hpp" - -using namespace xstudio; -using namespace xstudio::utility; - -void ShotgunMediaWorker::add_media_step_1( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const FrameRate &media_rate) { - request( - actor_cast(this), - infinite, - media::add_media_source_atom_v, - jsn, - media_rate, - true) - .then( - [=](const UuidActor &movie_source) mutable { - add_media_step_2(rp, media, jsn, media_rate, movie_source); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); -} - -void ShotgunMediaWorker::add_media_step_2( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const FrameRate &media_rate, - const UuidActor &movie_source) { - // now get image.. - request( - actor_cast(this), infinite, media::add_media_source_atom_v, jsn, media_rate) - .then( - [=](const UuidActor &image_source) mutable { - // check to see if what we've got.. - // failed... - if (movie_source.uuid().is_null() and image_source.uuid().is_null()) { - spdlog::warn("{} No valid sources {}", __PRETTY_FUNCTION__, jsn.dump(2)); - rp.deliver(false); - } else { - try { - UuidActorVector srcs; - - if (not movie_source.uuid().is_null()) - srcs.push_back(movie_source); - if (not image_source.uuid().is_null()) - srcs.push_back(image_source); - - - add_media_step_3(rp, media, jsn, srcs); - - } catch (const std::exception &err) { - spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), jsn.dump(2)); - rp.deliver(make_error(xstudio_error::error, err.what())); - } - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); -} - -void ShotgunMediaWorker::add_media_step_3( - caf::typed_response_promise rp, - caf::actor media, - const JsonStore &jsn, - const UuidActorVector &srcs) { - request(media, infinite, media::add_media_source_atom_v, srcs) - .then( - [=](const bool) mutable { - rp.deliver(true); - // push metadata to media actor. - anon_send( - media, - json_store::set_json_atom_v, - utility::Uuid(), - jsn, - ShotgunMetadataPath + "/version"); - - // dispatch delayed shot data. - try { - auto shotreq = JsonStore(GetShotFromId); - shotreq["shot_id"] = - jsn.at("relationships").at("entity").at("data").value("id", 0); - - request( - caf::actor_cast(data_source_), - infinite, - data_source::get_data_atom_v, - shotreq) - .then( - [=](const JsonStore &jsn) mutable { - try { - if (jsn.count("data")) - anon_send( - media, - json_store::set_json_atom_v, - utility::Uuid(), - JsonStore(jsn.at("data")), - ShotgunMetadataPath + "/shot"); - } catch (const std::exception &err) { - spdlog::warn("A {} {}", __PRETTY_FUNCTION__, err.what()); - } - }, - [=](const error &err) mutable { - spdlog::warn("B {} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - } catch (const std::exception &err) { - spdlog::warn("C {} {}", __PRETTY_FUNCTION__, err.what()); - } - }, - [=](error &err) mutable { - spdlog::warn("D {} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); -} - - -ShotgunMediaWorker::ShotgunMediaWorker(caf::actor_config &cfg, const caf::actor_addr source) - : data_source_(std::move(source)), caf::event_based_actor(cfg) { - - // for each input we spawn one media item with upto two media sources. - - - behavior_.assign( - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - // movie - [=](media::add_media_source_atom, - const JsonStore &jsn, - const FrameRate &media_rate, - const bool /*movie*/) -> result { - auto rp = make_response_promise(); - try { - if (not jsn.at("attributes").at("sg_path_to_movie").is_null()) { - // spdlog::info("{}", i["attributes"]["sg_path_to_movie"]); - // prescan movie for duration.. - // it may contain slate, which needs trimming.. - // SLOW do we want to be offsetting the movie ? - // if we keep this code is needs threading.. - auto uri = posix_path_to_uri(jsn["attributes"]["sg_path_to_movie"]); - const auto source_uuid = Uuid::generate(); - auto source = spawn( - "SG Movie", uri, media_rate, source_uuid); - - request(source, infinite, media::acquire_media_detail_atom_v, media_rate) - .then( - [=](bool) mutable { rp.deliver(UuidActor(source_uuid, source)); }, - [=](error &err) mutable { - // even though there is an error, we want the broken media - // source added so the user can see it in the UI (and its error - // state) - rp.deliver(UuidActor(source_uuid, source)); - }); - - } else { - rp.deliver(UuidActor()); - } - } catch (const std::exception &err) { - spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), jsn.dump(2)); - rp.deliver(UuidActor()); - } - return rp; - }, - - // frames - [=](media::add_media_source_atom, - const JsonStore &jsn, - const FrameRate &media_rate) -> result { - auto rp = make_response_promise(); - try { - if (not jsn.at("attributes").at("sg_path_to_frames").is_null()) { - FrameList frame_list; - caf::uri uri; - - if (jsn.at("attributes").at("frame_range").is_null()) { - // no frame range specified.. - // try and aquire from filesystem.. - uri = parse_cli_posix_path( - jsn.at("attributes").at("sg_path_to_frames"), frame_list, true); - } else { - frame_list = FrameList( - jsn.at("attributes").at("frame_range").template get()); - uri = parse_cli_posix_path( - jsn.at("attributes").at("sg_path_to_frames"), frame_list, false); - } - - const auto source_uuid = Uuid::generate(); - auto source = - frame_list.empty() - ? spawn( - "SG Frames", uri, media_rate, source_uuid) - : spawn( - "SG Frames", uri, frame_list, media_rate, source_uuid); - - request(source, infinite, media::acquire_media_detail_atom_v, media_rate) - .then( - [=](bool) mutable { rp.deliver(UuidActor(source_uuid, source)); }, - [=](error &err) mutable { - // even though there is an error, we want the broken media - // source added so the user can see it in the UI (and its error - // state) - rp.deliver(UuidActor(source_uuid, source)); - }); - } else { - rp.deliver(UuidActor()); - } - } catch (const std::exception &err) { - spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, err.what(), jsn.dump(2)); - rp.deliver(UuidActor()); - } - - return rp; - }, - - [=](playlist::add_media_atom, - caf::actor media, - JsonStore jsn, - const FrameRate &media_rate) -> result { - auto rp = make_response_promise(); - - try { - // do stupid stuff, because data integrity is for losers. - // if we've got a movie in the sg_frames property, swap them over. - if (jsn.at("attributes").at("sg_path_to_movie").is_null() and - not jsn.at("attributes").at("sg_path_to_frames").is_null() and - jsn.at("attributes") - .at("sg_path_to_frames") - .template get() - .find_first_of('#') == std::string::npos) { - // movie in image sequence.. - jsn["attributes"]["sg_path_to_movie"] = - jsn.at("attributes").at("sg_path_to_frames"); - jsn["attributes"]["sg_path_to_frames"] = nullptr; - } - - // request movie .. THESE MUST NOT RETURN error on fail. - add_media_step_1(rp, media, jsn, media_rate); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(xstudio_error::error, err.what())); - } - return rp; - }); -} diff --git a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_worker.hpp b/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_worker.hpp deleted file mode 100644 index a95ec5ab4..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/data_source_shotgun_worker.hpp +++ /dev/null @@ -1,135 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include - -#include "xstudio/event/event.hpp" -#include "xstudio/utility/frame_rate.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/uuid.hpp" - -using namespace xstudio; - -class BuildPlaylistMediaJob { - - public: - BuildPlaylistMediaJob( - caf::actor playlist_actor, - const utility::Uuid &media_uuid, - const std::string media_name, - utility::JsonStore sg_data, - utility::FrameRate media_rate, - std::string preferred_visual_source, - std::string preferred_audio_source, - std::shared_ptr event, - std::shared_ptr ordererd_uuids, - utility::Uuid before, - std::string flag_colour, - std::string flag_text, - caf::typed_response_promise response_promise, - std::shared_ptr result, - std::shared_ptr result_count) - : playlist_actor_(std::move(playlist_actor)), - media_uuid_(media_uuid), - media_name_(media_name), - sg_data_(sg_data), - media_rate_(media_rate), - preferred_visual_source_(std::move(preferred_visual_source)), - preferred_audio_source_(std::move(preferred_audio_source)), - event_msg_(std::move(event)), - ordererd_uuids_(std::move(ordererd_uuids)), - before_(std::move(before)), - flag_colour_(std::move(flag_colour)), - flag_text_(std::move(flag_text)), - response_promise_(std::move(response_promise)), - result_(std::move(result)), - result_count_(result_count) { - // increment a shared counter - the counter is shared between - // all the indiviaual Media creation jobs in a single build playlist - // task - (*result_count)++; - } - - BuildPlaylistMediaJob(const BuildPlaylistMediaJob &o) = default; - BuildPlaylistMediaJob() = default; - - ~BuildPlaylistMediaJob() { - // this gets destroyed when the job is done with. - if (media_actor_) { - result_->push_back(utility::UuidActor(media_uuid_, media_actor_)); - } - // decrement the counter - (*result_count_)--; - - if (!(*result_count_)) { - // counter has dropped to zero, all jobs within a single build playlist - // tas are done. Our 'result' member here is in the order that the - // media items were created (asynchronously), rather than the order - // of the final playlist ... so we need to reorder our 'result' to - // match the ordering in the playlist - utility::UuidActorVector reordered; - reordered.reserve(result_->size()); - for (const auto &uuid : (*ordererd_uuids_)) { - for (auto uai = result_->begin(); uai != result_->end(); uai++) { - if ((*uai).uuid() == uuid) { - reordered.push_back(*uai); - result_->erase(uai); - break; - } - } - } - response_promise_.deliver(reordered); - } - } - - caf::actor playlist_actor_; - utility::Uuid media_uuid_; - std::string media_name_; - utility::JsonStore sg_data_; - utility::FrameRate media_rate_; - std::string preferred_visual_source_; - std::string preferred_audio_source_; - std::shared_ptr event_msg_; - std::shared_ptr ordererd_uuids_; - utility::Uuid before_; - std::string flag_colour_; - std::string flag_text_; - caf::typed_response_promise response_promise_; - std::shared_ptr result_; - std::shared_ptr result_count_; - caf::actor media_actor_; -}; - -class ShotgunMediaWorker : public caf::event_based_actor { - public: - ShotgunMediaWorker(caf::actor_config &cfg, const caf::actor_addr source); - ~ShotgunMediaWorker() override = default; - - const char *name() const override { return NAME.c_str(); } - - private: - inline static const std::string NAME = "ShotgunMediaWorker"; - caf::behavior make_behavior() override { return behavior_; } - - void add_media_step_1( - caf::typed_response_promise rp, - caf::actor media, - const utility::JsonStore &jsn, - const utility::FrameRate &media_rate); - void add_media_step_2( - caf::typed_response_promise rp, - caf::actor media, - const utility::JsonStore &jsn, - const utility::FrameRate &media_rate, - const utility::UuidActor &movie_source); - void add_media_step_3( - caf::typed_response_promise rp, - caf::actor media, - const utility::JsonStore &jsn, - const utility::UuidActorVector &srcs); - - private: - caf::behavior behavior_; - caf::actor_addr data_source_; -}; \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/CMakeLists.txt b/src/plugin/data_source/dneg/shotgun/src/qml/CMakeLists.txt deleted file mode 100644 index b3217bcc6..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/CMakeLists.txt +++ /dev/null @@ -1,65 +0,0 @@ - -set(CMAKE_AUTOUIC ON) -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTORCC ON) - -find_package(Qt5 COMPONENTS Qml Core Quick Gui Widgets OpenGL REQUIRED) -configure_file(.clang-tidy .clang-tidy) - -# find_package(Qt5 COMPONENTS Core Gui Widgets OpenGL QUIET) - -# QT5_ADD_RESOURCES(PROTOTYPE_RCS) -if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpic") - set(DCMAKE_EXE_LINKER_FLAGS "${DCMAKE_EXE_LINKER_FLAGS} -fpic") -endif - -project(data_source_shotgun_ui VERSION 0.1.0 LANGUAGES CXX) - -QT5_WRAP_CPP(DATA_SOURCE_SHOTGUN_UI_MOC_SRC "${CMAKE_CURRENT_SOURCE_DIR}/data_source_shotgun_ui.hpp") -QT5_WRAP_CPP(SHOTGUN_MODEL_UI_MOC_SRC "${CMAKE_CURRENT_SOURCE_DIR}/shotgun_model_ui.hpp") - -set(SOURCES - shotgun_model_ui.cpp - data_source_shotgun_ui.cpp - data_source_shotgun_requests_ui.cpp - data_source_shotgun_query_ui.cpp - ../data_source_shotgun_query_engine.cpp - ${DATA_SOURCE_SHOTGUN_UI_MOC_SRC} - ${SHOTGUN_MODEL_UI_MOC_SRC} -) - -add_library(${PROJECT_NAME} SHARED ${SOURCES}) -default_options_qt(${PROJECT_NAME}) - -target_link_libraries(${PROJECT_NAME} - PUBLIC - xstudio::ui::qml::helper - xstudio::shotgun_client - ${CAF_LIBRARY_core} - Qt5::Core - Qt5::Qml -) - -set_target_properties(${PROJECT_NAME} - PROPERTIES - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/plugin/qml/Shotgun.1" -) - -install(TARGETS ${PROJECT_NAME} - LIBRARY DESTINATION share/xstudio/plugin/qml/Shotgun.1) - -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/Shotgun.1/ DESTINATION share/xstudio/plugin/qml/Shotgun.1) - -file(GLOB QMLFILES ${CMAKE_CURRENT_SOURCE_DIR}/Shotgun.1/*) - -add_custom_target(COPY_QML) - -foreach(QMLFile ${QMLFILES}) - add_custom_command(TARGET COPY_QML POST_BUILD - COMMAND ${CMAKE_COMMAND} -E - copy ${QMLFile} ${CMAKE_BINARY_DIR}/bin/plugin/qml/Shotgun.1/) -endforeach() - - -add_dependencies(${PROJECT_NAME} COPY_QML) diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoiceEdit.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoiceEdit.qml deleted file mode 100644 index e85856ef7..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoiceEdit.qml +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient -import QtQuick.Controls.Styles 1.4 //for TextFieldStyle -import Qt.labs.qmlmodels 1.0 - -import xStudio 1.1 - -DelegateChoice { - roleValue: "Edit" - Rectangle { - property bool isSelected: selectionModel.isSelected(searchResultsViewModel.index(index, 0)) - property bool isMouseHovered: mArea.containsMouse - width: searchResultsView.cellWidth-itemSpacing - height: searchResultsView.cellHeight-itemSpacing - color: isSelected? Qt.darker(itemColorActive, 2.75): itemColorNormal - border.color: isMouseHovered? itemColorActive: itemColorNormal - clip: true - - Connections { - target: selectionModel - function onSelectionChanged(selected, deselected) { - isSelected = selectionModel.isSelected(searchResultsViewModel.index(index, 0)) - } - } - - MouseArea{ - id: mArea - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: { - searchResultsDiv.itemClicked(mouse, index, isSelected) - } - - } - - Item{ - width: parent.width; height: parent.height - - Text{ id: txt1 - text: nameRole - font.pixelSize: fontSize*1.2 - font.family: fontFamily - font.bold: true - color: isMouseHovered? textColorActive: textColorNormal - width: parent.width - elide: Text.ElideRight - horizontalAlignment: Text.AlignHCenter - anchors.centerIn: parent - } - } - } -} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoiceNote.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoiceNote.qml deleted file mode 100644 index 790aabab4..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoiceNote.qml +++ /dev/null @@ -1,511 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient -import QtQuick.Controls.Styles 1.4 //for TextFieldStyle -import Qt.labs.qmlmodels 1.0 - -import xStudio 1.1 - -DelegateChoice { - roleValue: "Note" - - Rectangle { - id: root - property bool isSelected: selectionModel.isSelected(searchResultsViewModel.index(index, 0)) - property bool isMouseHovered: mArea.containsMouse || nameDisplayText.hovered || toToolTip.containsMouse || fromToolTip.containsMouse || - dateToolTip.containsMouse || bodyToolTip.containsMouse || subjectToolTip.containsMouse - width: searchResultsView.cellWidth - height: searchResultsView.cellHeight-itemSpacing - color: isSelected? Qt.darker(itemColorActive, 2.50): "#2e2e2e" - border.color: isMouseHovered? itemColorActive: itemColorNormal - clip: true - - Connections { - target: selectionModel - function onSelectionChanged(selected, deselected) { - isSelected = selectionModel.isSelected(searchResultsViewModel.index(index, 0)) - } - } - - MouseArea{ - id: mArea - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: { - searchResultsDiv.itemClicked(mouse, index, isSelected) - } - onDoubleClicked: { - if (mouse.button == Qt.LeftButton) { - selectionModel.select(searchResultsViewModel.index(index, 0), ItemSelectionModel.ClearAndSelect) - selectionModel.resetSelection() - selectionModel.countSelection(true) - selectionModel.prevSelectedIndex = index - applySelection(function(items){ - addShotsToPlaylist(items, - data_source.preferredVisual(currentCategory), - data_source.preferredAudio(currentCategory), - data_source.flagText(currentCategory), - data_source.flagColour(currentCategory) - ) - }) - } - } - } - - - GridLayout { - anchors.fill: parent - anchors.margins: framePadding - rows: 7 - columns: 3 - - Rectangle{ id: infoDisplay - Layout.rowSpan: 7 - Layout.columnSpan: 1 - Layout.alignment: Qt.AlignLeft - Layout.preferredWidth: 150 - Layout.maximumWidth: 170 - Layout.preferredHeight: parent.height - color: root.isSelected? Qt.darker(itemColorActive, 3):"#242424" - - GridLayout { - anchors.fill: parent - anchors.margins: framePadding - rows: 7 - columns: 2 - - Item{ - id: mainImage - Layout.fillWidth: true - Layout.preferredHeight: parent.height/2 - Layout.rowSpan: 3 - Layout.columnSpan: 2 - - property bool failed: thumbRole == undefined - - Text{ id: noThumbnailDisplay - text: parent.failed? "No ShotGrid Thumbnail":"Loading..." - width: parent.width - height: parent.height - wrapMode: Text.WordWrap - font.pixelSize: fontSize*1.2 - font.weight: Font.Black - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: textColorNormal - opacity: parent.failed? 0.4 : 0.9 - } - Image { id: thumbnail - visible: !parent.failed - anchors.fill: parent - source: parent.failed ? "": thumbRole - - cache: true - asynchronous: true - sourceSize.height: height - sourceSize.width: width - opacity: 0 - Behavior on opacity {NumberAnimation {duration: 150}} - - onStatusChanged: { - if (status == Image.Error) { parent.failed=true; opacity=0 } - else if (status == Image.Ready) opacity=1 - else opacity=0 - } - } - } - - Text{ - text: "Date :" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - } - - Text{ id: dateDisplay - Layout.alignment: Qt.AlignRight - property var dateFormatted: createdDateRole.toLocaleString().split(" ") - text: typeof dateFormatted !== 'undefined'? dateFormatted[1].substr(0,3)+" "+dateFormatted[2]+" "+dateFormatted[3] : "" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignRight - - ToolTip.text: dateFormatted[0].substr(0,dateFormatted[0].length-1) //createdDateRole.toLocaleString().substr(0,3) - ToolTip.visible: dateToolTip.containsMouse - MouseArea { - id: dateToolTip - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } - } - - Text{ - text: "Time :" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - } - - Text{ id: timeDisplay - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - property var dateFormatted: createdDateRole.toLocaleString().split(" ") - property var timeFormatted: dateFormatted[4].split(":") - text: typeof timeFormatted !== 'undefined'? - typeof dateFormatted[6] !== 'undefined'? - timeFormatted[0]+":"+timeFormatted[1]+" "+dateFormatted[5]+" "+dateFormatted[6] : - timeFormatted[0]+":"+timeFormatted[1]+" "+dateFormatted[5] : "" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignRight - } - - Text{ - text: "Type :" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - } - - Text{ id: typeDisplay - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - // Layout.fillWidth: true - text: noteTypeRole ? noteTypeRole : "" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - elide: Text.ElideRight - horizontalAlignment: Text.AlignRight - } - - - Text{ - text: "From :" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - Layout.preferredHeight: typeDisplay.height - } - - Text{ - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - text: createdByRole ? createdByRole : "" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignRight - Layout.fillWidth: true - Layout.preferredHeight: typeDisplay.height - - XsToolTip{ - text: parent.text - visible: fromToolTip.containsMouse && parent.truncated - width: textDivMetrics.width == 0? 0 : 150 - x: 0 - } - MouseArea { - id: fromToolTip - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } - } - - Text{ - text: "To :" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - Layout.preferredHeight: typeDisplay.height - } - - Text{ - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - text: addressingRole ? addressingRole.join("\n") : "" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignRight - Layout.fillWidth: true - Layout.preferredHeight: typeDisplay.height - - XsToolTip{ - text: parent.text - visible: toToolTip.containsMouse && parent.truncated - width: textDivMetrics.width == 0? 0 : 150 - x: 0 - } - MouseArea { - id: toToolTip - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } - } - - } - } - - Rectangle{ id: nameDisplay - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - Layout.minimumWidth: 120 - Layout.columnSpan: 1 - Layout.preferredHeight: itemHeight - color: root.isSelected? Qt.darker(itemColorActive, 3):"#242424" //"transparent" - - XsTextButton{ id: nameDisplayText - text: versionNameRole || "" - - anchors.left: nameDisplay.left - anchors.leftMargin: framePadding - width: nameDisplay.width - framePadding*2 - - textDiv.font.pixelSize: fontSize*1.2 - textDiv.font.weight: Font.Bold - textDiv.elide: Text.ElideMiddle - textDiv.horizontalAlignment: Text.AlignLeft - // textDiv.verticalAlignment: Text.AlignVCenter - anchors.bottom: nameDisplay.bottom - anchors.bottomMargin: (parent.height - height)/2 - 1 - forcedMouseHover: isMouseHovered - isClickable: shotNameRole !== undefined - - onTextClicked: { - let mymodel = noteTreePresetsModel - // mymodel.clearLoaded() - mymodel.insert( - mymodel.rowCount(), - mymodel.index(mymodel.rowCount(), 0), - { - "expanded": false, - "name": shotNameRole + " Notes", - "queries": [ - { - "enabled": true, - "term": "Twig Name", - "value": "^"+twigNameRole+"$" - }, - { - "enabled": true, - "term": "Shot", - "value": shotNameRole - }, - { - "enabled": false, - "term": "Version Name", - "value": versionNameRole - }, - { - "enabled": false, - "term": "Note Type", - "value": "Director" - }, - { - "enabled": false, - "term": "Note Type", - "value": "Client" - }, - { - "enabled": false, - "term": "Note Type", - "value": "VFXSupe" - }, - { - "enabled": false, - "term": "Recipient", - "value": data_source.getShotgunUserName() - } - ] - } - ) - - currentCategory = "Notes Tree" - mymodel.activePreset = mymodel.rowCount()-1 - } - - ToolTip.text: versionNameRole || "" - ToolTip.visible: hovered & textDiv.truncated - } - } - - Rectangle{ id: thumbnailsDisplay - Layout.rowSpan: 7 - Layout.columnSpan: 1 - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredWidth: mainImage.width/2.2 //nameDisplay.width/5 - Layout.preferredHeight: parent.height - color: root.isSelected? Qt.darker(itemColorActive, 3):"#242424" - - // ColumnLayout{ - // spacing: framePadding - // anchors.horizontalCenter: parent.horizontalCenter - // y: framePadding - - // Repeater{ - // model: Math.floor(Math.random() * 3) + 1; // #TODO - // Image { - // width: thumbnailsDisplay.width - framePadding*2 - // height: width/2 - // property bool failed: false - // source: !parent.visible ? "" : ( failed ? "qrc:/feather_icons/film.svg": thumbRole ) - // asynchronous: true - // sourceSize.height: height - // sourceSize.width: width - // onStatusChanged: if (status == Image.Error) failed=true - // layer { - // enabled: failed - // effect: ColorOverlay { - // color: XsStyle.mainBackground - // } - // } - // } - // } - // } - - } - - - Rectangle{ id: subjectDisplay - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - Layout.preferredWidth: 100 - Layout.fillWidth: true - Layout.preferredHeight: itemHeight - color: root.isSelected? Qt.darker(itemColorActive, 3):"#242424" - - Text{ - text: subjectRole ? subjectRole : "" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - anchors.bottom: parent.bottom - anchors.bottomMargin: (parent.height - height)/2 - 1 - padding: framePadding-.1 - width: parent.width - - XsToolTip{ - text: parent.text - visible: subjectToolTip.containsMouse & parent.truncated - width: textDivMetrics.width == 0? 0 : parent.width>500? 520:parent.width+20 - x: 0 - } - MouseArea { - id: subjectToolTip - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } - } - } - - Rectangle{ id: bodyDisplay - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - Layout.preferredWidth: 100 - Layout.fillWidth: true - Layout.fillHeight: true - Layout.rowSpan: 5 - color: root.isSelected? Qt.darker(itemColorActive, 3):"#242424" - - XsTextEdit{ id: bodyTextDisplay - text: contentRole ? contentRole : "" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignTop - padding: framePadding- 0.1 - readOnly: true - width: parent.width - height: parent.height - wrapMode: TextEdit.Wrap - textFormat: TextEdit.AutoText - textDiv.height: parent.height - - XsToolTip{ - id: toolTip - text: bodyTextDisplay.text - visible: bodyToolTip.containsMouse && bodyTextDisplay.lineCount>7 - width: textDivMetrics.width == 0? 0 : bodyTextDisplay.width>500? 520: bodyTextDisplay.width+20 - x: 0 - } - MouseArea { - id: bodyToolTip - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } - } - Rectangle{anchors.fill: overlapImage; color: bodyDisplay.color; radius: 2; visible: overlapImage.visible} - Image { - id: overlapImage - visible: bodyTextDisplay.lineCount>7 - source: "qrc:/feather_icons/corner-down-left.svg" - width: itemHeight/2 - height: itemHeight/2 - sourceSize.width: width - sourceSize.height: height - anchors.right: parent.right - anchors.rightMargin: 2 - anchors.bottom: parent.bottom - anchors.bottomMargin: 2 - smooth: true - opacity: 0.7 - layer { - enabled: true - effect: - ColorOverlay { - color: toolTip.visible? palette.highlight : bodyTextDisplay.color - } - } - } - } - - } - } -} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoicePlaylist.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoicePlaylist.qml deleted file mode 100644 index d28f85867..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoicePlaylist.qml +++ /dev/null @@ -1,305 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient -import QtQuick.Controls.Styles 1.4 //for TextFieldStyle -import Qt.labs.qmlmodels 1.0 - -import xStudio 1.1 - -DelegateChoice { - roleValue: "Playlist" - Rectangle { - property bool isSelected: selectionModel.isSelected(searchResultsViewModel.index(index, 0)) - property bool isMouseHovered: mArea.containsMouse || noteDisplay.hovered || - dateToolTip.containsMouse || typeToolTip.containsMouse || - nameToolTip.containsMouse || deptToolTip.containsMouse || - authorToolTip.containsMouse || versionsButton.hovered - width: searchResultsView.cellWidth - height: searchResultsView.cellHeight-itemSpacing - color: isSelected? Qt.darker(itemColorActive, 2.75): itemColorNormal - border.color: isMouseHovered? itemColorActive: itemColorNormal - clip: true - - Connections { - target: selectionModel - function onSelectionChanged(selected, deselected) { - isSelected = selectionModel.isSelected(searchResultsViewModel.index(index, 0)) - } - } - - MouseArea{ - id: mArea - anchors.fill: parent - hoverEnabled: true - - acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: { - searchResultsDiv.itemClicked(mouse, index, isSelected) - } - - onDoubleClicked: { - if (mouse.button == Qt.LeftButton) { - selectionModel.select(searchResultsViewModel.index(index, 0), ItemSelectionModel.ClearAndSelect) - selectionModel.resetSelection() - selectionModel.countSelection(true) - selectionModel.prevSelectedIndex = index - applySelection( - function(items){ - loadPlaylists( - items, - data_source.preferredVisual(currentCategory), - data_source.preferredAudio(currentCategory), - data_source.flagText(currentCategory), - data_source.flagColour(currentCategory) - ) - } - ) - } - } - } - - - GridLayout { - anchors.fill: parent - anchors.margins: framePadding - rows: 2 - columns: 7 - - XsButton{ id: noteDisplay - property bool hasNotes: noteCountRole === undefined || noteCountRole == 0 ? false :true - text: "N" - Layout.preferredWidth: 20 - Layout.fillHeight: true - Layout.rowSpan: 2 - font.pixelSize: fontSize*1.2 - font.weight: Font.Medium - focus: false - enabled: hasNotes - bgColorNormal: hasNotes ? "#c27b02" : palette.base - textDiv.topPadding: 3 - - onClicked: { - let mymodel = noteTreePresetsModel - - mymodel.clearLoaded() - mymodel.insert( - mymodel.rowCount(), - mymodel.index(mymodel.rowCount(), 0), - { - "expanded": false, - "name": nameRole + " Notes", - "queries": [ - { - "enabled": true, - "term": "Playlist", - "value": nameRole - }, - { - "enabled": false, - "term": "Note Type", - "value": "Director" - }, - { - "enabled": false, - "term": "Note Type", - "value": "Client" - }, - { - "enabled": false, - "term": "Note Type", - "value": "VFXSupe" - }, - { - "enabled": false, - "term": "Recipient", - "value": data_source.getShotgunUserName() - } - ] - } - ) - currentCategory = "Notes Tree" - mymodel.activePreset = mymodel.rowCount()-1 - } - } - - XsButton{ id: versionsButton - Layout.preferredWidth: noteDisplay.width - Layout.preferredHeight: parent.height - Layout.rowSpan: 2 - - text: "Versions" - textDiv.width: parent.height - textDiv.opacity: hovered ? 1 : isMouseHovered? 0.8 : 0.6 - textDiv.rotation: -90 - textDiv.topPadding: 2.5 - textDiv.rightPadding: 3 - font.pixelSize: fontSize - font.weight: Font.DemiBold - padding: 0 - bgDiv.border.color: down || hovered ? bgColorPressed: Qt.darker(bgColorNormal,1.5) - onClicked: { - let mymodel = shotPresetsModel - mymodel.clearLoaded() - mymodel.insert( - mymodel.rowCount(), - mymodel.index(mymodel.rowCount(), 0), - { - "expanded": false, - "name": nameRole + " Versions", - "queries": [ - { - "enabled": true, - "term": "Playlist", - "value": nameRole - } - ] - } - ) - currentCategory = "Versions" - mymodel.activePreset = mymodel.rowCount()-1 - } - } - - Text{ id: countDisplay - text: itemCountRole - color: isMouseHovered? textColorActive: textColorNormal - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - Layout.preferredWidth: parent.height + 10 - Layout.maximumWidth: parent.height + 10 - Layout.preferredHeight: parent.height - Layout.rowSpan: 2 - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - - font.pixelSize: fontSize*1.2 - font.family: fontFamily - font.bold: true - } - - Text{ id: nameDisplay - text: nameRole || "" - Layout.columnSpan: 3 - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - font.pixelSize: fontSize*1.2 - font.family: fontFamily - font.bold: true - color: isMouseHovered? textColorActive: textColorNormal - elide: Text.ElideMiddle - horizontalAlignment: Text.AlignLeft - - ToolTip.text: text - ToolTip.visible: nameToolTip.containsMouse && truncated - MouseArea { - id: nameToolTip - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } - } - - Text{ - text: departmentRole || "" - Layout.alignment: Qt.AlignRight - Layout.preferredWidth: 140 - Layout.maximumWidth: 200 - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignRight - - ToolTip.text: text - ToolTip.visible: deptToolTip.containsMouse && truncated - MouseArea { - id: deptToolTip - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } - } - - Text{ id: typeDisplay - text: typeRole || "" - Layout.column: 3 - Layout.row: 1 - Layout.alignment: Qt.AlignLeft - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - Layout.preferredWidth: 140 - Layout.maximumWidth: 140 - - ToolTip.text: typeRole || "" - ToolTip.visible: typeToolTip.containsMouse && truncated - MouseArea { - id: typeToolTip - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } - } - - Text{ id: dateDisplay - Layout.alignment: Qt.AlignLeft - Layout.minimumWidth: 68 - Layout.preferredWidth: 76 - Layout.maximumWidth: 76 - property var dateFormatted: createdDateRole.toLocaleString().split(" ") - text: typeof dateFormatted !== 'undefined'? dateFormatted[1].substr(0,3)+" "+dateFormatted[2]+" "+dateFormatted[3] : "" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - - ToolTip.text: createdDateRole.toLocaleString() - ToolTip.visible: dateToolTip.containsMouse - MouseArea { - id: dateToolTip - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } - } - - Item{ - id: emptyDiv - Layout.preferredHeight: 1 - Layout.fillWidth: true - } - - Text{ - text: authorRole || "" - Layout.alignment: Qt.AlignRight - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignRight - - ToolTip.text: text - ToolTip.visible: authorToolTip.containsMouse && truncated - MouseArea { - id: authorToolTip - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } - } - } - } -} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoiceReference.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoiceReference.qml deleted file mode 100644 index 365592add..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoiceReference.qml +++ /dev/null @@ -1,423 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient -import QtQuick.Controls.Styles 1.4 //for TextFieldStyle -import Qt.labs.qmlmodels 1.0 - -import xStudio 1.1 - -DelegateChoice { - roleValue: "Reference" - Rectangle { id: shotDelegate - property bool isSelected: selectionModel.isSelected(searchResultsViewModel.index(index, 0)) - property bool isMouseHovered: mArea.containsMouse || nameDisplay.hovered || stepDisplay.hovered || - authorDisplay.hovered || pipelineStatusDisplay.hovered || - pipeStatusDisplay.hovered || submitToClientDisplay.hovered || - frameToolTip.containsMouse || dateToolTip.containsMouse// || versionsButton.hovered || allVersionsButton.hovered - width: searchResultsView.cellWidth - height: searchResultsView.cellHeight-itemSpacing - color: isSelected ? Qt.darker(itemColorActive, 2.75): itemColorNormal - border.color: isMouseHovered? itemColorActive: itemColorNormal - clip: true - - Connections { - target: selectionModel - function onSelectionChanged(selected, deselected) { - isSelected = selectionModel.isSelected(searchResultsViewModel.index(index, 0)) - } - } - - MouseArea{ - id: mArea - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - onDoubleClicked: { - if (mouse.button == Qt.LeftButton) { - selectionModel.select(searchResultsViewModel.index(index, 0), ItemSelectionModel.ClearAndSelect) - selectionModel.resetSelection() - selectionModel.countSelection(true) - selectionModel.prevSelectedIndex = index - applySelection( - function(items){ - addShotsToPlaylist( - items, - data_source.preferredVisual("Versions"), - data_source.preferredAudio("Versions"), - data_source.flagText("Versions"), - data_source.flagColour("Versions") - ) - } - ) - } - } - - onClicked: { - searchResultsDiv.itemClicked(mouse, index, isSelected) - } - } - - GridLayout { - anchors.fill: parent - anchors.margins: framePadding - rows: 2 - columns: 8 - rowSpacing: itemSpacing - - Rectangle{ id: indicators - - Layout.preferredWidth: 20 - Layout.fillHeight: true - Layout.columnSpan: 1 - Layout.rowSpan: 2 - color: "transparent" - - Column{ - spacing: itemSpacing - anchors.fill: parent - y: itemSpacing - - XsButton{ id: pipeStatusDisplay - property bool hasNotes: noteCountRole === undefined || noteCountRole == 0 ? false :true - text: "N" - width: 20 - height: parent.height/3 - itemSpacing/2 - font.pixelSize: fontSize*1.2 - font.weight: Font.Medium - focus: false - enabled: hasNotes - bgColorNormal: hasNotes ? "#c27b02" : palette.base - textDiv.topPadding: 2.5 - - onClicked: { - let mymodel = noteTreePresetsModel - mymodel.clearLoaded() - mymodel.insert( - mymodel.rowCount(), - mymodel.index(mymodel.rowCount(),0), - { - "expanded": false, - "name": nameRole + " Notes", - "queries": [ - { - "enabled": true, - "term": "Twig Name", - "value": "^"+twigNameRole+"$" - }, - { - "enabled": shotRole != undefined, - "term": "Shot", - "value": shotRole != undefined ? shotRole : "" - }, - { - "enabled": true, - "term": "Version Name", - "value": nameRole - }, - { - "enabled": false, - "term": "Note Type", - "value": "Director" - }, - { - "enabled": false, - "term": "Note Type", - "value": "Client" - }, - { - "enabled": false, - "term": "Note Type", - "value": "VFXSupe" - } - ] - } - ) - currentCategory = "Notes Tree" - mymodel.activePreset = mymodel.rowCount()-1 - } - } - XsButton{ id: dailiesStatusDisplay - property bool hasDailes: submittedToDailiesRole === undefined ? false :true - text: "D" - width: pipeStatusDisplay.width - height: pipeStatusDisplay.height - font.pixelSize: fontSize*1.2 - font.weight: Font.Medium - focus: false - enabled: false //hasDailes - bgColorNormal: hasDailes ? "#2b7ffc" : palette.base - textDiv.topPadding: 2.5 - } - XsButton{ id: submitToClientDisplay - property bool isSent: dateSubmittedToClientRole === undefined ? false : true - enabled: false //isSent - text: "C" - width: pipeStatusDisplay.width - height: pipeStatusDisplay.height - font.pixelSize: fontSize*1.2 - font.weight: Font.DemiBold - focus: false - bgColorNormal: isSent ? "green" : palette.base - textDiv.topPadding: 2.5 - - onClicked: createPreset("Sent To Client", "True") - } - - } - - } - Item{ id: thumbnail - Layout.preferredWidth: parent.height * 1.5 - Layout.fillHeight: true - Layout.rowSpan: 2 - - property bool failed: thumbRole == undefined - - Text{ id: noThumbnailDisplay - text: parent.failed ? "No ShotGrid Thumbnail" : "Loading..." - width: parent.width - height: parent.height - wrapMode: Text.WordWrap - font.pixelSize: fontSize*1.2 - font.weight: Font.Black - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: isSelected? Qt.darker("dark grey",1) : parent.failed? XsStyle.mainBackground : textColorNormal - opacity: isSelected? 0.3 : 0.9 - } - Image { - visible: !parent.failed - anchors.fill: parent - source: parent.failed ? "" : thumbRole - - asynchronous: true - cache: true - sourceSize.height: height - sourceSize.width: width - opacity: 0 - Behavior on opacity {NumberAnimation {duration: 150}} - - onStatusChanged: { - if (status == Image.Error) { parent.failed=true; opacity=0 } - else if (status == Image.Ready) opacity=1 - else opacity=0 - } - } - } - - XsTextButton{ id: nameDisplay - text: " "+nameRole - isClickable: false - onTextClicked: createPreset("Twig Name", twigNameRole) - Layout.columnSpan: stepDisplay.visible? 3 : 5 - - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - // Layout.preferredWidth: 150 - textDiv.font.pixelSize: fontSize*1.2 - textDiv.font.weight: Font.Medium - textDiv.elide: Text.ElideMiddle - textDiv.horizontalAlignment: Text.AlignLeft - textDiv.verticalAlignment: Text.AlignVCenter - forcedMouseHover: isMouseHovered - - ToolTip.text: nameRole - ToolTip.visible: hovered && textDiv.truncated - } - - XsTextButton{ id: stepDisplay - visible: text != "" - text: pipelineStepRole ? pipelineStepRole : "" - isClickable: false - textDiv.font.pixelSize: fontSize*1.2 - textDiv.font.weight: Font.Medium - textDiv.elide: Text.ElideRight - textDiv.horizontalAlignment: Text.AlignRight - textDiv.rightPadding: 3 - forcedMouseHover: isMouseHovered - Layout.alignment: Qt.AlignRight - Layout.fillWidth: true - Layout.minimumWidth: 65 - Layout.preferredWidth: 70 - Layout.maximumWidth: 92 - Layout.columnSpan: 2 - - ToolTip.text: text - ToolTip.visible: hovered && textDiv.truncated - - onTextClicked: createPreset("Pipeline Step", textDiv.text) - } - - Rectangle{ id: siteDisplay - Layout.fillWidth: true - Layout.minimumWidth: 50 - Layout.preferredWidth: 83 - Layout.maximumWidth: 93 - Layout.fillHeight: true - Layout.columnSpan: 1 - Layout.rowSpan: 2 - color: "transparent" - - Grid{ id: col - spacing: itemSpacing/3 - rows: 3 - columns: 2 - flow: Grid.TopToBottom - anchors.fill: parent - - Repeater{ - model: siteModel //["chn","lon","mtl","mum","syd",van"] - - XsButton{ id: onDiskDisplay - property int onDisk: { - if(index==0) onSiteChn - else if(index==1) onSiteLon - else if(index==2) onSiteMtl - else if(index==3) onSiteMum - else if(index==4) onSiteSyd - else if(index==5) onSiteVan - else false - } - text: siteName - width: siteDisplay.width/2 - height: siteDisplay.height/3 -col.spacing - font.pixelSize: fontSize/1.2 - font.weight: Font.Medium - focus: false - enabled: false - borderWidth: 0 - bgColorNormal: onDisk ? Qt.darker(siteColour, onDisk == 1 ? 1.5:1.0) : palette.base - textDiv.topPadding: 2 - } - } - ListModel{ - id: siteModel - ListElement{siteName:"chn"; siteColour:"#508f00"} - ListElement{siteName:"lon"; siteColour:"#2b7ffc"} - ListElement{siteName:"mtl"; siteColour:"#979700"} - ListElement{siteName:"mum"; siteColour:"#ef9526"} - ListElement{siteName:"syd"; siteColour:"#008a46"} - ListElement{siteName:"van"; siteColour:"#7a1a39"} - } - } - - } - - - - Text{ id: dateDisplay - Layout.alignment: Qt.AlignLeft - // Layout.fillWidth: true - property var dateFormatted: createdDateRole.toLocaleString().split(" ") - text: typeof dateFormatted !== 'undefined'? dateFormatted[1].substr(0,3)+" "+dateFormatted[2]+" "+dateFormatted[3] : "" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - Layout.fillWidth: true - Layout.minimumWidth: 68 - Layout.preferredWidth: 74 - Layout.maximumWidth: 74 - - ToolTip.text: createdDateRole.toLocaleString() - ToolTip.visible: dateToolTip.containsMouse //&& truncated - MouseArea { - id: dateToolTip - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } - } - - Text{ id: frameDisplay - text: frameRangeRole ? frameRangeRole : "" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignHCenter - Layout.alignment: Qt.AlignHCenter - Layout.fillWidth: true - Layout.minimumWidth: 60 - Layout.preferredWidth: 80 - Layout.maximumWidth: 80 - - ToolTip.text: frameRangeRole ? frameRangeRole : "" - ToolTip.visible: frameToolTip.containsMouse && truncated - MouseArea { - id: frameToolTip - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } - } - - XsTextButton{ id: authorDisplay - text: authorRole? authorRole : "" - isClickable: false - textDiv.font.pixelSize: fontSize - opacity: 0.6 - textDiv.elide: Text.ElideRight - textDiv.horizontalAlignment: Text.AlignLeft - forcedMouseHover: isMouseHovered - Layout.fillWidth: true - Layout.alignment: Qt.AlignLeft - - onTextClicked: createPreset("Author", textDiv.text) - - ToolTip.text: authorRole - ToolTip.visible: hovered && textDiv.truncated - } - - - XsTextButton{ - text: tagRole ? ""+tagRole : "" - isClickable: false - textDiv.font.pixelSize: fontSize - opacity: 0.6 - textDiv.elide: Text.ElideRight - textDiv.horizontalAlignment: Text.AlignLeft - forcedMouseHover: isMouseHovered - Layout.fillWidth: true - Layout.alignment: Qt.AlignLeft - // Layout.columnSpan: - // ToolTip.text: tagRole - ToolTip.text: tagRole ? tagRole.join("\n") : "" - ToolTip.visible: hovered && textDiv.truncated - } - - XsTextButton{ id: pipelineStatusDisplay - text: pipelineStatusFullRole? pipelineStatusFullRole : "" - isClickable: false - - Layout.alignment: Qt.AlignRight - Layout.columnSpan: 1 - Layout.fillWidth: true - Layout.minimumWidth: 50 - Layout.preferredWidth: 60 - Layout.maximumWidth: 120 - textDiv.font.pixelSize: fontSize - opacity: 0.6 - textDiv.elide: Text.ElideRight - textDiv.horizontalAlignment: Text.AlignRight - textDiv.verticalAlignment: Text.AlignVCenter - textDiv.rightPadding: 3 - forcedMouseHover: isMouseHovered - - ToolTip.text: pipelineStatusFullRole - ToolTip.visible: hovered && textDiv.truncated - } - - } - } -} diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoiceShot.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoiceShot.qml deleted file mode 100644 index be88207bf..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/DelegateChoiceShot.qml +++ /dev/null @@ -1,446 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient -import QtQuick.Controls.Styles 1.4 //for TextFieldStyle -import Qt.labs.qmlmodels 1.0 - -import xStudio 1.1 - -DelegateChoice { - roleValue: "Shot" - Rectangle { id: shotDelegate - property bool isSelected: selectionModel.isSelected(searchResultsViewModel.index(index, 0)) - property bool isMouseHovered: mArea.containsMouse || nameDisplay.hovered || stepDisplay.hovered || - authorDisplay.hovered || pipelineStatusDisplay.hovered || - pipeStatusDisplay.hovered || submitToClientDisplay.hovered || - frameToolTip.containsMouse || dateToolTip.containsMouse || treeVersionsButton.hovered || allVersionsButton.hovered - width: searchResultsView.cellWidth - height: searchResultsView.cellHeight-itemSpacing - color: isSelected ? Qt.darker(itemColorActive, 2.75): itemColorNormal - border.color: isMouseHovered? itemColorActive: itemColorNormal - clip: true - - Connections { - target: selectionModel - function onSelectionChanged(selected, deselected) { - isSelected = selectionModel.isSelected(searchResultsViewModel.index(index, 0)) - } - } - - MouseArea{ - id: mArea - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - onDoubleClicked: { - if (mouse.button == Qt.LeftButton) { - selectionModel.select(searchResultsViewModel.index(index, 0), ItemSelectionModel.ClearAndSelect) - selectionModel.resetSelection() - selectionModel.countSelection(true) - selectionModel.prevSelectedIndex = index - applySelection( - function(items){ - addShotsToPlaylist( - items, - data_source.preferredVisual(currentCategory), - data_source.preferredAudio(currentCategory), - data_source.flagText(currentCategory), - data_source.flagColour(currentCategory) - ) - } - ) - } - } - - onClicked: { - searchResultsDiv.itemClicked(mouse, index, isSelected) - } - } - - GridLayout { - anchors.fill: parent - anchors.margins: framePadding - rows: 2 - columns: treeMode ? 8: 9 - rowSpacing: itemSpacing - - Rectangle{ id: indicators - - Layout.preferredWidth: 20 - Layout.fillHeight: true - Layout.columnSpan: 1 - Layout.rowSpan: 2 - color: "transparent" - - Column{ - spacing: itemSpacing - anchors.fill: parent - y: itemSpacing - - XsButton{ id: pipeStatusDisplay - property bool hasNotes: noteCountRole === undefined || noteCountRole == 0 ? false :true - text: "N" - width: 20 - height: parent.height/3 - itemSpacing/2 - font.pixelSize: fontSize*1.2 - font.weight: Font.Medium - focus: false - enabled: hasNotes - bgColorNormal: hasNotes ? "#c27b02" : palette.base - textDiv.topPadding: 2.5 - - onClicked: { - let mymodel = noteTreePresetsModel - mymodel.clearLoaded() - mymodel.insert( - mymodel.rowCount(), - mymodel.index(mymodel.rowCount(),0), - { - "expanded": false, - "name": nameRole + " Notes", - "queries": [ - { - "enabled": true, - "term": "Twig Name", - "value": "^"+twigNameRole+"$" - }, - { - "enabled": shotRole != undefined, - "term": "Shot", - "value": shotRole != undefined ? shotRole : "" - }, - { - "enabled": true, - "term": "Version Name", - "value": nameRole - }, - { - "enabled": false, - "term": "Note Type", - "value": "Director" - }, - { - "enabled": false, - "term": "Note Type", - "value": "Client" - }, - { - "enabled": false, - "term": "Note Type", - "value": "VFXSupe" - } - ] - } - ) - currentCategory = "Notes Tree" - mymodel.activePreset = mymodel.rowCount()-1 - } - } - XsButton{ id: dailiesStatusDisplay - property bool hasDailes: submittedToDailiesRole === undefined ? false :true - text: "D" - width: pipeStatusDisplay.width - height: pipeStatusDisplay.height - font.pixelSize: fontSize*1.2 - font.weight: Font.Medium - focus: false - enabled: false //hasDailes - bgColorNormal: hasDailes ? "#2b7ffc" : palette.base - textDiv.topPadding: 2.5 - } - XsButton{ id: submitToClientDisplay - property bool isSent: dateSubmittedToClientRole === undefined ? false : true - enabled: false //isSent - text: "C" - width: pipeStatusDisplay.width - height: pipeStatusDisplay.height - font.pixelSize: fontSize*1.2 - font.weight: Font.DemiBold - focus: false - bgColorNormal: isSent ? "green" : palette.base - textDiv.topPadding: 2.5 - - onClicked: createPreset("Sent To Client", "True") - } - - } - - } - Item{ id: thumbnail - Layout.preferredWidth: parent.height * 1.5 - Layout.fillHeight: true - Layout.rowSpan: 2 - - property bool failed: thumbRole == undefined - - Text{ id: noThumbnailDisplay - text: parent.failed ? "No ShotGrid Thumbnail" : "Loading..." - width: parent.width - height: parent.height - wrapMode: Text.WordWrap - font.pixelSize: fontSize*1.2 - font.weight: Font.Black - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: isSelected? Qt.darker("dark grey",1) : parent.failed? XsStyle.mainBackground : textColorNormal - opacity: isSelected? 0.3 : 0.9 - } - Image { - visible: !parent.failed - anchors.fill: parent - source: parent.failed ? "" : thumbRole - - asynchronous: true - cache: true - sourceSize.height: height - sourceSize.width: width - opacity: 0 - Behavior on opacity {NumberAnimation {duration: 150}} - - onStatusChanged: { - if (status == Image.Error) { parent.failed=true; opacity=0 } - else if (status == Image.Ready) opacity=1 - else opacity=0 - } - } - } - - XsButton{ id: treeVersionsButton - Layout.preferredWidth: pipeStatusDisplay.width - Layout.preferredHeight: parent.height - Layout.rowSpan: 2 - - text: menuHistory - textDiv.width: parent.height - textDiv.opacity: hovered ? 1 : isMouseHovered? 0.8 : 0.6 - textDiv.rotation: -90 - textDiv.topPadding: 2.5 - textDiv.rightPadding: 3 - font.pixelSize: fontSize - font.weight: Font.DemiBold - padding: 0 - bgDiv.border.color: down || hovered ? bgColorPressed: Qt.darker(bgColorNormal,1.5) - onClicked: rightDiv.popupMenuAction(menuHistory, index) - } - - XsButton{ id: allVersionsButton - Layout.preferredWidth: pipeStatusDisplay.width - Layout.preferredHeight: parent.height - Layout.rowSpan: 2 - - visible: !treeMode - text: menuLatest - textDiv.width: parent.height - textDiv.opacity: hovered ? 1 : isMouseHovered? 0.8 : 0.6 - textDiv.rotation: -90 - textDiv.topPadding: 2.5 - textDiv.rightPadding: 3 - font.pixelSize: fontSize - font.weight: Font.DemiBold - padding: 0 - bgDiv.border.color: down || hovered ? bgColorPressed: Qt.darker(bgColorNormal,1.5) - onClicked: rightDiv.popupMenuAction(menuLatest, index) - } - - XsTextButton{ id: nameDisplay - text: " "+nameRole - isClickable: false - onTextClicked: createPreset("Twig Name", twigNameRole) - Layout.columnSpan: stepDisplay.visible? 3 : 4 - - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - // Layout.preferredWidth: 150 - textDiv.font.pixelSize: fontSize*1.2 - textDiv.font.weight: Font.Medium - textDiv.elide: Text.ElideMiddle - textDiv.horizontalAlignment: Text.AlignLeft - textDiv.verticalAlignment: Text.AlignVCenter - forcedMouseHover: isMouseHovered - - ToolTip.text: nameRole - ToolTip.visible: hovered && textDiv.truncated - } - - XsTextButton{ id: stepDisplay - visible: text != "" - text: pipelineStepRole ? pipelineStepRole : "" - isClickable: false - textDiv.font.pixelSize: fontSize*1.2 - textDiv.font.weight: Font.Medium - textDiv.elide: Text.ElideRight - textDiv.horizontalAlignment: Text.AlignRight - textDiv.rightPadding: 3 - forcedMouseHover: isMouseHovered - Layout.alignment: Qt.AlignRight - Layout.fillWidth: true - Layout.minimumWidth: 65 - Layout.preferredWidth: 70 - Layout.maximumWidth: 92 - Layout.columnSpan: 1 - - ToolTip.text: text - ToolTip.visible: hovered && textDiv.truncated - - onTextClicked: createPreset("Pipeline Step", textDiv.text) - } - - Rectangle{ id: siteDisplay - Layout.fillWidth: true - Layout.minimumWidth: 50 - Layout.preferredWidth: 83 - Layout.maximumWidth: 93 - Layout.fillHeight: true - Layout.columnSpan: 1 - Layout.rowSpan: 2 - color: "transparent" - - Grid{ id: col - spacing: itemSpacing/3 - rows: 3 - columns: 2 - flow: Grid.TopToBottom - anchors.fill: parent - - Repeater{ - model: siteModel //["chn","lon","mtl","mum","syd",van"] - - XsButton{ id: onDiskDisplay - property int onDisk: { - if(index==0) onSiteChn - else if(index==1) onSiteLon - else if(index==2) onSiteMtl - else if(index==3) onSiteMum - else if(index==4) onSiteSyd - else if(index==5) onSiteVan - else false - } - text: siteName - width: siteDisplay.width/2 - height: siteDisplay.height/3 -col.spacing - font.pixelSize: fontSize/1.2 - font.weight: Font.Medium - focus: false - enabled: false - borderWidth: 0 - bgColorNormal: onDisk ? Qt.darker(siteColour, onDisk == 1 ? 1.5:1.0) : palette.base - textDiv.topPadding: 2 - } - } - ListModel{ - id: siteModel - ListElement{siteName:"chn"; siteColour:"#508f00"} //"#6a9140"} - ListElement{siteName:"lon"; siteColour:"#2b7ffc"} //"#143390"} - ListElement{siteName:"mtl"; siteColour:"#979700"} //"#b1a350"} - ListElement{siteName:"mum"; siteColour:"#ef9526"} - ListElement{siteName:"syd"; siteColour:"#008a46"} //"#7f082f"} - ListElement{siteName:"van"; siteColour:"#7a1a39"} //"#7f082f"} - } - } - - } - - - - Text{ id: dateDisplay - Layout.alignment: Qt.AlignLeft - property var dateFormatted: createdDateRole.toLocaleString().split(" ") - text: typeof dateFormatted !== 'undefined'? dateFormatted[1].substr(0,3)+" "+dateFormatted[2]+" "+dateFormatted[3] : "" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - Layout.fillWidth: true - Layout.minimumWidth: 68 - Layout.preferredWidth: 74 - Layout.maximumWidth: 74 - - ToolTip.text: createdDateRole.toLocaleString() - ToolTip.visible: dateToolTip.containsMouse //&& truncated - MouseArea { - id: dateToolTip - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } - - } - // Component.onCompleted: { - // console.log("############# locale", createdDateRole.toLocaleString(Qt.locale(),{dateSty;e:"medium"})) - // } - - Text{ id: frameDisplay - text: frameRangeRole ? frameRangeRole : "" - font.pixelSize: fontSize - font.family: fontFamily - color: isMouseHovered? textColorActive: textColorNormal - opacity: 0.6 - elide: Text.ElideRight - horizontalAlignment: Text.AlignHCenter - Layout.alignment: Qt.AlignHCenter - Layout.fillWidth: true - Layout.minimumWidth: 60 - Layout.preferredWidth: 80 - Layout.maximumWidth: 80 - - ToolTip.text: frameRangeRole ? frameRangeRole : "" - ToolTip.visible: frameToolTip.containsMouse && truncated - MouseArea { - id: frameToolTip - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } - } - - XsTextButton{ id: authorDisplay - text: authorRole? authorRole : "" - isClickable: false - textDiv.font.pixelSize: fontSize - opacity: 0.6 - textDiv.elide: Text.ElideRight - textDiv.horizontalAlignment: Text.AlignLeft - forcedMouseHover: isMouseHovered - Layout.fillWidth: true - Layout.alignment: Qt.AlignLeft - - onTextClicked: createPreset("Author", textDiv.text) - - ToolTip.text: authorRole - ToolTip.visible: hovered && textDiv.truncated - } - - XsTextButton{ id: pipelineStatusDisplay - text: pipelineStatusFullRole? pipelineStatusFullRole : "" - isClickable: false - - Layout.alignment: Qt.AlignRight - Layout.columnSpan: 1 - Layout.fillWidth: true - Layout.minimumWidth: 50 - Layout.preferredWidth: 60 - Layout.maximumWidth: 120 - textDiv.font.pixelSize: fontSize - opacity: 0.6 - textDiv.elide: Text.ElideRight - textDiv.horizontalAlignment: Text.AlignRight - textDiv.verticalAlignment: Text.AlignVCenter - textDiv.rightPadding: 3 - forcedMouseHover: isMouseHovered - - ToolTip.text: pipelineStatusFullRole - ToolTip.visible: hovered && textDiv.truncated - } - - } - } -} diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/LeftPresetView.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/LeftPresetView.qml deleted file mode 100644 index 019f04054..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/LeftPresetView.qml +++ /dev/null @@ -1,687 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient -import QtQuick.Controls.Styles 1.4 //for TextFieldStyle -import QuickFuture 1.0 -import QuickPromise 1.0 - -import Shotgun 1.0 -import xStudio 1.1 -import xstudio.qml.clipboard 1.0 - - -Rectangle{ id: presetsDiv - property alias presetSelectionModel: presetSelectionModel - property alias presetsModel: searchPresetsView.delegateModel - property alias searchPresetsView: searchPresetsView - property real presetTitleHeight: itemHeight - property var backendModel//: searchPresetsViewModel - property bool isActiveView: true - - color: "transparent" - border.color: frameColor - border.width: frameWidth - // focus: true - - ItemSelectionModel { - id: presetSelectionModel - model: backendModel - } - - Component { - id: dragDelegate - // MouseArea - MouseArea { - id: dragArea - - acceptedButtons: Qt.LeftButton | Qt.RightButton - - property var model: DelegateModel.model - property bool presetLoadedRole: backendModel.activePreset === index && isActiveView - property bool isMouseHovered: containsMouse || (editMenu.visible && searchPresetsView.menuActionIndex==index) - property bool held: false - property bool was_current: false - - hoverEnabled: true - - anchors { left: parent ? parent.left : undefined; right: parent ? parent.right : undefined} - - height: content.height - width: searchPresetsView.width - - drag.target: held ? content : undefined - drag.axis: Drag.YAxis - - onDoubleClicked: { - presetSelectionModel.select(presetsModel.modelIndex(index), ItemSelectionModel.ClearAndSelect) - if(presetsModel.model.activePreset == index) - presetsModel.model.activePreset = -1 - presetsModel.model.activePreset = index - } - - onClicked: { - if(mouse.modifiers == Qt.NoModifier) { - if(!held) { - if (mouse.button == Qt.RightButton) { - searchPresetsView.menuActionIndex = index - editMenu.popup() - } else { - presetSelectionModel.select(presetsModel.modelIndex(index), ItemSelectionModel.ClearAndSelect) - presetsModel.model.activePreset = index - } - } - } else if(mouse.button == Qt.LeftButton){ - if(mouse.modifiers == Qt.ShiftModifier){ - // Only works with one current selection. - if(presetSelectionModel.selectedIndexes.length==1) { - let s = Math.min(presetSelectionModel.selectedIndexes[0].row,index) - let e = Math.max(presetSelectionModel.selectedIndexes[0].row,index) - for(;s<=e;s++){ - presetSelectionModel.select(presetsModel.modelIndex(s), ItemSelectionModel.Select) - } - } - } else if(mouse.modifiers == Qt.ControlModifier) { - presetSelectionModel.select(presetsModel.modelIndex(index), ItemSelectionModel.Toggle) - } - } - } - - onPressAndHold: { - held = true - if(presetsModel.model.activePreset == index) { - presetsModel.model.activePreset = -1 - was_current = true - } - } - onReleased: { - held = false - if(was_current) { - was_current = false - presetsModel.model.activePreset = index - } - } - - DropArea { - keys: ["PRESET"] - anchors { fill: parent; margins: 10 } - - onEntered: { - // if(drag.source.DelegateModel.itemsIndex != dragArea.DelegateModel.itemsIndex) { - // presetsModel.items.move( - // drag.source.DelegateModel.itemsIndex, - // dragArea.DelegateModel.itemsIndex) - - presetsModel.move( - drag.source.DelegateModel.itemsIndex, - dragArea.DelegateModel.itemsIndex) - // } - } - } - // rectangle - Rectangle { - // Preset title bar - id: content - color: "transparent" - - height: (presetTitleHeight+ searchQueryView.height) - width: dragArea.width - - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } - - Drag.active: dragArea.held - Drag.source: dragArea - Drag.hotSpot.x: width / 2 - Drag.hotSpot.y: height / 2 - Drag.keys: ["PRESET"] - - states: State { - when: dragArea.held - - ParentChange { target: content; parent: presetsDiv } - AnchorChanges { - target: content - anchors { horizontalCenter: undefined; verticalCenter: undefined } - } - } - - // we need to somehow update these... - Connections { - target: searchQueryView.queryModel - function onJsonChanged() { - searchQueryView.queryRootIndex = model.modelIndex(index, model.rootIndex) - } - } - - - Component.onCompleted: { - searchQueryView.queryModel = model.model - searchQueryView.queryRootIndex = model.modelIndex(index, model.rootIndex) - } - - XsMenu { id: editMenu - width: 150 - - XsMenuItem { - mytext: "Rename..."; onTriggered: presetsDiv.onMenuAction("RENAME") - } - XsMenuItem { - mytext: "Duplicate"; onTriggered: presetsDiv.onMenuAction("DUPLICATE") - } - MenuSeparator{ } - XsMenuItem { - mytext: "Copy Preset"; onTriggered: presetsDiv.onMenuAction("COPY", index) - // shortcut: "Ctrl+C" - } - MenuSeparator{ } - XsMenuItem { - mytext: "Remove"; onTriggered: presetsDiv.onMenuAction("REMOVE", index) - } - } - - Rectangle{ - id: presetNameDiv - property bool divSelected: false - property int slNumber: index+1 - - Connections { - target: presetSelectionModel - function onSelectionChanged(selected, deselected) { - presetNameDiv.divSelected = presetSelectionModel.rowIntersectsSelection(index , presetsModel.parentModelIndex()) - } - } - - anchors { - horizontalCenter: parent.horizontalCenter - top: parent.top - } - - width: parent.width - height: presetTitleHeight - color: { - if(held) - Qt.darker(itemColorActive, 3.75) - else { - backendModel.activePreset == index || divSelected ? Qt.darker(itemColorActive, 2.75) : itemColorNormal - } - } - border.color: isMouseHovered? itemColorActive: itemColorNormal - - Rectangle{ id: activeIndicator - visible: backendModel.activePreset == index - z: 10 - width: framePadding/1.5; height: parent.height - anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter - color: itemColorActive - } - - XsText{ - text: !isCollapsed? nameRole : nameRole.substr(0,2)+".." - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } - elide: Text.ElideRight - width: isCollapsed? parent.width: parent.width - framePadding*2 - height: parent.height - font.pixelSize: fontSize*1.2 - font.weight: presetLoadedRole? Font.DemiBold : Font.Normal - color: isMouseHovered || presetLoadedRole? textColorActive: textColorNormal - horizontalAlignment: Text.AlignHCenter // Text.AlignLeft // Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - ToolTip.text: nameRole - ToolTip.visible: isMouseHovered && isCollapsed - visible: !(searchPresetsView.isEditable && searchPresetsView.menuActionIndex == index) - } - - XsTextField{ - id: textField - text: nameRole - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } - width: parent.width - height: parent.height - font.pixelSize: fontSize*1.2 - color: isMouseHovered || presetLoadedRole? textColorActive: textColorNormal - bgVisibility: textField.focus - visible: searchPresetsView.isEditable && searchPresetsView.menuActionIndex == index - focus: visible - - onFocusChanged: { - if(focus) { - forceActiveFocus() - selectAll() - } - } - - onEditingCompleted: { - searchPresetsView.isEditable = false - searchPresetsView.menuActionIndex = -1 - nameRole = text - } - - ToolTip.text: nameRole - ToolTip.visible: isMouseHovered && isCollapsed - } - - - RadialGradient { id: overlayBg - visible: !isCollapsed && (isMouseHovered) - anchors.centerIn: parent - width:parent.width - frameWidth*2 - height: parent.height - frameWidth*2 - angle: 0 - horizontalOffset: 0 - horizontalRadius: width/2 - verticalOffset: 0 - verticalRadius: height - gradient: - Gradient { - GradientStop { position: 0.0; color: "transparent" } - GradientStop { position: 0.5; color: "transparent" } - GradientStop { position: 0.85; color: presetNameDiv.color } - } - } - XsButton{id: expandButton - property bool isExpanded: expandedRole - - text: "" - imgSrc: "qrc:/feather_icons/chevron-down.svg" - visible: !isCollapsed && (isMouseHovered || hovered || isActive) - width: height - height: parent.height - framePadding - image.sourceSize.width: height/1.3 - image.sourceSize.height: height/1.3 - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: framePadding +frameWidth/2 - borderColorNormal: Qt.lighter(itemColorNormal, 0.3) - isActive: isExpanded - - scale: rotation==0 || rotation==-90?1:0.85 - rotation: (isExpanded)? 0: -90 - Behavior on rotation {NumberAnimation{id: rotationAnim; duration: 150 }} - - TapHandler { - acceptedModifiers: Qt.ShiftModifier - onTapped: { - expandedRole = !expandedRole - } - } - TapHandler { - acceptedModifiers: Qt.NoModifier - onTapped: { - if(!expandedRole) { - presetsModel.model.clearExpanded() - } - expandedRole = !expandedRole - } - } - } - Rectangle{ anchors{right: busy.right; verticalCenter: busy.verticalCenter; } - width: busy.width; height: busy.height; opacity: busy.visible? 0.7:0 - Behavior on opacity {NumberAnimation{duration: 150 }} - color: parent.color; radius: width/2; - } - XsBusyIndicator { - id: busy - running: false - width: 40 - height: parent.height - framePadding - anchors.verticalCenter: parent.verticalCenter - anchors.left: expandButton.right - anchors.leftMargin: itemSpacing - } - - Connections { - target: leftDiv - function onBusyQuery(queryIndex, isBusy){ - if(queryIndex == index) - busy.running=isBusy - } - } - - XsButton{id: deleteButton - - text: "" - imgSrc: "qrc:/feather_icons/x.svg" - visible: !isCollapsed && (isMouseHovered || hovered) - width: height - height: parent.height - framePadding - image.sourceSize.width: height/1.3 - image.sourceSize.height: height/1.3 - anchors.verticalCenter: parent.verticalCenter - anchors.right: moreButton.left - anchors.rightMargin: itemSpacing - borderColorNormal: Qt.lighter(itemColorNormal, 0.3) - - onClicked: presetsDiv.onMenuAction("REMOVE", index) - } - XsButton{id: moreButton - - text: "" - imgSrc: "qrc:/feather_icons/more-vertical.svg" - visible: !isCollapsed && (isMouseHovered || hovered || isActive) - width: height - height: parent.height - framePadding - image.sourceSize.width: height/1.3 - image.sourceSize.height: height/1.3 - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: framePadding +frameWidth - borderColorNormal: Qt.lighter(itemColorNormal, 0.3) - isActive: (editMenu.visible && searchPresetsView.menuActionIndex==index) - - onClicked: { - searchPresetsView.menuActionIndex = index - editMenu.popup() - } - - } - - // trigger after timer. - - // Component.onCompleted: { - // if(presetLoadedRole) - // searchPresetsView.currentIndex = index - // } - } - - // Preset TERMS - QueryListView { - id: searchQueryView - - x: frameWidth - y: presetTitleHeight + framePadding - width: parent.width - frameWidth*4 -framePadding - - interactive: true - snapMode: ListView.SnapToItem - onSnapshot: snapshotPresets() - expanded: expandedRole - isLoaded: presetLoadedRole - queryRootIndex: null - queryModel: null - } - } - // rectangle - } - // MouseArea - } - - XsTreeStructure{ id: searchPresetsView - property alias treeView: searchPresetsView - spacing: itemSpacing - snapMode: ListView.SnapToItem - scrollBar.visible: !searchShotListPopup.visible && searchPresetsView.height < searchPresetsView.contentHeight - - width: parent.width - height: isCollapsed? parent.height : parent.height - (savePresetDiv.height + savePresetDiv.anchors.bottomMargin*2) - - delegateModel.model: backendModel - delegateModel.delegate: dragDelegate - - Connections { - target: backendModel - function onActivePresetChanged() { - searchPresetsView.currentIndex = backendModel.activePreset - searchPresetsView.isEditable = false - searchPresetsView.menuActionIndex = -1 - if(backendModel.activePreset != -1 && backendModel.get(backendModel.activePreset).startsWith("Live ")) - clearFilter() - presetSelectionModel.select(presetsModel.modelIndex(backendModel.activePreset), ItemSelectionModel.ClearAndSelect) - executeQuery() - } - } - - MouseArea{ id: focusMArea; width: parent.width; height: parent.height-parent.contentHeight; - anchors.bottom: parent.bottom; onPressed: focus= true; onReleased: focus= false; - } - - } - - - - function duplicatePreset(index, title) { - presetsModel.insert(presetsModel.count, presetsModel.get(index, "jsonRole")) - presetsModel.set(presetsModel.count-1, title, "nameRole") - presetsModel.model.setType(presetsModel.count-1) - - return presetsModel.count-1 - } - - function onMenuAction(actionText, index=-1) - { - if(actionText == "RESET") - { - snapshotPresets() - data_source.resetPreset(currentCategory) - clearResults() - // if(index !== -1) { - // data_source.resetPreset(currentCategory, index) - // } else{ - // for(let i=0;i 0 - } - XsMenuItem { - mytext: "Paste Presets"; onTriggered: presetsDiv.onMenuAction("PASTE") - shortcut: "Ctrl+V"; - enabled: clipboard.text !== "" - } - MenuSeparator{ } - XsMenuItem { - mytext: "Undo"; onTriggered: undo() - shortcut: "Ctrl+Z" - } - XsMenuItem { - mytext: "Redo"; onTriggered: redo() - shortcut: "Ctrl+Shift+Z" - } - MenuSeparator{ } - XsMenuItem { - mytext: "Reset Default Presets"; onTriggered: presetsDiv.onMenuAction("RESET") - // enabled: presetSelectionModel.selectedIndexes.length > 0 - } - MenuSeparator{ } - XsMenuItem { - mytext: "Remove Selected Presets"; onTriggered: presetsDiv.onMenuAction("REMOVE") - enabled: presetSelectionModel.selectedIndexes.length > 0 - } - } - } - - Rectangle { id: overlapGradient - visible: searchShotListPopup.visible - onVisibleChanged: { - if(visible){ - searchPresetsView.enabled = false - savePresetDiv.enabled = false - } - if(!visible){ - searchPresetsView.enabled = true - savePresetDiv.enabled = true - } - } - anchors.fill: parent - MouseArea{anchors.fill: parent;onClicked: searchTextField.focus = false} - - gradient: Gradient { - GradientStop { position: 0.0; color: "#77000000" } - GradientStop { position: 0.8; color: "#33000000" } - GradientStop { position: 1.0; color: "transparent"} - } - } -} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/LeftTreeView.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/LeftTreeView.qml deleted file mode 100644 index 2f510c922..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/LeftTreeView.qml +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient -import QtQuick.Controls.Styles 1.4 //for TextFieldStyle -import QuickFuture 1.0 -import QuickPromise 1.0 - -import Shotgun 1.0 -import xStudio 1.1 -import xstudio.qml.clipboard 1.0 - - -Rectangle{ id: section - property var model: sequenceTreeModel - color: "transparent" //palette.base - border.color: frameColor - border.width: frameWidth - - signal clicked(string type, string name, int id) - signal doubleClicked(string type, string name, int id) - - - XsTimer { - id: callback_delay_timer - } - - function selectItem(index) { - let i = index.parent - while(i.valid) { - itemExpandedModel.select(i, ItemSelectionModel.Select) - i = i.parent - } - callback_delay_timer.setTimeout(function(){ itemSelectionModel.select(index, ItemSelectionModel.ClearAndSelect) }, 100); - } - - function scrollToFunc(target) { - let ty = treeView.listView.mapFromItem(target, 0, 0).y + treeView.listView.contentY - let visibleHeight = treeView.listView.visibleArea.heightRatio * treeView.listView.contentHeight - - if(ty < treeView.listView.contentY) { - treeView.listView.contentY = ty - } else if(ty > (treeView.listView.contentY + visibleHeight - 20) ){ - treeView.listView.contentY = treeView.listView.contentY + (ty - (treeView.listView.contentY + visibleHeight - 20)) - } - } - - ItemSelectionModel {id: itemSelectionModel - model: section.model - onSelectionChanged: { - if(selectedIndexes.length){ - clicked( - section.model.get(selectedIndexes[0],"typeRole"), - section.model.get(selectedIndexes[0],"nameRole"), - section.model.get(selectedIndexes[0],"idRole"), - ) - } - } - } - - ItemSelectionModel {id: itemExpandedModel - model: section.model - } - - ShotsTreeView{ id: treeView - model: section.model - rootIndex: null - selectionModel: itemSelectionModel - expandedModel: itemExpandedModel - scrollBarVisibility: !searchShotListPopup.visible - itemDoubleClicked: itemDoubleClickedFunction - scrollTo: scrollToFunc - } - - function itemDoubleClickedFunction(type, name, id) { - doubleClicked(type, name, id) - } - - Rectangle { id: overlapGradient - visible: searchShotListPopup.visible - anchors.fill: parent - gradient: Gradient { - GradientStop { position: 0.0; color: "#77000000" } - GradientStop { position: 1.0; color: "#55000000" } - } - MouseArea{anchors.fill: parent;onClicked: searchTextField.focus = false; hoverEnabled: true} - } -} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/QueryListView.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/QueryListView.qml deleted file mode 100644 index c155d46ac..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/QueryListView.qml +++ /dev/null @@ -1,651 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient -import QtQuick.Controls.Styles 1.4 //for TextFieldStyle -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudio 1.1 - -ListView{ - id: queryList - - property var expanded: true - - property alias queryRootIndex: queryTreeModel.rootIndex - property alias queryModel: queryTreeModel.baseModel - - property bool isLoaded: true - property real queryItemHeight: itemHeight/1.15 - - height: expanded ? - queryList.model? - ((queryItemHeight+spacing)*(queryList.model.count+1)) + framePadding - : (queryItemHeight+spacing)+(framePadding*2) - : 0 - - Behavior on height {NumberAnimation{ duration: 150 }} - - spacing: itemSpacing - clip: true - orientation: ListView.Vertical - interactive: true - snapMode: ListView.SnapOneItem - currentIndex: -1 - // ScrollBar.vertical: XsScrollBar{id: queryScrollBar;} - // Keys.onPressed: (event)=> { - // if (event.matches(StandardKey.SelectNextLine)) { - // console.log("move up", currentIndex) - // event.accepted = true; - // } - // else if (event.matches(StandardKey.SelectPreviousLine)) { - // console.log("move down", currentIndex) - // event.accepted = true; - // } - // } - // Shortcut { - // sequence: StandardKey.SelectNextLine - // onActivated: console.log("move up", currentIndex) - // } - // Shortcut { - // sequence: StandardKey.SelectPreviousLine - // onActivated: console.log("move down", currentIndex) - // } - - model: queryTreeModel - - signal snapshot() - - TreeDelegateModel { - id: queryTreeModel - - model: null - rootIndex: null - - delegate: - Item {id: queryRowItem - property bool isEnabled: enabledRole == undefined ? false : enabledRole - width: queryList.width - height: queryItemHeight - - property bool held: false - property bool wasCurrent: false - DropArea { - keys: ["TERM"] - anchors { fill: parent; margins: 10 } - onEntered: { - if(drag.source.DelegateModel.itemsIndex != queryRowItem.DelegateModel.itemsIndex) { - queryTreeModel.move( - drag.source.DelegateModel.itemsIndex, - queryRowItem.DelegateModel.itemsIndex) - } - } - } - - Keys.onPressed: (event)=> { - if (event.matches(StandardKey.SelectNextLine)) { - if(currentIndex>=0) { - queryTreeModel.move(currentIndex, currentIndex+1) - } - event.accepted = true; - } - else if (event.matches(StandardKey.SelectPreviousLine)) { - if(currentIndex>0) { - queryTreeModel.move(currentIndex-1, currentIndex) - } - event.accepted = true; - } - } - - Rectangle{id: queryRow - width: parent.width - height: queryItemHeight - color: currentIndex == index ? Qt.darker(itemColorActive, 3.75) : "transparent" - - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } - Drag.active: queryRowItem.held - Drag.source: queryRowItem - Drag.hotSpot.x: width / 2 - Drag.hotSpot.y: height / 2 - Drag.keys: ["TERM"] - states: State { - when: queryRowItem.held - AnchorChanges { - target: queryRow - anchors { horizontalCenter: undefined; verticalCenter: undefined } - } - ParentChange { target: queryRow; parent: queryList } - } - - RowLayout { - width: parent.width - height: parent.height - spacing: itemSpacing - - Rectangle{ id: dragHandle - Layout.preferredWidth: framePadding*2 - Layout.preferredHeight: queryItemHeight - color: "transparent" - - Image { id: dragHandleIcon - x: itemSpacing - anchors.verticalCenter: parent.verticalCenter - source: "qrc:/feather_icons/align-justify.svg" - height: parent.height - width: parent.width - frameWidth*2 - // rotation: 90 - smooth: true - layer { - enabled: true - effect: - ColorOverlay { - color: dragArea.pressed? itemColorActive: dragArea.containsMouse? textColorNormal : Qt.darker(textColorNormal, 2) - } - } - } - - MouseArea{ id: dragArea - anchors.fill: parent - hoverEnabled: true - // onHoveredChanged: { - // if(queryRowItem.containsMouse) { - // queryRow.forceActiveFocus() - // } else { - // queryRow.focus = false - // } - // } - onClicked: currentIndex = (currentIndex == index ? -1 : index) - - drag.target: queryRowItem.held ? queryRow : undefined - drag.axis: Drag.YAxis - onPressed: { - queryRowItem.held = true - if(currentIndex == index) { - currentIndex = -1 - queryRowItem.wasCurrent = true - } - } - onReleased: { - queryRowItem.held = false - if(wasCurrent) { - currentIndex = index - queryRowItem.wasCurrent = false - } - } - } - - } - - XsCheckbox{id: checkBox - // Layout.leftMargin: framePadding - Layout.preferredWidth: queryItemHeight - Layout.preferredHeight: queryItemHeight - imgIndicator.sourceSize.width: queryItemHeight*0.7 - imgIndicator.sourceSize.height: queryItemHeight*0.7 - spacing: 0 - - text: "" - indicatorType: "tick" - display: AbstractButton.IconOnly - checked: isEnabled - padding: 0 - onClicked: { - enabledRole = !enabledRole - if(isLoaded) { - executeQuery() - } - } - } - - XsButton {id: negation - property bool hasNegation: negationRole !== undefined - z: 1 - subtleActive: true - isActive: hasNegation ? negationRole : false - enabled: hasNegation && isEnabled ? true : false - opacity: hasNegation ? 1.0 : 0.0 - Layout.preferredWidth: queryItemHeight/2//-framePadding*1.5 - Layout.preferredHeight: queryItemHeight//-framePadding*1.5 - // image.sourceSize.height: height/1.1 - // image.sourceSize.width: width/1.2 - text: "!" - textColorNormal: hasNegation && negationRole ? bgColorPressed : "darkgray" - // imgSrc: "qrc:/feather_icons/link-2.svg" - // rotation: 90 - clip: true - onClicked: { - negationRole = !negationRole - if(isLoaded) { - executeQuery() - } - } - } - - - XsComboBox{ id: queryBox - Layout.preferredWidth: 150 - Layout.preferredHeight: queryItemHeight - property var termrole: termRole - - model: { - if(currentCategory == "Versions" || currentCategory == "Versions Tree") - return shotFilters - else if(currentCategory == "Playlists") - return playlistFilters - else if(currentCategory == "Edits") - return editFilters - else if(currentCategory == "Reference") - return referenceFilters - else if(currentCategory == "Menu Setup") - return mediaActionFilters - else if(currentCategory == "Notes" || currentCategory == "Notes Tree") - return noteFilters - } - editable: false - // currentIndex: {console.log(termRole, queryBox.find(termRole)); return queryBox.find(termRole)} - enabled: isEnabled - - onTermroleChanged: { - currentIndex = queryBox.indexOfValue(termRole) - } - - Component.onCompleted: { - currentIndex = queryBox.indexOfValue(termRole) - } - - onActivated: { - if(currentText !== "") { - termRole = currentText - if(valueBox.count<50) valueBox.popupOptions.open() - - if(isLoaded) { - executeQuery() - } - } - } - } - - XsButton {id: liveLink - property bool hasLiveLink: liveLinkRole !== undefined - z: 1 - subtleActive: true - isActive: hasLiveLink ? liveLinkRole : false - enabled: hasLiveLink && isEnabled ? true : false - opacity: hasLiveLink ? 1.0 : 0.0 - Layout.preferredWidth: queryItemHeight-framePadding*1.5 - Layout.preferredHeight: queryItemHeight-framePadding*1.5 - image.sourceSize.height: height/1.1 - image.sourceSize.width: width/1.2 - text: "" - imgSrc: "qrc:/feather_icons/link-2.svg" - // rotation: 90 - clip: true - onClicked: { - liveLinkRole = !liveLinkRole - } - } - - XsComboBox{ id: valueBox - Layout.fillWidth: true - Layout.preferredHeight: queryItemHeight - property var argrole: argRole - - editable: true - enabled: isEnabled && !liveLink.isActive - textRole: "nameRole" - valueRole: "nameRole" - textColorNormal: liveLinkRole? palette.highlight : "light grey" - textColorDisabled: liveLinkRole? palette.highlight : Qt.darker(textColorNormal, 1.6) - bgColorEditable: isEnabled && liveLink.isActive? Qt.darker(palette.highlight, 2) : "light grey" - - onArgroleChanged: { - //special handling.. - if((model == dummyModel || model == sourceModel|| termRole == "Reference Tags") && argrole != ""){ - if(find(argrole) === -1) { - let tmp = argrole - model.append({nameRole: tmp}) - } - } - - if(currentIndex != find(argrole)) { - // could be login... - // dirty hack.. - currentIndex = find(argrole) - - if(currentIndex === -1 && model === authorModel) { - // try and resolve Shotgunname.. - argrole = data_source.getShotgunUserName() - currentIndex = find(argrole) - } - doUpdate() - } else if(currentIndex == 0) { - // model reset can cause confusion.. - doUpdate() - } - } - - ListModel { - id: dummyModel - } - - ListModel { - id: ignoreModel - - ListElement { nameRole: "Author" } - ListElement { nameRole: "Client Note" } - ListElement { nameRole: "Completion Location" } - ListElement { nameRole: "Department" } - ListElement { nameRole: "Filter" } - ListElement { nameRole: "Flag Media" } - ListElement { nameRole: "Has Contents" } - ListElement { nameRole: "Has Notes" } - ListElement { nameRole: "Is Hero" } - ListElement { nameRole: "Latest Version" } - ListElement { nameRole: "Lookback" } - ListElement { nameRole: "Exclude Shot Status" } - ListElement { nameRole: "Note Type" } - ListElement { nameRole: "On Disk" } - ListElement { nameRole: "Order By" } - ListElement { nameRole: "Pipeline Status" } - ListElement { nameRole: "Pipeline Step" } - ListElement { nameRole: "Playlist Type" } - ListElement { nameRole: "Playlist" } - ListElement { nameRole: "Preferred Audio" } - ListElement { nameRole: "Preferred Visual" } - ListElement { nameRole: "Production Status" } - ListElement { nameRole: "Recipient" } - ListElement { nameRole: "Reference Tag" } - ListElement { nameRole: "Reference Tags" } - ListElement { nameRole: "Result Limit" } - ListElement { nameRole: "Review Location" } - ListElement { nameRole: "Sent To Client" } - ListElement { nameRole: "Sent To Dailies" } - ListElement { nameRole: "Sequence" } - ListElement { nameRole: "Shot" } - ListElement { nameRole: "Shot Status" } - ListElement { nameRole: "Site" } - ListElement { nameRole: "Tag (Version)" } - ListElement { nameRole: "Tag" } - ListElement { nameRole: "Twig Name" } - ListElement { nameRole: "Twig Type" } - ListElement { nameRole: "Unit" } - ListElement { nameRole: "Version Name" } - } - - ListModel { - id: sourceModel - - ListElement { nameRole: "SG Movie" } - ListElement { nameRole: "SG Frames" } - ListElement { nameRole: "main_proxy0" } - ListElement { nameRole: "main_proxy1" } - ListElement { nameRole: "main_proxy2" } - ListElement { nameRole: "main_proxy3" } - ListElement { nameRole: "movie_client" } - ListElement { nameRole: "movie_dneg" } - ListElement { nameRole: "movie_dneg_editorial" } - ListElement { nameRole: "movie_scqt" } - ListElement { nameRole: "movie_scqt_prores" } - ListElement { nameRole: "orig_main_proxy0" } - ListElement { nameRole: "orig_main_proxy1" } - ListElement { nameRole: "orig_main_proxy2" } - ListElement { nameRole: "orig_main_proxy3" } - ListElement { nameRole: "review_proxy_1" } - ListElement { nameRole: "review_proxy_2" } - } - - ListModel { - id: flagModel - - ListElement { nameRole: "Red"; colour: "#FFFF0000" } - ListElement { nameRole: "Green"; colour: "#FF00FF00" } - ListElement { nameRole: "Blue"; colour: "#FF0000FF" } - ListElement { nameRole: "Yellow"; colour: "#FFFFFF00" } - ListElement { nameRole: "Orange"; colour: "#FFFFA500" } - ListElement { nameRole: "Purple"; colour: "#FF800080" } - ListElement { nameRole: "Black"; colour: "#FF000000" } - ListElement { nameRole: "White"; colour: "#FFFFFFFF" } - } - - model: { - if(termRole=="Author") authorModel - else if(termRole=="Client Note") boolModel - else if(termRole=="Completion Location") primaryLocationModel - else if(termRole=="Department") departmentModel - else if(termRole=="Filter") dummyModel - else if(termRole=="Flag Media") flagModel - else if(termRole=="Has Contents") boolModel - else if(termRole=="Has Notes") boolModel - else if(termRole=="Disable Global") ignoreModel - else if(termRole=="Is Hero") boolModel - else if(termRole=="Latest Version") boolModel - else if(termRole=="Lookback") lookbackModel - else if(termRole=="Exclude Shot Status") shotStatusModel - else if(termRole=="Newer Version") dummyModel - else if(termRole=="Note Type") noteTypeModel - else if(termRole=="Older Version") dummyModel - else if(termRole=="On Disk") onDiskModel - else if(termRole=="Order By") orderByModel - else if(termRole=="Pipeline Status") pipelineStatusModel - else if(termRole=="Pipeline Step") stepModel - else if(termRole=="Playlist Type") playlistTypeModel - else if(termRole=="Playlist") playlistModel - else if(termRole=="Preferred Audio") sourceModel - else if(termRole=="Preferred Visual") sourceModel - else if(termRole=="Production Status") productionStatusModel - else if(termRole=="Recipient") authorModel - else if(termRole=="Result Limit") resultLimitModel - else if(termRole=="Review Location") reviewLocationModel - else if(termRole=="Reference Tag") referenceTagModel - else if(termRole=="Reference Tags") referenceTagModel - else if(termRole=="Sent To Client") boolModel - else if(termRole=="Sent To Dailies") boolModel - else if(termRole=="Sequence") sequenceModel - else if(termRole=="Shot Status") shotStatusModel - else if(termRole=="Shot") shotModel - else if(termRole=="Site") siteModel - else if(termRole=="Tag (Version)") dummyModel - else if(termRole=="Tag") dummyModel - else if(termRole=="Twig Name") dummyModel - else if(termRole=="Twig Type") twigTypeCodeModel - else if(termRole=="Unit") customEntity24Model - else if(termRole=="Version Name") dummyModel - } - Timer { - id: updateIndex - interval: 100 - running: false - repeat: false - onTriggered: { - if(valueBox.model && valueBox.model.count) { - valueBox.currentIndex = valueBox.indexOfValue(argRole) - if(valueBox.currentIndex === -1 && valueBox.model === authorModel) { - // try and resolve Shotgunname.. - currentIndex = valueBox.indexOfValue(data_source.getShotgunUserName()) - argRole = data_source.getShotgunUserName() - } - - if(valueBox.currentIndex == -1 && !(valueBox.model == dummyModel || valueBox.model == sourceModel|| termRole == "Reference Tags") && !liveLink.isActive && queryList.expanded) { - // trigger selection.. - valueBox.popupOptions.open() - } - } else { - updateIndex.start() - } - } - } - - Connections { - target: queryList - function onExpandedChanged() { - queryList.expanded && valueBox.currentIndex == -1 && updateIndex.start() - } - } - - onAccepted: { - // special handling for Name text - if((model == dummyModel || model == sourceModel || termRole == "Reference Tags") && find(editText) === -1) { - if(editText != "") { - model.append({nameRole: editText}) - } - } - focus=false - doUpdate() - } - - Component.onCompleted: { - if(!(model == dummyModel || model == sourceModel || termRole == "Reference Tags")) - updateIndex.start() - else { - model.append({nameRole: argRole}) - currentIndex = find(argRole) - } - } - - onFocusChanged: if(!focus) accepted() - - function doUpdate() { - // console.log(argRole, currentText) - if(currentText !== "" && argRole != currentText) { - if(termRole == "Reference Tags" && !currentText.includes(",")) { - // split.. - let items = argRole.split(",") - if(items.includes(currentText)) { - items.splice(items.indexOf(currentText),1) - } else { - items.push(currentText) - } - // filter empty items. - items = items.filter(Boolean) - - argRole = items.join(",") - } - else - argRole = currentText - } - - if(isLoaded) { - executeQuery() - } - } - - onActivated: { - doUpdate() - } - } - - XsButton{id: deleteButton - Layout.preferredWidth: queryItemHeight - Layout.preferredHeight: queryItemHeight - // Layout.rightMargin: framePadding - - text: "" - imgSrc: "qrc:/feather_icons/x.svg" - onClicked: { - snapshot() - queryTreeModel.remove(index) - snapshot() - if(isLoaded) { - executeQuery() - } - } - } - } - - } - } - } - - footer: - ColumnLayout { - width: queryList.width - height: queryItemHeight+itemSpacing*2 - anchors.topMargin: itemSpacing*2 - - XsComboBox{ - id: selectField - Layout.preferredWidth: 150 - Layout.preferredHeight: queryItemHeight - Layout.leftMargin: (queryItemHeight/2)+itemSpacing+(queryItemHeight+itemSpacing) + (framePadding*2+itemSpacing) - - model: { - if(currentCategory == "Versions" || currentCategory == "Versions Tree") - return ["-- Select --"].concat(shotFilters) - else if(currentCategory == "Playlists") - return ["-- Select --"].concat(playlistFilters) - else if(currentCategory == "Edits") - return ["-- Select --"].concat(editFilters) - else if(currentCategory == "Reference") - return ["-- Select --"].concat(referenceFilters) - else if(currentCategory == "Menu Setup") - return ["-- Select --"].concat(mediaActionFilters) - else if(currentCategory == "Notes" || currentCategory == "Notes Tree") - return ["-- Select --"].concat(noteFilters) - } - - onModelChanged: currentIndex = 0 - currentIndex: 0 - - onActivated: { - if(currentIndex !== -1 && currentIndex !== 0) { - let value = "" - let id = "" - if(selectField.currentText == "Author") { - value = data_source.getShotgunUserName() - } - else if(selectField.currentText == "Recipient") { - value = data_source.getShotgunUserName() - } - else if(selectField.currentText == "Site") - value = helpers.getEnv("DNSITEDATA_SHORT_NAME", "") - else if(selectField.currentText == "Has Contents") - value = "True" - else if(selectField.currentText == "Sent To Client") - value = "True" - else if(selectField.currentText == "Sent To Dailies") - value = "True" - else if(selectField.currentText == "Has Notes") - value = "True" - else if(selectField.currentText == "Latest Version") - value = "True" - - snapshot() - - let term = {"term": selectField.currentText, "value": value, "enabled": true} - - // only certain terms can be pinned.. - if(["Older Version", "Newer Version", "Version Name", "Author", "Recipient", "Shot", "Pipeline Step", "Twig Name", "Twig Type", "Sequence"].includes(selectField.currentText)) { - term["livelink"] = false - } - - if(["Pipeline Step", "Playlist Type", "Site", "Department", - "Filter", "Tag", "Unit", "Note Type","Version Name", "Pipeline Status", - "Production Status", "Shot Status", "Twig Type", "Twig Name", "Shot Status", - "Tag (Version)", "Reference Tag", "Reference Tags", "Twig Name", "Completion Location", "On Disk"].includes(selectField.currentText)) { - term["negated"] = false - } - - queryTreeModel.insert(queryTreeModel.count, term) - snapshot() - - currentIndex = 0 - // update query on change. - if(isLoaded && value != "") { - executeQuery() - } - } - } - } - } -} - diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/SBLeftPanel.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/SBLeftPanel.qml deleted file mode 100644 index 842d86ffc..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/SBLeftPanel.qml +++ /dev/null @@ -1,1254 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient -import QtQuick.Controls.Styles 1.4 //for TextFieldStyle -import QuickFuture 1.0 -import QuickPromise 1.0 - -import Shotgun 1.0 -import xStudio 1.1 -import xstudio.qml.clipboard 1.0 - -Rectangle{ id: leftDiv - property var projectModel: null - property alias projectCurrentIndex: projComboBox.currentIndex - - property bool liveLinkProjectChange: false - property var authenticateFunc: null - - property var treeMode: null - - property var authorModel: null - property var siteModel: null - property var departmentModel: null - property var noteTypeModel: null - property var playlistTypeModel: null - property var productionStatusModel: null - property var pipelineStatusModel: null - - property var boolModel: null - property var resultLimitModel: null - property var reviewLocationModel: null - property var referenceTagModel: null - property var orderByModel: null - property var primaryLocationModel: null - property var lookbackModel: null - property var stepModel: null - property var onDiskModel: null - property var twigTypeCodeModel: null - property var shotStatusModel: null - - property var sequenceModel: null - property var sequenceModelFunc: null - - property var sequenceTreeModel: null - property var sequenceTreeModelFunc: null - - property var shotModel: null - property var shotModelFunc: null - - property var customEntity24Model: null - property var customEntity24ModelFunc: null - - property var shotSearchFilterModel: null - property var shotSearchFilterModelFunc: null - - property var playlistModel: null - property var playlistModelFunc: null - - property var filterViewModel: null - - property alias presetSelectionModel: presetsDiv.presetSelectionModel - property alias presetTreeSelectionModel: presetsMiniDiv.presetSelectionModel - - property var searchPresetsViewModel: null - property var searchTreePresetsViewModel: null - property var searchQueryViewModel: null - - property var shotPresetsModel: null - property var shotTreePresetsModel: null - property var playlistPresetsModel: null - property var editPresetsModel: null - property var referencePresetsModel: null - property var notePresetsModel: null - property var noteTreePresetsModel: null - property var mediaActionPresetsModel: null - - property var shotFilterModel: null - property var playlistFilterModel: null - property var editFilterModel: null - property var referenceFilterModel: null - property var noteFilterModel: null - property var mediaActionFilterModel: null - - property var executeQueryFunc: null - property var mergeQueriesFunc: null - - // dynamic filters from right panel - property var pipelineStepFilterIndex: null - property var onDiskFilterIndex: null - - property var lastQuery: null - property var categoryLastMode: {"Versions": "Versions Tree", "Notes": "Notes"} - - property var presetsModel: presetsDiv.presetsModel - property var searchPresetsView: presetsDiv.searchPresetsView - property alias presetsDiv: presetsDiv - property var clearFilter: null - - - color: "transparent" - - property var shotFilters: [ -"Author", -"Completion Location", -"Disable Global", -"Exclude Shot Status", -"Filter", -"Flag Media", -"Has Notes", -"Is Hero", -"Latest Version", -"Lookback", -"Newer Version", -"Older Version", -"On Disk", -"Order By", -"Pipeline Status", -"Pipeline Step", -"Playlist", -"Preferred Audio", -"Preferred Visual", -"Production Status", -"Reference Tag", -"Reference Tags", -"Result Limit", -"Sent To Client", -"Sent To Dailies", -"Sequence", -"Shot Status", -"Shot", -"Tag", -"Twig Name", -"Twig Type" - ] - property var mediaActionFilters: [ -"Author", -"Completion Location", -"Disable Global", -"Exclude Shot Status", -"Filter", -"Flag Media", -"Has Notes", -"Is Hero", -"Latest Version", -"Lookback", -"Newer Version", -"Older Version", -"On Disk", -"Order By", -"Pipeline Status", -"Pipeline Step", -"Playlist", -"Preferred Audio", -"Preferred Visual", -"Production Status", -"Reference Tag", -"Reference Tags", -"Result Limit", -"Sent To Client", -"Sent To Dailies", -"Sequence", -"Shot Status", -"Shot", -"Tag", -"Twig Name", -"Twig Type" - ] - property var playlistFilters: [ -"Author", -"Department", -"Disable Global", -"Exclude Shot Status", -"Filter", -"Flag Media", -"Has Contents", -"Has Notes", -"Lookback", -"Order By", -"Playlist Type", -"Preferred Audio", -"Preferred Visual", -"Review Location", -"Result Limit", -"Shot Status", -"Site", -"Tag", -"Unit" - ] - property var referenceFilters: [ -"Author", -"Completion Location", -"Disable Global", -"Exclude Shot Status", -"Filter", -"Flag Media", -"Has Notes", -"Is Hero", -"Latest Version", -"Lookback", -"On Disk", -"Order By", -"Pipeline Status", -"Pipeline Step", -"Preferred Audio", -"Preferred Visual", -"Production Status", -"Reference Tag", -"Reference Tags", -"Result Limit", -"Sent To Client", -"Sent To Dailies", -"Sequence", -"Shot Status", -"Shot", -"Tag", -"Twig Name", -"Twig Type" - ] - property var editFilters: [ - ] - property var noteFilters: [ -"Author", -"Disable Global", -"Exclude Shot Status", -"Filter", -"Flag Media", -"Lookback", -"Newer Version", -"Note Type", -"Older Version", -"Order By", -"Pipeline Step", -"Playlist", -"Preferred Audio", -"Preferred Visual", -"Recipient", -"Result Limit", -"Sequence", -"Shot Status", -"Shot", -"Tag", -"Twig Name", -"Twig Type", -"Version Name" - ] - - onTreeModeChanged:{ - tabBar.currentIndex = treeMode ? 1 : 0 - if(treeMode) { - presetsModel = presetsMiniDiv.presetsModel - searchPresetsView = presetsMiniDiv.searchPresetsView - } else { - presetsModel = presetsDiv.presetsModel - searchPresetsView = presetsDiv.searchPresetsView - } - - if( !["Versions","Notes"].includes(currentCategoryClass) ) tabBar.currentIndex = 0 - } - - property bool isCollapsed: false - onIsCollapsedChanged:{ - - if(isCollapsed) { - if(presetsModel){ - for(let i=0; i0) { - if(searchShotListPopup.currentIndex==-1) searchShotListPopup.currentIndex=0 - searchShotListPopup.focus = true - } - } - onTextEdited: shotSearchFilterModel.setFilterWildcard(text) - - onFocusChanged: { - if(focus) searchShotListPopup.currentIndex=-1 - } - Keys.onPressed: (event)=> { - if (event.key == Qt.Key_Down) { - if(shotSearchFilterModel.count>0) { - if(searchShotListPopup.currentIndex===-1) searchShotListPopup.currentIndex=0 - else if(searchShotListPopup.currentIndex!==-1) searchShotListPopup.currentIndex+=1 - searchShotListPopup.focus = true - } - } - else if (event.key == Qt.Key_Up) { - if(shotSearchFilterModel.count>0) { - if(searchShotListPopup.currentIndex!==-1) { - searchShotListPopup.currentIndex-=1 - searchShotListPopup.focus = true - } - } - } - } - } - XsButton{ - id: clearButton - text: "" - imgSrc: "qrc:/feather_icons/x.svg" - visible: searchTextField.length !== 0 - width: height - height: itemHeight - framePadding - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: framePadding/2 - borderColorNormal: Qt.lighter(itemColorNormal, 0.3) - onClicked: { - searchTextField.clear() - searchTextField.textEdited() - } - } - - } - } - - ListView{ id: searchShotListPopup - z: 10 - property real searchItemHeight: itemHeight/1.3 - property int searchModelCount: shotSearchFilterModel ? shotSearchFilterModel.count : 0 - model: shotSearchFilterModel - Rectangle{ anchors.fill: parent; color: "transparent"; - border.color: Qt.lighter(frameColor, 1.3); border.width: frameWidth } - - visible: searchTextField.text.length !== 0 - clip: true - orientation: ListView.Vertical - snapMode: ListView.SnapToItem - currentIndex: -1 - - x: searchField.x + framePadding - y: searchField.y + searchField.height + framePadding - width: searchField.width - height: searchModelCount>=12? searchItemHeight*12 : searchItemHeight*searchModelCount - ScrollBar.vertical: XsScrollBar { id: searchScrollBar; padding: 2}//; thumbWidth: 8} - onContentHeightChanged: { - if(height < contentHeight) searchScrollBar.policy = ScrollBar.AlwaysOn - else { - searchScrollBar.policy = ScrollBar.AsNeeded - } - } - - Keys.onUpPressed: { - if(currentIndex!=-1) currentIndex-=1 - - if(currentIndex==-1) searchTextField.focus=true - } - Keys.onDownPressed: { - if(currentIndex!=model.count-1) currentIndex+=1 - } - Keys.onPressed: (event)=> { - if (event.key == Qt.Key_Enter || event.key == Qt.Key_Return) { - selectSearchedIndex(currentIndex) - } - } - - delegate: Rectangle{ - property bool isHovered: mouseArea.containsMouse - width: searchShotListPopup.width - height: searchShotListPopup.searchItemHeight - color: searchShotListPopup.currentIndex==index? Qt.darker(itemColorActive, 2.75) : Qt.darker(palette.base, 1.5) - - Text{ - text: nameRole - font.pixelSize: fontSize - font.family: XsStyle.fontFamily - font.weight: Font.Normal - color: searchShotListPopup.currentIndex==index? textColorActive: textColorNormal - elide: Text.ElideRight - width: parent.width - height: parent.height - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - - ToolTip.text: nameRole - ToolTip.visible: mouseArea.containsMouse && truncated - } - - MouseArea{id: mouseArea; anchors.fill: parent; hoverEnabled: true - - onClicked: selectSearchedIndex(index) - - onEntered: searchShotListPopup.currentIndex=index - } - } - } - - - } - - - StackLayout { - Layout.fillWidth: true - currentIndex: tabBar.currentIndex - - onCurrentIndexChanged: { - if(currentIndex == 1) { - currentCategory = currentCategoryClass + " Tree" - } else { - currentCategory = currentCategoryClass - } - categoryLastMode[currentCategoryClass] = currentCategory - // context_pref2.value = currentCategory - } - - LeftPresetView{id: presetsDiv - isActiveView: !treeMode - backendModel: searchPresetsViewModel - onBackendModelChanged: { - if(searchPresetsViewModel) { - searchPresetsView.currentIndex = searchPresetsViewModel.activePreset - // console.log("onBackendModelChanged", searchPresetsViewModel, searchPresetsViewModel.activePreset) - } - } - } - - XsSplitView { id: leftAndRightDivs - orientation: Qt.Vertical - - Connections { - target: searchTreePresetsViewModel - function onActiveShotChanged() { - if(treeMode) { - let index = sequenceTreeModel.search_recursive(searchTreePresetsViewModel.activeShot, "nameRole") - if(index.valid) - treeTab.selectItem(index) - } - } - // function onActiveSeqChanged() { - // if(treeMode) { - // let index = sequenceTreeModel.search_recursive(searchTreePresetsViewModel.activeSeq, "nameRole") - // treeTab.selectItem(index) - // } - // } - } - - LeftTreeView{id: treeTab - SplitView.minimumWidth: parent.width - SplitView.minimumHeight: parent.height/8 - SplitView.fillHeight: true - - onClicked: { - if(searchTreePresetsViewModel.activePreset == -1) { - createOrUpdate(type, name, currentCategoryClass) - setShotSequenceTermPreset(type, name, -1) - } else { - if(!setShotSequenceTermPreset(type, name, searchTreePresetsViewModel.activePreset)) { - createOrUpdate(type, name, currentCategoryClass) - } - } - } - - onDoubleClicked: { - createOrUpdate(type, name, currentCategoryClass) - setShotSequenceTermPreset(type, name, -1) - } - } - LeftPresetView{id: presetsMiniDiv - SplitView.minimumWidth: parent.width - SplitView.minimumHeight: (itemHeight * 2) + (framePadding*2) + itemSpacing - SplitView.preferredHeight: (itemHeight + framePadding*2) + (presetTitleHeight+itemSpacing)*2.5 - isActiveView: treeMode - backendModel: searchTreePresetsViewModel - onBackendModelChanged: { - if(searchTreePresetsViewModel) { - presetsMiniDiv.searchPresetsView.currentIndex = searchTreePresetsViewModel.activePreset - } - } - } - } - } - } -} diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/SBRightPanel.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/SBRightPanel.qml deleted file mode 100644 index 28aec25d2..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/SBRightPanel.qml +++ /dev/null @@ -1,978 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient -import QtQuick.Controls.Styles 1.4 //for TextFieldStyle -import Qt.labs.qmlmodels 1.0 -import Qt.labs.platform 1.0 - -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudio 1.1 - -Rectangle{ id: rightDiv - property var searchResultsViewModel - onSearchResultsViewModelChanged:{ - searchResultsViewModel.setFilterWildcard(filterTextField.text) - } - - property var currentPresetIndex: -1 - - property var loadPlaylists: dummyFunction - property var addShotsToNewPlaylist: dummyFunction - property var addShotsToPlaylist: dummyFunction - property var addAndCompareShotsToPlaylist: dummyFunction - - property alias selectionModel: selectionModel - - // dynamic filters - property alias pipelineStepFilterIndex: filterStepBox.currentIndex - property alias onDiskFilterIndex: filterOnDiskBox.currentIndex - - color: "transparent" - signal createPreset(query: string, value: string) - signal forceSelectPreset(index: int) - - signal showLatestVersion(type: string, name: string, id: int, mode: string) - signal showVersionHistory(shot_seq: string, is_shot: bool, twigname: string, pstep: string, twigtype: string) - - SplitView.minimumWidth: minimumRightSplitViewWidth //+ (minimumWidth - (minimumSplitViewWidth*2)) - SplitView.preferredWidth: minimumRightSplitViewWidth //+ (minimumWidth - (minimumSplitViewWidth*2)) - SplitView.minimumHeight: parent.height - - MouseArea{ id: rightDivMArea; anchors.fill: parent; onPressed: focus= true; onReleased: focus= false; } - - function clearFilter() { - filterTextField.clear() - filterTextField.textEdited() - filterStepBox.currentIndex = -1 - } - - function popupMenuAction(actionText, index=-1) - { - if(actionText == menuLatest) { - let i = index - - if(i == -1) { - if(!selectionModel.selectedIndexes.length) - return - i = selectionModel.selectedIndexes[0].row - } - - let shot = searchResultsViewModel.get(i,"shotRole") - - showLatestVersion("Shot", shot, 0, "Versions") - } else if(actionText == menuHistory) { - let i = index - - if(i == -1) { - if(!selectionModel.selectedIndexes.length) - return - i = selectionModel.selectedIndexes[0].row - } - - let shot = searchResultsViewModel.get(i,"shotRole") - let seq = searchResultsViewModel.get(i,"sequenceRole") - let twigname = searchResultsViewModel.get(i,"twigNameRole") - let pstep = searchResultsViewModel.get(i,"pipelineStepRole") - let twigtype = searchResultsViewModel.get(i,"twigTypeRole") - - showVersionHistory(shot ? shot : seq, shot ? true : false, twigname, pstep, twigtype) - } else if(actionText == "Select All") { - selectionModel.resetSelection() - for(let idx=0; idx 0? 0.5:0 - - gradient: Gradient { - GradientStop { position: 0.0; color: "#77000000"} - GradientStop { position: 1.0; color: "transparent" } - } - } - Rectangle { id: bottomGradient - width: parent.width - framePadding*2 - height: itemHeight*1 - x: framePadding - anchors.bottom: buttonsDiv.top - // anchors.bottomMargin: framePadding - visible: searchResultsView.model.count - opacity: 0.5 //searchResultsView.contentY === searchResultsView.contentHeight - searchResultsView.height? 0:0.5 - - gradient: Gradient { - GradientStop { position: 0.0; color: "transparent" } - GradientStop { position: 1.0; color: "#77000000" } - } - } - Rectangle { id: bottomGradientDuplicate - width: parent.width - framePadding*2 - height: itemHeight*1 - x: framePadding - z: -1 - anchors.bottom: buttonsDiv.top - // anchors.bottomMargin: framePadding - visible: searchResultsView.model.count==0 - opacity: 0.5 //-bottomGradient.opacity - - gradient: Gradient { - GradientStop { position: 0.0; color: "transparent" } - GradientStop { position: 1.0; color: "#77000000" } - } - } - - Rectangle{ id: buttonsDiv - color: "transparent" - // x: framePadding - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottomMargin: framePadding - - anchors.leftMargin: framePadding - anchors.rightMargin: framePadding - - width: parent.width - height: ["Versions","Reference","Notes","Menu Setup"].includes(currentCategoryClass) ? itemHeight*2 + itemSpacing : itemHeight - - GridLayout{ - visible: ["Versions","Reference","Notes","Menu Setup"].includes(currentCategoryClass) - - width: parent.width - height: parent.height - - flow: GridLayout.TopToBottom - - rows: 2 - columns: 3 - rowSpacing: itemSpacing - columnSpacing: itemSpacing - - XsButton{ - text: "Add to New Playlist" - font.pixelSize: fontSize*1.2 - Layout.preferredHeight: itemHeight - Layout.fillWidth: true - onClicked: applySelection(function(items){addShotsToNewPlaylist(items)}) - focus: false - } - XsButton{ - text: "Add to Current Playlist" - font.pixelSize: fontSize*1.2 - Layout.preferredHeight: itemHeight - Layout.fillWidth: true - onClicked: applySelection(function(items){addShotsToPlaylist(items)}) - focus: false - } - XsButton{ - text: "Compare With Selected" - font.pixelSize: fontSize*1.2 - Layout.preferredHeight: itemHeight - Layout.fillWidth: true - focus: false - onClicked: applySelection(function(items){addAndCompareShotsToPlaylist(items, "A/B")}) - } - XsButton{ - text: "Compare String" - font.pixelSize: fontSize*1.2 - Layout.preferredHeight: itemHeight - Layout.fillWidth: true - focus: false - onClicked: applySelection(function(items){addAndCompareShotsToPlaylist(items, "String")}) - } - XsButton{ - text: "Compare Grid" - font.pixelSize: fontSize*1.2 - Layout.preferredHeight: itemHeight - Layout.fillWidth: true - focus: false - enabled: false - } - XsButton{ - text: "Add to Timeline" - font.pixelSize: fontSize*1.2 - Layout.preferredHeight: itemHeight - Layout.fillWidth: true - focus: false - enabled: false - } - } - - RowLayout{ - visible: !["Versions","Reference","Notes","Media Actions"].includes(currentCategoryClass) - spacing: itemSpacing - anchors.fill: parent - - XsButton{ - visible: currentCategory == "Playlists" - text: "Load Playlist" + (selectionModel.selectedIndexes.length>1 ? "s" : "") - enabled: selectionModel.selectedIndexes.length - font.pixelSize: fontSize*1.2 - Layout.preferredHeight: itemHeight - Layout.fillWidth: true - focus: false - onClicked: applySelection( - function(items) { - loadPlaylists( - items, - data_source.preferredVisual("Playlists"), - data_source.preferredAudio("Playlists"), - data_source.flagText("Playlists"), - data_source.flagColour("Playlists") - ) - } - ) - } - XsButton{ - visible: currentCategory == "Edits" - text: "Add to New Edit" - font.pixelSize: fontSize*1.2 - Layout.preferredHeight: itemHeight - Layout.fillWidth: true - focus: false - } - } - } - } - } -} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunAuthenticate.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunAuthenticate.qml deleted file mode 100644 index 2f01d2568..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunAuthenticate.qml +++ /dev/null @@ -1,280 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.12 -import QtQuick.Layouts 1.3 -import xStudio 1.1 - -import xstudio.qml.module 1.0 - -XsDialogModal { - id: dlg - property string message: "" - title: "ShotGrid Authentication" + (message ? " - "+message:"") - - width: 300 - height: 200 - - XsModuleAttributes { - id: attrs_options - attributesGroupNames: "shotgun_datasource_preference" - roleName: "combo_box_options" - } - - XsModuleAttributes { - id: attrs_values - attributesGroupNames: "shotgun_datasource_preference" - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - - GridLayout { - Layout.fillWidth: true - Layout.fillHeight: true - - id: main_layout - columns: 2 - columnSpacing: 12 - rowSpacing: 8 - - XsLabel { - id: label - Layout.alignment: Qt.AlignVCenter|Qt.AlignRight - text: "Authentication Method : " - } - - XsComboBox { - Layout.fillWidth: true - Layout.rightMargin: 6 - Layout.preferredHeight: label.height*2 - property var auth_method: attrs_values.authentication_method ? attrs_values.authentication_method : null - property bool inited: false - - font.pixelSize: XsStyle.sessionBarFontSize - font.family: XsStyle.controlTitleFontFamily - font.hintingPreference: Font.PreferNoHinting - - model: attrs_options.authentication_method ? attrs_options.authentication_method : null - - onModelChanged: { - if(model && attrs_values.authentication_method != undefined) { - currentIndex = find(attrs_values.authentication_method) - // currentText = attrs_values.authentication_method - } - } - onAuth_methodChanged: { - if(currentIndex != find(attrs_values.authentication_method)){ - inited = true - currentIndex = find(attrs_values.authentication_method) - } - } - - onCurrentIndexChanged: { - if(inited) { - attrs_values.authentication_method = model[currentIndex] - } - } - } - - XsLabel { - visible: attrs_values.authentication_method == "client_credentials" - text: "Client Identifier : " - Layout.alignment: Qt.AlignVCenter|Qt.AlignRight - } - - TextField { - Layout.fillWidth: true - Layout.rightMargin: 6 - Layout.alignment: Qt.AlignVCenter|Qt.AlignLeft - selectByMouse: true - font.family: XsStyle.fontFamily - font.hintingPreference: Font.PreferNoHinting - color: XsStyle.hoverColor - selectionColor: XsStyle.highlightColor - background: Rectangle { - anchors.fill: parent - color: XsStyle.popupBackground - radius: 5 - } - - visible: attrs_values.authentication_method == "client_credentials" - text: attrs_values.client_id ? attrs_values.client_id : null - onEditingFinished: {attrs_values.client_id = text;} - } - - XsLabel { - visible: attrs_values.authentication_method == "password" - text: "Username : " - Layout.alignment: Qt.AlignVCenter|Qt.AlignRight - } - - TextField { - Layout.fillWidth: true - Layout.rightMargin: 6 - Layout.alignment: Qt.AlignVCenter|Qt.AlignLeft - selectByMouse: true - font.family: XsStyle.fontFamily - font.hintingPreference: Font.PreferNoHinting - color: XsStyle.hoverColor - selectionColor: XsStyle.highlightColor - background: Rectangle { - anchors.fill: parent - color: XsStyle.popupBackground - radius: 5 - } - - visible: attrs_values.authentication_method == "password" - text: attrs_values.username ? attrs_values.username : null - onEditingFinished: {attrs_values.username = text;} - } - - - XsLabel { - visible: attrs_values.authentication_method == "session_token" - text: "Session Token : " - Layout.alignment: Qt.AlignVCenter|Qt.AlignRight - } - - TextField { - Layout.fillWidth: true - Layout.rightMargin: 6 - Layout.alignment: Qt.AlignVCenter|Qt.AlignLeft - selectByMouse: true - font.family: XsStyle.fontFamily - font.hintingPreference: Font.PreferNoHinting - color: XsStyle.hoverColor - selectionColor: XsStyle.highlightColor - background: Rectangle { - anchors.fill: parent - color: XsStyle.popupBackground - radius: 5 - } - - visible: attrs_values.authentication_method == "session_token" - text: attrs_values.session_token ? attrs_values.session_token : null - onEditingFinished: {attrs_values.session_token = text;} - } - - XsLabel { - visible: attrs_values.authentication_method == "client_credentials" - text: "Client Secret : " - Layout.alignment: Qt.AlignVCenter|Qt.AlignRight - } - - TextField { - Layout.fillWidth: true - Layout.rightMargin: 6 - Layout.alignment: Qt.AlignVCenter|Qt.AlignLeft - - echoMode: TextInput.Password - selectByMouse: true - font.family: XsStyle.fontFamily - font.hintingPreference: Font.PreferNoHinting - color: XsStyle.hoverColor - selectionColor: XsStyle.highlightColor - background: Rectangle { - anchors.fill: parent - color: XsStyle.popupBackground - radius: 5 - } - - visible: attrs_values.authentication_method == "client_credentials" - text: attrs_values.client_secret ? attrs_values.client_secret : null - onEditingFinished: { - attrs_values.client_secret = text - // text = attrs_values.client_secret - } - } - - XsLabel { - visible: attrs_values.authentication_method == "password" - text: "Password : " - Layout.alignment: Qt.AlignVCenter|Qt.AlignRight - } - - TextField { - Layout.fillWidth: true - Layout.rightMargin: 6 - Layout.alignment: Qt.AlignVCenter|Qt.AlignLeft - - echoMode: TextInput.Password - selectByMouse: true - font.family: XsStyle.fontFamily - font.hintingPreference: Font.PreferNoHinting - color: XsStyle.hoverColor - selectionColor: XsStyle.highlightColor - background: Rectangle { - anchors.fill: parent - color: XsStyle.popupBackground - radius: 5 - } - - visible: attrs_values.authentication_method == "password" - text: attrs_values.password ? attrs_values.password : null - onEditingFinished: { - attrs_values.password = text - // text = attrs_values.password - } - } - - - XsLabel { - Layout.alignment: Qt.AlignVCenter|Qt.AlignRight - Layout.fillHeight: true - } - } - - - DialogButtonBox { - id: myFooter - Layout.fillWidth: true - Layout.fillHeight: false - Layout.topMargin: 10 - Layout.minimumHeight: 35 - horizontalPadding: 12 - verticalPadding: 18 - background: Rectangle { - anchors.fill: parent - color: "transparent" - } - - XsLabel { - text: message - } - - RoundButton { - id: btnOK - text: qsTr("Authenticate") - width: 80 - height: 30 - radius: 5 - DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole - background: Rectangle { - radius: 5 - color: mouseArea.containsMouse?XsStyle.primaryColor:XsStyle.controlBackground - gradient:mouseArea.containsMouse?styleGradient.accent_gradient:Gradient.gradient - anchors.fill: parent - } - contentItem: Text { - text: btnOK.text - color: XsStyle.hoverColor//:XsStyle.mainColor - font.family: XsStyle.fontFamily - font.hintingPreference: Font.PreferNoHinting - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - MouseArea { - id: mouseArea - hoverEnabled: true - anchors.fill: btnOK - cursorShape: Qt.PointingHandCursor - onClicked: {forceActiveFocus() ;accept()} - } - } - } - } -} - diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunBrowserDialog.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunBrowserDialog.qml deleted file mode 100644 index 5fb516969..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunBrowserDialog.qml +++ /dev/null @@ -1,767 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient -import QtQuick.Controls.Styles 1.4 //for TextFieldStyle -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudio 1.1 - -XsWindow { id: shotgunBrowser - centerOnOpen: true - onTop: false - - property var projectModel: null - property alias projectCurrentIndex: leftDiv.projectCurrentIndex - property alias liveLinkProjectChange: leftDiv.liveLinkProjectChange - - property var authorModel: null - property var siteModel: null - property var noteTypeModel: null - property var departmentModel: null - property var playlistTypeModel: null - property var productionStatusModel: null - property var pipelineStatusModel: null - property var boolModel: null - property var reviewLocationModel: null - property var referenceTagModel: null - property var resultLimitModel: null - property var orderByModel: null - property var primaryLocationModel: null - property var lookbackModel: null - property var stepModel: null - property var onDiskModel: null - property var twigTypeCodeModel: null - property var shotStatusModel: null - - property var sequenceModel: null - property var sequenceModelFunc: null - - property var sequenceTreeModel: null - property var sequenceTreeModelFunc: null - - property var shotModel: null - property var shotModelFunc: null - - property var customEntity24Model: null - property var customEntity24ModelFunc: null - - property var shotSearchFilterModel: null - property var shotSearchFilterModelFunc: null - - property var playlistModel: null - property var playlistModelFunc: null - - property var shotResultsModel: null - property var shotTreeResultsModel: null - property var playlistResultsModel: null - property var editResultsModel: null - property var referenceResultsModel: null - property var noteResultsModel: null - property var noteTreeResultsModel: null - property var mediaActionResultsModel: null - - property var shotPresetsModel: null - property var shotTreePresetsModel: null - property var playlistPresetsModel: null - property var editPresetsModel: null - property var referencePresetsModel: null - property var notePresetsModel: null - property var noteTreePresetsModel: null - property var mediaActionPresetsModel: null - - property var shotFilterModel: null - property var playlistFilterModel: null - property var editFilterModel: null - property var referenceFilterModel: null - property var noteFilterModel: null - property var mediaActionFilterModel: null - - property var mergeQueriesFunc: null - property var executeQueryFunc: null - property var authenticateFunc: null - - property var preferredVisual: null - property var preferredAudio: null - property var flagText: null - property var flagColour: null - - property var loadPlaylists: dummyFunction - property var addShotsToNewPlaylist: dummyFunction - property var addAndCompareShotsToPlaylist: dummyFunction - property var addShotsToPlaylist: dummyFunction - - property alias presetListView: leftDiv.searchPresetsView - property bool executeQueryOnCategorySwitch: true - property bool categorySwitchedOnClick: false - - signal showRelatedVersions() - - title: "Shot Browser" - - XsWindowStateSaver - { - windowObj: shotgunBrowser - windowName: "shotgun_browser" - } - - width: 840 - minimumWidth: leftDiv.isCollapsed? minimumLeftSplitViewWidth * 1.4 : 840 - height: 480 - minimumHeight: 480 - property real oldWidth: minimumWidth - property real oldHeight: 480 - property real minimumLeftSplitViewWidth: 350 - property real minimumRightSplitViewWidth: minimumLeftSplitViewWidth * 1.25 - property real minimumSplitViewHeight: 120 - onWidthChanged: { - if((width-leftDiv.width) < minimumLeftSplitViewWidth) - leftDiv.SplitView.preferredWidth = leftDiv.width - (oldWidth-width) - oldWidth = width - } - onHeightChanged: { - if((height-leftDiv.height) < minimumSplitViewHeight) - leftDiv.SplitView.preferredHeight = leftDiv.height - (oldHeight-height) - oldHeight = height - } - - property int framePadding: 6 - property int frameWidth: 1 - property int frameRadius: 2 - property color frameColor: itemColorNormal - - property int itemHeight: 26 - property int itemSpacing: 2 - property color itemColorActive: palette.highlight - property color itemColorNormal: palette.base - - property color textColorActive: "white" - property color textColorNormal: "light grey" - property real fontSize: XsStyle.menuFontSize - property string fontFamily: XsStyle.menuFontFamily - - // property string currentCategory: context_pref.properties.value - property string currentCategoryClass: currentCategory.replace(' Tree','') - property bool treeMode: currentCategory.includes(" Tree") - - Shortcut { - context: Qt.WindowShortcut - sequence: "Ctrl+V" - onActivated: leftDiv.presetsDiv.onMenuAction("PASTE") - } - Shortcut { - context: Qt.WindowShortcut - sequence: "Ctrl+C" - onActivated: leftDiv.presetsDiv.onMenuAction("COPY") - } - Shortcut { - context: Qt.WindowShortcut - sequence: "Ctrl+Z" - onActivated: data_source.undo() - } - Shortcut { - context: Qt.WindowShortcut - sequence: "Ctrl+Shift+Z" - onActivated: data_source.redo() - } - - Shortcut { - context: Qt.WindowShortcut - sequence: "V" - onActivated: showRelatedVersions() - } - - Shortcut { - context: Qt.WindowShortcut - sequence: "S" - onActivated: { - if (visible) { - hide() - } else { - show() - requestActivate() - } - } - } - - onVisibleChanged: { - if (!visible) { - // ensure keyboard events are returned to the viewport - sessionWidget.playerWidget.viewport.forceActiveFocus() - } - } - - Component.onCompleted: { - currentCategoryUpdate() - } - - function executeMediaActionQuery(action_name, media_metadata, func) { - // find index of MediaAction.. - let mai = mediaActionPresetsModel.search(action_name) - let pid = mediaActionPresetsModel.getProjectId(media_metadata) - - // there is a possible race here with populating the project caches.. - data_source.liveLinkMetadata = media_metadata - - // console.log(action_name, projectCurrentIndex, projectModel, projectModel.count, pid) - - if(mai !== -1 && pid != -1) { - // we have a selection to iterate over.. - let query = mergeQueriesFunc(mediaActionPresetsModel.get(mai, "jsonRole"), mediaActionFilterModel.get(0, "jsonRole")) - - // we need to refresh livelinks for each item.. - query = mediaActionPresetsModel.applyLiveLink(query, media_metadata) - - Future.promise( - executeQueryFunc("Menu Setup", pid, query, false) - ).then(function(json_string) { - try { - var data = JSON.parse(json_string) - // should be array of uuids.. - func(data) - } catch(err) { - console.log(err) - } - }, - function() { - }) - } - } - - function createPresetType(mode) { - if(mode == menuLiveNotes) { - let mymodel = noteTreePresetsModel - - // check it doesn't already exist. - let ind = mymodel.search(menuLiveNotes) - if(ind == -1) { - mymodel.insert( - mymodel.rowCount(), - mymodel.index(mymodel.rowCount(),0), - { - "expanded": false, - "name": menuLiveNotes, - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "" - }, - { - "enabled": true, - "livelink": true, - "term": "Twig Name", - "value": "" - }, - { - "enabled": true, - "livelink": true, - "term": "Version Name", - "value": "" - }, - { - "enabled": false, - "livelink": true, - "term": "Pipeline Step", - "value": "" - }, - { - "enabled": false, - "term": "Note Type", - "value": "Client" -     }, - { - "enabled": true, - "term": "Flag Media", - "value": "Orange" - } - ] - } - ) - ind = mymodel.rowCount()-1 - } - currentCategory = "Notes Tree" - if(mymodel.activePreset == ind) { - executeQuery() - } else { - mymodel.activePreset = ind - } - } else if(mode == menuLiveLatest) { - let mymodel = shotTreePresetsModel - let ind = mymodel.search(menuLiveLatest) - if(ind == -1) { - mymodel.insert( - mymodel.rowCount(), - mymodel.index(mymodel.rowCount(),0), - { - "expanded": false, - "name": menuLiveLatest, - "queries": [ - { - "enabled": true, - "term": "Shot", - "livelink": true, - "value": "" - }, - { - "enabled": true, - "term": "Latest Version", - "value": "True" - }, -     { -       "enabled": true, -       "livelink": false, -       "term": "Twig Type", -       "value": "scan" -     }, -     { -       "enabled": true, -       "livelink": false, -       "term": "Twig Type", -       "value": "render/element" -     }, -     { -       "enabled": true, -       "livelink": false, -       "term": "Twig Type", -       "value": "render/out" -     }, -     { -       "enabled": true, -       "livelink": false, -       "term": "Twig Type", -       "value": "render/playblast" -     }, -     { -       "enabled": true, -       "livelink": false, -       "term": "Twig Type", -       "value": "render/playblast/working" -     }, - { - "enabled": true, - "term": "Flag Media", - "value": "Orange" - } - ] - } - ) - ind = mymodel.rowCount()-1 - } - currentCategory = "Versions Tree" - if(mymodel.activePreset == ind) { - executeQuery() - } else { - mymodel.activePreset = ind - } - } else if(mode == menuLiveHistory) { - let mymodel = shotTreePresetsModel - let ind = mymodel.search(menuLiveHistory) - - if(ind == -1) { - mymodel.insert( - mymodel.rowCount(), - mymodel.index(mymodel.rowCount(),0), - { - "expanded": false, - "name": menuLiveHistory, - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "" - }, - { - "enabled": true, - "livelink": true, - "term": "Pipeline Step", - "value": "" - }, - { - "enabled": true, - "livelink": true, - "term": "Twig Type", - "value": "" - }, - { - "enabled": true, - "livelink": true, - "term": "Twig Name", - "value": "" - }, - { - "enabled": false, - "term": "Latest Version", - "value": "True" - }, - { - "enabled": false, - "term": "Sent To Client", - "value": "True" -     }, - { - "enabled": true, - "term": "Flag Media", - "value": "Orange" - } - ] - } - ) - ind = mymodel.rowCount()-1 - } - currentCategory = "Versions Tree" - - if(mymodel.activePreset == ind) { - executeQuery() - } else { - mymodel.activePreset = ind - } - } else if(mode == "All") { - shotPresetsModel.insert( - shotPresetsModel.rowCount(), - shotPresetsModel.index(shotPresetsModel.rowCount(),0), - { - "expanded": false, - "name": "All", - "queries": [ - { - "enabled": true, - "livelink": true, - "term": "Shot", - "value": "" - } - ] - } - ) - shotPresetsModel.activePreset = shotPresetsModel.rowCount()-1 - } - } - - function updateLiveLink(media){ - if(visible && currentCategory && leftDiv.searchPresetsView.currentIndex != -1) { - // check current preset has a live link field active. - if(leftDiv.searchPresetsViewModel.hasActiveLiveLink) { - leftDiv.executeQuery() - } - } - } - - function currentCategoryUpdate() { - if(currentCategory == "Versions") - { - leftDiv.presetSelectionModel.clearSelection() - - leftDiv.filterViewModel = shotFilterModel - leftDiv.searchPresetsViewModel = shotPresetsModel - leftDiv.searchTreePresetsViewModel = shotTreePresetsModel - rightDiv.searchResultsViewModel = shotResultsModel - } - else if(currentCategory == "Versions Tree") - { - leftDiv.presetSelectionModel.clearSelection() - - leftDiv.filterViewModel = shotFilterModel - leftDiv.searchPresetsViewModel = shotPresetsModel - leftDiv.searchTreePresetsViewModel = shotTreePresetsModel - rightDiv.searchResultsViewModel = shotTreeResultsModel - } - else if(currentCategory == "Playlists") - { - leftDiv.presetSelectionModel.clearSelection() - - leftDiv.filterViewModel = playlistFilterModel - leftDiv.searchPresetsViewModel = playlistPresetsModel - rightDiv.searchResultsViewModel = playlistResultsModel - } - else if(currentCategory == "Edits") - { - leftDiv.presetSelectionModel.clearSelection() - - leftDiv.filterViewModel = editFilterModel - leftDiv.searchPresetsViewModel = editPresetsModel - rightDiv.searchResultsViewModel = editResultsModel - } - else if(currentCategory == "Reference") - { - leftDiv.presetSelectionModel.clearSelection() - - leftDiv.filterViewModel = referenceFilterModel - leftDiv.searchPresetsViewModel = referencePresetsModel - rightDiv.searchResultsViewModel = referenceResultsModel - } - else if(currentCategory == "Notes") - { - leftDiv.presetSelectionModel.clearSelection() - - leftDiv.filterViewModel = noteFilterModel - leftDiv.searchPresetsViewModel = notePresetsModel - leftDiv.searchTreePresetsViewModel = noteTreePresetsModel - rightDiv.searchResultsViewModel = noteResultsModel - } - else if(currentCategory == "Notes Tree") - { - leftDiv.presetSelectionModel.clearSelection() - - leftDiv.filterViewModel = noteFilterModel - leftDiv.searchPresetsViewModel = notePresetsModel - leftDiv.searchTreePresetsViewModel = noteTreePresetsModel - rightDiv.searchResultsViewModel = noteTreeResultsModel - } - else if(currentCategory == "Menu Setup") - { - leftDiv.presetSelectionModel.clearSelection() - - leftDiv.filterViewModel = mediaActionFilterModel - leftDiv.searchPresetsViewModel = mediaActionPresetsModel - rightDiv.searchResultsViewModel = mediaActionResultsModel - } - - // leftDiv.searchPresetsView.currentIndex = leftDiv.searchPresetsViewModel.activePreset - - // let found = false - - // for(let i=0; i< leftDiv.presetsModel.count;i++){ - // if(leftDiv.presetsModel.activePreset != -1) { - // if(categorySwitchedOnClick) executeQueryOnCategorySwitch = false - // if(leftDiv.searchPresetsView.currentIndex == leftDiv.presetsModel.activePreset) leftDiv.searchPresetsView.currentIndex = -1 - // leftDiv.searchPresetsView.currentIndex = leftDiv.presetsModel.activePreset - // found = true - // break - // } - // } - - // if(!found) - // leftDiv.searchPresetsView.currentIndex = -1 - - categorySwitchedOnClick = false - executeQueryOnCategorySwitch = true - } - - Connections { - target: context_preference - function onValueChanged() { - currentCategoryUpdate() - } - } - - Connections { - target: leftDiv - function onProjectChanged(project_id) { - // reset models - shotResultsModel.clear() - shotTreeResultsModel.clear() - playlistResultsModel.clear() - editResultsModel.clear() - referenceResultsModel.clear() - noteResultsModel.clear() - noteTreeResultsModel.clear() - - // set all preset loaded state to false. - shotPresetsModel.clearLoaded() - shotTreePresetsModel.clearLoaded() - playlistPresetsModel.clearLoaded() - editPresetsModel.clearLoaded() - referencePresetsModel.clearLoaded() - notePresetsModel.clearLoaded() - noteTreePresetsModel.clearLoaded() - } - } - - function dummyFunction(id_list) { - console.log(id_list) - } - - function addShotsToPlaylistWrapper(id_list) { - shotgunBrowser.addShotsToPlaylist( - id_list, - preferredVisual(currentCategory), - preferredAudio(currentCategory), - flagText(currentCategory), - flagColour(currentCategory) - ) - } - - function addShotsToNewPlaylistWrapper(id_list) { - shotgunBrowser.addShotsToNewPlaylist( - leftDiv.presetsModel.get(leftDiv.searchPresetsView.currentIndex, "nameRole"), - id_list, - preferredVisual(currentCategory), - preferredAudio(currentCategory), - flagText(currentCategory), - flagColour(currentCategory) - ) - } - - function addAndCompareShotsToPlaylistWrapper(id_list, mode) { - shotgunBrowser.addAndCompareShotsToPlaylist( - leftDiv.presetsModel.get(leftDiv.searchPresetsView.currentIndex, "nameRole"), - id_list, - mode, - preferredVisual(currentCategory), - preferredAudio(currentCategory), - flagText(currentCategory), - flagColour(currentCategory) - ) - } - - - XsSplitView { id: leftAndRightDivs - anchors.fill: parent - - onResizingChanged: { - if(!resizing) { leftDiv.SplitView.preferredWidth = leftDiv.width; rightDiv.SplitView.preferredWidth = rightDiv.width } - } - - SBLeftPanel{ id: leftDiv - treeMode: shotgunBrowser.treeMode - projectModel: shotgunBrowser.projectModel - projectCurrentIndex: shotgunBrowser.projectCurrentIndex - - authenticateFunc: shotgunBrowser.authenticateFunc - - authorModel: shotgunBrowser.authorModel - - sequenceModel: shotgunBrowser.sequenceModel - sequenceModelFunc: shotgunBrowser.sequenceModelFunc - - sequenceTreeModel: shotgunBrowser.sequenceTreeModel - sequenceTreeModelFunc: shotgunBrowser.sequenceTreeModelFunc - - shotModel: shotgunBrowser.shotModel - shotModelFunc: shotgunBrowser.shotModelFunc - - customEntity24Model: shotgunBrowser.customEntity24Model - customEntity24ModelFunc: shotgunBrowser.customEntity24ModelFunc - - shotSearchFilterModel: shotgunBrowser.shotSearchFilterModel - shotSearchFilterModelFunc: shotgunBrowser.shotSearchFilterModelFunc - - playlistModel: shotgunBrowser.playlistModel - playlistModelFunc: shotgunBrowser.playlistModelFunc - - siteModel: shotgunBrowser.siteModel - noteTypeModel: shotgunBrowser.noteTypeModel - departmentModel: shotgunBrowser.departmentModel - playlistTypeModel: shotgunBrowser.playlistTypeModel - productionStatusModel: shotgunBrowser.productionStatusModel - pipelineStatusModel: shotgunBrowser.pipelineStatusModel - - primaryLocationModel: shotgunBrowser.primaryLocationModel - orderByModel: shotgunBrowser.orderByModel - resultLimitModel: shotgunBrowser.resultLimitModel - referenceTagModel: shotgunBrowser.referenceTagModel - reviewLocationModel: shotgunBrowser.reviewLocationModel - boolModel: shotgunBrowser.boolModel - lookbackModel: shotgunBrowser.lookbackModel - stepModel: shotgunBrowser.stepModel - onDiskModel: shotgunBrowser.onDiskModel - twigTypeCodeModel: shotgunBrowser.twigTypeCodeModel - shotStatusModel: shotgunBrowser.shotStatusModel - - shotPresetsModel: shotgunBrowser.shotPresetsModel - shotTreePresetsModel: shotgunBrowser.shotTreePresetsModel - playlistPresetsModel: shotgunBrowser.playlistPresetsModel - editPresetsModel: shotgunBrowser.editPresetsModel - referencePresetsModel: shotgunBrowser.referencePresetsModel - notePresetsModel: shotgunBrowser.notePresetsModel - noteTreePresetsModel: shotgunBrowser.noteTreePresetsModel - mediaActionPresetsModel: shotgunBrowser.mediaActionPresetsModel - - shotFilterModel: shotgunBrowser.shotFilterModel - playlistFilterModel: shotgunBrowser.playlistFilterModel - editFilterModel: shotgunBrowser.editFilterModel - referenceFilterModel: shotgunBrowser.referenceFilterModel - noteFilterModel: shotgunBrowser.noteFilterModel - mediaActionFilterModel: shotgunBrowser.mediaActionFilterModel - - executeQueryFunc: shotgunBrowser.executeQueryFunc - mergeQueriesFunc: shotgunBrowser.mergeQueriesFunc - - pipelineStepFilterIndex: rightDiv.pipelineStepFilterIndex - onDiskFilterIndex: rightDiv.onDiskFilterIndex - - clearFilter: rightDiv.clearFilter - - onUndo: data_source.undo() - onRedo: data_source.redo() - onSnapshotGlobals: { - let preset = "" - if(currentCategory == "Versions") - preset = "shot_filter" - else if(currentCategory == "Versions Tree") - preset = "shot_filter" - else if(currentCategory == "Playlists") - preset = "playlist_filter" - else if(currentCategory == "Edits") - preset = "edit_filter" - else if(currentCategory == "Reference") - preset = "reference_filter" - else if(currentCategory == "Notes") - preset = "note_filter" - else if(currentCategory == "Notes Tree") - preset = "note_filter" - else if(currentCategory == "Menu Setup") - preset = "media_action_filter" - - data_source.snapshot(preset) - } - onSnapshotPresets: { - let preset = "" - if(currentCategory == "Versions") - preset = "shot" - else if(currentCategory == "Versions Tree") - preset = "shot_tree" - else if(currentCategory == "Playlists") - preset = "playlist" - else if(currentCategory == "Edits") - preset = "edit" - else if(currentCategory == "Reference") - preset = "reference" - else if(currentCategory == "Notes") - preset = "note" - else if(currentCategory == "Notes Tree") - preset = "note_tree" - else if(currentCategory == "Menu Setup") - preset = "media_action" - - data_source.snapshot(preset) - } - } - - SBRightPanel{ id: rightDiv - currentPresetIndex: leftDiv.searchPresetsView.currentIndex - - loadPlaylists: shotgunBrowser.loadPlaylists - addShotsToPlaylist: shotgunBrowser.addShotsToPlaylistWrapper - addShotsToNewPlaylist: shotgunBrowser.addShotsToNewPlaylistWrapper - addAndCompareShotsToPlaylist: shotgunBrowser.addAndCompareShotsToPlaylistWrapper - - onShowLatestVersion: { - currentCategory = "Versions Tree" - leftDiv.createOrUpdate(type, name, mode) - } - - onShowVersionHistory: { - // console.log("onShowVersionHistory", shot, twigname, pstep, twigtype) - currentCategory = "Versions Tree" - leftDiv.updateVersionHistory(shot_seq, is_shot, twigname, pstep, twigtype) - } - - Connections { - target: shotgunBrowser - function onShowRelatedVersions() { - rightDiv.popupMenuAction(menuHistory) - } - } - } - } -} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunButton.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunButton.qml deleted file mode 100644 index 6fc63ce30..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunButton.qml +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.12 - -import xStudio 1.0 - -import xstudio.qml.module 1.0 - -XsTrayButton { - // doesn't work - anchors.fill: parent - text: "Shot Browser" - source: "qrc:///feather_icons/download-cloud.svg" - tooltip: "Open the Shot Browser Panel." - buttonPadding: pad - toggled_on: sessionFunction.object_map["ShotgunRoot"].browser.visible - onClicked: sessionFunction.object_map["ShotgunRoot"].toggle_browser() -} \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunCreatePlaylist.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunCreatePlaylist.qml deleted file mode 100644 index 602b586ed..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunCreatePlaylist.qml +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.12 -import QtQuick.Layouts 1.3 -import xStudio 1.1 - -import xstudio.qml.module 1.0 - -XsDialogModal { - id: dialog - title: "Create ShotGrid Playlist" - - property var playlist_uuid: null - property int validMediaCount: 0 - property int invalidMediaCount: 0 - property bool linking: false - - property var siteModel: null - property var siteCurrentIndex: -1 - property var projectModel: null - property var projectCurrentIndex: -1 - property var playlistTypeModel: null - - - property alias playlist_name: playlist_name_.text - property alias site_name: siteCB.currentText - property alias playlist_type: ptype.currentText - property int project_id: project.currentIndex !==-1 ? data_source.termModels.projectModel.get(project.currentIndex, "idRole") : -1 - - onVisibleChanged: { - if(visible) { - playlist_name_.selectAll() - playlist_name_.forceActiveFocus() - } - } - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - - GridLayout { - id: main_layout - Layout.fillWidth: true - Layout.fillHeight: true - columns: 2 - columnSpacing: 12 - rowSpacing: 8 - - XsLabel { - id: project_label - text: "Project : " - Layout.alignment: Qt.AlignVCenter|Qt.AlignRight - } - XsComboBox { - id: project - Layout.fillWidth: true - Layout.preferredHeight: project_label.height*2 - Layout.rightMargin: 6 - - model: projectModel - currentIndex: projectCurrentIndex - textRole: "nameRole" - valueRole: "nameRole" - - font.pixelSize: XsStyle.sessionBarFontSize - font.family: XsStyle.controlTitleFontFamily - font.hintingPreference: Font.PreferNoHinting - selectTextByMouse: true - editable: true - } - - XsLabel { - text: "Site : " - Layout.alignment: Qt.AlignVCenter|Qt.AlignRight - } - XsComboBox { - id: siteCB - Layout.fillWidth: true - Layout.preferredHeight: project_label.height*2 - Layout.rightMargin: 6 - - font.pixelSize: XsStyle.sessionBarFontSize - font.family: XsStyle.controlTitleFontFamily - font.hintingPreference: Font.PreferNoHinting - - model: siteModel - currentIndex: siteCurrentIndex - textRole: "nameRole" - valueRole: "nameRole" - } - - XsLabel { - id: ptype_label - text: "Type : " - Layout.alignment: Qt.AlignVCenter|Qt.AlignRight - } - XsComboBox { - id: ptype - Layout.fillWidth: true - Layout.preferredHeight: ptype_label.height*2 - Layout.rightMargin: 6 - - model: playlistTypeModel - - onModelChanged: { - if(model) - currentIndex = model.search("Dailies", "nameRole") - } - - currentIndex: -1 - textRole: "nameRole" - valueRole: "nameRole" - - font.pixelSize: XsStyle.sessionBarFontSize - font.family: XsStyle.controlTitleFontFamily - font.hintingPreference: Font.PreferNoHinting - selectTextByMouse: true - editable: true - } - - - XsLabel { - text: "Name : " - Layout.alignment: Qt.AlignVCenter|Qt.AlignRight - } - - TextField { - id: playlist_name_ - Layout.fillWidth: true - Layout.rightMargin: 6 - Layout.alignment: Qt.AlignVCenter|Qt.AlignLeft - - selectByMouse: true - font.family: XsStyle.fontFamily - font.hintingPreference: Font.PreferNoHinting - font.pixelSize: XsStyle.popupControlFontSize - color: XsStyle.hoverColor - selectionColor: XsStyle.highlightColor - onAccepted: dialog.accept() - background: Rectangle { - anchors.fill: parent - color: XsStyle.popupBackground - radius: 5 - } - } - XsLabel { - text: "Valid ShotGrid Media : " - Layout.alignment: Qt.AlignVCenter|Qt.AlignRight - } - XsLabel { - text: linking ? "Linking media to versions..." : validMediaCount + " / " + (validMediaCount+invalidMediaCount) - Layout.alignment: Qt.AlignVCenter|Qt.AlignLeft - } - XsLabel { - Layout.fillHeight: true - } - } - - - RowLayout { - id: myFooter - Layout.fillWidth: true - Layout.fillHeight: false - Layout.topMargin: 10 - Layout.minimumHeight: 35 - - focus: true - Keys.onReturnPressed: accept() - Keys.onEscapePressed: reject() - - XsRoundButton { - text: qsTr("Cancel") - Layout.leftMargin: 10 - Layout.bottomMargin: 10 - Layout.fillWidth: true - Layout.fillHeight: true - Layout.minimumWidth: dialog.width / 5 - - onClicked: { - reject() - } - } - XsHSpacer{} - XsRoundButton { - text: "Create" - highlighted: !linking - enabled: !linking - - Layout.rightMargin: 10 - Layout.minimumWidth: dialog.width / 5 - Layout.fillWidth: true - Layout.fillHeight: true - Layout.bottomMargin: 10 - onClicked: { - accept() - } - } - } - } -} - diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunHelpers.js b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunHelpers.js deleted file mode 100644 index fa0c0e3fb..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunHelpers.js +++ /dev/null @@ -1,56 +0,0 @@ - -.pragma library - -function handle_response(result_string, title, only_on_error=true, body = "", dialog=null) { - var result = false - - if(result_string.length) { - try { - var data = JSON.parse(result_string) - - // got json.. these are massively inconsistent.. - let has_error = false - // if("status" in data && status !== null) { - // throw "Bad status." - // } - - if(! "data" in data) { - throw "Missing data." - } - - if("error" in data) { - throw "ShotGrid error." - } - - if("errors" in data) { - if(data["errors"][0]["status"] !== null) - throw "ShotGrid error." - } - - // if("status" in data["data"] && data["data"]["status"] !== null && data["data"]["status"] !== "success" ) { - // throw "Bad status." - // } - - result = true - - if(!only_on_error && dialog !== null) { - dialog.title = title - if(body) { - dialog.text = body + " completed." - } else { - dialog.text = data["data"]["status"] - } - dialog.open() - } - } catch(err) { - console.log(err, result_string) - if(title && dialog !== null) { - dialog.title = title - dialog.text = err + "\n" + body + "\n" + result_string - dialog.open() - } - result = false - } - } - return result -} diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunMenu.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunMenu.qml deleted file mode 100644 index aaf06576a..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunMenu.qml +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import xStudio 1.0 - -XsMenu { - title: qsTr("ShotGrid Playlists") - - XsMenuItem { - mytext: qsTr("Create Selected ShotGrid Playlists...") - onTriggered: sessionFunction.object_map["ShotgunRoot"].create_playlist() - } - - XsMenuItem { - mytext: qsTr("Update Selected ShotGrid Playlists") - onTriggered: sessionFunction.object_map["ShotgunRoot"].update_playlist() - } - - XsMenuItem { - mytext: qsTr("Refresh Selected ShotGrid Playlists") - onTriggered: sessionFunction.object_map["ShotgunRoot"].refresh_playlist() - } - - XsMenuSeparator {} - - XsMenuItem { - mytext: qsTr("Authentication...") - onTriggered: sessionFunction.object_map["ShotgunRoot"].do_authentication() - } - // XsMenuItem { - // mytext: qsTr("Preferences...") - // onTriggered: session.object_map["ShotgunPlaylist"].do_preferences() - // } -} diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunPreferences.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunPreferences.qml deleted file mode 100644 index bc469abb4..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunPreferences.qml +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.12 -import QtQuick.Layouts 1.3 -import xStudio 1.0 - -import xstudio.qml.module 1.0 - -XsDialogModal { - id: dlg - title: "ShotGrid Preferences" -} - diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunPublishNotes.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunPublishNotes.qml deleted file mode 100644 index c4d548109..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunPublishNotes.qml +++ /dev/null @@ -1,486 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient -import Qt.labs.qmlmodels 1.0 //for DelegateChooser -import xstudio.qml.helpers 1.0 - -import xStudio 1.1 - - -XsWindow { - id: publishNotesDialog - - title: publishSelected ? "Published Selected Media Notes" : "Publish Playlist Notes" - - width: minimumWidth - minimumWidth: 400 - // maximumWidth: app_window.width - - height: minimumHeight - minimumHeight: 550 - maximumHeight: 570 - - centerOnOpen: true - - property int itemHeight: 22 - property int itemSpacing: 4 - - property int padding: 6 - - property int notesCount: (payload_obj ? payload_obj["payload"].length : 0) - onNotesCountChanged:{ - if(notesCount===1) countDisplay.text = "Ready to publish " +notesCount+" note." - else if(notesCount>1) countDisplay.text = "Ready to publish " +notesCount+" notes." - else countDisplay.text = "No notes to publish." - } - - property var projectModel: null - property var playlists: null - property var groups: null - - property var playlist_uuid: null - - property var payload: null - property var payload_obj: null - - property var data_source: null - property var publish_func: null - - property bool publishSelected: false - - property alias notify_owner: notify_owner_cb.checked - property alias combine: combine_cb.checked - property alias add_time: add_time_cb.checked - property alias add_playlist_name: add_name_cb.checked - property alias add_type: add_type_cb.checked - property alias ignore_with_only_drawing: ignore_with_only_drawing_cb.checked - property alias skip_already_published: skip_already_published_cb.checked - property string default_type: prefs.values.defaultType !== undefined ? prefs.values.defaultType : "" - - property int notify_group_id: notify_group_cb.currentIndex !==-1 && notify_group_cb.checked && notify_group_cb.model ? notify_group_cb.model.get(notify_group_cb.currentIndex, "idRole") : 0 - // property var notify_group_ids: notify_group_cb.model ? notify_group_cb.checkedIndexes:0 //#TODO - - Connections { - target: app_window.mediaSelectionModel - function onSelectionChanged(selected,deselected) { - if(publishNotesDialog.visible) { - let updated = false - for(let i =0; i a.row - b.row )), ItemSelectionModel.ClearAndSelect - ) - - }, 500); - } else { - status_bar.normalMessage("No results", "Shot Browser - Substitute with") - } - } - ) - } - ) - } - - function compare_with(preset_name) { - let selected = XsUtils.cloneArray(app_window.mediaSelectionModel.selectedIndexes) - - selected.forEach( - function (item, index) { - if(item.valid) { - browser.executeMediaActionQuery(preset_name, item.model.getJSON(item, ""), - function (data) { - // load item after existing - let parent_uuid = item.model.get(item.parent.parent, "actorUuidRole") - let media_uuid = item.model.get(item, "actorUuidRole") - let next_media_uuid = item.model.get(item.model.index(item.row + 1, 0, item.parent), "actorUuidRole") - - if(next_media_uuid == undefined) - next_media_uuid = helpers.QVariantFromUuidString("") - - let result = JSON.parse( - data_source.addVersionToPlaylist( - JSON.stringify(data), - parent_uuid, - next_media_uuid - ) - ) - - // order matters,, - - if(result.length) { - connection_delay_timer.setTimeout(function(){ - let indexs = XsUtils.cloneArray(app_window.mediaSelectionModel.selectedIndexes) - for(let i=0;i a.row - b.row )), - ItemSelectionModel.ClearAndSelect - ) - }, 500); - - } else { - status_bar.normalMessage("No results", "Shot Browser - Compare with") - } - } - ) - } - } - ) - playhead_attrs.compare = "A/B" - } - - -// 2023-04-21 15:36:32.576] [xstudio] [info] processChildren PlayheadSelection 1 -// "Could not convert argument 2 at" -// "@file:///user_data/RND/dev/xstudio/build_rel/bin/plugin/qml/Shotgun.1/ShotgunRoot.qml:1047" -// "@file:///user_data/RND/dev/xstudio/build_rel/bin/plugin/qml/Shotgun.1/ShotgunBrowserDialog.qml:227" -// "@qrc:/extern/QuickPromise/promise.js:104" -// "@qrc:/extern/QuickPromise/promise.js:284" -// "@qrc:/extern/QuickPromise/promise.js:301" -// "@qrc:/extern/QuickPromise/promise.js:248" -// "@qrc:/extern/QuickPromise/promise.js:237" -// "@qrc:/extern/QuickPromise/promise.js:28" -// "Passing incompatible arguments to C++ functions from JavaScript is dangerous and deprecated." -// "This will throw a JavaScript TypeError in future releases of Qt!" -// [2023-04-21 15:36:39.925] [xstudio] [warning] SEND -// qml: QModelIndex() -// qml: TypeError: Property 'clear' of object QModelIndex(5,0,0x9bad210,xstudio::ui::qml::SessionModel(0x343b2d0)) is not a function - - - - - function createPresetType(mode) { - browser.createPresetType(mode) - } - - function toggle_browser() { - data_source.connected = true - browser.toggle() - if(browser.visible) { - browser.raise() - browser.requestActivate() - } - } - - function show_browser() { - data_source.connected = true - browser.show() - browser.raise() - } - - function loadPlaylists(items, preferred_visual="SG Movie", preferred_audio="SG Movie", flag_text="", flag_colour="") { - items.forEach( - function (item, index) { - load_playlist(data_source, item.id, item.name, error, index==0, preferred_visual, preferred_audio, flag_text, flag_colour) - } - ) - } - - // items are version metadata from shotgun. - function addVersionsToPlaylist(uuid, items, preferred_visual="SG Movie", preferred_audio="SG Movie", flag_text="", flag_colour="") { - let query = "" - - if(typeof(items) == "string") { - try { - let j = JSON.parse(items) - j["context"] = new Map() - j["context"]["visual_source"] = preferred_visual - j["context"]["audio_source"] = preferred_audio - j["context"]["flag_text"] = flag_text - j["context"]["flag_colour"] = flag_colour - query = JSON.stringify(j) - }catch(err){ - } - } else { - let versions = []; - items.forEach( - function (item, index) { - versions.push(JSON.stringify(item.json)) - } - ) - if(versions.length) { - query = '{"data":[' + versions.join(",") + '], ' + - '"context": {' + - '"visual_source": "' + preferred_visual + '",' + - '"audio_source": "' + preferred_audio + '",' + - '"flag_text": "' + flag_text + '",' + - '"flag_colour": "' + flag_colour + '"' + - '}'+ - '}' - } - } - - if(query.length) { - // add versions to playlist. - var fa = data_source.addVersionToPlaylistFuture( - query, - uuid - ) - Future.promise(fa).then( - function(json_string) { - try { - var data = JSON.parse(json_string) - let index = sessionSelectionModel.model.search_recursive(uuid,"actorUuidRole") - app_window.sessionFunction.setActivePlaylist(index) - app_window.requestActivate() - app_window.raise() - sessionWidget.playerWidget.forceActiveFocus() - - // add media uuid to selection and focus it. - let mind = sessionSelectionModel.model.search("{"+data[0]+"}", "actorUuidRole", sessionSelectionModel.model.index(0,0, index)) - if(mind.valid) - app_window.sessionFunction.setActiveMedia(mind) - - } catch(err) { - console.log(err) - ShotgunHelpers.handle_response(json_string) - } - }, - function() { - } - ) - } - } - - - // button - function addAndCompareShotsToPlaylist(name, items, mode="A/B", preferred_visual="SG Movie", preferred_audio="SG Movie", flag_text="", flag_colour="") { - - let query = "" - - if(typeof(items) == "string") { - try { - let j = JSON.parse(items) - j["context"] = new Map() - j["context"]["visual_source"] = preferred_visual - j["context"]["audio_source"] = preferred_audio - j["context"]["flag_text"] = flag_text - j["context"]["flag_colour"] = flag_colour - query = JSON.stringify(j) - }catch(err){ - } - } else { - let versions = []; - items.forEach( - function (item, index) { - versions.push(JSON.stringify(item.json)) - } - ) - if(versions.length) { - query = '{"data":[' + versions.join(",") + '], ' + - '"context": {' + - '"visual_source": "' + preferred_visual + '",' + - '"audio_source": "' + preferred_audio + '",' + - '"flag_text": "' + flag_text + '",' + - '"flag_colour": "' + flag_colour + '"' + - '}'+ - '}' - } - } - - if(query.length) { - var uuid - - if(!app_window.sessionSelectionModel.currentIndex.valid) { - let index = app_window.sessionFunction.createPlaylist(name) - uuid = index.model.get(index, "actorUuidRole") - - app_window.sessionFunction.setActivePlaylist(index) - - // app_window.sessionSelectionModel.setCurrentIndex(index, ItemSelectionModel.setCurrentIndex|ItemSelectionModel.ClearAndSelect) - } else { - uuid = app_window.sessionSelectionModel.currentIndex.model.get(app_window.sessionSelectionModel.currentIndex, "actorUuidRole") - } - - let next_media_uuid = undefined - let selected = XsUtils.cloneArray(app_window.mediaSelectionModel.selectedIndexes) - - if(selected.length) { - let mind = selected[selected.length-1] - let nmind = mind.model.index(mind.row+1, 0, mind.parent) - if(nmind.valid) { - next_media_uuid = nmind.model.get(nmind, "actorUuidRole") - } - } - - if(next_media_uuid == undefined) - next_media_uuid = helpers.QVariantFromUuidString("") - - // add versions to playlist. - - Future.promise( - data_source.addVersionToPlaylistFuture( - query, - uuid, - next_media_uuid - ) - ).then( - function(json_string) { - // select media uuids.. - try { - var data = JSON.parse(json_string) - // should be array of uuids.. - - // let selection = app_window.mediaSelectionModel.selectedIndexes - let indexs = [] - - for(let i = 0; i< data.length; ++i) { - indexs.push(app_window.mediaSelectionModel.model.search_recursive(helpers.QVariantFromUuidString(data[i]), "actorUuidRole")) - } - - app_window.mediaSelectionModel.select(helpers.createItemSelection(indexs), ItemSelectionModel.Select) - - playhead_attrs.compare = mode - } catch(err) { - console.log(err) - ShotgunHelpers.handle_response(json_string) - } - }, - function() { - } - ) - } - } - - function addShotsToNewPlaylist(name, items, preferred_visual="SG Movie", preferred_audio="SG Movie", flag_text="", flag_colour="") { - let index = app_window.sessionFunction.createPlaylist(name) - let uuid = index.model.get(index, "actorUuidRole") - - app_window.sessionFunction.setActivePlaylist(index) - - // app_window.sessionSelectionModel.setCurrentIndex(index, ItemSelectionModel.setCurrentIndex|ItemSelectionModel.ClearAndSelect) - - addVersionsToPlaylist(uuid, items, preferred_visual, preferred_audio, flag_text, flag_colour ) - } - - function addShotsToPlaylist(items, preferred_visual="SG Movie", preferred_audio="SG Movie", flag_text="", flag_colour="") { - let index = app_window.currentSource.index - - if(index == undefined || !index.valid) { - index = app_window.sessionFunction.createPlaylist("Shot Browser Media") - } - - let uuid = index.model.get(index, "actorUuidRole") - - app_window.sessionFunction.setActivePlaylist(index) - - // app_window.sessionSelectionModel.setCurrentIndex(index, ItemSelectionModel.setCurrentIndex|ItemSelectionModel.ClearAndSelect) - - addVersionsToPlaylist(uuid, items, preferred_visual, preferred_audio, flag_text, flag_colour) - } - - function load_playlist(data_source, id, name, error, make_active=true, preferred_visual="SG Movie", preferred_audio="SG Movie", flag_text="", flag_colour="") { - // create new playlist - let index = app_window.sessionFunction.createPlaylist(name) - - if(make_active){ - app_window.sessionFunction.setActivePlaylist(index) - } - - let uuid = index.model.get(index, "actorUuidRole") - index.model.set(index, true, "busyRole") - - // get playlist json from shotgun - Future.promise(data_source.getPlaylistVersionsFuture(id)).then(function(json_string) { - try { - var data = JSON.parse(json_string) - if(data["data"]){ - Future.promise(index.model.setJSONFuture(index, JSON.stringify(data['data']), "/metadata/shotgun/playlist")).then( - function(result) { - addDecorator(uuid) - addMenusFull(uuid) - }, - function() { - } - ) - - data["context"] = new Map() - data["context"]["visual_source"] = preferred_visual - data["context"]["audio_source"] = preferred_audio - data["context"]["flag_text"] = flag_text - data["context"]["flag_colour"] = flag_colour - - // add versions to playlist. - Future.promise(data_source.addVersionToPlaylistFuture(JSON.stringify(data), uuid)).then( - function(json_string) { - if(make_active) { - var data = JSON.parse(json_string) - let index = sessionSelectionModel.model.search_recursive(uuid,"actorUuidRole") - - // add media uuid to selection and focus it. - let mind = sessionSelectionModel.model.search("{"+data[0]+"}", "actorUuidRole", sessionSelectionModel.model.index(0,0, index)) - if(mind.valid) - app_window.sessionFunction.setActiveMedia(mind) - } - - index.model.set(index, false, "busyRole") - ShotgunHelpers.handle_response(json_string) - }, - function() { - index.model.set(index, false, "busyRole") - } - ) - } else { - index.model.set(index, false, "busyRole") - error.title = "Load ShotGrid Playlist " + name - error.text = json_string - error.open() - } - } - catch(err) { - index.model.set(index, false, "busyRole") - error.title = "Load ShotGrid Playlist " + name - error.text = err + "\n" + json_string - error.open() - console.log(err, json_string) - } - }) - } -} diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunTagDialog.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunTagDialog.qml deleted file mode 100644 index ef6a1f51e..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/ShotgunTagDialog.qml +++ /dev/null @@ -1,171 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient -import QtQuick.Controls.Styles 1.4 //for TextFieldStyle -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudio 1.1 - -XsWindow { - id: shotgunDialog - centerOnOpen: true - onTop: false - - width: 600 - height: 600 - - title: "ShotGrid Tag Browser" - - XsWindowStateSaver - { - windowObj: shotgunDialog - windowName: "shotgun_tag_browser" - } - - property var data_source: null - property var tagMethod: null - property var untagMethod: null - property var newTagMethod: null - property var renameTagMethod: null - property var removeTagMethod: null - property var activeTags: [] - signal updateView() - - XsTimer { - id: delayTimer - } - - onVisibleChanged: { - if(visible) - updateActive() - } - - function doAction(id, name) { - if(actionMode.currentText == "Assign") { - tagMethod(id) - delayTimer.setTimeout(function() { - updateActive() - }, 1000) - } else if(actionMode.currentText == "De-Assign") { - untagMethod(id) - delayTimer.setTimeout(function() { - updateActive() - }, 1000) - } else if(actionMode.currentText == "Rename") { - renameTagMethod(id, name) - } else if(actionMode.currentText == "Remove") { - removeTagMethod(id) - } - } - - function updateActive() { - if(shotgunDialog.visible) { - if(app_window.mediaSelectionModel.selectedIndexes.length) { - Future.promise( - app_window.sessionModel.getJSONFuture(app_window.mediaSelectionModel.selectedIndexes[0], "/metadata/shotgun/version/relationships/tags/data") - ).then(function(json_string) { - activeTags = [] - let reftags = JSON.parse(json_string) - for(let i =0;i", dst_row, rootIndex) - // invert logic if moving down - if(dst_row > src_row) { - baseModel.moveRows(rootIndex, dst_row, 1, rootIndex, src_row) - } else { - baseModel.moveRows(rootIndex, src_row, 1, rootIndex, dst_row) - } - } - - function insert(row, data) { - baseModel.insert(row, rootIndex, data) - } - - function append(data, root=rootIndex) { - baseModel.insert(rowCount(root), root, data) - } - - function rowCount(root=rootIndex) { - return baseModel.rowCount(root) - } - - function get(row, role) { - return baseModel.get(row, rootIndex, role) - } - - function set(row, value, role) { - return baseModel.set(row, value, role, rootIndex) - } - - model: baseModel - - delegate: - MouseArea { id: treeDelegate - property bool isExpanded: expandedModel.selectedIndexes.includes(treeModel.modelIndex(index, treeModel.rootIndex, expandedModel)) - property bool isSelected: selectionModel.selectedIndexes.includes(treeModel.modelIndex(index, treeModel.rootIndex, selectionModel)) - property bool isPopulated: false - property int childCount: treeModel.rowCount(treeModel.modelIndex(index, treeModel.rootIndex)) - // property var isMouseHovered: containsMouse - readonly property int treeItemHeight: 20 - - hoverEnabled: true - width: treeView.width - height: isExpanded ? (treeItemHeight + childView.height) : treeItemHeight - - onClicked: { - selectionModel.select(treeModel.modelIndex(index, treeModel.rootIndex), ItemSelectionModel.ClearAndSelect) - } - onDoubleClicked: { - itemDoubleClicked(typeRole, nameRole, idRole) - } - - onIsExpandedChanged: { - if(isExpanded) { - if(!isPopulated) { - createTreeNode(childView, treeModel.model, treeModel.modelIndex(index, treeModel.rootIndex), selectionModel, expandedModel, itemDoubleClicked, scrollTo) - isPopulated = true - } - jumpToSelected() - } - } - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - Rectangle{id: nameDiv - Layout.preferredWidth: parent.width - Layout.preferredHeight: treeItemHeight - color: isSelected ? Qt.darker(itemColorActive, 2.75) : "transparent" - - XsButton{id: expandButton - - text: "" - imgSrc: childCount > 0 ? "qrc:/feather_icons/chevron-down.svg" : null - width: height - height: parent.height - frameWidth*2 - image.sourceSize.width: height/1.2 - image.sourceSize.height: height/1.2 - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - borderColorNormal: Qt.lighter(itemColorNormal, 0.3) - opacity: !childCount ? 0 : 1 - isActive: isExpanded - enabled: childCount > 0 - - scale: rotation==0 || rotation==-90? 1 : 0.85 - rotation: (isExpanded)? 0: -90 - Behavior on rotation {NumberAnimation{id: rotationAnim; duration: 150 }} - - onClicked: expandedModel.select(treeModel.modelIndex(index, treeModel.rootIndex), ItemSelectionModel.Toggle) - Component.onCompleted: { - if(isExpanded) { - createTreeNode(childView, treeModel.model, treeModel.modelIndex(index, treeModel.rootIndex), selectionModel, expandedModel, itemDoubleClicked, scrollTo) - isPopulated = true - jumpToSelected() - } - } - } - - // Image { id: icon - // width: treeView.isIconVisible? height : 0 - // height: parent.height - framePadding - // anchors.verticalCenter: parent.verticalCenter - // anchors.left: expandButton.right - // anchors.leftMargin: treeView.isIconVisible? framePadding : 0 - // source: childCount > 0 ? "qrc:/feather_icons/archive.svg" : "qrc:/feather_icons/box.svg" - // sourceSize.width: height/1.3 - // sourceSize.height: height/1.3 - // layer { - // enabled: true - // effect: - // ColorOverlay { - // color: isMouseHovered? textColorNormal : isSelected? itemColorActive : "#404040" - // } - // } - // } - - XsText{ - text: !isCollapsed? nameRole : nameRole.substr(0,2)+".." - width: isCollapsed? parent.width: parent.width - expandButton.width - framePadding*5 //icon.width - framePadding*5 - height: parent.height - anchors.verticalCenter: parent.verticalCenter - anchors.left: expandButton.right//icon.right - anchors.leftMargin: framePadding - - elide: Text.ElideRight - font.pixelSize: fontSize*1.2 - - - color: isSelected ? (containsMouse ? textColorNormal : textColorActive) : (containsMouse? itemColorActive : textColorNormal) - // isSelected? textColorActive : containsMouse? itemColorActive : textColorNormal//itemColorNormal - // color: isMouseHovered || presetLoadedRole? textColorActive: textColorNormal - - - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - ToolTip.text: nameRole - ToolTip.visible: containsMouse && isCollapsed - visible: !(treeView.isEditable && treeView.menuActionIndex == index) - } - - // XsButton{id: favButton - - // text: "" - // imgSrc: isFavorited? "qrc:/icons/star-filled.svg":"qrc:/feather_icons/star.svg" - // visible: typeRole=="Shot" && (containsMouse || isFavorited) - // width: height - // height: parent.height - frameWidth*2 - // image.sourceSize.width: height/1.2 - // image.sourceSize.height: height/1.2 - // anchors.verticalCenter: parent.verticalCenter - // anchors.right: parent.right - // anchors.rightMargin: itemSpacing*4 - // bgColorNormal: "transparent" - // borderWidth: hovered? 1 : 0 - // property bool isFavorited: false - // layer { - // textureSize: Qt.size(favButton.width, favButton.height) - // enabled: true - // effect: - // ColorOverlay { - // color: favButton.pressed? "transparent": favButton.isFavorited? itemColorActive : "transparent" - // } - // } - - // onClicked: { - // isFavorited= !isFavorited - // } - // } - // XsButton{id: pinButton - - // text: "" - // imgSrc: "qrc:/icons/pin.png" - // visible: typeRole=="Shot" && containsMouse - // width: height - // height: parent.height - frameWidth*2 - // image.sourceSize.width: height/1.2 - // image.sourceSize.height: height/1.2 - // anchors.verticalCenter: parent.verticalCenter - // anchors.right: favButton.left - // anchors.rightMargin: itemSpacing - // bgColorNormal: "transparent" - // borderWidth: hovered? 1 : 0 - // // layer { - // // textureSize: Qt.size(pinButton.width, pinButton.height) - // // enabled: true - // // effect: - // // ColorOverlay { - // // color: pinButton.hovered? pinButton.textColorPressed : "transparent" - // // } - // // } - // onClicked: { - // } - // } - } - - Item { id: childView - // visible: isExpanded - Layout.fillWidth: true - Layout.minimumWidth: parent.width - Layout.fillHeight: true - Layout.minimumHeight: isExpanded ? treeItemHeight * children[0].totalChildCount : 0 - Layout.maximumHeight: isExpanded ? treeItemHeight * children[0].totalChildCount : 0 - } - } - } - } - } -} diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/TreeDelegateModel.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/TreeDelegateModel.qml deleted file mode 100644 index 87b433729..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/TreeDelegateModel.qml +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQml.Models 2.14 - -DelegateModel { - property var baseModel: null - - onBaseModelChanged: model = baseModel - - function remove(row) { - model.removeRows(row, 1, rootIndex) - } - - function move(src_row, dst_row) { - // console.log("TreeDelegateModel.qml", src_row, "->", dst_row, rootIndex) - // invert logic if moving down - if(dst_row > src_row) { - model.moveRows(rootIndex, dst_row, 1, rootIndex, src_row) - } else { - model.moveRows(rootIndex, src_row, 1, rootIndex, dst_row) - } - } - - function insert(row, data) { - model.insert(row, rootIndex, data) - } - - function append(data, root=rootIndex) { - model.insert(rowCount(root), root, data) - } - - function rowCount(root=rootIndex) { - return model.rowCount(root) - } - - function get(row, role) { - return model.get(row, rootIndex, role) - } - - function set(row, value, role) { - return model.set(row, value, role, rootIndex) - } -} diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/TreeNode.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/TreeNode.qml deleted file mode 100644 index 6ddd86724..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/TreeNode.qml +++ /dev/null @@ -1,191 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.12 -import QtQuick.Layouts 1.3 -import QtQml.Models 2.14 -import xStudio 1.1 - - -Item { - property alias model: treeModel.model - property alias rootIndex: treeModel.rootIndex - property var selection: null - - function createTreeNode(parent, myModel, myRootIndex, myselection) { - if (Qt.createComponent("TreeNode.qml").createObject(parent, {model: myModel, rootIndex: myRootIndex, selection:myselection}) == null) { - console.log("Error creating object"); - } - } - anchors.fill: parent - - ListView { - id: control - model: treeModel - anchors.fill: parent - - TreeDelegateModel { - id: treeModel - - delegate: - Rectangle { - color: "transparent" - width: control.width - height: 20 * (hasModelChildren ? 5 : 1) - property bool expanded: false - property bool populated: false - property bool isSelected: selection.selectedIndexes.includes(treeModel.modelIndex(index, treeModel.rootIndex)) - - - ColumnLayout { - anchors.fill: parent - RowLayout { - Layout.fillHeight: false - Layout.fillWidth: false - Layout.preferredHeight: 20 - - XsButton { - Layout.fillHeight: false - Layout.fillWidth: false - Layout.preferredWidth: parent.height - Layout.preferredHeight: parent.height - - text: "" - imgSrc: "qrc:/feather_icons/chevron-down.svg" - visible: hasModelChildren - image.sourceSize.width: height/1.3 - image.sourceSize.height: height/1.3 - borderColorNormal: Qt.lighter(palette.base, 0.3) - // isActive: isExpanded - - scale: rotation==0 || rotation==-90?1:0.85 - rotation: expanded ? 0: -90 - Behavior on rotation {NumberAnimation{id: rotationAnim; duration: 150 }} - - onClicked: { - if(!expanded) { - expanded = true - if(!populated) { - createTreeNode(children, treeModel.model, treeModel.modelIndex(index, treeModel.rootIndex), selection) - populated = true - } - } else { - expanded = false - } - } - } - - Label { - text: display - Layout.fillHeight: false - Layout.fillWidth: false - } - Label { - text: datatypeRole ? datatypeRole : "" - Layout.fillHeight: false - Layout.fillWidth: false - } - Label { - text: datatypeRole != "group" && contextRole ? contextRole : "" - Layout.fillHeight: false - Layout.fillWidth: false - } - XsButton { - Layout.fillHeight: false - Layout.fillWidth: false - Layout.preferredWidth: parent.height - Layout.preferredHeight: parent.height - - text: isSelected ? "S" : "s" - borderColorNormal: Qt.lighter(palette.base, 0.3) - - onClicked: { - selection.select(treeModel.modelIndex(index, treeModel.rootIndex), ItemSelectionModel.Toggle) - } - } - XsButton { - Layout.fillHeight: false - Layout.fillWidth: false - Layout.preferredWidth: parent.height - Layout.preferredHeight: parent.height - - text: "R" - borderColorNormal: Qt.lighter(palette.base, 0.3) - - onClicked: { - console.log(treeModel.model.removeRows(index, 1, treeModel.rootIndex)) - } - } - XsButton { - Layout.fillHeight: false - Layout.fillWidth: false - Layout.preferredWidth: parent.height - Layout.preferredHeight: parent.height - - text: "I" - borderColorNormal: Qt.lighter(palette.base, 0.3) - - onClicked: { - console.log(treeModel.model.insertRows(index, 1, treeModel.rootIndex)) - } - } - XsButton { - Layout.fillHeight: false - Layout.fillWidth: false - Layout.preferredWidth: parent.height - Layout.preferredHeight: parent.height - - text: "U" - borderColorNormal: Qt.lighter(palette.base, 0.3) - - onClicked: { - console.log(treeModel.model.moveRows(treeModel.rootIndex, index, 1, treeModel.rootIndex, index-1)) - } - } - XsButton { - Layout.fillHeight: false - Layout.fillWidth: false - Layout.preferredWidth: parent.height - Layout.preferredHeight: parent.height - - text: "D" - borderColorNormal: Qt.lighter(palette.base, 0.3) - - onClicked: { - console.log(treeModel.model.moveRows(treeModel.rootIndex, index+1, 1, treeModel.rootIndex, index)) - } - } - XsButton { - Layout.fillHeight: false - Layout.fillWidth: false - Layout.preferredWidth: parent.height - Layout.preferredHeight: parent.height - - text: "P" - borderColorNormal: Qt.lighter(palette.base, 0.3) - - onClicked: { - console.log(treeModel.model.moveRows(treeModel.rootIndex, index, 1, treeModel.parentModelIndex(), 0)) - } - } - } - - Item { - id: children - - visible: expanded - Layout.fillHeight: true - Layout.fillWidth: true - Layout.minimumHeight: 80 - } - } - } - } - } -} - - -// hasModelChildren role property to determine whether a node has child nodes. -// DelegateModel::rootIndex allows the root node to be specified -// DelegateModel::modelIndex() returns a QModelIndex which can be assigned to DelegateModel::rootIndex -// DelegateModel::parentModelIndex() returns a QModelIndex which can be assigned to DelegateModel::rootIndex diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/TreeTest.qml b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/TreeTest.qml deleted file mode 100644 index 5b12659f5..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/TreeTest.qml +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.12 -import QtQuick.Layouts 1.3 -import QtQml.Models 2.14 -import xStudio 1.1 - - -XsDialog { - id: dlg - title: "test" - - width: 600 - height: 600 - - property var model: null - - ItemSelectionModel { - id: selectionModel - model: dlg.model - } - - TreeNode { - id: tree_node - anchors.fill: parent - model: dlg.model - rootIndex: null - selection: selectionModel - } -} - diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/key.svg b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/key.svg deleted file mode 100644 index e778e74eb..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/key.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/more-horizontal.svg b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/more-horizontal.svg deleted file mode 100644 index dc6a85564..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/more-horizontal.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/qmldir b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/qmldir deleted file mode 100644 index fa6cd873b..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/qmldir +++ /dev/null @@ -1,32 +0,0 @@ -module Shotgun - - -DelegateChoiceEdit 1.0 DelegateChoiceEdit.qml -DelegateChoiceNote 1.0 DelegateChoiceNote.qml -DelegateChoicePlaylist 1.0 DelegateChoicePlaylist.qml -DelegateChoiceShot 1.0 DelegateChoiceShot.qml -DelegateChoiceReference 1.0 DelegateChoiceReference.qml -LeftPresetView 1.0 LeftPresetView.qml -LeftTreeView 1.0 LeftTreeView.qml -QueryListView 1.0 QueryListView.qml -SBLeftPanel 1.0 SBLeftPanel.qml -SBRightPanel 1.0 SBRightPanel.qml -ShotgunAuthenticate 1.0 ShotgunAuthenticate.qml -ShotgunBrowserDialog 1.0 ShotgunBrowserDialog.qml -ShotgunButton 1.0 ShotgunButton.qml -ShotgunCreatePlaylist 1.0 ShotgunCreatePlaylist.qml -ShotgunMenu 1.0 ShotgunMenu.qml -ShotgunPreferences 1.0 ShotgunPreferences.qml -ShotgunPublishNotes 1.0 ShotgunPublishNotes.qml -ShotgunRoot 1.0 ShotgunRoot.qml -ShotgunTagDialog 1.0 ShotgunTagDialog.qml -ShotgunUpdatePlaylist 1.0 ShotgunUpdatePlaylist.qml -ShotsTreeView 1.0 ShotsTreeView.qml -TreeDelegateModel 1.0 TreeDelegateModel.qml -TreeTest 1.0 TreeTest.qml -TreeNode 1.0 TreeNode.qml - -ShotgunHelpers 1.0 ShotgunHelpers.js - -plugin data_source_shotgun_ui -classname ShotgunDataSourceUI \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/rotate-cw.svg b/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/rotate-cw.svg deleted file mode 100644 index 83dca3514..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/rotate-cw.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_query_ui.cpp b/src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_query_ui.cpp deleted file mode 100644 index 09ead9d63..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_query_ui.cpp +++ /dev/null @@ -1,1009 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "data_source_shotgun_ui.hpp" -#include "shotgun_model_ui.hpp" - -#include "../data_source_shotgun.hpp" -#include "../data_source_shotgun_definitions.hpp" -#include "../data_source_shotgun_query_engine.hpp" - -// #include "xstudio/utility/string_helpers.hpp" -// #include "xstudio/ui/qml/json_tree_model_ui.hpp" -// #include "xstudio/global_store/global_store.hpp" -// #include "xstudio/atoms.hpp" -// #include "xstudio/ui/qml/module_ui.hpp" -// #include "xstudio/utility/chrono.hpp" - -// #include -// #include -// #include - -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::shotgun_client; -using namespace xstudio::ui::qml; -using namespace xstudio::global_store; - - -void ShotgunDataSourceUI::updateQueryValueCache( - const std::string &type, const utility::JsonStore &data, const int project_id) { - std::map cache; - - auto _type = type; - if (project_id != -1) - _type += "-" + std::to_string(project_id); - - // load map.. - if (not data.is_null()) { - try { - for (const auto &i : data) { - if (i.count("name")) - cache[i.at("name").get()] = i.at("id"); - else if (i.at("attributes").count("name")) - cache[i.at("attributes").at("name").get()] = i.at("id"); - else if (i.at("attributes").count("code")) - cache[i.at("attributes").at("code").get()] = i.at("id"); - } - } catch (...) { - } - - // add reverse map - try { - for (const auto &i : data) { - if (i.count("name")) - cache[i.at("id").get()] = i.at("name"); - else if (i.at("attributes").count("name")) - cache[i.at("id").get()] = i.at("attributes").at("name"); - else if (i.at("attributes").count("code")) - cache[i.at("id").get()] = i.at("attributes").at("code"); - } - } catch (...) { - } - } - - query_value_cache_[_type] = cache; -} - -utility::JsonStore ShotgunDataSourceUI::getQueryValue( - const std::string &type, const utility::JsonStore &value, const int project_id) const { - // look for map - auto _type = type; - auto mapped_value = utility::JsonStore(); - - if (_type == "Author" || _type == "Recipient") - _type = "User"; - - if (project_id != -1) - _type += "-" + std::to_string(project_id); - - try { - auto val = value.get(); - if (query_value_cache_.count(_type)) { - if (query_value_cache_.at(_type).count(val)) { - mapped_value = query_value_cache_.at(_type).at(val); - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {} {} {}", _type, __PRETTY_FUNCTION__, err.what(), value.dump(2)); - } - - if (mapped_value.is_null()) - throw XStudioError("Invalid term value " + value.dump()); - - return mapped_value; -} - - -// merge global filters with Preset. -// Not sure if this should really happen here.. -// DST = PRESET src == Global - -QVariant ShotgunDataSourceUI::mergeQueries( - const QVariant &dst, const QVariant &src, const bool ignore_duplicates) const { - - - JsonStore dst_qry; - JsonStore src_qry; - - try { - if (std::string(dst.typeName()) == "QJSValue") { - dst_qry = nlohmann::json::parse( - QJsonDocument::fromVariant(dst.value().toVariant()) - .toJson(QJsonDocument::Compact) - .constData()); - } else { - dst_qry = nlohmann::json::parse( - QJsonDocument::fromVariant(dst).toJson(QJsonDocument::Compact).constData()); - } - - if (std::string(src.typeName()) == "QJSValue") { - src_qry = nlohmann::json::parse( - QJsonDocument::fromVariant(src.value().toVariant()) - .toJson(QJsonDocument::Compact) - .constData()); - } else { - src_qry = nlohmann::json::parse( - QJsonDocument::fromVariant(src).toJson(QJsonDocument::Compact).constData()); - } - - auto merged = QueryEngine::merge_query( - dst_qry["queries"], src_qry.at("queries"), ignore_duplicates); - dst_qry["queries"] = merged; - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return QVariantMapFromJson(dst_qry); -} - -QFuture ShotgunDataSourceUI::executeQuery( - const QString &context, - const int project_id, - const QVariant &query, - const bool update_result_model) { - // build and dispatch query, we then pass result via message back to ourself. - - // executeQueryNew(context, project_id, query, update_result_model); - - auto cxt = StdFromQString(context); - JsonStore qry; - - try { - qry = JsonStore(nlohmann::json::parse( - QJsonDocument::fromVariant(query.value().toVariant()) - .toJson(QJsonDocument::Compact) - .constData())); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return QtConcurrent::run([=]() { - if (backend_ and not qry.is_null()) { - scoped_actor sys{system()}; - - auto request = JsonStore(GetQueryResult); - - request["context"] = R"({ - "type": null, - "epoc": null, - "audio_source": "", - "visual_source": "", - "flag_text": "", - "flag_colour": "", - "truncated": false - })"_json; - - request["context"]["epoc"] = utility::to_epoc_milliseconds(utility::clock::now()); - - if (cxt == "Playlists") { - request["context"]["type"] = "playlist_result"; - request["entity"] = "Playlists"; - request["fields"] = PlaylistFields; - } else if (cxt == "Versions") { - request["context"]["type"] = "shot_result"; - request["entity"] = "Versions"; - request["fields"] = VersionFields; - } else if (cxt == "Reference") { - request["context"]["type"] = "reference_result"; - request["entity"] = "Versions"; - request["fields"] = VersionFields; - } else if (cxt == "Versions Tree") { - request["context"]["type"] = "shot_tree_result"; - request["entity"] = "Versions"; - request["fields"] = VersionFields; - } else if (cxt == "Menu Setup") { - request["context"]["type"] = "media_action_result"; - request["entity"] = "Versions"; - request["fields"] = VersionFields; - } else if (cxt == "Notes") { - request["context"]["type"] = "note_result"; - request["entity"] = "Notes"; - request["fields"] = NoteFields; - } else if (cxt == "Notes Tree") { - request["context"]["type"] = "note_tree_result"; - request["entity"] = "Notes"; - request["fields"] = NoteFields; - } - - try { - const auto &[filter, orderby, max_count, source_selection, flag_selection] = - buildQuery(cxt, project_id, qry); - request["max_result"] = max_count; - request["order"] = orderby; - request["query"] = filter; - - - request["context"]["visual_source"] = source_selection.first; - request["context"]["audio_source"] = source_selection.second; - request["context"]["flag_text"] = flag_selection.first; - request["context"]["flag_colour"] = flag_selection.second; - - auto data = request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, get_data_atom_v, request); - - if (data.at("result").at("data").is_array()) - data["context"]["truncated"] = - (static_cast(data.at("result").at("data").size()) == max_count); - - if (update_result_model) - anon_send(as_actor(), shotgun_info_atom_v, data); - - return QStringFromStd(data.dump()); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - // silence error.. - if (update_result_model) - anon_send(as_actor(), shotgun_info_atom_v, request); - - if (starts_with(std::string(err.what()), "LiveLink ")) { - return QStringFromStd(request.dump()); // R"({"data":[]})"); - } - - return QStringFromStd(err.what()); - } - } - return QString(); - }); -} - -QFuture ShotgunDataSourceUI::executeQueryNew( - const QString &context, - const int project_id, - const QVariant &query, - const bool update_result_model) { - // build and dispatch query, we then pass result via message back to ourself. - JsonStore qry; - - try { - qry = JsonStore(nlohmann::json::parse( - QJsonDocument::fromVariant(query.value().toVariant()) - .toJson(QJsonDocument::Compact) - .constData())); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return QtConcurrent::run([=]() { - if (backend_ and not qry.is_null()) { - scoped_actor sys{system()}; - - std::string entity; - auto query_context = R"({ - "type": null, - "epoc": null, - "audio_source": "", - "visual_source": "", - "flag_text": "", - "flag_colour": "", - "truncated": false - })"_json; - - query_context["epoc"] = utility::to_epoc_milliseconds(utility::clock::now()); - - if (context == "Playlists") { - query_context["type"] = "playlist_result"; - entity = "Playlists"; - } else if (context == "Versions") { - query_context["type"] = "shot_result"; - entity = "Versions"; - } else if (context == "Reference") { - query_context["type"] = "reference_result"; - entity = "Versions"; - } else if (context == "Versions Tree") { - query_context["type"] = "shot_tree_result"; - entity = "Versions"; - } else if (context == "Menu Setup") { - query_context["type"] = "media_action_result"; - entity = "Versions"; - } else if (context == "Notes") { - query_context["type"] = "note_result"; - entity = "Notes"; - } else if (context == "Notes Tree") { - query_context["type"] = "note_tree_result"; - entity = "Notes"; - } - - - try { - auto request = QueryEngine::build_query( - project_id, entity, R"([])"_json, qry, query_context, utility::JsonStore()); - - try { - - spdlog::warn("{}", request.dump(2)); - - // const auto &[filter, orderby, max_count, source_selection, - // flag_selection] = - // buildQuery(cxt, project_id, qry); - - // request["max_result"] = max_count; - // request["order"] = orderby; - // request["query"] = filter; - - // request["context"]["visual_source"] = source_selection.first; - // request["context"]["audio_source"] = source_selection.second; - // request["context"]["flag_text"] = flag_selection.first; - // request["context"]["flag_colour"] = flag_selection.second; - - return QString(); - - auto data = request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, get_data_atom_v, request); - - if (data.at("result").at("data").is_array()) - data["context"]["truncated"] = - (static_cast(data.at("result").at("data").size()) == - data.at("result").at("max_result")); - - if (update_result_model) - anon_send(as_actor(), shotgun_info_atom_v, data); - - return QStringFromStd(data.dump()); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - // silence error.. - if (update_result_model) - anon_send(as_actor(), shotgun_info_atom_v, request); - - if (starts_with(std::string(err.what()), "LiveLink ")) { - return QStringFromStd(request.dump()); // R"({"data":[]})"); - } - - return QStringFromStd(err.what()); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - return QString(); - }); -} - - -Text ShotgunDataSourceUI::addTextValue( - const std::string &filter, const std::string &value, const bool negated) const { - if (starts_with(value, "^") and ends_with(value, "$")) { - if (negated) - return Text(filter).is_not(value.substr(0, value.size() - 1).substr(1)); - - return Text(filter).is(value.substr(0, value.size() - 1).substr(1)); - } else if (ends_with(value, "$")) { - return Text(filter).ends_with(value.substr(0, value.size() - 1)); - } else if (starts_with(value, "^")) { - return Text(filter).starts_with(value.substr(1)); - } - if (negated) - return Text(filter).not_contains(value); - - return Text(filter).contains(value); -} - -void ShotgunDataSourceUI::addTerm( - const int project_id, const std::string &context, FilterBy *qry, const JsonStore &term) { - // qry->push_back(Text("versions").is_not_null()); - auto trm = term.at("term").get(); - auto val = term.at("value").get(); - auto live = term.value("livelink", false); - auto negated = term.value("negated", false); - - - // kill queries with invalid shot live link. - if (val.empty() and live and trm == "Shot") { - auto rel = R"({"type": "Shot", "id":0})"_json; - qry->push_back(RelationType("entity").is(JsonStore(rel))); - } - - if (val.empty()) { - throw XStudioError("Empty query value " + trm); - } - - if (context == "Playlists") { - if (trm == "Lookback") { - if (val == "Today") - qry->push_back(DateTime("updated_at").in_calendar_day(0)); - else if (val == "1 Day") - qry->push_back(DateTime("updated_at").in_last(1, Period::DAY)); - else if (val == "3 Days") - qry->push_back(DateTime("updated_at").in_last(3, Period::DAY)); - else if (val == "7 Days") - qry->push_back(DateTime("updated_at").in_last(7, Period::DAY)); - else if (val == "20 Days") - qry->push_back(DateTime("updated_at").in_last(20, Period::DAY)); - else if (val == "30 Days") - qry->push_back(DateTime("updated_at").in_last(30, Period::DAY)); - else if (val == "30-60 Days") { - qry->push_back(DateTime("updated_at").not_in_last(30, Period::DAY)); - qry->push_back(DateTime("updated_at").in_last(60, Period::DAY)); - } else if (val == "60-90 Days") { - qry->push_back(DateTime("updated_at").not_in_last(60, Period::DAY)); - qry->push_back(DateTime("updated_at").in_last(90, Period::DAY)); - } else if (val == "100-150 Days") { - qry->push_back(DateTime("updated_at").not_in_last(100, Period::DAY)); - qry->push_back(DateTime("updated_at").in_last(150, Period::DAY)); - } else if (val == "Future Only") { - qry->push_back(DateTime("sg_date_and_time").in_next(30, Period::DAY)); - } else { - throw XStudioError("Invalid query term " + trm + " " + val); - } - } else if (trm == "Playlist Type") { - if (negated) - qry->push_back(Text("sg_type").is_not(val)); - else - qry->push_back(Text("sg_type").is(val)); - } else if (trm == "Has Contents") { - if (val == "False") - qry->push_back(Text("versions").is_null()); - else if (val == "True") - qry->push_back(Text("versions").is_not_null()); - else - throw XStudioError("Invalid query term " + trm + " " + val); - } else if (trm == "Site") { - if (negated) - qry->push_back(Text("sg_location").is_not(val)); - else - qry->push_back(Text("sg_location").is(val)); - } else if (trm == "Review Location") { - if (negated) - qry->push_back(Text("sg_review_location_1").is_not(val)); - else - qry->push_back(Text("sg_review_location_1").is(val)); - } else if (trm == "Department") { - if (negated) - qry->push_back(Number("sg_department_unit.Department.id") - .is_not(getQueryValue(trm, JsonStore(val)).get())); - else - qry->push_back(Number("sg_department_unit.Department.id") - .is(getQueryValue(trm, JsonStore(val)).get())); - } else if (trm == "Author") { - qry->push_back(Number("created_by.HumanUser.id") - .is(getQueryValue(trm, JsonStore(val)).get())); - } else if (trm == "Filter") { - qry->push_back(addTextValue("code", val, negated)); - } else if (trm == "Tag") { - qry->push_back(addTextValue("tags.Tag.name", val, negated)); - } else if (trm == "Has Notes") { - if (val == "False") - qry->push_back(Text("notes").is_null()); - else if (val == "True") - qry->push_back(Text("notes").is_not_null()); - else - throw XStudioError("Invalid query term " + trm + " " + val); - } else if (trm == "Unit") { - auto tmp = R"({"type": "CustomEntity24", "id":0})"_json; - tmp["id"] = getQueryValue(trm, JsonStore(val), project_id).get(); - if (negated) - qry->push_back(RelationType("sg_unit2").in({JsonStore(tmp)})); - else - qry->push_back(RelationType("sg_unit2").not_in({JsonStore(tmp)})); - } - - } else if (context == "Notes" || context == "Notes Tree") { - if (trm == "Lookback") { - if (val == "Today") - qry->push_back(DateTime("created_at").in_calendar_day(0)); - else if (val == "1 Day") - qry->push_back(DateTime("created_at").in_last(1, Period::DAY)); - else if (val == "3 Days") - qry->push_back(DateTime("created_at").in_last(3, Period::DAY)); - else if (val == "7 Days") - qry->push_back(DateTime("created_at").in_last(7, Period::DAY)); - else if (val == "20 Days") - qry->push_back(DateTime("created_at").in_last(20, Period::DAY)); - else if (val == "30 Days") - qry->push_back(DateTime("created_at").in_last(30, Period::DAY)); - else if (val == "30-60 Days") { - qry->push_back(DateTime("created_at").not_in_last(30, Period::DAY)); - qry->push_back(DateTime("created_at").in_last(60, Period::DAY)); - } else if (val == "60-90 Days") { - qry->push_back(DateTime("created_at").not_in_last(60, Period::DAY)); - qry->push_back(DateTime("created_at").in_last(90, Period::DAY)); - } else if (val == "100-150 Days") { - qry->push_back(DateTime("created_at").not_in_last(100, Period::DAY)); - qry->push_back(DateTime("created_at").in_last(150, Period::DAY)); - } else - throw XStudioError("Invalid query term " + trm + " " + val); - } else if (trm == "Filter") { - qry->push_back(addTextValue("subject", val, negated)); - } else if (trm == "Note Type") { - if (negated) - qry->push_back(Text("sg_note_type").is_not(val)); - else - qry->push_back(Text("sg_note_type").is(val)); - } else if (trm == "Author") { - qry->push_back(Number("created_by.HumanUser.id") - .is(getQueryValue(trm, JsonStore(val)).get())); - } else if (trm == "Recipient") { - auto tmp = R"({"type": "HumanUser", "id":0})"_json; - tmp["id"] = getQueryValue(trm, JsonStore(val)).get(); - qry->push_back(RelationType("addressings_to").in({JsonStore(tmp)})); - } else if (trm == "Shot") { - auto tmp = R"({"type": "Shot", "id":0})"_json; - tmp["id"] = getQueryValue(trm, JsonStore(val), project_id).get(); - qry->push_back(RelationType("note_links").in({JsonStore(tmp)})); - } else if (trm == "Sequence") { - try { - if (sequences_map_.count(project_id)) { - auto row = sequences_map_[project_id]->search( - QVariant::fromValue(QStringFromStd(val)), QStringFromStd("display"), 0); - if (row != -1) { - auto rel = std::vector(); - // auto sht = R"({"type": "Shot", "id":0})"_json; - // auto shots = sequences_map_[project_id] - // ->modelData() - // .at(row) - // .at("relationships") - // .at("shots") - // .at("data"); - - // for (const auto &i : shots) { - // sht["id"] = i.at("id").get(); - // rel.emplace_back(sht); - // } - auto seq = R"({"type": "Sequence", "id":0})"_json; - seq["id"] = - sequences_map_[project_id]->modelData().at(row).at("id").get(); - rel.emplace_back(seq); - - qry->push_back(RelationType("note_links").in(rel)); - } else - throw XStudioError("Invalid query term " + trm + " " + val); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - throw XStudioError("Invalid query term " + trm + " " + val); - } - } else if (trm == "Playlist") { - auto tmp = R"({"type": "Playlist", "id":0})"_json; - tmp["id"] = getQueryValue(trm, JsonStore(val), project_id).get(); - qry->push_back(RelationType("note_links").in({JsonStore(tmp)})); - } else if (trm == "Version Name") { - qry->push_back(addTextValue("note_links.Version.code", val, negated)); - } else if (trm == "Tag") { - qry->push_back(addTextValue("tags.Tag.name", val, negated)); - } else if (trm == "Twig Type") { - if (negated) - qry->push_back( - Text("note_links.Version.sg_twig_type_code") - .is_not( - getQueryValue("TwigTypeCode", JsonStore(val)).get())); - else - qry->push_back( - Text("note_links.Version.sg_twig_type_code") - .is(getQueryValue("TwigTypeCode", JsonStore(val)).get())); - } else if (trm == "Twig Name") { - qry->push_back(addTextValue("note_links.Version.sg_twig_name", val, negated)); - } else if (trm == "Client Note") { - if (val == "False") - qry->push_back(Checkbox("client_note").is(false)); - else if (val == "True") - qry->push_back(Checkbox("client_note").is(true)); - else - throw XStudioError("Invalid query term " + trm + " " + val); - - } else if (trm == "Pipeline Step") { - if (negated) { - if (val == "None") - qry->push_back(Text("sg_pipeline_step").is_not_null()); - else - qry->push_back(Text("sg_pipeline_step").is_not(val)); - } else { - if (val == "None") - qry->push_back(Text("sg_pipeline_step").is_null()); - else - qry->push_back(Text("sg_pipeline_step").is(val)); - } - } else if (trm == "Older Version") { - qry->push_back( - Number("note_links.Version.sg_dneg_version").less_than(std::stoi(val))); - } else if (trm == "Newer Version") { - qry->push_back( - Number("note_links.Version.sg_dneg_version").greater_than(std::stoi(val))); - } - - } else if ( - context == "Versions" or context == "Reference" or context == "Versions Tree" or - context == "Menu Setup") { - if (trm == "Lookback") { - if (val == "Today") - qry->push_back(DateTime("created_at").in_calendar_day(0)); - else if (val == "1 Day") - qry->push_back(DateTime("created_at").in_last(1, Period::DAY)); - else if (val == "3 Days") - qry->push_back(DateTime("created_at").in_last(3, Period::DAY)); - else if (val == "7 Days") - qry->push_back(DateTime("created_at").in_last(7, Period::DAY)); - else if (val == "20 Days") - qry->push_back(DateTime("created_at").in_last(20, Period::DAY)); - else if (val == "30 Days") - qry->push_back(DateTime("created_at").in_last(30, Period::DAY)); - else if (val == "30-60 Days") { - qry->push_back(DateTime("created_at").not_in_last(30, Period::DAY)); - qry->push_back(DateTime("created_at").in_last(60, Period::DAY)); - } else if (val == "60-90 Days") { - qry->push_back(DateTime("created_at").not_in_last(60, Period::DAY)); - qry->push_back(DateTime("created_at").in_last(90, Period::DAY)); - } else if (val == "100-150 Days") { - qry->push_back(DateTime("created_at").not_in_last(100, Period::DAY)); - qry->push_back(DateTime("created_at").in_last(150, Period::DAY)); - } else - throw XStudioError("Invalid query term " + trm + " " + val); - } else if (trm == "Playlist") { - auto tmp = R"({"type": "Playlist", "id":0})"_json; - tmp["id"] = getQueryValue(trm, JsonStore(val), project_id).get(); - qry->push_back(RelationType("playlists").in({JsonStore(tmp)})); - } else if (trm == "Author") { - qry->push_back(Number("created_by.HumanUser.id") - .is(getQueryValue(trm, JsonStore(val)).get())); - } else if (trm == "Older Version") { - qry->push_back(Number("sg_dneg_version").less_than(std::stoi(val))); - } else if (trm == "Newer Version") { - qry->push_back(Number("sg_dneg_version").greater_than(std::stoi(val))); - } else if (trm == "Site") { - if (negated) - qry->push_back(Text("sg_location").is_not(val)); - else - qry->push_back(Text("sg_location").is(val)); - } else if (trm == "On Disk") { - std::string prop = std::string("sg_on_disk_") + val; - if (negated) - qry->push_back(Text(prop).is("None")); - else - qry->push_back(FilterBy().Or(Text(prop).is("Full"), Text(prop).is("Partial"))); - } else if (trm == "Pipeline Step") { - if (negated) { - if (val == "None") - qry->push_back(Text("sg_pipeline_step").is_not_null()); - else - qry->push_back(Text("sg_pipeline_step").is_not(val)); - } else { - if (val == "None") - qry->push_back(Text("sg_pipeline_step").is_null()); - else - qry->push_back(Text("sg_pipeline_step").is(val)); - } - } else if (trm == "Pipeline Status") { - if (negated) - qry->push_back( - Text("sg_status_list") - .is_not(getQueryValue(trm, JsonStore(val)).get())); - else - qry->push_back(Text("sg_status_list") - .is(getQueryValue(trm, JsonStore(val)).get())); - } else if (trm == "Production Status") { - if (negated) - qry->push_back( - Text("sg_production_status") - .is_not(getQueryValue(trm, JsonStore(val)).get())); - else - qry->push_back(Text("sg_production_status") - .is(getQueryValue(trm, JsonStore(val)).get())); - } else if (trm == "Shot Status") { - if (negated) - qry->push_back( - Text("entity.Shot.sg_status_list") - .is_not(getQueryValue(trm, JsonStore(val)).get())); - else - qry->push_back(Text("entity.Shot.sg_status_list") - .is(getQueryValue(trm, JsonStore(val)).get())); - } else if (trm == "Exclude Shot Status") { - qry->push_back(Text("entity.Shot.sg_status_list") - .is_not(getQueryValue(trm, JsonStore(val)).get())); - } else if (trm == "Latest Version") { - if (val == "False") - qry->push_back(Text("sg_latest").is_null()); - else if (val == "True") - qry->push_back(Text("sg_latest").is("Yes")); - else - throw XStudioError("Invalid query term " + trm + " " + val); - } else if (trm == "Is Hero") { - if (val == "False") - qry->push_back(Checkbox("sg_is_hero").is(false)); - else if (val == "True") - qry->push_back(Checkbox("sg_is_hero").is(true)); - else - throw XStudioError("Invalid query term " + trm + " " + val); - } else if (trm == "Shot") { - auto rel = R"({"type": "Shot", "id":0})"_json; - rel["id"] = getQueryValue(trm, JsonStore(val), project_id).get(); - qry->push_back(RelationType("entity").is(JsonStore(rel))); - } else if (trm == "Sequence") { - try { - if (sequences_map_.count(project_id)) { - auto row = sequences_map_[project_id]->search( - QVariant::fromValue(QStringFromStd(val)), QStringFromStd("display"), 0); - if (row != -1) { - auto rel = std::vector(); - // auto sht = R"({"type": "Shot", "id":0})"_json; - // auto shots = sequences_map_[project_id] - // ->modelData() - // .at(row) - // .at("relationships") - // .at("shots") - // .at("data"); - - // for (const auto &i : shots) { - // sht["id"] = i.at("id").get(); - // rel.emplace_back(sht); - // } - auto seq = R"({"type": "Sequence", "id":0})"_json; - seq["id"] = - sequences_map_[project_id]->modelData().at(row).at("id").get(); - rel.emplace_back(seq); - - qry->push_back(RelationType("entity").in(rel)); - } else - throw XStudioError("Invalid query term " + trm + " " + val); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - throw XStudioError("Invalid query term " + trm + " " + val); - } - } else if (trm == "Sent To Client") { - if (val == "False") - qry->push_back(DateTime("sg_date_submitted_to_client").is_null()); - else if (val == "True") - qry->push_back(DateTime("sg_date_submitted_to_client").is_not_null()); - else - throw XStudioError("Invalid query term " + trm + " " + val); - - - } else if (trm == "Sent To Dailies") { - if (val == "False") - qry->push_back(FilterBy().And( - DateTime("sg_submit_dailies").is_null(), - DateTime("sg_submit_dailies_chn").is_null(), - DateTime("sg_submit_dailies_mtl").is_null(), - DateTime("sg_submit_dailies_van").is_null(), - DateTime("sg_submit_dailies_mum").is_null())); - else if (val == "True") - qry->push_back(FilterBy().Or( - DateTime("sg_submit_dailies").is_not_null(), - DateTime("sg_submit_dailies_chn").is_not_null(), - DateTime("sg_submit_dailies_mtl").is_not_null(), - DateTime("sg_submit_dailies_van").is_not_null(), - DateTime("sg_submit_dailies_mum").is_not_null())); - else - throw XStudioError("Invalid query term " + trm + " " + val); - } else if (trm == "Has Notes") { - if (val == "False") - qry->push_back(Text("notes").is_null()); - else if (val == "True") - qry->push_back(Text("notes").is_not_null()); - else - throw XStudioError("Invalid query term " + trm + " " + val); - } else if (trm == "Filter") { - qry->push_back(addTextValue("code", val, negated)); - } else if (trm == "Tag") { - qry->push_back(addTextValue("entity.Shot.tags.Tag.name", val, negated)); - } else if (trm == "Reference Tag" or trm == "Reference Tags") { - - if (val.find(',') != std::string::npos) { - // split ... - for (const auto &i : split(val, ',')) { - if (negated) - qry->push_back( - RelationType("tags").name_not_contains(i + ".REFERENCE")); - else - qry->push_back(RelationType("tags").name_is(i + ".REFERENCE")); - } - } else { - if (negated) - qry->push_back(RelationType("tags").name_not_contains(val + ".REFERENCE")); - else - qry->push_back(RelationType("tags").name_is(val + ".REFERENCE")); - } - } else if (trm == "Tag (Version)") { - qry->push_back(addTextValue("tags.Tag.name", val, negated)); - } else if (trm == "Twig Name") { - qry->push_back(addTextValue("sg_twig_name", val, negated)); - } else if (trm == "Twig Type") { - if (negated) - qry->push_back( - Text("sg_twig_type_code") - .is_not( - getQueryValue("TwigTypeCode", JsonStore(val)).get())); - else - qry->push_back( - Text("sg_twig_type_code") - .is(getQueryValue("TwigTypeCode", JsonStore(val)).get())); - } else if (trm == "Completion Location") { - auto rel = R"({"type": "CustomNonProjectEntity16", "id":0})"_json; - rel["id"] = getQueryValue(trm, JsonStore(val)).get(); - if (negated) - qry->push_back(RelationType("entity.Shot.sg_primary_shot_location") - .is_not(JsonStore(rel))); - else - qry->push_back( - RelationType("entity.Shot.sg_primary_shot_location").is(JsonStore(rel))); - - } else { - spdlog::warn("{} Unhandled {} {}", __PRETTY_FUNCTION__, trm, val); - } - } -} - - -std::tuple< - utility::JsonStore, - std::vector, - int, - std::pair, - std::pair> -ShotgunDataSourceUI::buildQuery( - const std::string &context, const int project_id, const utility::JsonStore &query) { - - int max_count = maximum_result_count_; - std::vector order_by; - std::pair source_selection; - std::pair flag_selection; - - FilterBy qry; - try { - - std::multimap qry_terms; - - // collect terms in map - for (const auto &i : query.at("queries")) { - if (i.at("enabled").get()) { - // filter out order by and max count.. - if (i.at("term") == "Disable Global") { - // filtered out - } else if (i.at("term") == "Result Limit") { - max_count = std::stoi(i.at("value").get()); - } else if (i.at("term") == "Preferred Visual") { - source_selection.first = i.at("value").get(); - } else if (i.at("term") == "Preferred Audio") { - source_selection.second = i.at("value").get(); - } else if (i.at("term") == "Flag Media") { - flag_selection.first = i.at("value").get(); - if (flag_selection.first == "Red") - flag_selection.second = "#FFFF0000"; - else if (flag_selection.first == "Green") - flag_selection.second = "#FF00FF00"; - else if (flag_selection.first == "Blue") - flag_selection.second = "#FF0000FF"; - else if (flag_selection.first == "Yellow") - flag_selection.second = "#FFFFFF00"; - else if (flag_selection.first == "Orange") - flag_selection.second = "#FFFFA500"; - else if (flag_selection.first == "Purple") - flag_selection.second = "#FF800080"; - else if (flag_selection.first == "Black") - flag_selection.second = "#FF000000"; - else if (flag_selection.first == "White") - flag_selection.second = "#FFFFFFFF"; - } else if (i.at("term") == "Order By") { - auto val = i.at("value").get(); - bool descending = false; - - if (ends_with(val, " ASC")) { - val = val.substr(0, val.size() - 4); - } else if (ends_with(val, " DESC")) { - val = val.substr(0, val.size() - 5); - descending = true; - } - - std::string field = ""; - // get sg term.. - if (context == "Playlists") { - if (val == "Date And Time") - field = "sg_date_and_time"; - else if (val == "Created") - field = "created_at"; - else if (val == "Updated") - field = "updated_at"; - } else if ( - context == "Versions" or context == "Versions Tree" or - context == "Reference" or context == "Menu Setup") { - if (val == "Date And Time") - field = "created_at"; - else if (val == "Created") - field = "created_at"; - else if (val == "Updated") - field = "updated_at"; - else if (val == "Client Submit") - field = "sg_date_submitted_to_client"; - else if (val == "Version") - field = "sg_dneg_version"; - } else if (context == "Notes" or context == "Notes Tree") { - if (val == "Created") - field = "created_at"; - else if (val == "Updated") - field = "updated_at"; - } - - if (not field.empty()) - order_by.push_back(descending ? "-" + field : field); - } else { - // add normal term to map. - qry_terms.insert(std::make_pair( - std::string(i.value("negated", false) ? "Not " : "") + - i.at("term").get(), - i)); - } - } - - // set defaults if not specified - if (source_selection.first.empty()) - source_selection.first = "SG Movie"; - if (source_selection.second.empty()) - source_selection.second = source_selection.first; - } - - // add terms we always want. - if (context == "Playlists") { - qry.push_back(Number("project.Project.id").is(project_id)); - } else if ( - context == "Versions" or context == "Versions Tree" or context == "Menu Setup") { - qry.push_back(Number("project.Project.id").is(project_id)); - qry.push_back(Text("sg_deleted").is_null()); - // qry.push_back(Entity("entity").type_is("Shot")); - qry.push_back(FilterBy().Or( - Text("sg_path_to_movie").is_not_null(), - Text("sg_path_to_frames").is_not_null())); - } else if (context == "Reference") { - qry.push_back(Number("project.Project.id").is(project_id)); - qry.push_back(Text("sg_deleted").is_null()); - qry.push_back(FilterBy().Or( - Text("sg_path_to_movie").is_not_null(), - Text("sg_path_to_frames").is_not_null())); - // qry.push_back(Entity("entity").type_is("Asset")); - } else if (context == "Notes" or context == "Notes Tree") { - qry.push_back(Number("project.Project.id").is(project_id)); - } - - // create OR group for multiples of same term. - std::string key; - FilterBy *dest = &qry; - for (const auto &i : qry_terms) { - if (key != i.first) { - key = i.first; - // multiple identical terms OR / AND them.. - if (qry_terms.count(key) > 1) { - if (starts_with(key, "Not ") or starts_with(key, "Exclude ")) - qry.push_back(FilterBy(BoolOperator::AND)); - else - qry.push_back(FilterBy(BoolOperator::OR)); - dest = &std::get(qry.back()); - } else { - dest = &qry; - } - } - try { - addTerm(project_id, context, dest, i.second); - } catch (const std::exception &err) { - // spdlog::warn("{}", err.what()); - // bad term.. we ignore them.. - - // if(i.second.value("livelink", false)) - // throw XStudioError(std::string("LiveLink ") + err.what()); - - // throw; - } - } - } catch (const std::exception &err) { - throw; - } - - if (order_by.empty()) { - if (context == "Playlists") - order_by.emplace_back("-created_at"); - else if (context == "Versions" or context == "Versions Tree") - order_by.emplace_back("-created_at"); - else if (context == "Reference") - order_by.emplace_back("-created_at"); - else if (context == "Menu Setup") - order_by.emplace_back("-created_at"); - else if (context == "Notes" or context == "Notes Tree") - order_by.emplace_back("-created_at"); - } - - // spdlog::warn("{}", JsonStore(qry).dump(2)); - // spdlog::warn("{}", join_as_string(order_by,",")); - return std::make_tuple( - JsonStore(qry), order_by, max_count, source_selection, flag_selection); -} diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_requests_ui.cpp b/src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_requests_ui.cpp deleted file mode 100644 index 4f283dc28..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_requests_ui.cpp +++ /dev/null @@ -1,1012 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "data_source_shotgun_ui.hpp" -#include "shotgun_model_ui.hpp" - -#include "../data_source_shotgun.hpp" -#include "../data_source_shotgun_definitions.hpp" -#include "../data_source_shotgun_query_engine.hpp" - -#include "xstudio/atoms.hpp" - -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::shotgun_client; -using namespace xstudio::ui::qml; - -#define REQUEST_BEGIN() return QtConcurrent::run([=]() { \ - if (backend_) { \ - try { - -#define REQUEST_END() \ - } \ - catch (const XStudioError &err) { \ - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); \ - auto error = R"({'error':{})"_json; \ - error["error"]["source"] = to_string(err.type()); \ - error["error"]["message"] = err.what(); \ - return QStringFromStd(JsonStore(error).dump()); \ - } \ - catch (const std::exception &err) { \ - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); \ - return QStringFromStd(err.what()); \ - } \ - } \ - return QString(); \ - }); - - -QFuture ShotgunDataSourceUI::getProjectsFuture() { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - auto projects = request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, shotgun_projects_atom_v); - // send to self.. - - if (not projects.count("data")) - throw std::runtime_error(projects.dump(2)); - - anon_send( - as_actor(), shotgun_info_atom_v, JsonStore(R"({"type": "project"})"_json), projects); - - return QStringFromStd(projects.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::getSchemaFieldsFuture( - const QString &entity, const QString &field, const QString &update_name) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - auto data = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_schema_entity_fields_atom_v, - StdFromQString(entity), - StdFromQString(field), - -1); - - if (not update_name.isEmpty()) { - auto jsn = JsonStore(R"({"type": null})"_json); - jsn["type"] = StdFromQString(update_name); - anon_send(as_actor(), shotgun_info_atom_v, jsn, buildDataFromField(data)); - } - - return QStringFromStd(data.dump()); - - REQUEST_END() -} - -// get json of versions data from shotgun. -QFuture -ShotgunDataSourceUI::getVersionsFuture(const int project_id, const QVariant &qids) { - - std::vector ids; - for (const auto &i : qids.toList()) - ids.push_back(i.toInt()); - - // return QtConcurrent::run([=, project_id = project_id]() { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - auto filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["project", "is", {"type":"Project", "id":0}], - ["id", "in", []] - ] - })"_json; - - filter["conditions"][0][2]["id"] = project_id; - for (const auto i : ids) - filter["conditions"][1][2].push_back(i); - - // we've got more that 5000 employees.... - auto data = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "Versions", - JsonStore(filter), - VersionFields, - std::vector({"id"}), - 1, - 4999); - - if (not data.count("data")) - throw std::runtime_error(data.dump(2)); - - // reorder results based on request order.. - std::map result_items; - for (const auto &i : data["data"]) - result_items[i.at("id").get()] = i; - - data["data"].clear(); - for (const auto i : ids) { - if (result_items.count(i)) - data["data"].push_back(result_items[i]); - } - - return QStringFromStd(data.dump()); - - REQUEST_END() -} - - -QFuture ShotgunDataSourceUI::getPlaylistLinkMediaFuture(const QUuid &playlist) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - auto req = JsonStore(GetLinkMedia); - req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); - - return QStringFromStd( - request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::get_data_atom_v, req) - .dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::getPlaylistValidMediaCountFuture(const QUuid &playlist) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - auto req = JsonStore(GetValidMediaCount); - req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); - - return QStringFromStd( - request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::get_data_atom_v, req) - .dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::getGroupsFuture(const int project_id) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - auto groups = request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, shotgun_groups_atom_v, project_id); - - if (not groups.count("data")) - throw std::runtime_error(groups.dump(2)); - - auto request = R"({"type": "group", "id": 0})"_json; - request["id"] = project_id; - anon_send(as_actor(), shotgun_info_atom_v, JsonStore(request), groups); - - return QStringFromStd(groups.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::getSequencesFuture(const int project_id) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - - auto filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["project", "is", {"type":"Project", "id":0}], - ["sg_status_list", "not_in", ["na","del"]] - ] - })"_json; - - filter["conditions"][0][2]["id"] = project_id; - - auto delfilter = R"( - { - "logical_operator": "and", - "conditions": [ - ["project", "is", {"type":"Project", "id":0}], - ["sg_status_list", "in", ["na","del"]] - ] - })"_json; - - delfilter["conditions"][0][2]["id"] = project_id; - - // we've got more that 5000 employees.... - auto data = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "Sequences", - JsonStore(filter), - std::vector( - {"id", "code", "shots", "type", "sg_parent", "sg_sequence_type", "sg_status_list"}), - std::vector({"code"}), - 1, - 4999); - - if (not data.count("data")) - throw std::runtime_error(data.dump(2)); - - if (data.at("data").size() == 4999) - spdlog::warn("{} Sequence list truncated.", __PRETTY_FUNCTION__); - - // get deleted shots list.. - auto deldata = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "Shots", - JsonStore(delfilter), - std::vector({"id"}), - std::vector({"id"}), - 1, - 4999); - - if (not deldata.count("data")) - throw std::runtime_error(deldata.dump(2)); - - if (deldata.at("data").size() == 4999) - spdlog::warn("{} Shot list truncated.", __PRETTY_FUNCTION__); - - // build set of deleted shot id's - std::set del_shots; - for (const auto &i : deldata.at("data")) - del_shots.insert(i.at("id").get()); - - if (not del_shots.empty()) { - // iterate over sequence -> shots and remove deleted - for (auto &i : data["data"]) { - bool done = false; - auto &t = i["relationships"]["shots"]["data"]; - for (auto it = t.begin(); it != t.end();) { - if (del_shots.count(it->at("id"))) { - it = t.erase(it); - } else { - it++; - } - } - } - } - - auto request = R"({"type": "sequence", "id": 0})"_json; - request["id"] = project_id; - anon_send(as_actor(), shotgun_info_atom_v, JsonStore(request), data); - - return QStringFromStd(data.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::getPlaylistsFuture(const int project_id) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - - auto filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["project", "is", {"type":"Project", "id":0}], - ["versions", "is_not", null] - ] - })"_json; - // ["updated_at", "in_last", [7, "DAY"]] - - filter["conditions"][0][2]["id"] = project_id; - - // we've got more that 5000 employees.... - auto data = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "Playlists", - JsonStore(filter), - std::vector({"id", "code"}), - std::vector({"-created_at"}), - 1, - 4999); - - if (not data.count("data")) - throw std::runtime_error(data.dump(2)); - - auto request = R"({"type": "playlist", "id": 0})"_json; - request["id"] = project_id; - anon_send(as_actor(), shotgun_info_atom_v, JsonStore(request), data); - - return QStringFromStd(data.dump()); - - REQUEST_END() -} - - -QFuture ShotgunDataSourceUI::getShotsFuture(const int project_id) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - - auto filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["project", "is", {"type":"Project", "id":0}], - ["sg_status_list", "not_in", ["na", "del"]] - ] - })"_json; - - filter["conditions"][0][2]["id"] = project_id; - - // we've got more that 5000 employees.... - auto data = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "Shots", - JsonStore(filter), - ShotFields, - std::vector({"code"}), - 1, - 4999); - - if (not data.count("data")) - throw std::runtime_error(data.dump(2)); - - bool more_data = (data["data"].size() == 4999); - auto page = 2; - - while (more_data) { - more_data = false; - - auto data2 = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "Shots", - JsonStore(filter), - ShotFields, - std::vector({"code"}), - page, - 4999); - - if (data2["data"].size() == 4999) { - more_data = true; - page++; - } - - data["data"].insert(data["data"].end(), data2["data"].begin(), data2["data"].end()); - } - - // spdlog::warn("shot count {}", data["data"].size()); - - auto request = R"({"type": "shot", "id": 0})"_json; - request["id"] = project_id; - anon_send(as_actor(), shotgun_info_atom_v, JsonStore(request), data); - - return QStringFromStd(data.dump()); - - REQUEST_END() -} - - -QFuture ShotgunDataSourceUI::getUsersFuture() { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - - auto filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["sg_status_list", "is", "act"] - ] - })"_json; - - // we've got more that 5000 employees.... - auto data = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "HumanUsers", - JsonStore(filter), - std::vector({"name", "id", "login"}), - std::vector({"name"}), - 1, - 4999); - - if (not data.count("data")) - throw std::runtime_error(data.dump(2)); - - bool more_data = (data["data"].size() == 4999); - auto page = 2; - - while (more_data) { - more_data = false; - auto data2 = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "HumanUsers", - JsonStore(filter), - std::vector({"name", "id", "login"}), - std::vector({"name"}), - page, - 4999); - - if (data2["data"].size() == 4999) { - more_data = true; - page++; - } - - data["data"].insert(data["data"].end(), data2["data"].begin(), data2["data"].end()); - } - - // spdlog::warn("user count {}", data["data"].size()); - - anon_send(as_actor(), shotgun_info_atom_v, JsonStore(R"({"type": "user"})"_json), data); - - return QStringFromStd(data.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::getDepartmentsFuture() { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - - auto filter = R"( - { - "logical_operator": "and", - "conditions": [ - ] - })"_json; - // ["sg_status_list", "is", "act"] - - auto data = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "Departments", - JsonStore(filter), - std::vector({"name", "id"}), - std::vector({"name"}), - 1, - 4999); - - if (not data.count("data")) - throw std::runtime_error(data.dump(2)); - - anon_send( - as_actor(), shotgun_info_atom_v, JsonStore(R"({"type": "department"})"_json), data); - - return QStringFromStd(data.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::getReferenceTagsFuture() { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - - auto filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["name", "ends_with", ".REFERENCE"] - ] - })"_json; - - // we've got more that 5000 employees.... - auto data = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "Tags", - JsonStore(filter), - std::vector({"name", "id"}), - std::vector({"name"}), - 1, - 4999); - - if (not data.count("data")) - throw std::runtime_error(data.dump(2)); - - for (auto &i : data["data"]) { - auto str = i["attributes"]["name"].get(); - i["attributes"]["name"] = str.substr(0, str.size() - sizeof(".REFERENCE") + 1); - } - - anon_send( - as_actor(), shotgun_info_atom_v, JsonStore(R"({"type": "reference_tag"})"_json), data); - - return QStringFromStd(data.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::getCustomEntity24Future(const int project_id) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - - auto filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["project", "is", {"type":"Project", "id":0}] - ] - })"_json; - - filter["conditions"][0][2]["id"] = project_id; - - auto data = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "CustomEntity24", - JsonStore(filter), - std::vector({"code", "id"}), - std::vector({"code"}), - 1, - 4999); - - if (not data.count("data")) - throw std::runtime_error(data.dump(2)); - - auto request = R"({"type": "custom_entity_24", "id": 0})"_json; - request["id"] = project_id; - anon_send(as_actor(), shotgun_info_atom_v, JsonStore(request), data); - - return QStringFromStd(data.dump()); - - REQUEST_END() -} - - -QFuture ShotgunDataSourceUI::addVersionToPlaylistFuture( - const QString &version, const QUuid &playlist, const QUuid &before) { - - REQUEST_BEGIN() - - scoped_actor sys{system()}; - auto media = request_receive( - *sys, - backend_, - playlist::add_media_atom_v, - JsonStore(nlohmann::json::parse(StdFromQString(version))), - UuidFromQUuid(playlist), - caf::actor(), - UuidFromQUuid(before)); - auto result = nlohmann::json::array(); - // return uuids.. - for (const auto &i : media) { - result.push_back(i.uuid()); - } - - return QStringFromStd(JsonStore(result).dump()); - - REQUEST_END() -} - - -QFuture ShotgunDataSourceUI::updateEntityFuture( - const QString &entity, const int record_id, const QString &update_json) { - - REQUEST_BEGIN() - - scoped_actor sys{system()}; - - auto js = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_update_entity_atom_v, - StdFromQString(entity), - record_id, - utility::JsonStore(nlohmann::json::parse(StdFromQString(update_json)))); - return QStringFromStd(js.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::preparePlaylistNotesFuture( - const QUuid &playlist, - const QList &media, - const bool notify_owner, - const std::vector notify_group_ids, - const bool combine, - const bool add_time, - const bool add_playlist_name, - const bool add_type, - const bool anno_requires_note, - const bool skip_already_published, - const QString &defaultType) { - - return QtConcurrent::run([=]() { - if (backend_) { - try { - scoped_actor sys{system()}; - auto req = JsonStore(GetPrepareNotes); - - for (const auto &i : media) - req["media_uuids"].push_back(to_string(UuidFromQUuid(i))); - - req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); - req["notify_owner"] = notify_owner; - req["notify_group_ids"] = notify_group_ids; - req["combine"] = combine; - req["add_time"] = add_time; - req["add_playlist_name"] = add_playlist_name; - req["add_type"] = add_type; - req["anno_requires_note"] = anno_requires_note; - req["skip_already_published"] = skip_already_published; - req["default_type"] = StdFromQString(defaultType); - - auto js = request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::get_data_atom_v, req); - return QStringFromStd(js.dump()); - } catch (const XStudioError &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - auto error = R"({'error':{})"_json; - // error["error"]["source"] = to_string(err.type()); - // error["error"]["message"] = err.what(); - return QStringFromStd(JsonStore(error).dump()); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - return QStringFromStd(err.what()); - } - } - return QString(); - }); -} - - -QFuture -ShotgunDataSourceUI::pushPlaylistNotesFuture(const QString ¬es, const QUuid &playlist) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - auto req = JsonStore(PostCreateNotes); - req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); - req["payload"] = JsonStore(nlohmann::json::parse(StdFromQString(notes))["payload"]); - - auto js = request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::post_data_atom_v, req); - - return QStringFromStd(js.dump()); - - REQUEST_END() -} - - -QFuture ShotgunDataSourceUI::createPlaylistFuture( - const QUuid &playlist, - const int project_id, - const QString &name, - const QString &location, - const QString &playlist_type) { - - REQUEST_BEGIN() - - scoped_actor sys{system()}; - auto req = JsonStore(PostCreatePlaylist); - req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); - req["project_id"] = project_id; - req["code"] = StdFromQString(name); - req["location"] = StdFromQString(location); - req["playlist_type"] = StdFromQString(playlist_type); - - auto js = request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::post_data_atom_v, req); - return QStringFromStd(js.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::updatePlaylistVersionsFuture(const QUuid &playlist) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - auto req = JsonStore(PutUpdatePlaylistVersions); - req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); - auto js = request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::put_data_atom_v, req); - return QStringFromStd(js.dump()); - - REQUEST_END() -} - -// find playlist id from playlist -// request versions from shotgun -// add to playlist. -QFuture ShotgunDataSourceUI::refreshPlaylistVersionsFuture(const QUuid &playlist) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - auto req = JsonStore(UseRefreshPlaylist); - req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); - auto js = request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::use_data_atom_v, req); - return QStringFromStd(js.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::getPlaylistNotesFuture(const int id) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - - auto note_filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["note_links", "in", {"type":"Playlist", "id":0}] - ] - })"_json; - - note_filter["conditions"][0][2]["id"] = id; - - auto order = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "Notes", - JsonStore(note_filter), - std::vector({"*"}), - std::vector(), - 1, - 4999); - - return QStringFromStd(order.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::getPlaylistVersionsFuture(const int id) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - - auto vers = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_atom_v, - "Playlists", - id, - std::vector()); - - // spdlog::warn("{}", vers.dump(2)); - - auto order_filter = R"( - { - "logical_operator": "and", - "conditions": [ - ["playlist", "is", {"type":"Playlist", "id":0}] - ] - })"_json; - - order_filter["conditions"][0][2]["id"] = id; - - auto order = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_search_atom_v, - "PlaylistVersionConnection", - JsonStore(order_filter), - std::vector({"sg_sort_order", "version"}), - std::vector({"sg_sort_order"}), - 1, - 4999); - - // should be returned in the correct order.. - - // spdlog::warn("{}", order.dump(2)); - - std::vector version_ids; - for (const auto &i : order["data"]) - version_ids.emplace_back( - std::to_string(i["relationships"]["version"]["data"].at("id").get())); - - if (version_ids.empty()) - return QStringFromStd(vers.dump()); - - // expand version information.. - // get versions - auto query = R"({})"_json; - auto chunked_ids = split_vector(version_ids, 100); - auto data = R"([])"_json; - - for (const auto &chunk : chunked_ids) { - query["id"] = join_as_string(chunk, ","); - - auto js = request_receive_wait( - *sys, - backend_, - SHOTGUN_TIMEOUT, - shotgun_entity_filter_atom_v, - "Versions", - JsonStore(query), - VersionFields, - std::vector(), - 1, - 4999); - // reorder list based on playlist.. - // spdlog::warn("{}", js.dump(2)); - - for (const auto &i : chunk) { - for (auto &j : js["data"]) { - - // spdlog::warn("{} {}", std::to_string(j["id"].get()), i); - if (std::to_string(j["id"].get()) == i) { - data.push_back(j); - break; - } - } - } - } - - auto data_tmp = R"({"data":[]})"_json; - data_tmp["data"] = data; - - // spdlog::warn("{}", js.dump(2)); - - // add back in - vers["data"]["relationships"]["versions"] = data_tmp; - - // create playlist.. - return QStringFromStd(vers.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::tagEntityFuture( - const QString &entity, const int record_id, const int tag_id) { - - REQUEST_BEGIN() - - scoped_actor sys{system()}; - - auto req = JsonStore(PostTagEntity); - - req["entity"] = StdFromQString(entity); - req["entity_id"] = record_id; - req["tag_id"] = tag_id; - - auto js = request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::post_data_atom_v, req); - - return QStringFromStd(js.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::renameTagFuture(const int tag_id, const QString &text) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - - auto req = JsonStore(); - - if (tag_id) { - req = JsonStore(PostRenameTag); - req["tag_id"] = tag_id; - req["value"] = StdFromQString(text); - } else { - req = JsonStore(PostCreateTag); - req["value"] = StdFromQString(text); - } - - auto js = request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::post_data_atom_v, req); - - // trigger update to get new tag. - getReferenceTagsFuture(); - - return QStringFromStd(js.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::removeTagFuture(const int tag_id) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - - auto js = request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, shotgun_delete_entity_atom_v, "Tag", tag_id); - - // trigger update to get new tag. - getReferenceTagsFuture(); - - return QStringFromStd(js.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::untagEntityFuture( - const QString &entity, const int record_id, const int tag_id) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - - auto req = JsonStore(PostUnTagEntity); - - req["entity"] = StdFromQString(entity); - req["entity_id"] = record_id; - req["tag_id"] = tag_id; - - auto js = request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::post_data_atom_v, req); - - return QStringFromStd(js.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::createTagFuture(const QString &text) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - auto req = JsonStore(PostCreateTag); - req["value"] = StdFromQString(text); - - auto js = request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::post_data_atom_v, req); - - // trigger update to get new tag. - getReferenceTagsFuture(); - - return QStringFromStd(js.dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::getEntityFuture(const QString &qentity, const int id) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - auto entity = StdFromQString(qentity); - std::vector fields; - - if (entity == "Version") { - fields = VersionFields; - } - - return QStringFromStd( - request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, shotgun_entity_atom_v, entity, id, fields) - .dump()); - - REQUEST_END() -} - -QFuture ShotgunDataSourceUI::addDownloadToMediaFuture(const QUuid &media) { - REQUEST_BEGIN() - - scoped_actor sys{system()}; - auto req = JsonStore(GetDownloadMedia); - req["media_uuid"] = to_string(UuidFromQUuid(media)); - - return QStringFromStd( - request_receive_wait( - *sys, backend_, SHOTGUN_TIMEOUT, data_source::get_data_atom_v, req) - .dump()); - - REQUEST_END() -} diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_ui.cpp b/src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_ui.cpp deleted file mode 100644 index 02a0a5808..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_ui.cpp +++ /dev/null @@ -1,1084 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "data_source_shotgun_ui.hpp" -#include "shotgun_model_ui.hpp" - -#include "../data_source_shotgun.hpp" -#include "../data_source_shotgun_definitions.hpp" - -#include "xstudio/utility/string_helpers.hpp" -#include "xstudio/ui/qml/json_tree_model_ui.hpp" -#include "xstudio/global_store/global_store.hpp" -#include "xstudio/atoms.hpp" -#include "xstudio/ui/qml/module_ui.hpp" -#include "xstudio/utility/chrono.hpp" - -#include -#include -#include - -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::shotgun_client; -using namespace xstudio::ui::qml; -using namespace std::chrono_literals; -using namespace xstudio::global_store; - -const auto PresetModelLookup = std::map( - {{"edit", "editPresetsModel"}, - {"edit_filter", "editFilterModel"}, - {"media_action", "mediaActionPresetsModel"}, - {"media_action_filter", "mediaActionFilterModel"}, - {"note", "notePresetsModel"}, - {"note_filter", "noteFilterModel"}, - {"note_tree", "noteTreePresetsModel"}, - {"playlist", "playlistPresetsModel"}, - {"playlist_filter", "playlistFilterModel"}, - {"reference", "referencePresetsModel"}, - {"reference_filter", "referenceFilterModel"}, - {"shot", "shotPresetsModel"}, - {"shot_filter", "shotFilterModel"}, - {"shot_tree", "shotTreePresetsModel"}}); - -const auto PresetPreferenceLookup = std::map( - {{"edit", "presets/edit"}, - {"edit_filter", "global_filters/edit"}, - {"media_action", "presets/media_action"}, - {"media_action_filter", "global_filters/media_action"}, - {"note", "presets/note"}, - {"note_filter", "global_filters/note"}, - {"note_tree", "presets/note_tree"}, - {"playlist", "presets/playlist"}, - {"playlist_filter", "global_filters/playlist"}, - {"reference", "presets/reference"}, - {"reference_filter", "global_filters/reference"}, - {"shot", "presets/shot"}, - {"shot_filter", "global_filters/shot"}, - {"shot_tree", "presets/shot_tree"}}); - - -ShotgunDataSourceUI::ShotgunDataSourceUI(QObject *parent) : QMLActor(parent) { - - term_models_ = new QQmlPropertyMap(this); - result_models_ = new QQmlPropertyMap(this); - preset_models_ = new QQmlPropertyMap(this); - - term_models_->insert( - "primaryLocationModel", QVariant::fromValue(new ShotgunListModel(this))); - qvariant_cast(term_models_->value("primaryLocationModel")) - ->populate(locationsJSON); - - term_models_->insert("stepModel", QVariant::fromValue(new ShotgunListModel(this))); - qvariant_cast(term_models_->value("stepModel"))->populate(R"([ - {"name": "None"} - ])"_json); - - term_models_->insert("twigTypeCodeModel", QVariant::fromValue(new ShotgunListModel(this))); - qvariant_cast(term_models_->value("twigTypeCodeModel")) - ->populate(TwigTypeCodes); - - term_models_->insert("onDiskModel", QVariant::fromValue(new ShotgunListModel(this))); - qvariant_cast(term_models_->value("onDiskModel"))->populate(R"([ - {"name": "chn"}, - {"name": "lon"}, - {"name": "mtl"}, - {"name": "mum"}, - {"name": "syd"}, - {"name": "van"} - ])"_json); - - term_models_->insert("lookbackModel", QVariant::fromValue(new ShotgunListModel(this))); - qvariant_cast(term_models_->value("lookbackModel"))->populate(R"([ - {"name": "Today"}, - {"name": "1 Day"}, - {"name": "3 Days"}, - {"name": "7 Days"}, - {"name": "20 Days"}, - {"name": "30 Days"}, - {"name": "30-60 Days"}, - {"name": "60-90 Days"}, - {"name": "100-150 Days"}, - {"name": "Future Only"} - ])"_json); - - term_models_->insert("boolModel", QVariant::fromValue(new ShotgunListModel(this))); - qvariant_cast(term_models_->value("boolModel"))->populate(R"([ - {"name": "True"}, - {"name": "False"} - ])"_json); - - term_models_->insert("resultLimitModel", QVariant::fromValue(new ShotgunListModel(this))); - qvariant_cast(term_models_->value("resultLimitModel"))->populate(R"([ - {"name": "1"}, - {"name": "2"}, - {"name": "4"}, - {"name": "8"}, - {"name": "10"}, - {"name": "20"}, - {"name": "40"}, - {"name": "100"}, - {"name": "200"}, - {"name": "500"}, - {"name": "1000"}, - {"name": "2500"}, - {"name": "4500"} - ])"_json); - - term_models_->insert("orderByModel", QVariant::fromValue(new ShotgunListModel(this))); - qvariant_cast(term_models_->value("orderByModel"))->populate(R"([ - {"name": "Date And Time ASC"}, - {"name": "Date And Time DESC"}, - {"name": "Created ASC"}, - {"name": "Created DESC"}, - {"name": "Updated ASC"}, - {"name": "Updated DESC"}, - {"name": "Client Submit ASC"}, - {"name": "Client Submit DESC"}, - {"name": "Version ASC"}, - {"name": "Version DESC"} - ])"_json); - - updateQueryValueCache( - "Completion Location", - qvariant_cast(term_models_->value("primaryLocationModel")) - ->modelData()); - updateQueryValueCache("TwigTypeCode", TwigTypeCodes); - - term_models_->insert("shotStatusModel", QVariant::fromValue(new ShotgunListModel(this))); - term_models_->insert("projectModel", QVariant::fromValue(new ShotgunListModel(this))); - term_models_->insert("userModel", QVariant::fromValue(new ShotgunListModel(this))); - term_models_->insert("referenceTagModel", QVariant::fromValue(new ShotgunListModel(this))); - term_models_->insert("departmentModel", QVariant::fromValue(new ShotgunListModel(this))); - term_models_->insert("locationModel", QVariant::fromValue(new ShotgunListModel(this))); - term_models_->insert( - "reviewLocationModel", QVariant::fromValue(new ShotgunListModel(this))); - term_models_->insert("playlistTypeModel", QVariant::fromValue(new ShotgunListModel(this))); - term_models_->insert( - "productionStatusModel", QVariant::fromValue(new ShotgunListModel(this))); - term_models_->insert("noteTypeModel", QVariant::fromValue(new ShotgunListModel(this))); - term_models_->insert( - "pipelineStatusModel", QVariant::fromValue(new ShotgunListModel(this))); - - { - auto model = new ShotModel(this); - auto filter = new ShotgunFilterModel(this); - - model->setSequenceMap(&sequences_map_); - model->setQueryValueCache(&query_value_cache_); - filter->setSourceModel(model); - - result_models_->insert("shotResultsModel", QVariant::fromValue(filter)); - result_models_->insert("shotResultsBaseModel", QVariant::fromValue(model)); - } - - { - auto model = new ShotModel(this); - auto filter = new ShotgunFilterModel(this); - - model->setSequenceMap(&sequences_map_); - model->setQueryValueCache(&query_value_cache_); - filter->setSourceModel(model); - - result_models_->insert("shotTreeResultsModel", QVariant::fromValue(filter)); - result_models_->insert("shotTreeResultsBaseModel", QVariant::fromValue(model)); - } - - { - auto model = new PlaylistModel(this); - auto filter = new ShotgunFilterModel(this); - - model->setQueryValueCache(&query_value_cache_); - filter->setSourceModel(model); - - result_models_->insert("playlistResultsModel", QVariant::fromValue(filter)); - result_models_->insert("playlistResultsBaseModel", QVariant::fromValue(model)); - } - - { - auto model = new EditModel(this); - model->populate( - R"([{"name": "Edit 001"}, {"name": "Edit 002"}, {"name": "Edit 003"}, {"name": "Edit 004"}])"_json); - - auto filter = new ShotgunFilterModel(this); - - model->setQueryValueCache(&query_value_cache_); - filter->setSourceModel(model); - - result_models_->insert("editResultsModel", QVariant::fromValue(filter)); - result_models_->insert("editResultsBaseModel", QVariant::fromValue(model)); - } - - { - auto model = new ReferenceModel(this); - auto filter = new ShotgunFilterModel(this); - - model->setQueryValueCache(&query_value_cache_); - model->setSequenceMap(&sequences_map_); - filter->setSourceModel(model); - - result_models_->insert("referenceResultsModel", QVariant::fromValue(filter)); - result_models_->insert("referenceResultsBaseModel", QVariant::fromValue(model)); - } - - { - auto model = new NoteModel(this); - auto filter = new ShotgunFilterModel(this); - - model->setQueryValueCache(&query_value_cache_); - filter->setSourceModel(model); - - result_models_->insert("noteResultsModel", QVariant::fromValue(filter)); - result_models_->insert("noteResultsBaseModel", QVariant::fromValue(model)); - } - - { - auto model = new NoteModel(this); - auto filter = new ShotgunFilterModel(this); - - model->setQueryValueCache(&query_value_cache_); - filter->setSourceModel(model); - - result_models_->insert("noteTreeResultsModel", QVariant::fromValue(filter)); - result_models_->insert("noteTreeResultsBaseModel", QVariant::fromValue(model)); - } - - { - auto model = new MediaActionModel(this); - auto filter = new ShotgunFilterModel(this); - - model->setQueryValueCache(&query_value_cache_); - filter->setSourceModel(model); - model->setSequenceMap(&sequences_map_); - - result_models_->insert("mediaActionResultsModel", QVariant::fromValue(filter)); - result_models_->insert("mediaActionResultsBaseModel", QVariant::fromValue(model)); - } - - for (const auto &[m, f] : std::vector>{ - {"shotPresetsModel", "shotFilterModel"}, - {"shotTreePresetsModel", "shotFilterModel"}, - {"playlistPresetsModel", "playlistFilterModel"}, - {"editPresetsModel", "editFilterModel"}, - {"referencePresetsModel", "referenceFilterModel"}, - {"notePresetsModel", "noteFilterModel"}, - {"noteTreePresetsModel", "noteFilterModel"}, - {"mediaActionPresetsModel", "mediaActionFilterModel"}}) { - - if (not preset_models_->contains(QStringFromStd(m))) { - auto model = new ShotgunTreeModel(this); - model->setSequenceMap(&sequences_map_); - preset_models_->insert(QStringFromStd(m), QVariant::fromValue(model)); - } - - if (not preset_models_->contains(QStringFromStd(f))) { - auto filter = new ShotgunTreeModel(this); - preset_models_->insert(QStringFromStd(f), QVariant::fromValue(filter)); - } - } - - init(CafSystemObject::get_actor_system()); - - for (const auto &[m, n] : std::vector>{ - {"shotPresetsModel", "shot"}, - {"shotTreePresetsModel", "shot_tree"}, - {"playlistPresetsModel", "playlist"}, - {"editPresetsModel", "edit"}, - {"referencePresetsModel", "reference"}, - {"notePresetsModel", "note"}, - {"noteTreePresetsModel", "note_tree"}, - {"mediaActionPresetsModel", "media_action"}, - - {"shotFilterModel", "shot_filter"}, - {"playlistFilterModel", "playlist_filter"}, - {"editFilterModel", "edit_filter"}, - {"referenceFilterModel", "reference_filter"}, - {"noteFilterModel", "note_filter"}, - {"mediaActionFilterModel", "media_action_filter"}}) { - connect( - qvariant_cast(preset_models_->value(QStringFromStd(m))), - &ShotgunTreeModel::modelChanged, - this, - [this, n = n]() { syncModelChanges(QStringFromStd(n)); }); - } -} - -QObject *ShotgunDataSourceUI::groupModel(const int project_id) { - if (not groups_map_.count(project_id)) { - groups_map_[project_id] = new ShotgunListModel(this); - groups_filter_map_[project_id] = new ShotgunFilterModel(this); - groups_filter_map_[project_id]->setSourceModel(groups_map_[project_id]); - - getGroupsFuture(project_id); - } - - return groups_filter_map_[project_id]; -} - -void ShotgunDataSourceUI::createSequenceModels(const int project_id) { - if (not sequences_map_.count(project_id)) { - sequences_map_[project_id] = new ShotgunListModel(this); - sequences_tree_map_[project_id] = new ShotgunSequenceModel(this); - getSequencesFuture(project_id); - } -} - -QObject *ShotgunDataSourceUI::sequenceModel(const int project_id) { - createSequenceModels(project_id); - return sequences_map_[project_id]; -} - -QObject *ShotgunDataSourceUI::sequenceTreeModel(const int project_id) { - createSequenceModels(project_id); - return sequences_tree_map_[project_id]; -} - -void ShotgunDataSourceUI::createShotModels(const int project_id) { - if (not shots_map_.count(project_id)) { - shots_map_[project_id] = new ShotgunListModel(this); - shots_filter_map_[project_id] = new ShotgunFilterModel(this); - shots_filter_map_[project_id]->setSourceModel(shots_map_[project_id]); - getShotsFuture(project_id); - } -} - -void ShotgunDataSourceUI::createCustomEntity24Models(const int project_id) { - if (not custom_entity_24_map_.count(project_id)) { - custom_entity_24_map_[project_id] = new ShotgunListModel(this); - getCustomEntity24Future(project_id); - } -} - - -QObject *ShotgunDataSourceUI::shotModel(const int project_id) { - createShotModels(project_id); - return shots_map_[project_id]; -} - -QObject *ShotgunDataSourceUI::customEntity24Model(const int project_id) { - createCustomEntity24Models(project_id); - return custom_entity_24_map_[project_id]; -} - -QObject *ShotgunDataSourceUI::shotSearchFilterModel(const int project_id) { - createShotModels(project_id); - return shots_filter_map_[project_id]; -} - -QObject *ShotgunDataSourceUI::playlistModel(const int project_id) { - if (not playlists_map_.count(project_id)) { - playlists_map_[project_id] = new ShotgunListModel(this); - getPlaylistsFuture(project_id); - } - - return playlists_map_[project_id]; -} - -// unused ? -QString ShotgunDataSourceUI::getShotSequence(const int project_id, const QString &shot) { - QString result; - - if (sequences_map_.count(project_id)) { - // get data.. - const auto &data = sequences_map_[project_id]->modelData(); - - auto needle = StdFromQString(shot); - - for (const auto &i : data) { - try { - for (const auto &s : i.at("relationships").at("shots").at("data")) { - if (s.at("type") == "Shot" and s.at("name") == needle) { - result = QStringFromStd(i.at("attributes").at("code")); - break; - } - } - } catch (...) { - } - if (not result.isEmpty()) - break; - } - } - - return result; -} - -void ShotgunDataSourceUI::syncModelChanges(const QString &preset) { - auto tmp = StdFromQString(preset); - - // the change has already happened - if (not(preset_update_pending_.count(tmp) and preset_update_pending_[tmp])) { - preset_update_pending_[tmp] = true; - - delayed_anon_send(as_actor(), std::chrono::seconds(5), shotgun_preferences_atom_v, tmp); - } -} - - -void ShotgunDataSourceUI::setLiveLinkMetadata(QString &data) { - if (data == "null") - data = "{}"; - - try { - if (data != live_link_metadata_string_) { - try { - live_link_metadata_ = JsonStore(nlohmann::json::parse(StdFromQString(data))); - live_link_metadata_string_ = data; - } catch (...) { - } - - try { - auto project = live_link_metadata_.at("metadata") - .at("shotgun") - .at("version") - .at("relationships") - .at("project") - .at("data") - .at("name") - .get(); - auto project_id = live_link_metadata_.at("metadata") - .at("shotgun") - .at("version") - .at("relationships") - .at("project") - .at("data") - .at("id") - .get(); - - // update model caches. - groupModel(project_id); - sequenceModel(project_id); - shotModel(project_id); - playlistModel(project_id); - - emit projectChanged(project_id, QStringFromStd(project)); - } catch (...) { - } - - // trigger update of models with new livelink data.. - for (const auto &i : - {"shotPresetsModel", - "shotTreePresetsModel", - "playlistPresetsModel", - "editPresetsModel", - "referencePresetsModel", - "notePresetsModel", - "noteTreePresetsModel", - "mediaActionPresetsModel"}) - qvariant_cast(preset_models_->value(i)) - ->updateLiveLinks(live_link_metadata_); - - emit liveLinkMetadataChanged(); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } -} - -QString ShotgunDataSourceUI::getShotgunUserName() { - QString result; // = QString(get_user_name()); - - auto ind = - qvariant_cast(term_models_->value("userModel")) - ->search(QVariant::fromValue(QString(get_login_name().c_str())), "loginRole"); - if (ind != -1) - result = qvariant_cast(term_models_->value("userModel")) - ->get(ind) - .toString(); - - return result; -} - -// remove old system presets from user preferences. -utility::JsonStore ShotgunDataSourceUI::purgeOldSystem( - const utility::JsonStore &vprefs, const utility::JsonStore &dprefs) const { - - utility::JsonStore result = dprefs; - - size_t count = 0; - - for (const auto &i : vprefs) { - try { - if (i.value("type", "") == "system") - count++; - } catch (...) { - } - } - - if (count != result.size()) { - spdlog::warn("Purging old presets. {} {}", count, result.size()); - for (const auto &i : vprefs) { - try { - if (i.value("type", "") == "system") - continue; - } catch (...) { - } - result.push_back(i); - } - } else { - result = vprefs; - } - - return result; -} - -void ShotgunDataSourceUI::populatePresetModel( - const utility::JsonStore &prefs, - const std::string &path, - ShotgunTreeModel *model, - const bool purge_old, - const bool clear_flags) { - try { - // replace embedded envvars - std::string tmp; - - // spdlog::warn("path {}", path); - - if (purge_old) - tmp = expand_envvars(purgeOldSystem( - preference_value(prefs, path), - preference_default_value(prefs, path)) - .dump()); - else - tmp = expand_envvars(preference_value(prefs, path).dump()); - - model->populate(JsonStore(nlohmann::json::parse(tmp))); - if (clear_flags) { - model->clearLoaded(); - model->clearExpanded(); - model->clearLiveLinks(); - } else { - // filter model set index 0 as active. - model->setActivePreset(0); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } -} - -Q_INVOKABLE void ShotgunDataSourceUI::resetPreset(const QString &qpreset, const int index) { - try { - auto prefs = GlobalStoreHelper(system()); - auto preset = StdFromQString(qpreset); - auto defval = JsonStore(); - ShotgunTreeModel *model = nullptr; - - if (preset == "Versions") - preset = "shot"; - else if (preset == "Playlists") - preset = "playlist"; - else if (preset == "Notes") - preset = "note"; - else if (preset == "Media Actions") - preset = "media_action"; - else if (preset == "Edits") - preset = "edit"; - else if (preset == "Reference") - preset = "reference"; - else if (preset == "Versions Tree") - preset = "shot_tree"; - else if (preset == "Notes Tree") - preset = "note_tree"; - - if (PresetModelLookup.count(preset)) { - defval = prefs.default_value( - "/plugin/data_source/shotgun/" + PresetPreferenceLookup.at(preset)); - model = qvariant_cast( - preset_models_->value(QStringFromStd(PresetModelLookup.at(preset)))); - } - - if (model == nullptr) - return; - - // expand envs - defval = JsonStore(nlohmann::json::parse(expand_envvars(defval.dump()))); - - // update and add - // update globals ? - // and presets.. - JsonStore data(model->modelData().at("queries")); - - // massage format.. - for (const auto &i : defval) { - auto name = i.at("name").get(); - // find name in current.. - auto found = false; - for (int j = 0; j < static_cast(data.size()); j++) { - if (data[j].at("name") == name) { - found = true; - - if (index != -1 && j != index) - break; - - data[j] = i; - break; - } - } - // no match then insert,, - - if (not found) - data.push_back(i); - } - - // reset model.. - model->populate(data); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } -} - -void ShotgunDataSourceUI::loadPresets(const bool purge_old) { - try { - auto prefs = GlobalStoreHelper(system()); - JsonStore js; - prefs.get_group(js); - - try { - maximum_result_count_ = - preference_value(js, "/plugin/data_source/shotgun/maximum_result_count"); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - try { - qvariant_cast(term_models_->value("stepModel")) - ->populate( - preference_value(js, "/plugin/data_source/shotgun/pipestep")); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - for (const auto &[m, f] : std::vector>{ - {"shot", "shotPresetsModel"}, - {"shot_tree", "shotTreePresetsModel"}, - {"playlist", "playlistPresetsModel"}, - {"reference", "referencePresetsModel"}, - {"edit", "editPresetsModel"}, - {"note", "notePresetsModel"}, - {"note_tree", "noteTreePresetsModel"}, - {"media_action", "mediaActionPresetsModel"}}) { - populatePresetModel( - js, - "/plugin/data_source/shotgun/presets/" + m, - qvariant_cast(preset_models_->value(QStringFromStd(f))), - purge_old, - true); - } - - for (const auto &[m, f] : std::vector>{ - {"shot", "shotFilterModel"}, - {"playlist", "playlistFilterModel"}, - {"reference", "referenceFilterModel"}, - {"edit", "editFilterModel"}, - {"note", "noteFilterModel"}, - {"media_action", "mediaActionFilterModel"}}) { - populatePresetModel( - js, - "/plugin/data_source/shotgun/global_filters/" + m, - qvariant_cast(preset_models_->value(QStringFromStd(f))), - false, - false); - } - - disable_flush_ = false; - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } -} - -void ShotgunDataSourceUI::handleResult( - const JsonStore &request, const std::string &model, const std::string &name) { - - if (not epoc_map_.count(request.at("context").at("type")) or - epoc_map_.at(request.at("context").at("type")) < request.at("context").at("epoc")) { - auto slm = - qvariant_cast(result_models_->value(QStringFromStd(model))); - - slm->populate(request.at("result").at("data")); - slm->setTruncated(request.at("context").at("truncated").get()); - - source_selection_[name] = std::make_pair( - request.at("context").at("visual_source").get(), - request.at("context").at("audio_source").get()); - flag_selection_[name] = std::make_pair( - request.at("context").at("flag_text").get(), - request.at("context").at("flag_colour").get()); - epoc_map_[request.at("context").at("type")] = request.at("context").at("epoc"); - } -} - -void ShotgunDataSourceUI::init(caf::actor_system &system) { - QMLActor::init(system); - - // access global data store to retrieve stored filters. - // delay this just in case we're to early.. - delayed_anon_send(as_actor(), std::chrono::seconds(2), shotgun_preferences_atom_v); - - set_message_handler([=](actor_companion *) -> message_handler { - return { - [=](shotgun_acquire_authentication_atom, const std::string &message) { - emit requestSecret(QStringFromStd(message)); - }, - [=](shotgun_preferences_atom) { loadPresets(); }, - - [=](shotgun_preferences_atom, const std::string &preset) { flushPreset(preset); }, - - // catchall for dealing with results from shotgun - [=](shotgun_info_atom, const JsonStore &request, const JsonStore &data) { - try { - if (request.at("type") == "project") - qvariant_cast(term_models_->value("projectModel")) - ->populate(data.at("data")); - else if (request.at("type") == "user") { - qvariant_cast(term_models_->value("userModel")) - ->populate(data.at("data")); - updateQueryValueCache("User", data.at("data")); - } else if (request.at("type") == "department") { - qvariant_cast( - term_models_->value("departmentModel")) - ->populate(data.at("data")); - updateQueryValueCache("Department", data.at("data")); - } else if (request.at("type") == "location") - qvariant_cast(term_models_->value("locationModel")) - ->populate(data.at("data")); - else if (request.at("type") == "review_location") - qvariant_cast( - term_models_->value("reviewLocationModel")) - ->populate(data.at("data")); - else if (request.at("type") == "reference_tag") - qvariant_cast( - term_models_->value("referenceTagModel")) - ->populate(data.at("data")); - else if (request.at("type") == "shot_status") { - updateQueryValueCache("Exclude Shot Status", data.at("data")); - updateQueryValueCache("Shot Status", data.at("data")); - qvariant_cast( - term_models_->value("shotStatusModel")) - ->populate(data.at("data")); - } else if (request.at("type") == "playlist_type") - qvariant_cast( - term_models_->value("playlistTypeModel")) - ->populate(data.at("data")); - else if (request.at("type") == "custom_entity_24") { - custom_entity_24_map_[request.at("id")]->populate(data.at("data")); - updateQueryValueCache( - "Unit", data.at("data"), request.at("id").get()); - } else if (request.at("type") == "production_status") { - qvariant_cast( - term_models_->value("productionStatusModel")) - ->populate(data.at("data")); - updateQueryValueCache("Production Status", data.at("data")); - } else if (request.at("type") == "note_type") - qvariant_cast(term_models_->value("noteTypeModel")) - ->populate(data.at("data")); - else if (request.at("type") == "pipeline_status") { - qvariant_cast( - term_models_->value("pipelineStatusModel")) - ->populate(data.at("data")); - updateQueryValueCache("Pipeline Status", data.at("data")); - } else if (request.at("type") == "group") - groups_map_[request.at("id")]->populate(data.at("data")); - else if (request.at("type") == "sequence") { - sequences_map_[request.at("id")]->populate(data.at("data")); - sequences_tree_map_[request.at("id")]->setModelData( - ShotgunSequenceModel::flatToTree(data.at("data"))); - } else if (request.at("type") == "shot") { - shots_map_[request.at("id")]->populate(data.at("data")); - updateQueryValueCache( - "Shot", data.at("data"), request.at("id").get()); - } else if (request.at("type") == "playlist") { - playlists_map_[request.at("id")]->populate(data.at("data")); - updateQueryValueCache( - "Playlist", data.at("data"), request.at("id").get()); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - }, - - // catchall for dealing with results from shotgun - [=](shotgun_info_atom, const JsonStore &request) { - try { - auto type = request.at("context").at("type").get(); - - if (type == "playlist_result") { - handleResult(request, "playlistResultsBaseModel", "Playlists"); - } else if (type == "shot_result") { - handleResult(request, "shotResultsBaseModel", "Versions"); - } else if (type == "shot_tree_result") { - handleResult(request, "shotTreeResultsBaseModel", "Versions Tree"); - } else if (type == "media_action_result") { - handleResult(request, "mediaActionResultsBaseModel", "Menu Setup"); - } else if (type == "reference_result") { - handleResult(request, "referenceResultsBaseModel", "Reference"); - } else if (type == "note_result") { - handleResult(request, "noteResultsBaseModel", "Notes"); - } else if (type == "note_tree_result") { - handleResult(request, "noteTreeResultsBaseModel", "Notes Tree"); - } else if (type == "edit_result") { - handleResult(request, "editResultsBaseModel", "Edits"); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - }, - - - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) { - // if(msg.source == store_events) - // unsubscribe(); - }}; - }); -} - -void ShotgunDataSourceUI::undo() { - auto tmp = history_.undo(); - if (tmp) { - auto current = getPresetData((*tmp).first); - if (current != (*tmp).second) { - // spdlog::warn("undo {}", (*tmp).first); - setPreset((*tmp).first, (*tmp).second); - } else - undo(); - } -} - -void ShotgunDataSourceUI::redo() { - auto tmp = history_.redo(); - if (tmp) { - auto current = getPresetData((*tmp).first); - if (current != (*tmp).second) { - // spdlog::warn("redo {}", (*tmp).first); - setPreset((*tmp).first, (*tmp).second); - } else - redo(); - } -} - -void ShotgunDataSourceUI::snapshot(const QString &preset) { - auto tmp = StdFromQString(preset); - auto data = getPresetData(tmp); - - // spdlog::warn("snapshot {}", tmp); - - history_.push(std::make_pair(tmp, data)); -} - -void ShotgunDataSourceUI::setPreset(const std::string &preset, const utility::JsonStore &data) { - if (PresetModelLookup.count(preset)) - qvariant_cast( - preset_models_->value(QStringFromStd(PresetModelLookup.at(preset)))) - ->populate(data); -} - -utility::JsonStore ShotgunDataSourceUI::getPresetData(const std::string &preset) { - if (PresetModelLookup.count(preset)) - return qvariant_cast( - preset_models_->value(QStringFromStd(PresetModelLookup.at(preset)))) - ->modelData() - .at("queries"); - - return utility::JsonStore(); -} - -void ShotgunDataSourceUI::flushPreset(const std::string &preset) { - if (disable_flush_) - return; - - preset_update_pending_[preset] = false; - // flush relevant changes to global dict. - auto prefs = GlobalStoreHelper(system()); - - if (PresetModelLookup.count(preset)) { - prefs.set_value( - qvariant_cast( - preset_models_->value(QStringFromStd(PresetModelLookup.at(preset)))) - ->modelData() - .at("queries"), - "/plugin/data_source/shotgun/" + PresetPreferenceLookup.at(preset)); - } -} - -void ShotgunDataSourceUI::setConnected(const bool connected) { - if (connected_ != connected) { - connected_ = connected; - if (connected_) { - populateCaches(); - } - emit connectedChanged(); - } -} - -void ShotgunDataSourceUI::authenticate(const bool cancel) { - anon_send(backend_, shotgun_acquire_authentication_atom_v, cancel); -} - -void ShotgunDataSourceUI::set_backend(caf::actor backend) { - backend_ = backend; - // this forces the use of futures to access datasource - // if this is not done the application will deadlock if the password - // is requested, as we'll already be in this class, - // this is because the UI runs in a single thread. - // I'm not aware of any solutions to this. - // we can use timeouts though.. - // but as we're accessing a remote service, we'll need to be careful.. - - anon_send(backend_, module::connect_to_ui_atom_v); - anon_send(backend_, shotgun_authentication_source_atom_v, as_actor()); -} - -void ShotgunDataSourceUI::setBackendId(const QString &qid) { - caf::actor_id id = std::stoull(StdFromQString(qid).c_str()); - - if (id and id != backend_id_) { - auto actor = system().registry().template get(id); - if (actor) { - set_backend(actor); - backend_str_ = QStringFromStd(to_string(backend_)); - backend_id_ = backend_.id(); - emit backendChanged(); - emit backendIdChanged(); - } else { - spdlog::warn("{} Actor lookup failed", __PRETTY_FUNCTION__); - } - } -} - -void ShotgunDataSourceUI::populateCaches() { - getProjectsFuture(); - getUsersFuture(); - getDepartmentsFuture(); - getReferenceTagsFuture(); - - getSchemaFieldsFuture("playlist", "sg_location", "location"); - getSchemaFieldsFuture("playlist", "sg_review_location_1", "review_location"); - getSchemaFieldsFuture("playlist", "sg_type", "playlist_type"); - - getSchemaFieldsFuture("shot", "sg_status_list", "shot_status"); - - getSchemaFieldsFuture("note", "sg_note_type", "note_type"); - getSchemaFieldsFuture("version", "sg_production_status", "production_status"); - getSchemaFieldsFuture("version", "sg_status_list", "pipeline_status"); -} - -JsonStore ShotgunDataSourceUI::buildDataFromField(const JsonStore &data) { - auto result = R"({"data": []})"_json; - - std::map entries; - - for (const auto &i : data["data"]["properties"]["valid_values"]["value"]) { - auto value = i.get(); - auto key = value; - if (data["data"]["properties"].count("display_values") and - data["data"]["properties"]["display_values"]["value"].count(value)) { - key = - data["data"]["properties"]["display_values"]["value"][value].get(); - } - - entries.insert(std::make_pair(key, value)); - } - - for (const auto &i : entries) { - auto field = R"({"id": null, "attributes": {"name": null}})"_json; - field["attributes"]["name"] = i.first; - field["id"] = i.second; - result["data"].push_back(field); - } - - return JsonStore(result); -} - -// QFuture ShotgunDataSourceUI::refreshPlaylistNotesFuture(const QUuid &playlist) { -// return QtConcurrent::run([=]() { -// if (backend_) { -// try { -// scoped_actor sys{system()}; -// auto req = JsonStore(RefreshPlaylistNotesJSON); -// req["playlist_uuid"] = to_string(UuidFromQUuid(playlist)); -// auto js = request_receive_wait( -// *sys, backend_, SHOTGUN_TIMEOUT, data_source::use_data_atom_v, req); -// return QStringFromStd(js.dump()); -// } catch (const XStudioError &err) { -// spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); -// auto error = R"({'error':{})"_json; -// error["error"]["source"] = to_string(err.type()); -// error["error"]["message"] = err.what(); -// return QStringFromStd(JsonStore(error).dump()); -// } catch (const std::exception &err) { -// spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); -// return QStringFromStd(err.what()); -// } -// } -// return QString(); -// }); -// } - - -QFuture ShotgunDataSourceUI::requestFileTransferFuture( - const QVariantList &uuids, - const QString &project, - const QString &src_location, - const QString &dest_location) { - return QtConcurrent::run([=]() { - QString program = "dnenv-do"; - QString result; - auto show_env = utility::get_env("SHOW"); - auto shot_env = utility::get_env("SHOT"); - - QStringList args = { - QStringFromStd(show_env ? *show_env : "NSFL"), - QStringFromStd(shot_env ? *shot_env : "ldev_pipe"), - "--", - "ft-cp", - // "-n", - "-debug", - "-show", - project, - "-e", - "production", - "--watchers", - QStringFromStd(get_login_name()), - "-priority", - "medium", - "-description", - "File transfer requested by xStudio"}; - - std::vector dnuuids; - for (const auto &i : uuids) { - dnuuids.emplace_back(to_string(UuidFromQUuid(i.toUuid()))); - } - args.push_back(src_location + ":" + QStringFromStd(join_as_string(dnuuids, ","))); - args.push_back(dest_location); - - qint64 pid; - QProcess::startDetached(program, args, "", &pid); - - return result; - }); -} - - -void ShotgunDataSourceUI::updateModel(const QString &qname) { - auto name = StdFromQString(qname); - - if (name == "referenceTagModel") - getReferenceTagsFuture(); -} - - -// ft-cp -n -debug -show MEG2 -e production b3ae9497-6218-4124-8f8e-07158e3165dd mum --watchers -// al -priority medium -description "File transfer requested by xStudio - - -//![plugin] -class QShotgunDataSourceQml : public QQmlExtensionPlugin { - Q_OBJECT - Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid) - - public: - void registerTypes(const char *uri) override { - qmlRegisterType(uri, 1, 0, "ShotgunDataSource"); - } -}; -//![plugin] - -#include "data_source_shotgun_ui.moc" diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_ui.hpp b/src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_ui.hpp deleted file mode 100644 index ea9ffef59..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/data_source_shotgun_ui.hpp +++ /dev/null @@ -1,514 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include - -CAF_PUSH_WARNINGS -#include -#include -#include -#include -#include -#include -#include -#include -CAF_POP_WARNINGS - -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/shotgun_client/shotgun_client.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/history/history.hpp" - -namespace xstudio { -using namespace shotgun_client; -namespace ui { - namespace qml { - using namespace std::chrono_literals; - const auto SHOTGUN_TIMEOUT = 120s; - - class ShotModel; - class ShotgunListModel; - class PlaylistModel; - class EditModel; - class ReferenceModel; - class NoteModel; - class MediaActionModel; - class ShotgunFilterModel; - class ShotgunTreeModel; - class ShotgunSequenceModel; - class JSONTreeModel; - - class ShotgunDataSourceUI : public QMLActor { - - Q_OBJECT - Q_PROPERTY(bool connected READ connected WRITE setConnected NOTIFY connectedChanged) - Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) - Q_PROPERTY(QString backend READ backend NOTIFY backendChanged) - Q_PROPERTY( - QString backendId READ backendId WRITE setBackendId NOTIFY backendIdChanged) - - Q_PROPERTY(QQmlPropertyMap *termModels READ termModels NOTIFY termModelsChanged) - Q_PROPERTY( - QQmlPropertyMap *resultModels READ resultModels NOTIFY resultModelsChanged) - Q_PROPERTY( - QQmlPropertyMap *presetModels READ presetModels NOTIFY presetModelsChanged) - - Q_PROPERTY(QString liveLinkMetadata READ liveLinkMetadata WRITE setLiveLinkMetadata - NOTIFY liveLinkMetadataChanged) - - QML_NAMED_ELEMENT("ShotgunDataSource") - //![0] - public: - explicit ShotgunDataSourceUI(QObject *parent = nullptr); - ~ShotgunDataSourceUI() override = default; - - void init(caf::actor_system &system) override; - QString name() { return name_; } - QString backend() { return backend_str_; } - QString backendId() { return QStringFromStd(std::to_string(backend_id_)); } - - void set_backend(caf::actor backend); - void setBackendId(const QString &id); - - QQmlPropertyMap *termModels() const { return term_models_; } - QQmlPropertyMap *resultModels() const { return result_models_; } - QQmlPropertyMap *presetModels() const { return preset_models_; } - - Q_INVOKABLE QObject *groupModel(const int project_id); - Q_INVOKABLE QObject *sequenceModel(const int project_id); - Q_INVOKABLE QObject *sequenceTreeModel(const int project_id); - Q_INVOKABLE QObject *shotModel(const int project_id); - Q_INVOKABLE QObject *customEntity24Model(const int project_id); - Q_INVOKABLE QObject *shotSearchFilterModel(const int project_id); - Q_INVOKABLE QObject *playlistModel(const int project_id); - - Q_INVOKABLE QString getShotgunUserName(); - - // unused ? - Q_INVOKABLE QString getShotSequence(const int project_id, const QString &shot); - - [[nodiscard]] bool connected() const { return connected_; } - - void setConnected(const bool connected); - - QString liveLinkMetadata() const { return live_link_metadata_string_; } - void setLiveLinkMetadata(QString &data); - - Q_INVOKABLE void undo(); - Q_INVOKABLE void redo(); - Q_INVOKABLE void snapshot(const QString &preset); - - Q_INVOKABLE void resetPreset(const QString &preset, const int index = -1); - - signals: - void liveLinkMetadataChanged(); - void connectedChanged(); - void nameChanged(); - void requestSecret(const QString &message); - void backendChanged(); - void backendIdChanged(); - void systemInit(QObject *object); - - void termModelsChanged(); - void resultModelsChanged(); - void presetModelsChanged(); - - void projectChanged(const int project_id, const QString &project_name); - - public slots: - void authenticate(const bool cancel = false); - void setName(const QString &name) { - if (name_ != name) { - name_ = name; - emit nameChanged(); - } - } - - void updateModel(const QString &name); - - QString getPlaylists(const int project_id) { - return getPlaylistsFuture(project_id).result(); - } - QFuture getPlaylistsFuture(const int project_id); - - QString getPlaylistVersions(const int id) { - return getPlaylistVersionsFuture(id).result(); - } - QFuture getPlaylistVersionsFuture(const int id); - - QString getPlaylistNotes(const int id) { - return getPlaylistNotesFuture(id).result(); - } - QFuture getPlaylistNotesFuture(const int id); - - QString addVersionToPlaylist( - const QString &version, const QUuid &playlist, const QUuid &before = QUuid()) { - return addVersionToPlaylistFuture(version, playlist, before).result(); - } - QFuture addVersionToPlaylistFuture( - const QString &version, const QUuid &playlist, const QUuid &before = QUuid()); - - QString getProjects() { return getProjectsFuture().result(); } - QFuture getProjectsFuture(); - - - QString getSchemaFields( - const QString &entity, const QString &field, const QString &update_name = "") { - return getSchemaFieldsFuture(entity, field, update_name).result(); - } - QFuture getSchemaFieldsFuture( - const QString &entity, const QString &field, const QString &update_name = ""); - - QString getGroups(const int project_id) { - return getGroupsFuture(project_id).result(); - } - QFuture getGroupsFuture(const int project_id); - - QString getSequences(const int project_id) { - return getSequencesFuture(project_id).result(); - } - QFuture getSequencesFuture(const int project_id); - - QString getShots(const int project_id) { - return getShotsFuture(project_id).result(); - } - QFuture getShotsFuture(const int project_id); - - QString getUsers() { return getUsersFuture().result(); } - QFuture getUsersFuture(); - - QString getEntity(const QString &entity, const int id) { - return getEntityFuture(entity, id).result(); - } - QFuture getEntityFuture(const QString &entity, const int id); - - QString getReferenceTags() { return getReferenceTagsFuture().result(); } - QFuture getReferenceTagsFuture(); - - QString tagEntity(const QString &entity, const int record_id, const int tag_id) { - return tagEntityFuture(entity, record_id, tag_id).result(); - } - QFuture - tagEntityFuture(const QString &entity, const int record_id, const int tag_id); - - QString untagEntity(const QString &entity, const int record_id, const int tag_id) { - return untagEntityFuture(entity, record_id, tag_id).result(); - } - QFuture - untagEntityFuture(const QString &entity, const int record_id, const int tag_id); - - QString createTag(const QString &text) { return createTagFuture(text).result(); } - QFuture createTagFuture(const QString &text); - - QString renameTag(const int tag_id, const QString &text) { - return renameTagFuture(tag_id, text).result(); - } - QFuture renameTagFuture(const int tag_id, const QString &text); - - QString removeTag(const int tag_id) { return removeTagFuture(tag_id).result(); } - QFuture removeTagFuture(const int tag_id); - - QString getDepartments() { return getDepartmentsFuture().result(); } - QFuture getDepartmentsFuture(); - - QString getCustomEntity24(const int project_id) { - return getCustomEntity24Future(project_id).result(); - } - QFuture getCustomEntity24Future(const int project_id); - - QString updateEntity( - const QString &entity, const int record_id, const QString &update_json) { - return updateEntityFuture(entity, record_id, update_json).result(); - } - QFuture updateEntityFuture( - const QString &entity, const int record_id, const QString &update_json); - - QString updatePlaylistVersions(const QUuid &playlist) { - return updatePlaylistVersionsFuture(playlist).result(); - } - QFuture updatePlaylistVersionsFuture(const QVariant &playlist) { - return updatePlaylistVersionsFuture(playlist.toUuid()); - } - QFuture updatePlaylistVersionsFuture(const QUuid &playlist); - - QString refreshPlaylistVersions(const QUuid &playlist) { - return refreshPlaylistVersionsFuture(playlist).result(); - } - QFuture refreshPlaylistVersionsFuture(const QVariant &playlist) { - return refreshPlaylistVersionsFuture(playlist.toUuid()); - } - QFuture refreshPlaylistVersionsFuture(const QUuid &playlist); - - // QString refreshPlaylistNotes(const QUuid &playlist) { - // return refreshPlaylistNotesFuture(playlist).result(); - // } - // QFuture refreshPlaylistNotesFuture(const QVariant &playlist) { - // return refreshPlaylistNotesFuture(playlist.toUuid()); - // } - // QFuture refreshPlaylistNotesFuture(const QUuid &playlist); - - QString createPlaylist( - const QUuid &playlist, - const int project_id, - const QString &name, - const QString &location, - const QString &playlist_type) { - - return createPlaylistFuture(playlist, project_id, name, location, playlist_type) - .result(); - } - QFuture createPlaylistFuture( - const QVariant &playlist, - const int project_id, - const QString &name, - const QString &location, - const QString &playlist_type) { - return createPlaylistFuture( - playlist.toUuid(), project_id, name, location, playlist_type); - } - QFuture createPlaylistFuture( - const QUuid &playlist, - const int project_id, - const QString &name, - const QString &location, - const QString &playlist_type); - - - QString getVersions(const int project_id, const QVariant &ids) { - return getVersionsFuture(project_id, ids).result(); - } - QFuture getVersionsFuture(const int project_id, const QVariant &ids); - - QString preparePlaylistNotes( - const QUuid &playlist, - const QList &media, - const bool notify_owner = false, - const std::vector notify_group_ids = {}, - const bool combine = false, - const bool add_time = false, - const bool add_playlist_name = false, - const bool add_type = false, - const bool anno_requires_note = true, - const bool skip_already_published = false, - const QString &defaultType = "") { - return preparePlaylistNotesFuture( - playlist, - media, - notify_owner, - notify_group_ids, - combine, - add_time, - add_playlist_name, - add_type, - anno_requires_note, - skip_already_published, - defaultType) - .result(); - } - QFuture preparePlaylistNotesFuture( - const QUuid &playlist, - const QList &media, - const bool notify_owner = false, - const std::vector notify_group_ids = {}, - const bool combine = false, - const bool add_time = false, - const bool add_playlist_name = false, - const bool add_type = false, - const bool anno_requires_note = true, - const bool skip_already_published = false, - const QString &defaultType = ""); - - QString pushPlaylistNotes(const QString ¬es, const QUuid &playlist) { - return pushPlaylistNotesFuture(notes, playlist).result(); - } - QFuture - pushPlaylistNotesFuture(const QString ¬es, const QUuid &playlist); - - QString getPlaylistValidMediaCount(const QUuid &playlist) { - return getPlaylistValidMediaCountFuture(playlist).result(); - } - QFuture getPlaylistValidMediaCountFuture(const QUuid &playlist); - - QString getPlaylistLinkMedia(const QUuid &playlist) { - return getPlaylistLinkMediaFuture(playlist).result(); - } - QFuture getPlaylistLinkMediaFuture(const QUuid &playlist); - - QString requestFileTransfer( - const QVariantList &uuids, - const QString &project, - const QString &src_location, - const QString &dest_location) { - return requestFileTransferFuture(uuids, project, src_location, dest_location) - .result(); - } - QFuture requestFileTransferFuture( - const QVariantList &uuids, - const QString &project, - const QString &src_location, - const QString &dest_location); - - void populateCaches(); - - void syncModelChanges(const QString &name); - - QFuture executeQuery( - const QString &context, - const int project_id, - const QVariant &query, - const bool update_result_model = false); - - QFuture executeQueryNew( - const QString &context, - const int project_id, - const QVariant &query, - const bool update_result_model = false); - - QVariant mergeQueries( - const QVariant &dst, - const QVariant &src, - const bool ignore_duplicates = true) const; - - // value used by query, may map to id. - utility::JsonStore getQueryValue( - const std::string &type, - const utility::JsonStore &value, - const int project_id = -1) const; - void updateQueryValueCache( - const std::string &type, - const utility::JsonStore &data, - const int project_id = -1); - - QString preferredVisual(const QString &category) const { - QString result("SG Movie"); - try { - result = - QStringFromStd(source_selection_.at(StdFromQString(category)).first); - } catch (...) { - } - return result; - } - - QString preferredAudio(const QString &category) const { - QString result("SG Movie"); - try { - result = - QStringFromStd(source_selection_.at(StdFromQString(category)).second); - } catch (...) { - } - return result; - } - - QString flagText(const QString &category) const { - QString result(""); - try { - result = QStringFromStd(flag_selection_.at(StdFromQString(category)).first); - } catch (...) { - } - return result; - } - - QString flagColour(const QString &category) const { - QString result(""); - try { - result = - QStringFromStd(flag_selection_.at(StdFromQString(category)).second); - } catch (...) { - } - return result; - } - - QFuture addDownloadToMediaFuture(const QUuid &uuid); - QString addDownloadToMedia(const QUuid &uuid) { - return addDownloadToMediaFuture(uuid).result(); - } - - private: - utility::JsonStore purgeOldSystem( - const utility::JsonStore &vprefs, const utility::JsonStore &drefs) const; - - void populatePresetModel( - const utility::JsonStore &prefs, - const std::string &path, - ShotgunTreeModel *model, - const bool purge_old = true, - const bool clear_flags = false); - shotgun_client::Text addTextValue( - const std::string &filter, - const std::string &value, - const bool negated = false) const; - - void addTerm( - const int project_id, - const std::string &context, - shotgun_client::FilterBy *qry, - const utility::JsonStore &term); - - std::tuple< - utility::JsonStore, - std::vector, - int, - std::pair, - std::pair> - buildQuery( - const std::string &context, - const int project_id, - const utility::JsonStore &query); - - void loadPresets(const bool purge_old = true); - void flushPreset(const std::string &preset); - utility::JsonStore buildDataFromField(const utility::JsonStore &data); - - void setPreset(const std::string &preset, const utility::JsonStore &data); - utility::JsonStore getPresetData(const std::string &preset); - - void createCustomEntity24Models(const int project_id); - void createShotModels(const int project_id); - void createSequenceModels(const int project_id); - - void handleResult( - const utility::JsonStore &request, - const std::string &model, - const std::string &name); - - bool connected_{false}; - caf::actor backend_; - caf::actor_id backend_id_{0}; - QString backend_str_; - QString name_{"test"}; - - QMap groups_map_; - QMap groups_filter_map_; - QMap sequences_map_; - QMap sequences_tree_map_; - QMap shots_map_; - QMap custom_entity_24_map_; - QMap shots_filter_map_; - QMap playlists_map_; - - std::map preset_update_pending_; - int maximum_result_count_{2500}; - - QString live_link_metadata_string_; - utility::JsonStore live_link_metadata_; - - // map UI value to query value - std::map> query_value_cache_; - // capture default source selection - - std::map> source_selection_; - std::map> flag_selection_; - - history::History> history_; - - QQmlPropertyMap *term_models_{nullptr}; - QQmlPropertyMap *result_models_{nullptr}; - QQmlPropertyMap *preset_models_{nullptr}; - bool disable_flush_{true}; - std::map epoc_map_; - }; - - } // namespace qml -} // namespace ui -} // namespace xstudio diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/shotgun_model_ui.cpp b/src/plugin/data_source/dneg/shotgun/src/qml/shotgun_model_ui.cpp deleted file mode 100644 index 7f98689ac..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/shotgun_model_ui.cpp +++ /dev/null @@ -1,1817 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "shotgun_model_ui.hpp" -#include "../data_source_shotgun.hpp" -#include "xstudio/utility/string_helpers.hpp" -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/global_store/global_store.hpp" -#include "xstudio/atoms.hpp" - -#include -#include -#include - -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::shotgun_client; -using namespace xstudio::ui::qml; -using namespace std::chrono_literals; -using namespace xstudio::global_store; - -namespace { -void dumpNames(const nlohmann::json &jsn, const int depth) { - if (jsn.is_array()) { - for (const auto &item : jsn) { - dumpNames(item, depth); - } - } else { - spdlog::warn("{:>{}} {}", " ", depth * 4, jsn.value("name", "unnamed")); - if (jsn.count("children") and jsn.at("children").is_array()) { - for (const auto &item : jsn.at("children")) { - dumpNames(item, depth + 1); - } - } - } -} -} // namespace - -nlohmann::json ShotgunSequenceModel::sortByName(const nlohmann::json &jsn) { - // this needs - auto result = sort_by(jsn, nlohmann::json::json_pointer("/name")); - for (auto &item : result) { - if (item.count("children")) { - item["children"] = sortByName(item.at("children")); - } - } - - return result; -} - -nlohmann::json ShotgunSequenceModel::flatToTree(const nlohmann::json &src) { - // manipulate data into tree structure. - // spdlog::warn("{}", src.size()); - - auto result = R"([])"_json; - std::map seqs; - - // spdlog::warn("{}", src.dump(2)); - - try { - - if (src.is_array()) { - auto done = false; - - while (not done) { - done = true; - - for (auto seq : src) { - try { - auto id = seq.at("id").get(); - // already logged ? - // if already there then skip - - if (not seqs.count(id)) { - seq["name"] = seq.at("attributes").at("code"); - auto parent_id = seq.at("relationships") - .at("sg_parent") - .at("data") - .at("id") - .get(); - - // no parent add to results. and store pointer to results entry. - if (parent_id == id) { - // spdlog::warn("new root level item {}", - // seq["name"].get()); - - auto &shots = seq["relationships"]["shots"]["data"]; - if (shots.is_array()) - seq["children"] = - sort_by(shots, nlohmann::json::json_pointer("/name")); - else - seq["children"] = R"([])"_json; - - seq["parent_id"] = parent_id; - seq["relationships"].erase("shots"); - seq["relationships"].erase("sg_parent"); - - // store in result - // and pointer to entry. - result.emplace_back(seq); - - seqs.emplace(std::make_pair( - id, - nlohmann::json::json_pointer( - std::string("/") + std::to_string(result.size() - 1)))); - - done = false; - - } else if (seqs.count(parent_id)) { - // parent exists - // spdlog::warn("add to parent {} {}", parent_id, - // seq["name"].get()); - - auto parent_pointer = seqs[parent_id]; - - auto &shots = seq["relationships"]["shots"]["data"]; - if (shots.is_array()) - seq["children"] = - sort_by(shots, nlohmann::json::json_pointer("/name")); - else - seq["children"] = R"([])"_json; - - seq["parent_id"] = parent_id; - seq["relationships"].erase("shots"); - seq["relationships"].erase("sg_parent"); - - result[parent_pointer]["children"].emplace_back(seq); - // spdlog::warn("{}", result[parent_pointer].dump(2)); - - // add path to new entry.. - seqs.emplace(std::make_pair( - id, - parent_pointer / - nlohmann::json::json_pointer( - std::string("/children/") + - std::to_string( - result[parent_pointer]["children"].size() - - 1)))); - done = false; - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - } - - // un parented sequences - auto count = 0; - // unresolved.. - for (auto unseq : src) { - try { - auto id = unseq.at("id").get(); - // already logged ? - if (not seqs.count(id)) { - unseq["name"] = unseq.at("attributes").at("code"); - - auto parent_id = - unseq["relationships"]["sg_parent"]["data"]["id"].get(); - // no parent - auto &shots = unseq["relationships"]["shots"]["data"]; - - spdlog::warn("{} {}", id, parent_id); - - if (shots.is_array()) - unseq["children"] = - sort_by(shots, nlohmann::json::json_pointer("/name")); - else - unseq["children"] = R"([])"_json; - - unseq["parent_id"] = parent_id; - unseq["relationships"].erase("shots"); - unseq["relationships"].erase("sg_parent"); - result.emplace_back(unseq); - seqs.emplace(std::make_pair( - id, - nlohmann::json::json_pointer( - std::string("/") + std::to_string(result.size() - 1)))); - count++; - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - if (count) - spdlog::warn("{} unresolved sequences.", count); - } - - result = sortByName(result); - // dumpNames(result, 0); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - // sort results.. - - // spdlog::warn("{}", result.dump(2)); - - return result; -} - - -QVariant ShotgunSequenceModel::data(const QModelIndex &index, int role) const { - auto result = QVariant(); - - try { - const auto &j = indexToFullData(index); - - switch (role) { - case JSONTreeModel::Roles::JSONRole: - result = QVariantMapFromJson(j); - break; - - case JSONTreeModel::Roles::JSONTextRole: - result = QString::fromStdString(j.dump(2)); - break; - - case Roles::idRole: - try { - if (j.at("id").is_number()) - result = j.at("id").get(); - else - result = QString::fromStdString(j.at("id").get()); - } catch (...) { - } - break; - - case Roles::typeRole: - try { - result = QString::fromStdString(j.at("type").get()); - } catch (...) { - } - break; - - case Roles::nameRole: - case Qt::DisplayRole: - try { - if (j.count("name")) - result = QString::fromStdString(j.at("name")); - else if (j.count("attributes")) - result = QString::fromStdString(j.at("attributes").at("code")); - } catch (...) { - } - break; - - default: - break; - } - } catch (const std::exception &err) { - spdlog::warn("{} {} {} {}", __PRETTY_FUNCTION__, err.what(), role, index.row()); - } - - return result; -} - - -bool ShotgunFilterModel::filterAcceptsRow( - int source_row, const QModelIndex &source_parent) const { - - static const QString qtrue("true"); - static const QString qfalse("false"); - // check level - if (not selection_filter_.empty() and sourceModel()) { - QModelIndex index = sourceModel()->index(source_row, 0, source_parent); - if (not selection_filter_.contains(index)) - return false; - } - - if (not roleFilterMap_.empty() and sourceModel()) { - QModelIndex index = sourceModel()->index(source_row, 0, source_parent); - for (const auto &[k, v] : roleFilterMap_) { - if (not v.isEmpty()) { - if (v == "None" and k == ShotgunListModel::Roles::pipelineStepRole) - continue; - try { - auto qv = sourceModel()->data(index, k).toString(); - - if (v == qtrue or v == qfalse) { - if (v == qtrue and not sourceModel()->data(index, k).toBool()) - return false; - else if (v == qfalse and sourceModel()->data(index, k).toBool()) - return false; - - } else if (v != qv) - return false; - } catch (...) { - } - } - } - } - return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); -} - -bool ShotgunFilterModel::lessThan( - const QModelIndex &source_left, const QModelIndex &source_right) const { - auto left_selected = selection_sort_.contains(source_left); - auto right_selected = selection_sort_.contains(source_right); - - if (left_selected and not right_selected) - return sortOrder() == Qt::AscendingOrder; - - if (not left_selected and right_selected) - return sortOrder() == Qt::DescendingOrder; - - return QSortFilterProxyModel::lessThan(source_left, source_right); -} - -QString ShotgunFilterModel::getRoleFilter(const QString &role) const { - auto id = getRoleId(role); - QString result; - - if (roleFilterMap_.count(id)) - result = roleFilterMap_.at(id); - - return result; -} - -void ShotgunFilterModel::setRoleFilter(const QString &filter, const QString &role) { - auto id = getRoleId(role); - if (not roleFilterMap_.count(id) or roleFilterMap_.at(id) != filter) { - roleFilterMap_[id] = filter; - invalidateFilter(); - } -} - -int ShotgunFilterModel::search(const QVariant &value, const QString &role, const int start) { - auto smodel = dynamic_cast(sourceModel()); - int role_id = -1; - auto row = -1; - - QHashIterator it(smodel->roleNames()); - if (it.findNext(role.toUtf8())) { - role_id = it.key(); - } - - auto indexes = match(createIndex(start, 0), role_id, value); - - if (not indexes.empty()) - row = indexes[0].row(); - - return row; -} - -QVariant ShotgunFilterModel::get(int row, const QString &role) { - // map row to source.. - auto src_index = mapToSource(index(row, 0)); - auto smodel = dynamic_cast(sourceModel()); - return smodel->get(src_index.row(), role); -} - -void ShotgunListModel::populate(const utility::JsonStore &data) { - // dirty update.. - beginResetModel(); - data_ = data; - endResetModel(); - emit lengthChanged(); -} - -utility::JsonStore ShotgunListModel::getQueryValue( - const std::string &type, const utility::JsonStore &value, const int project_id) const { - // look for map - auto _type = type; - auto mapped_value = utility::JsonStore(); - - if (query_value_cache_ == nullptr) - return mapped_value; - - if (_type == "Author" || _type == "Recipient") - _type = "User"; - - if (project_id != -1) - _type += "-" + std::to_string(project_id); - - try { - auto val = value.get(); - if (query_value_cache_->count(_type)) { - if (query_value_cache_->at(_type).count(val)) { - mapped_value = query_value_cache_->at(_type).at(val); - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {} {} {}", _type, __PRETTY_FUNCTION__, err.what(), value.dump(2)); - } - - return mapped_value; -} - -void ShotgunListModel::append(const QVariant &data) { - auto jsn = mapFromValue(data); - - // no exact duplicates.. - for (const auto &i : data_) - if (i == jsn) - return; - - auto rows = rowCount(); - beginInsertRows(QModelIndex(), rows, rows); - data_.push_back(jsn); - endInsertRows(); -} - - -int ShotgunListModel::search(const QVariant &value, const QString &role, const int start) { - int role_id = -1; - auto row = -1; - - QHashIterator it(roleNames()); - if (it.findNext(role.toUtf8())) { - role_id = it.key(); - } - - auto indexes = match(createIndex(start, 0), role_id, value, 1, Qt::MatchFixedString); - - if (not indexes.empty()) - row = indexes[0].row(); - - return row; -} - -QVariant ShotgunListModel::get(int row, const QString &role) { - int role_id = -1; - - QHashIterator it(roleNames()); - if (it.findNext(role.toUtf8())) { - role_id = it.key(); - } - - return data(createIndex(row, 0), role_id); -} - -QVariant ShotgunListModel::data(const QModelIndex &index, int role) const { - auto result = QVariant(); - try { - if (index.row() > -1 and index.row() < static_cast(data_.size())) { - const auto &data = data_[index.row()]; - - switch (role) { - case Roles::nameRole: - case Qt::DisplayRole: - if (data.count("nameRole")) - result = QString::fromStdString(data.at("nameRole")); - else if (data.count("name")) - result = QString::fromStdString(data.at("name")); - else - result = QString::fromStdString( - data.at("attributes") - .value("name", data.at("attributes").value("code", ""))); - break; - - case Roles::idRole: - try { - if (data.at("id").is_number()) - result = data.at("id").get(); - else - result = QString::fromStdString(data.at("id").get()); - } catch (...) { - } - break; - - case Roles::JSONRole: - case Roles::detailRole: - result = QVariantMapFromJson(data); - break; - - case Roles::JSONTextRole: - result = QString::fromStdString(data.dump(2)); - break; - - case Roles::loginRole: - result = QString::fromStdString( - data.at("attributes").at("login").get()); - break; - - case Roles::createdByRole: - case Roles::authorRole: - result = QString::fromStdString( - data.at("relationships").at("created_by").at("data").value("name", "")); - break; - - case Roles::projectRole: - result = QString::fromStdString( - data.at("relationships").at("project").at("data").value("name", "")); - break; - - case Roles::projectIdRole: - result = data.at("relationships").at("project").at("data").value("id", 0); - break; - - case Roles::createdDateRole: - result = QDateTime::fromString( - QStringFromStd(data.at("attributes").value("created_at", "")), Qt::ISODate); - break; - - case Roles::thumbRole: - result = "qrc:/feather_icons/film.svg"; - break; - - default: - break; - } - } - } catch (...) { - } - - return result; -} - -// { -// "attributes": { -// "code": "033_jts_seq" -// }, -// "id": 288766, -// "links": { -// "self": "/api/v1/entity/sequences/288766" -// }, -// "relationships": { -// "shots": { -// "data": [ -// { -// "id": 1079204, -// "name": "033_jts_9250", -// "type": "Shot" -// }, - - -JsonStore ShotModel::getSequence(const int project_id, const int shot_id) const { - if (sequence_map_) { - if (sequence_map_->count(project_id)) { - auto jsn = (*sequence_map_)[project_id]->modelData(); - - for (const auto &i : jsn) { - for (const auto &j : i.at("relationships").at("shots").at("data")) { - if (j.at("id") == shot_id) { - JsonStore result(R"({"attributes": {"code":null}, "id": 0})"_json); - result["id"] = i.at("id"); - result["attributes"]["code"] = i.at("attributes").at("code"); - return result; - } - } - } - } - } - - return JsonStore(); -} - -QVariant ReferenceModel::data(const QModelIndex &index, int role) const { - auto result = QVariant(); - - try { - if (index.row() > -1 and index.row() < static_cast(data_.size())) { - const auto &data = data_[index.row()]; - - switch (role) { - case Roles::resultTypeRole: - result = "Reference"; - break; - - default: - return ShotModel::data(index, role); - break; - } - } - } catch (...) { - } - - return result; -} - -QVariant MediaActionModel::data(const QModelIndex &index, int role) const { - auto result = QVariant(); - - try { - if (index.row() > -1 and index.row() < static_cast(data_.size())) { - const auto &data = data_[index.row()]; - - switch (role) { - case Roles::resultTypeRole: - result = "MediaAction"; - break; - - default: - return ShotModel::data(index, role); - break; - } - } - } catch (...) { - } - - return result; -} - -QVariant ShotModel::data(const QModelIndex &index, int role) const { - auto result = QVariant(); - - try { - if (index.row() > -1 and index.row() < static_cast(data_.size())) { - const auto &data = data_[index.row()]; - - switch (role) { - - case submittedToDailiesRole: - if (not data.at("attributes").at("sg_submit_dailies").is_null()) - result = QDateTime::fromString( - QStringFromStd(data.at("attributes").at("sg_submit_dailies")), - Qt::ISODate); - else if (not data.at("attributes").at("sg_submit_dailies_chn").is_null()) - result = QDateTime::fromString( - QStringFromStd(data.at("attributes").at("sg_submit_dailies_chn")), - Qt::ISODate); - else if (not data.at("attributes").at("sg_submit_dailies_mtl").is_null()) - result = QDateTime::fromString( - QStringFromStd(data.at("attributes").at("sg_submit_dailies_mtl")), - Qt::ISODate); - else if (not data.at("attributes").at("sg_submit_dailies_van").is_null()) - result = QDateTime::fromString( - QStringFromStd(data.at("attributes").at("sg_submit_dailies_van")), - Qt::ISODate); - else if (not data.at("attributes").at("sg_submit_dailies_mum").is_null()) - result = QDateTime::fromString( - QStringFromStd(data.at("attributes").at("sg_submit_dailies_mum")), - Qt::ISODate); - break; - - case Roles::pipelineStepRole: - result = - QString::fromStdString(data.at("attributes").value("sg_pipeline_step", "")); - break; - case Roles::frameRangeRole: - result = QString::fromStdString(data.at("attributes").value("frame_range", "")); - break; - case Roles::versionRole: - result = data.at("attributes").value("sg_dneg_version", 0); - break; - - case Roles::pipelineStatusRole: - result = - QString::fromStdString(data.at("attributes").value("sg_status_list", "")); - break; - - case Roles::pipelineStatusFullRole: - result = QString::fromStdString( - getQueryValue( - "Pipeline Status", data.at("attributes").at("sg_status_list"), -1) - .get()); - break; - - case Roles::productionStatusRole: - result = QString::fromStdString( - data.at("attributes").value("sg_production_status", "")); - break; - - case Roles::dateSubmittedToClientRole: - result = QDateTime::fromString( - QStringFromStd( - data.at("attributes").value("sg_date_submitted_to_client", "")), - Qt::ISODate); - break; - - case Roles::shotRole: - if (data.at("relationships").at("entity").at("data").at("type") == "Shot") - result = QString::fromStdString( - data.at("relationships").at("entity").at("data").at("name")); - break; - - case Roles::assetRole: - if (data.at("relationships").at("entity").at("data").at("type") == "Asset") - result = QString::fromStdString( - data.at("relationships").at("entity").at("data").at("name")); - break; - - case Roles::movieRole: - result = QString::fromStdString(data.at("attributes").at("sg_path_to_movie")); - break; - - case Roles::sequenceRole: { - if (data.at("relationships").at("entity").at("data").value("type", "") == - "Sequence") { - result = QString::fromStdString( - data.at("relationships").at("entity").at("data").value("name", "")); - } else { - auto seq_data = getSequence( - data.at("relationships").at("project").at("data").value("id", 0), - data.at("relationships").at("entity").at("data").value("id", 0)); - result = QString::fromStdString(seq_data.at("attributes").at("code")); - } - } break; - - case Roles::frameSequenceRole: - result = QString::fromStdString(data.at("attributes").at("sg_path_to_frames")); - break; - - case Roles::noteCountRole: - try { - result = static_cast( - data.at("relationships").at("notes").at("data").size()); - } catch (...) { - result = static_cast(0); - } - break; - - case Roles::onSiteMum: - try { - result = data.at("attributes").at("sg_on_disk_mum") == "Full" ? 2 - : data.at("attributes").at("sg_on_disk_mum") == "Partial" ? 1 - : 0; - } catch (...) { - result = 0; - } - break; - case Roles::onSiteMtl: - try { - result = data.at("attributes").at("sg_on_disk_mtl") == "Full" ? 2 - : data.at("attributes").at("sg_on_disk_mtl") == "Partial" ? 1 - : 0; - } catch (...) { - result = 0; - } - break; - case Roles::onSiteVan: - try { - result = data.at("attributes").at("sg_on_disk_van") == "Full" ? 2 - : data.at("attributes").at("sg_on_disk_van") == "Partial" ? 1 - : 0; - } catch (...) { - result = 0; - } - break; - case Roles::onSiteChn: - try { - result = data.at("attributes").at("sg_on_disk_chn") == "Full" ? 2 - : data.at("attributes").at("sg_on_disk_chn") == "Partial" ? 1 - : 0; - } catch (...) { - result = 0; - } - break; - case Roles::onSiteLon: - try { - result = data.at("attributes").at("sg_on_disk_lon") == "Full" ? 2 - : data.at("attributes").at("sg_on_disk_lon") == "Partial" ? 1 - : 0; - } catch (...) { - result = 0; - } - break; - - case Roles::onSiteSyd: - try { - result = data.at("attributes").at("sg_on_disk_syd") == "Full" ? 2 - : data.at("attributes").at("sg_on_disk_syd") == "Partial" ? 1 - : 0; - } catch (...) { - result = 0; - } - break; - - - case Roles::twigNameRole: - result = - QString::fromStdString(data.at("attributes").value("sg_twig_name", "")); - break; - - case Roles::tagRole: { - auto tmp = QStringList(); - for (const auto &i : data.at("relationships").at("tags").at("data")) { - auto name = QStringFromStd(i.at("name").get()); - name.replace(QRegExp("\\.REFERENCE$"), ""); - tmp.append(name); - } - result = tmp; - } break; - - case Roles::twigTypeRole: - result = - QString::fromStdString(data.at("attributes").value("sg_twig_type", "")); - break; - - case Roles::stalkUuidRole: - result = - QUuidFromUuid(utility::Uuid(data.at("attributes").at("sg_ivy_dnuuid"))); - break; - - case Roles::URLRole: - result = QStringFromStd(std::string( - fmt::format("http://shotgun/detail/Version/{}", data.at("id").get()))); - break; - - case Roles::resultTypeRole: - result = "Shot"; - break; - - case Roles::thumbRole: - if (not data.at("attributes").at("image").is_null()) - result = QStringFromStd(fmt::format( - "image://shotgun/thumbnail/{}/{}", - data.value("type", ""), - data.value("id", 0))); - break; - - default: - return ShotgunListModel::data(index, role); - break; - } - } - } catch (const std::exception &err) { - // spdlog::warn("{}", err.what()); - } - - return result; -} - -QVariant PlaylistModel::data(const QModelIndex &index, int role) const { - auto result = QVariant(); - - try { - if (index.row() > -1 and index.row() < static_cast(data_.size())) { - const auto &data = data_[index.row()]; - - switch (role) { - case Roles::itemCountRole: - try { - result = static_cast( - data.at("relationships").at("versions").at("data").size()); - } catch (...) { - result = static_cast(-1); - } - break; - - case Roles::typeRole: - result = QString::fromStdString(data.at("attributes").value("sg_type", "")); - break; - - case Roles::departmentRole: - result = QString::fromStdString(data.at("relationships") - .at("sg_department_unit") - .at("data") - .value("name", "")); - break; - - case Roles::siteRole: - result = QString::fromStdString(data.at("attributes").value("sg_location", "")); - break; - - case Roles::noteCountRole: - try { - result = static_cast( - data.at("relationships").at("notes").at("data").size()); - } catch (...) { - result = static_cast(0); - } - break; - - case Roles::resultTypeRole: - result = "Playlist"; - break; - - case Roles::createdDateRole: - result = QDateTime::fromString( - QStringFromStd(data.at("attributes").value("created_at", "")), Qt::ISODate); - break; - - case Roles::URLRole: - result = QStringFromStd(std::string(fmt::format( - "http://shotgun/detail/Playlist/{}", data.at("id").get()))); - break; - - case Roles::thumbRole: - result = QStringFromStd(fmt::format( - "image://shotgun/thumbnail/{}/{}", - data.value("type", ""), - data.value("id", 0))); - break; - - default: - return ShotgunListModel::data(index, role); - break; - } - } - } catch (...) { - } - - return result; -} - -QVariant NoteModel::data(const QModelIndex &index, int role) const { - auto result = QVariant(); - - try { - if (index.row() > -1 and index.row() < static_cast(data_.size())) { - const auto &data = data_[index.row()]; - - switch (role) { - case Roles::nameRole: - case Qt::DisplayRole: { - for (const auto &i : data.at("relationships").at("note_links").at("data")) { - if (i.at("type") == "Version") { - result = QStringFromStd(i.at("name")); - break; - } else if (i.at("type") == "Shot") { - result = QStringFromStd(i.at("name")); - } else if (result.isNull() and i.at("type") == "Playlist") { - result = QStringFromStd(i.at("name")); - } - } - } break; - - case Roles::subjectRole: - result = QString::fromStdString(data.at("attributes").value("subject", "")); - break; - - case Roles::contentRole: - result = QString::fromStdString(data.at("attributes").value("content", "")); - break; - - case Roles::siteRole: - result = QString::fromStdString(data.at("attributes").value("sg_location", "")); - break; - - case Roles::pipelineStepRole: - result = - QString::fromStdString(data.at("attributes").value("sg_pipeline_step", "")); - break; - - case Roles::noteTypeRole: - result = - QString::fromStdString(data.at("attributes").value("sg_note_type", "")); - break; - - case Roles::clientNoteRole: - result = data.at("attributes").value("client_note", false); - break; - - case Roles::URLRole: - result = QStringFromStd(std::string( - fmt::format("http://shotgun/detail/Note/{}", data.at("id").get()))); - break; - - case Roles::addressingRole: { - auto tmp = QStringList(); - for (const auto &i : data.at("relationships").at("addressings_to").at("data")) - tmp.append(QStringFromStd(i.at("name").get())); - result = tmp; - } break; - - case Roles::artistRole: - result = QString::fromStdString(data.at("attributes").value("sg_artist", "")); - break; - - case Roles::shotNameRole: - for (const auto &i : data.at("relationships").at("note_links").at("data")) { - if (i.at("type").get() == "Shot") { - result = QString::fromStdString(i.at("name").get()); - break; - } - } - break; - - case Roles::twigNameRole: - for (const auto &i : data.at("relationships").at("note_links").at("data")) { - if (i.at("type").get() == "Version") { - static std::regex remove_version_re(R"(_[VvSs]\d+$)"); - result = QString::fromStdString(std::regex_replace( - i.at("name").get(), remove_version_re, "")); - break; - } - } - break; - - case Roles::playlistNameRole: - for (const auto &i : data.at("relationships").at("note_links").at("data")) { - if (i.at("type").get() == "Playlist") { - result = QString::fromStdString(i.at("name").get()); - break; - } - } - break; - - case Roles::linkedVersionsRole: { - auto v = QVariantList(); - for (const auto &i : data.at("relationships").at("note_links").at("data")) { - if (i.at("type").get() == "Version") { - v.append(QVariant::fromValue(i.at("id").get())); - break; - } - } - result = v; - } break; - - case Roles::versionNameRole: - for (const auto &i : data.at("relationships").at("note_links").at("data")) { - if (i.at("type").get() == "Version") { - result = QString::fromStdString(i.at("name").get()); - break; - } - } - break; - - case Roles::resultTypeRole: - result = "Note"; - break; - - case Roles::thumbRole: - for (const auto &i : data.at("relationships").at("note_links").at("data")) { - if (i.at("type") == "Version") { - result = QStringFromStd(fmt::format( - "image://shotgun/thumbnail/{}/{}", - i.value("type", ""), - i.value("id", 0))); - break; - } else if (i.at("type") == "Shot") { - result = QStringFromStd(fmt::format( - "image://shotgun/thumbnail/{}/{}", - i.value("type", ""), - i.value("id", 0))); - } else if (result.isNull() and i.at("type") == "Playlist") { - result = QStringFromStd(fmt::format( - "image://shotgun/thumbnail/{}/{}", - i.value("type", ""), - i.value("id", 0))); - } - } - break; - - default: - return ShotgunListModel::data(index, role); - break; - } - } - } catch (const std::exception & /*err*/) { - // spdlog::warn("{}", err.what()); - } - - return result; -} - -QVariant EditModel::data(const QModelIndex &index, int role) const { - if (role == Roles::resultTypeRole) - return "Edit"; - - return ShotgunListModel::data(index, role); -} - - -void ShotgunTreeModel::populate(const utility::JsonStore &data) { - // dirty update.. - setModelData(data); - - setActivePreset(); - emit lengthChanged(); - - checkForActiveFilter(); - checkForActiveLiveLink(); -} - -void ShotgunTreeModel::checkForActiveLiveLink() { - try { - bool active = false; - if (active_preset_ != -1 and active_preset_ < static_cast(data_.size())) { - - auto active_node = data_.begin(); - std::advance(active_node, active_preset_); - - for (const auto &j : *active_node) { - if (j.data().value("enabled", false) and j.data().value("livelink", false)) { - active = true; - break; - } - } - } - - if (active != has_active_live_link_) { - has_active_live_link_ = active; - emit hasActiveLiveLinkChanged(); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } -} - -void ShotgunTreeModel::checkForActiveFilter() { - - try { - bool active = false; - if (active_preset_ != -1 and active_preset_ < static_cast(data_.size())) { - - auto active_node = data_.begin(); - std::advance(active_node, active_preset_); - - for (const auto &j : *active_node) { - if (j.data().at("enabled")) { - active = true; - break; - } - } - } - - if (active != has_active_filter_) { - has_active_filter_ = active; - emit hasActiveFilterChanged(); - } - } catch (...) { - } -} - - -bool ShotgunTreeModel::removeRows(int row, int count, const QModelIndex &parent) { - auto result = JSONTreeModel::removeRows(row, count, parent); - - if (result) { - if (active_preset_ >= row) { - setActivePreset(std::max(active_preset_ - count, -1)); - } - - checkForActiveFilter(); - checkForActiveLiveLink(); - } - - return result; -} - -bool ShotgunTreeModel::insert(int row, const QModelIndex &parent, const QVariant &data) { - bool result = insertRows(row, 1, parent); - - if (result) { - if (not data.isNull()) { - setData(index(row, 0, parent), data, JSONTreeModel::Roles::JSONRole); - } - - checkForActiveFilter(); - checkForActiveLiveLink(); - } - - return result; -} - -void ShotgunTreeModel::clearLoaded() { - setActivePreset(); - checkForActiveFilter(); - checkForActiveLiveLink(); -} - -void ShotgunTreeModel::setType(const int row, const QString &type) { - if (row < static_cast(data_.size())) { - set(row, type, "typeRole", QModelIndex()); - } -} - - -void ShotgunTreeModel::clearLiveLinks() { - try { - auto i_ind = 0; - for (const auto &i : data_) { - auto j_ind = 0; - for (const auto &j : i) { - if (j.data().value("livelink", false)) { - set(j_ind, "", "argRole", index(i_ind, 0, QModelIndex())); - } - j_ind++; - } - i_ind++; - } - - // for (size_t i = 0; i < data_.size(); i++) { - // for (size_t j = 0; j < data_.at(i).at(children_).size(); j++) { - // if (data_.at(i).at(children_).at(j).value("livelink", false)) - // set(j, "", "argRole", index(i, 0, QModelIndex())); - // } - // } - } catch (...) { - } -} - -void ShotgunTreeModel::clearExpanded() { - for (size_t i = 0; i < data_.size(); i++) { - set(i, false, "expandedRole", QModelIndex()); - } -} - -QVariant ShotgunTreeModel::get(int row, const QModelIndex &parent, const QString &role) const { - int role_id = -1; - - QHashIterator it(roleNames()); - if (it.findNext(role.toUtf8())) { - role_id = it.key(); - } - - return data(index(row, 0, parent), role_id); -} - -QVariant ShotgunTreeModel::get(const QModelIndex &index, const QString &role) const { - int role_id = -1; - - QHashIterator it(roleNames()); - if (it.findNext(role.toUtf8())) { - role_id = it.key(); - } - - return data(index, role_id); -} - - -int ShotgunTreeModel::search( - const QVariant &value, const QString &role, const QModelIndex &parent, const int start) { - int role_id = -1; - auto row = -1; - - QHashIterator it(roleNames()); - if (it.findNext(role.toUtf8())) { - role_id = it.key(); - } - - auto indexes = match(index(start, 0, parent), role_id, value); - - if (not indexes.empty()) - row = indexes[0].row(); - - return row; -} - -bool ShotgunTreeModel::set( - int row, const QVariant &value, const QString &role, const QModelIndex &parent) { - int role_id = -1; - - QHashIterator it(roleNames()); - if (it.findNext(role.toUtf8())) { - role_id = it.key(); - } - - return setData(index(row, 0, parent), value, role_id); -} - -JsonStore ShotgunTreeModel::getSequence(const int project_id, const int shot_id) const { - if (sequence_map_) { - if (sequence_map_->count(project_id)) { - auto jsn = (*sequence_map_)[project_id]->modelData(); - - for (const auto &i : jsn) { - for (const auto &j : i.at("relationships").at("shots").at("data")) { - if (j.at("id") == shot_id) { - JsonStore result(R"({"attributes": {"code":null}, "id": 0})"_json); - result["id"] = i.at("id"); - result["attributes"]["code"] = i.at("attributes").at("code"); - return result; - } - } - } - } - } - - return JsonStore(); -} - -QVariant ShotgunTreeModel::data(const QModelIndex &index, int role) const { - auto result = QVariant(); - - try { - const auto &j = indexToData(index); - - switch (role) { - case JSONTreeModel::Roles::JSONRole: - result = QVariantMapFromJson(indexToFullData(index)); - break; - - case JSONTreeModel::Roles::JSONTextRole: - result = QString::fromStdString(indexToFullData(index).dump(2)); - break; - - case Roles::enabledRole: - result = j.at("enabled").get(); - break; - - case Roles::termRole: - result = QString::fromStdString(j.at("term")); - break; - - case Roles::liveLinkRole: - try { - result = j.at("livelink").get(); - } catch (...) { - } - break; - - case Roles::negationRole: - try { - result = j.at("negated").get(); - } catch (...) { - } - break; - - case Roles::argRole: - result = QString::fromStdString(j.at("value")); - break; - - case Roles::detailRole: - if (j.count("detail")) - result = QVariantMapFromJson(j.at("detail")); - break; - - case Roles::idRole: - try { - if (j.at("id").is_number()) - result = j.at("id").get(); - else - result = QString::fromStdString(j.at("id").get()); - } catch (...) { - } - break; - - case Roles::nameRole: - case Qt::DisplayRole: - result = QString::fromStdString(j.at("name")); - break; - - case Roles::typeRole: - result = QString::fromStdString(j.value("type", "user")); - break; - - case Roles::expandedRole: - result = j.at("expanded").get(); - break; - - case Roles::queriesRole: - // return index.. - result = index; - break; - - default: - break; - } - } catch (const std::exception &err) { - - spdlog::warn( - "{} {} {} {} {}", - __PRETTY_FUNCTION__, - err.what(), - role, - index.row(), - index.internalId()); - } - - return result; -} - - -bool ShotgunTreeModel::setData(const QModelIndex &index, const QVariant &value, int role) { - bool result = false; - bool sync_change = true; - - QVector roles({role}); - - try { - nlohmann::json &j = indexToData(index); - result = true; - switch (role) { - - case JSONTreeModel::Roles::JSONRole: { - // more involved.. - nlohmann::json jval; - - if (std::string(value.typeName()) == "QJSValue") { - jval = nlohmann::json::parse( - QJsonDocument::fromVariant(value.value().toVariant()) - .toJson(QJsonDocument::Compact) - .constData()); - } else { - jval = nlohmann::json::parse(QJsonDocument::fromVariant(value) - .toJson(QJsonDocument::Compact) - .constData()); - } - - // we now need to update / replace the TreeNode.. - auto new_node = json_to_tree(jval, "queries"); - auto old_node = indexToTree(index); - // remove old children - old_node->clear(); - // replace data.. - old_node->data() = new_node.data(); - // copy children - old_node->splice(old_node->end(), new_node.base()); - - refreshLiveLinks(); - checkForActiveFilter(); - checkForActiveLiveLink(); - roles.clear(); - } break; - - case Roles::enabledRole: - j["enabled"] = value.toBool(); - checkForActiveFilter(); - checkForActiveLiveLink(); - break; - - case Roles::liveLinkRole: - j["livelink"] = value.toBool(); - if (value.toBool()) - updateLiveLink(index); - - checkForActiveLiveLink(); - break; - - case Roles::negationRole: - j["negated"] = value.toBool(); - break; - - case Roles::idRole: - if (value.type() == QVariant::String) { - j["id"] = value.toByteArray(); - } else { - j["id"] = value.toInt(); - } - break; - - case Roles::termRole: - if (j.value("livelink", false) or j.value("dynamic", false)) - sync_change = false; - j["term"] = value.toByteArray(); - checkForActiveLiveLink(); - break; - - case Roles::argRole: - if (j.value("livelink", false) or j.value("dynamic", false)) - sync_change = false; - - j["value"] = value.toByteArray(); - checkForActiveLiveLink(); - break; - - case Roles::detailRole: - if (value.isNull()) - j["detail"] = nullptr; - else - j["detail"] = nlohmann::json::parse( - QJsonDocument::fromVariant(value.value().toVariant()) - .toJson(QJsonDocument::Compact) - .constData()); - break; - - case Roles::nameRole: - j["name"] = value.toByteArray(); - break; - - case Roles::typeRole: - j["type"] = value.toByteArray(); - break; - - case Roles::expandedRole: - sync_change = false; - j["expanded"] = value.toBool(); - break; - - default: - result = false; - break; - } - } catch (const std::exception &err) { - spdlog::warn( - "{} {} {} {}", - __PRETTY_FUNCTION__, - err.what(), - role, - StdFromQString(value.toString())); - result = false; - } - - if (result) { - // ignore these state changes. We don't want to sync these to disk. - if (sync_change) - emit ShotgunTreeModel::modelChanged(); - - emit dataChanged(index, index, roles); - } - - return result; -} - -void ShotgunTreeModel::updateLiveLinks(const utility::JsonStore &data) { - live_link_data_ = data; - - auto shot = QStringFromStd( - applyLiveLinkValue(JsonStore(R"({"term":"Shot"})"_json), live_link_data_)); - auto sequence = QStringFromStd( - applyLiveLinkValue(JsonStore(R"({"term":"Sequence"})"_json), live_link_data_)); - - if (active_shot_ != shot) { - active_shot_ = shot; - emit activeShotChanged(); - } - - if (active_seq_ != sequence) { - active_seq_ = sequence; - emit activeSeqChanged(); - } - - refreshLiveLinks(); -} - -void ShotgunTreeModel::refreshLiveLinks() { - - try { - auto i_ind = 0; - for (const auto &i : data_) { - auto j_ind = 0; - for (const auto &j : i) { - if (j.data().value("livelink", false)) { - updateLiveLink(index(j_ind, 0, index(i_ind, 0, QModelIndex()))); - } - j_ind++; - } - i_ind++; - } - } catch (...) { - } -} - -QVariant ShotgunTreeModel::applyLiveLink(const QVariant &preset, const QVariant &livelink) { - JsonStore result; - - try { - auto preset_jsn = JsonStore(nlohmann::json::parse( - QJsonDocument::fromVariant(preset.value().toVariant()) - .toJson(QJsonDocument::Compact) - .constData())); - - auto livelink_jsn = - JsonStore(nlohmann::json::parse(StdFromQString(livelink.value()))); - - result = applyLiveLink(preset_jsn, livelink_jsn); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - return preset; - } - - return QVariantMapFromJson(result); -} - -int ShotgunTreeModel::getProjectId(const QVariant &livelink) const { - int result = -1; - - try { - auto livelink_jsn = - JsonStore(nlohmann::json::parse(StdFromQString(livelink.value()))); - - result = livelink_jsn.at("metadata") - .at("shotgun") - .at("version") - .at("relationships") - .at("project") - .at("data") - .at("id") - .get(); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return result; -} - - -JsonStore ShotgunTreeModel::applyLiveLink(const JsonStore &preset, const JsonStore &livelink) { - JsonStore result = preset; - - auto shot = - QStringFromStd(applyLiveLinkValue(JsonStore(R"({"term":"Shot"})"_json), livelink)); - auto sequence = - QStringFromStd(applyLiveLinkValue(JsonStore(R"({"term":"Sequence"})"_json), livelink)); - - if (active_shot_ != shot) { - active_shot_ = shot; - emit activeShotChanged(); - } - - if (active_seq_ != sequence) { - active_seq_ = sequence; - emit activeSeqChanged(); - } - - try { - if (not result.is_null()) { - for (int j = 0; j < static_cast(result.at(children_).size()); j++) { - if (result.at(children_).at(j).value("livelink", false)) { - result[children_][j]["value"] = - applyLiveLinkValue(result.at(children_).at(j), livelink); - } - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return result; -} - -JsonStore -ShotgunTreeModel::applyLiveLinkValue(const JsonStore &query, const JsonStore &livelink) { - JsonStore result(""); - - try { - if (not query.is_null() and not livelink.is_null()) { - auto term = query.value("term", ""); - - if (livelink.contains(json::json_pointer("/metadata/shotgun/version"))) { - if (term == "Version Name") { - result = livelink.at( - json::json_pointer("/metadata/shotgun/version/attributes/code")); - } else if (term == "Older Version" or term == "Newer Version") { - auto val = livelink - .at(json::json_pointer( - "/metadata/shotgun/version/attributes/sg_dneg_version")) - .get(); - result = nlohmann::json(std::to_string(val)); - } else if (term == "Author" or term == "Recipient") { - result = livelink.at(json::json_pointer( - "/metadata/shotgun/version/relationships/user/data/name")); - } else if (term == "Shot") { - result = livelink.at(json::json_pointer( - "/metadata/shotgun/version/relationships/entity/data/name")); - } else if (term == "Twig Name") { - result = nlohmann::json( - std::string("^") + - livelink - .at(json::json_pointer( - "/metadata/shotgun/version/attributes/sg_twig_name")) - .get() + - std::string("$")); - } else if (term == "Pipeline Step") { - result = livelink.at(json::json_pointer( - "/metadata/shotgun/version/attributes/sg_pipeline_step")); - } else if (term == "Twig Type") { - result = livelink.at(json::json_pointer( - "/metadata/shotgun/version/attributes/sg_twig_type")); - } else if (term == "Sequence") { - auto type = livelink.at(json::json_pointer( - "/metadata/shotgun/version/relationships/entity/data/type")); - if (type == "Sequence") { - result = livelink.at(json::json_pointer( - "/metadata/shotgun/version/relationships/entity/data/name")); - } else { - auto project_id = - livelink - .at(json::json_pointer( - "/metadata/shotgun/version/relationships/project/data/id")) - .get(); - auto shot_id = - livelink - .at(json::json_pointer( - "/metadata/shotgun/version/relationships/entity/data/id")) - .get(); - auto seq_data = getSequence(project_id, shot_id); - if (not seq_data.is_null()) - result = seq_data.at("attributes").at("code"); - } - } - } - } - } catch (const std::exception &err) { - spdlog::warn( - "{} {} {} {}", __PRETTY_FUNCTION__, err.what(), query.dump(2), livelink.dump(2)); - } - - return result; -} - -void ShotgunTreeModel::setActivePreset(const int row) { - if (active_preset_ != row) { - active_preset_ = row; - checkForActiveFilter(); - checkForActiveLiveLink(); - - emit activePresetChanged(); - if (active_preset_ >= 0) { - try { - if (active_preset_ < static_cast(data_.size())) { - auto row = data_.begin(); - std::advance(row, active_preset_); - if (not row->empty()) { - auto jsn = row->front().data(); - if (jsn.at("term") == "Shot" and jsn.value("livelink", false) and - active_shot_ != QStringFromStd(jsn.at("value"))) { - active_shot_ = QStringFromStd(jsn.at("value")); - emit activeShotChanged(); - } else if ( - jsn.at("term") == "Sequence" and jsn.value("livelink", false) and - active_seq_ != QStringFromStd(jsn.at("value"))) { - active_seq_ = QStringFromStd(jsn.at("value")); - emit activeSeqChanged(); - } - } - } - } catch (...) { - } - } - } -} - -void ShotgunTreeModel::updateLiveLink(const QModelIndex &index) { - // spdlog::warn("updateLiveLink {}", live_link_data_.dump(2)); - try { - auto jsn = indexToData(index); - auto value = jsn.at("value"); - auto result = applyLiveLinkValue(jsn, live_link_data_); - - if (result != value) { - set(index.row(), - QVariant::fromValue(QStringFromStd(result)), - QStringFromStd("argRole"), - index.parent()); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - set(index.row(), - QVariant::fromValue(QString("")), - QStringFromStd("argRole"), - index.parent()); - } -} - -#include "shotgun_model_ui.moc" - - -// Sequence - -// "metadata": { -// "shotgun": { -// "version": { -// "attributes": { -// "code": "O_078_las_0670_lighting_slap_v009", -// "created_at": "2022-09-19T10:33:09Z", -// "frame_count": 49, -// "frame_range": "1041-1089", -// "sg_date_submitted_to_client": null, -// "sg_dneg_version": 9, -// "sg_ivy_dnuuid": "47c697dd-9e23-43b2-a068-b6d7e016af5c", -// "sg_on_disk_chn": "None", -// "sg_on_disk_lon": "Full", -// "sg_on_disk_mtl": "None", -// "sg_on_disk_mum": "None", -// "sg_on_disk_van": "None", -// "sg_path_to_frames": -// "/jobs/MEG2/078_las_0670/OUT/O_078_las_0670_lighting_slap_v009/4222x1768/O_078_las_0670_lighting_slap_v009.####.exr", -// "sg_path_to_movie": -// "/jobs/MEG2/078_las_0670/movie/out/O_078_las_0670_lighting_slap_v009/O_078_las_0670_lighting_slap_v009.dneg.mov", -// "sg_pipeline_step": "Lighting", -// "sg_production_status": "rev", -// "sg_status_list": "rev", -// "sg_twig_name": "O_078_las_0670_lighting_slap", -// "sg_twig_type_code": "out" -// }, -// "id": 104526415, -// "links": { -// "self": "/api/v1/entity/versions/104526415" -// }, -// "relationships": { -// "created_by": { -// "data": { -// "id": 26981, -// "name": "Nessa Zhang Mingfang", -// "type": "HumanUser" -// }, -// "links": { -// "related": "/api/v1/entity/human_users/26981", -// "self": "/api/v1/entity/versions/104526415/relationships/created_by" -// } -// }, -// "entity": { -// "data": { -// "id": 1222548, -// "name": "078_las_0670", -// "type": "Shot" -// }, -// "links": { -// "related": "/api/v1/entity/shots/1222548", -// "self": "/api/v1/entity/versions/104526415/relationships/entity" -// } -// }, -// "notes": { -// "data": [ -// { -// "id": 24872042, -// "name": "MEG2 : Dailies : O_078_las_0670_lighting_slap_v009 : london - Submit -// to Dailies : O_078_las_0670_lighting_slap_v009\nupdated visor comp", "type": -// "Note" -// } -// ], -// "links": { -// "self": "/api/v1/entity/versions/104526415/relationships/notes" -// } -// }, -// "project": { -// "data": { -// "id": 1419, -// "name": "MEG2", -// "type": "Project" -// }, -// "links": { -// "related": "/api/v1/entity/projects/1419", -// "self": "/api/v1/entity/versions/104526415/relationships/project" -// } -// }, -// "user": { -// "data": { -// "id": 26981, -// "name": "Nessa Zhang Mingfang", -// "type": "HumanUser" -// }, -// "links": { -// "related": "/api/v1/entity/human_users/26981", -// "self": "/api/v1/entity/versions/104526415/relationships/user" -// } -// } -// }, -// "type": "Version" -// } -// } -// } -// } diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/shotgun_model_ui.hpp b/src/plugin/data_source/dneg/shotgun/src/qml/shotgun_model_ui.hpp deleted file mode 100644 index bde8ff90c..000000000 --- a/src/plugin/data_source/dneg/shotgun/src/qml/shotgun_model_ui.hpp +++ /dev/null @@ -1,599 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include -#include - -CAF_PUSH_WARNINGS -#include -#include -#include -#include -#include -#include -#include -#include -#include -CAF_POP_WARNINGS - -#include "xstudio/ui/qml/helper_ui.hpp" -#include "xstudio/ui/qml/json_tree_model_ui.hpp" -#include "xstudio/shotgun_client/shotgun_client.hpp" -#include "xstudio/utility/json_store.hpp" - -namespace xstudio { -using namespace shotgun_client; -namespace ui { - namespace qml { - - class ShotgunListModel; - - class ShotgunSequenceModel : public JSONTreeModel { - public: - enum Roles { idRole = JSONTreeModel::Roles::LASTROLE, nameRole, typeRole }; - - ShotgunSequenceModel(QObject *parent = nullptr) : JSONTreeModel(parent) { - setRoleNames(std::vector({"idRole", "nameRole", "typeRole"})); - } - [[nodiscard]] QVariant - data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - static nlohmann::json flatToTree(const nlohmann::json &src); - - private: - static nlohmann::json sortByName(const nlohmann::json &json); - }; - - class ShotgunTreeModel : public JSONTreeModel { - Q_OBJECT - - Q_PROPERTY(bool hasActiveFilter READ hasActiveFilter NOTIFY hasActiveFilterChanged) - Q_PROPERTY( - bool hasActiveLiveLink READ hasActiveLiveLink NOTIFY hasActiveLiveLinkChanged) - Q_PROPERTY(int length READ length NOTIFY lengthChanged) - Q_PROPERTY(int activePreset READ activePreset WRITE setActivePreset NOTIFY - activePresetChanged) - - Q_PROPERTY(QString activeShot READ activeShot NOTIFY activeShotChanged) - Q_PROPERTY(QString activeSeq READ activeSeq NOTIFY activeSeqChanged) - - public: - enum Roles { - argRole = JSONTreeModel::Roles::LASTROLE, - detailRole, - enabledRole, - expandedRole, - idRole, - nameRole, - negationRole, - queriesRole, - liveLinkRole, - termRole, - typeRole - }; - - ShotgunTreeModel(QObject *parent = nullptr) : JSONTreeModel(parent) { - setChildren("queries"); - setRoleNames(std::vector( - {"argRole", - "detailRole", - "enabledRole", - "expandedRole", - "idRole", - "nameRole", - "negationRole", - "queriesRole", - "liveLinkRole", - "termRole", - "typeRole"})); - connect( - this, - &QAbstractListModel::rowsInserted, - this, - &ShotgunTreeModel::modelChanged); - connect( - this, - &QAbstractListModel::rowsRemoved, - this, - &ShotgunTreeModel::modelChanged); - connect( - this, - &QAbstractListModel::rowsMoved, - this, - &ShotgunTreeModel::modelChanged); - connect( - this, - &ShotgunTreeModel::modelChanged, - this, - &ShotgunTreeModel::lengthChanged); - } - - [[nodiscard]] int length() const { return rowCount(); } - [[nodiscard]] int activePreset() const { return active_preset_; } - [[nodiscard]] QString activeSeq() const { return active_seq_; } - [[nodiscard]] QString activeShot() const { return active_shot_; } - - [[nodiscard]] QVariant - data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - bool setData( - const QModelIndex &index, - const QVariant &value, - int role = Qt::EditRole) override; - Q_INVOKABLE bool - removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; - - Q_INVOKABLE bool insert( - int row, - const QModelIndex &parent = QModelIndex(), - const QVariant &data = QVariant()); - - Q_INVOKABLE [[nodiscard]] QVariant - get(int row, - const QModelIndex &parent = QModelIndex(), - const QString &role = "display") const; - - Q_INVOKABLE [[nodiscard]] QVariant - get(const QModelIndex &index, const QString &role = "display") const; - - Q_INVOKABLE QVariant get(int row, const QString &role) const { - return get(row, QModelIndex(), role); - } - Q_INVOKABLE bool - set(int row, - const QVariant &value, - const QString &role = "display", - const QModelIndex &parent = QModelIndex()); - - Q_INVOKABLE int search( - const QVariant &value, - const QString &role = "display", - const QModelIndex &parent = QModelIndex(), - const int start = 0); - Q_INVOKABLE void setType(const int row, const QString &type = "user"); - - Q_INVOKABLE void clearLoaded(); - Q_INVOKABLE void clearExpanded(); - Q_INVOKABLE void clearLiveLinks(); - - Q_INVOKABLE void setActivePreset(const int row = -1); - - Q_INVOKABLE QVariant - applyLiveLink(const QVariant &preset, const QVariant &livelink); - Q_INVOKABLE [[nodiscard]] int getProjectId(const QVariant &livelink) const; - - void populate(const utility::JsonStore &); - - [[nodiscard]] bool hasActiveFilter() const { return has_active_filter_; } - [[nodiscard]] bool hasActiveLiveLink() const { return has_active_live_link_; } - - void updateLiveLinks(const utility::JsonStore &data); - void refreshLiveLinks(); - - utility::JsonStore - applyLiveLink(const utility::JsonStore &preset, const utility::JsonStore &livelink); - utility::JsonStore applyLiveLinkValue( - const utility::JsonStore &query, const utility::JsonStore &livelink); - - virtual void updateLiveLink(const QModelIndex &index); - - void setSequenceMap(const QMap *map) { - sequence_map_ = map; - } - - protected: - [[nodiscard]] utility::JsonStore - getSequence(const int project_id, const int shot_id) const; - - signals: - void modelChanged(); - void lengthChanged(); - void hasActiveFilterChanged(); - void hasActiveLiveLinkChanged(); - void activePresetChanged(); - void activeShotChanged(); - void activeSeqChanged(); - - private: - void checkForActiveFilter(); - void checkForActiveLiveLink(); - - protected: - utility::JsonStore live_link_data_; - bool has_active_filter_{true}; - bool has_active_live_link_{false}; - const QMap *sequence_map_{nullptr}; - int active_preset_{-1}; - QString active_seq_{}; - QString active_shot_{}; - }; - - - class ShotgunListModel : public QAbstractListModel { - Q_OBJECT - - Q_PROPERTY(int length READ length NOTIFY lengthChanged) - Q_PROPERTY(int count READ length NOTIFY lengthChanged) - Q_PROPERTY(bool truncated READ truncated NOTIFY truncatedChanged) - - public: - enum Roles { - JSONRole = Qt::UserRole + 1, - addressingRole, - artistRole, - assetRole, - authorRole, - clientNoteRole, - contentRole, - createdByRole, - createdDateRole, - dateSubmittedToClientRole, - departmentRole, - detailRole, - frameRangeRole, - frameSequenceRole, - idRole, - loginRole, - itemCountRole, - JSONTextRole, - linkedVersionsRole, - movieRole, - nameRole, - noteCountRole, - noteTypeRole, - onSiteChn, - onSiteLon, - onSiteMtl, - onSiteMum, - onSiteSyd, - onSiteVan, - pipelineStatusRole, - pipelineStatusFullRole, - pipelineStepRole, - playlistNameRole, - productionStatusRole, - projectRole, - projectIdRole, - resultTypeRole, - sequenceRole, - shotNameRole, - shotRole, - siteRole, - stalkUuidRole, - subjectRole, - submittedToDailiesRole, - tagRole, - thumbRole, - twigNameRole, - twigTypeRole, - typeRole, - URLRole, - versionNameRole, - versionRole - }; - - inline static const std::map role_names = { - {addressingRole, "addressingRole"}, - {artistRole, "artistRole"}, - {assetRole, "assetRole"}, - {authorRole, "authorRole"}, - {clientNoteRole, "clientNoteRole"}, - {contentRole, "contentRole"}, - {createdByRole, "createdByRole"}, - {createdDateRole, "createdDateRole"}, - {dateSubmittedToClientRole, "dateSubmittedToClientRole"}, - {departmentRole, "departmentRole"}, - {detailRole, "detailRole"}, - {frameRangeRole, "frameRangeRole"}, - {frameSequenceRole, "frameSequenceRole"}, - {idRole, "idRole"}, - {itemCountRole, "itemCountRole"}, - {JSONRole, "jsonRole"}, - {loginRole, "loginRole"}, - {JSONTextRole, "jsonTextRole"}, - {linkedVersionsRole, "linkedVersionsRole"}, - {movieRole, "movieRole"}, - {nameRole, "nameRole"}, - {noteCountRole, "noteCountRole"}, - {noteTypeRole, "noteTypeRole"}, - {onSiteChn, "onSiteChn"}, - {onSiteLon, "onSiteLon"}, - {onSiteMtl, "onSiteMtl"}, - {onSiteMum, "onSiteMum"}, - {onSiteSyd, "onSiteSyd"}, - {onSiteVan, "onSiteVan"}, - {pipelineStatusRole, "pipelineStatusRole"}, - {pipelineStatusFullRole, "pipelineStatusFullRole"}, - {pipelineStepRole, "pipelineStepRole"}, - {playlistNameRole, "playlistNameRole"}, - {productionStatusRole, "productionStatusRole"}, - {projectRole, "projectRole"}, - {projectIdRole, "projectIdRole"}, - {Qt::DisplayRole, "display"}, - {resultTypeRole, "resultTypeRole"}, - {sequenceRole, "sequenceRole"}, - {shotNameRole, "shotNameRole"}, - {shotRole, "shotRole"}, - {siteRole, "siteRole"}, - {stalkUuidRole, "stalkUuidRole"}, - {subjectRole, "subjectRole"}, - {submittedToDailiesRole, "submittedToDailiesRole"}, - {tagRole, "tagRole"}, - {thumbRole, "thumbRole"}, - {twigNameRole, "twigNameRole"}, - {twigTypeRole, "twigTypeRole"}, - {typeRole, "typeRole"}, - {URLRole, "URLRole"}, - {versionNameRole, "versionNameRole"}, - {versionRole, "versionRole"}}; - - ShotgunListModel(QObject *parent = nullptr) : QAbstractListModel(parent) { - connect( - this, - &QAbstractListModel::rowsInserted, - this, - &ShotgunListModel::lengthChanged); - connect( - this, - &QAbstractListModel::rowsRemoved, - this, - &ShotgunListModel::lengthChanged); - } - - void populate(const utility::JsonStore &); - void setTruncated(const bool truncated = true) { - if (truncated_ != truncated) { - truncated_ = truncated; - emit truncatedChanged(); - } - } - - [[nodiscard]] int - rowCount(const QModelIndex &parent = QModelIndex()) const override { - Q_UNUSED(parent); - return (data_.is_null() ? 0 : data_.size()); - } - [[nodiscard]] int length() const { return rowCount(); } - [[nodiscard]] bool truncated() const { return truncated_; } - - Q_INVOKABLE int - search(const QVariant &value, const QString &role = "display", const int start = 0); - Q_INVOKABLE QVariant get(int row, const QString &role = "display"); - [[nodiscard]] QVariant - data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - Q_INVOKABLE void clear() { populate(utility::JsonStore(R"([])"_json)); } - - Q_INVOKABLE void append(const QVariant &data); - - [[nodiscard]] QHash roleNames() const override { - QHash roles; - for (const auto &p : role_names) { - roles[p.first] = p.second.c_str(); - } - return roles; - } - - utility::JsonStore &modelData() { return data_; } - - void setQueryValueCache( - const std::map> *qvc) { - query_value_cache_ = qvc; - } - utility::JsonStore getQueryValue( - const std::string &type, - const utility::JsonStore &value, - const int project_id = -1) const; - - - signals: - void lengthChanged(); - void truncatedChanged(); - - protected: - utility::JsonStore data_; - bool truncated_{false}; - const std::map> - *query_value_cache_{nullptr}; - }; - - class ShotgunFilterModel : public QSortFilterProxyModel { - Q_OBJECT - - Q_PROPERTY(int length READ length NOTIFY lengthChanged) - Q_PROPERTY(int count READ length NOTIFY lengthChanged) - Q_PROPERTY(bool sortAscending READ sortAscending WRITE setSortAscending NOTIFY - sortAscendingChanged) - Q_PROPERTY(QString sortRoleName READ sortRoleName WRITE setSortRoleName NOTIFY - sortRoleNameChanged) - - Q_PROPERTY(QItemSelection selection READ selection WRITE setSelection NOTIFY - selectionChanged) - - Q_PROPERTY(QItemSelection selectionFilter READ selectionFilter WRITE - setSelectionFilter NOTIFY selectionFilterChanged) - - public: - ShotgunFilterModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) { - setFilterRole(ShotgunListModel::Roles::nameRole); - setFilterCaseSensitivity(Qt::CaseInsensitive); - setDynamicSortFilter(true); - setSortRole(ShotgunListModel::Roles::createdDateRole); - sort(0, Qt::DescendingOrder); - - connect( - this, - &QAbstractListModel::rowsInserted, - this, - &ShotgunFilterModel::lengthChanged); - connect( - this, - &QAbstractListModel::modelReset, - this, - &ShotgunFilterModel::lengthChanged); - connect( - this, - &QAbstractListModel::rowsRemoved, - this, - &ShotgunFilterModel::lengthChanged); - } - - [[nodiscard]] QItemSelection selection() const { return selection_sort_; } - [[nodiscard]] QItemSelection selectionFilter() const { return selection_filter_; } - - [[nodiscard]] int length() const { return rowCount(); } - // [[nodiscard]] QObject *srcModel() const { return } - - Q_INVOKABLE int - search(const QVariant &value, const QString &role = "display", const int start = 0); - Q_INVOKABLE QVariant get(int row, const QString &role = "display"); - - Q_INVOKABLE [[nodiscard]] QString - getRoleFilter(const QString &role = "display") const; - Q_INVOKABLE void - setRoleFilter(const QString &filter, const QString &role = "display"); - - [[nodiscard]] bool sortAscending() const { - return sortOrder() == Qt::AscendingOrder; - } - - [[nodiscard]] QString sortRoleName() const { - try { - return roleNames().value(sortRole()); - } catch (...) { - } - - return QString(); - } - - void setSelection(const QItemSelection &selection) { - if (selection_sort_ != selection) { - selection_sort_ = selection; - emit selectionChanged(); - setDynamicSortFilter(false); - sort(0, sortOrder()); - setDynamicSortFilter(true); - } - } - - void setSelectionFilter(const QItemSelection &selection) { - if (selection_filter_ != selection) { - selection_filter_ = selection; - emit selectionFilterChanged(); - invalidateFilter(); - setDynamicSortFilter(false); - sort(0, sortOrder()); - setDynamicSortFilter(true); - } - } - - void setSortAscending(const bool ascending = true) { - if (ascending != (sortOrder() == Qt::AscendingOrder ? true : false)) { - sort(0, ascending ? Qt::AscendingOrder : Qt::DescendingOrder); - emit sortAscendingChanged(); - } - } - [[nodiscard]] int getRoleId(const QString &role) const { - auto role_id = -1; - - QHashIterator it(roleNames()); - if (it.findNext(role.toUtf8())) { - role_id = it.key(); - } - return role_id; - } - - void setSortRoleName(const QString &role) { - auto role_id = getRoleId(role); - if (role_id != sortRole()) { - setSortRole(role_id); - emit sortRoleNameChanged(); - } - } - - signals: - void lengthChanged(); - void sortAscendingChanged(); - void sortRoleNameChanged(); - void selectionChanged(); - void selectionFilterChanged(); - - protected: - [[nodiscard]] bool - filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; - [[nodiscard]] bool - lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const; - - private: - std::map roleFilterMap_; - - QItemSelection selection_sort_; - QItemSelection selection_filter_; - }; - - class ShotModel : public ShotgunListModel { - Q_OBJECT - - public: - ShotModel(QObject *parent = nullptr) : ShotgunListModel(parent) {} - [[nodiscard]] QVariant - data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - void setSequenceMap(const QMap *map) { - sequence_map_ = map; - } - - protected: - [[nodiscard]] utility::JsonStore - getSequence(const int project_id, const int shot_id) const; - - private: - const QMap *sequence_map_{nullptr}; - }; - - class PlaylistModel : public ShotgunListModel { - Q_OBJECT - - public: - PlaylistModel(QObject *parent = nullptr) : ShotgunListModel(parent) {} - [[nodiscard]] QVariant - data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - }; - - class EditModel : public ShotgunListModel { - Q_OBJECT - - public: - EditModel(QObject *parent = nullptr) : ShotgunListModel(parent) {} - [[nodiscard]] QVariant - data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - }; - - class ReferenceModel : public ShotModel { - Q_OBJECT - - public: - ReferenceModel(QObject *parent = nullptr) : ShotModel(parent) {} - [[nodiscard]] QVariant - data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - }; - - class MediaActionModel : public ShotModel { - Q_OBJECT - - public: - MediaActionModel(QObject *parent = nullptr) : ShotModel(parent) {} - [[nodiscard]] QVariant - data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - }; - - class NoteModel : public ShotgunListModel { - Q_OBJECT - - public: - NoteModel(QObject *parent = nullptr) : ShotgunListModel(parent) {} - [[nodiscard]] QVariant - data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - }; - - } // namespace qml -} // namespace ui -} // namespace xstudio diff --git a/src/plugin/data_source/dneg/shotgun/test/CMakeLists.txt b/src/plugin/data_source/dneg/shotgun/test/CMakeLists.txt deleted file mode 100644 index a73825679..000000000 --- a/src/plugin/data_source/dneg/shotgun/test/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -include(CTest) - -SET(LINK_DEPS - caf::core -) - -create_tests("${LINK_DEPS}") diff --git a/src/plugin/hud/exr_data_window/src/CMakeLists.txt b/src/plugin/hud/exr_data_window/src/CMakeLists.txt index 8eb7e4950..a281a8707 100644 --- a/src/plugin/hud/exr_data_window/src/CMakeLists.txt +++ b/src/plugin/hud/exr_data_window/src/CMakeLists.txt @@ -8,4 +8,4 @@ SET(LINK_DEPS find_package(Imath) -create_plugin_with_alias(exr_data_window xstudio::viewport::exr_data_window 0.1.0 "${LINK_DEPS}") \ No newline at end of file +create_plugin_with_alias(exr_data_window xstudio::viewport::exr_data_window ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") \ No newline at end of file diff --git a/src/plugin/hud/exr_data_window/src/exr_data_window.cpp b/src/plugin/hud/exr_data_window/src/exr_data_window.cpp index b94235271..d8fb0e86b 100644 --- a/src/plugin/hud/exr_data_window/src/exr_data_window.cpp +++ b/src/plugin/hud/exr_data_window/src/exr_data_window.cpp @@ -6,8 +6,12 @@ #include "xstudio/utility/blind_data.hpp" #include "xstudio/ui/viewport/viewport_helpers.hpp" +#ifdef __apple__ +#include +#else #include #include +#endif using namespace xstudio; using namespace xstudio::ui::viewport; @@ -24,7 +28,7 @@ class HudData : public utility::BlindDataObject { class EXRDataWindowRenderer : public plugin::ViewportOverlayRenderer { public: - void render_opengl( + void render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float /*viewport_du_dpixel*/, @@ -64,7 +68,7 @@ class EXRDataWindowRenderer : public plugin::ViewportOverlayRenderer { frame ? frame->image_pixels_bounding_box().min : Imath::V2i(); auto image_bounds_max = frame ? frame->image_pixels_bounding_box().max : Imath::V2i(); - auto pixel_aspect = frame ? frame->pixel_aspect() : 1.0f; + auto pixel_aspect = frame.frame_id().pixel_aspect(); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -80,6 +84,7 @@ class EXRDataWindowRenderer : public plugin::ViewportOverlayRenderer { image_bounds_max, image_dims, transform_matrix, pixel_aspect); glUseProgram(0); +#ifndef __OPENGL_4_1__ glLineWidth(w); glColor4f(c.r, c.g, c.b, 1.0f); glBegin(GL_LINE_LOOP); @@ -88,6 +93,7 @@ class EXRDataWindowRenderer : public plugin::ViewportOverlayRenderer { glVertex2f(bottom_right.x, bottom_right.y); glVertex2f(top_left.x, bottom_right.y); glEnd(); +#endif } } }; @@ -95,10 +101,7 @@ class EXRDataWindowRenderer : public plugin::ViewportOverlayRenderer { EXRDataWindowHUD::EXRDataWindowHUD( caf::actor_config &cfg, const utility::JsonStore &init_settings) - : HUDPluginBase(cfg, "OpenEXR Data Window", init_settings) { - - enabled_->set_preference_path("/plugin/exr_data_window/enabled"); - enabled_->set_role_data(module::Attribute::ToolbarPosition, 1.0f); + : plugin::HUDPluginBase(cfg, "OpenEXR Data Window", init_settings, 1.0f) { colour_ = add_colour_attribute("Line Colour", "Colour", utility::ColourTriplet(0.0f, 1.0f, 0.0f)); @@ -110,14 +113,18 @@ EXRDataWindowHUD::EXRDataWindowHUD( add_hud_settings_attribute(width_); } -plugin::ViewportOverlayRendererPtr EXRDataWindowHUD::make_overlay_renderer(const int) { +plugin::ViewportOverlayRendererPtr +EXRDataWindowHUD::make_overlay_renderer(const std::string &viewport_name) { return plugin::ViewportOverlayRendererPtr(new EXRDataWindowRenderer()); } EXRDataWindowHUD::~EXRDataWindowHUD() = default; -utility::BlindDataObjectPtr EXRDataWindowHUD::prepare_overlay_data( - const media_reader::ImageBufPtr &image, const bool /*offscreen*/) const { +utility::BlindDataObjectPtr EXRDataWindowHUD::onscreen_render_data( + const media_reader::ImageBufPtr &image, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid, + const bool is_hero_image) const { auto r = utility::BlindDataObjectPtr(); @@ -150,7 +157,8 @@ plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { {std::make_shared>( utility::Uuid("f8a09960-606d-11ed-9b6a-0242ac120002"), "EXRDataWindowHUD", - plugin_manager::PluginFlags::PF_HEAD_UP_DISPLAY, + plugin_manager::PluginFlags::PF_HEAD_UP_DISPLAY | + plugin_manager::PluginFlags::PF_VIEWPORT_OVERLAY, true, "Clement Jovet", "Viewport HUD Plugin")})); diff --git a/src/plugin/hud/exr_data_window/src/exr_data_window.hpp b/src/plugin/hud/exr_data_window/src/exr_data_window.hpp index dc716bd2d..15b037057 100644 --- a/src/plugin/hud/exr_data_window/src/exr_data_window.hpp +++ b/src/plugin/hud/exr_data_window/src/exr_data_window.hpp @@ -3,13 +3,13 @@ #include "xstudio/plugin_manager/plugin_base.hpp" #include "xstudio/ui/opengl/shader_program_base.hpp" -#include "xstudio/ui/viewport/hud_plugin.hpp" +#include "xstudio/plugin_manager/hud_plugin.hpp" namespace xstudio { namespace ui { namespace viewport { - class EXRDataWindowHUD : public HUDPluginBase { + class EXRDataWindowHUD : public plugin::HUDPluginBase { public: EXRDataWindowHUD(caf::actor_config &cfg, const utility::JsonStore &init_settings); @@ -20,10 +20,14 @@ namespace ui { ) override; protected: - utility::BlindDataObjectPtr prepare_overlay_data( - const media_reader::ImageBufPtr &, const bool /*offscreen*/) const override; - - plugin::ViewportOverlayRendererPtr make_overlay_renderer(const int) override; + utility::BlindDataObjectPtr onscreen_render_data( + const media_reader::ImageBufPtr &, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid, + const bool is_hero_image) const override; + + plugin::ViewportOverlayRendererPtr + make_overlay_renderer(const std::string &viewport_name) override; private: module::ColourAttribute *colour_ = nullptr; diff --git a/src/plugin/hud/image_boundary/src/CMakeLists.txt b/src/plugin/hud/image_boundary/src/CMakeLists.txt index 6bbf57672..ea1027c27 100644 --- a/src/plugin/hud/image_boundary/src/CMakeLists.txt +++ b/src/plugin/hud/image_boundary/src/CMakeLists.txt @@ -8,4 +8,4 @@ SET(LINK_DEPS find_package(Imath) -create_plugin_with_alias(image_boundary_hud xstudio::viewport::image_boundary_hud 0.1.0 "${LINK_DEPS}") \ No newline at end of file +create_plugin_with_alias(image_boundary_hud xstudio::viewport::image_boundary_hud ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") \ No newline at end of file diff --git a/src/plugin/hud/image_boundary/src/image_boundary_hud.cpp b/src/plugin/hud/image_boundary/src/image_boundary_hud.cpp index 8bcfd0006..7f47826e9 100644 --- a/src/plugin/hud/image_boundary/src/image_boundary_hud.cpp +++ b/src/plugin/hud/image_boundary/src/image_boundary_hud.cpp @@ -6,8 +6,12 @@ #include "xstudio/utility/blind_data.hpp" #include "xstudio/ui/viewport/viewport_helpers.hpp" +#ifdef __apple__ +#include +#else #include #include +#endif using namespace xstudio; using namespace xstudio::ui::viewport; @@ -24,7 +28,7 @@ class HudData : public utility::BlindDataObject { class ImageBoundaryRenderer : public plugin::ViewportOverlayRenderer { public: - void render_opengl( + void render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float /*viewport_du_dpixel*/, @@ -60,7 +64,7 @@ class ImageBoundaryRenderer : public plugin::ViewportOverlayRenderer { transform_window_to_viewport_space; auto image_dims = frame ? frame->image_size_in_pixels() : Imath::V2i(); - auto pixel_aspect = frame ? frame->pixel_aspect() : 1.0f; + auto pixel_aspect = frame.frame_id().pixel_aspect(); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -76,6 +80,7 @@ class ImageBoundaryRenderer : public plugin::ViewportOverlayRenderer { get_transformed_point(image_dims, image_dims, transform_matrix, pixel_aspect); glUseProgram(0); +#ifndef __apple__ glLineWidth(w); glColor4f(c.r, c.g, c.b, 1.0f); glBegin(GL_LINE_LOOP); @@ -84,6 +89,7 @@ class ImageBoundaryRenderer : public plugin::ViewportOverlayRenderer { glVertex2f(bottom_right.x, bottom_right.y); glVertex2f(top_left.x, bottom_right.y); glEnd(); +#endif } } }; @@ -91,10 +97,7 @@ class ImageBoundaryRenderer : public plugin::ViewportOverlayRenderer { ImageBoundaryHUD::ImageBoundaryHUD( caf::actor_config &cfg, const utility::JsonStore &init_settings) - : HUDPluginBase(cfg, "Image Boundary", init_settings) { - - enabled_->set_preference_path("/plugin/image_boundary/enabled"); - enabled_->set_role_data(module::Attribute::ToolbarPosition, 2.0f); + : plugin::HUDPluginBase(cfg, "Image Boundary", init_settings) { colour_ = add_colour_attribute("Line Colour", "Colour", utility::ColourTriplet(1.0f, 0.0f, 0.0f)); @@ -106,14 +109,18 @@ ImageBoundaryHUD::ImageBoundaryHUD( add_hud_settings_attribute(width_); } -plugin::ViewportOverlayRendererPtr ImageBoundaryHUD::make_overlay_renderer(const int) { +plugin::ViewportOverlayRendererPtr +ImageBoundaryHUD::make_overlay_renderer(const std::string &viewport_name) { return plugin::ViewportOverlayRendererPtr(new ImageBoundaryRenderer()); } ImageBoundaryHUD::~ImageBoundaryHUD() = default; -utility::BlindDataObjectPtr ImageBoundaryHUD::prepare_overlay_data( - const media_reader::ImageBufPtr &image, const bool /*offscreen*/) const { +utility::BlindDataObjectPtr ImageBoundaryHUD::onscreen_render_data( + const media_reader::ImageBufPtr &image, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid, + const bool is_hero_image) const { auto r = utility::BlindDataObjectPtr(); @@ -146,7 +153,8 @@ plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { {std::make_shared>( utility::Uuid("95268f7c-88d1-48da-8543-c5275ef5b2c5"), "ImageBoundaryHUD", - plugin_manager::PluginFlags::PF_HEAD_UP_DISPLAY, + plugin_manager::PluginFlags::PF_HEAD_UP_DISPLAY | + plugin_manager::PluginFlags::PF_VIEWPORT_OVERLAY, true, "Clement Jovet", "Viewport HUD Plugin")})); diff --git a/src/plugin/hud/image_boundary/src/image_boundary_hud.hpp b/src/plugin/hud/image_boundary/src/image_boundary_hud.hpp index 1e1277bf6..d02106718 100644 --- a/src/plugin/hud/image_boundary/src/image_boundary_hud.hpp +++ b/src/plugin/hud/image_boundary/src/image_boundary_hud.hpp @@ -3,13 +3,13 @@ #include "xstudio/plugin_manager/plugin_base.hpp" #include "xstudio/ui/opengl/shader_program_base.hpp" -#include "xstudio/ui/viewport/hud_plugin.hpp" +#include "xstudio/plugin_manager/hud_plugin.hpp" namespace xstudio { namespace ui { namespace viewport { - class ImageBoundaryHUD : public HUDPluginBase { + class ImageBoundaryHUD : public plugin::HUDPluginBase { public: ImageBoundaryHUD(caf::actor_config &cfg, const utility::JsonStore &init_settings); @@ -20,10 +20,14 @@ namespace ui { ) override; protected: - utility::BlindDataObjectPtr prepare_overlay_data( - const media_reader::ImageBufPtr &, const bool /*offscreen*/) const override; - - plugin::ViewportOverlayRendererPtr make_overlay_renderer(const int) override; + utility::BlindDataObjectPtr onscreen_render_data( + const media_reader::ImageBufPtr &, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid, + const bool is_hero_image) const override; + + plugin::ViewportOverlayRendererPtr + make_overlay_renderer(const std::string &viewport_name) override; private: module::ColourAttribute *colour_ = nullptr; diff --git a/src/plugin/hud/pixel_probe/src/CMakeLists.txt b/src/plugin/hud/pixel_probe/src/CMakeLists.txt index 20f3cff4f..263a19808 100644 --- a/src/plugin/hud/pixel_probe/src/CMakeLists.txt +++ b/src/plugin/hud/pixel_probe/src/CMakeLists.txt @@ -8,6 +8,6 @@ SET(LINK_DEPS find_package(Imath) -create_plugin_with_alias(pixel_probe_hud xstudio::viewport::pixel_probe_hud 0.1.0 "${LINK_DEPS}") +create_plugin_with_alias(pixel_probe_hud xstudio::viewport::pixel_probe_hud ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") -add_subdirectory(qml) \ No newline at end of file +add_plugin_qml(${PROJECT_NAME} qml) \ No newline at end of file diff --git a/src/plugin/hud/pixel_probe/src/pixel_probe.cpp b/src/plugin/hud/pixel_probe/src/pixel_probe.cpp index 0bca83786..52f3b8a55 100644 --- a/src/plugin/hud/pixel_probe/src/pixel_probe.cpp +++ b/src/plugin/hud/pixel_probe/src/pixel_probe.cpp @@ -1,19 +1,25 @@ // SPDX-License-Identifier: Apache-2.0 +#include + #include "pixel_probe.hpp" #include "xstudio/plugin_manager/plugin_base.hpp" -#include "xstudio/media_reader/image_buffer.hpp" +#include "xstudio/media_reader/image_buffer_set.hpp" #include "xstudio/global_store/global_store.hpp" #include "xstudio/utility/blind_data.hpp" #include "xstudio/ui/viewport/viewport_helpers.hpp" #include "xstudio/utility/helpers.hpp" +#ifdef __apple__ +#include +#else #include #include +#endif using namespace xstudio; using namespace xstudio::ui::viewport; -/*void PixelProbeHUDRenderer::render_opengl( +/*void PixelProbeHUDRenderer::render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, @@ -40,7 +46,7 @@ using namespace xstudio::ui::viewport; if (!text_renderer_) { text_renderer_ = std::make_unique( - utility::xstudio_root("/fonts/Overpass-Regular.ttf"), 96); + utility::xstudio_resources_dir("fonts/Overpass-Regular.ttf"), 96); } auto font_scale = 30.0f * 1920.0f * viewport_du_dpixel; @@ -84,7 +90,7 @@ void PixelProbeHUDRenderer::set_mouse_pointer_position(const Imath::V2f p) }*/ PixelProbeHUD::PixelProbeHUD(caf::actor_config &cfg, const utility::JsonStore &init_settings) - : HUDPluginBase(cfg, "Pixel Probe", init_settings) { + : plugin::HUDPluginBase(cfg, "Pixel Probe", init_settings, 3.0f) { pixel_info_text_ = add_string_attribute("Pixel Info", "Pixel Info", ""); pixel_info_text_->expose_in_ui_attrs_group("pixel_info_attributes"); @@ -92,22 +98,9 @@ PixelProbeHUD::PixelProbeHUD(caf::actor_config &cfg, const utility::JsonStore &i pixel_info_title_ = add_string_attribute("Pixel Info Title", "Pixel Info Title", ""); pixel_info_title_->expose_in_ui_attrs_group("pixel_info_attributes"); - // expose the enabled toggle (from HUDPluginBase) - enabled_->set_value(false); - enabled_->expose_in_ui_attrs_group("pixel_info_attributes"); - enabled_->set_preference_path("/plugin/pixel_probe/enabled"); - enabled_->set_role_data(module::Attribute::ToolbarPosition, 3.0f); - - // Here we declare QML code that is reponsible for drawing the pixel info over the xSTUDIO - // viewport. The main Viewport class will take care of instanciating the qml object from - // this code. - hud_element_qml( - R"( - import PixelProbe 1.0 - PixelProbeOverlay { - } - )", - HUDPluginBase::TopLeft); + pixel_info_current_viewport_ = + add_string_attribute("Current Viewport", "Current Viewport", ""); + pixel_info_current_viewport_->expose_in_ui_attrs_group("pixel_info_attributes"); auto font_size = add_float_attribute("Font Size", "Font Size", 10.0f, 5.0f, 50.0f, 1.0f); font_size->expose_in_ui_attrs_group("pixel_info_attributes"); @@ -166,85 +159,103 @@ PixelProbeHUD::PixelProbeHUD(caf::actor_config &cfg, const utility::JsonStore &i show_final_screen_rgb_pixel_values_->set_preference_path( "/plugin/pixel_probe/show_final_screen_rgb_pixel_values"); value_precision_->set_preference_path("/plugin/pixel_probe/decimals"); -} -PixelProbeHUD::~PixelProbeHUD() { - colour_pipelines_.clear(); - colour_pipeline_ = caf::actor(); + // Here we declare QML code that is reponsible for drawing the pixel info over the xSTUDIO + // viewport. The main Viewport class will take care of instanciating the qml object from + // this code. + hud_element_qml( + R"( + import PixelProbe 1.0 + PixelProbeOverlay { + } + )", + plugin::TopLeft); } +PixelProbeHUD::~PixelProbeHUD() { colour_pipelines_.clear(); } + bool PixelProbeHUD::pointer_event(const ui::PointerEvent &e) { - last_pointer_pos_ = e.position_in_viewport_coord_sys(); - update_onscreen_info(e.context()); + update_onscreen_info(e.context(), e.position_in_viewport_coord_sys()); return true; } -void PixelProbeHUD::update_onscreen_info(const std::string &viewport_name) { +void PixelProbeHUD::update_onscreen_info( + const std::string &viewport_name, const Imath::V2f &pointer_position) { - if (is_enabled_ != enabled_->value()) { - is_enabled_ = enabled_->value(); + if (is_enabled_ != visible()) { + is_enabled_ = visible(); if (is_enabled_) { connect_to_ui(); } else { - current_onscreen_image_ = media_reader::ImageBufPtr(); + return; } } - if (is_enabled_ && not viewport_name.empty()) { + media_reader::ImageBufDisplaySetPtr onscreen_image_set; + caf::actor colour_pipeline; + if (not viewport_name.empty()) { if (current_onscreen_images_.find(viewport_name) != current_onscreen_images_.end()) { - current_onscreen_image_ = current_onscreen_images_[viewport_name]; - } else { - current_onscreen_image_ = media_reader::ImageBufPtr(); + onscreen_image_set = current_onscreen_images_[viewport_name]; } - colour_pipeline_ = get_colour_pipeline_actor(viewport_name); + colour_pipeline = get_colour_pipeline_actor(viewport_name); } - static Imath::V2i image_dims(1920, 1080); - - if (current_onscreen_image_ && enabled_->value()) { - - // WIP! - image_dims = current_onscreen_image_->image_size_in_pixels(); + if (onscreen_image_set && visible() && colour_pipeline) { + + Imath::V4f pointer(pointer_position.x, pointer_position.y, 0.0, 1.0f); + bool ptr_in_image = false; + // loop over on-screen images + for (int i = 0; i < onscreen_image_set->num_onscreen_images(); ++i) { + const auto &im = onscreen_image_set->onscreen_image(i); + if (im) { + + // Convert pointer_position to normalised image coordinates + // (image width always spans -1.0, 1.0 in x) + const float a = 1.0f / image_aspect(im); + Imath::V4f p = pointer * im.layout_transform().inverse(); + Imath::V2f p0(p.x / p.w, p.y / p.w); + if (p0.x >= -1.0f && p0.x <= 1.0f && p0.y >= -a && p0.y <= a) { + + // pointer is inside image boundary + Imath::V2i image_coord( + int(round((p0.x + 1.0f) * 0.5f * im->image_size_in_pixels().x)), + int(round((p0.y / a + 1.0f) * 0.5f * im->image_size_in_pixels().y))); + + // here we get the RGB, YUV value at the image coordinate + const auto pixel_info = im->pixel_info(image_coord); + ptr_in_image = true; + + if (!viewport_name.empty()) { + // update our attribute that tracks which viewport the + // mouse pointer is in - used to reveal/hide the pixel + // info overlay per viewport + pixel_info_current_viewport_->set_value(viewport_name); + } - // Image is width-fitted to viewport coordinates -1.0 to 1.0: - const float image_aspect = - image_dims.y / (image_dims.x * current_onscreen_image_->pixel_aspect()); - Imath::V2i image_coord( - int(round((last_pointer_pos_.x + 1.0f) * 0.5f * image_dims.x)), - int(round((last_pointer_pos_.y / image_aspect + 1.0f) * 0.5f * image_dims.y))); - - const auto pixel_info = current_onscreen_image_->pixel_info(image_coord); - - if (colour_pipeline_) { - - // we send the pixel info to the colour pipeline to add it's own colourspace - // transforms and info, if it needs/wants to - request( - colour_pipeline_, - infinite, - colour_pipeline::pixel_info_atom_v, - pixel_info, - current_onscreen_image_.frame_id()) - .then( - [=](const media_reader::PixelInfo &extended_info) mutable { - make_pixel_info_onscreen_text(extended_info); - }, - [=](caf::error &err) { - - }); + // we send the pixel info to the colour pipeline to add it's own colourspace + // transforms and info, if it needs/wants to + mail(colour_pipeline::pixel_info_atom_v, pixel_info, im.frame_id()) + .request(colour_pipeline, infinite) + .then( + [=](const media_reader::PixelInfo &extended_info) mutable { + make_pixel_info_onscreen_text(extended_info); + }, + [=](caf::error &err) { + + }); + break; + } + } + } + if (!ptr_in_image) { + // empty info + make_pixel_info_onscreen_text(media_reader::PixelInfo()); } - } else if (enabled_->value()) { - const float image_aspect = image_dims.y / (image_dims.x); - Imath::V2i image_coord( - int(round((last_pointer_pos_.x + 1.0f) * 0.5f * image_dims.x)), - int(round((last_pointer_pos_.y / image_aspect + 1.0f) * 0.5f * image_dims.y))); - make_pixel_info_onscreen_text(media_reader::PixelInfo(image_coord)); } else { - pixel_info_text_->set_value(""); - pixel_info_title_->set_value(""); + make_pixel_info_onscreen_text(media_reader::PixelInfo()); } } @@ -362,20 +373,26 @@ caf::actor PixelProbeHUD::get_colour_pipeline_actor(const std::string &viewport_ } void PixelProbeHUD::attribute_changed(const utility::Uuid &attribute_uuid, const int role) { - update_onscreen_info(); - HUDPluginBase::attribute_changed(attribute_uuid, role); + + if (attribute_uuid == show_code_values_->uuid() || + attribute_uuid == show_raw_pixel_values_->uuid() || + attribute_uuid == show_linear_pixel_values_->uuid() || + attribute_uuid == show_final_screen_rgb_pixel_values_->uuid() || + attribute_uuid == value_precision_->uuid()) { + update_onscreen_info(); + } + plugin::HUDPluginBase::attribute_changed(attribute_uuid, role); } void PixelProbeHUD::images_going_on_screen( - const std::vector &images, + const media_reader::ImageBufDisplaySetPtr &images, const std::string viewport_name, const bool playhead_playing) { - if (images.size()) - current_onscreen_images_[viewport_name] = images.front(); + if (images) + current_onscreen_images_[viewport_name] = images; else current_onscreen_images_[viewport_name].reset(); - update_onscreen_info(); } extern "C" { diff --git a/src/plugin/hud/pixel_probe/src/pixel_probe.hpp b/src/plugin/hud/pixel_probe/src/pixel_probe.hpp index 52b211bc3..e9a9f46cc 100644 --- a/src/plugin/hud/pixel_probe/src/pixel_probe.hpp +++ b/src/plugin/hud/pixel_probe/src/pixel_probe.hpp @@ -3,7 +3,7 @@ #include "xstudio/plugin_manager/plugin_base.hpp" #include "xstudio/ui/opengl/shader_program_base.hpp" -#include "xstudio/ui/viewport/hud_plugin.hpp" +#include "xstudio/plugin_manager/hud_plugin.hpp" #include "xstudio/ui/opengl/opengl_text_rendering.hpp" namespace xstudio { @@ -12,7 +12,7 @@ namespace ui { /*class PixelProbeHUDRenderer : public plugin::ViewportOverlayRenderer { public: - void render_opengl( + void render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, @@ -30,7 +30,7 @@ namespace ui { };*/ - class PixelProbeHUD : public HUDPluginBase { + class PixelProbeHUD : public plugin::HUDPluginBase { public: PixelProbeHUD(caf::actor_config &cfg, const utility::JsonStore &init_settings); @@ -41,7 +41,7 @@ namespace ui { ) override; void images_going_on_screen( - const std::vector & /*images*/, + const media_reader::ImageBufDisplaySetPtr & /*images*/, const std::string viewport_name, const bool playhead_playing) override; @@ -49,23 +49,23 @@ namespace ui { bool pointer_event(const ui::PointerEvent &e) override; private: - void update_onscreen_info(const std::string &viewport_name = std::string()); + void update_onscreen_info( + const std::string &viewport_name = std::string(), + const Imath::V2f &pointer = Imath::V2f(-1, -1)); caf::actor get_colour_pipeline_actor(const std::string &viewport_name); void make_pixel_info_onscreen_text(const media_reader::PixelInfo &pixel_info); - media_reader::ImageBufPtr current_onscreen_image_; - std::map current_onscreen_images_; + std::map current_onscreen_images_; std::map colour_pipelines_; module::StringAttribute *pixel_info_text_; module::StringAttribute *pixel_info_title_; + module::StringAttribute *pixel_info_current_viewport_; module::BooleanAttribute *show_code_values_; module::BooleanAttribute *show_raw_pixel_values_; module::BooleanAttribute *show_linear_pixel_values_; module::BooleanAttribute *show_final_screen_rgb_pixel_values_; module::IntegerAttribute *value_precision_; - caf::actor colour_pipeline_; - Imath::V2f last_pointer_pos_; bool is_enabled_ = {false}; }; diff --git a/src/plugin/hud/pixel_probe/src/qml/CMakeLists.txt b/src/plugin/hud/pixel_probe/src/qml/CMakeLists.txt deleted file mode 100644 index 8b11a20c0..000000000 --- a/src/plugin/hud/pixel_probe/src/qml/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -project(pixel_probe VERSION 0.1.0 LANGUAGES CXX) - -if(WIN32) -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/PixelProbe.1/ DESTINATION ${CMAKE_INSTALL_PREFIX}/plugin/qml/PixelProbe.1) -else() -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/PixelProbe.1/ DESTINATION share/xstudio/plugin/qml/PixelProbe.1) -endif() - -add_custom_target(COPY_PIXELPROBE_QML ALL) - -add_custom_command(TARGET COPY_PIXELPROBE_QML POST_BUILD - COMMAND ${CMAKE_COMMAND} -E - copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/PixelProbe.1 ${CMAKE_BINARY_DIR}/bin/plugin/qml/PixelProbe.1) - diff --git a/src/plugin/hud/pixel_probe/src/qml/PixelProbe.1/PixelProbeOverlay.qml b/src/plugin/hud/pixel_probe/src/qml/PixelProbe.1/PixelProbeOverlay.qml index 81f4cad7c..34a42eb38 100644 --- a/src/plugin/hud/pixel_probe/src/qml/PixelProbe.1/PixelProbeOverlay.qml +++ b/src/plugin/hud/pixel_probe/src/qml/PixelProbe.1/PixelProbeOverlay.qml @@ -1,11 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.3 -import QtQuick 2.14 + +import QtQuick.Layouts +import QtQuick import QuickFuture 1.0 import QuickPromise 1.0 import xStudio 1.0 -import xstudio.qml.module 1.0 +import xstudio.qml.models 1.0 Rectangle { @@ -14,13 +14,54 @@ Rectangle { color: "transparent" id: control - visible: pixel_info_string != ""//pixel_info_attributes.enabled ? pixel_info_attributes.enabled : false + visible: pixel_info_string != "" && pixel_info_viewport == view.name//pixel_info_attributes.enabled ? pixel_info_attributes.enabled : false + + XsModuleData { + id: pixel_info_model_data + modelDataName: "pixel_info_attributes" + } - XsModuleAttributes { + XsAttributeValue { + id: __pixel_info_string + attributeTitle: "Pixel Info" + model: pixel_info_model_data + } + property alias pixel_info_string: __pixel_info_string.value - id: pixel_info_attributes - attributesGroupNames: "pixel_info_attributes" + XsAttributeValue { + id: __pixel_info_title + attributeTitle: "Pixel Info Title" + model: pixel_info_model_data } + property alias pixel_info_title: __pixel_info_title.value + + XsAttributeValue { + id: __pixel_info_viewport + attributeTitle: "Current Viewport" + model: pixel_info_model_data + } + property alias pixel_info_viewport: __pixel_info_viewport.value + + XsAttributeValue { + id: __font_size + attributeTitle: "Font Size" + model: pixel_info_model_data + } + property alias font_size: __font_size.value + + XsAttributeValue { + id: __font_opacity + attributeTitle: "Font Opacity" + model: pixel_info_model_data + } + property alias font_opacity: __font_opacity.value + + XsAttributeValue { + id: __bg_opacity + attributeTitle: "Bg Opacity" + model: pixel_info_model_data + } + property alias bg_opacity: __bg_opacity.value Rectangle { anchors.fill: parent @@ -28,18 +69,9 @@ Rectangle { color: "black" opacity: bg_opacity border.color: "white" - border.width: 2 + border.width: 2 } - // Properties on the XsModuleAttributes items are created at runtime after - // this item is 'completed' and therefore we need to map to local variables - // with checks as follows: - property var pixel_info_string: pixel_info_attributes.pixel_info ? pixel_info_attributes.pixel_info : "" - property var pixel_info_title: pixel_info_attributes.pixel_info_title ? pixel_info_attributes.pixel_info_title : "" - property var font_size: pixel_info_attributes.font_size ? pixel_info_attributes.font_size : 8.0 - property var font_opacity: pixel_info_attributes.font_opacity ? pixel_info_attributes.font_opacity : 0.8 - property var bg_opacity: pixel_info_attributes.bg_opacity ? pixel_info_attributes.bg_opacity : 0.4 - Column { id: layout @@ -52,8 +84,8 @@ Rectangle { text: pixel_info_title color: "white" font.pixelSize: font_size - font.family: XsStyle.fixWidthFontFamily - opacity: font_opacity + font.family: XsStyleSheet.fixedWidthFontFamily + opacity: font_opacity } Rectangle { @@ -67,8 +99,8 @@ Rectangle { text: pixel_info_string color: "white" font.pixelSize: font_size - font.family: XsStyle.fixWidthFontFamily - opacity: font_opacity + font.family: XsStyleSheet.fixedWidthFontFamily + opacity: font_opacity } } diff --git a/src/plugin/media_hook/CMakeLists.txt b/src/plugin/media_hook/CMakeLists.txt index 12b698435..29d7f5b75 100644 --- a/src/plugin/media_hook/CMakeLists.txt +++ b/src/plugin/media_hook/CMakeLists.txt @@ -1,2 +1,7 @@ +# only build default hook if STUDIO_PLUGINS is +# an empty string +if("${STUDIO_PLUGINS}" STREQUAL "") + add_src_and_test(default_hook) +endif() -build_studio_plugins("${STUDIO_PLUGINS}") +build_studio_plugins("${STUDIO_PLUGINS}") \ No newline at end of file diff --git a/src/plugin/media_hook/default_hook/src/CMakeLists.txt b/src/plugin/media_hook/default_hook/src/CMakeLists.txt new file mode 100644 index 000000000..fb1a3ba9c --- /dev/null +++ b/src/plugin/media_hook/default_hook/src/CMakeLists.txt @@ -0,0 +1,10 @@ +find_package(OpenColorIO CONFIG) + +SET(LINK_DEPS + xstudio::media_hook + xstudio::utility + xstudio::module + OpenColorIO::OpenColorIO +) + +create_plugin_with_alias(default_hook xstudio::media_hook::default_hook ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/plugin/media_hook/default_hook/src/default_hook.cpp b/src/plugin/media_hook/default_hook/src/default_hook.cpp new file mode 100644 index 000000000..0f06af9ce --- /dev/null +++ b/src/plugin/media_hook/default_hook/src/default_hook.cpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include + +#include "xstudio/media_hook/media_hook.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/string_helpers.hpp" +#include "xstudio/utility/json_store.hpp" + +namespace fs = std::filesystem; + +using namespace xstudio; +using namespace xstudio::media_hook; +using namespace xstudio::utility; + +/* This default media hook plugin does very little except to +ensure that the fallback ACES OpenColorIO config is enabled +and media are set to the 'Raw' colourspace as a starting point. + +To see a more complex Hook that is applying various rules to set +colour management metadata and modifying media frame ranges see +the dneg/dnhook code for reference. +*/ +class DefaultMediaHook : public MediaHook { + public: + DefaultMediaHook() : MediaHook("Default") {} + ~DefaultMediaHook() override = default; + + std::optional modify_media_reference( + const utility::MediaReference &mr, const utility::JsonStore &jsn) override { + utility::MediaReference result = mr; + auto changed = false; + + // here we can add our own logic to modify the MediaReference by returning + // a modified copy. An example use case would be to automatically trim a + // slate frame, for example, by changing the frames data in the MediaReference + if (not changed) + return {}; + + return result; + } + + /* + Inject any metadata we desire into the metadata json structure + */ + + utility::JsonStore modify_metadata( + const utility::MediaReference &mr, const utility::JsonStore &metadata) override { + utility::JsonStore result = metadata; + // Example code to get the filepath from the MediaReference + const caf::uri &uri = + mr.container() or mr.uris().empty() ? mr.uri() : mr.uris()[0].first; + + const std::string path = to_string(uri); + auto ppath = uri_to_posix_path(uri); + result["colour_pipeline"] = colour_params(path, metadata); + result["colour_pipeline"]["path"] = path; + return result; + } + + /* + Colour management is enabled at this entry point. We can return a json dict + containing key/value pairs that drive the built-in OCIO plugin in xstudio. + Refer to the src/plugin/colour_pipeline/ocio/README.md file for more details. + */ + + utility::JsonStore + colour_params(const std::string &path, const utility::JsonStore &metadata) { + + utility::JsonStore r(R"({})"_json); + // using the builtin ACES config grom OCIO, assigning the colourspace + // to be Raw + r["ocio_config"] = "cg-config-v1.0.0_aces-v1.3_ocio-v2.1"; + r["input_colorspace"] = "Raw"; + return r; + } +}; + +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>( + {std::make_shared>>( + Uuid("e4e1d569-2338-4e6e-b127-5a9688df161a"), + "Default Media Hook", + "Ted Waine", + "Minimal Hook to set up ACES colour management.", + semver::version("1.0.0"))})); +} +} \ No newline at end of file diff --git a/src/plugin/media_hook/default_hook/test/CMakeLists.txt b/src/plugin/media_hook/default_hook/test/CMakeLists.txt new file mode 100644 index 000000000..66caa1970 --- /dev/null +++ b/src/plugin/media_hook/default_hook/test/CMakeLists.txt @@ -0,0 +1,7 @@ +include(CTest) + +SET(LINK_DEPS + CAF::core +) + +create_tests("${LINK_DEPS}") diff --git a/src/plugin/media_hook/dneg/dnhook/src/CMakeLists.txt b/src/plugin/media_hook/dneg/dnhook/src/CMakeLists.txt index aeba1263d..d419eb1cb 100644 --- a/src/plugin/media_hook/dneg/dnhook/src/CMakeLists.txt +++ b/src/plugin/media_hook/dneg/dnhook/src/CMakeLists.txt @@ -3,7 +3,8 @@ find_package(OpenColorIO CONFIG) SET(LINK_DEPS xstudio::media_hook xstudio::utility + xstudio::module OpenColorIO::OpenColorIO ) -create_plugin_with_alias(media_hook_dneg xstudio::media_hook::dnhook 0.1.0 "${LINK_DEPS}") +create_plugin_with_alias(media_hook_dneg xstudio::media_hook::dnhook ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/plugin/media_hook/dneg/dnhook/src/dneg.cpp b/src/plugin/media_hook/dneg/dnhook/src/dneg.cpp index 167ce6d33..385fd13cc 100644 --- a/src/plugin/media_hook/dneg/dnhook/src/dneg.cpp +++ b/src/plugin/media_hook/dneg/dnhook/src/dneg.cpp @@ -12,7 +12,8 @@ #include "xstudio/utility/string_helpers.hpp" #include "xstudio/utility/json_store.hpp" -namespace fs = std::filesystem; +namespace fs = std::filesystem; +namespace OCIO = OCIO_NAMESPACE; using namespace xstudio; using namespace xstudio::media_hook; @@ -39,11 +40,12 @@ std::optional find_stalk_uuid_from_path(const std::string &path) { } std::optional find_stalk_uuid(const std::string &path) { + // /jobs/SGE/094_bge_1005/SCAN/S_094_bge_1005_bg01_s01_00/505x266/S_094_bge_1005_bg01_s01_00.1049.exr static const std::regex resolution_regex( - R"(^\/?((\/hosts\/[a-z]+fs[0-9]+\/user_data[1-9]{0,1}|\/jobs)\/[^\/]+\/[^\/]+\/.+?)\/+\d+x\d+[^\/]*\/+[^\/]+$)"); + R"(^\/?((\/hosts\/[a-z]+fs[0-9]+\/user_data[1-9]{0,1}|\/jobs|J\:)\/[^\/]+\/[^\/]+\/.+?)\/+\d+x\d+[^\/]*\/+[^\/]+$)"); static const std::regex prefix_regex( - R"(^\/?((\/hosts\/[a-z]+fs[0-9]+\/user_data[1-9]{0,1}|\/jobs)\/[^\/]+\/[^\/]+)\/(.+?)\/([^\/]+)$)"); + R"(^\/?((\/hosts\/[a-z]+fs[0-9]+\/user_data[1-9]{0,1}|\/jobs|J\:)\/[^\/]+\/[^\/]+)\/(.+?)\/([^\/]+)$)"); static const std::regex filename_regex(R"(^([^_]+)_([^.]+).*$)"); std::smatch match; @@ -91,6 +93,12 @@ class DNegMediaHook : public MediaHook { "Some DNEG pipeline movies have metadata indicating if there is a slate frame. " "Enable this option to use the metadata to automatically trim the slate."); + adjust_timecode_ = add_boolean_attribute("Adjust Timecode", "Adjust Timecode", false); + + adjust_timecode_->set_preference_path("/plugin/dneg_media_hook/adjust_timecode"); + adjust_timecode_->expose_in_ui_attrs_group("media_hook_settings"); + adjust_timecode_->set_tool_tip("Use timeline_range from pipequery to adjust timecode."); + slate_trim_behaviour_ = add_string_choice_attribute( "Default Trim Slate Behaviour", "Trim Slate", @@ -109,6 +117,34 @@ class DNegMediaHook : public MediaHook { module::StringChoiceAttribute *slate_trim_behaviour_; module::BooleanAttribute *auto_trim_slate_; + module::BooleanAttribute *adjust_timecode_; + + utility::JsonStore modify_clip_metadata( + const utility::JsonStore &clip_metadata, + const utility::JsonStore &media_metadata) override { + static auto pq_stalk_uuid = json::json_pointer("/metadata/ivy/version/id"); + static auto sg_stalk_uuid = + json::json_pointer("/metadata/shotgun/version/attributes/sg_ivy_dnuuid"); + + auto meta = R"({})"_json; + + if (clip_metadata.contains("DNEG_MEDIA_STALK_DNUUID")) { + meta["DNEG_MEDIA_STALK_DNUUID"] = R"(null)"_json; + } + + try { + if (media_metadata.contains(pq_stalk_uuid)) + meta["DNEG_MEDIA_STALK_DNUUID"] = media_metadata.at(pq_stalk_uuid); + else if (media_metadata.contains(sg_stalk_uuid)) + meta["DNEG_MEDIA_STALK_DNUUID"] = media_metadata.at(sg_stalk_uuid); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return utility::JsonStore(meta); + } + std::optional modify_media_reference( const utility::MediaReference &mr, const utility::JsonStore &jsn) override { @@ -131,10 +167,48 @@ class DNegMediaHook : public MediaHook { changed = true; } } + if (adjust_timecode_->value()) { + // check for ivy timeline_range + try { + const static auto tcp = json::json_pointer("/metadata/ivy/file/timeline_range"); + if (jsn.contains(tcp) and jsn.at(tcp).is_string()) { + auto ifr = FrameList(jsn.at(tcp).get()); + + // there is a slate frame ? + if (ifr.count() + 1 == result.frame_list().count()) { + // offset ifr start.. + auto &ifg = ifr.frame_groups(); + ifg.at(0).set_start(ifg.at(0).start() - 1); + } + + if (result.timecode().total_frames() != ifr.start()) { + // force range.. + if (result.frame_list().size() == 1) { + auto before = result.timecode(); + result.set_timecode( + result.timecode() + + (ifr.start() - + static_cast(result.timecode().total_frames()))); + // spdlog::info( + // "Adjust timecode {} before {} {} after {} {}", + // to_string(result.uri()), + // to_string(before), + // before.total_frames(), + // to_string(result.timecode()), + // result.timecode().total_frames()); + changed = true; + } + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + // we chomp the first frame if internal movie.. // why do we come here multiple times ?? if (not result.frame_list().start() and result.container()) { - auto path = to_string(result.uri().path()); + auto path = std::string(result.uri().path()); if (ends_with(path, ".dneg.mov") or ends_with(path, ".dneg.webm")) { // check metadata.. @@ -180,6 +254,7 @@ class DNegMediaHook : public MediaHook { if (fr.pop_front()) { result.set_frame_list(fr); result.set_timecode(result.timecode() + 1); + result.set_start_frame_offset(result.start_frame_offset() - 1); changed = true; } slate_frames--; @@ -289,13 +364,29 @@ class DNegMediaHook : public MediaHook { std::smatch match; + static const std::regex show_shot_edit_ref_regex( + R"([\/]+jobs\/([^\/]+)\/EDITORIAL\/CUTS\/edit_ref\/([^\/]+))"); + static const std::regex show_shot_regex( - R"([\/]+(hosts\/\w+fs\w+\/user_data[1-9]{0,1}|jobs)\/([^\/]+)\/([^\/]+))"); + R"([\/]+(hosts\/\w+fs\w+\/user_data[1-9]{0,1}|jobs|J\:)\/([^\/]+)\/([^\/]+))"); static const std::regex show_shot_alternative_regex( R"(.+-([^-]+)-([^-]+)\.dneg\.webm$)"); - if (std::regex_search(path, match, show_shot_regex)) { + if (std::regex_search(path, match, show_shot_edit_ref_regex) && match[1] == "FUR") { + // FUR includes the Client_Graded_Rec709 space which fully inverts + // the client look (including primary CDL). Detecting the SHOT for + // edit ref will correctly allow to preview the Client look under the + // Client graded view. + // For other shows where Client_Graded_Rec709 doesn't exists, having + // the edit ref SHOT not detected is actually somewhat better because + // the correct look will be available under both Client and Client graded. + // The risk to view the incorrect look is reduced. + // We should remove this condition in the future when Client_Graded_Rec709 + // is more widely available. + context["SHOW"] = match[1]; + context["SHOT"] = match[2]; + } else if (std::regex_search(path, match, show_shot_regex)) { context["SHOW"] = match[2]; context["SHOT"] = match[3]; } else if (std::regex_search(path, match, show_shot_alternative_regex)) { @@ -306,13 +397,14 @@ class DNegMediaHook : public MediaHook { if (context.count("SHOW")) { r["ocio_context"] = context; - // Detect OCIO config path - const std::string default_config = - fmt::format("/tools/{}/data/colsci/config.ocio", context["SHOW"]); - const std::string ocio_config = - get_showvar_or(context["SHOW"], "OCIO", default_config); - r["ocio_config"] = ocio_config; + std::string ocio_config = find_ocio_config(context["SHOW"]); +#ifdef _WIN32 + r["ocio_config"] = + utility::uri_to_posix_path(utility::posix_path_to_uri(ocio_config)); +#else + r["ocio_config"] = ocio_config; +#endif // Detect the pipeline version of config const std::string pipeline_version = get_showvar_or(context["SHOW"], "DN_COLOR_PIPELINE_VERSION", "1"); @@ -341,7 +433,7 @@ class DNegMediaHook : public MediaHook { static const std::regex review_regex(".+\\.review[0-9]\\.mov$"); static const std::regex internal_regex(".+\\.dneg.mov$"); - const std::string ext = utility::to_lower(fs::path(path).extension()); + const std::string ext = utility::to_lower(fs::path(path).extension().string()); static const std::set linear_ext{".exr", ".sxr", ".mxr", ".movieproc"}; static const std::set log_ext{".cin", ".dpx"}; static const std::set stills_ext{ @@ -369,13 +461,14 @@ class DNegMediaHook : public MediaHook { // Input colour space detection - // Extract OCIO metadata from internal and review proxy movies. + // Extract OCIO metadata from DNEG generated movies std::string media_colorspace; std::string media_display; std::string media_view; - if (std::regex_match(path, review_regex) || - std::regex_match(path, internal_regex)) { + if (input_category == "review_proxy" || input_category == "internal_movie" || + input_category == "movie_media") { + try { const utility::JsonStore &tags = metadata.at("metadata").at("media").at("@").at("format").at("tags"); @@ -397,7 +490,14 @@ class DNegMediaHook : public MediaHook { if (!media_colorspace.empty()) { r["input_colorspace"] = media_colorspace; } else if (!media_display.empty() && !media_view.empty()) { - r["input_colorspace"] = media_view + "_" + media_display; + // Experimental support for Client_Graded_Rec709 in select shows (eg. FUR) + // Always add Client view as fallback for other shows. + if (media_view == "Client graded") { + r["input_colorspace"] = + "Client_Graded_" + media_display + ":Client_" + media_display; + } else { + r["input_colorspace"] = media_view + "_" + media_display; + } } else if (input_category == "review_proxy") { r["input_colorspace"] = "dneg_proxy_log:log"; // LBP review proxy before CMS1 migration (no metadata) @@ -416,15 +516,12 @@ class DNegMediaHook : public MediaHook { r["input_display"] = "Rec709"; r["input_view"] = "Film"; } - } else if (input_category == "edit_ref") { - if (is_cms1_config or has_untonemapped_view) { - r["input_colorspace"] = "disp_Rec709-G24"; - r["working_space"] = "display_linear"; - r["automatic_view"] = "Un-tone-mapped"; + } else if (input_category == "edit_ref" || input_category == "movie_media") { + if (is_cms1_config) { + r["input_colorspace"] = "Client_Graded_Rec709:Client_Rec709"; } else { - r["input_display"] = "Rec709"; - r["input_view"] = "Film"; - r["automatic_view"] = "Film"; + r["input_display"] = "Rec709"; + r["input_view"] = "Film"; } } else if (input_category == "linear_media") { r["input_colorspace"] = "scene_linear:linear"; @@ -433,41 +530,40 @@ class DNegMediaHook : public MediaHook { } else if (input_category == "still_media") { if (is_cms1_config) { r["input_colorspace"] = "DNEG_sRGB"; - r["automatic_view"] = "DNEG"; - } else { - r["input_display"] = "sRGB"; - r["input_view"] = "Film"; - r["automatic_view"] = "Film"; - } - } else if (input_category == "movie_media") { - if (is_cms1_config or has_untonemapped_view) { - r["input_colorspace"] = "disp_Rec709-G24"; - r["working_space"] = "display_linear"; - r["automatic_view"] = "Un-tone-mapped"; } else { - r["input_display"] = "Rec709"; - r["input_view"] = "Film"; - r["automatic_view"] = "Film"; + r["input_display"] = "sRGB"; + r["input_view"] = "Film"; } } - // Detect automatic view assignment in case not found yet - if (!r.count("automatic_view")) { - if (path.find("/ASSET/") != std::string::npos) { - r["automatic_view"] = "DNEG"; - } else if ( - path.find("/out/") != std::string::npos || - path.find("/ELEMENT/") != std::string::npos) { - r["automatic_view"] = is_cms1_config ? "Client graded" : "Film primary"; - } else { - r["automatic_view"] = is_cms1_config ? "Client" : "Film"; - } + // Un-tone-mapped space + if (input_category == "edit_ref" || input_category == "movie_media" || + input_category == "internal_movie") { + r["untonemapped_colorspace"] = "disp_Rec709-G24"; + } else if (input_category == "still_media") { + r["untonemapped_colorspace"] = "disp_sRGB"; + } + + // Detect automatic view assignment + if (input_category == "edit_ref" || input_category == "movie_media" || + input_category == "still_media") { + r["automatic_view"] = + is_cms1_config or has_untonemapped_view ? "Un-tone-mapped" : "Film"; + } else if (path.find("/ASSET/") != std::string::npos) { + r["automatic_view"] = "DNEG"; + } else if ( + path.find("/out/") != std::string::npos || + path.find("/ELEMENT/") != std::string::npos) { + r["automatic_view"] = is_cms1_config ? "Client graded" : "Film primary"; + } else { + r["automatic_view"] = is_cms1_config ? "Client" : "Film"; } // Detect grading CDLs slots to upgrade as GradingPrimary auto dynamic_cdl = utility::JsonStore(); dynamic_cdl["primary"] = is_cms1_config ? "$GRD_PRIMARY" : "GRD_primary"; dynamic_cdl["neutral"] = is_cms1_config ? "$GRD_NEUTRAL" : "GRD_neutral"; + dynamic_cdl["alt"] = is_cms1_config ? "$GRD_ALT" : "GRD_alt"; r["dynamic_cdl"] = dynamic_cdl; // Enable DNEG display detection rules @@ -477,10 +573,85 @@ class DNegMediaHook : public MediaHook { r["ocio_config"] = "__raw__"; r["working_space"] = "raw"; } - return r; } + struct Version { + int major; + int minor; + + bool operator<(const Version &rhs) const { + return major < rhs.major || (major == rhs.major && minor < rhs.minor); + } + bool operator==(const Version &rhs) const { + return major == rhs.major && minor == rhs.minor; + } + bool operator!=(const Version &rhs) const { return !(*this == rhs); } + bool operator<=(const Version &rhs) const { return !(rhs < *this); } + bool operator>(const Version &rhs) const { return rhs < *this; } + bool operator>=(const Version &rhs) const { return !(*this < rhs); } + }; + + // Find highest OCIO config version supported + std::string find_ocio_config(const std::string &show) { + + Version library_version{OCIO_VERSION_MAJOR, OCIO_VERSION_MINOR}; + std::vector fs_versions; + + const std::regex ocio_version_regex(R"(config_ocio-v(\d)\.(\d)\.ocio)"); + std::smatch match; + + // Detect all OCIO versions available in the show's colsci folder + const fs::path colsci_dir{utility::forward_remap_file_path(fmt::format("/tools/{}/data/colsci", show))}; + if (fs::is_directory(colsci_dir)) { + for (auto const &dir_entry : fs::directory_iterator{colsci_dir}) { + if (dir_entry.path().extension() == ".ocio") { + std::string filename = dir_entry.path().filename().string(); + if (std::regex_match(filename, match, ocio_version_regex)) { + if (match.size() == 3) { + fs_versions.push_back({std::stoi(match[1]), std::stoi(match[2])}); + } + } + } + } + } + + // Pick the first version supported + if (!fs_versions.empty()) { + std::sort(fs_versions.begin(), fs_versions.end()); + for (auto it = fs_versions.rbegin(); it != fs_versions.rend(); ++it) { + if (*it <= library_version) { + return (colsci_dir / + fmt::format("config_ocio-v{}.{}.ocio", it->major, it->minor)).string(); + } + } + } + + // Return the default version otherwise + return fmt::format("/tools/{}/data/colsci/config.ocio", show); + } + + std::string detect_display( + const std::string &name, + const std::string &model, + const std::string &manufacturer, + const std::string &serialNumber, + const utility::JsonStore &meta) override { + + if (meta.contains("ocio_config")) { + try { + const std::string config_name = meta["ocio_config"]; + auto config = OCIO::Config::CreateFromFile(config_name.c_str()); + const std::string device = manufacturer + " " + model; + return dneg_ocio_default_display(config, device); + } catch (...) { + // pass + } + } + + return ""; + } + std::string get_showvar_or( const std::string &show, const std::string &variable, const std::string &default_val) { @@ -507,7 +678,11 @@ class DNegMediaHook : public MediaHook { std::map variables; try { +#ifdef __linux__ std::ifstream ifs(fmt::format("/tools/{}/data/general.dat", show)); +#else + std::ifstream ifs(fmt::format("N:\\{}/data/general.dat", show)); +#endif if (!ifs.is_open()) return {}; @@ -530,6 +705,173 @@ class DNegMediaHook : public MediaHook { return variables; } + std::string get_playback_display() { + const std::string CONST_PLAYBACK_FILE = "/var/playback/sys-config.yaml"; + + std::map CONST_DISPLAY = { + {"cinema", "DCI-P3"}, {"hdr", "HDR"}, {"playback", "Playback"}}; + + std::fstream data_load; + data_load.open(CONST_PLAYBACK_FILE, std::ios::in); + + if (data_load.is_open()) { + std::string temp; + while (std::getline(data_load, temp)) { + if (temp.find("cinema") != std::string::npos) { + return CONST_DISPLAY["cinema"]; + } else if (temp.find("hdr") != std::string::npos) { + return CONST_DISPLAY["hdr"]; + } + } + data_load.close(); + } + + return CONST_DISPLAY["playback"]; + } + + std::string dneg_ocio_default_display( + const OCIO::ConstConfigRcPtr &ocio_config, const std::string &device) { + std::string display = ""; + std::map> display_views; + std::vector displays; + + std::map CONST_DISPLAY = { + {"srgb", "sRGB"}, {"eizo", "EIZO"}, {"hdr", "HDR"}, {"playback", "Playback"}}; + + // Helper lambda to check for string in displays vector + auto check_displays = [&displays](std::string &check_string) { + if (std::find(displays.begin(), displays.end(), check_string) != displays.end()) { + return true; + } + + return false; + }; + + // Get the Hostname of the machine + const char *hostname_env = std::getenv("HOSTNAME"); + std::string hostname; + std::string hostname_lower; + + if (hostname_env && *hostname_env) { + hostname = hostname_env; + hostname_lower = hostname; + } + hostname[0] = std::toupper(hostname[0]); + + std::transform( + hostname_lower.begin(), hostname_lower.end(), hostname_lower.begin(), ::tolower); + + // Get ocio config and + const std::string default_display = ocio_config->getDefaultDisplay(); + + // Parse display views + for (int i = 0; i < ocio_config->getNumDisplays(); ++i) { + const std::string display = ocio_config->getDisplay(i); + displays.push_back(display); + + display_views[display] = std::vector(); + for (int j = 0; j < ocio_config->getNumViews(display.c_str()); ++j) { + const std::string view = ocio_config->getView(display.c_str(), j); + display_views[display].push_back(view); + } + } + + // Check for India sRGB lock + const char *site_name_env = std::getenv("DN_SITE"); + const bool srgb_calibration = (bool)std::getenv("DN_SRGB_CALIBRATION"); + std::string site_name; + + if (site_name_env && *site_name_env) { + site_name = site_name_env; + } + + if (((site_name == "mumbai") || (site_name == "chennai")) && srgb_calibration) { + // Return sRGB + return CONST_DISPLAY["srgb"]; + } + + // At-desk monitor Logic + std::string device_upper = xstudio::utility::to_upper(device); + + // EIZO monitors + if (device_upper.find(CONST_DISPLAY["eizo"]) != std::string::npos) { + /* + NOTE: this rule is kept for backward compatibility only, a time + where each EIZO model had its specific calibration. + Some OCIO configs have a display per Device model, whereas others + have one for all EIZO monitors + */ + if (check_displays(CONST_DISPLAY["eizo"])) { + display = CONST_DISPLAY["eizo"]; + } + /* + Try to match the model exactly something like this is expected + 'Eizo CG247X (DFP-0)', in the OCIO config, this would be EIZO247X + */ + else { + std::regex expression("CG([0-9A-Z]+)"); + std::smatch match; + if (std::regex_search(device_upper, match, expression)) { + std::string config_name = "EIZO" + match.str(1); + if (check_displays(config_name)) { + display = config_name; + } + } + } + + if (display.empty()) { + display = default_display; + spdlog::warn("Could not find OCIO Display for device " + device); + } + } + // DELL monitors + else if (device_upper.find("DELL") != std::string::npos) { + if (check_displays(CONST_DISPLAY["srgb"])) { + display = CONST_DISPLAY["srgb"]; + } else { + display = default_display; + } + } + + // Playback room logic + + // KONA video cards + else if (device_upper.find("KONA") != std::string::npos) { + if (check_displays(CONST_DISPLAY["hdr"])) { + display = CONST_DISPLAY["hdr"]; + } else { + display = default_display; + spdlog::warn("Could not set HDR as the display for device " + device); + } + } + + /* + Fallback: Projectors + New style is to have a 'Playback' display. Fallback to old-style where + we check whether the hostname is one of the display options. + Note that for DCI, playback display default view is a straight + DCI-P3 output (playback characterization tranforms are identity matrix + and 1D LUT). + */ + + else if ( + hostname_lower.find("playback") != std::string::npos && + check_displays(CONST_DISPLAY["playback"])) { + display = get_playback_display(); + } + + else if (check_displays(hostname)) { + display = hostname; + } + + // Fall back to default + if (display.empty()) { + display = default_display; + } + return display; + } + + std::map> show_variables_store_; }; @@ -544,4 +886,4 @@ plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { "DNeg Media Hook", semver::version("1.0.0"))})); } -} +} \ No newline at end of file diff --git a/src/plugin/media_hook/dneg/dnhook/test/CMakeLists.txt b/src/plugin/media_hook/dneg/dnhook/test/CMakeLists.txt index a73825679..66caa1970 100644 --- a/src/plugin/media_hook/dneg/dnhook/test/CMakeLists.txt +++ b/src/plugin/media_hook/dneg/dnhook/test/CMakeLists.txt @@ -1,7 +1,7 @@ include(CTest) SET(LINK_DEPS - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/plugin/media_metadata/ffprobe/src/CMakeLists.txt b/src/plugin/media_metadata/ffprobe/src/CMakeLists.txt index a32a558e3..639e8c9fa 100644 --- a/src/plugin/media_metadata/ffprobe/src/CMakeLists.txt +++ b/src/plugin/media_metadata/ffprobe/src/CMakeLists.txt @@ -1,12 +1,23 @@ -find_package(FFMPEG REQUIRED COMPONENTS avcodec avformat swscale avutil) configure_file(.clang-tidy .clang-tidy) -SET(LINK_DEPS - xstudio::media_metadata - FFMPEG::avcodec - FFMPEG::avformat - FFMPEG::swscale - FFMPEG::avutil -) +if (${USE_VCPKG}) + find_package(FFMPEG REQUIRED) + SET(LINK_DEPS + xstudio::media_metadata + PRIVATE ${FFMPEG_LIBRARIES} + ) + create_plugin_with_alias(media_metadata_ffprobe xstudio::media_metadata::ffprobe ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") + target_include_directories(media_metadata_ffprobe PRIVATE ${FFMPEG_INCLUDE_DIRS}) + target_link_directories(media_metadata_ffprobe PRIVATE ${FFMPEG_LIBRARY_DIRS}) -create_plugin_with_alias(media_metadata_ffprobe xstudio::media_metadata::ffprobe 0.1.0 "${LINK_DEPS}") +else() + find_package(FFMPEG REQUIRED COMPONENTS avcodec avformat swscale avutil) + SET(LINK_DEPS + xstudio::media_metadata + FFMPEG::avcodec + FFMPEG::avformat + FFMPEG::swscale + FFMPEG::avutil + ) + create_plugin_with_alias(media_metadata_ffprobe xstudio::media_metadata::ffprobe ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") +endif() diff --git a/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp b/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp index b420339d9..742793245 100644 --- a/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp +++ b/src/plugin/media_metadata/ffprobe/src/ffprobe_lib.cpp @@ -484,6 +484,21 @@ nlohmann::json populate_streams(MediaFile &src) { return result; } + +std::string uri_convert(const caf::uri &uri) { + + // Note, turning off non-file uri support for now + return utility::uri_to_posix_path(uri); + + // This may be a kettle of fish. + + // uri like https://aswf.s3-accelerate.amazonaws.com/ALab_h264_MOVs/mk020_0220.mov + // can be passed through. + // uri like file://localhost/user_data/my_vid.mov needs the 'localhost' removed. + /* auto path = to_string(uri); + utility::replace_string_in_place(path, "file://localhost", "file:"); + return path;*/ +} } // namespace @@ -496,7 +511,8 @@ FFProbe::~FFProbe() { avformat_network_deinit(); } utility::JsonStore FFProbe::probe_file(const caf::uri &uri_path) { auto result = R"({})"_json; - auto ptr = open_file(utility::uri_to_posix_path(uri_path)); + + auto ptr = open_file(uri_convert(uri_path)); if (ptr) { try { diff --git a/src/plugin/media_metadata/openexr/src/CMakeLists.txt b/src/plugin/media_metadata/openexr/src/CMakeLists.txt index dd2a4a52f..f04b83ba8 100644 --- a/src/plugin/media_metadata/openexr/src/CMakeLists.txt +++ b/src/plugin/media_metadata/openexr/src/CMakeLists.txt @@ -7,4 +7,4 @@ SET(LINK_DEPS Imath::Imath ) -create_plugin_with_alias(media_metadata_openexr xstudio::media_metadata::openexr 0.1.0 "${LINK_DEPS}") +create_plugin_with_alias(media_metadata_openexr xstudio::media_metadata::openexr ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/plugin/media_metadata/openexr/test/CMakeLists.txt b/src/plugin/media_metadata/openexr/test/CMakeLists.txt index a73825679..66caa1970 100644 --- a/src/plugin/media_metadata/openexr/test/CMakeLists.txt +++ b/src/plugin/media_metadata/openexr/test/CMakeLists.txt @@ -1,7 +1,7 @@ include(CTest) SET(LINK_DEPS - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/plugin/media_reader/blank/src/CMakeLists.txt b/src/plugin/media_reader/blank/src/CMakeLists.txt index 5fb4caf17..78c08783b 100644 --- a/src/plugin/media_reader/blank/src/CMakeLists.txt +++ b/src/plugin/media_reader/blank/src/CMakeLists.txt @@ -1,8 +1,5 @@ -find_package(GLEW REQUIRED) - SET(LINK_DEPS xstudio::media_reader - GLEW::GLEW ) -create_plugin_with_alias(media_reader_blank xstudio::media_reader::blank 0.1.0 "${LINK_DEPS}") +create_plugin_with_alias(media_reader_blank xstudio::media_reader::blank ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/plugin/media_reader/blank/src/blank.cpp b/src/plugin/media_reader/blank/src/blank.cpp index 7b9c59d16..3d8821221 100644 --- a/src/plugin/media_reader/blank/src/blank.cpp +++ b/src/plugin/media_reader/blank/src/blank.cpp @@ -36,7 +36,7 @@ vec4 fetch_rgba_pixel(ivec2 image_coord) int bytes_per_pixel = 4; int pixel_bytes_offset_in_texture_memory = (image_coord.x + image_coord.y*blank_width)*bytes_per_pixel; uvec4 c = get_image_data_4bytes(pixel_bytes_offset_in_texture_memory); - return vec4(float(c.x)/255.0f,float(c.y)/255.0f,float(c.z)/255.0f,1.0f); + return vec4(float(c.x)/255.0f,float(c.y)/255.0f,float(c.z)/255.0f,0.1f); } )"}; @@ -51,7 +51,7 @@ utility::Uuid BlankMediaReader::plugin_uuid() const { return s_plugin_uuid; } std::array BlankMediaReader::get_colour(const std::string &colour) const { if (colour == "gray") - return std::array{std::byte{0x20}, std::byte{0x20}, std::byte{0x20}}; + return std::array{std::byte{0x10}, std::byte{0x10}, std::byte{0x10}}; else if (colour == "green") return std::array{std::byte{0x20}, std::byte{0xff}, std::byte{0x20}}; @@ -60,8 +60,8 @@ std::array BlankMediaReader::get_colour(const std::string &colour) ImageBufPtr BlankMediaReader::image(const media::AVFrameID &mpr) { ImageBufPtr buf; - int width = 128; - int height = 128; + int width = 192; + int height = 108; size_t size = width * height; int bytes_per_channel = 1; @@ -91,17 +91,25 @@ ImageBufPtr BlankMediaReader::image(const media::AVFrameID &mpr) { buf->set_shader(blank_shader); buf->set_image_dimensions(Imath::V2i(width, height)); - if (mpr.error_ != "") { - buf->set_error(mpr.error_); + if (mpr.error() != "") { + buf->set_error(mpr.error()); } - auto i = mpr.uri_.query().find("colour"); - if (i != mpr.uri_.query().end()) { + auto i = mpr.uri().query().find("colour"); + if (i != mpr.uri().query().end()) { auto c = get_colour(i->second); + int i = 0; for (byte *b = buf->buffer(); b < (buf->buffer() + size * bytes_per_pixel); b += 4) { - b[0] = (byte)c[0]; - b[1] = (byte)c[1]; - b[2] = (byte)c[2]; + if (((i / 16) & 1) == (i / (192 * 16) & 1)) { + b[0] = (byte)c[0]; + b[1] = (byte)c[1]; + b[2] = (byte)c[2]; + } else { + b[0] = (byte)0; + b[1] = (byte)0; + b[2] = (byte)0; + } + ++i; } } else { std::memset(buf->buffer(), 0, size * bytes_per_pixel); @@ -115,8 +123,8 @@ BlankMediaReader::thumbnail(const media::AVFrameID &mpr, const size_t thumb_size auto thumb = std::make_shared( thumb_size, thumb_size, thumbnail::TF_RGB24); - auto i = mpr.uri_.query().find("colour"); - if (i != mpr.uri_.query().end()) { + auto i = mpr.uri().query().find("colour"); + if (i != mpr.uri().query().end()) { auto c = get_colour(i->second); auto b = &(thumb->data()[0]); @@ -139,7 +147,7 @@ MRCertainty BlankMediaReader::supported(const caf::uri &uri, const std::array #include #include -#ifdef __linux__ + +#ifndef _WIN32 #include #endif @@ -29,14 +30,77 @@ using namespace xstudio::utility; namespace { +static Uuid blankshader_uuid{"d6c8722b-dc2a-42f9-981d-a2485c6ceea1"}; + +static std::string blankshader{R"( +#version 330 core +uniform int blank_width; +uniform int dummy; + +// forward declaration +uvec4 get_image_data_4bytes(int byte_address); + +vec4 fetch_rgba_pixel(ivec2 image_coord) +{ + int bytes_per_pixel = 4; + int pixel_bytes_offset_in_texture_memory = (image_coord.x + image_coord.y*blank_width)*bytes_per_pixel; + uvec4 c = get_image_data_4bytes(pixel_bytes_offset_in_texture_memory); + return vec4(float(c.x)/255.0f,float(c.y)/255.0f,float(c.z)/255.0f,1.0f); +} +)"}; + +static ui::viewport::GPUShaderPtr + blank_shader(new ui::opengl::OpenGLShader(blankshader_uuid, blankshader)); + +ImageBufPtr make_blank_image() { + + ImageBufPtr buf; + int width = 192; + int height = 108; + size_t size = width * height; + int bytes_per_channel = 1; + + // we are totally free to choose the pixel layout, but we need to unpack + // in the shader. RGBA 4 bytes matches underlying texture format, so most + // simple option. + int bytes_per_pixel = 4 * bytes_per_channel; + + JsonStore jsn; + jsn["blank_width"] = width; + + buf.reset(new ImageBuffer(blankshader_uuid, jsn)); + buf->allocate(size * bytes_per_pixel); + buf->set_shader(blank_shader); + buf->set_image_dimensions(Imath::V2i(width, height)); + + std::array c = {48, 48, 48}; + int i = 0; + uint8_t *b = (uint8_t *)buf->buffer(); + while (i < size) { + if (((i / 16) & 1) == (i / (192 * 16) & 1)) { + b[0] = c[0]; + b[1] = c[1]; + b[2] = c[2]; + } else { + b[0] = 0; + b[1] = 0; + b[2] = 0; + } + b += 4; // buf is implicitly rgba + ++i; + } + return buf; +} + + static Uuid s_plugin_uuid("87557f93-55f8-4650-8905-4834f1f4b78d"); static Uuid ffmpeg_shader_uuid_yuv{"9854e7c0-2e32-4600-aedd-463b2a6de95a"}; static Uuid ffmpeg_shader_uuid_rgb{"20015805-0b83-426a-bf7e-f6549226bfef"}; static std::string the_shader_yuv = {R"( -#version 430 core -uniform ivec2 image_dims; +#version 410 core uniform ivec2 texture_dims; +uniform int frame_width_pixels; uniform int rgb; uniform int y_linesize; uniform int u_linesize; @@ -94,7 +158,7 @@ vec4 fetch_rgba_pixel(ivec2 image_coord) yuv_tex_lookup_10bit(uv_coord, v_plane_bytes_offset, v_linesize) ); - if (half_scale_uvx && ((image_coord.x & 1) == 1) && image_coord.x*2 < image_dims.x) { + if (half_scale_uvx && ((image_coord.x & 1) == 1) && image_coord.x*2 < frame_width_pixels) { uv_coord.x = uv_coord.x + 1; ivec3 yuv2 = ivec3(yuv.x, @@ -125,8 +189,7 @@ vec4 fetch_rgba_pixel(ivec2 image_coord) )"}; static std::string the_shader_rgb = {R"( -#version 430 core -uniform ivec2 image_dims; +#version 410 core uniform ivec2 texture_dims; uniform int rgb; uniform int y_linesize; @@ -162,7 +225,7 @@ vec4 fetch_rgba_pixel_from_rgba32(ivec2 image_coord) int address = image_coord.x*4 + image_coord.y*y_linesize; uvec4 rgba = get_image_data_4bytes(address); if (rgb == 3) { // AV_PIX_FMT_ARGB - rgba.xyzw = rgba.wxyz; + rgba.xyzw = rgba.yzwx; } else if (rgb == 4) { // AV_PIX_FMT_RGBA //nope } else if (rgb == 5) { // AV_PIX_FMT_ABGR @@ -238,6 +301,43 @@ static ui::viewport::GPUShaderPtr static ui::viewport::GPUShaderPtr ffmpeg_shader_rgb(new ui::opengl::OpenGLShader(ffmpeg_shader_uuid_rgb, the_shader_rgb)); +// See 'uri_convert' - I'm doing this because 'uri_to_posix_path' which is used +// in most places we need to go from uri to filsystem can't deal with uris like +// https://aswf.s3-accelerate.amazonaws.com/ALab_h264_MOVs/mk020_0220.mov. For +// FFMPEG reader, we might want to access media via https and other protocols +// so I have avoided using uri_to_posix_path +std::string uri_decode(const std::string &eString) { + std::string ret; + char ch; + unsigned int i, j; + for (i = 0; i < eString.length(); i++) { + if (int(eString[i]) == 37) { + sscanf(eString.substr(i + 1, 2).c_str(), "%x", &j); + ch = static_cast(j); + ret += ch; + i = i + 2; + } else { + ret += eString[i]; + } + } + return (ret); +} + +std::string uri_convert(const caf::uri &uri) { + + // Note, turning off non-file uri support for now + return utility::uri_to_posix_path(uri); + + // This may be a kettle of fish. + + // uri like https://aswf.s3-accelerate.amazonaws.com/ALab_h264_MOVs/mk020_0220.mov + // can be passed through. + // uri like file://localhost/user_data/my_vid.mov needs the 'localhost' removed. + /*auto path = to_string(uri); + utility::replace_string_in_place(path, "file://localhost", "file:"); + return forward_remap_file_path(uri_decode(path));*/ +} + } // namespace @@ -263,44 +363,69 @@ FFMpegMediaReader::FFMpegMediaReader(const utility::JsonStore &prefs) utility::Uuid FFMpegMediaReader::plugin_uuid() const { return s_plugin_uuid; } void FFMpegMediaReader::update_preferences(const utility::JsonStore &prefs) { + try { + readers_per_source_ = preference_value(prefs, "/plugin/media_reader/FFMPEG/readers_per_source"); -#ifdef __linux__ - soundcard_sample_rate_ = - preference_value(prefs, "/core/audio/pulse_audio_prefs/sample_rate"); -#endif + #ifdef _WIN32 soundcard_sample_rate_ = preference_value(prefs, "/core/audio/windows_audio_prefs/sample_rate"); +#else + soundcard_sample_rate_ = + preference_value(prefs, "/core/audio/pulse_audio_prefs/sample_rate"); #endif + default_rate_ = utility::FrameRate( + preference_value(prefs, "/core/session/media_rate")); + } catch (const std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } ImageBufPtr FFMpegMediaReader::image(const media::AVFrameID &mptr) { - std::string path = uri_to_posix_path(mptr.uri_); - if (last_decoded_image_ && last_decoded_image_->media_key() == mptr.key_) { - return last_decoded_image_; - } + ImageBufPtr rt; - if (!decoder || decoder->path() != path) { - decoder.reset(new FFMpegDecoder(path, soundcard_sample_rate_, mptr.stream_id_)); + if (mptr.stream_id() == "stream -1") { + // dummy stream, return empty image + ImageBufPtr blank = make_blank_image(); + if (mptr.error() != "") { + blank->set_error(mptr.error()); + } + return blank; } - ImageBufPtr rt; - decoder->decode_video_frame(mptr.frame_, rt); + try { + std::string path = uri_convert(mptr.uri()); - if (rt && !rt->shader_params().is_null()) { - if (rt->shader_params().value("rgb", 0) != 0) { - rt->set_shader(ffmpeg_shader_rgb); - } else { - rt->set_shader(ffmpeg_shader_yuv); + if (last_decoded_image_ && last_decoded_image_->media_key() == mptr.key()) { + return last_decoded_image_; + } + + if (!decoder || decoder->path() != path) { + decoder.reset(new FFMpegDecoder( + path, soundcard_sample_rate_, default_rate_, mptr.stream_id())); + } + + decoder->decode_video_frame(mptr.frame(), rt); + + if (rt && !rt->shader_params().is_null()) { + if (rt->shader_params().value("rgb", 0) != 0) { + rt->set_shader(ffmpeg_shader_rgb); + } else { + rt->set_shader(ffmpeg_shader_yuv); + } + last_decoded_image_ = rt; + } + + } catch (std::exception &e) { + rt = make_blank_image(); + if (mptr.error() != "") { + rt->set_error(e.what()); } - last_decoded_image_ = rt; } return rt; @@ -312,19 +437,19 @@ AudioBufPtr FFMpegMediaReader::audio(const media::AVFrameID &mptr) { // Set the path for the media file. Currently, it's hard-coded to a specific file. // This may be updated later to use the URI from the AVFrameID object. - std::string path = uri_to_posix_path(mptr.uri_); + std::string path = uri_convert(mptr.uri()); // If the audio_decoder object doesn't exist or the path it's using differs // from the one we're interested in, then create a new audio_decoder. if (!audio_decoder || audio_decoder->path() != path) { - audio_decoder.reset( - new FFMpegDecoder(path, soundcard_sample_rate_, mptr.stream_id_)); + audio_decoder.reset(new FFMpegDecoder( + path, soundcard_sample_rate_, default_rate_, mptr.stream_id())); } AudioBufPtr rt; // Decode the audio frame using the decoder and get the resulting audio buffer. - audio_decoder->decode_audio_frame(mptr.frame_, rt); + audio_decoder->decode_audio_frame(mptr.frame(), rt); // If decoding didn't produce an audio buffer (i.e., rt is null), then initialize // a new empty audio buffer. @@ -347,22 +472,29 @@ AudioBufPtr FFMpegMediaReader::audio(const media::AVFrameID &mptr) { xstudio::media::MediaDetail FFMpegMediaReader::detail(const caf::uri &uri) const { - FFMpegDecoder t_decoder(uri_to_posix_path(uri), soundcard_sample_rate_); + FFMpegDecoder t_decoder(uri_convert(uri), soundcard_sample_rate_, default_rate_); // N.B. MediaDetail needs frame duration, so invert frame rate std::vector streams; + bool have_video_stream = false; + bool have_audio_stream = false; + + for (auto &p : t_decoder.streams()) { if (p.second->codec_type() == AVMEDIA_TYPE_VIDEO || p.second->codec_type() == AVMEDIA_TYPE_AUDIO) { auto frameRate = t_decoder.frame_rate(p.first); + have_audio_stream |= p.second->codec_type() == AVMEDIA_TYPE_AUDIO; + have_video_stream |= p.second->codec_type() == AVMEDIA_TYPE_VIDEO; + // If the stream has a duration of 1 then it is probably frame based. // FFMPEG assigns a default frame rate of 25fps to JPEGs, for example - // If this has happened, we want to ignore this and let xstudio apply // xSTUDIO's default frame rate preference instead. - if (t_decoder.duration_frames() == 1 && - frameRate.to_flicks() == timebase::flicks(28224000)) { + if ((t_decoder.duration_frames() == 1 && + frameRate.to_flicks() == timebase::flicks(28224000))) { // setting a null frame rate will make xstudio use its own preference frameRate = utility::FrameRate(); } @@ -380,6 +512,25 @@ xstudio::media::MediaDetail FFMpegMediaReader::detail(const caf::uri &uri) const } } + + // Leaving this here - dummy video streams was a bad idea because audio only + // sources were showing up in the list of valid video sources. + /*if (have_audio_stream && !have_video_stream) { + // audio only source. We need a dummy video stream, because xstudio playheads + // currently require an media::MT_IMAGE type media stream to be available for + // a given source. The duration of the source is always inferred from the + // active video track. This is easily done by adding a phoney video stream with + // the same duration as the first audio stream: + streams.emplace_back(media::StreamDetail( + streams[0].duration_, + "stream -1", + media::MT_IMAGE, + "{0}@{1}/{2}", + Imath::V2f(1920, 1080), + 1.0f, + -1)); + }*/ + return xstudio::media::MediaDetail(name(), streams, t_decoder.first_frame_timecode()); } @@ -402,16 +553,16 @@ std::shared_ptr FFMpegMediaReader::thumbnail(const media::AVFrameID &mptr, const size_t thumb_size) { try { - std::string path = uri_to_posix_path(mptr.uri_); + std::string path = uri_convert(mptr.uri()); - // DebugTimer d(path, mptr.frame_); + // DebugTimer d(path, mptr.frame()); if (!thumbnail_decoder || thumbnail_decoder->path() != path) { - thumbnail_decoder.reset( - new FFMpegDecoder(path, soundcard_sample_rate_, mptr.stream_id_)); + thumbnail_decoder.reset(new FFMpegDecoder( + path, soundcard_sample_rate_, default_rate_, mptr.stream_id())); } std::shared_ptr rt = - thumbnail_decoder->decode_thumbnail_frame(mptr.frame_, thumb_size); + thumbnail_decoder->decode_thumbnail_frame(mptr.frame(), thumb_size); // for now immediately deleting the decoder to prevent memory hogging // when generating many thumbnails. This could make the scrubbable @@ -460,10 +611,9 @@ PixelInfo FFMpegMediaReader::ffmpeg_buffer_pixel_picker( const int half_scale_uvy = buf.shader_params().value("half_scale_uvy", 0); const int half_scale_uvx = buf.shader_params().value("half_scale_uvx", 0); const int bits_per_channel = buf.shader_params().value("bits_per_channel", 0); - const Imath::M33f yuv_conv = - buf.shader_params().value("yuv_conv", Imath::M33f()).transposed(); - const Imath::V3i yuv_offsets = buf.shader_params().value("yuv_offsets", Imath::V3i()); - const float norm_coeff = buf.shader_params().value("norm_coeff", 1.0f); + const Imath::M33f yuv_conv = buf.shader_params().value("yuv_conv", Imath::M33f()); + const Imath::V3i yuv_offsets = buf.shader_params().value("yuv_offsets", Imath::V3i()); + const float norm_coeff = buf.shader_params().value("norm_coeff", 1.0f); auto get_image_data_4bytes = [&](const int address) -> std::array { if (address < 0 || address >= (int)buf.size()) @@ -514,7 +664,7 @@ PixelInfo FFMpegMediaReader::ffmpeg_buffer_pixel_picker( auto bytes4 = get_image_data_4bytes(address); Imath::V4f r; if (rgb == 3) { // AV_PIX_FMT_ARGB - r = Imath::V4f(bytes4[3], bytes4[0], bytes4[1], bytes4[2]); + r = Imath::V4f(bytes4[1], bytes4[2], bytes4[3], bytes4[0]); } else if (rgb == 4) { // AV_PIX_FMT_RGBA // nope } else if (rgb == 5) { // AV_PIX_FMT_ABGR diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg.hpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg.hpp index 61e950162..d3352e261 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg.hpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg.hpp @@ -45,8 +45,9 @@ namespace media_reader { std::shared_ptr thumbnail_decoder; int readers_per_source_; - int soundcard_sample_rate_ = {4000}; - int channels_ = 2; + int soundcard_sample_rate_ = {48000}; + int channels_ = 2; + utility::FrameRate default_rate_ = {utility::FrameRate(timebase::k_flicks_24fps)}; ImageBufPtr last_decoded_image_; }; diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg_decoder.cpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg_decoder.cpp index 72c1f640e..c1bc3750c 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_decoder.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_decoder.cpp @@ -23,12 +23,16 @@ std::string make_stream_id(const int stream_index) { int FFMpegDecoder::ffmpeg_threads = 8; FFMpegDecoder::FFMpegDecoder( - std::string path, const int soundcard_sample_rate, std::string stream_id) + std::string path, + const int soundcard_sample_rate, + const utility::FrameRate default_rate, + std::string stream_id) : movie_file_path_(std::move(path)), last_requested_frame_(-100), avc_packet_(nullptr), av_format_ctx_(nullptr), soundcard_sample_rate_(soundcard_sample_rate), + default_rate_(default_rate), last_decoded_frame_(-100), stream_id_(std::move(stream_id)) @@ -38,7 +42,6 @@ FFMpegDecoder::FFMpegDecoder( FFMpegDecoder::~FFMpegDecoder() { close_handles(); } - void FFMpegDecoder::open_handles() { if (not avc_packet_) { @@ -133,8 +136,7 @@ void FFMpegDecoder::calc_duration_frames() { // account as it streams audio frames to the soundcard. for (auto &stream : streams_) { - stream.second->set_virtual_frame_rate( - utility::FrameRate(timebase::k_flicks_one_sixtieth_second)); + stream.second->set_virtual_frame_rate(default_rate_); } } @@ -436,6 +438,11 @@ void FFMpegDecoder::decode_video_frame( try { + if (decode_stream_ && decode_stream_->is_attached_pic()) { + image_buffer = decode_stream_->attached_pic(); + return; + } + requested_decode_frame_ = frame_num; decoding_backwards_ = (last_requested_frame_ - frame_num) == 1 || @@ -616,7 +623,6 @@ void FFMpegDecoder::pull_video_buffer_from_stream(StreamPtr &video_stream) { // an appropriate timestamp ... we can then attach audio frames to this // video buffer to send back to playback engine. buf.reset(new ImageBuffer("No Video")); - buf->set_duration_seconds(frame_rate().to_seconds()); buf->set_display_timestamp_seconds(requested_decode_frame_ * frame_rate().to_seconds()); buf->set_decoder_frame_number(requested_decode_frame_); } @@ -648,6 +654,7 @@ void FFMpegDecoder::pull_audio_buffer_from_stream(StreamPtr &audio_stream) { // the samples in the audio output loop. double xstudio_frame_dts = virtual_frame * audio_stream->frame_rate().to_seconds(); double ffmpeg_frame_dts = buf->display_timestamp_seconds(); + buf->set_time_delta_to_video_frame(std::chrono::microseconds( (int)round((ffmpeg_frame_dts - xstudio_frame_dts) * 1000000.0))); diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg_decoder.hpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg_decoder.hpp index e346f7a1c..628fbaf96 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_decoder.hpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_decoder.hpp @@ -12,7 +12,10 @@ namespace media_reader { class FFMpegDecoder { public: FFMpegDecoder( - std::string path, const int soundcard_sample_rate, std::string stream_id = ""); + std::string path, + const int soundcard_sample_rate, + const utility::FrameRate default_rate, + std::string stream_id = ""); ~FFMpegDecoder(); @@ -79,6 +82,7 @@ namespace media_reader { const int soundcard_sample_rate_; int64_t duration_frames_; const std::string stream_id_; + const utility::FrameRate default_rate_; }; } // namespace ffmpeg } // namespace media_reader diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp index f8ee75459..2e76ebfe9 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include "ffmpeg_stream.hpp" #include "xstudio/media/media_error.hpp" @@ -22,8 +21,13 @@ void xstudio::media_reader::ffmpeg::AVC_CHECK_THROW(int errorNum, const char *av std::array buf; if (!av_strerror(errorNum, buf.data(), buf.size())) { - throw media_corrupt_error( - std::string() + avc_command + " " + buf.data() + " (ffmpeg reader)."); + if (-errorNum == ENOENT) { + throw media_missing_error( + std::string() + avc_command + " " + buf.data() + " (ffmpeg reader)."); + } else { + throw media_corrupt_error( + std::string() + avc_command + " " + buf.data() + " (ffmpeg reader)."); + } } else { throw media_corrupt_error( std::string("FFMPEG reader: ") + avc_command + " unknown error num " + @@ -164,23 +168,12 @@ void set_shader_pix_format_info( yuv_to_rgb *= scale; Imath::V3f offset(16, 128, 128); - offset *= std::pow(2.0f, float(bitdepth - 8)); + offset *= std::pow(2, bitdepth - 8); jsn["yuv_offsets"] = {"ivec3", 1, offset[0], offset[1], offset[2]}; } } - jsn["yuv_conv"] = { - "mat3", - 1, - yuv_to_rgb[0][0], - yuv_to_rgb[0][1], - yuv_to_rgb[0][2], - yuv_to_rgb[1][0], - yuv_to_rgb[1][1], - yuv_to_rgb[1][2], - yuv_to_rgb[2][0], - yuv_to_rgb[2][1], - yuv_to_rgb[2][2]}; + jsn["yuv_conv"] = yuv_to_rgb.transposed(); } @@ -274,6 +267,10 @@ ImageBufPtr FFMpegStream::get_ffmpeg_frame_as_xstudio_image() { if (shader_supported_pix_formats.find(ffmpeg_pixel_format) == shader_supported_pix_formats.end()) { + if (ffmpeg_pixel_format == -1) { + throw media_corrupt_error("FFMPEG could not decode the image."); + } + if (!format_conversion_warning_issued) { format_conversion_warning_issued = true; spdlog::warn( @@ -286,6 +283,7 @@ ImageBufPtr FFMpegStream::get_ffmpeg_frame_as_xstudio_image() { image_buffer.reset(new ImageBuffer()); auto buffer = (uint8_t *)image_buffer->allocate(4 * frame->width * frame->height); // not one of the ffmpeg pixel formats that our shader can deal with, so convert to + // something we can sws_context_ = sws_getCachedContext( sws_context_, @@ -383,6 +381,8 @@ ImageBufPtr FFMpegStream::get_ffmpeg_frame_as_xstudio_image() { jsn["a_plane_bytes_offset"] = offsets[3]; } + jsn["frame_width_pixels"] = frame->width; + image_buffer->set_image_dimensions(Imath::V2i(frame->width, frame->height)); set_shader_pix_format_info( @@ -398,21 +398,6 @@ ImageBufPtr FFMpegStream::get_ffmpeg_frame_as_xstudio_image() { double(frame->pts) * double(avc_stream_->time_base.num) / double(avc_stream_->time_base.den)); - AVRational aspect = av_guess_sample_aspect_ratio(format_context_, avc_stream_, frame); - - if (aspect.den && aspect.num) { - image_buffer->set_pixel_aspect(float(aspect.num) / float(aspect.den)); - } else { - image_buffer->set_pixel_aspect(1.0f); - } - - if (fpsNum_) { - if (fpsDen_) - image_buffer->set_duration_seconds(double(fpsDen_) / double(fpsNum_)); - else - image_buffer->set_duration_seconds(1.0 / double(fpsNum_)); - } - // determine if image has alpha - if planar, look for 'a_linesize' != 0. // Otherwise check for interleaved RGB pix formats that have an alpha int rgb_format_code = jsn.value("rgb", 0); @@ -521,7 +506,7 @@ AudioBufPtr FFMpegStream::get_ffmpeg_frame_as_xstudio_audio(const int soundcard_ case audio::SampleFormat::INT16: target_sample_format_ = AV_SAMPLE_FMT_S16; break; - case audio::SampleFormat::INT32: + case audio::SampleFormat::SFINT32: target_sample_format_ = AV_SAMPLE_FMT_S32; break; case audio::SampleFormat::FLOAT32: @@ -536,7 +521,6 @@ AudioBufPtr FFMpegStream::get_ffmpeg_frame_as_xstudio_audio(const int soundcard_ default: throw media_corrupt_error("Audio buffer format is not set."); } - target_sample_rate_ = audio_buffer->sample_rate(); target_audio_channels_ = audio_buffer->num_channels(); @@ -544,18 +528,11 @@ AudioBufPtr FFMpegStream::get_ffmpeg_frame_as_xstudio_audio(const int soundcard_ double(frame->pts) * double(avc_stream_->time_base.num) / double(avc_stream_->time_base.den)); - // spdlog::info( - // "Calculated display timestamp: {} seconds.", - // double(frame->pts) * double(avc_stream_->time_base.num) / - // double(avc_stream_->time_base.den)); - resample_audio(frame, audio_buffer, -1); return audio_buffer; } -static int asd = 0; - FFMpegStream::FFMpegStream( AVFormatContext *fmt_ctx, AVStream *stream, int index, int thread_count, std::string path) : stream_index_(index), @@ -638,21 +615,32 @@ FFMpegStream::FFMpegStream( throw std::runtime_error("No decoder found."); } - // Set the fps if it has been set correctly in the stream - if (avc_stream_->avg_frame_rate.num != 0 && avc_stream_->avg_frame_rate.den != 0) { - fpsNum_ = avc_stream_->avg_frame_rate.num; - fpsDen_ = avc_stream_->avg_frame_rate.den; - frame_rate_ = xstudio::utility::FrameRate( - static_cast(fpsDen_) / static_cast(fpsNum_)); - } else if (avc_stream_->r_frame_rate.num != 0 && avc_stream_->r_frame_rate.den != 0) { - fpsNum_ = avc_stream_->r_frame_rate.num; - fpsDen_ = avc_stream_->r_frame_rate.den; - frame_rate_ = xstudio::utility::FrameRate( - static_cast(fpsDen_) / static_cast(fpsNum_)); + if ((avc_stream_->disposition & AV_DISPOSITION_ATTACHED_PIC) == + AV_DISPOSITION_ATTACHED_PIC) { + + // for attached pic stream, we override frame rate to 24pfs + frame_rate_ = xstudio::utility::FrameRate(timebase::k_flicks_24fps); + is_attached_pic_ = true; + decode_attached_pic(); + } else { - fpsNum_ = 0; - fpsDen_ = 0; - frame_rate_ = xstudio::utility::FrameRate(timebase::k_flicks_24fps); + + // Set the fps if it has been set correctly in the stream + if (avc_stream_->avg_frame_rate.num != 0 && avc_stream_->avg_frame_rate.den != 0) { + fpsNum_ = avc_stream_->avg_frame_rate.num; + fpsDen_ = avc_stream_->avg_frame_rate.den; + frame_rate_ = xstudio::utility::FrameRate( + static_cast(fpsDen_) / static_cast(fpsNum_)); + } else if (avc_stream_->r_frame_rate.num != 0 && avc_stream_->r_frame_rate.den != 0) { + fpsNum_ = avc_stream_->r_frame_rate.num; + fpsDen_ = avc_stream_->r_frame_rate.den; + frame_rate_ = xstudio::utility::FrameRate( + static_cast(fpsDen_) / static_cast(fpsNum_)); + } else { + fpsNum_ = 0; + fpsDen_ = 0; + frame_rate_ = xstudio::utility::FrameRate(timebase::k_flicks_24fps); + } } } @@ -905,12 +893,18 @@ int FFMpegStream::duration_frames() const { } -/* Note 1: this experiment is disabled for now. The idea is that we circumvent -// ffmpeg's buffer allocation and insert our own pixel buffer (owned by video_frame) -// into which ffmpeg decodes. This will avoid the data copy happening in the -// call to FFMPegStream::copy_avframe_to_xstudio_buffer and does indeed speed up -// decoding. 2-3ms for typical HD res frame, more for UHD and so-on. However, the -// approach doesn't work when ffmpeg is doing multithreading on frames as the buffer -// allocation happens out of sync with the decode of a given video frame. Some more -// work could be done to fix that problem and gain a few ms per frame which may -// be needed for high res playback */ +void FFMpegStream::decode_attached_pic() { + + int rx = send_packet(&(avc_stream_->attached_pic)); + av_frame_unref(frame); + int rt = avcodec_receive_frame(codec_context_, frame); + if (rt == 0) { + attached_pic_ = get_ffmpeg_frame_as_xstudio_image(); + } else { + try { + AVC_CHECK_THROW(rt, "avcodec_receive_frame"); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + } +} diff --git a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.hpp b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.hpp index f8a69ef0c..136f06593 100644 --- a/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.hpp +++ b/src/plugin/media_reader/ffmpeg/src/ffmpeg_stream.hpp @@ -83,6 +83,8 @@ namespace media_reader { AudioBufPtr get_ffmpeg_frame_as_xstudio_audio(const int soundcard_sample_rate); + const ImageBufPtr &attached_pic() const { return attached_pic_; } + std::shared_ptr convert_av_frame_to_thumbnail(const size_t size_hint); @@ -101,9 +103,17 @@ namespace media_reader { [[nodiscard]] FFMpegStreamType stream_type() const { return stream_type_; } + [[nodiscard]] Imath::V2i resolution() const { return resolution_; } + + [[nodiscard]] float pixel_aspect() const { return pixel_aspect_; } + [[nodiscard]] int duration_frames() const; - [[nodiscard]] bool is_single_frame() const { return duration_frames() < 2; } + [[nodiscard]] bool is_single_frame() const { + return is_attached_pic_ || duration_frames() < 2; + } + + [[nodiscard]] bool is_attached_pic() const { return is_attached_pic_; } [[nodiscard]] int stream_index() const { return stream_index_; } @@ -111,10 +121,6 @@ namespace media_reader { return is_drop_frame_timecode_; } - [[nodiscard]] Imath::V2i resolution() const { return resolution_; } - - [[nodiscard]] float pixel_aspect() const { return pixel_aspect_; } - [[nodiscard]] double duration_seconds() const; [[nodiscard]] AVDictionary *tags() { return avc_stream_->metadata; } @@ -135,6 +141,8 @@ namespace media_reader { return avc_stream_->start_time != AV_NOPTS_VALUE ? avc_stream_->start_time : 0; } + void decode_attached_pic(); + // void setup_frame(ImageStorePtr & video_frame); int stream_index_; AVCodecContext *codec_context_; @@ -154,6 +162,7 @@ namespace media_reader { int current_frame_ = {CURRENT_FRAME_UNKNOWN}; Imath::V2i resolution_ = {Imath::V2i(0, 0)}; float pixel_aspect_ = 1.0f; + bool is_attached_pic_ = {false}; // for video rescaling SwsContext *sws_context_ = {nullptr}; @@ -168,6 +177,7 @@ namespace media_reader { SwrContext *audio_resampler_ctx_ = {0}; utility::FrameRate frame_rate_; + ImageBufPtr attached_pic_; }; } // namespace ffmpeg } // namespace media_reader diff --git a/src/plugin/media_reader/ffmpeg/test/CMakeLists.txt b/src/plugin/media_reader/ffmpeg/test/CMakeLists.txt index 4dee79665..2a0bc89d6 100644 --- a/src/plugin/media_reader/ffmpeg/test/CMakeLists.txt +++ b/src/plugin/media_reader/ffmpeg/test/CMakeLists.txt @@ -12,6 +12,7 @@ target_link_libraries(ffmpeg_test xstudio::playhead xstudio::timeline xstudio::colour_pipeline + fmt::fmt ${GTEST_LDFLAGS} ) target_include_directories(ffmpeg_test diff --git a/src/plugin/media_reader/openexr/src/CMakeLists.txt b/src/plugin/media_reader/openexr/src/CMakeLists.txt index 330fc8d0b..917210585 100644 --- a/src/plugin/media_reader/openexr/src/CMakeLists.txt +++ b/src/plugin/media_reader/openexr/src/CMakeLists.txt @@ -1,12 +1,10 @@ find_package(OpenEXR) find_package(Imath) -find_package(GLEW) SET(LINK_DEPS xstudio::media_reader - GLEW::GLEW OpenEXR::OpenEXR Imath::Imath ) -create_plugin_with_alias(media_reader_openexr xstudio::media_reader::openexr 0.1.0 "${LINK_DEPS}") +create_plugin_with_alias(media_reader_openexr xstudio::media_reader::openexr ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/plugin/media_reader/openexr/src/openexr.cpp b/src/plugin/media_reader/openexr/src/openexr.cpp index adb12cb5d..f89b320d3 100644 --- a/src/plugin/media_reader/openexr/src/openexr.cpp +++ b/src/plugin/media_reader/openexr/src/openexr.cpp @@ -34,63 +34,53 @@ namespace fs = std::filesystem; - using namespace xstudio; using namespace xstudio::global_store; using namespace xstudio::media_reader; using namespace xstudio::utility; +namespace xstudio { +namespace exr_reader { + bool dump_json_headers(const Imf::Header &h, nlohmann::json &root); +} +} // namespace xstudio namespace { - static Uuid s_plugin_uuid("9fd34c7e-8b35-44c7-8976-387bae1e35e0"); /* Given an image display and data window and a maximum overscan amount, compute the cropped data window that limits the overscan in the data window. -Returns true if a crop is required to meet max overscan requirement +Returns true if a crop is required to meet max overscan requirement*/ bool crop_data_window( - Imath::Box2i &data_window, - const Imath::Box2i &display_window, - const float overscan_percent -) -{ + Imath::Box2i &data_window, + const Imath::Box2i &display_window, + const float overscan_percent) { - const int width = display_window.size().x+1; - const int height = display_window.size().y+1; + const int width = display_window.size().x + 1; + const int height = display_window.size().y + 1; - const Imath::Box2i in_data_window = data_window; + const Imath::Box2i in_data_window = data_window; - data_window.min.x = std::max( - data_window.min.x, - (int)round(-float(width)*overscan_percent/100.0f) - ); + data_window.min.x = + std::max(data_window.min.x, (int)round(-float(width) * overscan_percent / 100.0f)); - data_window.max.x = std::min( - data_window.max.x, - (int)round(float(width)*(overscan_percent/100.0f + 1.0f)) - ); + data_window.max.x = std::min( + data_window.max.x, (int)round(float(width) * (overscan_percent / 100.0f + 1.0f))); - data_window.min.y = std::max( - data_window.min.y, - (int)round(-float(height)*overscan_percent/100.0f) - ); + data_window.min.y = + std::max(data_window.min.y, (int)round(-float(height) * overscan_percent / 100.0f)); - data_window.max.y = std::min( - data_window.max.y, - (int)round(float(height)*(overscan_percent/100.0f + 1.0f)) - ); - - return in_data_window != data_window; + data_window.max.y = std::min( + data_window.max.y, (int)round(float(height) * (overscan_percent / 100.0f + 1.0f))); + return in_data_window != data_window; } -*/ - static Uuid openexr_shader_uuid{"1c9259fc-46a5-11ea-87fe-989096adb429"}; static std::string shader{R"( -#version 430 core +#version 410 core uniform int width; uniform int height; uniform int num_channels; @@ -215,184 +205,155 @@ void OpenEXRMediaReader::update_preferences(const utility::JsonStore &prefs) { } ImageBufPtr OpenEXRMediaReader::image(const media::AVFrameID &mptr) { - try { - std::string path = uri_to_posix_path(mptr.uri_); - - // DebugTimer dd(path); - - Imf::MultiPartInputFile input(path.c_str()); - int parts = input.parts(); - int part_idx = -1; - std::array pix_type; - std::vector exr_channels_to_load; - - for (int prt = 0; prt < parts; ++prt) { - // skip incomplete parts - maybe better error/handling messaging required? - if (!input.partComplete(prt)) - continue; - const Imf::Header &part_header = input.header(prt); - std::vector stream_ids; - stream_ids_from_exr_part(part_header, stream_ids); - for (const auto &stream_id : stream_ids) { - if (stream_id == mptr.stream_id_) { - pix_type = pick_exr_channels_from_stream_id( - part_header, mptr.stream_id_, exr_channels_to_load); - part_idx = prt; - } + std::string path = uri_to_posix_path(mptr.uri()); + + // DebugTimer dd(path); + + Imf::MultiPartInputFile input(path.c_str()); + int parts = input.parts(); + int part_idx = -1; + std::array pix_type; + std::vector exr_channels_to_load; + + for (int prt = 0; prt < parts; ++prt) { + // skip incomplete parts - maybe better error/handling messaging required? + const Imf::Header &part_header = input.header(prt); + std::vector stream_ids; + stream_ids_from_exr_part(part_header, stream_ids); + for (const auto &stream_id : stream_ids) { + if (stream_id == mptr.stream_id()) { + pix_type = pick_exr_channels_from_stream_id( + part_header, mptr.stream_id(), exr_channels_to_load); + part_idx = prt; } } + } - if (part_idx == -1 && mptr.stream_id_ == "Main" && input.partComplete(0)) { - // Older version of exr reader only provided a stream called "Main". - // For backwards compatibility map this to the first stream from the - // first 'part' (which is what you got with the old reader) - const Imf::Header &part_header = input.header(0); - std::vector stream_ids; - stream_ids_from_exr_part(part_header, stream_ids); - if (stream_ids.empty()) { - std::stringstream ss; - ss << "Unable to find readable layer/stream in part 0 for file \"" << path - << "\"\n"; - throw std::runtime_error(ss.str().c_str()); - } - pix_type = pick_exr_channels_from_stream_id( - part_header, stream_ids[0], exr_channels_to_load); - part_idx = 0; - } else if (part_idx == -1) { + if (part_idx == -1 && mptr.stream_id() == "Main") { + // Older version of exr reader only provided a stream called "Main". + // For backwards compatibility map this to the first stream from the + // first 'part' (which is what you got with the old reader) + const Imf::Header &part_header = input.header(0); + std::vector stream_ids; + stream_ids_from_exr_part(part_header, stream_ids); + if (stream_ids.empty()) { std::stringstream ss; - ss << "Failed to pick exr channels for file \"" << path << "\"\n"; + ss << "Unable to find readable layer/stream in part 0 for file \"" << path + << "\"\n"; throw std::runtime_error(ss.str().c_str()); } + pix_type = + pick_exr_channels_from_stream_id(part_header, stream_ids[0], exr_channels_to_load); + part_idx = 0; + } else if (part_idx == -1) { + std::stringstream ss; + ss << "Failed to pick exr channels for file \"" << path << "\"\n"; + throw std::runtime_error(ss.str().c_str()); + } - Imf::InputPart in(input, part_idx); - - Imath::Box2i data_window = in.header().dataWindow(); - Imath::Box2i display_window = in.header().displayWindow(); - - // decide the area of the image we want to load - const bool cropped_data_window = false; /*crop_data_window( - data_window, - display_window, - max_exr_overscan_percent_ - );*/ - - // compute the size of the buffer we need - const size_t n_pixels = (data_window.size().x + 1) * (data_window.size().y + 1); - const size_t bytes_per_channel_r = - (pix_type[0] == -1 ? 0 - : pix_type[0] == Imf::PixelType::HALF ? 2 - : 4); - const size_t bytes_per_channel_g = - (pix_type[1] == -1 ? 0 - : pix_type[1] == Imf::PixelType::HALF ? 2 - : 4); - const size_t bytes_per_channel_b = - (pix_type[2] == -1 ? 0 - : pix_type[2] == Imf::PixelType::HALF ? 2 - : 4); - const size_t bytes_per_channel_a = - (pix_type[3] == -1 ? 0 - : pix_type[3] == Imf::PixelType::HALF ? 2 - : 4); - const size_t bytes_per_pixel = bytes_per_channel_r + bytes_per_channel_g + - bytes_per_channel_b + bytes_per_channel_a; - const size_t buf_size = n_pixels * bytes_per_pixel; - - // const size_t gl_line_size = 8192*4; - // const size_t padded_buf_size = (buf_size & (gl_line_size-1)) ? - // ((buf_size/gl_line_size) + 1)*gl_line_size : buf_size; - - JsonStore jsn; - jsn["num_channels"] = exr_channels_to_load.size(); - jsn["pix_type_r"] = int(pix_type[0]); - jsn["pix_type_g"] = int(pix_type[1]); - jsn["pix_type_b"] = int(pix_type[2]); - jsn["pix_type_a"] = int(pix_type[3]); - jsn["bytes_per_pixel"] = int(bytes_per_pixel); - // jsn["path"] = to_string(mptr.uri_); - - ImageBufPtr buf(new ImageBuffer(openexr_shader_uuid, jsn)); - buf->allocate(buf_size); - buf->set_pixel_aspect(in.header().pixelAspectRatio()); - - // 4th channel is always put into 'alpha' channel as per shader code - // above - buf->set_has_alpha(exr_channels_to_load.size() > 3); - - buf->set_shader(openexr_shader); - buf->set_image_dimensions( - display_window.size(), - Imath::Box2i( - data_window.min, Imath::V2i(data_window.max.x + 1, data_window.max.y + 1))); - - buf->params()["path"] = to_string(mptr.uri_); - buf->params()["channel_names"] = exr_channels_to_load; - buf->params()["stream_id"] = mptr.stream_id_; - - if (cropped_data_window) { - // if we are not loading the whole data window, we need to provide a temporary - // buffer that matches the EXR data window width for OpenEXR to load pixels into, we - // then copy the pixels we want into our cropped image buffer. We do this in chunks - // in the Y dimension to take advantage of OpenEXR decompress threads that are - // (possibly) more efficient when decoding blocks of pixels at once - const Imath::Box2i actual_data_window = in.header().dataWindow(); - const size_t actual_data_window_width = actual_data_window.size().x + 1; - const size_t line_stride = actual_data_window_width * bytes_per_pixel; - const size_t cropped_line_stride = (data_window.size().x + 1) * bytes_per_pixel; - Imf::FrameBuffer fb; - - std::vector tmp_buf( - bytes_per_pixel * actual_data_window_width * EXR_READ_BLOCK_HEIGHT); - byte *buffer = buf->buffer(); - - for (int chunk_y_min = data_window.min.y; chunk_y_min < data_window.max.y; - chunk_y_min += EXR_READ_BLOCK_HEIGHT) { - - uint8_t *fPtr = tmp_buf.data() - actual_data_window.min.x * bytes_per_pixel - - chunk_y_min * line_stride; - - Imf::FrameBuffer fb; - int ii = 0; - std::for_each( - exr_channels_to_load.begin(), - exr_channels_to_load.end(), - [&](const std::string chan_name) { - Imf::PixelType channel_type = pix_type[ii++]; - fb.insert( - chan_name.c_str(), - Imf::Slice( - channel_type, - (char *)fPtr, - bytes_per_pixel, - line_stride, - 1, - 1, - 0)); - fPtr += channel_type == Imf::PixelType::HALF ? 2 : 4; - }); - in.setFrameBuffer(fb); - - const int ymax = - std::min(chunk_y_min + EXR_READ_BLOCK_HEIGHT - 1, data_window.max.y); - in.readPixels(chunk_y_min, ymax); + Imf::InputPart in(input, part_idx); - // TODO: this copy may benefit from threading - fPtr = tmp_buf.data() + - (data_window.min.x - actual_data_window.min.x) * bytes_per_pixel; - for (int l = chunk_y_min; l < ymax; ++l) { - memcpy(buffer, fPtr, cropped_line_stride); - buffer += cropped_line_stride; - fPtr += line_stride; - } - } + utility::JsonStore part_metadata; + try { + const Imf::Header &h = in.header(); + exr_reader::dump_json_headers(h, part_metadata.ref()); + } catch (const std::exception &e) { + part_metadata["METADATA LOAD ERROR"] = e.what(); + } - } else { + Imath::Box2i data_window = in.header().dataWindow(); + Imath::Box2i display_window = in.header().displayWindow(); + + // decide the area of the image we want to load + const bool cropped_data_window = + crop_data_window(data_window, display_window, max_exr_overscan_percent_); + + // compute the size of the buffer we need + const size_t n_pixels = (data_window.size().x + 1) * (data_window.size().y + 1); + const size_t bytes_per_channel_r = + (pix_type[0] == -1 ? 0 + : pix_type[0] == Imf::PixelType::HALF ? 2 + : 4); + const size_t bytes_per_channel_g = + (pix_type[1] == -1 ? 0 + : pix_type[1] == Imf::PixelType::HALF ? 2 + : 4); + const size_t bytes_per_channel_b = + (pix_type[2] == -1 ? 0 + : pix_type[2] == Imf::PixelType::HALF ? 2 + : 4); + const size_t bytes_per_channel_a = + (pix_type[3] == -1 ? 0 + : pix_type[3] == Imf::PixelType::HALF ? 2 + : 4); + const size_t bytes_per_pixel = + bytes_per_channel_r + bytes_per_channel_g + bytes_per_channel_b + bytes_per_channel_a; + const size_t buf_size = n_pixels * bytes_per_pixel; + + // const size_t gl_line_size = 8192*4; + // const size_t padded_buf_size = (buf_size & (gl_line_size-1)) ? + // ((buf_size/gl_line_size) + 1)*gl_line_size : buf_size; + + JsonStore jsn; + jsn["num_channels"] = exr_channels_to_load.size(); + jsn["pix_type_r"] = int(pix_type[0]); + jsn["pix_type_g"] = int(pix_type[1]); + jsn["pix_type_b"] = int(pix_type[2]); + jsn["pix_type_a"] = int(pix_type[3]); + jsn["bytes_per_pixel"] = int(bytes_per_pixel); + // jsn["path"] = to_string(mptr.uri()); + + ImageBufPtr buf(new ImageBuffer(openexr_shader_uuid, jsn)); + + auto b = buf->allocate(buf_size); + + if (!input.partComplete(part_idx)) { + // expecting to read only part of the image, so clear the buffer + memset(b, 0, buf_size); + } - const size_t line_stride = - (data_window.max.x - data_window.min.x + 1) * bytes_per_pixel; - byte *buffer = buf->buffer() - data_window.min.x * bytes_per_pixel - - data_window.min.y * line_stride; + buf->set_metadata(part_metadata); + + // 4th channel is always put into 'alpha' channel as per shader code + // above + buf->set_has_alpha(exr_channels_to_load.size() > 3); + + buf->set_shader(openexr_shader); + buf->set_image_dimensions( + Imath::V2i( + display_window.max.x - display_window.min.x + 1, + display_window.max.y - display_window.min.y + 1), + Imath::Box2i( + data_window.min, Imath::V2i(data_window.max.x + 1, data_window.max.y + 1))); + + buf->params()["path"] = to_string(mptr.uri()); + buf->params()["channel_names"] = exr_channels_to_load; + buf->params()["stream_id"] = mptr.stream_id(); + + if (cropped_data_window) { + // if we are not loading the whole data window, we need to provide a temporary + // buffer that matches the EXR data window width for OpenEXR to load pixels into, we + // then copy the pixels we want into our cropped image buffer. We do this in chunks + // in the Y dimension to take advantage of OpenEXR decompress threads that are + // (possibly) more efficient when decoding blocks of pixels at once + const Imath::Box2i actual_data_window = in.header().dataWindow(); + const size_t actual_data_window_width = actual_data_window.size().x + 1; + const size_t line_stride = actual_data_window_width * bytes_per_pixel; + const size_t cropped_line_stride = (data_window.size().x + 1) * bytes_per_pixel; + Imf::FrameBuffer fb; + + std::vector tmp_buf( + bytes_per_pixel * actual_data_window_width * EXR_READ_BLOCK_HEIGHT); + byte *buffer = buf->buffer(); + + int good_scanline = data_window.min.y; + bool partial = false; + for (int chunk_y_min = data_window.min.y; chunk_y_min < data_window.max.y; + chunk_y_min += EXR_READ_BLOCK_HEIGHT) { + + uint8_t *fPtr = tmp_buf.data() - actual_data_window.min.x * bytes_per_pixel - + chunk_y_min * line_stride; Imf::FrameBuffer fb; int ii = 0; @@ -400,31 +361,81 @@ ImageBufPtr OpenEXRMediaReader::image(const media::AVFrameID &mptr) { exr_channels_to_load.begin(), exr_channels_to_load.end(), [&](const std::string chan_name) { - std::string chan_lower_case = to_lower(chan_name); Imf::PixelType channel_type = pix_type[ii++]; - fb.insert( chan_name.c_str(), Imf::Slice( - channel_type, - (char *)buffer, - bytes_per_pixel, - line_stride, - 1, - 1, - 0)); - buffer += channel_type == Imf::PixelType::HALF ? 2 : 4; + channel_type, (char *)fPtr, bytes_per_pixel, line_stride, 1, 1, 0)); + fPtr += channel_type == Imf::PixelType::HALF ? 2 : 4; }); in.setFrameBuffer(fb); + + const int ymax = + std::min(chunk_y_min + EXR_READ_BLOCK_HEIGHT - 1, data_window.max.y); + + try { + in.readPixels(chunk_y_min, ymax); + good_scanline = ymax; + } catch (Iex::InputExc &e) { + partial = true; + } + + // TODO: this copy may benefit from threading + fPtr = tmp_buf.data() + + (data_window.min.x - actual_data_window.min.x) * bytes_per_pixel; + for (int l = chunk_y_min; l <= ymax; ++l) { + memcpy(buffer, fPtr, cropped_line_stride); + buffer += cropped_line_stride; + fPtr += line_stride; + } + } + + if (partial) { + // probably a partial EXR, but we've loaded some scanlines. + // We need a unique key incase the user hits reload and we load + // the same frame again but get a different result (in terms + // of pixels). The Viewport uses the image key to tell if it + // needs to re-upload texture data .... + std::string key = to_string(mptr.key()) + fmt::format("-partial-{}", good_scanline); + buf->set_media_key(media::MediaKey(key)); + } + + } else { + + const size_t line_stride = + (data_window.max.x - data_window.min.x + 1) * bytes_per_pixel; + byte *buffer = buf->buffer() - data_window.min.x * bytes_per_pixel - + data_window.min.y * line_stride; + + Imf::FrameBuffer fb; + int ii = 0; + std::for_each( + exr_channels_to_load.begin(), + exr_channels_to_load.end(), + [&](const std::string chan_name) { + std::string chan_lower_case = to_lower(chan_name); + Imf::PixelType channel_type = pix_type[ii++]; + + fb.insert( + chan_name.c_str(), + Imf::Slice( + channel_type, (char *)buffer, bytes_per_pixel, line_stride, 1, 1, 0)); + buffer += channel_type == Imf::PixelType::HALF ? 2 : 4; + }); + in.setFrameBuffer(fb); + try { in.readPixels(data_window.min.y, data_window.max.y); + } catch (Iex::InputExc &e) { + // probably a partial EXR, but we've loaded some scanlines. + // We need a unique key incase the user hits reload and we load + // the same frame again but get a different result (in terms + // of pixels). The Viewport uses the image key to tell if it + // needs to re-upload texture data .... + std::string key = to_string(mptr.key()) + e.what(); + buf->set_media_key(media::MediaKey(key)); } - return buf; - } catch (const std::exception &err) { - throw media_corrupt_error(err.what()); } - - spdlog::error("SHouldn't reach this"); - return ImageBufPtr(); + return buf; } MRCertainty @@ -483,7 +494,17 @@ void OpenEXRMediaReader::stream_ids_from_exr_part( if (channel_names_by_layer.find("RGBA") != channel_names_by_layer.end()) { // make sure RGBA layer is first Stream stream_ids.emplace_back("RGBA"); + } else { + // optherwise look for a layer matching *.rgb to pick as the first one + for (auto p = channel_names_by_layer.begin(); p != channel_names_by_layer.end(); ++p) { + if (utility::to_lower(p->first).find(".rgb") != std::string::npos) { + stream_ids.emplace_back(p->first); + channel_names_by_layer.erase(p); + break; + } + } } + if (channel_names_by_layer.find("XYZ") != channel_names_by_layer.end()) { // make sure XYZ layer is first or second Stream stream_ids.emplace_back("XYZ"); @@ -581,85 +602,86 @@ xstudio::media::MediaDetail OpenEXRMediaReader::detail(const caf::uri &uri) cons utility::Timecode tc("00:00:00:00"); std::vector streams; - try { - - Imf::MultiPartInputFile input(path.c_str()); - double fr = 0.0; - - int parts = input.parts(); - // bool fileComplete = true; - - // we use timecode from the primary 'part' only - xSTUDIO doesn't yet handle streams - // with different frame rates - const Imf::Header &h = input.header(0); - const auto rate = h.findTypedAttribute("framesPerSecond"); - const auto rate_bogus = h.findTypedAttribute("framesPerSecond"); - const auto rate_nuke = h.findTypedAttribute("nuke/input/frame_rate"); - const auto timecode = h.findTypedAttribute("timeCode"); - const auto timecode_rate = h.findTypedAttribute("timecodeRate"); - - // Note - possible bug in Nuke where denominator of 'framesPerSecond' - // metadata value gets set to 1 on file write. - // For 23.976 framesPerSecond would be 24000/1001 but you might get a - // value of 24000 here if Nuke has knackered the data. Hence extra - // sanity check on the 'rate' metadata value here - - if (rate && rate->value() < 1000.0 && rate->value() > 1.0) - fr = static_cast(rate->value()); - else if (rate_nuke and rate_nuke->value().y > 0) - fr = static_cast(rate_nuke->value().x) / - static_cast(rate_nuke->value().y); - else if (rate_bogus and rate_bogus->value().y > 0) - fr = static_cast(rate_bogus->value().x) / - static_cast(rate_bogus->value().y); - else if (timecode_rate) - fr = static_cast(timecode_rate->value()); - - if (timecode) { - // note if frame rate is no known from metadata we use 24pfs - // as a default - tc = utility::Timecode( - timecode->value().hours(), - timecode->value().minutes(), - timecode->value().seconds(), - timecode->value().frame(), - fr == 0.0 ? 24.0 : fr, - timecode->value().dropFrame()); - } else if (rate) { - tc = utility::Timecode("00:00:00:00", fr); - } + Imf::MultiPartInputFile input(path.c_str()); + double fr = 0.0; + + int parts = input.parts(); + // bool fileComplete = true; + + // we use timecode from the primary 'part' only - xSTUDIO doesn't yet handle streams + // with different frame rates + const Imf::Header &h = input.header(0); + const auto rate = h.findTypedAttribute("framesPerSecond"); + const auto rate_bogus = h.findTypedAttribute("framesPerSecond"); + const auto rate_nuke = h.findTypedAttribute("nuke/input/frame_rate"); + const auto timecode = h.findTypedAttribute("timeCode"); + const auto timecode_rate = h.findTypedAttribute("timecodeRate"); + + // Note - possible bug in Nuke where denominator of 'framesPerSecond' + // metadata value gets set to 1 on file write. + // For 23.976 framesPerSecond would be 24000/1001 but you might get a + // value of 24000 here if Nuke has knackered the data. Hence extra + // sanity check on the 'rate' metadata value here + + if (rate && rate->value() < 1000.0 && rate->value() > 1.0) + fr = static_cast(rate->value()); + else if (rate_nuke and rate_nuke->value().y > 0) + fr = static_cast(rate_nuke->value().x) / + static_cast(rate_nuke->value().y); + else if (rate_bogus and rate_bogus->value().y > 0) + fr = static_cast(rate_bogus->value().x) / + static_cast(rate_bogus->value().y); + else if (timecode_rate) + fr = static_cast(timecode_rate->value()); + + if (timecode) { + // note if frame rate is no known from metadata we use 24pfs + // as a default + tc = utility::Timecode( + timecode->value().hours(), + timecode->value().minutes(), + timecode->value().seconds(), + timecode->value().frame(), + fr == 0.0 ? 24.0 : fr, + timecode->value().dropFrame()); + } else if (rate) { + tc = utility::Timecode("00:00:00:00", fr); + } - // if frame rate is not known, return null frame rate so xSTUDIO will - // apply its global frame rate - frd.set_rate(fr == 0.0 ? utility::FrameRate() : utility::FrameRate(1.0 / fr)); + // if frame rate is not known, return null frame rate so xSTUDIO will + // apply its global frame rate + frd.set_rate(fr == 0.0 ? utility::FrameRate() : utility::FrameRate(1.0 / fr)); + struct PartDetail { std::vector stream_ids; - std::vector resolutions; - std::vector pixel_aspects; - std::vector part_number; - for (int prt = 0; prt < parts; ++prt) { - // skip incomplete parts - maybe better error/handling messaging required? - if (!input.partComplete(prt)) - continue; - const Imf::Header &part_header = input.header(prt); - stream_ids_from_exr_part(part_header, stream_ids); - resolutions.emplace_back( - part_header.displayWindow().max.x - part_header.displayWindow().min.x, - part_header.displayWindow().max.y - part_header.displayWindow().min.y); - pixel_aspects.emplace_back(part_header.pixelAspectRatio()); - part_number.emplace_back(prt); - } + Imath::V2i resolution; + float pixel_aspect; + int part_number; + }; - int ct = 0; - for (const auto &stream_id : stream_ids) { + std::vector parts_detail; + + for (int prt = 0; prt < parts; ++prt) { + + const Imf::Header &part_header = input.header(prt); + + PartDetail pd; + stream_ids_from_exr_part(part_header, pd.stream_ids); + pd.resolution = { + part_header.displayWindow().max.x - part_header.displayWindow().min.x, + part_header.displayWindow().max.y - part_header.displayWindow().min.y}; + pd.pixel_aspect = part_header.pixelAspectRatio(); + pd.part_number = prt; + parts_detail.push_back(pd); + } + + for (const auto &part : parts_detail) { + for (const auto &stream_id : part.stream_ids) { streams.emplace_back(media::StreamDetail(frd, stream_id)); - streams.back().resolution_ = resolutions[ct]; - streams.back().pixel_aspect_ = pixel_aspects[ct]; - streams.back().index_ = part_number[ct++]; + streams.back().resolution_ = part.resolution; + streams.back().pixel_aspect_ = part.pixel_aspect; + streams.back().index_ = part.part_number; } - - } catch (const std::exception &e) { - throw media_corrupt_error(e.what()); } return xstudio::media::MediaDetail(name(), streams, tc); @@ -668,7 +690,7 @@ xstudio::media::MediaDetail OpenEXRMediaReader::detail(const caf::uri &uri) cons thumbnail::ThumbnailBufferPtr OpenEXRMediaReader::thumbnail(const media::AVFrameID &mptr, const size_t thumb_size) { - Imf::RgbaInputFile file(uri_to_posix_path(mptr.uri_).c_str()); + Imf::RgbaInputFile file(uri_to_posix_path(mptr.uri()).c_str()); if (file.header().hasPreviewImage()) { const Imf::PreviewImage &preview = file.header().previewImage(); @@ -690,7 +712,7 @@ OpenEXRMediaReader::thumbnail(const media::AVFrameID &mptr, const size_t thumb_s int exr_width = full_image_buffer->image_size_in_pixels().x; int exr_height = full_image_buffer->image_size_in_pixels().y; - int adj_exr_width = (int)round(float(exr_width) * full_image_buffer->pixel_aspect()); + int adj_exr_width = (int)round(float(exr_width) * mptr.pixel_aspect()); int thumb_width = adj_exr_width > exr_height ? thumb_size : (thumb_size * adj_exr_width) / exr_height; @@ -707,7 +729,7 @@ OpenEXRMediaReader::thumbnail(const media::AVFrameID &mptr, const size_t thumb_s return thumb; } - throw media_corrupt_error("Failed to read preview " + uri_to_posix_path(mptr.uri_)); + throw media_corrupt_error("Failed to read preview " + uri_to_posix_path(mptr.uri())); } /* @@ -818,4 +840,4 @@ PixelInfo OpenEXRMediaReader::exr_buffer_pixel_picker( } // else 1 channel, assume luminance return r; -} +} \ No newline at end of file diff --git a/src/plugin/media_reader/openexr/src/openexr_metadata.cpp b/src/plugin/media_reader/openexr/src/openexr_metadata.cpp new file mode 100644 index 000000000..007eb9db1 --- /dev/null +++ b/src/plugin/media_reader/openexr/src/openexr_metadata.cpp @@ -0,0 +1,449 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "nlohmann/json.hpp" +#include "xstudio/media_metadata/media_metadata.hpp" +#include "xstudio/utility/helpers.hpp" + +namespace fs = std::filesystem; + +using namespace xstudio::media_metadata; +using namespace xstudio::utility; +using namespace xstudio; + +namespace xstudio::exr_reader { + +bool dump_json_headers(const Imf::Header &h, nlohmann::json &root); + +template void to_json_value(nlohmann::json &root, const T *value) { + root = value->value(); +} + +template void to_json_box_value(nlohmann::json &root, const T *value) { + root[0][0] = value->value().min[0]; + root[0][1] = value->value().min[1]; + root[1][0] = value->value().max[0]; + root[1][1] = value->value().max[1]; +} + +template void to_json_vector_value(nlohmann::json &root, const T *value) { + for (unsigned int i = 0; i < value->value().dimensions(); ++i) { + root[i] = value->value()[i]; + } +} + +template void to_json_compression_value(nlohmann::json &root, const T *value) { + switch (value->value()) { + case Imf::NO_COMPRESSION: + root = "uncompressed"; + break; + + case Imf::RLE_COMPRESSION: + root = "RLE"; + break; + + case Imf::ZIPS_COMPRESSION: + root = "ZIP"; + break; + + case Imf::ZIP_COMPRESSION: + root = "ZIPS"; + break; + + case Imf::PIZ_COMPRESSION: + root = "PIZ"; + break; + + case Imf::PXR24_COMPRESSION: + root = "PXR24"; + break; + + case Imf::B44_COMPRESSION: + root = "B44"; + break; + + case Imf::B44A_COMPRESSION: + root = "B44A"; + break; + + case Imf::DWAA_COMPRESSION: + root = "DWAA"; + break; + + case Imf::DWAB_COMPRESSION: + root = "DWAB"; + break; + + default: + root = int(value->value()); + break; + } +} + +template void to_json_lineorder_value(nlohmann::json &root, const T *value) { + switch (value->value()) { + case Imf::INCREASING_Y: + root = "increasing y"; + break; + + case Imf::DECREASING_Y: + root = "decreasing y"; + break; + + case Imf::RANDOM_Y: + root = "random y"; + break; + + default: + root = int(value->value()); + break; + } +} + + +template void to_json_chromaticities_value(nlohmann::json &root, const T *value) { + root["red"][0] = value->value().red[0]; + root["red"][1] = value->value().red[1]; + root["green"][0] = value->value().green[0]; + root["green"][1] = value->value().green[1]; + root["blue"][0] = value->value().blue[0]; + root["blue"][1] = value->value().blue[1]; + root["white"][0] = value->value().white[0]; + root["white"][1] = value->value().white[1]; +} + +template void to_json_stdvector_value(nlohmann::json &root, const T *value) { + for (unsigned int i = 0; i < value->value().size(); ++i) { + root[i] = value->value()[i]; + } +} + +template +void to_json_matrix_value(nlohmann::json &root, const T *value, int size) { + for (int i = 0; i < size; ++i) { + for (int j = 0; j < size; ++j) { + root[i][j] = value->value()[i][j]; + } + } +} + +template void to_json_rational_value(nlohmann::json &root, const T *value) { + root["numerator"] = value->value().n; + root["denominator"] = value->value().d; + root = double(value->value()); +} + +template void to_json_envmap_value(nlohmann::json &root, const T *value) { + + switch (value->value()) { + case Imf::ENVMAP_LATLONG: + root = "latitude-longitude map"; + break; + + case Imf::ENVMAP_CUBE: + root = "cube-face map"; + break; + + default: + root = int(value->value()); + break; + } +} + + +template void to_json_channel_list_value(nlohmann::json &root, const T *value) { + + // this is surely too verbose for our needs ... + /*int j = 0; + for (Imf::ChannelList::ConstIterator i = value->value().begin(); i != value->value().end(); + ++i, ++j) { + root[j]["name"] = i.name(); + + switch (i.channel().type) { + case Imf::UINT: + root[j]["pixelType"] = "32 bits uint"; + break; + + case Imf::HALF: + root[j]["pixelType"] = "16 bits float"; + break; + + case Imf::FLOAT: + root[j]["pixelType"] = "32 bits float"; + break; + + default: + root[j]["pixelType"] = int(i.channel().type); + break; + } + + root[j]["xSampling"] = i.channel().xSampling; + root[j]["ySampling"] = i.channel().ySampling; + root[j]["plinear"] = i.channel().pLinear; + }*/ + + // Instead of above, names and format, just list channel names and + // pixel type in one string entry + int fmt = -1; + for (Imf::ChannelList::ConstIterator i = value->value().begin(); i != value->value().end(); + ++i) { + + if (fmt == -1) { + fmt = i.channel().type; + } else if (fmt != i.channel().type) { + fmt = -2; + break; + } + } + + std::string chan_names; + if (fmt != -2) { + + for (Imf::ChannelList::ConstIterator i = value->value().begin(); + i != value->value().end(); + ++i) { + if (i != value->value().begin()) + chan_names += ", "; + chan_names += i.name(); + } + + switch (fmt) { + case Imf::UINT: + chan_names += " (32 bits uint)"; + break; + + case Imf::HALF: + chan_names += " (16 bits float)"; + break; + + case Imf::FLOAT: + chan_names += " (32 bits float)"; + break; + + default: + chan_names += fmt::format(" (pix fmt id {})", fmt); + break; + } + + } else { + + for (Imf::ChannelList::ConstIterator i = value->value().begin(); + i != value->value().end(); + ++i) { + + if (i != value->value().begin()) + chan_names += ", "; + chan_names += i.name(); + + switch (i.channel().type) { + case Imf::UINT: + chan_names += " (32 bits uint)"; + break; + + case Imf::HALF: + chan_names += " (16 bits float)"; + break; + + case Imf::FLOAT: + chan_names += " (32 bits float)"; + break; + + default: + chan_names += fmt::format(" (pix fmt id {})", fmt); + break; + } + } + } + + root = chan_names; +} + +template void to_json_key_code_value(nlohmann::json &root, const T *value) { + root["film manufacturer code"] = value->value().filmMfcCode(); + root["film type code"] = value->value().filmType(); + root["prefix"] = value->value().prefix(); + root["count"] = value->value().count(); + root["perf offset"] = value->value().perfOffset(); + root["perfs per frame"] = value->value().perfsPerFrame(); + root["perfs per count"] = value->value().perfsPerCount(); +} + +template void to_json_time_code_value(nlohmann::json &root, const T *value) { + std::stringstream ss; + ss << std::setfill('0') << std::setw(2) << value->value().hours() << ":" << std::setw(2) + << value->value().minutes() << ":" << std::setw(2) << value->value().seconds() << ":" + << std::setw(2) << value->value().frame(); + + root["time"] = ss.str(); + root["drop frame"] = value->value().dropFrame(); + root["color frame"] = value->value().colorFrame(); + root["field/phase"] = value->value().fieldPhase(); + root["bgf"][0] = value->value().bgf0(); + root["bgf"][1] = value->value().bgf1(); + root["bgf"][2] = value->value().bgf2(); + root["user data"] = value->value().userData(); +} + +template void to_json_preview_value(nlohmann::json &root, const T *value) { + root["width"] = value->value().width(); + root["height"] = value->value().height(); +} + + +template +void to_json_tile_description_value(nlohmann::json &root, const T *value) { + + root["width"] = value->value().xSize; + root["height"] = value->value().ySize; + + switch (value->value().mode) { + case Imf::ONE_LEVEL: + root["level mode"] = "single level"; + break; + + case Imf::MIPMAP_LEVELS: + root["level mode"] = "mip-map"; + break; + + case Imf::RIPMAP_LEVELS: + root["level mode"] = "rip-map"; + break; + + default: + root["level mode"] = int(value->value().mode); + break; + } + + if (value->value().mode != Imf::ONE_LEVEL) { + switch (value->value().roundingMode) { + case Imf::ROUND_DOWN: + root["level rounding mode"] = "down"; + break; + + case Imf::ROUND_UP: + root["level rounding mode"] = "up"; + break; + + default: + root["level rounding mode"] = int(value->value().roundingMode); + break; + } + } +} + +bool dump_json_headers(const Imf::Header &h, nlohmann::json &root) { + for (Imf::Header::ConstIterator i = h.begin(); i != h.end(); ++i) { + try { + if (auto ta = dynamic_cast(&i.attribute())) + to_json_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_box_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_box_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_vector_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_vector_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_vector_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_vector_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_compression_value(root[i.name()], ta); + + else if ( + auto ta = dynamic_cast(&i.attribute())) + to_json_chromaticities_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_stdvector_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_matrix_value(root[i.name()], ta, 3); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_matrix_value(root[i.name()], ta, 4); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_lineorder_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_channel_list_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_rational_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_envmap_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_key_code_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_time_code_value(root[i.name()], ta); + + else if (auto ta = dynamic_cast(&i.attribute())) + to_json_preview_value(root[i.name()], ta); + + else if ( + auto ta = dynamic_cast(&i.attribute())) + to_json_tile_description_value(root[i.name()], ta); + + else { + root[i.name()] = nullptr; + } + } catch ([[maybe_unused]] const Iex::TypeExc &e) { + root[i.name()] = nullptr; + } + } + + return true; +} +} // namespace xstudio::exr_reader \ No newline at end of file diff --git a/src/plugin/media_reader/ppm/src/CMakeLists.txt b/src/plugin/media_reader/ppm/src/CMakeLists.txt index a4be192ab..b168621a4 100644 --- a/src/plugin/media_reader/ppm/src/CMakeLists.txt +++ b/src/plugin/media_reader/ppm/src/CMakeLists.txt @@ -1,8 +1,5 @@ -find_package(GLEW REQUIRED) - SET(LINK_DEPS xstudio::media_reader - GLEW::GLEW ) -create_plugin_with_alias(media_reader_ppm xstudio::media_reader::ppm 0.1.0 "${LINK_DEPS}") +create_plugin_with_alias(media_reader_ppm xstudio::media_reader::ppm ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/plugin/media_reader/ppm/src/ppm.cpp b/src/plugin/media_reader/ppm/src/ppm.cpp index d8153a1b9..b74845401 100644 --- a/src/plugin/media_reader/ppm/src/ppm.cpp +++ b/src/plugin/media_reader/ppm/src/ppm.cpp @@ -64,7 +64,7 @@ static ui::viewport::GPUShaderPtr ImageBufPtr PPMMediaReader::image(const media::AVFrameID &mptr) { ImageBufPtr buf; - std::ifstream inp(uri_to_posix_path(mptr.uri_), std::ios::in | std::ios::binary); + std::ifstream inp(uri_to_posix_path(mptr.uri()), std::ios::in | std::ios::binary); if (inp.is_open()) { size_t width; size_t height; @@ -75,7 +75,7 @@ ImageBufPtr PPMMediaReader::image(const media::AVFrameID &mptr) { std::getline(inp, line); if (line != "P6") throw media_corrupt_error( - "Error. Unrecognized file format." + to_string(mptr.uri_)); + "Error. Unrecognized file format." + to_string(mptr.uri())); std::getline(inp, line); while (line[0] == '#') { @@ -117,7 +117,7 @@ ImageBufPtr PPMMediaReader::image(const media::AVFrameID &mptr) { inp.close(); } else { - throw media_unreadable_error("Unable to open " + to_string(mptr.uri_)); + throw media_unreadable_error("Unable to open " + to_string(mptr.uri())); } return buf; diff --git a/src/plugin/media_reader/ppm/src/ppm.hpp b/src/plugin/media_reader/ppm/src/ppm.hpp index 230bae7db..16ace0dc6 100644 --- a/src/plugin/media_reader/ppm/src/ppm.hpp +++ b/src/plugin/media_reader/ppm/src/ppm.hpp @@ -14,8 +14,8 @@ namespace media_reader { : MediaReader("PPM", prefs) {} virtual ~PPMMediaReader() = default; - ImageBufPtr image(const media::AVFrameID &mptr); - MRCertainty supported(const caf::uri &uri, const std::array &signature); + ImageBufPtr image(const media::AVFrameID &mptr) override; + MRCertainty supported(const caf::uri &uri, const std::array &signature) override; // media::MediaDetail detail(const caf::uri &uri) const override; [[nodiscard]] utility::Uuid plugin_uuid() const override; }; diff --git a/src/plugin/python_plugins/CMakeLists.txt b/src/plugin/python_plugins/CMakeLists.txt new file mode 100644 index 000000000..7989312d7 --- /dev/null +++ b/src/plugin/python_plugins/CMakeLists.txt @@ -0,0 +1,4 @@ +add_python_plugin(viewport_flag_indicator) +add_python_plugin(picture_in_picture) +add_python_plugin(on_screen_version_name) +add_python_plugin(viewport_field_chart) \ No newline at end of file diff --git a/src/plugin/python_plugins/on_screen_version_name/__init__.py b/src/plugin/python_plugins/on_screen_version_name/__init__.py new file mode 100644 index 000000000..559d87fe7 --- /dev/null +++ b/src/plugin/python_plugins/on_screen_version_name/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +from .on_screen_version_name import create_plugin_instance \ No newline at end of file diff --git a/src/plugin/python_plugins/on_screen_version_name/on_screen_version_name.py b/src/plugin/python_plugins/on_screen_version_name/on_screen_version_name.py new file mode 100644 index 000000000..b5592750f --- /dev/null +++ b/src/plugin/python_plugins/on_screen_version_name/on_screen_version_name.py @@ -0,0 +1,162 @@ +#!/bin/env python +# SPDX-License-Identifier: Apache-2.0 + +from xstudio.connection import Connection +from xstudio.plugin import HUDPlugin +from xstudio.core import JsonStore +from xstudio.core import event_atom, show_atom +from xstudio.core import HUDElementPosition, ColourTriplet +from xstudio.api.session.media import Media, MediaSource + +import json + +# path to QML resources relative to this .py file +qml_folder_name = "qml/OnScreenVersionName.1" + +# QML code necessary to create the overlay item that is drawn over the xSTUDIO +# viewport. +overlay_qml = """ +OnScreenVersionNameOverlay { +} +""" + +# Declare our plugin class - we're using the HUDPlugin base class meaning we +# get a toggle under the 'HUD' button in the viewport toolbar to turn our +# hud on and off +class OnScreenVersionName(HUDPlugin): + + def __init__(self, connection): + + HUDPlugin.__init__( + self, + connection, + "On Screen Version Name", # the name of the HUD item + qml_folder=qml_folder_name, + position_in_hud_list=-9.0) + + # add an attribute to control the size of the text + self.font_size = self.add_attribute( + "Font Size", + 30.0, # default size + # additional attribute role data is provided as a dictionary + # as follows. The keys must be valid role names. See attribute.hpp + # for a list of the attribute role data names + { + "float_scrub_min": 5.0, + "float_scrub_max": 100.0, + "float_scrub_step": 2.0, + "float_display_decimals": 2 + }, + register_as_preference=True + ) + # adding to a ui attrs group means we can access the attribute in our + # QML code by referencing the group name and the attribute title + self.font_size.expose_in_ui_attrs_group("on_screen_version_name") + self.font_size.set_tool_tip("Set the size of the font for displaying the version name") + self.font_size.set_redraw_viewport_on_change(True) + + # add an attribute to control the size of the text + self.font_colour = self.add_attribute( + "Font Colour", + ColourTriplet(1.0, 1.0, 1.0) + ) + self.font_colour.expose_in_ui_attrs_group("on_screen_version_name") + + self.auto_hide = self.add_attribute( + "Auto Hide", + True, + register_as_preference=True + ) + self.auto_hide.expose_in_ui_attrs_group("on_screen_version_name") + + self.hide_timeout = self.add_attribute( + "Auto Hide Timeout (seconds)", + 10.0, # default number of seconds to hide the version + { + "float_scrub_min": 0.0, + "float_scrub_max": 20.0, + "float_scrub_step": 0.5, + "float_display_decimals": 1 + }, + register_as_preference=True + ) + self.hide_timeout.expose_in_ui_attrs_group("on_screen_version_name") + + # this attr holds the actual text to be displayed on screen + self.display_text = self.add_attribute( + "Display Text", + "", + {} + ) # default size + self.display_text.expose_in_ui_attrs_group("on_screen_version_name") + + # the following calls mean that these attributes get controls in + # the settings dialog for thie HUD, accessed via the cog icon next to + # the item in the HUD pop-up menu + self.add_hud_settings_attribute(self.font_size) + self.add_hud_settings_attribute(self.hide_timeout) + self.add_hud_settings_attribute(self.auto_hide) + self.add_hud_settings_attribute(self.font_colour) + + # here we provide the QML code to instance the item that will draw + # the overlay graphics + self.hud_element_qml( + overlay_qml, + HUDElementPosition.TopCenter) + + # expose our attributes in the UI layer + self.connect_to_ui() + + # listen for crucial events about the on-screen media changing etc. + self.subscribe_to_playhead_events(self.playhead_event_handler) + + def on_screen_source_changed(self, media_source): + + # This needs some more work, as we can have multiple things on screen + # in different viewports + + path = str(media_source.media_reference.uri()) + # if it's a dneg.mov it will have a burn-in and we don't need the + # version name overlay + if path.find(".dneg.mov") != -1: + self.display_text.set_value("") + return + + # strip the path string of its folder and extension + path = path[(path.rfind("/")+1):] + path = path[0:path.find(".")] + + self.display_text.set_value(path) + + def playhead_event_handler(self, event_args): + + # Skipping this bit, using playhead data instead in the QML overlay + return + + + # various events come in here from the playhead objects. You can simply + # print 'event_args' to see the content and try and work out how to + # use them. + # + # In our case we are interested when the on-screen media changes - this + # event has a particular signature and data types .... + # We get a tuple like this + # (event_atom, show_atom, actor(media), actor(media_source), str(viewport name)) + + if self.enabled: + if len(event_args) == 5 and type(event_args[0]) == event_atom and type(event_args[1]) == show_atom: + if event_args[3]: + viewport_name = event_args[4] + media_source = MediaSource(self.connection, event_args[3]) + self.on_screen_source_changed(media_source) + +# This method is required by xSTUDIO +def create_plugin_instance(connection): + return OnScreenVersionName(connection) + + +if __name__=="__main__": + + XSTUDIO = Connection(auto_connect=True) + mask_plugin_instance = create_plugin_instance(XSTUDIO) + XSTUDIO.process_events_forever() \ No newline at end of file diff --git a/src/plugin/python_plugins/on_screen_version_name/qml/OnScreenVersionName.1/OnScreenVersionNameOverlay.qml b/src/plugin/python_plugins/on_screen_version_name/qml/OnScreenVersionName.1/OnScreenVersionNameOverlay.qml new file mode 100644 index 000000000..e0f33df8a --- /dev/null +++ b/src/plugin/python_plugins/on_screen_version_name/qml/OnScreenVersionName.1/OnScreenVersionNameOverlay.qml @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQuick +import QuickFuture +import QuickPromise + +// These imports are necessary to have access to custom QML types that are +// part of the xSTUDIO UI implementation. +import xStudio 1.0 +import xstudio.qml.models 1.0 + +// This overlay is as simple as it gets. We're just putting a circle in a corner +// of the screen that shows the 'flag' colour that is set on the media item that +// is on screen. If no flag is set, we don't show anything +Item { + + id: root + width: visible ? text_metrics.width : 0 + height: visible ? text_metrics.height : 0 + + // Turning this off for QuickView windows, which won't work due to use + // of currentOnScreenMediaData + visible: isQuickview ? false : version_name != undefined && opacity != 0.0 + + Rectangle { + color: "black" + opacity: 0.5 + anchors.fill: parent + } + + property var media_info: currentOnScreenMediaData.values.mediaDisplayInfoRole + property var version_name: media_info ? media_info[3] : "" + + XsText { + + id: thetext + anchors.fill: parent + font.pixelSize: font_size*view.width/1920 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: version_name + color: font_colour + onTextChanged: (text)=> { + root.opacity = 1.0 + if (auto_hide && text != "") { + auto_hide_timer.running = false + auto_hide_timer.running = true + } + } + + } + + XsTimer { + id: auto_hide_timer + interval: hide_timeout*1000 + running: false + repeat: false + onTriggered: root.opacity = 0 + } + + TextMetrics { + id: text_metrics + font: thetext.font + text: thetext.text + } + + + // access the 'dnmask_settings' attribute group + XsModuleData { + id: onscreen_attrs + modelDataName: "on_screen_version_name" + } + + + XsAttributeValue { + id: __font_colour + attributeTitle: "Font Colour" + model: onscreen_attrs + } + property alias font_colour: __font_colour.value + + XsAttributeValue { + id: __hide_timeout + attributeTitle: "Auto Hide Timeout (seconds)" + model: onscreen_attrs + } + property alias hide_timeout: __hide_timeout.value + + XsAttributeValue { + id: __auto_hide + attributeTitle: "Auto Hide" + model: onscreen_attrs + } + property alias auto_hide: __auto_hide.value + + XsAttributeValue { + id: __the_text + attributeTitle: "Display Text" + model: onscreen_attrs + } + property alias display_text: __the_text.value + + XsAttributeValue { + id: __font_size + attributeTitle: "Font Size" + model: onscreen_attrs + } + property alias font_size: __font_size.value + + /*XsAttributeValue { + id: __flag_colours + attributeTitle: "Flag Colours" + model: vp_flag_indicator_settings + } + property alias flagColours: __flag_colours.value*/ + +} \ No newline at end of file diff --git a/src/plugin/python_plugins/on_screen_version_name/qml/OnScreenVersionName.1/qmldir b/src/plugin/python_plugins/on_screen_version_name/qml/OnScreenVersionName.1/qmldir new file mode 100644 index 000000000..5c6822862 --- /dev/null +++ b/src/plugin/python_plugins/on_screen_version_name/qml/OnScreenVersionName.1/qmldir @@ -0,0 +1,3 @@ +module OnScreenVersionName + +OnScreenVersionNameOverlay 1.0 OnScreenVersionNameOverlay.qml \ No newline at end of file diff --git a/src/plugin/python_plugins/picture_in_picture/__init__.py b/src/plugin/python_plugins/picture_in_picture/__init__.py new file mode 100644 index 000000000..8d10e5519 --- /dev/null +++ b/src/plugin/python_plugins/picture_in_picture/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +from .picture_in_picture_plugin import create_plugin_instance \ No newline at end of file diff --git a/src/plugin/python_plugins/picture_in_picture/picture_in_picture_plugin.py b/src/plugin/python_plugins/picture_in_picture/picture_in_picture_plugin.py new file mode 100644 index 000000000..4d7a82210 --- /dev/null +++ b/src/plugin/python_plugins/picture_in_picture/picture_in_picture_plugin.py @@ -0,0 +1,147 @@ +#!/bin/env python +# SPDX-License-Identifier: Apache-2.0 + +from xstudio.connection import Connection +from xstudio.plugin import ViewportLayoutPlugin +from xstudio.core import JsonStore +from xstudio.core import event_atom, show_atom +from xstudio.core import AssemblyMode +from xstudio.api.session.media import Media +import json + + +# Declare our plugin class - we're using the HUDPlugin base class meaning we +# get a toggle under the 'HUD' button in the viewport toolbar to turn our +# hud on and off +class PictureInPicturePlugin(ViewportLayoutPlugin): + + def __init__(self, connection): + + ViewportLayoutPlugin.__init__( + self, + connection, + "Picture In Picture") + + # add an attribute to control the inset border for the small images + self.inset = self.add_attribute( + "PiP Inset", + 0.0, # default inset + # additional attribute role data is provided as a dictionary + # as follows. The keys must be valid role names. See attribute.hpp + # for a list of the attribute role data names + { + "float_scrub_min": -50.0, + "float_scrub_max": 50.0, + "float_scrub_step": 0.5, + "float_display_decimals": 2 + }, + # The flag below ensures that if the user changes the Indicator Size + # the value will be stored in the user's preferences files amnd + register_as_preference=True + ) + self.inset.set_tool_tip("Set the border inset size for images that are placed in the picture") + + # add an attribute to control the size of the inset pictures + self.sizing = self.add_attribute( + "PiP Size Percent", + 15.0, # default sizing + { + "float_scrub_min": 0.0, + "float_scrub_max": 100.0, + "float_scrub_step": 2.0, + "float_display_decimals": 2 + }, + register_as_preference=True + ) + self.sizing.set_tool_tip("Set the percent scaling of the inset pictures.") + + # add an attribute to control the size of the inset pictures + self.start_position = self.add_attribute( + "Start Position", + "Top Left", # default sizing + { + "combo_box_options": ["Top Left", "Top Right", "Bottom Left", "Bottom Right"] + }, + register_as_preference=True + ) + self.start_position.set_tool_tip("The position of the first inset image.") + + # add an attribute to control the layout positioning of subsequent images + self.layout_direction = self.add_attribute( + "Layout Direction", + "Horizontal", # default sizing + { + "combo_box_options": ["Horizontal", "Vertical"] + }, + register_as_preference=True + ) + self.layout_direction.set_tool_tip("The position of the first inset image.") + + self.add_layout_settings_attribute(self.inset, "PiP") + self.add_layout_settings_attribute(self.sizing, "PiP") + self.add_layout_settings_attribute(self.start_position, "PiP") + self.add_layout_settings_attribute(self.layout_direction, "PiP") + + # declare our mode + self.add_layout_mode("PiP", 23.0, AssemblyMode.AM_ALL) + + + def do_layout(self, layout_name, image_set_data): + + # we draw all images in order + image_draw_order = [i for i in range(0, len(image_set_data["image_info"]))] + + # first image is not moved or scaled + image_transforms = [(0.0, 0.0, 1.0)] + + inset = self.inset.value()/100.0 + scaling = self.sizing.value()/100.0 + start = self.start_position.value() + horiz = self.layout_direction.value() == "Horizontal" + + hero_image_idx = image_set_data["hero_image_index"] + hero_image_size = image_set_data["image_info"][hero_image_idx]['image_size_in_pixels'] + hero_pixel_aspect = image_set_data["image_info"][hero_image_idx]['pixel_aspect'] + aspect = hero_pixel_aspect*hero_image_size[2]/hero_image_size[3] + + if "Left" in start: + if horiz: + positioning = [(-1.0, -1.0), (1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)] + else: + positioning = [(-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0)] + else: + if horiz: + positioning = [(1.0, -1.0), (-1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] + else: + positioning = [(1.0, -1.0), (1.0, 1.0), (-1.0, -1.0), (-1.0, 1.0)] + + if "Bottom" in start: + positioning = [(r[0], -r[1]) for r in positioning] + + i = 0 + for d in range(len(image_set_data["image_info"])-1): + + r = positioning[i%len(positioning)] + n = (int(i/len(positioning))*2)+1 + dx = scaling*n + inset + dy = scaling + inset + xpos = r[0] - dx if r[0] > 0.0 else r[0] + dx + ypos = r[1] - dy if r[1] > 0.0 else r[1] + dy + image_transforms.append((xpos, ypos/aspect, scaling)) + i += 1 + + return (image_transforms, image_draw_order, 16.0/9.0) + + + + +# This method is required by xSTUDIO +def create_plugin_instance(connection): + return PictureInPicturePlugin(connection) + + +if __name__=="__main__": + + XSTUDIO = Connection(auto_connect=True) + mask_plugin_instance = create_plugin_instance(XSTUDIO) + XSTUDIO.process_events_forever() \ No newline at end of file diff --git a/src/plugin/python_plugins/viewport_field_chart/__init__.py b/src/plugin/python_plugins/viewport_field_chart/__init__.py new file mode 100644 index 000000000..5088cf6d9 --- /dev/null +++ b/src/plugin/python_plugins/viewport_field_chart/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +from .viewport_field_chart import create_plugin_instance \ No newline at end of file diff --git a/src/plugin/python_plugins/viewport_field_chart/qml/ViewportFieldChartPlugin.1/ViewportFieldChart.qml b/src/plugin/python_plugins/viewport_field_chart/qml/ViewportFieldChartPlugin.1/ViewportFieldChart.qml new file mode 100644 index 000000000..c4a8677d4 --- /dev/null +++ b/src/plugin/python_plugins/viewport_field_chart/qml/ViewportFieldChartPlugin.1/ViewportFieldChart.qml @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQuick +import QtQuick.Effects + +// These imports are necessary to have access to custom QML types that are +// part of the xSTUDIO UI implementation. +import xStudio 1.0 +import xstudio.qml.models 1.0 + +// Our Overlay is based on an item that simply fills the +// xSTUDIO viewport. Within this we draw the overlay graphics as required. +Item { + + // Note that viewport overlays are instanced by the Viewport QML instance + // which has the id 'view' and is visible to us here. To learn more + // about the viewport see files XsViewport.qml and qml_viewport.cpp from + // the xSTUDIO source code. + + id: control + width: view.width + height: view.height + + XsModuleData { + id: field_charts_data + modelDataName: "fieldchart_image_set" + } + + XsAttributeValue { + id: __active_charts + attributeTitle: "Active Charts" + model: field_charts_data + } + property alias active_charts: __active_charts.value + + XsModuleData { + id: fieldchart_settings + modelDataName: "fieldchart_settings" + } + + XsAttributeValue { + id: __opacity + attributeTitle: "Opacity" + model: fieldchart_settings + } + property alias fieldCharOpacity: __opacity.value + + XsAttributeValue { + id: __colour + attributeTitle: "Colour" + model: fieldchart_settings + } + property alias fieldCharColour: __colour.value + + // Note: Viewport has property imageBoundariesInViewport - this is a vector + // of QRect desribing the coordinates of the image(s) in the viewport. + property var imBoundaries: view.imageBoundariesInViewport + + Repeater { + + model: imBoundaries + Item { + property var imageBox: imBoundaries[index] + Repeater { + model: active_charts + Item { + + x: imageBox.x + y: imageBox.y + width: imageBox.width + height: imageBox.height + + Image { + id: im + anchors.fill: parent + sourceSize: Qt.size(2560, 1440) + source: active_charts[index] + mipmap: true + } + + MultiEffect { + colorization: 1 + colorizationColor: Qt.rgba(fieldCharColour.r, fieldCharColour.g, fieldCharColour.b, 1.0) + anchors.fill: parent + source: im + brightness: 1 + } + + opacity: fieldCharOpacity + } + } + } + } +} \ No newline at end of file diff --git a/src/plugin/python_plugins/viewport_field_chart/qml/ViewportFieldChartPlugin.1/ViewportFieldChartSettingsDialog.qml b/src/plugin/python_plugins/viewport_field_chart/qml/ViewportFieldChartPlugin.1/ViewportFieldChartSettingsDialog.qml new file mode 100644 index 000000000..30ca0fcc1 --- /dev/null +++ b/src/plugin/python_plugins/viewport_field_chart/qml/ViewportFieldChartPlugin.1/ViewportFieldChartSettingsDialog.qml @@ -0,0 +1,390 @@ + + +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQuick.Window +import Qt.labs.qmlmodels + +import xstudio.qml.models 1.0 +import xStudio 1.0 + + +// This is an adapted version of XsAttributesPanel, allowing us to add a dynamic +// list of masks that the user can enable/disable + +XsWindow { + + id: dialog + width: 500 + height: thelayout.childrenRect.height+30 + title: "Field Chart Settings" + property real cwidth: 50 + property real cwidth2: 50 + XsModuleData { + id: field_charts_data + modelDataName: "fieldchart_image_set" + } + + XsAttributeValue { + id: __all_charts + attributeTitle: "All Charts" + model: field_charts_data + } + property alias all_charts: __all_charts.value + property var field_chart_names: Object.keys(all_charts) + + XsAttributeValue { + id: __user_charts + attributeTitle: "User Charts" + model: field_charts_data + } + property alias user_charts: __user_charts.value + property var user_char_names: Object.keys(user_charts) + + XsAttributeValue { + id: __visible_charts + attributeTitle: "Visibile Charts" + model: field_charts_data + } + property alias visible_charts: __visible_charts.value + + + function setCWidth(ww) { + if (ww > cwidth) cwidth = ww + } + + function setCWidth2(ww) { + if (ww > cwidth2) cwidth2 = ww + } + + // make an alias so the mask options are accessible as an array + property alias attribute_set: attribute_set + + property var deleteName + function deleteFieldChart() { + if (user_char_names.includes(deleteName)) { + var v = user_charts + delete v[deleteName] + user_charts = v + deleteName = undefined + } + } + + property var new_svg + function doAddFieldChart_pt1(path) { + + if (path == false) return; // load was cancelled + new_svg = path + dialogHelpers.textInputDialog( + doAddFieldChart_pt2, + "New Field Chart", + "Enter a name for the new field chart.", + "Field Chart", + ["Cancel", "Add"]) + + } + + function doAddFieldChart_pt2(new_name, button) { + + if (button == "Add" && new_svg !== undefined) { + + if (field_chart_names.includes(new_name)) { + + dialogHelpers.errorDialogFunc("Error", "There is already a chart named \"" + new_name + "\""); + return + } + + var v = user_charts + v[new_name] = new_svg, + user_charts = v + new_svg = undefined + } + } + + XsModuleData { + id: attribute_set + modelDataName: "fieldchart_settings" + } + + ColumnLayout { + + id: thelayout + anchors.left: parent.left + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: 10 + spacing: 10 + + RowLayout { + spacing: 10 + Layout.fillWidth: true + Rectangle { + Layout.fillWidth: true + height: 1 + color: XsStyleSheet.menuBorderColor + } + XsText { + text: "Field Chart Images" + Layout.alignment: Qt.AlignHCenter + } + Rectangle { + Layout.fillWidth: true + height: 1 + color: XsStyleSheet.menuBorderColor + } + } + + Repeater { + + // N.B the 'combo_box_options' attribute is propagated via the + // XsToolBox which instantiates the BasicViewportMaskButton + model: field_chart_names + id: r1 + + RowLayout { + + id: settings_items + Layout.alignment: Qt.AlignCenter + Layout.margins: 0 + Layout.preferredHeight: 20 + + XsCheckBox { + width: 20 + height: 20 + checked: visible_charts.includes(tt.text) + onClicked: { + var v = visible_charts + if (visible_charts.includes(tt.text)) { + let idx = v.indexOf(tt.text); + if (idx > -1) { + v.splice(idx, 1); + } + } else { + v.push(tt.text) + } + visible_charts = v; + } + } + + XsText { + id: tt + Layout.preferredWidth: cwidth2 + text: field_chart_names[index] + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + y: index*20 + TextMetrics { + font: tt.font + text: tt.text + onWidthChanged: setCWidth2(width) + } + } + + XsPrimaryButton { + Layout.preferredHeight: 20 + Layout.preferredWidth: 40 + imgSrc: "qrc:/icons/delete.svg" + enabled: user_char_names.includes(tt.text) + onClicked: { + deleteName = tt.text + dialogHelpers.multiChoiceDialog( + deleteFieldChart, + "Remove Field Chart", + "Remove the Field Chart \"" + tt.text + "\"?", + ["Cancel", "Remove"], + undefined) + } + } + + } + + } + + XsPrimaryButton { + id: addBut + Layout.preferredHeight: 30 + Layout.preferredWidth: 60 + Layout.alignment: Qt.AlignRight + text: "Add ..." + onClicked: { + dialogHelpers.showFileDialog( + doAddFieldChart_pt1, + "", + "Select SVG File for Field Chart", + "svg", + ["SVG (*.svg)"], + true, + false) + } + XsToolTip { + visible: addBut.isHovered + text: "You can select any SVG file you wish from the file system to use as a field chart overlay." + } + } + + Item { + Layout.preferredHeight: 20 + } + + RowLayout { + spacing: 10 + Layout.fillWidth: true + Rectangle { + Layout.fillWidth: true + height: 1 + color: XsStyleSheet.menuBorderColor + } + XsText { + text: "Field Chart Settings" + Layout.alignment: Qt.AlignHCenter + } + Rectangle { + Layout.fillWidth: true + height: 1 + color: XsStyleSheet.menuBorderColor + } + } + + RowLayout { + + Layout.fillWidth: true + Layout.margins: 10 + + ColumnLayout { + + Layout.margins: 5 + + Repeater { + + // N.B the 'combo_box_options' attribute is propagated via the + // XsToolBox which instantiates the BasicViewportMaskButton + model: attribute_set + + Rectangle { + + height: 20 + color: "transparent" + width: label_metrics.width + onWidthChanged: setCWidth(width) + Layout.alignment: Qt.AlignRight + + XsText { + id: thetext + anchors.fill: parent + text: title ? title : "" + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + y: index*20 + } + + TextMetrics { + id: label_metrics + font: thetext.font + text: thetext.text + } + } + + } + } + + ColumnLayout { + + Layout.fillWidth: true + Layout.margins: 5 + + Repeater { + + model: attribute_set + y: 5 + delegate: chooser + + DelegateChooser { + id: chooser + role: "type" + + DelegateChoice { + roleValue: "FloatScrubber"; + XsFloatAttributeSlider { + height: 20 + Layout.fillWidth: true + } + } + + DelegateChoice { + roleValue: "IntegerValue"; + XsIntAttributeSlider { + height: 20 + Layout.fillWidth: true + } + } + + DelegateChoice { + roleValue: "ColourAttribute"; + XsColourChooser { + height: 20 + Layout.fillWidth: true + } + } + + + DelegateChoice { + roleValue: "OnOffToggle"; + Rectangle { + color: "transparent" + height: 20 + Layout.fillWidth: true + XsBoolAttributeCheckBox { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.topMargin: 2 + anchors.bottomMargin: 2 + } + } + } + + DelegateChoice { + roleValue: "ComboBox"; + Rectangle { + height: 20 + Layout.fillWidth: true + color: "transparent" + XsComboBox { + model: combo_box_options + anchors.fill: parent + property var value_: value ? value : null + onValue_Changed: { + currentIndex = indexOfValue(value_) + } + Component.onCompleted: currentIndex = indexOfValue(value_) + onCurrentValueChanged: { + if (value != currentValue) { + value = currentValue; + } + } + } + } + } + + } + + } + } + } + + Item { + Layout.preferredHeight: 20 + } + + XsSimpleButton { + text: qsTr("Close") + width: XsStyleSheet.primaryButtonStdWidth*2 + Layout.alignment: Qt.AlignVCenter|Qt.AlignRight + onClicked: { + dialog.close() + } + } + } + +} diff --git a/src/plugin/python_plugins/viewport_field_chart/qml/ViewportFieldChartPlugin.1/qmldir b/src/plugin/python_plugins/viewport_field_chart/qml/ViewportFieldChartPlugin.1/qmldir new file mode 100644 index 000000000..29074bc2a --- /dev/null +++ b/src/plugin/python_plugins/viewport_field_chart/qml/ViewportFieldChartPlugin.1/qmldir @@ -0,0 +1,4 @@ +module ViewportFieldChartPlugin + +ViewportFieldChart 1.0 ViewportFieldChart.qml +ViewportFieldCharSettingsDialog 1.0 ViewportFieldCharSettingsDialog.qml \ No newline at end of file diff --git a/src/plugin/python_plugins/viewport_field_chart/qml/mkt_anim_grid.svg b/src/plugin/python_plugins/viewport_field_chart/qml/mkt_anim_grid.svg new file mode 100644 index 000000000..efd884bf4 --- /dev/null +++ b/src/plugin/python_plugins/viewport_field_chart/qml/mkt_anim_grid.svg @@ -0,0 +1,836 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A + B + C + D + E + F + G + H + I + J + K + L + M + N + O + P + Q + R + S + T + U + V + W + X + + A + B + C + D + E + F + G + H + I + J + K + L + M + N + O + P + Q + R + S + T + U + V + W + X + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 + 23 + 24 + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 + 23 + 24 + + + + + N + S + E + W + + diff --git a/src/plugin/python_plugins/viewport_field_chart/viewport_field_chart.py b/src/plugin/python_plugins/viewport_field_chart/viewport_field_chart.py new file mode 100644 index 000000000..0f4c37450 --- /dev/null +++ b/src/plugin/python_plugins/viewport_field_chart/viewport_field_chart.py @@ -0,0 +1,141 @@ +#!/bin/env python +# SPDX-License-Identifier: Apache-2.0 + +from xstudio.plugin import HUDPlugin +from xstudio.core import JsonStore, ColourTriplet +from xstudio.core import AttributeRole +import json +import sys +from os import path +import pathlib + +qml_folder_name = "qml/ViewportFieldChartPlugin.1" + +settings_box_qml = """ +import QtQuick +ViewportFieldChartSettingsDialog { +} +""" + +overlay_qml = """ +ViewportFieldChart { +} +""" + +class ViewportFieldChart(HUDPlugin): + + def __init__(self, connection): + + HUDPlugin.__init__( + self, + connection, + "Field Charts", + qml_folder=qml_folder_name) + + self.set_custom_settings_qml(settings_box_qml) + + cdir = path.realpath(path.dirname(__file__)) + + # A json that is a list of the charts provided with this plugin + # that can be toggled on or off + uri = pathlib.Path("{0}/{1}".format(cdir, "qml/mkt_anim_grid.svg")).as_uri() + self.default_charts = { + "Anim Grid 1": str(uri) + } + + # this attr holds the list of charts that has been toggled on to be + # visible. It is registered as a preference so that the visibility + # of the charts persists between sessions for the user + self.charts_visibility = self.add_attribute( + "Visibile Charts", + ["Anim Grid 1"], + register_as_preference=True) + self.charts_visibility.expose_in_ui_attrs_group(["fieldchart_image_set"]) + + # This JSON contains details (file path and visibility) of charts added + # by the user + self.user_charts = self.add_attribute( + "User Charts", + JsonStore({}), + register_as_preference=True) + self.user_charts.expose_in_ui_attrs_group(["fieldchart_image_set"]) + + + # This JSON is the merged dict of user and default charts. It's jsut + # for convenience when building the settings widget that lets us + # toggle the charts on/off + self.all_charts = self.add_attribute( + "All Charts", + JsonStore({})) + self.all_charts.expose_in_ui_attrs_group(["fieldchart_image_set"]) + + # This attr holds merged list of the default charts and user charts that + # are active and therefore visible. It's just a list of the filepath + # to each svg file + self.active_charts = self.add_attribute( + "Active Charts", + []) + self.active_charts.expose_in_ui_attrs_group(["fieldchart_image_set"]) + + + # attribute to hold the current mask safety + self.opacity = self.add_attribute( + "Opacity", + 1.0, + { + "float_scrub_min": 0.0, + "float_scrub_max": 1.0, + "float_scrub_step": 0.1, + "float_display_decimals": 1 + }, + register_as_preference=True) + + self.opacity.expose_in_ui_attrs_group(["fieldchart_settings"]) + self.opacity.set_tool_tip( + "Sets the opacity of the field chart" + ) + + # add an attribute to control the size of the text + self.colour = self.add_attribute( + "Colour", + ColourTriplet(1.0, 1.0, 1.0), + register_as_preference=True) + self.colour.expose_in_ui_attrs_group("fieldchart_settings") + + # here we provide the QML code to instance the item that will draw + # the overlay graphics + self.hud_element_qml(overlay_qml) + + # expose our attributes in the UI layer + self.connect_to_ui() + + self.attribute_changed(self.charts_visibility, AttributeRole.Value) + + def attribute_changed(self, attr, role): + + if attr == self.charts_visibility or attr == self.user_charts: + + # here we update the list of svg filenames that are active + __user_charts = self.user_charts.value() + # merge user charts and default charts + m = {**__user_charts, **self.default_charts} + __active_charts = [] + v = self.charts_visibility.value() + for c in m: + if c in v: + __active_charts.append(m[c]) + self.active_charts.set_value(__active_charts) + self.all_charts.set_value(m) + + + +# This method is required by xSTUDIO +def create_plugin_instance(connection): + return ViewportFieldChart(connection) + + +if __name__=="__main__": + + XSTUDIO = Connection(auto_connect=True) + mask_plugin_instance = create_plugin_instance(XSTUDIO) + XSTUDIO.process_events_forever() \ No newline at end of file diff --git a/src/plugin/python_plugins/viewport_flag_indicator/__init__.py b/src/plugin/python_plugins/viewport_flag_indicator/__init__.py new file mode 100644 index 000000000..8833083b8 --- /dev/null +++ b/src/plugin/python_plugins/viewport_flag_indicator/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +from .viewport_flag_indicator_plugin import create_plugin_instance \ No newline at end of file diff --git a/src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsDialog.qml b/src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsDialog.qml new file mode 100644 index 000000000..2c5415f52 --- /dev/null +++ b/src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsDialog.qml @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import Qt.labs.qmlmodels + +import xStudio 1.0 +import xstudio.qml.models 1.0 + +// This is an adapted version of XsAttributesPanel, allowing us to add a dynamic +// list of masks that the user can enable/disable + +XsWindow { + + id: dialog + width: 500 + minimumHeight: maskOptions.length*25 + 300 + title: "DNEG Pipeline Mask Settings" + property real cwidth: 50 + + XsModuleData { + id: attribute_set + modelDataName: "dnmask_settings" + } + + XsModuleData { + id: mask_options_data + modelDataName: "dnmask_options" + } + + XsModuleData { + id: mask_other_data + modelDataName: "dnmask_other" + } + + function setCWidth(ww) { + if (ww > cwidth) cwidth = ww + } + + XsAttributeValue { + id: __current_show + attributeTitle: "dnMask Current Show" + model: mask_other_data + } + property alias currentShow: __current_show.value + + + // make an alias so the mask options are accessible as an array + property alias maskOptions: mask_options_data + + ColumnLayout { + + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + RowLayout { + spacing: 10 + Layout.fillWidth: true + Rectangle { + Layout.fillWidth: true + height: 1 + color: XsStyleSheet.menuBorderColor + } + XsText { + text: "Active Masks for " + currentShow + Layout.alignment: Qt.AlignHCenter + } + Rectangle { + Layout.fillWidth: true + height: 1 + color: XsStyleSheet.menuBorderColor + } + } + + RowLayout { + + Layout.fillWidth: true + Layout.margins: 10 + + ColumnLayout { + + Layout.margins: 5 + + Repeater { + + // N.B the 'combo_box_options' attribute is propagated via the + // XsToolBox which instantiates the BasicViewportMaskButton + model: maskOptions + + Rectangle { + + height: 20 + color: "transparent" + Layout.preferredWidth: cwidth + Layout.alignment: Qt.AlignRight + + XsText { + id: thetext + anchors.fill: parent + text: title ? title : "" + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + font.strikeout: user_data == undefined + } + + TextMetrics { + id: label_metrics + font: thetext.font + text: thetext.text + } + } + + } + } + + ColumnLayout { + + Layout.fillWidth: true + Layout.margins: 5 + + Repeater { + + model: maskOptions + y: 5 + delegate: Rectangle { + color: "transparent" + height: 20 + Layout.fillWidth: true + XsBoolAttributeCheckBox { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.topMargin: 2 + anchors.bottomMargin: 2 + } + } + + } + } + } + + XsText { + text: "Masks that are struck out have not been set-up for the current on-screen image format." + Layout.alignment: Qt.AlignHCenter + font.italic: true + } + + Item { + Layout.fillHeight: true + } + + RowLayout { + spacing: 10 + Layout.fillWidth: true + Rectangle { + Layout.fillWidth: true + height: 1 + color: XsStyleSheet.menuBorderColor + } + XsText { + text: "Mask Settings" + Layout.alignment: Qt.AlignHCenter + } + Rectangle { + Layout.fillWidth: true + height: 1 + color: XsStyleSheet.menuBorderColor + } + } + + RowLayout { + + id: settings_items + Layout.fillWidth: true + Layout.margins: 10 + + ColumnLayout { + + Layout.margins: 5 + + Repeater { + + // N.B the 'combo_box_options' attribute is propagated via the + // XsToolBox which instantiates the BasicViewportMaskButton + model: attribute_set + id: r1 + + Rectangle { + + height: 20 + color: "transparent" + width: label_metrics.width + onWidthChanged: setCWidth(width) + Layout.alignment: Qt.AlignRight + + XsText { + id: thetext + anchors.fill: parent + text: title ? title : "" + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + y: index*20 + } + + TextMetrics { + id: label_metrics + font: thetext.font + text: thetext.text + } + } + + } + } + + ColumnLayout { + + Layout.fillWidth: true + Layout.margins: 5 + + Repeater { + + model: attribute_set + y: 5 + delegate: chooser + + DelegateChooser { + id: chooser + role: "type" + + DelegateChoice { + roleValue: "FloatScrubber"; + XsFloatAttributeSlider { + height: 20 + Layout.fillWidth: true + } + } + + DelegateChoice { + roleValue: "IntegerValue"; + XsIntAttributeSlider { + height: 20 + Layout.fillWidth: true + } + } + + DelegateChoice { + roleValue: "ColourAttribute"; + XsColourChooser { + height: 20 + Layout.fillWidth: true + } + } + + + DelegateChoice { + roleValue: "OnOffToggle"; + Rectangle { + color: "transparent" + height: 20 + Layout.fillWidth: true + XsBoolAttributeCheckBox { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.topMargin: 2 + anchors.bottomMargin: 2 + } + } + } + + DelegateChoice { + roleValue: "ComboBox"; + Rectangle { + height: 20 + Layout.fillWidth: true + color: "transparent" + XsComboBox { + model: combo_box_options + anchors.fill: parent + property var value_: value ? value : null + onValue_Changed: { + currentIndex = indexOfValue(value_) + } + Component.onCompleted: currentIndex = indexOfValue(value_) + onCurrentValueChanged: { + if (value != currentValue) { + value = currentValue; + } + } + } + } + } + + } + + } + } + } + + Item { + Layout.fillHeight: true + } + + XsSimpleButton { + text: qsTr("Close") + width: XsStyleSheet.primaryButtonStdWidth*2 + Layout.alignment: Qt.AlignVCenter|Qt.AlignRight + onClicked: { + dialog.close() + } + } + } + +} diff --git a/src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsOverlay.qml b/src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsOverlay.qml new file mode 100644 index 000000000..9d27ff1a3 --- /dev/null +++ b/src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/ViewportFlagIndicatorSettingsOverlay.qml @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick.Layouts +import QtQuick +import QuickFuture 1.0 +import QuickPromise 1.0 + +// These imports are necessary to have access to custom QML types that are +// part of the xSTUDIO UI implementation. +import xStudio 1.0 +import xstudio.qml.models 1.0 + +// This overlay is as simple as it gets. We're just putting a circle in a corner +// of the screen that shows the 'flag' colour that is set on the media item that +// is on screen. If no flag is set, we don't show anything +Rectangle { + + width: indicatorSize*(view.width/1920) + height: width + radius: width/2 + color: flag ? flag : "transparent" + + property var flag: currentOnScreenMediaData.values.flagColourRole + + + + // access the 'dnmask_settings' attribute group + XsModuleData { + id: vp_flag_indicator_settings + modelDataName: "vp_flag_indicator" + } + + XsAttributeValue { + id: __indicator_size + attributeTitle: "Indicator Size" + model: vp_flag_indicator_settings + } + property alias indicatorSize: __indicator_size.value + + /*XsAttributeValue { + id: __flag_colours + attributeTitle: "Flag Colours" + model: vp_flag_indicator_settings + } + property alias flagColours: __flag_colours.value*/ + +} \ No newline at end of file diff --git a/src/demos/python_plugins/dnMask/qml/DnMask.1/check.svg b/src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/check.svg old mode 100755 new mode 100644 similarity index 100% rename from src/demos/python_plugins/dnMask/qml/DnMask.1/check.svg rename to src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/check.svg diff --git a/src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/qmldir b/src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/qmldir new file mode 100644 index 000000000..991e282e3 --- /dev/null +++ b/src/plugin/python_plugins/viewport_flag_indicator/qml/ViewportFlagIndicator.1/qmldir @@ -0,0 +1,4 @@ +module DnMask + +DnMaskSettingsDialog 1.0 DnMaskSettingsDialog.qml +DnMaskOverlay 1.0 DnMaskOverlay.qml \ No newline at end of file diff --git a/src/plugin/python_plugins/viewport_flag_indicator/viewport_flag_indicator_plugin.py b/src/plugin/python_plugins/viewport_flag_indicator/viewport_flag_indicator_plugin.py new file mode 100644 index 000000000..96549006d --- /dev/null +++ b/src/plugin/python_plugins/viewport_flag_indicator/viewport_flag_indicator_plugin.py @@ -0,0 +1,107 @@ +#!/bin/env python +# SPDX-License-Identifier: Apache-2.0 + +from xstudio.connection import Connection +from xstudio.plugin import HUDPlugin +from xstudio.core import JsonStore +from xstudio.core import event_atom, show_atom +from xstudio.core import HUDElementPosition +from xstudio.api.session.media import Media +import json + +# path to QML resources relative to this .py file +qml_folder_name = "qml/ViewportFlagIndicator.1" + +# QML code necessary to instance a custom settings panel for this HUD plugin. +# This is the dialog that is shown when the user clicks on the cog icon in the +# HUD button in the viewport toolbar. +# Note that if your plugin uses 'plain' attributes (int, string, float, bool) +# then you don't need a custom settings panel, xstudio will create a default +# one for you with controls for adjusting attributes that you want the user to +# be able to change. +settings_box_qml = """ +import QtQuick +ViewportFlagIndicatorSettingsDialog { +} +""" + +# QML code necessary to create the overlay item that is drawn over the xSTUDIO +# viewport. +overlay_qml = """ +ViewportFlagIndicatorSettingsOverlay { +} +""" + +# Declare our plugin class - we're using the HUDPlugin base class meaning we +# get a toggle under the 'HUD' button in the viewport toolbar to turn our +# hud on and off +class ViewportFlagIndicatorPlugin(HUDPlugin): + + def __init__(self, connection): + + HUDPlugin.__init__( + self, + connection, + "Media Dot", # the name of the HUD item + qml_folder=qml_folder_name, + position_in_hud_list=-10.0) + + # calling this function sets up our custom settings dialog with the + # front end. Uncomment this line to use the customised settings dialog. + # Otherwise the automated settings dialog will show. + # self.set_custom_settings_qml(settings_box_qml) + + # add an attribute to control the size of the flag indicator + self.indicator_size = self.add_attribute( + "Indicator Size", + 30.0, # default size + # additional attribute role data is provided as a dictionary + # as follows. The keys must be valid role names. See attribute.hpp + # for a list of the attribute role data names + { + "float_scrub_min": 5.0, + "float_scrub_max": 100.0, + "float_scrub_step": 2.0, + "float_display_decimals": 2 + }, + # The flag below ensures that if the user changes the Indicator Size + # the value will be stored in the user's preferences files amnd + register_as_preference=True + ) + + # here we add the attribute to a named 'group'. In the qml code, we can + # get to the attribute data using the 'XsModuleData' item and telling + # it which group we want to attach to. + self.indicator_size.expose_in_ui_attrs_group("vp_flag_indicator") + self.indicator_size.set_tool_tip("Set the size of the indicator in pixels") + self.indicator_size.set_redraw_viewport_on_change(True) + + # this call will mean xstudio can add a slider for the indicator size + # attribute to the settings panel for the plugin + self.add_hud_settings_attribute(self.indicator_size) + + # here we provide the QML code to instance the item that will draw + # the overlay graphics + self.hud_element_qml( + overlay_qml, + HUDElementPosition.BottomRight) + + # expose our attributes in the UI layer + self.connect_to_ui() + + # store the current flag colours (per viewport) and current onscreen + # media (also per viewport) + self.flag_colours = {} + self.onscreen_media_items = {} + + +# This method is required by xSTUDIO +def create_plugin_instance(connection): + return ViewportFlagIndicatorPlugin(connection) + + +if __name__=="__main__": + + XSTUDIO = Connection(auto_connect=True) + mask_plugin_instance = create_plugin_instance(XSTUDIO) + XSTUDIO.process_events_forever() \ No newline at end of file diff --git a/src/plugin/utility/CMakeLists.txt b/src/plugin/utility/CMakeLists.txt index 12b698435..beb30c9cf 100644 --- a/src/plugin/utility/CMakeLists.txt +++ b/src/plugin/utility/CMakeLists.txt @@ -1,2 +1,3 @@ +add_src_and_test(session_snapshots) build_studio_plugins("${STUDIO_PLUGINS}") diff --git a/src/plugin/utility/dneg/disable_screensaver/src/CMakeLists.txt b/src/plugin/utility/dneg/disable_screensaver/src/CMakeLists.txt deleted file mode 100644 index e7d14843e..000000000 --- a/src/plugin/utility/dneg/disable_screensaver/src/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -SET(LINK_DEPS - xstudio::utility -) - -create_plugin_with_alias(disable_screensaver xstudio::plugin::disable_screensaver 0.1.0 "${LINK_DEPS}") diff --git a/src/plugin/utility/dneg/disable_screensaver/src/disable_screensaver.cpp b/src/plugin/utility/dneg/disable_screensaver/src/disable_screensaver.cpp deleted file mode 100644 index 1bd3e0ccf..000000000 --- a/src/plugin/utility/dneg/disable_screensaver/src/disable_screensaver.cpp +++ /dev/null @@ -1,230 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include -#include -#include -#include -#include - -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/plugin_manager/plugin_utility.hpp" - -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::plugin_manager; - -class DisableScreensaver : public Utility { - public: - DisableScreensaver(const utility::JsonStore &jsn = utility::JsonStore()) - : Utility("DisableScreensaver", jsn) { - reproc::stop_actions stop = { - {reproc::stop::terminate, reproc::milliseconds(5000)}, - {reproc::stop::kill, reproc::milliseconds(2000)}, - {}}; - - options_.stop = stop; - options_.redirect.parent = false; - } - ~DisableScreensaver() override { stop_screensaver(); } - - void preference_update(const utility::JsonStore &jsn) override { - try { - enabled_ = global_store::preference_value( - jsn, "/plugin/utility/DisableScreensaver/enabled"); - interval_seconds_ = global_store::preference_value( - jsn, "/plugin/utility/DisableScreensaver/interval"); - enabled_when_playing_ = global_store::preference_value( - jsn, "/plugin/utility/DisableScreensaver/enabled_when_playing"); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - - [[nodiscard]] bool enabled() const { return enabled_; } - [[nodiscard]] bool running() const { return running_; } - [[nodiscard]] bool enabled_when_playing() const { return enabled_when_playing_; } - [[nodiscard]] int interval_seconds() const { return interval_seconds_; } - - int start_screensaver() { - if (running_) - return 0; - - process_ = reproc::process(); - std::error_code ec = process_.start( - std::vector{ - "watch", - "-t", - "-n", - std::to_string(interval_seconds_), - "-x", - "xscreensaver-command", - "-deactivate"}, - options_); - if (ec == std::errc::no_such_file_or_directory) { - spdlog::warn( - "{} Program not found. Make sure it's available from the PATH.", - __PRETTY_FUNCTION__); - return ec.value(); - } else if (ec) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, ec.message()); - return ec.value(); - } - - drain_async_ = std::async(std::launch::async, [this]() { - // `sink::thread_safe::string` locks a given mutex before appending to the - // given string, allowing working with the string across multiple threads if - // the mutex is locked in the other threads as well. - reproc::sink::thread_safe::string sink(output_, mutex_); - return reproc::drain(process_, sink, sink); - }); - - spdlog::debug("Starting screensaver block"); - running_ = true; - - return 0; - } - - int stop_screensaver() { - // spdlog::warn("{}",__PRETTY_FUNCTION__); - int status = 0; - if (running_) { - std::error_code ec; - // Check if any errors occurred in the background thread. - // drain.. - report_screensaver(); - - spdlog::debug("Stopping screensaver block"); - - std::tie(status, ec) = process_.stop(options_.stop); - drain_async_.get(); - running_ = false; - - if (ec) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, ec.message()); - return ec.value(); - } - } - - return status; - } - - std::string report_screensaver() { - if (running_) { - if (drain_async_.wait_for(std::chrono::milliseconds(100)) != - std::future_status::ready) { - std::lock_guard lock(mutex_); - auto result = output_; - output_.clear(); - return result; - } - } - return ""; - } - - private: - bool enabled_{false}; - bool enabled_when_playing_{false}; - int interval_seconds_{60}; - reproc::process process_; - reproc::options options_; - bool running_{false}; - std::string output_; - std::mutex mutex_; - std::future drain_async_; -}; - -template class DisableScreensaverPluginActor : public caf::event_based_actor { - - public: - DisableScreensaverPluginActor( - caf::actor_config &cfg, const utility::JsonStore &jsn = utility::JsonStore()) - : caf::event_based_actor(cfg), utility_(jsn) { - - spdlog::debug("Created DisableScreensaverPluginActor"); - - // watch for updates to global preferences - try { - auto prefs = global_store::GlobalStoreHelper(system()); - utility::JsonStore j; - join_broadcast(this, prefs.get_group(j)); - utility_.preference_update(j); - } catch (...) { - } - - // trigger loop.. - delayed_anon_send(this, std::chrono::seconds(5), utility::event_atom_v); - join_event_group(this, system().registry().template get(global_registry)); - - behavior_.assign( - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](global::api_exit_atom) {}, - [=](global::exit_atom) {}, - [=](utility::event_atom, global::status_atom, const global::StatusType status) { - playing_ = status & global::StatusType::ST_BUSY; - - if (playing_ and not utility_.running() and utility_.enabled() and - utility_.enabled_when_playing()) { - utility_.start_screensaver(); - } else if ( - not playing_ and utility_.running() and utility_.enabled() and - utility_.enabled_when_playing()) { - utility_.stop_screensaver(); - } - }, - [=](utility::name_atom) -> std::string { return utility_.name(); }, - - [=](utility::event_atom) { - if (utility_.running()) { - // capture output and dump it. - auto output = utility_.report_screensaver(); - - // check we've not been disabled. - if (not utility_.enabled()) { - utility_.stop_screensaver(); - } - } else { - if (utility_.enabled() and not utility_.enabled_when_playing()) { - utility_.start_screensaver(); - } - } - - delayed_anon_send( - this, - std::chrono::seconds(utility_.interval_seconds()), - utility::event_atom_v); - }, - - [=](json_store::update_atom, - const utility::JsonStore & /*change*/, - const std::string & /*path*/, - const utility::JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); - }, - - [=](json_store::update_atom, const utility::JsonStore &js) { - utility_.preference_update(js); - }); - } - - caf::behavior make_behavior() override { return behavior_; } - - private: - caf::behavior behavior_; - T utility_; - bool playing_{false}; -}; - -extern "C" { -plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { - return new plugin_manager::PluginFactoryCollection( - std::vector>( - {std::make_shared>>( - Uuid("2cd6db34-c696-421a-8564-90fa65d2a59a"), - "DisableScreensaver", - "xStudio", - "Disable screensaver when playing", - semver::version("1.0.0"))})); -} -} diff --git a/src/plugin/utility/dneg/disable_screensaver/test/CMakeLists.txt b/src/plugin/utility/dneg/disable_screensaver/test/CMakeLists.txt deleted file mode 100644 index a73825679..000000000 --- a/src/plugin/utility/dneg/disable_screensaver/test/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -include(CTest) - -SET(LINK_DEPS - caf::core -) - -create_tests("${LINK_DEPS}") diff --git a/src/plugin/utility/dneg/dnrun/src/CMakeLists.txt b/src/plugin/utility/dneg/dnrun/src/CMakeLists.txt index 53cb1d2c4..e29d663c5 100644 --- a/src/plugin/utility/dneg/dnrun/src/CMakeLists.txt +++ b/src/plugin/utility/dneg/dnrun/src/CMakeLists.txt @@ -1,6 +1,7 @@ -SET(LINK_DEPS - xstudio::utility -) - -create_plugin_with_alias(dnrun xstudio::plugin::dnrun 0.1.0 "${LINK_DEPS}") +if (NOT WIN32) + SET(LINK_DEPS + xstudio::utility + ) + create_plugin_with_alias(dnrun xstudio::plugin::dnrun ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") +endif() \ No newline at end of file diff --git a/src/plugin/utility/dneg/dnrun/src/dnrun.cpp b/src/plugin/utility/dneg/dnrun/src/dnrun.cpp index c4012baea..5b39f133b 100644 --- a/src/plugin/utility/dneg/dnrun/src/dnrun.cpp +++ b/src/plugin/utility/dneg/dnrun/src/dnrun.cpp @@ -9,7 +9,10 @@ #include #include +#include + #include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/frame_rate.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/json_store.hpp" #include "xstudio/plugin_manager/plugin_utility.hpp" @@ -91,7 +94,7 @@ class DNRun : public Utility { fds_[0].events = POLLIN; nfds_ = 1; - spdlog::info("DNRun port created: {}", port_name_v1_.c_str()); + spdlog::debug("DNRun port created: {}", port_name_v1_.c_str()); } catch (const std::exception &err) { if (sock_ >= 0) close(sock_); @@ -330,7 +333,7 @@ template class DNRunPluginActor : public caf::event_based_actor { // } catch (...) { // } - send(this, utility::event_atom_v); + mail(utility::event_atom_v).send(this); behavior_.assign( // [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, @@ -361,48 +364,18 @@ template class DNRunPluginActor : public caf::event_based_actor { spdlog::info("DNRun received request {}", jsn.dump(2)); - caf::actor playlist; - - // first, try and add to existing 'current' playlist - try { - // this throws an exception if there is no 'current' playlist - playlist = request_receive( - *sys, session, session::current_playlist_atom_v); - } catch (...) { - } - - // second, try and find the 'Ivy Media' named playlist - if (!playlist) { + bool quickview = jsn.at("args").contains("quickview") && + jsn.at("args").at("quickview"); - playlist = request_receive( - *sys, - session, - session::get_playlist_atom_v, - "DNRun Playlist"); - } - - // third, make a new 'Ivy Media' playlist - if (!playlist) { - - playlist = request_receive( - *sys, - session, - session::add_playlist_atom_v, - "DNRun Playlist") - .second.actor(); - } + // get the session to give us a playlist to add the + // media to. The session decides if it's the + // current playlist or a named playlist based on + // user prefs + auto playlist = request_receive( + *sys, session, session::get_push_playlist_atom_v); bool first = true; - bool quickview = false; - if (jsn.at("args").contains("quickview")) { - if (jsn.at("args")["quickview"].is_boolean()) { - quickview = jsn.at("args").at("quickview"); - } else if (jsn.at("args")["quickview"].is_string()) { - quickview = jsn.at("args").at("quickview") == "true"; - } - } - bool ab_compare = jsn.at("args").contains("compare") && jsn.at("args").at("compare") == "ab"; @@ -437,7 +410,8 @@ template class DNRunPluginActor : public caf::event_based_actor { FrameList fl; caf::uri uri = parse_cli_posix_path(path, fl, true); - validate_media(uri, fl); + // we shouldn't validate... As it maybe a partial + // sequence ? validate_media(uri, fl); UuidActor new_media; if (fl.empty()) @@ -459,39 +433,40 @@ template class DNRunPluginActor : public caf::event_based_actor { Uuid()); if (!new_media.uuid().is_null()) { - auto selection_actor = request_receive( - *sys, - playlist, - playlist::selection_actor_atom_v); - if (selection_actor) { - if (first) { - // clear the selection - request_receive( + + if (quickview) { + auto studio = system() + .registry() + .template get( + studio_registry); + + anon_mail( + ui::open_quickview_window_atom_v, + utility::UuidActorVector({new_media}), + "Off") + .send(studio); + } else { + + auto selection_actor = + request_receive( *sys, - selection_actor, - playlist::select_media_atom_v); - first = false; + playlist, + playlist::selection_actor_atom_v); + if (selection_actor) { + if (first) { + // clear the selection + request_receive( + *sys, + selection_actor, + playlist::select_media_atom_v); + first = false; + } + anon_mail( + playlist::select_media_atom_v, + new_media.uuid()) + .send(selection_actor); } - anon_send( - selection_actor, - playlist::select_media_atom_v, - new_media.uuid()); } - - // trigger the session to (perhaps - - // depending on quick view preference) - // launch a quick viewer for the new - // media - auto studio = - system().registry().template get( - studio_registry); - - anon_send( - studio, - ui::open_quickview_window_atom_v, - utility::UuidActorVector({new_media}), - "Off", - quickview); } } catch (const std::exception &e) { @@ -509,16 +484,20 @@ template class DNRunPluginActor : public caf::event_based_actor { } if (utility_.connected()) - delayed_send(this, std::chrono::milliseconds(100), utility::event_atom_v); + mail(utility::event_atom_v) + .delay(std::chrono::milliseconds(100)) + .send(this); else - delayed_send(this, std::chrono::milliseconds(5000), utility::event_atom_v); + mail(utility::event_atom_v) + .delay(std::chrono::milliseconds(5000)) + .send(this); } // [=](json_store::update_atom, // const utility::JsonStore & /*change*/, // const std::string & /*path*/, // const utility::JsonStore &full) { - // delegate(actor_cast(this), json_store::update_atom_v, full); + // mail(json_store::update_atom_v, full).delegate(actor_cast(this)); // }, // [=](json_store::update_atom, const utility::JsonStore &js) { @@ -553,15 +532,16 @@ void DNRunPluginActor::send_uri_request_to_plugin( const bool quickview, const bool ab_compare) { - request( - plugin_manager, infinite, data_source::use_data_atom_v, uri, session, playlist, rate) + mail(data_source::use_data_atom_v, uri, session, playlist, rate) + .request(plugin_manager, infinite) .then( [=](UuidActorVector &new_media) { if (!new_media.size()) return; // check if we're loading media - request(new_media[0].actor(), infinite, type_atom_v) + mail(type_atom_v) + .request(new_media[0].actor(), infinite) .then( [=](const std::string &type) { if (type == "Media") { @@ -569,14 +549,15 @@ void DNRunPluginActor::send_uri_request_to_plugin( // depending on quick view preference) // launch a quick viewer for the new // media - auto studio = system().registry().template get( - studio_registry); - anon_send( - studio, - ui::open_quickview_window_atom_v, - new_media, - ab_compare ? "Off" : "A/B", - quickview); + if (quickview) { + auto studio = system().registry().template get( + studio_registry); + anon_mail( + ui::open_quickview_window_atom_v, + new_media, + ab_compare ? "Off" : "A/B") + .send(studio); + } } }, [=](error &err) mutable { diff --git a/src/plugin/utility/dneg/dnrun/test/CMakeLists.txt b/src/plugin/utility/dneg/dnrun/test/CMakeLists.txt index a73825679..66caa1970 100644 --- a/src/plugin/utility/dneg/dnrun/test/CMakeLists.txt +++ b/src/plugin/utility/dneg/dnrun/test/CMakeLists.txt @@ -1,7 +1,7 @@ include(CTest) SET(LINK_DEPS - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/plugin/utility/session_snapshots/src/CMakeLists.txt b/src/plugin/utility/session_snapshots/src/CMakeLists.txt new file mode 100644 index 000000000..f544d8daa --- /dev/null +++ b/src/plugin/utility/session_snapshots/src/CMakeLists.txt @@ -0,0 +1,36 @@ +project(session_snapshots VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) + +find_package(Qt6 COMPONENTS Core Quick Gui Widgets Concurrent REQUIRED) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +QT6_WRAP_CPP(SNAPSHOTS_MODEL_UI_MOC_SRC "${CMAKE_CURRENT_SOURCE_DIR}/snapshot_model_ui.hpp") + +set(SOURCES + snapshot_model_ui.cpp + ${SNAPSHOTS_MODEL_UI_MOC_SRC} +) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +add_library(${PROJECT_NAME} SHARED ${SOURCES}) +add_library(xstudio::utility::session_snapshots ALIAS ${PROJECT_NAME}) +default_plugin_options(${PROJECT_NAME}) + +target_link_libraries(${PROJECT_NAME} + PUBLIC + xstudio::utility + xstudio::plugin_manager + xstudio::ui::qml::helper + Qt6::Core + Qt6::Quick + Qt6::Gui + Qt6::Widgets + Qt6::Concurrent +) + +set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) + +add_plugin_qml(${PROJECT_NAME} qml) diff --git a/src/plugin/utility/session_snapshots/src/qml/SessionSnapshots.1/NewSnapshotDialog.qml b/src/plugin/utility/session_snapshots/src/qml/SessionSnapshots.1/NewSnapshotDialog.qml new file mode 100644 index 000000000..8479951d9 --- /dev/null +++ b/src/plugin/utility/session_snapshots/src/qml/SessionSnapshots.1/NewSnapshotDialog.qml @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Dialogs + +import xStudio 1.0 + +XsWindow { + + id: popup + + title: "Select a Snapshot Folder" + property string body: "" + property string initialText: "" + width: 400 + height: 200 + signal response(variant text, variant button_press) + property var chaser + + FolderDialog { + id: select_path_dialog + title: "Select Snapshot Path" + currentFolder: file_functions.defaultSessionFolder() + + onAccepted: { + snapshot_folder.text = selectedFolder + snapshot_name.text = selectedFolder.toString().split('/').pop() + } + } + + ColumnLayout { + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.top: parent.top + anchors.margins: 20 + spacing: 20 + + RowLayout { + + Layout.fillWidth: true + Layout.preferredHeight: 30 + + XsTextField{ + + id: snapshot_folder + Layout.fillWidth: true + Layout.fillHeight: true + wrapMode: Text.Wrap + clip: true + focus: true + placeholderText: "Select Snapshot Folder" + horizontalAlignment: TextInput.AlignLeft + onActiveFocusChanged:{ + if(activeFocus) selectAll() + } + + } + + XsSimpleButton { + + text: "Browse ..." + width: XsStyleSheet.primaryButtonStdWidth*2 + Layout.fillHeight: true + onClicked: { + select_path_dialog.open() + } + } + } + + RowLayout { + + Layout.fillWidth: true + Layout.preferredHeight: 30 + + XsTextField + { + id: snapshot_name + Layout.fillWidth: true + Layout.fillHeight: true + text: "" + placeholderText: "Set Menu Title" + wrapMode: Text.Wrap + clip: true + focus: true + horizontalAlignment: TextInput.AlignLeft + onActiveFocusChanged:{ + if(activeFocus) selectAll() + } + + } + + } + + Item { + Layout.fillHeight: true + } + + RowLayout { + Layout.alignment: Qt.AlignRight + XsSimpleButton { + text: "Cancel" + minWidth: XsStyleSheet.primaryButtonStdWidth*2 + Layout.alignment: Qt.AlignRight + onClicked: { + popup.visible = false + } + } + XsSimpleButton { + text: "Select Folder" + minWidth: XsStyleSheet.primaryButtonStdWidth*2 + Layout.alignment: Qt.AlignRight + onClicked: { + if(snapshot_name.text.length && snapshot_folder.text.length) { + let v = snapshot_paths + v.push({'path': snapshot_folder.text, "name": snapshot_name.text}) + snapshot_paths = v + snapshot_folder.text = "" + snapshot_name.text = "" + popup.visible = false + } else { + dialogHelpers.errorDialogFunc( + "Error", + "You must select a folder from the filesystem and provide a snapshot menu title." + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/src/plugin/utility/session_snapshots/src/qml/SessionSnapshots.1/SessionSnapshotsMenu.qml b/src/plugin/utility/session_snapshots/src/qml/SessionSnapshots.1/SessionSnapshotsMenu.qml new file mode 100644 index 000000000..9505c8ab0 --- /dev/null +++ b/src/plugin/utility/session_snapshots/src/qml/SessionSnapshots.1/SessionSnapshotsMenu.qml @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + + +import QtQml 2.14 + +import QuickFuture 1.0 + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 +import SessionSnapshots 1.0 + +import "." + +XsMenuItemNew { + + is_in_bar: true + id: snapshotMenu + + property var menuContextData: undefined + property var menu_item_enabled: true + property var menu_icon: undefined + property var name: "Snapshots" + property var hotkey_sequence: undefined + property var action_idx + + function saveSnapshot(snapshot_name, button) { + + if (button != "Save Snapshot") return + let path = snapshotModel.buildSavePath(action_idx, snapshot_name) + file_functions.saveSessionAs(path, undefined, doRescan) + + } + + function saveSelectedSnapshot(snapshot_name, button) { + + if (button != "Save Snapshot") return + let path = snapshotModel.buildSavePath(action_idx, snapshot_name) + file_functions.saveSelectionAs(path, undefined, doRescan) + } + + function doRescan() { + if (action_idx && action_idx.valid) snapshotModel.rescan(action_idx, 1); + } + + function newFolder(folder_name, button) { + if (button != "Create Folder") return + snapshotModel.createFolder(action_idx, folder_name) + doRescan() + } + + XsModelProperty { + id: __snapshot_paths + role: "valueRole" + index: globalStoreModel.searchRecursive("/core/snapshot/paths", "pathRole") + } + property alias snapshot_paths: __snapshot_paths.value + + SessionSnapshotsMenuModel { + id: snapshotModel + paths: snapshot_paths + onJsonChanged: { + menu_model_index = snapshotModel.index(-1, -1) + } + + // we provide this function which is called by certain instances of + // XsMenuItem within the sub menus that we are dynamically building + // to show the filesystem tree in these menus. + // When a sub menu becomes visible, we get the XsSnapshotMenuModel to + // rescan the filesystem folder whose content is shown in the given + // sub-menu. + function menuItemVisibilityChanged(index, visibility, panel) { + if (visibility) { + rescan(index, 1); + } + } + + function nodeActivated(index, data, panel) { + + var action_type = get(index, "user_data") + var path = get(index, "snapshot_filesystem_path") + var name = get(index, "name") + action_idx = index.parent + + if (action_type == "SNAPSHOT_REMOVE_SNAPSHOT") { + + let v = snapshot_paths + let new_v = [] + for(let i =0; i< v.length;i++) { + if(helpers.QUrlFromQString(path) != helpers.QUrlFromQString(v[i].path)) + new_v.push(v[i]) + } + snapshot_paths = new_v + + } else if (action_type == "SNAPSHOT_ADD") { + + loader.sourceComponent = new_snapshot_folder_dlg + loader.item.visible = true + + } else if (action_type == "SNAPSHOT_SAVE") { + + dialogHelpers.textInputDialog( + saveSnapshot, + "Save Snapshot", + "Enter a name for your session snapshot.", + "", + ["Cancel", "Save Snapshot"]) + + } else if (action_type == "SNAPSHOT_SELECTED_SAVE") { + + dialogHelpers.textInputDialog( + saveSelectedSnapshot, + "Save Selected Snapshot", + "Enter a name for your session snapshot.", + "", + ["Cancel", "Save Snapshot"]) + + } else if (action_type == "SNAPSHOT_NEW_FOLDER") { + + action_idx = index + dialogHelpers.textInputDialog( + newFolder, + "Create a New Folder", + "Enter a name for a new snapshot sub-folder", + "", + ["Cancel", "Create Folder"]) + + } else if (action_type == "SNAPSHOT_REVEAL") { + console.log(helpers.showURIS([helpers.QUrlFromQString(path)])) + } else if (action_type == "SNAPSHOT_LOAD") { + + Future.promise(theSessionData.importFuture(path, null)).then( + function(result) { + // if (result) { + // dialogHelpers.errorDialogFunc("Snapshot Loaded", "Snapshot " + name + " was added to your session.") + // } else { + // dialogHelpers.errorDialogFunc("Snapshot Error", "Snapshot " + name + " was not added to your session, an error occurred. Check your terminal for more info.") + // } + }) + + } + } + } + + menu_model: snapshotModel + menu_model_index: snapshotModel.index(-1, -1) + + Component { + id: new_snapshot_folder_dlg + NewSnapshotDialog { + id: snapshot_path_dialog + onVisibilityChanged: { + if (!visible) { + // not sure if this actually helps, but assuming + // that 'unloading' the dialog destroys it + loader.sourceComponent = undefined + } + } + } + } + + Loader { + id: loader + } +} diff --git a/src/plugin/utility/session_snapshots/src/qml/SessionSnapshots.1/qmldir b/src/plugin/utility/session_snapshots/src/qml/SessionSnapshots.1/qmldir new file mode 100644 index 000000000..ae10044f1 --- /dev/null +++ b/src/plugin/utility/session_snapshots/src/qml/SessionSnapshots.1/qmldir @@ -0,0 +1,6 @@ +module SessionSnapshots + +SessionSnapshotsMenu 1.0 SessionSnapshotsMenu.qml +NewSnapshotDialog 1.0 NewSnapshotDialog.qml + +plugin session_snapshots diff --git a/src/plugin/utility/session_snapshots/src/snapshot_model_ui.cpp b/src/plugin/utility/session_snapshots/src/snapshot_model_ui.cpp new file mode 100644 index 000000000..52f3c8bdd --- /dev/null +++ b/src/plugin/utility/session_snapshots/src/snapshot_model_ui.cpp @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "snapshot_model_ui.hpp" + +CAF_PUSH_WARNINGS +#include +#include +CAF_POP_WARNINGS + +using namespace xstudio; +using namespace xstudio::utility; +using namespace xstudio::ui::qml; +using namespace xstudio::session_snapshots; + +SnapshotMenuModel::SnapshotMenuModel(QObject *parent) : JSONTreeModel(parent) { + + // This model provides the same roles as the MenusModelData class which + // is the QAbstractItemModel that drives xSTUDIO's dynamic menu creation. + setRoleNames(std::vector{ + "uuid", + "menu_model", + "menu_item_type", + "name", + "is_checked", + "choices", + "current_choice", + "hotkey_uuid", + "menu_icon", + "custom_menu_qml", + "user_data", + "snapshot_filesystem_path", + "hotkey_sequence", + "menu_item_enabled", + "watch_visibility"}); + + try { + items_.bind_ignore_entry_func(ignore_not_session); + rescan(QModelIndex(), 1); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} + +Q_INVOKABLE void SnapshotMenuModel::rescan(const QModelIndex &index, const int depth) { + + // slightly awkward . The items_ contains a list of 'root' items. One root + // might contain the folder of another root item. + // For example, I might have /user_data/ as one of my snapshot folders + // and /user_data/ted/ as another root snapshot folder. They both refer + // to /user_data/ted. + // + // So we have to check for this scenario and make sure all nodes in the + // FileSystemItem tree are updated if they refer to the same folder + std::vector items; + bool changed = false; + + if (index.isValid()) { + auto path = UriFromQUrl(get(index, "snapshot_filesystem_path").toUrl()); + for (auto &item : items_) { + auto i = item.find_by_path(path); + if (i) + items.push_back(i); + } + + } else { + items.push_back(&items_); + changed = true; + } + + for (auto &item : items) { + changed |= item->scan(depth); + } + + if (changed && !items.empty()) { + auto jsn = toMenuModelItemData(items[0]); + if (index.isValid()) { + setData(index, QVariantMapFromJson(jsn), JSONRole); + } else { + setModelData(jsn); + } + } +} + +utility::JsonStore SnapshotMenuModel::toMenuModelItemData(FileSystemItem *item) { + + // here we recursively build a JsonStore the represents a tree of menus. + // This is converted to model data in qml by the base class and drives + // the XsMenu and XsMenuItem qml items to dynamicall build nested menus + // that show the filesystem folders that have been added to the snapshot + // paths + + utility::JsonStore menu_item_data; + const std::string snapshot_filesystem_path(to_string(item->path())); + + if (item->type() == utility::FSIT_FILE) { + + menu_item_data["name"] = item->name(); + menu_item_data["menu_item_type"] = "button"; + menu_item_data["menu_item_enabled"] = true; + menu_item_data["snapshot_filesystem_path"] = snapshot_filesystem_path; + menu_item_data["watch_visibility"] = false; + menu_item_data["user_data"] = "SNAPSHOT_LOAD"; + + + } else if (item->type() != utility::FSIT_NONE) { + + // Not a file but a folder ... so we need to build a submenu to + // show the content of this subfolder + + menu_item_data["name"] = item->name(); + menu_item_data["menu_item_type"] = "menu"; + menu_item_data["children"] = nlohmann::json::array(); + menu_item_data["snapshot_filesystem_path"] = snapshot_filesystem_path; + + // setting this means the menu system will run a callback when the + // corresponding sub-menu for this folder becomes visible. We use + // this to trigger a scan to fill in the sub-menu contents + menu_item_data["watch_visibility"] = true; + + // make separate lists of files and subfolders + std::vector subfolders; + std::vector files; + for (auto p = item->begin(); p != item->end(); ++p) { + if (p->type() == utility::FSIT_FILE) { + files.push_back(&(*p)); + } else { + subfolders.push_back(&(*p)); + } + } + + // sort alaphabetically + std::sort( + subfolders.begin(), subfolders.end(), [](const auto &a, const auto &b) -> bool { + return utility::to_lower(a->name()) < utility::to_lower(b->name()); + }); + std::sort(files.begin(), files.end(), [](const auto &a, const auto &b) -> bool { + return utility::to_lower(a->name()) < utility::to_lower(b->name()); + }); + + // now add the subfolders as children of this menu + if (subfolders.size()) { + for (auto &subfolder : subfolders) { + menu_item_data["children"].push_back(toMenuModelItemData(subfolder)); + } + utility::JsonStore divider; + divider["name"] = ""; + divider["menu_item_type"] = "divider"; + divider["menu_item_enabled"] = true; + menu_item_data["children"].push_back(divider); + } + + // now add the files as children of this menu + if (files.size()) { + for (auto &file : files) { + menu_item_data["children"].push_back(toMenuModelItemData(file)); + } + utility::JsonStore divider; + divider["name"] = ""; + divider["menu_item_type"] = "divider"; + divider["menu_item_enabled"] = true; + menu_item_data["children"].push_back(divider); + } + + // append the common menu items for the folder + if (item != &items_) { + utility::JsonStore save_snapshot_item; + save_snapshot_item["name"] = "Save Snapshot ..."; + save_snapshot_item["menu_item_type"] = "button"; + save_snapshot_item["menu_item_enabled"] = true; + save_snapshot_item["snapshot_filesystem_path"] = snapshot_filesystem_path; + save_snapshot_item["watch_visibility"] = false; + save_snapshot_item["user_data"] = "SNAPSHOT_SAVE"; + menu_item_data["children"].push_back(save_snapshot_item); + + utility::JsonStore save_selected_snapshot_item; + save_selected_snapshot_item["name"] = "Save Selected As Snapshot ..."; + save_selected_snapshot_item["menu_item_type"] = "button"; + save_selected_snapshot_item["menu_item_enabled"] = true; + save_selected_snapshot_item["snapshot_filesystem_path"] = snapshot_filesystem_path; + save_selected_snapshot_item["watch_visibility"] = false; + save_selected_snapshot_item["user_data"] = "SNAPSHOT_SELECTED_SAVE"; + menu_item_data["children"].push_back(save_selected_snapshot_item); + + + utility::JsonStore options_menu_item; + options_menu_item["name"] = "Options"; + options_menu_item["menu_item_type"] = "menu"; + options_menu_item["children"] = nlohmann::json::array(); + options_menu_item["watch_visibility"] = false; + + utility::JsonStore create_folder_item; + create_folder_item["name"] = "Create New Folder ..."; + create_folder_item["menu_item_type"] = "button"; + create_folder_item["menu_item_enabled"] = true; + create_folder_item["snapshot_filesystem_path"] = snapshot_filesystem_path; + create_folder_item["watch_visibility"] = false; + create_folder_item["user_data"] = "SNAPSHOT_NEW_FOLDER"; + options_menu_item["children"].push_back(create_folder_item); + + utility::JsonStore reveal_folder_item; + reveal_folder_item["name"] = "Reveal On Disk ..."; + reveal_folder_item["menu_item_type"] = "button"; + reveal_folder_item["menu_item_enabled"] = true; + reveal_folder_item["snapshot_filesystem_path"] = snapshot_filesystem_path; + reveal_folder_item["watch_visibility"] = false; + reveal_folder_item["user_data"] = "SNAPSHOT_REVEAL"; + options_menu_item["children"].push_back(reveal_folder_item); + + menu_item_data["children"].push_back(options_menu_item); + } + + // the first snapshot menus have a 'remove' option. We know we are + // building the first level menu if our 'item' is a child of items_. + bool top_level = false; + for (auto &i : items_) { + if (item == &i) { + top_level = true; + break; + } + } + + if (top_level) { + utility::JsonStore divider; + divider["name"] = ""; + divider["menu_item_type"] = "divider"; + divider["menu_item_enabled"] = true; + menu_item_data["children"].push_back(divider); + + utility::JsonStore remove_menu_item; + remove_menu_item["name"] = "Remove " + item->name() + " Menu"; + remove_menu_item["menu_item_type"] = "button"; + remove_menu_item["menu_item_enabled"] = true; + remove_menu_item["snapshot_filesystem_path"] = snapshot_filesystem_path; + remove_menu_item["watch_visibility"] = false; + remove_menu_item["user_data"] = "SNAPSHOT_REMOVE_SNAPSHOT"; + menu_item_data["children"].push_back(remove_menu_item); + } + + if (item == &items_) { + utility::JsonStore add_snapshot_menu_item; + add_snapshot_menu_item["name"] = "Add Snapshot Folder ..."; + add_snapshot_menu_item["menu_item_type"] = "button"; + add_snapshot_menu_item["menu_item_enabled"] = true; + add_snapshot_menu_item["snapshot_filesystem_path"] = snapshot_filesystem_path; + add_snapshot_menu_item["watch_visibility"] = false; + add_snapshot_menu_item["user_data"] = "SNAPSHOT_ADD"; + menu_item_data["children"].push_back(add_snapshot_menu_item); + } + } + + return menu_item_data; +} + +void SnapshotMenuModel::setPaths(const QVariant &value) { + try { + + if (not value.isNull()) { + paths_ = value; + emit pathsChanged(); + + auto jsn = mapFromValue(paths_); + if (jsn.is_array()) { + items_.clear(); + + for (const auto &i : jsn) { + if (i.count("name") and i.count("path")) { + auto uri = caf::make_uri(i.at("path").get()); + if (uri) + items_.insert(items_.end(), FileSystemItem(i.at("name"), *uri)); + } + } + } + rescan(QModelIndex(), 1); + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} + +bool SnapshotMenuModel::createFolder(const QModelIndex &index, const QString &name) { + auto result = false; + + // get path.. + if (index.isValid()) { + auto uri = + caf::make_uri(StdFromQString(get(index, "snapshot_filesystem_path").toString())); + + if (uri) { + auto path = uri_to_posix_path(*uri); + auto new_path = fs::path(path) / StdFromQString(name); + try { + fs::create_directory(new_path); + rescan(index.parent().parent()); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } else { + spdlog::warn( + "{} {} {}", + __PRETTY_FUNCTION__, + "failed to create uri", + StdFromQString(get(index, "snapshot_filesystem_path").toString())); + } + } else { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, "Invalid index"); + } + + return result; +} + +QUrl SnapshotMenuModel::buildSavePath(const QModelIndex &index, const QString &name) const { + // get path.. + auto result = QUrl(); + if (index.isValid()) { + auto uri = + caf::make_uri(StdFromQString(get(index, "snapshot_filesystem_path").toString())); + if (uri) { + auto path = uri_to_posix_path(*uri); + auto new_path = fs::path(path) / std::string(StdFromQString(name) + ".xsz"); + result = QUrlFromUri(posix_path_to_uri(new_path.string())); + } + } + + return result; +} + +SessionSnapshotsPlugin::SessionSnapshotsPlugin( + caf::actor_config &cfg, const utility::JsonStore &init_settings) + : plugin::StandardPlugin(cfg, "SessionSnapshotsPlugin", init_settings) { + // this call is essential to set-up the base class + make_behavior(); + + // new method for instantiating a 'singleton' qml item which can + // do a one-time insertion of menu items into any menu model + + // In this case we add a custom menu item to the 'main menu bar' menu model. + // We provide the qml code to instantiate the menu widget when the user + // clicks on the top level menu item. + register_singleton_qml( + R"( + import QtQuick + import xstudio.qml.models 1.0 + + XsMenuModelItem { + id: menu_item + text: "Snapshots" + menuPath: "" + menuItemType: "custom" + menuItemPosition: 4.0 + menuModelName: "main menu bar" + customMenuQml: "import SessionSnapshots 1.0; SessionSnapshotsMenu {}" + } + )"); +} + +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>( + {std::make_shared>( + SessionSnapshotsPlugin::PLUGIN_UUID, + "SessionSnapshots", + plugin_manager::PluginFlags::PF_UTILITY, + true, // resident + "Al Crate/Ted Waine", + "Session Snapshots Plugin - Menu driven system for saving and loading sessions " + "from specific filesystem locations.")})); +} +} + +//![plugin] +class SnapshotMenuModelQml : public QQmlExtensionPlugin { + Q_OBJECT + Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid) + + public: + void registerTypes(const char *uri) override { + qmlRegisterType(uri, 1, 0, "SessionSnapshotsMenuModel"); + } +}; +//![plugin] +#include "snapshot_model_ui.moc" diff --git a/src/plugin/utility/session_snapshots/src/snapshot_model_ui.hpp b/src/plugin/utility/session_snapshots/src/snapshot_model_ui.hpp new file mode 100644 index 000000000..4bae89cb9 --- /dev/null +++ b/src/plugin/utility/session_snapshots/src/snapshot_model_ui.hpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/ui/qml/helper_ui.hpp" +#include "xstudio/ui/qml/json_tree_model_ui.hpp" +#include "xstudio/utility/file_system_item.hpp" +#include "xstudio/plugin_manager/plugin_base.hpp" + + +CAF_PUSH_WARNINGS +#include +CAF_POP_WARNINGS + +namespace xstudio::session_snapshots { + +using namespace caf; + +class SessionSnapshotsPlugin : public plugin::StandardPlugin { + public: + inline static const utility::Uuid PLUGIN_UUID = + utility::Uuid("5a095fde-296c-4d78-90e1-7da55ee44806"); + + SessionSnapshotsPlugin(caf::actor_config &cfg, const utility::JsonStore &init_settings); + virtual ~SessionSnapshotsPlugin() = default; +}; + +class SnapshotMenuModel : public ui::qml::JSONTreeModel { + Q_OBJECT + + Q_PROPERTY(QVariant paths READ paths WRITE setPaths NOTIFY pathsChanged) + + public: + explicit SnapshotMenuModel(QObject *parent = nullptr); + + QVariant paths() const { return paths_; } + void setPaths(const QVariant &value); + + Q_INVOKABLE bool createFolder(const QModelIndex &index, const QString &name); + + Q_INVOKABLE void rescan(const QModelIndex &index = QModelIndex(), const int depth = 0); + Q_INVOKABLE QUrl buildSavePath(const QModelIndex &index, const QString &name) const; + + QML_NAMED_ELEMENT("SnapshotMenuModel") + + signals: + void pathsChanged(); + + protected: + utility::JsonStore toMenuModelItemData(utility::FileSystemItem *item); + + + private: + utility::FileSystemItem items_; + + QVariant paths_; +}; + +} // namespace xstudio::session_snapshots diff --git a/src/plugin/utility/session_snapshots/test/CMakeLists.txt b/src/plugin/utility/session_snapshots/test/CMakeLists.txt new file mode 100644 index 000000000..66caa1970 --- /dev/null +++ b/src/plugin/utility/session_snapshots/test/CMakeLists.txt @@ -0,0 +1,7 @@ +include(CTest) + +SET(LINK_DEPS + CAF::core +) + +create_tests("${LINK_DEPS}") diff --git a/src/plugin/viewport_layout/CMakeLists.txt b/src/plugin/viewport_layout/CMakeLists.txt new file mode 100644 index 000000000..dd2ef64ec --- /dev/null +++ b/src/plugin/viewport_layout/CMakeLists.txt @@ -0,0 +1,6 @@ +add_src_and_test(composite_viewport_layout) +add_src_and_test(default_viewport_layout) +add_src_and_test(grid_viewport_layout) +add_src_and_test(wipe_viewport_layout) + +build_studio_plugins("${STUDIO_PLUGINS}") diff --git a/src/plugin/viewport_layout/composite_viewport_layout/src/CMakeLists.txt b/src/plugin/viewport_layout/composite_viewport_layout/src/CMakeLists.txt new file mode 100644 index 000000000..b954025c2 --- /dev/null +++ b/src/plugin/viewport_layout/composite_viewport_layout/src/CMakeLists.txt @@ -0,0 +1,11 @@ + +SET(LINK_DEPS + xstudio::module + xstudio::plugin_manager + xstudio::ui::opengl::viewport + Imath::Imath +) + +find_package(Imath) + +create_plugin_with_alias(composite_viewport_layout xstudio::viewport::composite_viewport_layout ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") \ No newline at end of file diff --git a/src/plugin/viewport_layout/composite_viewport_layout/src/composite_viewport_layout.cpp b/src/plugin/viewport_layout/composite_viewport_layout/src/composite_viewport_layout.cpp new file mode 100644 index 000000000..d3b6453d8 --- /dev/null +++ b/src/plugin/viewport_layout/composite_viewport_layout/src/composite_viewport_layout.cpp @@ -0,0 +1,394 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include + +#include "composite_viewport_layout.hpp" +#include "xstudio/media_reader/image_buffer.hpp" +#include "xstudio/ui/opengl/opengl_viewport_renderer.hpp" +#include "xstudio/ui/opengl/opengl_offscreen_renderer.hpp" +#include "xstudio/ui/opengl/shader_program_base.hpp" +#include "xstudio/ui/opengl/opengl_multi_buffered_texture.hpp" + +using namespace xstudio::utility; +using namespace xstudio::ui::viewport; +using namespace xstudio; + +namespace { +const char *vertex_shader = R"( + #version 330 core + layout (location = 0) in vec4 aPos; + uniform mat4 to_canvas; + uniform mat4 canvas_to_image; + out vec2 canvasCoordinate; + + void main() + { + vec4 rpos = aPos; + gl_Position = (rpos*to_canvas); + vec4 ipos = (aPos*canvas_to_image); + canvasCoordinate = (rpos.xy + vec2(1.0, 1.0))*0.5f; + } + )"; + +const char *frag_shader = R"( + #version 410 core + out vec4 FragColor; + uniform sampler2D textureSamplerA; + uniform sampler2D textureSamplerB; + in vec2 canvasCoordinate; + uniform float boost; + uniform bool screen; + uniform bool monochrome; + + void main(void) + { + vec4 a = texture(textureSamplerA, canvasCoordinate); + vec4 b = texture(textureSamplerB, canvasCoordinate); + if (screen) { + + vec4 m = max(a, b); + vec4 screen = vec4(1.0) - (vec4(1.0) - a)*(vec4(1.0) - b); + FragColor = vec4( + m.x > 1.0 ? m.x : screen.x, + m.y > 1.0 ? m.y : screen.y, + m.z > 1.0 ? m.z : screen.z, + m.w > 1.0 ? m.w : screen.w + ); + + } else if (monochrome) { + float al = length(a.rgb); + float bl = length(b.xyz); + float scale = pow(2.0, boost); + float d = (al - bl)*scale; + FragColor = vec4(0.5) + vec4(vec3(d), (a.a-b.a)*scale); + } else { + float scale = pow(2.0, boost); + vec4 d = (a - b)*scale; + FragColor = vec4(0.5, 0.5, 0.5, 0.5) + d; + } + } + + )"; +} // namespace + +using namespace xstudio::ui::opengl; + +class OpenGLViewportCompositeRenderer : public OpenGLViewportRenderer { + + public: + OpenGLViewportCompositeRenderer( + const std::string &window_id, const utility::JsonStore &prefs) + : OpenGLViewportRenderer(window_id, prefs) {} + + virtual ~OpenGLViewportCompositeRenderer() = default; + + void pre_init() override { + OpenGLViewportRenderer::pre_init(); + offscreen_texture_target_A_ = std::make_unique(GL_RGBA32F); + offscreen_texture_target_B_ = std::make_unique(GL_RGBA32F); + shader_ = std::make_unique(vertex_shader, frag_shader); + } + + void draw_image( + const media_reader::ImageBufPtr &image, + const media_reader::ImageSetLayoutDataPtr &layout_data, + const int index, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx) override; + + void render_difference( + const media_reader::ImageBufPtr &image_to_be_drawn, + const bool first_im, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx, + const utility::JsonStore ¶ms); + + void render_screen( + const media_reader::ImageBufPtr &image_to_be_drawn, + const bool first_im, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx); + + OpenGLOffscreenRendererPtr offscreen_texture_target_A_; + OpenGLOffscreenRendererPtr offscreen_texture_target_B_; + std::unique_ptr shader_; +}; + +void OpenGLViewportCompositeRenderer::draw_image( + const media_reader::ImageBufPtr &image_to_be_drawn, + const media_reader::ImageSetLayoutDataPtr &layout_data, + const int index, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx) { + + active_shader_program_->use(); + + int mode = layout_data->custom_layout_data_.value("mode", 0); + int first_im_idx = layout_data->custom_layout_data_.value("first_im", 0); + + if (mode == 3 || mode == 4) { + + render_difference( + image_to_be_drawn, + index == first_im_idx, + window_to_viewport_matrix, + viewport_to_image_space, + viewport_du_dx, + layout_data->custom_layout_data_); + return; + } + // set-up core shader parameters (e.g. image transform matrix etc) + utility::JsonStore shader_params = core_shader_params( + image_to_be_drawn, + window_to_viewport_matrix, + viewport_to_image_space, + viewport_du_dx, + layout_data->custom_layout_data_, + index); + + if (mode == 0) { + float blend_ratio = layout_data->custom_layout_data_.value("blend_ratio", 0.5f); + if (index == first_im_idx) { + // we don't blend the first image, just draw it + glDisable(GL_BLEND); + } else { + glBlendColor(blend_ratio, blend_ratio, blend_ratio, blend_ratio); + glEnable(GL_BLEND); + glBlendFunc(GL_CONSTANT_COLOR, GL_ONE_MINUS_CONSTANT_COLOR); + glBlendEquation(GL_FUNC_ADD); + } + } else { + + if (index == first_im_idx) { + // we don't pre-mult the first image, just draw it + glDisable(GL_BLEND); + } else { + shader_params["use_alpha"] = true; + glEnable(GL_BLEND); + if (mode == 1) { + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE); + } else { + // 'Add' mode + glBlendFunc(GL_ONE, GL_ONE); + } + glBlendEquation(GL_FUNC_ADD); + } + } + + active_shader_program_->set_shader_parameters(shader_params); + + // the actual draw .. a quad that spans -1.0, 1.0 in x & y. + glBindVertexArray(vao()); + glEnableVertexAttribArray(0); + glDrawArrays(GL_TRIANGLES, 0, 6); + glDisableVertexAttribArray(0); + glBindVertexArray(0); + + glUseProgram(0); + + // this set-up allows the image to be drawn 'under' overlays that are + // drawn first onto the black canvas - (e.g. annotations that can + // be drawn better this way) + glDisable(GL_BLEND); +} + +void OpenGLViewportCompositeRenderer::render_difference( + const media_reader::ImageBufPtr &image_to_be_drawn, + const bool first_im, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx, + const utility::JsonStore &mode_params) { + { + auto offscreen_texture_target = + first_im ? offscreen_texture_target_A_.get() : offscreen_texture_target_B_.get(); + + // STEP 1 - render the viewport (for given image) to a texture + offscreen_texture_target->resize( + Imath::V2f(viewport_coords_in_window()[2], viewport_coords_in_window()[3])); + offscreen_texture_target->begin(); + + glDisable(GL_SCISSOR_TEST); + glClear(GL_COLOR_BUFFER_BIT); + utility::JsonStore j; + utility::JsonStore shader_params = core_shader_params( + image_to_be_drawn, Imath::M44f(), viewport_to_image_space, viewport_du_dx, j, 0); + + active_shader_program_->set_shader_parameters(shader_params); + + // the actual draw .. a quad that spans -1.0, 1.0 in x & y. + glBindVertexArray(vao()); + glEnableVertexAttribArray(0); + glDrawArrays(GL_TRIANGLES, 0, 6); + glDisableVertexAttribArray(0); + glBindVertexArray(0); + + glUseProgram(0); + offscreen_texture_target->end(); + } + + // if this is first image, return as we only continue to render the difference + // if we've got the 2nd image rendered to a texture + if (first_im) + return; + + + // STEP 2 - render the difference image + utility::JsonStore params; + params["to_canvas"] = window_to_viewport_matrix; + params["canvas_to_image"] = viewport_to_image_space; + params["textureSamplerA"] = 10; + params["textureSamplerB"] = 11; + params["monochrome"] = mode_params.value("monochrome", true); + params["boost"] = mode_params.value("boost", 0.0f); + params["screen"] = mode_params.value("screen", false); + + // set the active tex IDs for our texture targets + glActiveTexture(GL_TEXTURE10); + glBindTexture(GL_TEXTURE_2D, offscreen_texture_target_A_->texture_handle()); + glActiveTexture(GL_TEXTURE11); + glBindTexture(GL_TEXTURE_2D, offscreen_texture_target_B_->texture_handle()); + + shader_->use(); + shader_->set_shader_parameters(params); + + glEnable(GL_SCISSOR_TEST); + + glBindVertexArray(vao()); + glEnableVertexAttribArray(0); + glDrawArrays(GL_TRIANGLES, 0, 6); + glDisableVertexAttribArray(0); + glBindVertexArray(0); + + glUseProgram(0); + glBindTexture(GL_TEXTURE_2D, 0); +} + +CompositeViewportLayout::CompositeViewportLayout( + caf::actor_config &cfg, const utility::JsonStore &init_settings) + : ViewportLayoutPlugin(cfg, init_settings) { + + blend_ratio_ = add_float_attribute("Blend Ratio", "Blend Ratio", 0.5f, 0.0f, 1.0f, 0.005f); + difference_boost_ = + add_float_attribute("Difference Boost", "Diff Boost", 0.0f, -5.0f, 5.0f, 0.005f); + monochrome_ = add_boolean_attribute("Monochrome", "Monochrome", true); + + add_layout_mode( + "Over", 1.0, playhead::AssemblyMode::AM_TEN, playhead::AutoAlignMode::AAM_ALIGN_FRAMES); + + add_layout_mode( + "A/B Blend", + 2.0, + playhead::AssemblyMode::AM_TEN, + playhead::AutoAlignMode::AAM_ALIGN_FRAMES); + + add_layout_mode( + "Add", 3.0, playhead::AssemblyMode::AM_TEN, playhead::AutoAlignMode::AAM_ALIGN_FRAMES); + + add_layout_mode( + "A/B Difference", + 4.0, + playhead::AssemblyMode::AM_TEN, + playhead::AutoAlignMode::AAM_ALIGN_FRAMES); + + add_layout_mode( + "Screen", + 5.0, + playhead::AssemblyMode::AM_TEN, + playhead::AutoAlignMode::AAM_ALIGN_FRAMES); + + add_layout_settings_attribute(blend_ratio_, "A/B Blend"); + add_layout_settings_attribute(difference_boost_, "A/B Difference"); + add_layout_settings_attribute(monochrome_, "A/B Difference"); + + blend_ratio_->set_preference_path("/plugin/composite_vp_modes/blend_ratio"); + difference_boost_->set_preference_path("/plugin/composite_vp_modes/difference_boost"); + monochrome_->set_preference_path("/plugin/composite_vp_modes/difference_monochrome"); +} + +ViewportRenderer *CompositeViewportLayout::make_renderer( + const std::string &window_id, const utility::JsonStore &prefs) { + return new OpenGLViewportCompositeRenderer(window_id, prefs); +} + +void CompositeViewportLayout::do_layout( + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set, + media_reader::ImageSetLayoutData &layout_data) { + const int num_images = image_set->num_onscreen_images(); + if (!num_images) { + return; + } + + layout_data.draw_hero_overlays_only_ = true; + + if (num_images >= 2 && (layout_mode == "A/B Blend" || layout_mode == "A/B Difference" || + layout_mode == "Screen")) { + + // if 'hero' image is at index 0, we wipe between image 0 and image 1 + // otherwise we wipe between hero image and image 0. + int wipeA = image_set->hero_sub_playhead_index(); + layout_data.custom_layout_data_["first_im"] = wipeA; + + layout_data.image_draw_order_hint_.push_back(wipeA); + if (num_images > 1) { + int wipeB = image_set->previous_hero_sub_playhead_index() != -1 + ? image_set->previous_hero_sub_playhead_index() + : wipeA ? 0 + : 1; + layout_data.image_draw_order_hint_.push_back(wipeB); + } + + // identity matrices here, no transform for A/B wipes + layout_data.image_transforms_.resize(image_set->num_onscreen_images()); + + if (layout_mode == "A/B Blend") { + layout_data.custom_layout_data_["blend_ratio"] = blend_ratio_->value(); + layout_data.custom_layout_data_["mode"] = 0; + } else if (layout_mode == "A/B Difference") { + layout_data.custom_layout_data_["mode"] = 3; + layout_data.custom_layout_data_["monochrome"] = monochrome_->value(); + layout_data.custom_layout_data_["boost"] = difference_boost_->value(); + } else if (layout_mode == "Screen") { + layout_data.custom_layout_data_["mode"] = 4; + layout_data.custom_layout_data_["screen"] = true; + } + + layout_data.layout_aspect_ = image_aspect(image_set->onscreen_image(wipeA)); + + } else { + + // draw the images in reverse order. So Image selection 0 is drawn + // 'over' image 1 etc (if in over mode) + for (int i = 0; i < num_images; ++i) { + layout_data.image_draw_order_hint_.push_back(num_images - i - 1); + } + layout_data.custom_layout_data_["first_im"] = + layout_data.image_draw_order_hint_.front(); + + // identity matrices here, no transform for 'over' mode' + layout_data.image_transforms_.resize(image_set->num_onscreen_images()); + + layout_data.layout_aspect_ = image_aspect(image_set->onscreen_image(num_images - 1)); + layout_data.custom_layout_data_["mode"] = layout_mode == "Over" ? 1 : 2; + } +} + +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>( + {std::make_shared>( + CompositeViewportLayout::PLUGIN_UUID, + "CompositeViewportLayout", + plugin_manager::PluginFlags::PF_VIEWPORT_RENDERER, + true, + "Ted Waine", + "Composite Viewport Layout Plugin")})); +} +} diff --git a/src/plugin/viewport_layout/composite_viewport_layout/src/composite_viewport_layout.hpp b/src/plugin/viewport_layout/composite_viewport_layout/src/composite_viewport_layout.hpp new file mode 100644 index 000000000..2ba27c3fc --- /dev/null +++ b/src/plugin/viewport_layout/composite_viewport_layout/src/composite_viewport_layout.hpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/ui/viewport/viewport_layout_plugin.hpp" + +namespace xstudio { +namespace ui { + namespace viewport { + + /** + * @brief GridViewportLayout class. + * + * @details + * This plugin provides a layout plugin for arranging multiple images + * in a grid + */ + + class CompositeViewportLayout : public ViewportLayoutPlugin { + public: + CompositeViewportLayout( + caf::actor_config &cfg, const utility::JsonStore &init_settings); + + ~CompositeViewportLayout() override = default; + + inline static const utility::Uuid PLUGIN_UUID = { + "fdb9204f-f00f-4345-8616-b7bfbedc5829"}; + + protected: + virtual ViewportRenderer * + make_renderer(const std::string &window_id, const utility::JsonStore &prefs); + + void do_layout( + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set, + media_reader::ImageSetLayoutData &layout_data) override; + + module::FloatAttribute *blend_ratio_; + module::FloatAttribute *difference_boost_; + module::BooleanAttribute *monochrome_; + }; + } // namespace viewport +} // namespace ui +} // namespace xstudio diff --git a/src/plugin/viewport_layout/composite_viewport_layout/test/CMakeLists.txt b/src/plugin/viewport_layout/composite_viewport_layout/test/CMakeLists.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugin/viewport_layout/default_viewport_layout/src/CMakeLists.txt b/src/plugin/viewport_layout/default_viewport_layout/src/CMakeLists.txt new file mode 100644 index 000000000..bf68976f7 --- /dev/null +++ b/src/plugin/viewport_layout/default_viewport_layout/src/CMakeLists.txt @@ -0,0 +1,11 @@ + +SET(LINK_DEPS + xstudio::module + xstudio::plugin_manager + xstudio::ui::opengl::viewport + Imath::Imath +) + +find_package(Imath) + +create_plugin_with_alias(default_viewport_layout xstudio::viewport::default_viewport_layout ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") \ No newline at end of file diff --git a/src/plugin/viewport_layout/default_viewport_layout/src/viewport_layout_default.cpp b/src/plugin/viewport_layout/default_viewport_layout/src/viewport_layout_default.cpp new file mode 100644 index 000000000..0624adbfd --- /dev/null +++ b/src/plugin/viewport_layout/default_viewport_layout/src/viewport_layout_default.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include + +#include "viewport_layout_default.hpp" +#include "xstudio/media_reader/image_buffer.hpp" +#include "xstudio/ui/opengl/opengl_viewport_renderer.hpp" + +using namespace xstudio::utility; +using namespace xstudio::ui::viewport; +using namespace xstudio; + +DefaultViewportLayout::DefaultViewportLayout( + caf::actor_config &cfg, const utility::JsonStore &init_settings) + : ViewportLayoutPlugin(cfg, init_settings) { + + // Note: the base class takes care of making the actual layout data. The + // default layout is to just display the 'hero' image in the ImageDisplaySet + // with no transform applied. + + // String mode will trigger the playhead's logic for AM_STRING AssemblyMode + // where it takes N input sources and 'strings' them togoether so that they + // play in sequence. + + // this is awkward - to avoid circular build dependency between openglviewport and viewport + // I have to use this DefaultViewportLayout for backing python plugins that + // are a ViewportLayoutPlugin. We don't want to add these layout modes if + // this is an instance that is backing a python plugin. + if (!init_settings.value("is_python", false)) { + add_layout_mode("Off", 102.0, playhead::AssemblyMode::AM_ONE); + add_layout_mode( + "A/B", + 101.0, + playhead::AssemblyMode::AM_TEN, + playhead::AutoAlignMode::AAM_ALIGN_FRAMES); + add_layout_mode("String", 100.0, playhead::AssemblyMode::AM_STRING); + } +} + +ViewportRenderer *DefaultViewportLayout::make_renderer( + const std::string &window_id, const utility::JsonStore &prefs) { + return new opengl::OpenGLViewportRenderer(window_id, prefs); +} + +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>( + {std::make_shared>( + DefaultViewportLayout::PLUGIN_UUID, + "DefaultViewportLayout", + plugin_manager::PluginFlags::PF_VIEWPORT_RENDERER, + true, + "Ted Waine", + "Basic Viewport Layout Plugin")})); +} +} diff --git a/src/plugin/viewport_layout/default_viewport_layout/src/viewport_layout_default.hpp b/src/plugin/viewport_layout/default_viewport_layout/src/viewport_layout_default.hpp new file mode 100644 index 000000000..dd065b7b3 --- /dev/null +++ b/src/plugin/viewport_layout/default_viewport_layout/src/viewport_layout_default.hpp @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + + +#include "xstudio/ui/viewport/viewport_layout_plugin.hpp" + +#define NO_FRAME INT_MIN + + +namespace xstudio { +namespace ui { + namespace viewport { + + /** + * @brief DefaultViewportLayout class. + * + * @details + * This plugin provides the default viewport layout behviour. It simply + * plots a single image to the viewport fitted to the -1.0 < x < 1.0 and + * centered on (0.0, 0.0). + * + * When there are multiple playhead sources selected it simply plots the + * 'hero' image and ignores other image streams. + * No compositing or multi-image layouts are available. + * + * It provides the 'String' and 'Off' + */ + + class DefaultViewportLayout : public ViewportLayoutPlugin { + public: + DefaultViewportLayout( + caf::actor_config &cfg, const utility::JsonStore &init_settings); + + ~DefaultViewportLayout() override = default; + + inline static const utility::Uuid PLUGIN_UUID = { + "f7d63ed9-80ed-4ce9-8b39-1d5e5079ce4b"}; + + ViewportRenderer *make_renderer( + const std::string &window_id, const utility::JsonStore &prefs) override; + }; + } // namespace viewport +} // namespace ui +} // namespace xstudio diff --git a/src/plugin/viewport_layout/default_viewport_layout/test/CMakeLists.txt b/src/plugin/viewport_layout/default_viewport_layout/test/CMakeLists.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugin/viewport_layout/grid_viewport_layout/src/CMakeLists.txt b/src/plugin/viewport_layout/grid_viewport_layout/src/CMakeLists.txt new file mode 100644 index 000000000..574fa37d9 --- /dev/null +++ b/src/plugin/viewport_layout/grid_viewport_layout/src/CMakeLists.txt @@ -0,0 +1,11 @@ + +SET(LINK_DEPS + xstudio::module + xstudio::plugin_manager + xstudio::ui::opengl::viewport + Imath::Imath +) + +find_package(Imath) + +create_plugin_with_alias(grid_viewport_layout xstudio::viewport::grid_viewport_layout ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") \ No newline at end of file diff --git a/src/plugin/viewport_layout/grid_viewport_layout/src/viewport_layout_grid.cpp b/src/plugin/viewport_layout/grid_viewport_layout/src/viewport_layout_grid.cpp new file mode 100644 index 000000000..37970b86f --- /dev/null +++ b/src/plugin/viewport_layout/grid_viewport_layout/src/viewport_layout_grid.cpp @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include + +#include "viewport_layout_grid.hpp" +#include "xstudio/media_reader/image_buffer.hpp" +#include "xstudio/ui/opengl/opengl_viewport_renderer.hpp" + +using namespace xstudio::utility; +using namespace xstudio::ui::viewport; +using namespace xstudio; + +GridViewportLayout::GridViewportLayout( + caf::actor_config &cfg, const utility::JsonStore &init_settings) + : ViewportLayoutPlugin(cfg, init_settings) { + + spacing_ = add_float_attribute("Spacing", "Spacing", 0.0f, 0.0f, 50.0f, 0.5f); + spacing_->set_redraw_viewport_on_change(true); + spacing_->set_role_data( + module::Attribute::ToolTip, + "Spacing between images in grid layout as a % of image size."); + add_layout_settings_attribute(spacing_, "Grid"); + + aspect_mode_ = add_string_choice_attribute( + "Cell Aspect", + "Cell Aspect", + "Auto", + std::vector({"Auto", "Hero", "16:9", "1.89"})); + aspect_mode_->set_redraw_viewport_on_change(true); + aspect_mode_->set_role_data( + module::Attribute::ToolTip, + R"(Set how the cell aspect is set. Auto means the most common image aspect in the +set of images being displayed is used. Hero means that the current 'hero' image +sets the aspect of all cells in the layout. 16:9 or 1.89 forces equivalent aspect.)"); + add_layout_settings_attribute(aspect_mode_, "Grid"); + aspect_mode_->set_preference_path("/plugin/viewport_layout/grid_layout_aspect_mode"); + + add_layout_mode("Grid", 20.0, playhead::AssemblyMode::AM_ALL); + add_layout_mode("Horizontal", 21.0, playhead::AssemblyMode::AM_ALL); + add_layout_mode("Vertical", 22.0, playhead::AssemblyMode::AM_ALL); +} + +void GridViewportLayout::do_layout( + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set, + media_reader::ImageSetLayoutData &layout_data) { + const int num_images = image_set->num_onscreen_images(); + if (!num_images) { + return; + } + + if (!image_set) + return; + + const auto hero_image = image_set->hero_image(); + + float hero_aspect = image_aspect(hero_image); + if (aspect_mode_->value() == "Auto") { + std::map aspects; + for (int i = 0; i < num_images; ++i) { + const auto &im = image_set->onscreen_image(i); + if (im) { + float a = image_aspect(im); + if (aspects.count(a)) { + aspects[a]++; + } else { + aspects[a] = 1; + } + } + } + int c = 0; + for (const auto &p : aspects) { + if (p.second > c) { + c = p.second; + hero_aspect = p.first; + } + } + } else if (hero_aspect && aspect_mode_->value() == "Hero") { + } else if (aspect_mode_->value() == "16:9") { + hero_aspect = 16.0f / 9.0f; + } else if (aspect_mode_->value() == "1.89") { + hero_aspect = 2048.0f / 1080.0f; + } + + layout_data.image_transforms_.resize(image_set->num_onscreen_images()); + + int num_rows = layout_mode == "Grid" ? (int)round(sqrt(float(num_images))) + : layout_mode == "Vertical" ? num_images + : 1; + int num_cols = layout_mode == "Grid" ? (int)ceil(float(num_images) / float(num_rows)) + : layout_mode == "Vertical" ? 1 + : num_images; + + layout_data.layout_aspect_ = (hero_aspect) * float(num_cols) / float(num_rows); + layout_data.draw_hero_overlays_only_ = false; + + float scale = (1.0f - spacing_->value() / 100.0f) / float(num_cols); + + float x_off = -1.0f + 1.0f / num_cols; + float y_off = (-1.0f + 1.0f / num_rows) / layout_data.layout_aspect_; + float x_step = num_cols > 1 ? 2.0f / (num_cols) : 0.0f; + float y_step = num_rows > 1 ? 2.0f / (layout_data.layout_aspect_ * num_rows) : 0.0f; + + for (int i = 0; i < num_images; ++i) { + + const auto &im = image_set->onscreen_image(i); + float xs = 1.0f; + if (im) { + if (image_aspect(im) < hero_aspect) { + xs = image_aspect(im) / hero_aspect; + } + } + int row = i / num_cols; + int col = i % num_cols; + Imath::M44f &m = layout_data.image_transforms_[i]; + m.makeIdentity(); + m.translate(Imath::V3f(x_off + col * x_step, y_off + row * y_step, 0.0f)); + m.scale(Imath::V3f(scale * xs, scale * xs, 1.0f)); + } + + // we draw all images. It doesn't matter the order that the are drawn + // in so we will just respect their order in the ImageSet. + layout_data.image_draw_order_hint_.resize(num_images); + for (int i = 0; i < num_images; ++i) { + layout_data.image_draw_order_hint_[i] = i; + } +} + +ViewportRenderer *GridViewportLayout::make_renderer( + const std::string &window_id, const utility::JsonStore &prefs) { + return new opengl::OpenGLViewportRenderer(window_id, prefs); +} + +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>( + {std::make_shared>( + GridViewportLayout::PLUGIN_UUID, + "GridViewportLayout", + plugin_manager::PluginFlags::PF_VIEWPORT_RENDERER, + true, + "Ted Waine", + "Grid Viewport Layout Plugin")})); +} +} diff --git a/src/plugin/viewport_layout/grid_viewport_layout/src/viewport_layout_grid.hpp b/src/plugin/viewport_layout/grid_viewport_layout/src/viewport_layout_grid.hpp new file mode 100644 index 000000000..8e17abfd9 --- /dev/null +++ b/src/plugin/viewport_layout/grid_viewport_layout/src/viewport_layout_grid.hpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/ui/viewport/viewport_layout_plugin.hpp" + +namespace xstudio { +namespace ui { + namespace viewport { + + /** + * @brief GridViewportLayout class. + * + * @details + * This plugin provides a layout plugin for arranging multiple images + * in a grid + */ + + class GridViewportLayout : public ViewportLayoutPlugin { + public: + GridViewportLayout(caf::actor_config &cfg, const utility::JsonStore &init_settings); + + ~GridViewportLayout() override = default; + + inline static const utility::Uuid PLUGIN_UUID = { + "b2f442c8-a185-4267-af98-66af43fdce68"}; + + protected: + ViewportRenderer *make_renderer( + const std::string &window_id, const utility::JsonStore &prefs) override; + + void do_layout( + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set, + media_reader::ImageSetLayoutData &layout_data) override; + + module::FloatAttribute *spacing_; + module::StringChoiceAttribute *aspect_mode_; + }; + } // namespace viewport +} // namespace ui +} // namespace xstudio diff --git a/src/plugin/viewport_layout/grid_viewport_layout/test/CMakeLists.txt b/src/plugin/viewport_layout/grid_viewport_layout/test/CMakeLists.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugin/viewport_layout/wipe_viewport_layout/src/CMakeLists.txt b/src/plugin/viewport_layout/wipe_viewport_layout/src/CMakeLists.txt new file mode 100644 index 000000000..09ed34913 --- /dev/null +++ b/src/plugin/viewport_layout/wipe_viewport_layout/src/CMakeLists.txt @@ -0,0 +1,13 @@ + +SET(LINK_DEPS + xstudio::module + xstudio::plugin_manager + xstudio::ui::opengl::viewport + Imath::Imath +) + +find_package(Imath) + +create_plugin_with_alias(wipe_viewport_layout xstudio::viewport::wipe_viewport_layout ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") + +add_plugin_qml(${PROJECT_NAME} qml) \ No newline at end of file diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/.clang-tidy b/src/plugin/viewport_layout/wipe_viewport_layout/src/qml/.clang-tidy similarity index 100% rename from src/plugin/data_source/dneg/shotgun/src/qml/.clang-tidy rename to src/plugin/viewport_layout/wipe_viewport_layout/src/qml/.clang-tidy diff --git a/src/plugin/viewport_layout/wipe_viewport_layout/src/qml/WipeLayoutOverlay.1/WipeLayoutOverlay.qml b/src/plugin/viewport_layout/wipe_viewport_layout/src/qml/WipeLayoutOverlay.1/WipeLayoutOverlay.qml new file mode 100644 index 000000000..305952402 --- /dev/null +++ b/src/plugin/viewport_layout/wipe_viewport_layout/src/qml/WipeLayoutOverlay.1/WipeLayoutOverlay.qml @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQuick +import QuickFuture +import QuickPromise + +// These imports are necessary to have access to custom QML types that are +// part of the xSTUDIO UI implementation. +import xStudio 1.0 +import xstudio.qml.models 1.0 +import "." + +// Our Overlay is based on a transparent rectangle that simply fills the +// xSTUDIO view. Within this we draw the overlay graphics as required. +Item { + + // Note that viewport overlays are instanced by the Viewport QML instance + // which has the id 'viewport' and is visible to us here. To learn more + // about the viewport see files Xsview.qml and qml_view.cpp from + // the xSTUDIO source code. + + property var crosshair_size: 74 + property var circle_size: 12 + opacity: 0.7 + + id: control + width: crosshair_size + height: crosshair_size + x: wipe_position.x*parent.width-crosshair_size/2 + y: wipe_position.y*parent.height-crosshair_size/2 + property bool hovered: ma.containsMouse || ma.pressed + + visible: title == viewportPlayhead.compare_mode && viewportPlayhead.numSubPlayheads > 1 + + WipeShadowLine { + height: crosshair_size/2-circle_size/2 + width: 1 + x: crosshair_size/2 + } + + WipeShadowLine { + height: crosshair_size/2-circle_size/2 + width: 1 + y: crosshair_size-height + x: crosshair_size/2 + } + + WipeShadowLine { + width: crosshair_size/4-circle_size/2 + height: 1 + y: crosshair_size/2 + x: crosshair_size/2-crosshair_size/5 + } + + WipeShadowLine { + width: crosshair_size/4-circle_size/2 + height: 1 + x: crosshair_size/2+circle_size/2 + y: crosshair_size/2 + } + + Rectangle { + height: circle_size + width: circle_size + radius: circle_size/2 + x: parent.width/2+1-radius + y: parent.width/2+1-radius + color: "transparent" + border.color: "black" + MouseArea { + id: ma + anchors.fill: parent + hoverEnabled: true + onPressed: (mouse)=> { + x0=mouse.x-width/2 + y0=mouse.y-height/2 + } + property real x0 + property real y0 + onPositionChanged: (mouse)=> { + if (pressed) { + // wipe_position can't go outside the viewport or we'd lose + // it offscreen and wouldn't be able to get it back + var pt = mapToItem(view, mouse.x-x0, mouse.y-y0); + pt.x = Math.max(0.01, Math.min(0.99, pt.x/view.width)) + pt.y = Math.max(0.01, Math.min(0.99, pt.y/view.height)) + var a = wipe_position + a.x = pt.x + a.y = pt.y + wipe_position = a + } + } + } + + } + + Rectangle { + id: target + height: circle_size + width: circle_size + radius: circle_size/2 + x: parent.width/2-radius + y: parent.width/2-radius + color: hovered ? palette.highlight : "transparent" + border.color: "white" + } + + // This accesses the attribute group called wipe_layout_attrs. It contains + // just one attr ... see the cpp code for where this comes from + XsModuleData { + id: wipe_attrs + modelDataName: "wipe_layout_attrs" + } + + // Here we make an alias to attach to the Wipe attr that we need + XsAttributeValue { + id: __wipe_position + attributeTitle: "Wipe Position" + model: wipe_attrs + } + property alias wipe_position: __wipe_position.value + +} diff --git a/src/plugin/viewport_layout/wipe_viewport_layout/src/qml/WipeLayoutOverlay.1/WipeShadowLine.qml b/src/plugin/viewport_layout/wipe_viewport_layout/src/qml/WipeLayoutOverlay.1/WipeShadowLine.qml new file mode 100644 index 000000000..4d7c93024 --- /dev/null +++ b/src/plugin/viewport_layout/wipe_viewport_layout/src/qml/WipeLayoutOverlay.1/WipeShadowLine.qml @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + +Rectangle { + color: "white" + clip: false + Rectangle { + x: 1 + y: 1 + width: parent.width + height: parent.height + color: "black" + } +} diff --git a/src/plugin/viewport_layout/wipe_viewport_layout/src/qml/WipeLayoutOverlay.1/qmldir b/src/plugin/viewport_layout/wipe_viewport_layout/src/qml/WipeLayoutOverlay.1/qmldir new file mode 100644 index 000000000..87eb28596 --- /dev/null +++ b/src/plugin/viewport_layout/wipe_viewport_layout/src/qml/WipeLayoutOverlay.1/qmldir @@ -0,0 +1,3 @@ +module WipeLayoutOverlay + +WipeLayoutOverlay 1.0 WipeLayoutOverlay.qml \ No newline at end of file diff --git a/src/plugin/viewport_layout/wipe_viewport_layout/src/wipe_viewport_layout.cpp b/src/plugin/viewport_layout/wipe_viewport_layout/src/wipe_viewport_layout.cpp new file mode 100644 index 000000000..2e767ea7e --- /dev/null +++ b/src/plugin/viewport_layout/wipe_viewport_layout/src/wipe_viewport_layout.cpp @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include + +#include "wipe_viewport_layout.hpp" +#include "xstudio/media_reader/image_buffer.hpp" +#include "xstudio/ui/opengl/opengl_viewport_renderer.hpp" +#include "xstudio/ui/opengl/shader_program_base.hpp" +#include "xstudio/ui/opengl/opengl_multi_buffered_texture.hpp" + +using namespace xstudio::utility; +using namespace xstudio::ui::viewport; +using namespace xstudio; + + +using namespace xstudio::ui::opengl; + +class OpenGLViewportWipeRenderer : public OpenGLViewportRenderer { + + public: + OpenGLViewportWipeRenderer(const std::string &window_id, const utility::JsonStore &prefs) + : OpenGLViewportRenderer(window_id, prefs) {} + + virtual ~OpenGLViewportWipeRenderer() { + if (wipe_vbo_) + glDeleteBuffers(1, &wipe_vbo_); + if (wipe_vao_) + glDeleteVertexArrays(1, &wipe_vao_); + } + + void pre_init() override { + OpenGLViewportRenderer::pre_init(); + glGenBuffers(1, &wipe_vbo_); + glGenVertexArrays(1, &wipe_vao_); + } + + void draw_image( + const media_reader::ImageBufPtr &image, + const media_reader::ImageSetLayoutDataPtr &layout_data, + const int index, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx) override; + + unsigned int wipe_vbo_ = 0; + unsigned int wipe_vao_ = 0; +}; + +void OpenGLViewportWipeRenderer::draw_image( + const media_reader::ImageBufPtr &image_to_be_drawn, + const media_reader::ImageSetLayoutDataPtr &layout_data, + const int index, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx) { + + // wipe value is the position of the wipe handle normalised to the + // viewport width. + float wipe_screen_space = 0.5f; + if (layout_data && layout_data->custom_layout_data_.contains("wipe_pos")) { + wipe_screen_space = layout_data->custom_layout_data_.value("wipe_pos", 0.5f); + } + + const bool first_image = layout_data->image_draw_order_hint_.size() > 1 && + index == layout_data->image_draw_order_hint_[1] + ? false + : true; + + if (wipe_screen_space <= 0.011f && first_image) { + // wipe at the far left of the screen. Don't draw the wipe + return; + } else if (wipe_screen_space > 0.989f && !first_image) { + // wipe at the far right of the screen. Don't draw the wipe + return; + } + + // re-normalise to -1.0,1.0 and multiply by projection matrix to get wipe + // position in image space + Imath::V4f w(-1.0f + wipe_screen_space * 2.0f, 0.0f, 0.0f, 1.0f); + w *= viewport_to_image_space; + float wipe_pos_image_space = w.x / w.w; + + if (wipe_pos_image_space <= -1.0f && first_image) { + // wipe is all the way left. Only draw second image! + return; + } else if (wipe_pos_image_space > 0.9999f && !first_image) { + // wipe is all the way right. Don't draw second image! + return; + } + + const bool no_wipe = layout_data->image_draw_order_hint_.size() < 2 || + (wipe_screen_space <= 0.011f || wipe_screen_space > 0.989f) || + (wipe_pos_image_space <= -1.0f || wipe_pos_image_space > 0.9999f); + + + active_shader_program_->use(); + + // set-up core shader parameters (e.g. image transform matrix etc) + utility::JsonStore shader_params = core_shader_params( + image_to_be_drawn, + window_to_viewport_matrix, + viewport_to_image_space, + viewport_du_dx, + layout_data->custom_layout_data_, + index); + + active_shader_program_->set_shader_parameters(shader_params); + + { + + float left = no_wipe ? -1.0f : first_image ? -1.0 : wipe_pos_image_space; + float right = no_wipe ? 1.0f : first_image ? wipe_pos_image_space : 1.0; + std::array vertices = { + // 1st triangle + left, + 1.0f, + 0.0f, + 1.0f, // top left + right, + 1.0f, + 0.0f, + 1.0f, // top right + right, + -1.0f, + 0.0f, + 1.0f, // bottom right + // 2nd triangle + right, + -1.0f, + 0.0f, + 1.0f, // bottom right + left, + 1.0f, + 0.0f, + 1.0f, // top left + left, + -1.0f, + 0.0f, + 1.0f // bottom left + }; + + glBindVertexArray(wipe_vao_); + // 2. copy our vertices array in a buffer for OpenGL to use + glBindBuffer(GL_ARRAY_BUFFER, wipe_vbo_); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices.data(), GL_STATIC_DRAW); + // 3. then set our vertex module pointers + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr); + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + // the actual draw! + glEnableVertexAttribArray(0); + glDrawArrays(GL_TRIANGLES, 0, 6); + glDisableVertexAttribArray(0); + glBindVertexArray(0); + } + + + glUseProgram(0); +} + +WipeViewportLayout::WipeViewportLayout( + caf::actor_config &cfg, const utility::JsonStore &init_settings) + : ViewportLayoutPlugin(cfg, init_settings) { + + wipe_position_ = add_vec4f_attribute( + "Wipe Position", "Wipe Position", Imath::V4f(0.5f, 0.5f, 0.0f, 1.0f)); + wipe_position_->set_redraw_viewport_on_change(true); + wipe_position_->set_role_data( + module::Attribute::ToolTip, + "Spacing between images in grid layout as a % of image size."); + wipe_position_->expose_in_ui_attrs_group("wipe_layout_attrs"); + + add_layout_mode( + "Wipe", 2.0, playhead::AssemblyMode::AM_TEN, playhead::AutoAlignMode::AAM_ALIGN_FRAMES); + + add_viewport_layout_qml_overlay( + "Wipe", + R"( + import WipeLayoutOverlay 1.0 + WipeLayoutOverlay { + } + )"); +} + +void WipeViewportLayout::do_layout( + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set, + media_reader::ImageSetLayoutData &layout_data) { + const int num_images = image_set->num_onscreen_images(); + if (!num_images) { + return; + } + + // if 'hero' image is at index 0, we wipe between image 0 and image 1 + // otherwise we wipe between hero image and image 0. + int wipeA = image_set->hero_sub_playhead_index(); + + layout_data.image_draw_order_hint_.push_back(wipeA); + if (num_images > 1) { + int wipeB = image_set->previous_hero_sub_playhead_index() != -1 + ? image_set->previous_hero_sub_playhead_index() + : wipeA ? 0 + : 1; + layout_data.image_draw_order_hint_.push_back(wipeB); + } + + // identity matrices here, no transform for A/B wipes + layout_data.image_transforms_.resize(image_set->num_onscreen_images()); + layout_data.custom_layout_data_["wipe_pos"] = wipe_position_->value().x; + + layout_data.layout_aspect_ = image_aspect(image_set->onscreen_image(wipeA)); + layout_data.draw_hero_overlays_only_ = false; +} + +ViewportRenderer *WipeViewportLayout::make_renderer( + const std::string &window_id, const utility::JsonStore &prefs) { + return new OpenGLViewportWipeRenderer(window_id, prefs); +} + +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>( + {std::make_shared>( + WipeViewportLayout::PLUGIN_UUID, + "WipeViewportLayout", + plugin_manager::PluginFlags::PF_VIEWPORT_RENDERER, + true, + "Ted Waine", + "Wipe Viewport Layout Plugin")})); +} +} diff --git a/src/plugin/viewport_layout/wipe_viewport_layout/src/wipe_viewport_layout.hpp b/src/plugin/viewport_layout/wipe_viewport_layout/src/wipe_viewport_layout.hpp new file mode 100644 index 000000000..087dfa23e --- /dev/null +++ b/src/plugin/viewport_layout/wipe_viewport_layout/src/wipe_viewport_layout.hpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/ui/viewport/viewport_layout_plugin.hpp" + +namespace xstudio { +namespace ui { + namespace viewport { + + /** + * @brief GridViewportLayout class. + * + * @details + * This plugin provides a layout plugin for arranging multiple images + * in a grid + */ + + class WipeViewportLayout : public ViewportLayoutPlugin { + public: + WipeViewportLayout(caf::actor_config &cfg, const utility::JsonStore &init_settings); + + ~WipeViewportLayout() override = default; + + inline static const utility::Uuid PLUGIN_UUID = { + "fa41b47a-6fde-4627-bd38-6e7834e75007"}; + + protected: + virtual ViewportRenderer * + make_renderer(const std::string &window_id, const utility::JsonStore &prefs); + + void do_layout( + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set, + media_reader::ImageSetLayoutData &layout_data) override; + + module::Vec4fAttribute *wipe_position_; + }; + } // namespace viewport +} // namespace ui +} // namespace xstudio diff --git a/src/plugin/viewport_layout/wipe_viewport_layout/test/CMakeLists.txt b/src/plugin/viewport_layout/wipe_viewport_layout/test/CMakeLists.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugin/viewport_overlay/CMakeLists.txt b/src/plugin/viewport_overlay/CMakeLists.txt index 4056b725f..25e514562 100644 --- a/src/plugin/viewport_overlay/CMakeLists.txt +++ b/src/plugin/viewport_overlay/CMakeLists.txt @@ -1,4 +1,6 @@ add_src_and_test(basic_viewport_mask) add_src_and_test(annotations) +add_src_and_test(audio_waveform) +add_src_and_test(media_metadata_hud) build_studio_plugins("${STUDIO_PLUGINS}") diff --git a/src/plugin/viewport_overlay/annotations/src/CMakeLists.txt b/src/plugin/viewport_overlay/annotations/src/CMakeLists.txt index c12246157..05ce294e4 100644 --- a/src/plugin/viewport_overlay/annotations/src/CMakeLists.txt +++ b/src/plugin/viewport_overlay/annotations/src/CMakeLists.txt @@ -1,15 +1,18 @@ -project(annotations_tool VERSION 0.1.0 LANGUAGES CXX) +project(annotations_tool VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) find_package(Imath) +find_package(Qt6 COMPONENTS Core Quick Gui REQUIRED) set(SOURCES annotations_tool.cpp annotation.cpp annotation_opengl_renderer.cpp annotation_serialiser.cpp - serialisers/1.0/serialiser_1_pt_0.cpp + serialisers/1.0/serialiser_1_pt_0.cpp ) +qt6_add_resources(SOURCES qml/AnnotationsTool.2/anno_tools.qrc) + set(CMAKE_INCLUDE_CURRENT_DIR ON) add_library(${PROJECT_NAME} SHARED ${SOURCES}) @@ -22,8 +25,11 @@ target_link_libraries(${PROJECT_NAME} xstudio::plugin_manager xstudio::ui::opengl::viewport Imath::Imath + Qt6::Core + Qt6::Quick + Qt6::Gui ) set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) -add_subdirectory(qml) \ No newline at end of file +add_plugin_qml(${PROJECT_NAME} qml) \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/annotation.hpp b/src/plugin/viewport_overlay/annotations/src/annotation.hpp index f00c25e3a..4f7b74133 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotation.hpp +++ b/src/plugin/viewport_overlay/annotations/src/annotation.hpp @@ -22,6 +22,8 @@ namespace ui { [[nodiscard]] utility::JsonStore serialise(utility::Uuid &plugin_uuid) const override; + size_t hash() const override { return canvas_.hash(); } + xstudio::ui::canvas::Canvas &canvas() { return canvas_; } const xstudio::ui::canvas::Canvas &canvas() const { return canvas_; } diff --git a/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.cpp b/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.cpp index ea6720ba3..c6a3d72ed 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.cpp +++ b/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.cpp @@ -13,35 +13,39 @@ using namespace xstudio::ui::canvas; using namespace xstudio::ui::viewport; -AnnotationsRenderer::AnnotationsRenderer() { +AnnotationsRenderer::AnnotationsRenderer(const xstudio::ui::canvas::Canvas &interaction_canvas) + : interaction_canvas_(interaction_canvas), annotations_visible_(true) { canvas_renderer_.reset(new ui::opengl::OpenGLCanvasRenderer()); } -void AnnotationsRenderer::render_opengl( +void AnnotationsRenderer::render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, const xstudio::media_reader::ImageBufPtr &frame, const bool have_alpha_buffer) { - utility::BlindDataObjectPtr render_data = - frame.plugin_blind_data2(AnnotationsTool::PLUGIN_UUID); - const auto *data = dynamic_cast(render_data.get()); - - if (!data) { - // annotation tool hasn't attached any render data to this image. - // This means annotations aren't visible. - return; + auto render_data = frame.plugin_blind_data(AnnotationsTool::PLUGIN_UUID); + if (render_data) { + const auto *data = dynamic_cast(render_data.get()); + if (data) { + handle_ = data->handle_; + current_edited_bookmark_uuid_ = data->current_edited_bookmark_uuid_; + laser_drawing_mode_ = data->laser_mode_; + frame_being_annotated_ = data->interaction_frame_key_; + annotations_visible_ = data->show_annotations_; + } } - - // if the uuid for the interaction canvas is null we always draw it because - // it is 'lazer' pen strokes - bool draw_interaction_canvas = data && data->current_edited_bookmark_uuid_.is_null(); + + if (!annotations_visible_) + return; // the xstudio playhead takes care of attaching bookmark data to the // ImageBufPtr that we receive here. The bookmark data may have annotations // data attached which we can draw to screen.... + bool draw_interaction_canvas = false; + for (const auto &anno : frame.bookmarks()) { // .. we don't draw the annotation attached to the frame if its bookmark @@ -49,7 +53,7 @@ void AnnotationsRenderer::render_opengl( // being edited. The reason is that the strokes and captions of this // annotation are already cloned into 'interaction_canvas_' which we // draw below. - if (anno->detail_.uuid_ == data->current_edited_bookmark_uuid_) { + if (anno->detail_.uuid_ == current_edited_bookmark_uuid_) { draw_interaction_canvas = true; continue; } @@ -59,31 +63,52 @@ void AnnotationsRenderer::render_opengl( if (my_annotation) { canvas_renderer_->render_canvas( my_annotation->canvas(), - data->handle_, + HandleState(), transform_window_to_viewport_space, transform_viewport_to_image_space, viewport_du_dpixel, - have_alpha_buffer); + have_alpha_buffer, + 1.f); } } - // xSTUDIO supports multiple viewports, each can show different media or - // different frames of the same media. If the user is drawing annotations in - // one viewport we may (or may not) want to draw those strokes in realtime - // in other viewports: - // - // When user is currently drawing, we store the 'key' for the frame they are - // drawing on. Current drawings are stored in data->interaction_canvas_ ... - // we only want to plot these if the frame we are rendering over here matches - // the key of the frame the user is being drawn on. - if (draw_interaction_canvas && - data->interaction_frame_key_ == to_string(frame.frame_id().key_)) { + if (draw_interaction_canvas || (current_edited_bookmark_uuid_.is_null() && + (frame_being_annotated_ == frame.frame_id().key()))) { canvas_renderer_->render_canvas( - data->interaction_canvas_, - data->handle_, + interaction_canvas_, + handle_, transform_window_to_viewport_space, transform_viewport_to_image_space, viewport_du_dpixel, - have_alpha_buffer); + have_alpha_buffer, + 1.f); } -} \ No newline at end of file + + // The canvas drawing routines use the depth buffer and clear it + // for this purpose, we need to restore it to what QtQuick renderer + // has set it to which I'm pretty sure is 1.0 + glClearDepth(1.0); + glClear(GL_DEPTH_BUFFER_BIT); +} + +void AnnotationsRenderer::render_viewport_overlay( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_normalised_coords, + const float viewport_du_dpixel, + const bool have_alpha_buffer) { + if (laser_drawing_mode_) { + canvas_renderer_->render_canvas( + interaction_canvas_, + handle_, + transform_window_to_viewport_space, + transform_viewport_to_normalised_coords, + viewport_du_dpixel, + have_alpha_buffer, + 1.f); + } + // The canvas drawing routines use the depth buffer and clear it + // for this purpose, we need to restore it to what QtQuick renderer + // has set it to which I'm pretty sure is 1.0 + glClearDepth(1.0); + glClear(GL_DEPTH_BUFFER_BIT); +} diff --git a/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.hpp b/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.hpp index 309667a9a..90def9bf6 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.hpp +++ b/src/plugin/viewport_overlay/annotations/src/annotation_opengl_renderer.hpp @@ -13,19 +13,42 @@ namespace ui { class AnnotationsRenderer : public plugin::ViewportOverlayRenderer { public: - AnnotationsRenderer(); + AnnotationsRenderer(const xstudio::ui::canvas::Canvas &interaction_canvas); - void render_opengl( + void render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, const xstudio::media_reader::ImageBufPtr &frame, const bool have_alpha_buffer) override; + void render_viewport_overlay( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_normalised_coords, + const float viewport_du_dpixel, + const bool have_alpha_buffer) override; + RenderPass preferred_render_pass() const override { return BeforeImage; } + void update( + const bool show_annotations, + const xstudio::ui::canvas::HandleState &h, + const utility::Uuid ¤t_edited_bookmark_uuid, + const media::MediaKey &frame_being_annotated, + bool laser_mode); + private: std::unique_ptr canvas_renderer_; + + // Canvas is thread safe + const xstudio::ui::canvas::Canvas &interaction_canvas_; + + xstudio::ui::canvas::HandleState handle_; + bool annotations_visible_; + bool laser_drawing_mode_; + utility::Uuid current_edited_bookmark_uuid_; + media_reader::ImageBufPtr image_being_annotated_; + media::MediaKey frame_being_annotated_; }; } // end namespace viewport diff --git a/src/plugin/viewport_overlay/annotations/src/annotation_render_data.hpp b/src/plugin/viewport_overlay/annotations/src/annotation_render_data.hpp index ef2c8448a..47030730b 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotation_render_data.hpp +++ b/src/plugin/viewport_overlay/annotations/src/annotation_render_data.hpp @@ -1,4 +1,3 @@ -// SPDX-License-Identifier: Apache-2.0 #pragma once #include "xstudio/utility/blind_data.hpp" @@ -10,21 +9,22 @@ namespace ui { class AnnotationRenderDataSet : public utility::BlindDataObject { public: AnnotationRenderDataSet( - const xstudio::ui::canvas::Canvas &interaction_canvas, + const bool show_annotations, + const xstudio::ui::canvas::HandleState &h, const utility::Uuid ¤t_edited_bookmark_uuid, - const xstudio::ui::canvas::HandleState handle, - const std::string &interaction_frame_key) - : interaction_canvas_(interaction_canvas), + const media::MediaKey &frame_being_annotated, + bool laser_mode) + : show_annotations_(show_annotations), + handle_(h), current_edited_bookmark_uuid_(current_edited_bookmark_uuid), - handle_(handle), - interaction_frame_key_(interaction_frame_key) {} - - // Canvas is thread safe - const xstudio::ui::canvas::Canvas &interaction_canvas_; + interaction_frame_key_(frame_being_annotated), + laser_mode_(laser_mode) {} + const bool show_annotations_; const utility::Uuid current_edited_bookmark_uuid_; const xstudio::ui::canvas::HandleState handle_; - const std::string interaction_frame_key_; + const media::MediaKey interaction_frame_key_; + const bool laser_mode_; }; } // namespace viewport diff --git a/src/plugin/viewport_overlay/annotations/src/annotation_serialiser.cpp b/src/plugin/viewport_overlay/annotations/src/annotation_serialiser.cpp index 7d399bca3..146963d0a 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotation_serialiser.cpp +++ b/src/plugin/viewport_overlay/annotations/src/annotation_serialiser.cpp @@ -6,7 +6,9 @@ using namespace xstudio; std::map> AnnotationSerialiser::serialisers; +namespace { static const std::string ANNO_VERSION_KEY("Annotation Serialiser Version"); +} utility::JsonStore AnnotationSerialiser::serialise(const Annotation *anno) { if (serialisers.empty()) { diff --git a/src/plugin/viewport_overlay/annotations/src/annotations_tool.cpp b/src/plugin/viewport_overlay/annotations/src/annotations_tool.cpp index 95634b2d1..d9f3ff492 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotations_tool.cpp +++ b/src/plugin/viewport_overlay/annotations/src/annotations_tool.cpp @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 +#include + #include "xstudio/plugin_manager/plugin_base.hpp" -#include "xstudio/media_reader/image_buffer.hpp" +#include "xstudio/media_reader/image_buffer_set.hpp" #include "xstudio/global_store/global_store.hpp" #include "xstudio/utility/blind_data.hpp" #include "xstudio/utility/helpers.hpp" @@ -17,6 +19,8 @@ using namespace xstudio::bookmark; using namespace xstudio::ui::canvas; using namespace xstudio::ui::viewport; +namespace fs = std::filesystem; + namespace { @@ -39,49 +43,21 @@ std::vector annotations_uuids(const std::vector &a return res; } -} // anonymous namespace - static int __a_idx = 0; +} // anonymous namespace + AnnotationsTool::AnnotationsTool( caf::actor_config &cfg, const utility::JsonStore &init_settings) : plugin::StandardPlugin(cfg, fmt::format("AnnotationsTool{}", __a_idx++), init_settings) { - module::QmlCodeAttribute *button = add_qml_code_attribute( - "MyCode", - R"( -import AnnotationsTool 1.0 -AnnotationsButton { - anchors.fill: parent -} -)"); - - // TODO: remove the use of viewport index - this became obsolete when the - // design was changes so there's only one instance of the plugin. - int viewport_index = 0; - - const std::string media_buttons_group = - viewport_index ? fmt::format("media_tools_buttons_{}", viewport_index) - : "media_tools_buttons"; - const std::string fonts_group = fmt::format("annotations_tool_fonts_{}", viewport_index); - const std::string tools_group = fmt::format("annotations_tool_settings_{}", viewport_index); - const std::string scribble_mode_group = - fmt::format("anno_scribble_mode_backend_{}", viewport_index); - const std::string tool_types_group = - fmt::format("annotations_tool_types_{}", viewport_index); - const std::string draw_mode_group = - fmt::format("annotations_tool_draw_mode_{}", viewport_index); - - button->expose_in_ui_attrs_group(media_buttons_group); - button->set_role_data(module::Attribute::ToolbarPosition, 1000.0); - const auto &fonts_ = Fonts::available_fonts(); font_choice_ = add_string_choice_attribute( "font_choices", "font_choices", fonts_.size() ? fonts_.begin()->first : std::string(""), utility::map_key_to_vec(fonts_)); - font_choice_->expose_in_ui_attrs_group(fonts_group); + font_choice_->expose_in_ui_attrs_group("annotations_tool_fonts"); draw_pen_size_ = add_integer_attribute("Draw Pen Size", "Draw Pen Size", 10, 1, 300); @@ -104,14 +80,14 @@ AnnotationsButton { text_bgr_opacity_ = add_integer_attribute( "Text Background Opacity", "Text Background Opacity", 100, 0, 100); - draw_pen_size_->expose_in_ui_attrs_group(tools_group); - shapes_pen_size_->expose_in_ui_attrs_group(tools_group); - erase_pen_size_->expose_in_ui_attrs_group(tools_group); - pen_colour_->expose_in_ui_attrs_group(tools_group); - pen_opacity_->expose_in_ui_attrs_group(tools_group); - text_size_->expose_in_ui_attrs_group(tools_group); - text_bgr_colour_->expose_in_ui_attrs_group(tools_group); - text_bgr_opacity_->expose_in_ui_attrs_group(tools_group); + draw_pen_size_->expose_in_ui_attrs_group("annotations_tool_settings"); + shapes_pen_size_->expose_in_ui_attrs_group("annotations_tool_settings"); + erase_pen_size_->expose_in_ui_attrs_group("annotations_tool_settings"); + pen_colour_->expose_in_ui_attrs_group("annotations_tool_settings"); + pen_opacity_->expose_in_ui_attrs_group("annotations_tool_settings"); + text_size_->expose_in_ui_attrs_group("annotations_tool_settings"); + text_bgr_colour_->expose_in_ui_attrs_group("annotations_tool_settings"); + text_bgr_opacity_->expose_in_ui_attrs_group("annotations_tool_settings"); draw_pen_size_->set_preference_path("/plugin/annotations/draw_pen_size"); shapes_pen_size_->set_preference_path("/plugin/annotations/shapes_pen_size"); @@ -129,45 +105,16 @@ AnnotationsButton { active_tool_ = add_string_choice_attribute( - "Active Tool", - "Active Tool", - utility::map_value_to_vec(tool_names_).front(), - utility::map_value_to_vec(tool_names_)); - - active_tool_->expose_in_ui_attrs_group(tools_group); - active_tool_->expose_in_ui_attrs_group(tool_types_group); - - shape_tool_ = add_integer_attribute("Shape Tool", "Shape Tool", 0, 0, 2); - shape_tool_->expose_in_ui_attrs_group(tools_group); - shape_tool_->set_preference_path("/plugin/annotations/shape_tool"); - - draw_mode_ = add_string_choice_attribute( - "Draw Mode", - "Draw Mode", - utility::map_value_to_vec(draw_mode_names_).front(), - utility::map_value_to_vec(draw_mode_names_)); - draw_mode_->expose_in_ui_attrs_group(scribble_mode_group); - draw_mode_->set_preference_path("/plugin/annotations/draw_mode"); - draw_mode_->set_role_data( - module::Attribute::StringChoicesEnabled, std::vector({true, true, false})); - - tool_is_active_ = - add_boolean_attribute("annotations_tool_active", "annotations_tool_active", false); - - tool_is_active_->expose_in_ui_attrs_group(tools_group); - tool_is_active_->set_role_data( - module::Attribute::MenuPaths, - std::vector({"panels_main_menu_items|Draw Tools"})); + "Active Tool", "Active Tool", "None", utility::map_value_to_vec(tool_names_)); + active_tool_->expose_in_ui_attrs_group("annotations_tool_settings"); + active_tool_->expose_in_ui_attrs_group("annotations_tool_types"); action_attribute_ = add_string_attribute("action_attribute", "action_attribute", ""); - action_attribute_->expose_in_ui_attrs_group(tools_group); + action_attribute_->expose_in_ui_attrs_group("annotations_tool_settings"); display_mode_attribute_ = add_string_choice_attribute( - "Display Mode", - "Disp. Mode", - "With Drawing Tools", - {"Only When Paused", "Always", "With Drawing Tools"}); - display_mode_attribute_->expose_in_ui_attrs_group(draw_mode_group); + "Display Mode", "Disp. Mode", "With Drawing Tools", {"Only When Paused", "Always"}); + display_mode_attribute_->expose_in_ui_attrs_group("annotations_tool_draw_mode"); display_mode_attribute_->set_preference_path("/plugin/annotations/display_mode"); // this attr is used to implement the blinking cursor for caption edit mode @@ -176,13 +123,64 @@ AnnotationsButton { moving_scaling_text_attr_ = add_integer_attribute("moving_scaling_text", "moving_scaling_text", 0); - moving_scaling_text_attr_->expose_in_ui_attrs_group(tools_group); + moving_scaling_text_attr_->expose_in_ui_attrs_group("annotations_tool_settings"); // setting the active tool to -1 disables drawing via 'attribute_changed' attribute_changed(active_tool_->uuid(), module::Attribute::Value); make_behavior(); + connect_to_ui(); + listen_to_playhead_events(true); + + // here's the code for the 'reskin' UI (xSTUDIO v2) where we declare the + // drawing tools panel creation code. + + register_ui_panel_qml( + "Drawing Tools", + R"( + import AnnotationsTool 2.0 + + import QtQuick + Rectangle { + anchors.fill: parent + XsDrawingTools { + anchors.top: parent.top + anchors.topMargin: 20 + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + width: 140 + } + gradient: Gradient { + GradientStop { position: 0.0; color: "#5C5C5C" } + GradientStop { position: 1.0; color: "#474747" } + } + } + )", + 7.0f); + + dockable_widget_attr_ = register_viewport_dockable_widget( + "Annotate", + "qrc:/icons/stylus_note.svg", // icon for the button to activate the tool + "Show/Hide Annotation Toolbox", // tooltip for the button, + 3.0f, // button position in the buttons bar + true, + // qml code to create the left/right dockable widget + R"( + import AnnotationsTool 2.0 + import QtQuick + XsDrawingToolsLR { + } + )", + // qml code to create the top/bottom dockable widget (left empty here as we don't have + // one) + R"( + import AnnotationsTool 2.0 + import QtQuick + XsDrawingToolsTB { + } + )", + toggle_active_hotkey_); } AnnotationsTool::~AnnotationsTool() {} @@ -196,55 +194,62 @@ caf::message_handler AnnotationsTool::message_handler_extensions() { // this message is sent when the user finishes a laser bruish stroke if (!fade_looping_) { fade_looping_ = true; - anon_send(this, utility::event_atom_v); + anon_mail(utility::event_atom_v).send(this); } }, [=](utility::event_atom) { // note Annotation::fade_all_strokes() returns false when all strokes have vanished if (is_laser_mode() && interaction_canvas_.fade_all_strokes(pen_opacity_->value() / 100.f)) { - delayed_anon_send(this, std::chrono::milliseconds(25), utility::event_atom_v); + anon_mail(utility::event_atom_v) + .delay(std::chrono::milliseconds(25)) + .send(this); } else { fade_looping_ = false; } - redraw_viewport(); + do_redraw(); }); } -void AnnotationsTool::attribute_changed( - const utility::Uuid &attribute_uuid, const int /*role*/) { +void AnnotationsTool::attribute_changed(const utility::Uuid &attribute_uuid, const int role) { const std::string active_tool = active_tool_->value(); - if (attribute_uuid == tool_is_active_->uuid()) { + if (attribute_uuid == active_tool_->uuid()) { + + if (active_tool == "None") { - if (tool_is_active_->value()) { - if (active_tool == "None") - active_tool_->set_value("Draw"); - grab_mouse_focus(); - } else { release_mouse_focus(); release_keyboard_focus(); end_drawing(); clear_caption_handle(); - } + set_viewport_cursor(""); - } else if (attribute_uuid == active_tool_->uuid()) { - - if (tool_is_active_->value()) { + } else { - if (active_tool == "None") { - release_mouse_focus(); + grab_mouse_focus(); + if (active_tool != "Text") { + set_viewport_cursor("Qt.CrossCursor"); + end_drawing(); + release_keyboard_focus(); + clear_caption_handle(); } else { - grab_mouse_focus(); + set_viewport_cursor("Qt.IBeamCursor"); } - if (active_tool == "Text") { - } else { - end_drawing(); - release_keyboard_focus(); + if (active_tool == "Laser") { + + // switching INTO laser draw mode ... save any annotation to the + // bookmark if required + update_bookmark_annotation_data(); + interaction_canvas_.clear(true); clear_caption_handle(); + current_bookmark_uuid_ = utility::Uuid(); + } else if (last_tool_ == "Laser") { + interaction_canvas_.clear(true); } + + last_tool_ = active_tool; } } else if ( @@ -265,8 +270,6 @@ void AnnotationsTool::attribute_changed( display_mode_ = OnlyWhenPaused; } else if (display_mode_attribute_->value() == "Always") { display_mode_ = Always; - } else if (display_mode_attribute_->value() == "With Drawing Tools") { - display_mode_ = WithDrawTool; } } else if (attribute_uuid == text_cursor_blink_attr_->uuid()) { @@ -276,13 +279,13 @@ void AnnotationsTool::attribute_changed( if (interaction_canvas_.has_selected_caption()) { // send a delayed message to ourselves to continue the blinking - delayed_anon_send( - caf::actor_cast(this), - std::chrono::milliseconds(500), + anon_mail( module::change_attribute_value_atom_v, attribute_uuid, utility::JsonStore(!text_cursor_blink_attr_->value()), - true); + true) + .delay(std::chrono::milliseconds(500)) + .send(this); } } else if (attribute_uuid == pen_colour_->uuid()) { @@ -326,20 +329,8 @@ void AnnotationsTool::attribute_changed( } } - if (attribute_uuid == active_tool_->uuid() || attribute_uuid == draw_mode_->uuid()) { - if (active_tool_->value() == "Draw" && draw_mode_->value() == "Laser") { - - // switching INTO laser draw mode ... save any annotation to the - // bookmark if required - update_bookmark_annotation_data(); - interaction_canvas_.clear(true); - clear_caption_handle(); - current_bookmark_uuid_ = utility::Uuid(); - } - } - - redraw_viewport(); + do_redraw(); } void AnnotationsTool::update_attrs_from_preferences(const utility::JsonStore &j) { @@ -357,37 +348,57 @@ void AnnotationsTool::register_hotkeys() { int('D'), ui::NoModifier, "Toggle Annotations Tool", - "Show or hide the annotations toolbox. You can start drawing annotations immediately " - "whenever the toolbox is visible."); + "Show or hide the Annotate toolbox. You can start drawing annotations immediately " + "whenever the toolbox is visible.", + false, + "Drawing Tools"); undo_hotkey_ = register_hotkey( int('Z'), ui::ControlModifier, "Undo (Annotation edit)", - "Undoes your last edits to an annotation"); + "Undoes your last edits to an annotation", + false, + "Drawing Tools"); redo_hotkey_ = register_hotkey( int('Z'), ui::ControlModifier | ui::ShiftModifier, "Redo (Annotation edit)", - "Redoes your last undone edit on an annotation"); + "Redoes your last undone edit on an annotation", + false, + "Drawing Tools"); + + clear_hotkey_ = register_hotkey( + int('X'), + ui::ShiftModifier, + "Clear all strokes", + "Clears the entire current drawing. If there is no text in the assicated note it will " + "also be removed.", + false, + "Drawing Tools"); } void AnnotationsTool::hotkey_pressed( - const utility::Uuid &hotkey_uuid, const std::string & /*context*/) { + const utility::Uuid &hotkey_uuid, + const std::string & /*context*/, + const std::string & /*window*/) { if (hotkey_uuid == toggle_active_hotkey_) { - tool_is_active_->set_value(!tool_is_active_->value()); + // tool_is_active_->set_value(!tool_is_active_->value()); - } else if (hotkey_uuid == undo_hotkey_ && tool_is_active_->value()) { + } else if (hotkey_uuid == undo_hotkey_ && active_tool_->value() != "None") { undo(); - redraw_viewport(); + do_redraw(); - } else if (hotkey_uuid == redo_hotkey_ && tool_is_active_->value()) { + } else if (hotkey_uuid == redo_hotkey_ && active_tool_->value() != "None") { redo(); - redraw_viewport(); + do_redraw(); + } else if (hotkey_uuid == clear_hotkey_ && active_tool_->value() != "None") { + + clear_onscreen_annotations(); } } @@ -396,68 +407,79 @@ void AnnotationsTool::hotkey_released( bool AnnotationsTool::pointer_event(const ui::PointerEvent &e) { - if (!tool_is_active_->value()) + if (active_tool_->value() == "None") return false; bool redraw = true; const Imath::V2f pointer_pos = e.position_in_viewport_coord_sys(); - if (active_tool_->value() == "Draw" || active_tool_->value() == "Erase") { - if (e.type() == ui::Signature::EventType::ButtonDown && + if (active_tool_->value() == "Draw" || active_tool_->value() == "Erase" || + is_laser_mode()) { + if (e.type() == ui::EventType::ButtonDown && e.buttons() == ui::Signature::Button::Left) { - start_editing(e.context()); - start_stroke(pointer_pos); + start_editing(e.context(), pointer_pos); + start_stroke(image_transformed_ptr_pos(pointer_pos)); } else if ( - e.type() == ui::Signature::EventType::Drag && - e.buttons() == ui::Signature::Button::Left) { - update_stroke(pointer_pos); - } else if (e.type() == ui::Signature::EventType::ButtonRelease) { + e.type() == ui::EventType::Drag && e.buttons() == ui::Signature::Button::Left) { + update_stroke(image_transformed_ptr_pos(pointer_pos)); + } else if (e.type() == ui::EventType::ButtonRelease) { end_drawing(); } - } else if (active_tool_->value() == "Shapes") { - if (e.type() == ui::Signature::EventType::ButtonDown && + } else if ( + active_tool_->value() == "Square" || active_tool_->value() == "Circle" || + active_tool_->value() == "Arrow" || active_tool_->value() == "Line") { + if (e.type() == ui::EventType::ButtonDown && e.buttons() == ui::Signature::Button::Left) { - start_editing(e.context()); - start_shape(pointer_pos); + start_editing(e.context(), pointer_pos); + start_shape(image_transformed_ptr_pos(pointer_pos)); } else if ( - e.type() == ui::Signature::EventType::Drag && - e.buttons() == ui::Signature::Button::Left) { - update_shape(pointer_pos); - } else if (e.type() == ui::Signature::EventType::ButtonRelease) { + e.type() == ui::EventType::Drag && e.buttons() == ui::Signature::Button::Left) { + update_shape(image_transformed_ptr_pos(pointer_pos)); + } else if (e.type() == ui::EventType::ButtonRelease) { end_drawing(); } } else if (active_tool_->value() == "Text") { - if (e.type() == ui::Signature::EventType::ButtonDown && + if (e.type() == ui::EventType::ButtonDown && e.buttons() == ui::Signature::Button::Left) { - start_editing(e.context()); - start_or_edit_caption(pointer_pos, e.viewport_pixel_scale()); + start_editing(e.context(), pointer_pos); + start_or_edit_caption( + image_transformed_ptr_pos(pointer_pos), e.viewport_pixel_scale()); grab_mouse_focus(); } else if ( - e.type() == ui::Signature::EventType::Drag && - e.buttons() == ui::Signature::Button::Left) { - start_editing(e.context()); - update_caption_action(pointer_pos); + e.type() == ui::EventType::Drag && e.buttons() == ui::Signature::Button::Left) { + start_editing(e.context(), pointer_pos); + update_caption_action(image_transformed_ptr_pos(pointer_pos)); update_caption_handle(); } else if (e.buttons() == ui::Signature::Button::None) { - redraw = update_caption_hovered(pointer_pos, e.viewport_pixel_scale()); + redraw = update_caption_hovered( + image_transformed_ptr_pos(pointer_pos), e.viewport_pixel_scale()); } } else { redraw = false; } if (redraw) - redraw_viewport(); + do_redraw(); return false; } +Imath::V2f AnnotationsTool::image_transformed_ptr_pos(const Imath::V2f &p) const { + if (image_being_annotated_ && !is_laser_mode()) { + Imath::V4f pt(p.x, p.y, 0.0f, 1.0f); + pt *= image_being_annotated_.layout_transform().inverse(); + return Imath::V2f(pt.x / pt.w, pt.y / pt.w); + } + return p; +} + void AnnotationsTool::text_entered(const std::string &text, const std::string &context) { if (active_tool_->value() == "Text") { interaction_canvas_.update_caption_text(text); update_caption_handle(); - redraw_viewport(); + do_redraw(); } } @@ -471,51 +493,33 @@ void AnnotationsTool::key_pressed( } interaction_canvas_.move_caption_cursor(key); update_caption_handle(); - redraw_viewport(); + do_redraw(); } } utility::BlindDataObjectPtr AnnotationsTool::onscreen_render_data( - const media_reader::ImageBufPtr &image, const std::string &viewport_name) const { - - // Rendering the viewport (including viewport overlays) occurs in - // a separate thread to the one that instances of this class live in. - // - // xSTUDIO calls this function (in our thread) so we can attach any and all - // data we want to an image using a 'BlindDataObjectPtr'. We subclass - // BlindDataObject with AnnotationRenderDataSet allowing us to bung whatever - // draw time data we want and need. This is then later available in the - // rendering thread in a thread safe manner (as long as we do it right here - // and don't pass in pointers to member data of AnnotationsTool - with the - // exception of the Canvas class which has been made thread safe) - - if (!((display_mode_ == Always) || - (display_mode_ == WithDrawTool && tool_is_active_->value()) || - (display_mode_ == OnlyWhenPaused && !playhead_is_playing_))) { - // don't draw annotations, return empty data - return utility::BlindDataObjectPtr(); - } + const media_reader::ImageBufPtr &image, + const std::string & /*viewport_name*/, + const utility::Uuid &/*playhead_uuid*/, + const bool is_hero_image) const +{ - std::string onscreen_interaction_frame; - auto p = viewport_current_images_.find(current_interaction_viewport_name_); - if (p != viewport_current_images_.end() && p->second.size()) { - onscreen_interaction_frame = to_string(p->second.front().frame_id().key_); - } + bool show_annotations = + (display_mode_ == Always) || (display_mode_ == OnlyWhenPaused && !playhead_is_playing_); - // As noted elsewhere, interaction_canvas_ (class = Canvas) is read/write - // thread safe so we take a reference to it ready for render time. - auto immediate_render_data = new AnnotationRenderDataSet( - interaction_canvas_, // note a reference is taken here - current_bookmark_uuid_, + auto data = new AnnotationRenderDataSet( + show_annotations, handle_state_, - onscreen_interaction_frame); + current_bookmark_uuid_, + image.frame_id().key(), + is_laser_mode() + ); + return utility::BlindDataObjectPtr(data); - return utility::BlindDataObjectPtr( - static_cast(immediate_render_data)); } void AnnotationsTool::images_going_on_screen( - const std::vector &images, + const media_reader::ImageBufDisplaySetPtr &images, const std::string viewport_name, const bool playhead_playing) { @@ -528,47 +532,57 @@ void AnnotationsTool::images_going_on_screen( playhead_is_playing_ = playhead_playing; + // It's useful to keep a hold of the images that are on-screen so if the // user starts drawing when there is a bookmark on screen then we can // add the strokes to that existing bookmark instead of making a brand // new note - if (!playhead_playing) - viewport_current_images_[viewport_name] = images; - else - viewport_current_images_[viewport_name].clear(); + viewport_current_images_[viewport_name] = images; if (!interaction_canvas_.empty() && !current_bookmark_uuid_.is_null() && current_interaction_viewport_name_ == viewport_name) { bool edited_anno_is_onscreen = false; - // looks like we are editing an annotation. Is the annotation - for (auto &image : images) { - for (auto &bookmark : image.bookmarks()) { - - auto anno = dynamic_cast(bookmark->annotation_.get()); - if (bookmark->detail_.uuid_ == current_bookmark_uuid_) { - edited_anno_is_onscreen = true; + // looks like we are editing an annotation. Is the annotation still on + // screen (i.e. has the user scrubbed away from it) + if (images && images->layout_data()) { + const auto &im_idx = images->layout_data()->image_draw_order_hint_; + for (auto &idx : im_idx) { + // loop over onscreen images. Check if the current bookmark is + // visible on any of them. + const auto &cim = images->onscreen_image(idx); + for (auto &bookmark : cim.bookmarks()) { + + auto anno = dynamic_cast(bookmark->annotation_.get()); + if (bookmark->detail_.uuid_ == current_bookmark_uuid_) { + edited_anno_is_onscreen = true; + break; + } } } } + if (!edited_anno_is_onscreen) { // the annotation that we were editing is no longer on-screen. The // user must have scrubbed away from it in the timeline. Thus we // push it to the bookmark and clear update_bookmark_annotation_data(); + current_bookmark_uuid_ = utility::Uuid(); interaction_canvas_.clear(true); clear_caption_handle(); - current_bookmark_uuid_ = utility::Uuid(); // calling these updates the renderes with the now cleared // interaction canvas data } } + } plugin::ViewportOverlayRendererPtr -AnnotationsTool::make_overlay_renderer(const int /*viewer_index*/) { - return plugin::ViewportOverlayRendererPtr(new AnnotationsRenderer()); +AnnotationsTool::make_overlay_renderer(const std::string &viewport_name) { + + return plugin::ViewportOverlayRendererPtr(new AnnotationsRenderer(interaction_canvas_)); + } AnnotationBasePtr AnnotationsTool::build_annotation(const utility::JsonStore &anno_data) { @@ -576,32 +590,138 @@ AnnotationBasePtr AnnotationsTool::build_annotation(const utility::JsonStore &an static_cast(new Annotation(anno_data))); } -bool AnnotationsTool::is_laser_mode() const { - return active_tool_->value() == "Draw" && draw_mode_->value() == "Laser"; +bool AnnotationsTool::is_laser_mode() const { return active_tool_->value() == "Laser"; } + +void AnnotationsTool::viewport_dockable_widget_activated(std::string &widget_name) { + + if (widget_name == "Annotate") { + active_tool_->set_value(last_tool_); + } } -void AnnotationsTool::start_editing(const std::string &viewport_name) { +void AnnotationsTool::viewport_dockable_widget_deactivated(std::string &widget_name) { - if (is_laser_mode()) - return; + if (widget_name == "Annotate") { + active_tool_->set_value("None"); + } +} + +void AnnotationsTool::turn_off_overlay_interaction() { active_tool_->set_value("None"); } + +void AnnotationsTool::start_editing( + const std::string &viewport_name, const Imath::V2f &pointer_position) { + + // ensure playback is stopped + start_stop_playback(viewport_name, false); + + // if the viewport is in grid mode, with multiple images laid out, which one + // was clicked in ? + auto current_edited_bookmark_id = current_bookmark_uuid_; + auto before = image_being_annotated_; + media_reader::ImageBufPtr new_image_to_annotate; + auto p = viewport_current_images_.find(viewport_name); + if (p != viewport_current_images_.end() && p->second) { + + // first, check if pointer_position lands on one of the images in + // the viewport + bool curr_im_is_onscreen = false; + const media_reader::ImageBufDisplaySetPtr &onscreen_image_set = p->second; + const auto &im_idx = onscreen_image_set->layout_data()->image_draw_order_hint_; + for (auto &idx : im_idx) { + // loop over onscreen images. translate pointer position to image + // space coords + const auto &cim = onscreen_image_set->onscreen_image(idx); + + if (cim) { + + Imath::V4f pt(pointer_position.x, pointer_position.y, 0.0f, 1.0f); + pt *= cim.layout_transform().inverse(); + + // does the pointer land on the image? + float a = 1.0f / image_aspect(cim); + if (pt.x / pt.w >= -1.0f && pt.x / pt.w <= 1.0f && pt.y / pt.w >= -a && + pt.y / pt.w <= a) { + new_image_to_annotate = cim; + } + // check if image_being_annotated_ (from last time we entered this + // method) is in the onscreen set + if (image_being_annotated_ == cim) + curr_im_is_onscreen = true; + } + } + + if (new_image_to_annotate) { + image_being_annotated_ = new_image_to_annotate; + } else if (!curr_im_is_onscreen) { + image_being_annotated_ = onscreen_image_set->hero_image(); + } else { + // image_being_annotated_ is unchanged, as it belongs in the + // onscreen iamge set but we just haven't clicked inside any + // other images + } + } else { + image_being_annotated_ = media_reader::ImageBufPtr(); + } + + if (image_being_annotated_ != before) { + // image has changed since last time we modified an annotation ... we + // need to run our logic to decide which annotation (if there is an + // existing one on this frame) the start editing + current_bookmark_uuid_ = utility::Uuid(); + } if (!current_bookmark_uuid_.is_null() && current_interaction_viewport_name_ == viewport_name) { + // bookmark id is has not changed, neither has the viewport that + // the interaction is happening in. return; } current_interaction_viewport_name_ = viewport_name; - // Is there an annotation on screen that we should start appending to? - Annotation *to_edit = nullptr; - current_bookmark_uuid_ = utility::Uuid(); - utility::Uuid first_bookmark_uuid; - auto p = viewport_current_images_.find(viewport_name); - if (p != viewport_current_images_.end()) { - for (auto &image : p->second) { - for (auto &bookmark : image.bookmarks()) { + + if (is_laser_mode()) { + + // laser mode, no 'current' annotation + current_bookmark_uuid_ = utility::Uuid(); + clear_caption_handle(); + image_being_annotated_ = media_reader::ImageBufPtr(); + + } else { + + // The logic here will 'pick' an annotation to start modifying + // if there is an annotation on the current image. + // If we were already modifying an annotation and that same annotation + // is attached to the new image then we will stick with that annotation + Annotation *to_edit = nullptr; + current_bookmark_uuid_ = utility::Uuid(); + utility::Uuid first_bookmark_uuid; + if (image_being_annotated_) { + + // first, we check if the last bookmark that was being annotated is + // already attached to this image ... + for (auto &bookmark : image_being_annotated_.bookmarks()) { auto anno = dynamic_cast(bookmark->annotation_.get()); if (anno) { + if (bookmark->detail_.uuid_ == current_edited_bookmark_id) { + current_bookmark_uuid_ = current_edited_bookmark_id; + // this on-screen image is carrying the same bookmark + // as the one we were already modifying, so we can now + // exit + return; + } + } + } + + + // if we're here, we need to pick a new bookmark to start adding + // annotations to + for (auto &bookmark : image_being_annotated_.bookmarks()) { + + auto anno = dynamic_cast(bookmark->annotation_.get()); + if (anno) { + // we've found a bookmark with an annotation - pick this and + // exit loop to_edit = anno; current_bookmark_uuid_ = bookmark->detail_.uuid_; break; @@ -612,36 +732,35 @@ void AnnotationsTool::start_editing(const std::string &viewport_name) { first_bookmark_uuid = bookmark->detail_.uuid_; } } - if (to_edit) - break; } - } - clear_caption_handle(); + if (active_tool_->value() != "Text") + clear_caption_handle(); - // clone the whole annotation into our 'interaction_canvas_' - if (to_edit) - interaction_canvas_ = to_edit->canvas(); - else { - // there is a bookmark which doesn't have annotations (yet). We will - // add annotations to this bookmark - interaction_canvas_.clear(true); - current_bookmark_uuid_ = first_bookmark_uuid; + // clone the whole annotation into our 'interaction_canvas_' + if (to_edit) { + interaction_canvas_ = to_edit->canvas(); + } else { + // there is a bookmark which doesn't have annotations (yet). We will + // add annotations to this bookmark + interaction_canvas_.clear(true); + current_bookmark_uuid_ = first_bookmark_uuid; + } } } void AnnotationsTool::start_stroke(const Imath::V2f &point) { - if (active_tool_->value() == "Draw") { + if (active_tool_->value() == "Erase") { + interaction_canvas_.start_erase_stroke( + erase_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE); + } else { interaction_canvas_.start_stroke( pen_colour_->value(), draw_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE, 0.0f, pen_opacity_->value() / 100.0); - } else if (active_tool_->value() == "Erase") { - interaction_canvas_.start_erase_stroke( - erase_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE); } update_stroke(point); @@ -656,28 +775,28 @@ void AnnotationsTool::start_shape(const Imath::V2f &p) { shape_anchor_ = p; - if (shape_tool_->value() == Square) { + if (active_tool_->value() == "Square") { interaction_canvas_.start_square( pen_colour_->value(), shapes_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE, pen_opacity_->value() / 100.0f); - } else if (shape_tool_->value() == Circle) { + } else if (active_tool_->value() == "Circle") { interaction_canvas_.start_circle( pen_colour_->value(), shapes_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE, pen_opacity_->value() / 100.0f); - } else if (shape_tool_->value() == Arrow) { + } else if (active_tool_->value() == "Arrow") { interaction_canvas_.start_arrow( pen_colour_->value(), shapes_pen_size_->value() / PEN_STROKE_THICKNESS_SCALE, pen_opacity_->value() / 100.0f); - } else if (shape_tool_->value() == Line) { + } else if (active_tool_->value() == "Line") { interaction_canvas_.start_line( pen_colour_->value(), @@ -690,20 +809,20 @@ void AnnotationsTool::start_shape(const Imath::V2f &p) { void AnnotationsTool::update_shape(const Imath::V2f &pointer_pos) { - if (shape_tool_->value() == Square) { + if (active_tool_->value() == "Square") { interaction_canvas_.update_square(shape_anchor_, pointer_pos); - } else if (shape_tool_->value() == Circle) { + } else if (active_tool_->value() == "Circle") { interaction_canvas_.update_circle( shape_anchor_, (shape_anchor_ - pointer_pos).length()); - } else if (shape_tool_->value() == Arrow) { + } else if (active_tool_->value() == "Arrow") { interaction_canvas_.update_arrow(shape_anchor_, pointer_pos); - } else if (shape_tool_->value() == Line) { + } else if (active_tool_->value() == "Line") { interaction_canvas_.update_line(shape_anchor_, pointer_pos); } @@ -798,15 +917,13 @@ bool AnnotationsTool::update_caption_hovered( const HandleState old_state = handle_state_; - auto &canvas = interaction_canvas_; - - if (canvas.has_selected_caption()) { - handle_state_.current_caption_bdb = canvas.caption_bounding_box(); - handle_state_.hover_state = canvas.hover_selected_caption_handle( + if (interaction_canvas_.has_selected_caption()) { + handle_state_.current_caption_bdb = interaction_canvas_.caption_bounding_box(); + handle_state_.hover_state = interaction_canvas_.hover_selected_caption_handle( pointer_pos, handle_state_.handle_size, viewport_pixel_scale); } handle_state_.under_mouse_caption_bdb = - canvas.hover_caption_bounding_box(pointer_pos, viewport_pixel_scale); + interaction_canvas_.hover_caption_bounding_box(pointer_pos, viewport_pixel_scale); if (handle_state_ != old_state) { moving_scaling_text_attr_->set_value(int(handle_state_.hover_state)); } @@ -827,7 +944,7 @@ void AnnotationsTool::update_caption_handle() { } if (handle_state_ != old_state) { - redraw_viewport(); + do_redraw(); } } @@ -844,10 +961,9 @@ void AnnotationsTool::end_drawing() { if (is_laser_mode()) { // start up the laser fade timer loop - see the event handler // in the constructor here to see how this works - anon_send(caf::actor_cast(this), utility::event_atom_v, true); + anon_mail(utility::event_atom_v, true).send(caf::actor_cast(this)); } else { - update_bookmark_annotation_data(); } } @@ -882,11 +998,21 @@ void AnnotationsTool::update_bookmark_annotation_data() { // there is no bookmark, meaning the user started annotating a frame // with no bookmark. Here the base class creates a new bookmark on the // current frame for us - current_bookmark_uuid_ = StandardPlugin::create_bookmark_on_current_media( - current_interaction_viewport_name_, - "Annotated Frame", - bookmark::BookmarkDetail(), - false); + + std::string note_name = "Annotated Frame"; + if (image_being_annotated_) { + const media::AVFrameID &id = image_being_annotated_.frame_id(); + note_name = fs::path(utility::uri_to_posix_path(id.uri())).stem().string(); + if (note_name.find(".") != std::string::npos) { + note_name = std::string(note_name, 0, note_name.find(".")); + } + current_bookmark_uuid_ = StandardPlugin::create_bookmark_on_frame( + image_being_annotated_.frame_id(), + note_name, + bookmark::BookmarkDetail(), + false); + } + if (!current_bookmark_uuid_.is_null()) update_bookmark_annotation_data(); } @@ -898,7 +1024,7 @@ void AnnotationsTool::undo() { start_editing(current_interaction_viewport_name_); interaction_canvas_.undo(); update_bookmark_annotation_data(); - redraw_viewport(); + do_redraw(); } void AnnotationsTool::redo() { @@ -906,7 +1032,7 @@ void AnnotationsTool::redo() { start_editing(current_interaction_viewport_name_); interaction_canvas_.redo(); update_bookmark_annotation_data(); - redraw_viewport(); + do_redraw(); } @@ -914,7 +1040,7 @@ void AnnotationsTool::clear_onscreen_annotations() { clear_edited_annotation(); void AnnotationsTool::restore_onscreen_annotations() { // TODO: reinstate this behaviour - redraw_viewport(); + do_redraw(); } void AnnotationsTool::clear_edited_annotation() { @@ -923,9 +1049,11 @@ void AnnotationsTool::clear_edited_annotation() { start_editing(current_interaction_viewport_name_); interaction_canvas_.clear(); update_bookmark_annotation_data(); - redraw_viewport(); + do_redraw(); } +void AnnotationsTool::do_redraw() { redraw_viewport(); } + extern "C" { plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { return new plugin_manager::PluginFactoryCollection( diff --git a/src/plugin/viewport_overlay/annotations/src/annotations_tool.hpp b/src/plugin/viewport_overlay/annotations/src/annotations_tool.hpp index 6d34cf1a9..270acc0b8 100644 --- a/src/plugin/viewport_overlay/annotations/src/annotations_tool.hpp +++ b/src/plugin/viewport_overlay/annotations/src/annotations_tool.hpp @@ -27,7 +27,10 @@ namespace ui { void update_attrs_from_preferences(const utility::JsonStore &) override; void register_hotkeys() override; - void hotkey_pressed(const utility::Uuid &uuid, const std::string &context) override; + void hotkey_pressed( + const utility::Uuid &uuid, + const std::string &context, + const std::string &window) override; void hotkey_released(const utility::Uuid &uuid, const std::string &context) override; bool pointer_event(const ui::PointerEvent &e) override; @@ -35,25 +38,34 @@ namespace ui { void key_pressed( const int key, const std::string &context, const bool auto_repeat) override; - utility::BlindDataObjectPtr onscreen_render_data( - const media_reader::ImageBufPtr &, - const std::string &viewport_name) const override; - plugin::ViewportOverlayRendererPtr - make_overlay_renderer(const int viewer_index) override; + make_overlay_renderer(const std::string &viewport_name) override; bookmark::AnnotationBasePtr build_annotation(const utility::JsonStore &anno_data) override; void images_going_on_screen( - const std::vector &images, + const media_reader::ImageBufDisplaySetPtr &images, const std::string viewport_name, const bool playhead_playing) override; + utility::BlindDataObjectPtr onscreen_render_data( + const media_reader::ImageBufPtr &, const std::string & /*viewport_name*/, + const utility::Uuid &/*playhead_uuid*/, + const bool is_hero_image) const override; + + void viewport_dockable_widget_activated(std::string &widget_name) override; + + void viewport_dockable_widget_deactivated(std::string &widget_name) override; + + void turn_off_overlay_interaction() override; + private: bool is_laser_mode() const; - void start_editing(const std::string &viewport_name); + void start_editing( + const std::string &viewport_name, + const Imath::V2f &pointer_position = Imath::V2f(-1e6f, -1e6f)); void start_stroke(const Imath::V2f &point); void update_stroke(const Imath::V2f &point); @@ -78,18 +90,22 @@ namespace ui { void restore_onscreen_annotations(); void clear_edited_annotation(); void update_bookmark_annotation_data(); + Imath::V2f image_transformed_ptr_pos(const Imath::V2f &p) const; + void do_redraw(); private: - enum Tool { Draw, Shapes, Text, Erase, None }; - enum ShapeTool { Square, Circle, Arrow, Line }; - enum DisplayMode { OnlyWhenPaused, Always, WithDrawTool }; - enum ScribbleTool { Sketch, Laser, Onion }; + enum Tool { Draw, Laser, Square, Circle, Arrow, Line, Text, Erase, None }; + enum DisplayMode { OnlyWhenPaused, Always }; const std::map tool_names_ = { - {Draw, "Draw"}, {Shapes, "Shapes"}, {Text, "Text"}, {Erase, "Erase"}}; - - const std::map draw_mode_names_ = { - {Sketch, "Sketch"}, {Laser, "Laser"}, {Onion, "Onion"}}; + {Draw, "Draw"}, + {Laser, "Laser"}, + {Square, "Square"}, + {Circle, "Circle"}, + {Arrow, "Arrow"}, + {Line, "Line"}, + {Text, "Text"}, + {Erase, "Erase"}}; module::StringChoiceAttribute *active_tool_{nullptr}; @@ -103,21 +119,21 @@ namespace ui { module::IntegerAttribute *text_bgr_opacity_{nullptr}; module::BooleanAttribute *text_cursor_blink_attr_{nullptr}; - module::BooleanAttribute *tool_is_active_{nullptr}; module::StringAttribute *action_attribute_{nullptr}; - module::IntegerAttribute *shape_tool_{nullptr}; - module::StringChoiceAttribute *draw_mode_{nullptr}; module::IntegerAttribute *moving_scaling_text_attr_{nullptr}; module::StringChoiceAttribute *font_choice_{nullptr}; module::StringChoiceAttribute *display_mode_attribute_{nullptr}; + module::Attribute *dockable_widget_attr_{nullptr}; + DisplayMode display_mode_{OnlyWhenPaused}; bool playhead_is_playing_{false}; utility::Uuid toggle_active_hotkey_; utility::Uuid undo_hotkey_; utility::Uuid redo_hotkey_; + utility::Uuid clear_hotkey_; // Annotations utility::Uuid current_bookmark_uuid_; @@ -141,9 +157,12 @@ namespace ui { Imath::V2f caption_drag_width_height_; Imath::V2f shape_anchor_; + std::string last_tool_ = {"Draw"}; + bool fade_looping_{false}; - std::map> - viewport_current_images_; + std::map viewport_current_images_; + media_reader::ImageBufPtr image_being_annotated_; + }; } // namespace viewport diff --git a/src/plugin/viewport_overlay/annotations/src/caption.cpp b/src/plugin/viewport_overlay/annotations/src/caption.cpp deleted file mode 100644 index 4f880d194..000000000 --- a/src/plugin/viewport_overlay/annotations/src/caption.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "caption.hpp" - -using namespace xstudio::ui::viewport; -using namespace xstudio; - -Caption::Caption( - const Imath::V2f position, - const float wrap_width, - const float font_size, - const utility::ColourTriplet colour, - const float opacity, - const Justification justification, - const std::string font_name) - : position_(position), - wrap_width_(wrap_width), - font_size_(font_size), - colour_(colour), - opacity_(opacity), - justification_(justification), - font_name_(std::move(font_name)) {} - -void Caption::modify_text(const std::string &t, std::string::const_iterator &cursor) { - if (t.size() != 1) { - return; - } - - if (cursor < text_.cbegin() || cursor > text_.cend()) { - cursor = text_.cend(); - } - - - const char ascii_code = t.c_str()[0]; - - const int cpos = std::distance(text_.cbegin(), cursor); - - // N.B. - calling text_.begin() invalidates 'cursor' as the string data gets copied - // to writeable buffer (or something). Maybe the way I use a string iterator for - // the caption cursor is bad. - auto cr = text_.begin(); - - std::advance(cr, cpos); - - if (ascii_code == 127) { - // delete - if (cr != text_.end()) { - cr = text_.erase(cr); - } - } else if (ascii_code == 8) { - // backspace - if (text_.size() && cr != text_.begin()) { - auto p = cr; - p--; - cr = text_.erase(p); - } - } else if (ascii_code >= 32 || ascii_code == '\r' || ascii_code == '\n') { - // printable character - cr = text_.insert(cr, ascii_code); - cr++; - } - cursor = cr; -} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/caption.hpp b/src/plugin/viewport_overlay/annotations/src/caption.hpp deleted file mode 100644 index 76ebe118e..000000000 --- a/src/plugin/viewport_overlay/annotations/src/caption.hpp +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "xstudio/utility/json_store.hpp" -#include "xstudio/ui/font.hpp" -#include - -namespace xstudio { -namespace ui { - namespace viewport { - - class Caption { - - public: - enum HoverState { - NotHovered, - HoveredInCaptionArea, - HoveredOnMoveHandle, - HoveredOnResizeHandle, - HoveredOnDeleteHandle - }; - - Caption( - const Imath::V2f position, - const float wrap_width, - const float font_size, - const utility::ColourTriplet colour, - const float opacity, - const Justification justification, - const std::string font_name); - Caption() = default; - Caption(const Caption &o) = default; - - Caption &operator=(const Caption &o) = default; - - void modify_text(const std::string &t, std::string::const_iterator &cursor); - - std::string text_; - Imath::V2f position_; - Imath::Box2f bounding_box_; - float wrap_width_; - float font_size_; - std::string font_name_; - utility::ColourTriplet colour_ = {utility::ColourTriplet(1.0f, 1.0f, 1.0f)}; - float opacity_; - Justification justification_; - - private: - friend class AnnotationSerialiser; - }; - - } // end namespace viewport -} // end namespace ui -} // end namespace xstudio diff --git a/src/plugin/viewport_overlay/annotations/src/pen_stroke.cpp b/src/plugin/viewport_overlay/annotations/src/pen_stroke.cpp deleted file mode 100644 index 1b97da8fb..000000000 --- a/src/plugin/viewport_overlay/annotations/src/pen_stroke.cpp +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "pen_stroke.hpp" - -using namespace xstudio::ui::viewport; -using namespace xstudio; - -#define CIRC_PTS 16 - -namespace { -/*static const struct CircPts { - - std::vector pts0; - std::vector pts1; - - CircPts(const int n) { - for (int i = 0; i < n; ++i) { - pts0.push_back(Imath::V3f( - cos(float(i)*M_PI*2.0f/float(n)), - sin(float(i)*M_PI*2.0f/float(n)), - 0.0f - )); - pts1.push_back(Imath::V3f( - cos(float(i+1)*M_PI*2.0f/float(n)), - sin(float(i+1)*M_PI*2.0f/float(n)), - 0.0f - )); - - } - } - -} s_circ_pts(CIRC_PTS);*/ - -static const struct CircPts { - - std::vector pts_; - - CircPts(const int n) { - for (int i = 0; i < n + 1; ++i) { - pts_.emplace_back(Imath::V2f( - cos(float(i) * M_PI * 2.0f / float(n)), - sin(float(i) * M_PI * 2.0f / float(n)))); - } - } - -} s_circ_pts(48); - -} // namespace - -PenStroke::PenStroke( - const utility::ColourTriplet &colour, const float thickness, const float opacity) - : thickness_(thickness), colour_(colour), opacity_(opacity) {} - -PenStroke::PenStroke(const float thickness) - : thickness_(thickness), - colour_(1.0f, 1.0f, 1.0f), - // opacity_(1.0f), - is_erase_stroke_(true) {} - -void PenStroke::add_point(const Imath::V2f &pt) { - if (!(!points_.empty() && points_.back() == pt)) { - points_.emplace_back(pt); - } -} - -inline float cross(const Imath::V3f &a, const Imath::V3f &b) { return a.x * b.y - b.x * a.y; } - -inline float dot(const Imath::V3f &a, const Imath::V3f &b) { return a.x * b.x + b.y * a.y; } - -inline bool line_intersection( - const Imath::V3f &q, - const Imath::V3f &q1, - const Imath::V3f &p, - const Imath::V3f &p1, - Imath::V3f &pos) { - - if (q1 == p || p1 == q) - return false; - - const Imath::V3f r = p1 - p; - const Imath::V3f s = q1 - q; - - float cr = cross(r, s); - if (cr == 0.0f) - return false; - - float t = cross(q - p, s) / cr; - float u = cross(q - p, r) / cr; - - if (t > 0.0f && t < 1.0f && u > 0.0f && u < 1.0f) { - - pos = p + r * t; - return true; - } - return false; -} - -int PenStroke::fetch_render_data(std::vector &vertices) { - if (!points_.empty()) { - vertices.insert(vertices.end(), points_.begin(), points_.end()); - vertices.push_back(points_.back()); // repeat last point to make end 'cap' - return points_.size() + 1; - } else { - return 0; - } -} - -void PenStroke::make_square(const Imath::V2f &corner1, const Imath::V2f &corner2) { - points_ = std::vector( - {Imath::V2f(corner1.x, corner1.y), - Imath::V2f(corner2.x, corner1.y), - Imath::V2f(corner2.x, corner2.y), - Imath::V2f(corner1.x, corner2.y), - Imath::V2f(corner1.x, corner1.y)}); -} - -void PenStroke::make_circle(const Imath::V2f &origin, const float radius) { - - points_.clear(); - for (const auto &pt : s_circ_pts.pts_) { - points_.push_back(origin + pt * radius); - } -} - -void PenStroke::make_arrow(const Imath::V2f &start, const Imath::V2f &end) { - - Imath::V2f v; - if (start == end) { - v = Imath::V2f(1.0f, 0.0f) * thickness_ * 4.0f; - } else { - v = (start - end).normalized() * std::max(thickness_ * 4.0f, 0.01f); - } - const Imath::V2f t(v.y, -v.x); - - points_.clear(); - points_.push_back(start); - points_.push_back(end); - points_.push_back(end + v + t); - points_.push_back(end); - points_.push_back(end + v - t); -} - -void PenStroke::make_line(const Imath::V2f &start, const Imath::V2f &end) { - points_.clear(); - points_.push_back(start); - points_.push_back(end); -} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/pen_stroke.hpp b/src/plugin/viewport_overlay/annotations/src/pen_stroke.hpp deleted file mode 100644 index 85243a45b..000000000 --- a/src/plugin/viewport_overlay/annotations/src/pen_stroke.hpp +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "xstudio/utility/json_store.hpp" -#include - - -// If a pen stroke has thickness of 1, it will be 1 pixel thick agains -// an image that 3860 pixels in width. -#define PEN_STROKE_THICKNESS_SCALE 3860.0f - -namespace xstudio { -namespace ui { - namespace viewport { - - class PenStroke { - - public: - PenStroke( - const utility::ColourTriplet &colour, - const float thickness, - const float opacity); - PenStroke(const float thickness); - PenStroke(const PenStroke &o) = default; - PenStroke() {} - - PenStroke &operator=(const PenStroke &o) = default; - - bool operator==(const PenStroke &o) const { - return ( - points_ == o.points_ && opacity_ == o.opacity_ && - thickness_ == o.thickness_ && is_erase_stroke_ == o.is_erase_stroke_ && - colour_ == o.colour_); - } - - void add_point(const Imath::V2f &pt); - - int fetch_render_data(std::vector &vertices); - - void make_square(const Imath::V2f &corner1, const Imath::V2f &corner2); - - void make_circle(const Imath::V2f &origin, const float radius); - - void make_arrow(const Imath::V2f &start, const Imath::V2f &end); - - void make_line(const Imath::V2f &start, const Imath::V2f &end); - - /*int fetch_render_data( - const float depth, - std::vector &vertices, - std::vector &vertices_distance_from_line_centre, - const bool fetch_fat_line_data);*/ - - std::vector points_; - float opacity_ = {1.0f}; - float thickness_ = {0.0f}; - bool is_erase_stroke_ = {false}; - utility::ColourTriplet colour_; - - private: - /*void make_render_data( - std::vector &vtxs, - std::vector &vtxs_dist_from_centre, - const float thickness_delta) const; - - std::vector vertices_; - std::vector vertex_dist_from_line_centre_; - - std::vector fat_line_vertices_; - std::vector fat_line_vertex_dist_from_line_centre_; - - float vertex_depth_;*/ - - friend class AnnotationsRenderer; - friend class AnnotationSerialiser; - }; - - } // end namespace viewport -} // end namespace ui -} // end namespace xstudio \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsButton.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsButton.qml deleted file mode 100644 index 8a7ff6787..000000000 --- a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsButton.qml +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.12 - -import xStudio 1.0 - -import xstudio.qml.module 1.0 - -XsTrayButton { - // prototype: true - - anchors.fill: parent - text: "Draw" - source: "qrc:///icons/drawing.png" - tooltip: "Open the Annotations Panel. Sketch, Laser Onion-Skinning and Text modes." - buttonPadding: pad - toggled_on: annotationToolActive - onClicked: { - // toggle the value in the "annotations_tool_active" backend attribute - if (anno_tool_backend_settings.annotations_tool_active != null) { - anno_tool_backend_settings.annotations_tool_active = !anno_tool_backend_settings.annotations_tool_active - } - } - - property var drawingDialog - - - property bool dialogVisible: drawingDialog ? drawingDialog.visible : false - - onDialogVisibleChanged: { - if (anno_tool_backend_settings.annotations_tool_active && dialogVisible == false) { - anno_tool_backend_settings.annotations_tool_active = false - } - } - - // connect to the backend module to give access to attributes - XsModuleAttributes { - id: anno_tool_backend_settings - attributesGroupNames: "annotations_tool_settings_0" - } - - // make a read only binding to the "annotations_tool_active" backend attribute - property bool annotationToolActive: anno_tool_backend_settings.annotations_tool_active ? anno_tool_backend_settings.annotations_tool_active : false - - onAnnotationToolActiveChanged: - { - // there are two AnnotationsButtons - one for main win, one for pop-out, - // but we only want one instance of the AnnotationsDialog .. this test - // should ensure that is the case - if (sessionWidget.is_main_window) { - if (drawingDialog === undefined) { - drawingDialog = Qt.createQmlObject("import AnnotationsTool 1.0; AnnotationsDialog {}", app_window, "dnyamic") - } - if (annotationToolActive) { - drawingDialog.show() - drawingDialog.requestActivate() - } else { - drawingDialog.hide() - } - } - } - -} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/AnnotationsDialog.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/AnnotationsDialog.qml deleted file mode 100644 index 3bd7fd052..000000000 --- a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/AnnotationsDialog.qml +++ /dev/null @@ -1,1177 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient - -import xStudio 1.1 -import xstudio.qml.module 1.0 - -XsWindow { - - id: drawDialog - title: "Drawing Tools" - - width: minimumWidth - minimumWidth: 215 - maximumWidth: minimumWidth - - height: minimumHeight - minimumHeight: toolActionFrame.y+toolActionFrame.height+advancedOptionsBtn.height/2+framePadding - maximumHeight: minimumHeight - - property int maxDrawSize: 250 - - onVisibleChanged: { - if (!visible) { - // ensure keyboard events are returned to the viewport - sessionWidget.playerWidget.viewport.forceActiveFocus() - } - } - - property bool isAdvancedOptionsClicked: false - property real buttonHeight: 20 - property real toolPropLoaderHeight: 0 - property real toolAadvancedFrameHeight: 115 - property real defaultHeight: toolSelectorFrame.height + toolActionFrame.height + framePadding*3 - - - property real itemSpacing: framePadding/2 - property real framePadding: 6 - property real framePadding_x2: framePadding*2 - property real frameWidth: 1 - property real frameRadius: 2 - property real frameOpacity: 0.3 - property color frameColor: XsStyle.menuBorderColor - - - property color hoverTextColor: palette.text //-whitish //XsStyle.hoverBackground - property color hoverToolInactiveColor: XsStyle.indevColor //-greyish - property color toolActiveBgColor: palette.highlight //-orangish - property color toolActiveTextColor: "white" //palette.highlightedText - property color toolInactiveBgColor: palette.base //-greyish - property color toolInactiveTextColor: XsStyle.controlTitleColor//-greyish - - property real fontSize: XsStyle.menuFontSize/1.1 - property string fontFamily: XsStyle.menuFontFamily - property color textButtonColor: toolInactiveTextColor - property color textValueColor: "white" - - - property bool isAnyToolSelected: currentTool !== "None" - - XsModuleAttributes { - id: anno_tool_backend_settings - attributesGroupNames: "annotations_tool_settings_0" - } - - - // make a local binding to the backend attribute - property int currentDrawPenSizeBackendValue: anno_tool_backend_settings.draw_pen_size ? anno_tool_backend_settings.draw_pen_size : 0 - property int currentShapePenSizeBackendValue: anno_tool_backend_settings.shapes_pen_size ? anno_tool_backend_settings.shapes_pen_size : 0 - property int currentErasePenSizeBackendValue: anno_tool_backend_settings.erase_pen_size ? anno_tool_backend_settings.erase_pen_size : 0 - property int currentTextSizeBackendValue: anno_tool_backend_settings.text_size ? anno_tool_backend_settings.text_size : 0 - property color currentToolColourBackendValue: anno_tool_backend_settings.pen_colour ? anno_tool_backend_settings.pen_colour : "#000000" - property int currentOpacityBackendValue: anno_tool_backend_settings.pen_opacity ? anno_tool_backend_settings.pen_opacity : 0 - property string currentToolBackendValue: anno_tool_backend_settings.active_tool ? anno_tool_backend_settings.active_tool : "" - - property color currentToolColour: currentToolColourBackendValue - property int currentToolSize: currentTool === "Text" ? currentTextSizeBackendValue : currentTool === "Erase" ? currentErasePenSizeBackendValue : (currentTool === "Shapes" ? currentShapePenSizeBackendValue : currentDrawPenSizeBackendValue) - property int currentToolOpacity: currentOpacityBackendValue - property string currentTool: currentToolBackendValue - - function setPenSize(penSize) { - if(currentTool === "Draw") - { //Draw - anno_tool_backend_settings.draw_pen_size = penSize - } - else if(currentTool === "Erase") - { //Erase - anno_tool_backend_settings.erase_pen_size = penSize - } - else if(currentTool === "Shapes") - { //Shapes - anno_tool_backend_settings.shapes_pen_size = penSize - } - else if(currentTool === "Text") - { //Shapes - anno_tool_backend_settings.text_size = penSize - } - } - - onCurrentToolChanged: { - if(currentTool === "Draw") - { //Draw - currentColorPresetModel = drawColourPresetsModel - } - else if(currentTool === "Erase") - { //Erase - currentColorPresetModel = eraseColorPresetModel - } - else if(currentTool === "Text") - { //Text - currentColorPresetModel = textColourPresetsModel - } - else if(currentTool === "Shapes") - { //Shapes - currentColorPresetModel = shapesColourPresetsModel - } - - } - - // make a read only binding to the "annotations_tool_active_0" backend attribute - property bool annotationToolActive: anno_tool_backend_settings.annotations_tool_active ? anno_tool_backend_settings.annotations_tool_active : false - - // is the mouse over the handles for moving, scaling or deleting text captions? - property int movingScalingText: anno_tool_backend_settings.moving_scaling_text ? anno_tool_backend_settings.moving_scaling_text : 0 - - // Are we in an active drawing mode? - property bool drawingActive: annotationToolActive && currentTool !== "None" - - // Set the Cursor as required - property var activeCursor: drawingActive ? movingScalingText > 1 ? Qt.ArrowCursor : currentTool == "Text" ? Qt.IBeamCursor : Qt.CrossCursor : Qt.ArrowCursor - - onActiveCursorChanged: { - playerWidget.viewport.setRegularCursor(activeCursor) - } - - // map the local property for currentToolSize to the backend value ... to modify the tool size, we only change the backend - // value binding - - property ListModel currentColorPresetModel: drawColourPresetsModel - - XsWindowStateSaver - { - windowObj: drawDialog - windowName: "annotations_toolbox" - override_pref_path: "/plugin/annotations/toolbox_window_settings" - } - - // Rectangle{ anchors.fill: parent ;color: "yellow"; opacity: 0.3 } - - // We wrap all the widgets in a top level Item that can forward keyboard - // events back to the viewport for consistent - Item { - anchors.fill: parent - Keys.forwardTo: [sessionWidget] - focus: true - - Rectangle{ id: toolSelectorFrame - width: parent.width - framePadding_x2 - x: framePadding - anchors.top: parent.top - anchors.topMargin: framePadding - anchors.bottom: toolProperties.bottom - anchors.bottomMargin: -framePadding - - color: "transparent" - border.width: frameWidth - border.color: frameColor - opacity: frameOpacity - radius: frameRadius - - } - - ToolSelector { - id: toolSelector - opacity: 1 - anchors.fill: toolSelectorFrame - } - - Loader { id: toolProperties - width: toolSelectorFrame.width - height: toolPropLoaderHeight - x: toolSelectorFrame.x - y: buttonHeight*2+framePadding_x2//toolSelectorFrame.toolSelector.y + toolSelectorFrame.toolSelector.height - - sourceComponent: - Item{ - - Rectangle{ - - id: row1 - y: itemSpacing - width: toolProperties.width - height: buttonHeight*1.5 + itemSpacing*2 - color: "transparent"; - visible: (isAnyToolSelected && currentTool !== "Erase") - - DrawCategories { - id: drawCategories - anchors.fill: parent - visible: (currentTool === "Draw") - } - - TextCategories { - id: textCategories - anchors.fill: parent - visible: (currentTool === "Text") - } - - ShapeCategories { - - id: shapeCategories - anchors.fill: parent - visible: (currentTool === "Shapes") - - } - - } - - - Row{id: row2 - x: framePadding //+ itemSpacing/2 - y: row1.y + row1.height - z: 1 - width: toolProperties.width - framePadding*2 - height: (buttonHeight*3) + (spacing*2) - spacing: itemSpacing*2 - - Column { - z: 2 - width: parent.width/2-spacing - spacing: itemSpacing - - XsButton{ id: sizeProp - property bool isPressed: false - property bool isMouseHovered: sizeMArea.containsMouse - property real prevValue: maxDrawSize/2 - property real newValue: maxDrawSize/2 - enabled: isAnyToolSelected - isActive: isPressed - x: spacing/2 - width: parent.width-x; height: buttonHeight; - // color: isPressed || isMouseHovered? (enabled? toolActiveBgColor: hoverToolInactiveColor): toolInactiveBgColor; - - Text{ - text: (currentTool=="Shapes")?"Width": "Size" - font.pixelSize: fontSize - font.family: fontFamily - color: parent.isPressed || parent.isMouseHovered? textValueColor: textButtonColor - width: parent.width/1.8 - horizontalAlignment: Text.AlignHCenter - anchors.left: parent.left - anchors.leftMargin: 2 - topPadding: framePadding/1.4 - } - XsTextField{ id: sizeDisplay - text: currentToolSize - property var backendSize: currentToolSize - onBackendSizeChanged: { - text = currentToolSize - } - focus: sizeMArea.containsMouse && !parent.isPressed - onFocusChanged:{ - if(focus) { - drawDialog.requestActivate() - selectAll() - forceActiveFocus() - } - else{ - deselect() - } - } - maximumLength: 3 - inputMask: "900" - inputMethodHints: Qt.ImhDigitsOnly - // validator: IntValidator {bottom: 0; top: maxDrawSize;} - selectByMouse: false - font.pixelSize: fontSize - font.family: fontFamily - color: parent.enabled? textValueColor : Qt.darker(textValueColor,1.5) - width: parent.width/2.2 - height: parent.height - horizontalAlignment: TextInput.AlignHCenter - anchors.right: parent.right - topPadding: framePadding/5 - onEditingCompleted: { - accepted() - } - onAccepted:{ - if(parseInt(text) >= maxDrawSize){ - setPenSize(maxDrawSize) - } - else if(parseInt(text) <= 1){ - setPenSize(1) - } - else{ - setPenSize(parseInt(text)) - } - - text = "" + backendSize - selectAll() - } - } - MouseArea{ - id: sizeMArea - anchors.fill: parent - cursorShape: Qt.SizeHorCursor - hoverEnabled: true - propagateComposedEvents: true - property real prevMX: 0 - property real deltaMX: 0 - property real stepSize: 0.25 - property int valueOnPress: 0 - onMouseXChanged: { - if(parent.isPressed && parent.enabled) - { - deltaMX = mouseX - prevMX - - let deltaValue = parseInt(deltaMX*stepSize) - let valueToApply = Math.round(valueOnPress + deltaValue) - - if(deltaMX>0) - { - if(valueToApply >= maxDrawSize){ - setPenSize(maxDrawSize) - valueOnPress = maxDrawSize - prevMX = mouseX - } - else { - setPenSize(valueToApply) - } - } - else { - if(valueToApply < 1){ - setPenSize(1) - valueOnPress = 1 - prevMX = mouseX - } - else { - setPenSize(valueToApply) - } - } - - sizeDisplay.text = currentToolSize - - if(deltaMX!=0){ - sizeProp.newValue = currentToolSize - } - } - } - onPressed: { - prevMX = mouseX - valueOnPress = currentToolSize - - parent.isPressed = true - focus = true - } - onReleased: { - if(prevMX !== mouseX) { - sizeProp.prevValue = valueOnPress - sizeProp.newValue = currentToolSize - } - parent.isPressed = false - focus = false - } - onDoubleClicked: { - if(currentToolSize == sizeProp.newValue){ - setPenSize(sizeProp.prevValue) - } - else{ - sizeProp.prevValue = currentToolSize - setPenSize(sizeProp.newValue) - } - sizeDisplay.text = currentToolSize - } - } - } - XsButton{ id: opacityProp - property bool isPressed: false - property bool isMouseHovered: opacityMArea.containsMouse - property real prevValue: defaultValue/2 - property real defaultValue: 100 - enabled: isAnyToolSelected && currentTool != "Erase" - isActive: isPressed - x: spacing/2 - width: parent.width-x; height: buttonHeight; - // color: isPressed || isMouseHovered? (enabled? toolActiveBgColor: hoverToolInactiveColor): toolInactiveBgColor; - Text{ - text: "Opacity" - font.pixelSize: fontSize - font.family: fontFamily - color: parent.isPressed || parent.isMouseHovered? textValueColor: textButtonColor - width: parent.width/1.8 - horizontalAlignment: Text.AlignHCenter - anchors.left: parent.left - anchors.leftMargin: 2 - topPadding: framePadding/1.4 - } - XsTextField{ id: opacityDisplay - bgColorNormal: parent.enabled?palette.base:"transparent" - borderColor: bgColorNormal - text: currentTool != "Erase" ? currentToolOpacity : 100 - property var backendOpacity: currentTool != "Erase" ? currentToolOpacity : 100 // we don't set this anywhere else, so this is read-only - always tracks the backend opacity value - onBackendOpacityChanged: { - // if the backend value has changed, update the text - text = currentTool != "Erase" ? currentToolOpacity : 100 - } - focus: opacityMArea.containsMouse && !parent.isPressed - onFocusChanged:{ - if(focus) { - drawDialog.requestActivate() - selectAll() - forceActiveFocus() - } - else{ - deselect() - } - } - maximumLength: 3 - // inputMask: "900" - inputMethodHints: Qt.ImhDigitsOnly - // validator: IntValidator {bottom: 0; top: 100;} - selectByMouse: false - font.pixelSize: fontSize - font.family: fontFamily - color: parent.enabled? textValueColor : Qt.darker(textValueColor,1.5) - width: parent.width/2.2 - height: parent.height - horizontalAlignment: TextInput.AlignHCenter - anchors.right: parent.right - topPadding: framePadding/5 - onEditingCompleted:{ - accepted() - } - onAccepted:{ - if(currentTool != "Erase"){ - - if(parseInt(text) >= 100) { - anno_tool_backend_settings.pen_opacity = 100 - } - else if(parseInt(text) <= 1) { - anno_tool_backend_settings.pen_opacity = 1 - } - else { - anno_tool_backend_settings.pen_opacity = parseInt(text) - } - - text = "" + backendOpacity - selectAll() - } - } - } - MouseArea{ - id: opacityMArea - - anchors.fill: parent - cursorShape: Qt.SizeHorCursor - hoverEnabled: true - propagateComposedEvents: true - property real prevMX: 0 - property real deltaMX: 0.0 - property real stepSize: 0.25 - property int valueOnPress: 0 - onMouseXChanged: { - if(parent.isPressed) - { - deltaMX = mouseX - prevMX - // prevMX = mouseX - // var new_opac = (Math.max(Math.min(100.0, anno_tool_backend_settings.pen_opacity + stepSize), 0.0) + 0.1) - 0.1 - // anno_tool_backend_settings.pen_opacity = parseInt(new_opac) - - let deltaValue = parseInt(deltaMX*stepSize) - let valueToApply = Math.round(valueOnPress + deltaValue) - - if(deltaMX>0) - { - if(valueToApply >= 100) { - anno_tool_backend_settings.pen_opacity=100 - valueOnPress = 100 - prevMX = mouseX - } - else { - anno_tool_backend_settings.pen_opacity = valueToApply - } - } - else { - if(valueToApply < 1){ - anno_tool_backend_settings.pen_opacity=1 - valueOnPress = 1 - prevMX = mouseX - } - else { - anno_tool_backend_settings.pen_opacity = valueToApply - } - } - - opacityDisplay.text = currentTool != "Erase" ? currentToolOpacity : 100 - } - } - onPressed: { - prevMX = mouseX - valueOnPress = anno_tool_backend_settings.pen_opacity - - parent.isPressed = true - focus = true - } - onReleased: { - parent.isPressed = false - focus = false - } - onDoubleClicked: { - if(anno_tool_backend_settings.pen_opacity == opacityProp.defaultValue){ - anno_tool_backend_settings.pen_opacity = opacityProp.prevValue - } - else{ - opacityProp.prevValue = anno_tool_backend_settings.pen_opacity - anno_tool_backend_settings.pen_opacity = opacityProp.defaultValue - } - opacityDisplay.text = currentTool != "Erase" ? currentToolOpacity : 100 - } - } - } - XsButton{ id: colorProp - property bool isPressed: false - property bool isMouseHovered: colorMArea.containsMouse - enabled: (isAnyToolSelected && currentTool !== "Erase") - isActive: isPressed - x: spacing/2 - width: parent.width-x; height: buttonHeight; - // color: isPressed || isMouseHovered? (enabled? toolActiveBgColor: hoverToolInactiveColor): toolInactiveBgColor; - - MouseArea{ - id: colorMArea - // enabled: currentTool !== 1 - hoverEnabled: true - anchors.fill: parent - onClicked: { - parent.isPressed = false - colorDialog.open() - } - onPressed: { - parent.isPressed = true - } - onReleased: { - parent.isPressed = false - } - } - Text{ - text: "Colour" - font.pixelSize: fontSize - font.family: fontFamily - color: parent.isPressed || parent.isMouseHovered? textValueColor: textButtonColor - width: parent.width/2 - horizontalAlignment: Text.AlignHCenter - anchors.right: parent.horizontalCenter - anchors.rightMargin: -3 - topPadding: framePadding/1.2 - } - Rectangle{ id: colorPreviewDuplicate - opacity: (!isAnyToolSelected || currentTool === "Erase")? (parent.enabled?1:0.5): 0 - height: parent.height/1.4; - color: currentTool === "Erase" ? "white" : currentToolColour - border.width: frameWidth - border.color: parent.enabled? (currentToolColour=="white" || currentToolColour=="#ffffff")? "black": "white" : Qt.darker("white",1.5) - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.horizontalCenter - anchors.leftMargin: parent.width/7 - anchors.right: parent.right - anchors.rightMargin: parent.width/10 - } - Rectangle{ id: colorPreview - visible: (isAnyToolSelected && currentTool !== "Erase") - x: colorPreviewDuplicate.x - y: colorPreviewDuplicate.y - width: colorPreviewDuplicate.width - onWidthChanged: { - x= colorPreviewDuplicate.x - y= colorPreviewDuplicate.y - } - height: colorPreviewDuplicate.height - color: currentTool === "Erase" ? "white" : currentToolColour; - border.width: frameWidth; - border.color: (color=="white" || color=="#ffffff")? "black": "white" - - scale: dragArea.drag.active? 0.6: 1 - Behavior on scale {NumberAnimation{ duration: 250 }} - - Drag.active: dragArea.drag.active - Drag.hotSpot.x: colorPreview.width/2 - Drag.hotSpot.y: colorPreview.height/2 - MouseArea{ - id: dragArea - anchors.fill: parent - drag.target: parent - - drag.minimumX: -framePadding - drag.maximumX: toolSelectorFrame.width - framePadding*5 - drag.minimumY: buttonHeight - drag.maximumY: buttonHeight*2.5 - - onReleased: { - colorProp.isPressed = false - parent.Drag.drop() - parent.x = colorPreviewDuplicate.x - parent.y = colorPreviewDuplicate.y - } - onClicked: { - colorProp.isPressed = false - colorDialog.open() - } - onPressed: { - colorProp.isPressed = true - } - } - } - } - } - - Rectangle { id: toolPreview - width: parent.width/2 - spacing - height: currentTool === "Text"? (parent.height/3-spacing) : (parent.height-spacing) - color: "#595959" //"transparent" - border.color: frameColor - border.width: frameWidth - // clip: true - - Grid {id: checkerBg; - property real tileSize: framePadding - anchors.fill: parent; - anchors.centerIn: parent - anchors.margins: tileSize/2; - clip: true; - rows: Math.floor(height/tileSize); - columns: Math.floor(width/tileSize); - Repeater { - model: checkerBg.columns*checkerBg.rows - Rectangle { - property int oddRow: Math.floor(index / checkerBg.columns)%2 - property int oddColumn: (index % checkerBg.columns)%2 - width: checkerBg.tileSize; height: checkerBg.tileSize - color: (oddRow == 1 ^ oddColumn == 1) ? "#949494": "#595959" - } - } - } - - Rectangle{ - - id: clippedPreview - anchors.fill: parent - color: "transparent" - clip: true - - Rectangle {id: drawPreview - visible: currentTool === "Draw" - anchors.centerIn: parent - property real sizeScaleFactor: (parent.height)/maxDrawSize - width: currentToolSize *sizeScaleFactor - height: width - radius: width/2 - color: currentToolColour - opacity: currentToolOpacity/100 - - RadialGradient { - visible: false - anchors.fill: parent - source: parent - gradient: - Gradient { - GradientStop { - position: 0.1; color: currentToolColour - } - GradientStop { - position: 1.0; color: "black" - } - } - } - - } - - Rectangle { id: erasePreview - visible: currentTool === "Erase" - anchors.centerIn: parent - property real sizeScaleFactor: (parent.height)/maxDrawSize - width: currentToolSize * sizeScaleFactor - height: width - radius: width/2 - color: "white" - opacity: 1 - } - - Item{ id: textPreview - anchors.centerIn: parent - visible: currentTool === "Text" - - Rectangle{ id: textFillPreview - anchors.fill: textFontPreview - color: textCategories.backgroundColor - opacity: textCategories.backgroundOpacity / 100 - } - - Text{ id: textFontPreview - text: "Example" - property real sizeScaleFactor: 80/100 - font.pixelSize: currentToolSize * sizeScaleFactor - //font.family: textCategories.currentValue - color: currentToolColour - opacity: currentToolOpacity / 100 - horizontalAlignment: Text.AlignHCenter - anchors.centerIn: parent - } - } - - Image { id: shapePreview - visible: currentTool === "Shapes" - anchors.centerIn: parent - - width: parent.height - height: width - - fillMode: Image.PreserveAspectFit - antialiasing: true - smooth: true - - property string src: shapeCategories.shapePreviewSVGSource(currentToolSize) - onSrcChanged: { - shapePreview.source = "" - shapePreview.source = shapePreview.src - } - source: src - opacity: currentToolOpacity/100 - layer { - enabled: true - effect: - ColorOverlay { - color: currentToolColour - } - } - } - - MouseArea{ id: testPreviewWithKeys //#test-block - anchors.fill: parent - onWheel: { - if (wheel.modifiers) - { - if (wheel.angleDelta.y > 0 && Qt.ControlModifier) - { - if(currentToolSize < maxDrawSize) setPenSize(currentToolSize+1) - } - else if(wheel.angleDelta.y < 0 && Qt.ControlModifier) - { - if(currentToolSize >1) setPenSize(currentToolSize-1) - } - else if (wheel.angleDelta.x != 0) - { - - var delta = wheel.angleDelta.x < 0 ? -2 : 2 - var new_opac = anno_tool_backend_settings.pen_opacity + delta - anno_tool_backend_settings.pen_opacity = Math.round(Math.max(Math.min(1, new_opac), 0)) - } - wheel.accepted=true - } - else{ - wheel.accepted=false - } - } - } - } - } - } - - - Rectangle{ id: row3 - y: row2.y + row2.height + presetColours.spacing - width: toolProperties.width - height: buttonHeight *1.5 - visible: (isAnyToolSelected && currentTool !== "Erase") - color: "transparent" - - ListView{ id: presetColours - x: frameWidth +spacing*2 - width: parent.width - frameWidth*2 - spacing*2 - height: parent.height - anchors.verticalCenter: parent.verticalCenter - spacing: (itemSpacing!==0)?itemSpacing/2: 0 - clip: true - interactive: false - orientation: ListView.Horizontal - - model: currentColorPresetModel - delegate: - Item{ - property bool isMouseHovered: presetMArea.containsMouse - width: presetColours.width/9-presetColours.spacing; - height: presetColours.height - Rectangle { - anchors.centerIn: parent - width: parent.width - height: width - radius: width/2 - color: preset - border.width: 1 - border.color: parent.isMouseHovered? toolActiveBgColor: (currentToolColour === preset)? toolActiveTextColor: "black" - - MouseArea{ - id: presetMArea - property color temp_color - anchors.fill: parent - hoverEnabled: true - onClicked: { - - temp_color = currentColorPresetModel.get(index).preset; - anno_tool_backend_settings.pen_colour = temp_color - - } - } - - DropArea { - anchors.fill: parent - Image { - visible: parent.containsDrag - anchors.fill: parent - source: "qrc:///feather_icons/plus-circle.svg" - layer { - enabled: (preset=="black" || preset=="#000000") - effect: - ColorOverlay { - color: "white" - } - } - } - onDropped: { - currentColorPresetModel.setProperty(index, "preset", currentToolColour.toString()) - } - } - } - } - } - } - - Component.onCompleted: { - toolPropLoaderHeight = row3.y + row3.height - } - } - - - ColorDialog { id: colorDialog - title: "Please pick a color" - color: currentToolColour - onAccepted: { - anno_tool_backend_settings.pen_colour = currentColor - close() - } - onRejected: { - close() - } - } - - ListModel{ id: eraseColorPresetModel - ListElement{ - preset: "white" - } - } - ListModel{ id: drawColourPresetsModel - ListElement{ - preset: "#ff0000" //- "red" - } - ListElement{ - preset: "#ffa000" //- "orange" - } - ListElement{ - preset: "#ffff00" //- "yellow" - } - ListElement{ - preset: "#28dc00" //- "green" - } - ListElement{ - preset: "#0050ff" //- "blue" - } - ListElement{ - preset: "#8c00ff" //- "violet" - } - ListElement{ - preset: "#ff64ff" //- "pink" - } - ListElement{ - preset: "#ffffff" //- "white" - } - ListElement{ - preset: "#000000" //- "black" - } - } - ListModel{ id: textColourPresetsModel - ListElement{ - preset: "#ff0000" //- "red" - } - ListElement{ - preset: "#ffa000" //- "orange" - } - ListElement{ - preset: "#ffff00" //- "yellow" - } - ListElement{ - preset: "#28dc00" //- "green" - } - ListElement{ - preset: "#0050ff" //- "blue" - } - ListElement{ - preset: "#8c00ff" //- "violet" - } - ListElement{ - preset: "#ff64ff" //- "pink" - } - ListElement{ - preset: "#ffffff" //- "white" - } - ListElement{ - preset: "#000000" //- "black" - } - } - ListModel{ id: shapesColourPresetsModel - ListElement{ - preset: "#ff0000" //- "red" - } - ListElement{ - preset: "#ffa000" //- "orange" - } - ListElement{ - preset: "#ffff00" //- "yellow" - } - ListElement{ - preset: "#28dc00" //- "green" - } - ListElement{ - preset: "#0050ff" //- "blue" - } - ListElement{ - preset: "#8c00ff" //- "violet" - } - ListElement{ - preset: "#ff64ff" //- "pink" - } - ListElement{ - preset: "#ffffff" //- "white" - } - ListElement{ - preset: "#000000" //- "black" - } - } - } - - - // lower z layer than toolActionFrame - Rectangle{ id: toolAadvancedFrame - visible: isAdvancedOptionsClicked - x: framePadding - anchors.top: toolActionFrame.bottom - anchors.topMargin: -frameWidth - - width: parent.width - framePadding_x2 - height: toolAadvancedFrameHeight - - color: "transparent" - opacity: frameOpacity - border.width: frameWidth - border.color: frameColor - radius: frameRadius - } - Item{ - visible: isAdvancedOptionsClicked - anchors.fill: toolAadvancedFrame - Text{ - text: "Currently Unavailable" - font.pixelSize: fontSize // /1.3 - font.family: fontFamily - color: textButtonColor - width: parent.width - horizontalAlignment: Text.AlignHCenter - anchors.centerIn: parent - } - } - - - - Rectangle{ id: toolActionFrame - x: framePadding - anchors.top: toolSelectorFrame.bottom - anchors.topMargin: framePadding - - width: parent.width - framePadding_x2 - height: toolSelectorFrame.height/1.5 - - color: "transparent" - opacity: frameOpacity - border.width: frameWidth - border.color: frameColor - radius: frameRadius - } - Item{ id: toolActionSection - x: toolActionFrame.x - width: toolActionFrame.width - - ListView{ id: toolActionUndoRedo - - width: parent.width - framePadding_x2 - height: buttonHeight - x: framePadding + spacing/2 - y: toolActionFrame.y + framePadding + spacing/2 - - spacing: itemSpacing - clip: true - interactive: false - orientation: ListView.Horizontal - - model: - ListModel{ - id: modelUndoRedo - ListElement{ - action: "Undo" - } - ListElement{ - action: "Redo" - } - } - delegate: - XsButton{ - text: model.action - width: toolActionUndoRedo.width/modelUndoRedo.count - toolActionUndoRedo.spacing - height: buttonHeight - onClicked: { - anno_tool_backend_settings.action_attribute = text - } - } - } - - ListView{ id: toolActionCopyPasteClear - - width: parent.width - framePadding_x2 - height: buttonHeight - x: framePadding + spacing/2 - y: toolActionUndoRedo.y + toolActionUndoRedo.height + spacing - - spacing: itemSpacing - clip: true - interactive: false - orientation: ListView.Horizontal - - model: - ListModel{ - id: modelCopyPasteClear - ListElement{ - action: "Copy" - } - ListElement{ - action: "Paste" - } - ListElement{ - action: "Clear" - } - } - delegate: - XsButton{ - text: model.action - width: toolActionCopyPasteClear.width/modelCopyPasteClear.count - toolActionCopyPasteClear.spacing - height: buttonHeight - enabled: text == "Clear" - onClicked: { - anno_tool_backend_settings.action_attribute = text - } - - } - } - - Rectangle{ id: displayAnnotations - width: parent.width - framePadding_x2 - height: buttonHeight; - color: "transparent"; - anchors.top: toolActionCopyPasteClear.bottom - anchors.topMargin: framePadding_x2 - anchors.horizontalCenter: parent.horizontalCenter - - Text{ - text: "Display Annotations" - font.pixelSize: fontSize - font.family: fontFamily - color: textButtonColor - width: parent.width - horizontalAlignment: Text.AlignHCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: framePadding/2 - } - } - - // Sigh - hooking up the draw mode backen attr to the combo box here - // is horrible! Need something better than this! - - XsModuleAttributes { - // this lets us get at the combo_box_options for the 'Display Mode' attr - id: annotations_tool_draw_mode_options - attributesGroupNames: "annotations_tool_draw_mode_0" - roleName: "combo_box_options" - } - - XsModuleAttributes { - // this lets us get at the value for the 'Display Mode' attr - id: annotations_tool_draw_mode - attributesGroupNames: "annotations_tool_draw_mode_0" - } - - XsComboBox { - - id: dropdownAnnotations - - property var displayModeOptions: annotations_tool_draw_mode_options.display_mode ? annotations_tool_draw_mode_options.display_mode : [] - property var displayModeValue: annotations_tool_draw_mode.display_mode ? annotations_tool_draw_mode.display_mode : "" - - model: displayModeOptions - width: parent.width/1.3; - height: buttonHeight - anchors.top: displayAnnotations.bottom - anchors.topMargin: itemSpacing - anchors.horizontalCenter: parent.horizontalCenter - onCurrentTextChanged: { - if (currentText != displayModeValue && annotations_tool_draw_mode.display_mode != undefined) { - annotations_tool_draw_mode.display_mode = currentText - } - } - onDisplayModeValueChanged: { - - if (displayModeOptions.indexOf(displayModeValue) != -1) { - currentIndex = displayModeOptions.indexOf(displayModeValue) - } - } - - } - - - XsButton{ id: advancedOptionsBtn - text: "Advanced Options" - enabled: false - width: parent.width/1.3 - height: buttonHeight - borderRadius: 6 - y: toolActionFrame.y + toolActionFrame.height - height/2 - anchors.horizontalCenter: parent.horizontalCenter - Image{ - width: 10 - height: 10 - y: parent.height/2 -height/2 - anchors.left: parent.left - anchors.leftMargin: framePadding_x2 - source: "qrc:///feather_icons/play.svg" - rotation: (isAdvancedOptionsClicked)? 90: 0 - Behavior on rotation {NumberAnimation{ duration: 150 }} - layer { - enabled: true - effect: - ColorOverlay { - color: parent.isMouseHovered? textValueColor: toolInactiveTextColor - } - } - } - onClicked: { - isAdvancedOptionsClicked = !isAdvancedOptionsClicked - } - } - } - } - -} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/DrawCategories.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/DrawCategories.qml deleted file mode 100644 index 4229cce5b..000000000 --- a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/DrawCategories.qml +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient - -import xStudio 1.1 -import xstudio.qml.module 1.0 - -Item{ - - // Rectangle{ id: divLine - // anchors.centerIn: parent - // width: parent.width - height*2 - // height: frameWidth - // color: frameColor - // opacity: frameOpacity - // } - - // note this model only has one item, which is the 'scribble mode' attribute - // in the backend. - XsModuleAttributesModel { - id: anno_draw_mode_backend - attributesGroupNames: "anno_scribble_mode_backend_0" - } - - // we have to use a repeater to hook the model into the ListView - Repeater { - - id: the_view - width: parent.width-framePadding_x2 - height: buttonHeight - model: anno_draw_mode_backend - - ListView{ - - id: drawList - - // x: spacing/2 - // width: parent.width/1.3 - // height: buttonHeight/1.3 - width: parent.width-framePadding_x2 - height: buttonHeight - x: framePadding + spacing/2 - anchors.verticalCenter: parent.verticalCenter - spacing: itemSpacing //(itemSpacing!==0)?itemSpacing/2: 0 - clip: true - interactive: false - orientation: ListView.Horizontal - - model: combo_box_options - delegate: - XsButton{ - text: (combo_box_options && index >= 0) ? combo_box_options[index] : "" - width: drawList.width/combo_box_options.length-drawList.spacing - height: drawList.height - // radius: (height/1.2) - enabled: combo_box_options_enabled ? combo_box_options_enabled[index] : true - isActive: value == text - font.pixelSize: fontSize - - onClicked: { - // 'value' comes from the model .. it's a connection to - // the value of the 'Scribble Mode' attribute in the backend - value = text - } - } - } - } -} - diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/ShapeCategories.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/ShapeCategories.qml deleted file mode 100644 index 46d319c75..000000000 --- a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/ShapeCategories.qml +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient - -import xStudio 1.1 -import xstudio.qml.module 1.0 - -Item{ - - id: shapeCategories - anchors.fill: parent - visible: (currentTool === 3) - property alias shapesModel: shapesModel - - // Rectangle{ id: divLine - // anchors.centerIn: parent - // width: parent.width - height*2 - // height: frameWidth - // color: frameColor - // opacity: frameOpacity - // } - - XsModuleAttributes { - id: anno_tool_backend_settings - attributesGroupNames: "annotations_tool_settings_0" - } - - // make a local binding to the backend attribute - property int shapeTool: anno_tool_backend_settings.shape_tool ? anno_tool_backend_settings.shape_tool : 0.0 - - onShapeToolChanged: { - shapesList.currentIndex = shapeTool - } - - ListView{ id: shapesList - - width: parent.width-framePadding_x2 - height: buttonHeight - x: framePadding + spacing/2 - // y: -parent.height/3 - anchors.verticalCenter: parent.verticalCenter - - spacing: itemSpacing - clip: true - interactive: false - orientation: ListView.Horizontal - - model: shapesModel - - onCurrentIndexChanged: { - if (anno_tool_backend_settings.shape_tool != undefined) { - anno_tool_backend_settings.shape_tool = currentIndex - } - } - - delegate: - XsButton{ - id: shapeBtn - text: "" - width: shapesList.width/shapesModel.count-shapesList.spacing; - height: shapesList.height - isActive: shapesList.currentIndex===index - Image { - anchors.centerIn: parent - width: height - height: parent.height-(shapesList.spacing*2) - source: shapeImage - layer { - enabled: true - effect: - ColorOverlay { - color: (shapesList.currentIndex===index || shapeBtn.hovered)? toolActiveTextColor: toolInactiveTextColor - } - } - } - onClicked: { - shapesList.currentIndex = index - } - } - } - - ListModel{ id: shapesModel - - ListElement{ - shapeImage: "qrc:///feather_icons/square.svg" - shapeSVG1: "data: image/svg+xml;utf8, " - } - ListElement{ - shapeImage: "qrc:///feather_icons/circle.svg" - shapeSVG1: "data: image/svg+xml;utf8, " - } - ListElement{ - shapeImage: "qrc:///feather_icons/arrow-right.svg" - shapeSVG1: "data: image/svg+xml;utf8, " - } - ListElement{ - shapeImage: "qrc:///feather_icons/minus.svg" - shapeSVG1: "data: image/svg+xml;utf8, " - } - - } - - function shapePreviewSVGSource(tool_size) { - return shapesModel.get(shapesList.currentIndex).shapeSVG1 + (tool_size*7/100) + shapesModel.get(shapesList.currentIndex).shapeSVG2 - } - -} diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/TextCategories.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/TextCategories.qml deleted file mode 100644 index c619cf1ed..000000000 --- a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/TextCategories.qml +++ /dev/null @@ -1,294 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient - -import xStudio 1.1 -import xstudio.qml.module 1.0 - -Item{ id: textCategory - - property real itemSpacing: framePadding/2 - property real framePadding: 6 - property real framePadding_x2: framePadding*2 - property real frameWidth: 1 - - property int iconsize: XsStyle.menuItemHeight *.66 - property real fontSize: XsStyle.menuFontSize/1.1 - property string fontFamily: XsStyle.menuFontFamily - - property color toolInactiveTextColor: XsStyle.controlTitleColor - property color textButtonColor: toolInactiveTextColor - property color textValueColor: "white" - - XsModuleAttributesModel { - id: anno_font_options - attributesGroupNames: "annotations_tool_fonts_0" - } - - XsModuleAttributes { - id: anno_tool_backend_settings - attributesGroupNames: "annotations_tool_settings_0" - } - - property color backgroundColorBackendValue: anno_tool_backend_settings.text_background_colour ? anno_tool_backend_settings.text_background_colour : "#000000" - property int backgroundOpacityBackendValue: anno_tool_backend_settings.text_background_opacity ? anno_tool_backend_settings.text_background_opacity : 0 - - property color backgroundColor: backgroundColorBackendValue - property int backgroundOpacity: backgroundOpacityBackendValue - - Repeater { - - // Using a repeater here - but 'vp_mouse_wheel_behaviour_attr' only - // has one row by the way. The use of a repeater means the model role - // data are all visible in the XsComboBox instance. - model: anno_font_options - - XsComboBox { - - id: dropdownFonts - - width: parent.width -framePadding_x2 -itemSpacing/2 - height: buttonHeight - model: combo_box_options - anchors.left: parent.left - anchors.leftMargin: framePadding - anchors.verticalCenter: parent.verticalCenter - - property var value_: value ? value : null - onValue_Changed: { - currentIndex = indexOfValue(value_) - } - - Component.onCompleted: currentIndex = indexOfValue(value_) - onCurrentValueChanged: { - value = currentValue; - } - } - } - - XsButton{ id: fillOpacityProp - property bool isPressed: false - property bool isMouseHovered: opacityMArea.containsMouse - property real prevValue: defaultValue/2 - property real defaultValue: 50 - isActive: isPressed - - width: (parent.width-framePadding_x2)/2 -itemSpacing/2 - height: buttonHeight - anchors.right: parent.right - anchors.rightMargin: framePadding - // anchors.verticalCenter: parent.verticalCenter - y: buttonHeight*3 -1 - - Text{ - text: "BG Opac." - font.pixelSize: fontSize - font.family: fontFamily - color: parent.isPressed || parent.isMouseHovered? textValueColor: textButtonColor - width: parent.width/1.8 - horizontalAlignment: Text.AlignHCenter - anchors.left: parent.left - anchors.leftMargin: 2 - topPadding: framePadding/1.4 - } - XsTextField{ id: opacityDisplay - bgColorNormal: parent.enabled?palette.base:"transparent" - borderColor: bgColorNormal - text: backgroundOpacity - property var backendOpacity: backgroundOpacity - // we don't set this anywhere else, so this is read-only - always tracks the backend opacity value - onBackendOpacityChanged: { - // if the backend value has changed, update the text - text = backgroundOpacity - } - focus: opacityMArea.containsMouse && !parent.isPressed - onFocusChanged:{ - if(focus) { - drawDialog.requestActivate() - selectAll() - forceActiveFocus() - } - else{ - deselect() - } - } - maximumLength: 3 - inputMask: "900" - inputMethodHints: Qt.ImhDigitsOnly - // validator: IntValidator {bottom: 0; top: 100;} - selectByMouse: false - font.pixelSize: fontSize - font.family: fontFamily - color: parent.enabled? textValueColor : Qt.darker(textValueColor,1.5) - width: parent.width/2.2 - height: parent.height - horizontalAlignment: TextInput.AlignHCenter - anchors.right: parent.right - topPadding: framePadding/5 - onEditingCompleted:{ - accepted() - } - onAccepted:{ - if(parseInt(text) >= 100) { - anno_tool_backend_settings.text_background_opacity = 100 - } - else if(parseInt(text) <= 0) { - anno_tool_backend_settings.text_background_opacity = 0 - } - else { - anno_tool_backend_settings.text_background_opacity = parseInt(text) - } - selectAll() - } - } - MouseArea{ - id: opacityMArea - anchors.fill: parent - cursorShape: Qt.SizeHorCursor - hoverEnabled: true - propagateComposedEvents: true - property real prevMX: 0 - property real deltaMX: 0.0 - property real stepSize: 0.25 - property int valueOnPress: 0 - onMouseXChanged: { - if(parent.isPressed) - { - deltaMX = mouseX - prevMX - - let deltaValue = parseInt(deltaMX*stepSize) - let valueToApply = Math.round(valueOnPress + deltaValue) - - if(deltaMX>0) - { - if(valueToApply >= 100) { - anno_tool_backend_settings.text_background_opacity = 100 - valueOnPress = 100 - prevMX = mouseX - } - else { - anno_tool_backend_settings.text_background_opacity = valueToApply - } - } - else { - if(valueToApply < 1){ - anno_tool_backend_settings.text_background_opacity = 0 - valueOnPress = 0 - prevMX = mouseX - } - else { - anno_tool_backend_settings.text_background_opacity = valueToApply - } - } - opacityDisplay.text = backgroundOpacity - } - } - onPressed: { - prevMX = mouseX - valueOnPress = anno_tool_backend_settings.text_background_opacity - - parent.isPressed = true - focus = true - } - onReleased: { - parent.isPressed = false - focus = false - } - onDoubleClicked: { - if(anno_tool_backend_settings.text_background_opacity == fillOpacityProp.defaultValue){ - anno_tool_backend_settings.text_background_opacity = fillOpacityProp.prevValue - } - else{ - fillOpacityProp.prevValue = backgroundOpacity - anno_tool_backend_settings.text_background_opacity = fillOpacityProp.defaultValue - } - opacityDisplay.text = backgroundOpacity - } - } - } - - XsButton{ id: fillColorProp - property bool isPressed: false - property bool isMouseHovered: fillMArea.containsMouse - isActive: isPressed - width: (parent.width-framePadding_x2)/2 -itemSpacing/2 - height: buttonHeight - anchors.right: parent.right - anchors.rightMargin: framePadding - y: buttonHeight*4 + itemSpacing -1 - - MouseArea{ - id: fillMArea - hoverEnabled: true - anchors.fill: parent - onClicked: { - parent.isPressed = false - colorDialog.open() - } - onPressed: { - parent.isPressed = true - } - onReleased: { - parent.isPressed = false - } - } - - Text{ - text: " BG Col." - font.pixelSize: fontSize - font.family: fontFamily - color: parent.isPressed || parent.isMouseHovered? textValueColor: textButtonColor - width: parent.width/2 - horizontalAlignment: Text.AlignLeft //HCenter - anchors.left: parent.left - anchors.leftMargin: framePadding - topPadding: framePadding/1.2 - } - Rectangle{ id: colorPreview - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.horizontalCenter - anchors.leftMargin: parent.width/7 - anchors.right: parent.right - anchors.rightMargin: parent.width/10 - height: parent.height/1.4; - color: backgroundColor - border.width: frameWidth - border.color: (color=="white" || color=="#ffffff")? "black": "white" - MouseArea{ - id: dragArea - anchors.fill: parent - onReleased: { - fillColorProp.isPressed = false - } - onClicked: { - fillColorProp.isPressed = false - colorDialog.open() - } - onPressed: { - fillColorProp.isPressed = true - } - } - } - } - - ColorDialog { id: colorDialog - title: "Please pick a BG-Colour" - color: backgroundColor - onAccepted: { - anno_tool_backend_settings.text_background_colour = color - close() - } - onRejected: { - close() - } - } - -} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/ToolSelector.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/ToolSelector.qml deleted file mode 100644 index 27ee86de2..000000000 --- a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsDialog/ToolSelector.qml +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQuick.Window 2.15 -import QtQml 2.15 -import xstudio.qml.bookmarks 1.0 -import QtQml.Models 2.14 -import QtQuick.Dialogs 1.3 //for ColorDialog -import QtGraphicalEffects 1.15 //for RadialGradient - -import xStudio 1.1 -import xstudio.qml.module 1.0 - -Item{ - - anchors.fill: parent - - // note this model only has one item, which is the 'tool type' attribute - // in the backend. - - XsModuleAttributesModel { - id: annotations_tool_types - attributesGroupNames: "annotations_tool_types_0" - } - - property var toolImages: [ - "qrc:///icons/drawing.png", - "qrc:///feather_icons/star.svg", - "qrc:///feather_icons/type.svg", - "qrc:///feather_icons/book.svg" - ] - - // we have to use a repeater to hook the model into the ListView - Repeater { - - id: the_view - anchors.fill: parent - anchors.margins: framePadding - - // by using annotations_tool_types as the 'model' we are exposing the - // attributes in the "annotations_tool_types" group and their role. - // The 'ListView' is instanced for each attribute, and each instance - // can 'see' the attribute role data items (like 'value', 'combo_box_options'). - // In this case, there is only one attribute in the group which tracks - // the 'active tool' selection for the annotations plugin. - model: annotations_tool_types - - ListView{ - - id: toolSelector - - anchors.fill: parent - anchors.margins: framePadding - - spacing: itemSpacing - // clip: true - interactive: false - orientation: ListView.Horizontal - - model: combo_box_options // this is 'role data' from the backend attr - - delegate: toolSelectorDelegate - - // read only convenience binding to backend. - currentIndex: combo_box_options.indexOf(value) - - Component{ - - id: toolSelectorDelegate - - - Rectangle{ - - width: (toolSelector.width-toolSelector.spacing*(combo_box_options.length-1))/combo_box_options.length// - toolSelector.spacing - height: buttonHeight*2 - color: "transparent" - property bool isEnabled: true//index != 2 // Text disabled while WIP - can be enabled to see where it is - enabled: isEnabled - - XsButton{ id: toolBtn - width: parent.width - height: parent.height - text: "" - isActive: toolSelector.currentIndex===index - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - hoverEnabled: isEnabled - - ToolTip { - parent: toolBtn - visible: !isEnabled && toolBtn.down - text: "Text captions coming soon!" - } - - Text{ - id: tText - text: combo_box_options[index] - - font.pixelSize: fontSize - font.family: fontFamily - color: enabled? toolSelector.currentIndex===index || toolBtn.down || toolBtn.hovered || parent.isActive? toolActiveTextColor: toolInactiveTextColor : Qt.darker(toolInactiveTextColor,1.5) - horizontalAlignment: Text.AlignHCenter - - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: framePadding/2 - } - Image { - anchors.bottom: parent.bottom - anchors.bottomMargin: framePadding/2 - width: 20 - height: width - source: toolImages[index] - anchors.horizontalCenter: parent.horizontalCenter - layer { - enabled: true - effect: - ColorOverlay { - color: enabled? (toolSelector.currentIndex===index || toolBtn.down || toolBtn.hovered)? toolActiveTextColor: toolInactiveTextColor : Qt.darker(toolInactiveTextColor,1.5) - } - } - } - - onClicked: { - if (!isEnabled) return; - if(toolSelector.currentIndex == index) - { - //Disables tool by setting the 'value' of the 'active tool' - // attribute in the plugin backend to 'None' - value = "None" - } - else - { - value = tText.text - } - } - - } - - // Rectangle { - // anchors.fill: parent - // visible: !isEnabled - // color: "black" - // opacity: 0.2 - // } - } - } - } - } -} - diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsTextItems.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsTextItems.qml deleted file mode 100644 index 972a5ca8e..000000000 --- a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/AnnotationsTextItems.qml +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.3 -import QtQuick 2.14 -import QuickFuture 1.0 -import QuickPromise 1.0 -import xStudio 1.0 -import xstudio.qml.module 1.0 - -Rectangle { - - id: control - color: "transparent" - width: viewport.width - height: viewport.height - - property var imageBox: viewport.imageBoundaryInViewport - - XsModuleAttributesModel { - - id: text_times - attributesGroupNames: "annotations_text_items_0" - - } - - Repeater { - - id: the_view - anchors.fill: parent - model: text_times - focus: true - - delegate: Item { - - id: parent_item - anchors.centerIn: parent - width: 200 - height: 200 - property var dynamic_widget - - Text { - anchors.fill: parent - text: value - font.pixelSize: 50 - font.family: XsStyle.menuFontFamily - font.hintingPreference: Font.PreferNoHinting - color: attr_colour - onColorChanged: { - console.log("Attr color", color) - } - - } - } - } -} diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/qmldir b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/qmldir deleted file mode 100644 index b5a8ed6cb..000000000 --- a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.1/qmldir +++ /dev/null @@ -1,5 +0,0 @@ -module AnnotationsTool - -AnnotationsButton 1.0 AnnotationsButton.qml -AnnotationsDialog 1.0 AnnotationsDialog/AnnotationsDialog.qml -AnnotationsTextItems 1.0 AnnotationsTextItems.qml diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsColourPresets.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsColourPresets.qml new file mode 100644 index 000000000..fc3238784 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsColourPresets.qml @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects + + +import xstudio.qml.bookmarks 1.0 + + + +import xStudio 1.0 + +Rectangle{ + + color: "transparent" + + property int itemCount: currentColorPresetModel.count + + ListView{ id: presetColours + x: framePadding + width: parent.width - framePadding*2 + height: buttonHeight + anchors.verticalCenter: parent.verticalCenter + spacing: (itemSpacing!==0)?itemSpacing/2: 0 + clip: false + interactive: false + orientation: ListView.Horizontal + + model: currentColorPresetModel + delegate: + Item{ + property bool isMouseHovered: presetMArea.containsMouse + width: index == itemCount-1? presetColours.width/itemCount : presetColours.width/itemCount-presetColours.spacing; + height: presetColours.height + Rectangle { + anchors.centerIn: parent + width: parent.width + height: parent.height + radius: 0 + color: preset + border.width: 1 + border.color: parent.isMouseHovered? palette.highlight: (currentToolColour === preset)? "white": "black" + + MouseArea{ + id: presetMArea + property color temp_color + anchors.fill: parent + hoverEnabled: true + smooth: true + + onClicked: { + temp_color = currentColorPresetModel.get(index).preset; + currentToolColour = temp_color + } + } + + } + } + } + + +} + diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsDrawingTools.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsDrawingTools.qml new file mode 100644 index 000000000..2dc803af6 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsDrawingTools.qml @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Dialogs + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.bookmarks 1.0 + +Item{ + id: drawDialog + + objectName: "XStudioPanel" + + property real buttonWidth: 0 + property real buttonHeight: 20 + property real toolPropLoaderHeight: 0 + property real defaultHeight: toolSelector.height + toolProperties.height + toolActionSection.height + framePadding*3 + property real toolPropertiesWidthThreshold: 200 + + property real colSpacing: buttonHeight + property real itemSpacing: 1 + property real framePadding: XsStyleSheet.panelPadding/2 + property color toolInactiveTextColor: XsStyleSheet.secondaryTextColor + + property real fontSize: XsStyleSheet.fontSize + property string fontFamily: XsStyleSheet.fontFamily + property color textValueColor: "white" + + property int maxDrawSize: 250 + property bool isAnyToolSelected: currentTool !== "None" + + /* This connects to the backend annotations tool object and exposes its + ui data via model data */ + XsModuleData { + id: annotations_model_data + modelDataName: "annotations_tool_settings" + } + + /* Here we locate particular nodes in the annotations_model_data giving + convenient access to backend data. Seems crazy but this is the QML way! */ + XsAttributeValue { + id: draw_pen_size + attributeTitle: "Draw Pen Size" + model: annotations_model_data + } + + XsAttributeValue { + id: shapes_pen_size + attributeTitle: "Shapes Pen Size" + model: annotations_model_data + } + XsAttributeValue { + id: erase_pen_size + attributeTitle: "Erase Pen Size" + model: annotations_model_data + } + XsAttributeValue { + id: text_size + attributeTitle: "Text Size" + model: annotations_model_data + } + XsAttributeValue { + id: pen_colour + attributeTitle: "Pen Colour" + model: annotations_model_data + } + XsAttributeValue { + id: pen_opacity + attributeTitle: "Pen Opacity" + model: annotations_model_data + } + XsAttributeValue { + id: active_tool + attributeTitle: "Active Tool" + model: annotations_model_data + } + XsAttributeValue { + id: text_background_colour + attributeTitle: "Text Background Colour" + model: annotations_model_data + } + XsAttributeValue { + id: text_background_opacity + attributeTitle: "Text Background Opacity" + model: annotations_model_data + } + + // make a local binding to the backend attribute + property alias currentDrawPenSize: draw_pen_size.value + property alias currentShapePenSize: shapes_pen_size.value + property alias currentErasePenSize: erase_pen_size.value + property alias currentTextSize: text_size.value + property alias currentToolColour: pen_colour.value + property alias currentOpacity: pen_opacity.value + property alias currentTool: active_tool.value + property alias backgroundColor: text_background_colour.value + property alias backgroundOpacity: text_background_opacity.value + + property var toolSizeAttrName: "Draw Pen Size" + + onCurrentToolChanged: { + if(currentTool === "Draw" || currentTool === "Laser") + { + currentColorPresetModel = drawColourPresetsModel + toolSizeAttrName = "Draw Pen Size" + } + else if(currentTool === "Erase") + { + currentColorPresetModel = eraseColorPresetModel + toolSizeAttrName = "Erase Pen Size" + + } + else if(currentTool === "Text") + { + currentColorPresetModel = textColourPresetsModel + toolSizeAttrName = "Text Size" + + } + else if(currentTool === "Square" || currentTool === "Circle" || currentTool === "Arrow" || currentTool === "Line") + { + currentColorPresetModel = shapesColourPresetsModel + toolSizeAttrName = "Shapes Pen Size" + + } + } + + + function setPenSize(penSize) { + if(currentTool === "Draw" || currentTool === "Laser") + { + currentDrawPenSize = penSize + } + else if(currentTool === "Erase") + { + currentErasePenSize = penSize + } + else if(currentTool === "Square" || currentTool === "Circle" || currentTool === "Arrow" || currentTool === "Line") + { + currentShapePenSize = penSize + } + else if(currentTool === "Text") + { + currentTextSize = penSize + } + } + + // map the local property for currentToolSize to the backend value ... to modify the tool size, we only change the backend + // value binding + + property ListModel currentColorPresetModel: drawColourPresetsModel + + + XsGradientRectangle{ + anchors.fill: parent + } + + // We wrap all the widgets in a top level Item that can forward keyboard + // events back to the viewport for consistent + Item { + anchors.fill: parent + focus: true + // Keys.forwardTo: [sessionWidget] //#TODO - backend + + XsToolSelector { + id: toolSelector + x: framePadding/2 + width: parent.width - x*2 + height: itemHeight*2 + framePadding*3 + } + + Loader { id: toolProperties + width: toolSelector.width + height: toolPropLoaderHeight + x: toolSelector.x + y: toolSelector.y + toolSelector.height + colSpacing + + sourceComponent: XsToolProperties{ + root: drawDialog + } + + ColorDialog { + id: colorDialog + title: "Please pick a colour" + onAccepted: { + currentToolColour = currentColor + close() + } + onRejected: { + close() + } + onVisibleChanged: { + if (visible) { + color = currentToolColour + } + } + } + ColorDialog { + id: bgColorDialog + title: "Please pick a BG-Colour" + onAccepted: { + backgroundColor = color + close() + } + onRejected: { + close() + } + onVisibleChanged: { + if (visible) { + color = backgroundColor + } + } + + } + + + ListModel{ id: eraseColorPresetModel + ListElement{ + preset: "white" + } + } + ListModel{ id: drawColourPresetsModel + ListElement{ + preset: "#ff0000" //- "red" + } + ListElement{ + preset: "#ffa000" //- "orange" + } + ListElement{ + preset: "#ffff00" //- "yellow" + } + ListElement{ + preset: "#28dc00" //- "green" + } + ListElement{ + preset: "#0050ff" //- "blue" + } + ListElement{ + preset: "#8c00ff" //- "violet" + } + // ListElement{ + // preset: "#ff64ff" //- "pink" + // } + ListElement{ + preset: "#ffffff" //- "white" + } + ListElement{ + preset: "#000000" //- "black" + } + } + ListModel{ id: textColourPresetsModel + ListElement{ + preset: "#ff0000" //- "red" + } + ListElement{ + preset: "#ffa000" //- "orange" + } + ListElement{ + preset: "#ffff00" //- "yellow" + } + ListElement{ + preset: "#28dc00" //- "green" + } + ListElement{ + preset: "#0050ff" //- "blue" + } + ListElement{ + preset: "#8c00ff" //- "violet" + } + ListElement{ + preset: "#ffffff" //- "white" + } + ListElement{ + preset: "#000000" //- "black" + } + } + ListModel{ id: shapesColourPresetsModel + ListElement{ + preset: "#ff0000" //- "red" + } + ListElement{ + preset: "#ffa000" //- "orange" + } + ListElement{ + preset: "#ffff00" //- "yellow" + } + ListElement{ + preset: "#28dc00" //- "green" + } + ListElement{ + preset: "#0050ff" //- "blue" + } + ListElement{ + preset: "#8c00ff" //- "violet" + } + ListElement{ + preset: "#ffffff" //- "white" + } + ListElement{ + preset: "#000000" //- "black" + } + } + + } + + XsToolActions{ id: toolActionSection + x: framePadding + y: !isAnyToolSelected? + toolProperties.y : + toolProperties.y + toolProperties.height + colSpacing + + width: parent.width - framePadding + } + + } + +} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsTextCategories.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsTextCategories.qml new file mode 100644 index 000000000..02f7845ef --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsTextCategories.qml @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick + +import QtQuick.Layouts + +import xstudio.qml.bookmarks 1.0 + +import xStudio 1.0 +import xstudio.qml.models 1.0 + +Item{ id: textCategory + + XsModuleData { + id: anno_font_options + modelDataName: "annotations_tool_fonts" + } + + Repeater { + + // Using a repeater here - but 'vp_mouse_wheel_behaviour_attr' only + // has one row by the way. The use of a repeater means the model role + // data are all visible in the XsComboBox instance. + model: anno_font_options + + XsComboBox { + + id: dropdownFonts + + x: itemSpacing*2 + width: parent.width + height: buttonHeight + model: combo_box_options + anchors.verticalCenter: parent.verticalCenter + + property var value_: value ? value : null + onValue_Changed: { + currentIndex = indexOfValue(value_) + } + + onCurrentValueChanged: { + value = currentValue; + } + + Component.onCompleted: currentIndex = indexOfValue(value_) + } + } + + +} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsToolActions.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsToolActions.qml new file mode 100644 index 000000000..2431989eb --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsToolActions.qml @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + +import QtQuick.Layouts + +import xstudio.qml.bookmarks 1.0 + + + +import xStudio 1.0 +import xstudio.qml.models 1.0 + +Item{ + + XsAttributeValue { + id: __action_attr + attributeTitle: "action_attribute" + model: annotations_model_data + } + + property alias action_attribute: __action_attr.value + + ListView{ id: toolActionUndoRedo + + width: parent.width - framePadding*2 + height: buttonHeight + x: framePadding + y: framePadding + spacing/2 + + spacing: itemSpacing + interactive: false + orientation: ListView.Horizontal + + model: ListModel{ + id: modelUndoRedo + ListElement{ + action: "Undo" + } + ListElement{ + action: "Redo" + } + } + + delegate: XsPrimaryButton{ + text: model.action + width: toolActionUndoRedo.width/modelUndoRedo.count - toolActionUndoRedo.spacing + height: buttonHeight + + onClicked: { + action_attribute = text + } + } + } + + ListView{ id: toolActionCopyPasteClear + + width: parent.width - framePadding*2 + height: buttonHeight + x: framePadding //+ spacing/2 + y: toolActionUndoRedo.y + toolActionUndoRedo.height + spacing + + spacing: itemSpacing + interactive: false + orientation: ListView.Horizontal + + model: + ListModel{ + id: modelCopyPasteClear + ListElement{ action: "Clear" } + } + + delegate: + XsPrimaryButton{ + text: model.action + width: toolActionCopyPasteClear.width/modelCopyPasteClear.count - toolActionCopyPasteClear.spacing + height: buttonHeight + enabled: text == "Clear" + + onClicked: { + action_attribute = text + } + + } + } + + Rectangle{ id: displayAnnotations + width: parent.width - framePadding + height: buttonHeight; + color: "transparent"; + anchors.top: toolActionCopyPasteClear.bottom + anchors.topMargin: colSpacing + anchors.horizontalCenter: parent.horizontalCenter + + Text{ + text: "Display Annotations" + font.pixelSize: fontSize + font.family: fontFamily + color: toolInactiveTextColor + width: parent.width + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: framePadding + } + } + + // Sigh - hooking up the draw mode backen attr to the combo box here + // is horrible! Need something better than this! + XsModuleData { + id: annotations_tool_draw_mode_model + modelDataName: "annotations_tool_draw_mode" + } + + XsAttributeValue { + id: __display_mode + attributeTitle: "Display Mode" + model: annotations_tool_draw_mode_model + } + property alias display_mode: __display_mode.value + + XsAttributeValue { + id: __display_mode_options + attributeTitle: "Display Mode" + model: annotations_tool_draw_mode_model + role: "combo_box_options" + } + property alias display_mode_options: __display_mode_options.value + + XsComboBox { + + id: dropdownAnnotations + + model: display_mode_options + width: parent.width/1.3; + height: buttonHeight + anchors.top: displayAnnotations.bottom + // anchors.topMargin: itemSpacing + anchors.horizontalCenter: parent.horizontalCenter + onCurrentTextChanged: { + if (currentText != display_mode && display_mode != undefined) { + display_mode = currentText + } + } + property var displayModeValue: display_mode + onDisplayModeValueChanged: { + + if (display_mode_options.indexOf(display_mode) != -1) { + currentIndex = display_mode_options.indexOf(display_mode) + } + } + + } + +} + diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsToolProperties.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsToolProperties.qml new file mode 100644 index 000000000..c24dd0d8c --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsToolProperties.qml @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Dialogs + +import QtQml 2.15 +import xstudio.qml.bookmarks 1.0 + + + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 + +Item{ + id: toolProperties + property var root + + Rectangle{ id: row1_categories + width: (toolProperties.width - framePadding*2) + height: visible? buttonHeight + itemSpacing : 0 + + color: "transparent"; + visible: (currentTool == "Text") + + XsTextCategories { + id: textCategories + anchors.fill: parent + visible: (currentTool === "Text") + } + } + + XsModuleData { + id: annotations_model_data + modelDataName: "annotations_tool_settings" + } + Connections { + target: annotations_model_data // this bubbles up from XsSessionWindow + function onJsonChanged() { + setPropertyIndeces() + } + } + + function setPropertyIndeces() { + draw_pen_size.index = annotations_model_data.searchRecursive("Draw Pen Size", "title") + } + + /* Here we locate particular nodes in the annotations_model_data giving + convenient access to backend data. Seems crazy but this is the QML way! */ + XsModelProperty { + id: draw_pen_size + role: "value" + } + + property alias drawPenSize: draw_pen_size.value + + Grid{id: row2_controls + x: framePadding + y: row1_categories.y + row1_categories.height + itemSpacing + z: 1 + + columns: isDividedToCols? 2 : 1 + spacing: itemSpacing + flow: Grid.TopToBottom + width: (toolProperties.width - framePadding*2) + + property bool isDividedToCols: width>toolPropertiesWidthThreshold + property real gridItemWidth: isDividedToCols? width/2 : width + + XsIntegerAttrControl { + id: sizeProp + visible: enabled + width: row2_controls.gridItemWidth + height: visible? buttonHeight : 0 + text: (currentTool=="Square" || currentTool === "Circle" || currentTool === "Arrow" || currentTool === "Line")? "Width" : "Size" + enabled: isAnyToolSelected + attr_group_model: annotations_model_data + attr_title: toolSizeAttrName + } + + XsIntegerAttrControl{ + id: opacityProp + visible: isAnyToolSelected + width: row2_controls.gridItemWidth + height: visible? buttonHeight : 0 + text: "Opacity" + attr_group_model: annotations_model_data + attr_title: "Pen Opacity" + enabled: isAnyToolSelected && currentTool != "Erase" + } + + XsIntegerAttrControl { + id: bgOpacityProp + visible: currentTool === "Text" + width: row2_controls.gridItemWidth + height: visible? buttonHeight : 0 + text: "BG Opacity" + attr_group_model: annotations_model_data + attr_title: "Text Background Opacity" + } + + XsViewerMenuButton{ id: colorProp + visible: enabled + width: row2_controls.gridItemWidth + height: visible? buttonHeight : 0 + text: "Colour " + shortText: "Col " + enabled: (isAnyToolSelected && currentTool !== "Erase") + showBorder: isMouseHovered + isActive: isPressed + + property bool isPressed: false + property bool isMouseHovered: colorMArea.containsMouse + + MouseArea{ + id: colorMArea + propagateComposedEvents: true + hoverEnabled: true + anchors.fill: parent + onClicked: { + parent.isPressed = false + colorDialog.open() + } + onPressed: { + parent.isPressed = true + } + onReleased: { + parent.isPressed = false + } + } + Rectangle{ id: colorPreviewDuplicate + // opacity: (!isAnyToolSelected || currentTool === "Erase")? (parent.enabled?1:0.5): 0 + color: currentTool === "Erase" ? "white" : parent.enabled ? currentToolColour ? currentToolColour : "grey" : "grey" + border.width: 1 + border.color: parent.enabled? "black" : "dark grey" + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.horizontalCenter + anchors.leftMargin: parent.width/8 + anchors.right: parent.right + anchors.rightMargin: parent.width/10 + height: buttonHeight/1.4; + } + } + + + // XsViewerMenuButton{ id: bgColorProp + // visible: currentTool === "Text" + // width: row2_controls.gridItemWidth + // height: visible? buttonHeight : 0 + + // text: "BG Colour " + // shortText: "BG Col. " + // enabled: (isAnyToolSelected && currentTool !== "Erase") + // //forceHover: isMouseHovered + + // isActive: isPressed + // property bool isPressed: false + // property bool isMouseHovered: bgColorMArea.containsMouse + + // Rectangle{ id: bgColorPreview + // anchors.verticalCenter: parent.verticalCenter + // anchors.left: parent.horizontalCenter + // anchors.leftMargin: parent.width/8 + // anchors.right: parent.right + // anchors.rightMargin: parent.width/10 + // height: parent.height/1.4; + // color: backgroundColor ? backgroundColor : "grey" + // border.width: 1 + // border.color: "black" + // } + + // MouseArea{ + // id: bgColorMArea + // propagateComposedEvents: true + // hoverEnabled: true + // anchors.fill: parent + // onClicked: { + // parent.isPressed = false + // colorDialog.title = "Please pick a colour" + // bgColorDialog.open() + // } + // onPressed: { + // parent.isPressed = true + // } + // onReleased: { + // parent.isPressed = false + // } + // } + + // } + + } + + XsColourPresets{ id: row3_colourpresets + y: row2_controls.y + row2_controls.height + itemSpacing*1.5 + onYChanged: { + toolPropLoaderHeight = row3_colourpresets.y + row3_colourpresets.height + } + + visible: (isAnyToolSelected && currentTool !== "Erase") + width: toolProperties.width + height: visible? buttonHeight : 0 + onHeightChanged: { + toolPropLoaderHeight = row3_colourpresets.y + row3_colourpresets.height + } + } + + + Component.onCompleted: { + toolPropLoaderHeight = row3_colourpresets.y + row3_colourpresets.height + setPropertyIndeces() + } + +} + diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsToolSelector.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsToolSelector.qml new file mode 100644 index 000000000..d4bd1d82c --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/XsToolSelector.qml @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + +import QtQuick.Layouts + +import xstudio.qml.bookmarks 1.0 + + + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 + +Item{ + id: toolSetBg + + property int rowItemCount: 4 //5 + property real itemWidth: (width-framePadding*2)/rowItemCount + onItemWidthChanged: { + buttonWidth = itemWidth + } + property real itemHeight: XsStyleSheet.primaryButtonStdHeight + + /* This connects to the backend annotations tool object and exposes its + ui data via model data */ + XsModuleData { + id: annotations_tool_types + modelDataName: "annotations_tool_settings" + } + + XsAttributeValue { + id: tool_types_value + attributeTitle: "Active Tool" + model: annotations_tool_types + } + XsAttributeValue { + id: tool_types_choices + role: "combo_box_options" + attributeTitle: "Active Tool" + model: annotations_tool_types + } + + property alias current_tool: tool_types_value.value + + property alias tool_choices: tool_types_choices.value + + property var toolImages: [ + "qrc:///anno_icons/draw_brush.svg", + "qrc:///anno_icons/draw_laser.svg", + "qrc:///anno_icons/draw_shape_square.svg", + "qrc:///anno_icons/draw_shape_circle.svg", + "qrc:///anno_icons/draw_shape_arrow.svg", + "qrc:///anno_icons/draw_shape_line.svg", + "qrc:///anno_icons/draw_text.svg", + "qrc:///anno_icons/draw_eraser.svg" + ] + + GridView{ + + id: toolSet + + x: framePadding + y: framePadding + + width: itemWidth*rowItemCount + height: itemHeight * 2 + + cellWidth: itemWidth + cellHeight: itemHeight + + interactive: false + flow: GridView.FlowLeftToRight + + // read only convenience binding to backend. + currentIndex: tool_choices ? tool_choices.indexOf(current_tool) : undefined + + model: tool_choices // this is 'role data' from the backend attr + + delegate: toolSetDelegate + + Component{ + + id: toolSetDelegate + + Rectangle{ + width: toolSet.cellWidth + height: toolSet.cellHeight + color: "transparent" + + XsPrimaryButton{ id: toolBtn + + // x: index>=rowItemCount? width/2:0 + width: parent.width - itemSpacing //index==(rowItemCount-1)? parent.width : parent.width - itemSpacing + height: parent.height - itemSpacing + + clip: true + isToolTipEnabled: false + isActive: current_tool===text + anchors.top: parent.top + + text: tool_choices[index] + imgSrc: toolImages[index] + + onClicked: { + if(isActive) + { + //Disables tool by setting the 'value' of the 'active tool' + // attribute in the plugin backend to 'None' + current_tool = "None" + } + else + { + current_tool = text + } + } + + } + + } + } + + } + +} + diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/anno_tools.qrc b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/anno_tools.qrc new file mode 100644 index 000000000..719bc1cb6 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/anno_tools.qrc @@ -0,0 +1,22 @@ + + + icons/brush.svg + icons/stylus_laser_pointer.svg + icons/title.svg + icons/ink_eraser.svg + icons/star_rate.svg + icons/shapes.svg + icons/arrow_forward.svg + icons/circle.svg + icons/crop_square.svg + icons/horizontal_rule.svg + icons/colorize.svg + icons/check.svg + icons/undo.svg + icons/redo.svg + icons/delete.svg + icons/check_circle.svg + icons/pause_circle.svg + icons/play_circle.svg + + \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsColourPresetsLR.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsColourPresetsLR.qml new file mode 100644 index 000000000..63f44e141 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsColourPresetsLR.qml @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects + +import xstudio.qml.bookmarks 1.0 + + + +import xStudio 1.0 + +Rectangle{ + + color: "transparent" + + property int itemCount: currentColorPresetModel.count + + GridView{ id: presetColours + x: 0 + width: parent.width - x*2 + height: buttonHeight*2 + clip: false + interactive: false + + + flow: GridView.FlowLeftToRight + cellWidth: width/4 + cellHeight: buttonHeight + + model: currentColorPresetModel + delegate: + Item{ + property bool isMouseHovered: presetMArea.containsMouse + width: presetColours.cellWidth + height: presetColours.cellHeight + + Rectangle { + anchors.centerIn: parent + width: parent.width + height: parent.height + radius: 0 + color: preset + + MouseArea{ + id: presetMArea + property color temp_color + anchors.fill: parent + hoverEnabled: true + smooth: true + + onClicked: { + temp_color = currentColorPresetModel.get(index).preset; + currentToolColour = temp_color + } + } + + } + } + } + + +} + diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsDrawingToolsLR.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsDrawingToolsLR.qml new file mode 100644 index 000000000..0fc497eab --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsDrawingToolsLR.qml @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic +import QtQuick.Dialogs + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.bookmarks 1.0 + +Item { + + id: drawDialog + + objectName: "XStudioPanel" + anchors.fill: parent + + // this var is REQUIRED to set the vertical size of the widget + property var dockWidgetSize: 90 + + property real buttonWidth: 0 + property real buttonHeight: 20 + property real toolPropLoaderHeight: 0 + property real defaultHeight: toolSelector.height + toolProperties.height + toolActionSection.height + framePadding*3 + property real toolPropertiesWidthThreshold: 200 + + property real colSpacing: buttonHeight + property real itemSpacing: 1 + property real framePadding: XsStyleSheet.panelPadding/2 + property color toolInactiveTextColor: XsStyleSheet.secondaryTextColor + + property real fontSize: XsStyleSheet.fontSize + property string fontFamily: XsStyleSheet.fontFamily + property color textValueColor: "white" + + property int maxDrawSize: 250 + property bool isAnyToolSelected: currentTool != "None" + + /* This connects to the backend annotations tool object and exposes its + ui data via model data */ + XsModuleData { + id: annotations_model_data + modelDataName: "annotations_tool_settings" + } + + /* Here we locate particular nodes in the annotations_model_data giving + convenient access to backend data. Seems crazy but this is the QML way! */ + XsAttributeValue { + id: draw_pen_size + attributeTitle: "Draw Pen Size" + model: annotations_model_data + } + + XsAttributeValue { + id: shapes_pen_size + attributeTitle: "Shapes Pen Size" + model: annotations_model_data + } + XsAttributeValue { + id: erase_pen_size + attributeTitle: "Erase Pen Size" + model: annotations_model_data + } + XsAttributeValue { + id: text_size + attributeTitle: "Text Size" + model: annotations_model_data + } + XsAttributeValue { + id: pen_colour + attributeTitle: "Pen Colour" + model: annotations_model_data + } + XsAttributeValue { + id: pen_opacity + attributeTitle: "Pen Opacity" + model: annotations_model_data + } + XsAttributeValue { + id: active_tool + attributeTitle: "Active Tool" + model: annotations_model_data + } + XsAttributeValue { + id: text_background_colour + attributeTitle: "Text Background Colour" + model: annotations_model_data + } + XsAttributeValue { + id: text_background_opacity + attributeTitle: "Text Background Opacity" + model: annotations_model_data + } + + // make a local binding to the backend attribute + property alias currentDrawPenSize: draw_pen_size.value + property alias currentShapePenSize: shapes_pen_size.value + property alias currentErasePenSize: erase_pen_size.value + property alias currentTextSize: text_size.value + property alias currentToolColour: pen_colour.value + property alias currentOpacity: pen_opacity.value + property alias currentTool: active_tool.value + property alias backgroundColor: text_background_colour.value + property alias backgroundOpacity: text_background_opacity.value + + property var toolSizeAttrName: "Draw Pen Size" + + onCurrentToolChanged: { + if(currentTool === "Draw" || currentTool === "Laser") + { + currentColorPresetModel = drawColourPresetsModel + toolSizeAttrName = "Draw Pen Size" + } + else if(currentTool === "Erase") + { + currentColorPresetModel = eraseColorPresetModel + toolSizeAttrName = "Erase Pen Size" + + } + else if(currentTool === "Text") + { + currentColorPresetModel = textColourPresetsModel + toolSizeAttrName = "Text Size" + + } + else if(currentTool === "Square" || currentTool === "Circle" || currentTool === "Arrow" || currentTool === "Line") + { + currentColorPresetModel = shapesColourPresetsModel + toolSizeAttrName = "Shapes Pen Size" + + } + } + + + function setPenSize(penSize) { + if(currentTool === "Draw" || currentTool === "Laser") + { + currentDrawPenSize = penSize + } + else if(currentTool === "Erase") + { + currentErasePenSize = penSize + } + else if(currentTool === "Square" || currentTool === "Circle" || currentTool === "Arrow" || currentTool === "Line") + { + currentShapePenSize = penSize + } + else if(currentTool === "Text") + { + currentTextSize = penSize + } + } + + // map the local property for currentToolSize to the backend value ... to modify the tool size, we only change the backend + // value binding + + property ListModel currentColorPresetModel: drawColourPresetsModel + + XsGradientRectangle{ + anchors.fill: parent + } + + // We wrap all the widgets in a top level Item that can forward keyboard + // events back to the viewport for consistent + ColumnLayout { + anchors.fill: parent + spacing: 0 + + XsToolSelectorLR { + id: toolSelector + x: framePadding/2 + z: 1 + Layout.preferredWidth: parent.width - x*2 + Layout.preferredHeight: toolSet.height + framePadding + } + + Item{ + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 0 + Layout.preferredHeight: !isAnyToolSelected? 0 : XsStyleSheet.primaryButtonStdHeight + Layout.maximumHeight: !isAnyToolSelected? 0 : XsStyleSheet.primaryButtonStdHeight + } + Item{ id: toolProperties + Layout.preferredWidth: parent.width - x*2 + Layout.preferredHeight: !isAnyToolSelected? 0 : toolPropLoaderHeight + Layout.maximumHeight: !isAnyToolSelected? 0 : toolPropLoaderHeight + x: framePadding/2 + + Loader { + anchors.fill: parent + sourceComponent: XsToolPropertiesLR{ + root: drawDialog + } + } + + ColorDialog { + id: colorDialog + title: "Please pick a colour" + onAccepted: { + currentToolColour = colorDialog.currentColor + close() + } + onRejected: { + close() + } + onVisibleChanged: { + if (visible) { + colorDialog.color = currentToolColour + } + } + } + + ListModel{ id: eraseColorPresetModel + ListElement{ + preset: "white" + } + } + ListModel{ id: drawColourPresetsModel + ListElement{ + preset: "#ff0000" //- "red" + } + ListElement{ + preset: "#ffa000" //- "orange" + } + ListElement{ + preset: "#ffff00" //- "yellow" + } + ListElement{ + preset: "#28dc00" //- "green" + } + ListElement{ + preset: "#0050ff" //- "blue" + } + ListElement{ + preset: "#8c00ff" //- "violet" + } + // ListElement{ + // preset: "#ff64ff" //- "pink" + // } + ListElement{ + preset: "#ffffff" //- "white" + } + ListElement{ + preset: "#000000" //- "black" + } + } + ListModel{ id: textColourPresetsModel + ListElement{ + preset: "#ff0000" //- "red" + } + ListElement{ + preset: "#ffa000" //- "orange" + } + ListElement{ + preset: "#ffff00" //- "yellow" + } + ListElement{ + preset: "#28dc00" //- "green" + } + ListElement{ + preset: "#0050ff" //- "blue" + } + ListElement{ + preset: "#8c00ff" //- "violet" + } + ListElement{ + preset: "#ffffff" //- "white" + } + ListElement{ + preset: "#000000" //- "black" + } + } + ListModel{ id: shapesColourPresetsModel + ListElement{ + preset: "#ff0000" //- "red" + } + ListElement{ + preset: "#ffa000" //- "orange" + } + ListElement{ + preset: "#ffff00" //- "yellow" + } + ListElement{ + preset: "#28dc00" //- "green" + } + ListElement{ + preset: "#0050ff" //- "blue" + } + ListElement{ + preset: "#8c00ff" //- "violet" + } + ListElement{ + preset: "#ffffff" //- "white" + } + ListElement{ + preset: "#000000" //- "black" + } + } + } + + Item{ + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 0 + Layout.preferredHeight: XsStyleSheet.primaryButtonStdHeight + Layout.maximumHeight: XsStyleSheet.primaryButtonStdHeight + } + XsToolActionsLR{ id: toolActionSection + x: framePadding*2 + Layout.preferredWidth: parent.width - x*2 + Layout.preferredHeight: toolActionUndoRedo.height + framePadding/2 + } + + Item{ + Layout.fillWidth: true + Layout.fillHeight: true + } + + XsToolDisplayLR{ id: toolDispOptions + x: framePadding*2 + Layout.preferredWidth: parent.width - x*2 + Layout.preferredHeight: displayBtn.y + displayBtn.height + framePadding/2 + } + + Item{ + Layout.fillWidth: true + Layout.preferredHeight: XsStyleSheet.primaryButtonStdHeight + } + } + +} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsTextCategoriesLR.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsTextCategoriesLR.qml new file mode 100644 index 000000000..02f7845ef --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsTextCategoriesLR.qml @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick + +import QtQuick.Layouts + +import xstudio.qml.bookmarks 1.0 + +import xStudio 1.0 +import xstudio.qml.models 1.0 + +Item{ id: textCategory + + XsModuleData { + id: anno_font_options + modelDataName: "annotations_tool_fonts" + } + + Repeater { + + // Using a repeater here - but 'vp_mouse_wheel_behaviour_attr' only + // has one row by the way. The use of a repeater means the model role + // data are all visible in the XsComboBox instance. + model: anno_font_options + + XsComboBox { + + id: dropdownFonts + + x: itemSpacing*2 + width: parent.width + height: buttonHeight + model: combo_box_options + anchors.verticalCenter: parent.verticalCenter + + property var value_: value ? value : null + onValue_Changed: { + currentIndex = indexOfValue(value_) + } + + onCurrentValueChanged: { + value = currentValue; + } + + Component.onCompleted: currentIndex = indexOfValue(value_) + } + } + + +} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolActionsLR.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolActionsLR.qml new file mode 100644 index 000000000..c91d0ec1f --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolActionsLR.qml @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + +import QtQuick.Layouts + +import xstudio.qml.bookmarks 1.0 + + + +import xStudio 1.0 +import xstudio.qml.models 1.0 + +Item{ id: toolActionsDiv + + property alias toolActionUndoRedo: toolActionUndoRedo + + XsAttributeValue { + id: __action_attr + attributeTitle: "action_attribute" + model: annotations_model_data + } + + property alias action_attribute: __action_attr.value + + ListView{ id: toolActionUndoRedo + + x: framePadding + width: parent.width - x*2 + height: XsStyleSheet.primaryButtonStdHeight + + spacing: itemSpacing + interactive: false + orientation: ListView.Horizontal + + model: ListModel{ + id: modelUndoRedo + ListElement{ + action: "Undo" + icon: "qrc:///anno_icons/undo.svg" + } + ListElement{ + action: "Redo" + icon: "qrc:///anno_icons/redo.svg" + } + ListElement{ + action: "Clear" + icon: "qrc:///anno_icons/delete.svg" + } + } + + delegate: XsPrimaryButton{ + text: "" //model.action + imgSrc: model.icon + width: toolActionUndoRedo.width/modelUndoRedo.count - toolActionUndoRedo.spacing + height: toolActionUndoRedo.height + + onClicked: { + action_attribute = model.action + } + } + + } + +} + diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolDisplayLR.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolDisplayLR.qml new file mode 100644 index 000000000..61e9dca15 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolDisplayLR.qml @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + +import QtQuick.Layouts + +import xstudio.qml.bookmarks 1.0 + + + +import xStudio 1.0 +import xstudio.qml.models 1.0 + +Item{ id: toolActionsDiv + + property alias displayBtn: displayBtn + + Rectangle{ id: dispText + x: framePadding + width: parent.width - x*2 + height: XsStyleSheet.primaryButtonStdHeight/1.5 + color: "transparent" + + Text{ + text: "Display" + font.pixelSize: fontSize + font.family: fontFamily + color: toolInactiveTextColor + width: parent.width + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: framePadding + } + } + + // Sigh - hooking up the draw mode backen attr to the combo box here + // is horrible! Need something better than this! + XsModuleData { + id: annotations_tool_draw_mode_model + modelDataName: "annotations_tool_draw_mode" + } + + XsAttributeValue { + id: __display_mode + attributeTitle: "Display Mode" + model: annotations_tool_draw_mode_model + } + property alias display_mode: __display_mode.value + + XsButtonWithImageAndText{ id: displayBtn + iconText: display_mode.substring(display_mode.length-6, display_mode.length) + x: framePadding + width: parent.width- x*2 + height: XsStyleSheet.primaryButtonStdHeight + anchors.top: dispText.bottom + iconSrc: "qrc:///anno_icons/pause_circle.svg" + textDiv.visible: true + textDiv.font.bold: false + textDiv.font.pixelSize: XsStyleSheet.fontSize + paddingSpace: 4 + + onClicked:{ + if(displayMenu.visible) displayMenu.visible = false + else{ + displayMenu.x = x + displayMenu.y = y + height + displayMenu.visible = true + } + } + + } + + XsPopupMenu { + id: displayMenu + visible: false + menu_model_name: "displayMenu" + toolActionsDiv + } + XsMenuModelItem { + text: "Always" + menuPath: "" + menuCustomIcon: "qrc:///anno_icons/check_circle.svg" + menuItemPosition: 1 + menuModelName: displayMenu.menu_model_name + onActivated: { + display_mode = "Always" + displayBtn.iconSrc = menuCustomIcon + } + } + XsMenuModelItem { + text: "When Paused" + menuPath: "" + menuCustomIcon: "qrc:///anno_icons/pause_circle.svg" + menuItemPosition: 2 + menuModelName: displayMenu.menu_model_name + onActivated: { + display_mode = "Only When Paused" + displayBtn.iconSrc = menuCustomIcon + } + } + + +} + diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolPropertiesLR.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolPropertiesLR.qml new file mode 100644 index 000000000..289f62ae8 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolPropertiesLR.qml @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Dialogs + +import QtQml 2.15 + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 +import xstudio.qml.bookmarks 1.0 + +Item{ + id: toolProperties + property var root + + Rectangle{ id: row1_categories + width: toolProperties.width - framePadding*2 + height: visible? buttonHeight + itemSpacing*2 : 0 + + color: "transparent"; + visible: (currentTool == "Text") + + XsTextCategoriesLR { + id: textCategories + anchors.fill: parent + visible: (currentTool === "Text") + } + } + + XsModuleData { + id: annotations_model_data + modelDataName: "annotations_tool_settings" + } + Connections { + target: annotations_model_data // this bubbles up from XsSessionWindow + function onJsonChanged() { + setPropertyIndeces() + } + } + + function setPropertyIndeces() { + draw_pen_size.index = annotations_model_data.searchRecursive("Draw Pen Size", "title") + } + + /* Here we locate particular nodes in the annotations_model_data giving + convenient access to backend data. Seems crazy but this is the QML way! */ + XsModelProperty { + id: draw_pen_size + role: "value" + } + + property alias drawPenSize: draw_pen_size.value + + Grid{id: row2_controls + x: framePadding + y: row1_categories.visible? row1_categories.y + row1_categories.height : row1_categories.y + z: 1 + + columns: isDividedToCols? 2 : 1 + spacing: itemSpacing + flow: Grid.TopToBottom + width: (toolProperties.width - x*2) + + property bool isDividedToCols: width>toolPropertiesWidthThreshold + property real gridItemWidth: isDividedToCols? width/2 : width + + XsIntegerAttrControl { + id: sizeProp + visible: enabled + width: row2_controls.gridItemWidth + height: visible? buttonHeight : 0 + text: (currentTool=="Square" || currentTool === "Circle" || currentTool === "Arrow" || currentTool === "Line")? "Width" : "Size" + enabled: isAnyToolSelected + attr_group_model: annotations_model_data + attr_title: toolSizeAttrName + } + + XsIntegerAttrControl{ + id: opacityProp + visible: isAnyToolSelected + width: row2_controls.gridItemWidth + height: visible? buttonHeight : 0 + text: "Opacity" + attr_group_model: annotations_model_data + attr_title: "Pen Opacity" + enabled: isAnyToolSelected && currentTool != "Erase" + } + + XsIntegerAttrControl { + id: bgOpacityProp + visible: currentTool === "Text" + width: row2_controls.gridItemWidth + height: visible? buttonHeight : 0 + text: "BG Opa." + attr_group_model: annotations_model_data + attr_title: "Text Background Opacity" + } + + XsViewerMenuButton{ id: colorProp + visible: enabled + width: row2_controls.gridItemWidth + height: visible? buttonHeight : 0 + text: "Colour " + shortText: "Col " + enabled: (isAnyToolSelected && currentTool !== "Erase") + showBorder: isMouseHovered + isActive: isPressed + + property bool isPressed: false + property bool isMouseHovered: colorMArea.containsMouse + + MouseArea{ + id: colorMArea + propagateComposedEvents: true + hoverEnabled: true + anchors.fill: parent + onClicked: { + parent.isPressed = false + colorDialog.open() + } + onPressed: { + parent.isPressed = true + } + onReleased: { + parent.isPressed = false + } + } + Item{id:centerItem; anchors.centerIn: parent} + Rectangle{ id: colorPreviewDuplicate + // opacity: (!isAnyToolSelected || currentTool === "Erase")? (parent.enabled?1:0.5): 0 + color: currentTool === "Erase" ? "white" : parent.enabled ? currentToolColour ? currentToolColour : "grey" : "grey" + border.width: 1 + border.color: parent.enabled? "black" : "dark grey" + anchors.left: centerItem.horizontalCenter + anchors.leftMargin: 18 + anchors.right: parent.right + anchors.rightMargin: 3 + height: buttonHeight/1.4; + anchors.verticalCenter: centerItem.verticalCenter + } + } + + } + + XsColourPresetsLR{ id: row3_colourpresets + x: framePadding + y: row2_controls.y + row2_controls.height + itemSpacing*1.5 + onYChanged: { + toolPropLoaderHeight = row3_colourpresets.y + row3_colourpresets.height + } + + visible: (isAnyToolSelected && currentTool !== "Erase") + width: toolProperties.width- framePadding*2 + height: visible? buttonHeight*2 + framePadding : 0 + onHeightChanged: { + toolPropLoaderHeight = row3_colourpresets.y + row3_colourpresets.height + } + } + + + Component.onCompleted: { + toolPropLoaderHeight = row3_colourpresets.y + row3_colourpresets.height + setPropertyIndeces() + } + +} + diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolSelectorLR.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolSelectorLR.qml new file mode 100644 index 000000000..d617dd54b --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedLR/XsToolSelectorLR.qml @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + +import QtQuick.Layouts + +import xstudio.qml.bookmarks 1.0 + + + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 + +Item{ + id: toolSetBg + + property int colItemCount: 4 + property int rowItemCount: 2 + + property alias toolSet: toolSet + + property real itemWidth: (width-framePadding*2)/rowItemCount + onItemWidthChanged: { + buttonWidth = itemWidth + } + property real itemHeight: XsStyleSheet.primaryButtonStdHeight + + /* This connects to the backend annotations tool object and exposes its + ui data via model data */ + XsModuleData { + id: annotations_tool_types + modelDataName: "annotations_tool_settings" + } + + XsAttributeValue { + id: tool_types_value + attributeTitle: "Active Tool" + model: annotations_tool_types + } + XsAttributeValue { + id: tool_types_choices + role: "combo_box_options" + attributeTitle: "Active Tool" + model: annotations_tool_types + } + + property alias current_tool: tool_types_value.value + + // Un-comment this when Laser is implemented in combo_box_options + property alias tool_choices: tool_types_choices.value + + property var toolImages: [ + "qrc:///anno_icons/draw_brush.svg", + "qrc:///anno_icons/draw_laser.svg", + "qrc:///anno_icons/draw_shape_square.svg", + "qrc:///anno_icons/draw_shape_circle.svg", + "qrc:///anno_icons/draw_shape_arrow.svg", + "qrc:///anno_icons/draw_shape_line.svg", + "qrc:///anno_icons/draw_text.svg", + "qrc:///anno_icons/draw_eraser.svg" + ] + + GridView{ + id: toolSet + + x: framePadding + y: framePadding + + width: itemWidth*rowItemCount + height: itemHeight*colItemCount + + cellWidth: itemWidth + cellHeight: itemHeight + + interactive: false + flow: GridView.FlowLeftToRight + + // read only convenience binding to backend. + currentIndex: tool_choices ? tool_choices.indexOf(current_tool) : undefined + + model: tool_choices // this is 'role data' from the backend attr + + delegate: toolSetDelegate + + Component{ + id: toolSetDelegate + + Rectangle{ + width: toolSet.cellWidth + height: toolSet.cellHeight + color: "transparent" + + XsPrimaryButton{ id: toolBtn + + width: parent.width - itemSpacing //index == toolImages.length-1? parent.width*2 : parent.width - itemSpacing + height: parent.height - itemSpacing + + clip: true + isToolTipEnabled: false + + isActive: current_tool===text + anchors.top: parent.top + + text: tool_choices[index] + imgSrc: toolImages[index] + + onClicked: { + if(isActive) + { + //Disables tool by setting the 'value' of the 'active tool' + // attribute in the plugin backend to 'None' + current_tool = "None" + } + else + { + current_tool = text + } + } + + } + + } + } + + } + +} + diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsColourPresetsTB.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsColourPresetsTB.qml new file mode 100644 index 000000000..14f35f345 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsColourPresetsTB.qml @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects + +import xstudio.qml.bookmarks 1.0 + + + +import xStudio 1.0 + +Rectangle{ + + color: "transparent" + + property int itemCount: currentColorPresetModel.count + + GridView{ id: presetColours + x: framePadding + width: parent.width - x*2 + height: parent.height + anchors.verticalCenter: parent.verticalCenter + clip: false + interactive: false + + + flow: GridView.FlowLeftToRight + cellWidth: width / (model.count/2) + cellHeight: height / 2 + + model: currentColorPresetModel + delegate: + Item{ + property bool isMouseHovered: presetMArea.containsMouse + width: presetColours.cellWidth + height: presetColours.cellHeight + Rectangle { + anchors.centerIn: parent + width: parent.width + height: parent.height + radius: 0 + color: preset + + MouseArea{ + id: presetMArea + property color temp_color + anchors.fill: parent + hoverEnabled: true + smooth: true + + onClicked: { + temp_color = currentColorPresetModel.get(index).preset; + currentToolColour = temp_color + } + } + } + } + } + + +} + diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsDrawingToolsTB.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsDrawingToolsTB.qml new file mode 100644 index 000000000..9aa302f85 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsDrawingToolsTB.qml @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick.Dialogs + +import xstudio.qml.bookmarks 1.0 + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 + +Item { + + id: drawDialog + + objectName: "XStudioPanel" + anchors.fill: parent + + // this var is REQUIRED to set the vertical size of the widget + property var dockWidgetSize: XsStyleSheet.primaryButtonStdHeight + framePadding*2 + + + XsGradientRectangle{ + anchors.fill: parent + } + + property real buttonHeight: 20 + property real toolPropLoaderWidth: 0 + property real defaultHeight: toolSelector.height + toolProperties.height + toolActionSection.height + framePadding*3 + property real toolPropertiesWidthThreshold: 200 + + property real colSpacing: buttonHeight + property real itemSpacing: 1 + property real framePadding: XsStyleSheet.panelPadding/2 + property color toolInactiveTextColor: XsStyleSheet.secondaryTextColor + + property real fontSize: XsStyleSheet.fontSize + property string fontFamily: XsStyleSheet.fontFamily + property color textValueColor: "white" + + property int maxDrawSize: 250 + property bool isAnyToolSelected: currentTool !== "None" + + /* This connects to the backend annotations tool object and exposes its + ui data via model data */ + XsModuleData { + id: annotations_model_data + modelDataName: "annotations_tool_settings" + } + + /* Here we locate particular nodes in the annotations_model_data giving + convenient access to backend data. Seems crazy but this is the QML way! */ + XsAttributeValue { + id: draw_pen_size + attributeTitle: "Draw Pen Size" + model: annotations_model_data + } + + XsAttributeValue { + id: shapes_pen_size + attributeTitle: "Shapes Pen Size" + model: annotations_model_data + } + XsAttributeValue { + id: erase_pen_size + attributeTitle: "Erase Pen Size" + model: annotations_model_data + } + XsAttributeValue { + id: text_size + attributeTitle: "Text Size" + model: annotations_model_data + } + XsAttributeValue { + id: pen_colour + attributeTitle: "Pen Colour" + model: annotations_model_data + } + XsAttributeValue { + id: pen_opacity + attributeTitle: "Pen Opacity" + model: annotations_model_data + } + XsAttributeValue { + id: active_tool + attributeTitle: "Active Tool" + model: annotations_model_data + } + XsAttributeValue { + id: text_background_colour + attributeTitle: "Text Background Colour" + model: annotations_model_data + } + XsAttributeValue { + id: text_background_opacity + attributeTitle: "Text Background Opacity" + model: annotations_model_data + } + + // make a local binding to the backend attribute + property alias currentDrawPenSize: draw_pen_size.value + property alias currentShapePenSize: shapes_pen_size.value + property alias currentErasePenSize: erase_pen_size.value + property alias currentTextSize: text_size.value + property alias currentToolColour: pen_colour.value + property alias currentOpacity: pen_opacity.value + property alias currentTool: active_tool.value + property alias backgroundColor: text_background_colour.value + property alias backgroundOpacity: text_background_opacity.value + + property var toolSizeAttrName: "Draw Pen Size" + + onCurrentToolChanged: { + if(currentTool === "Draw" || currentTool === "Laser") + { + currentColorPresetModel = drawColourPresetsModel + toolSizeAttrName = "Draw Pen Size" + } + else if(currentTool === "Erase") + { + currentColorPresetModel = eraseColorPresetModel + toolSizeAttrName = "Erase Pen Size" + + } + else if(currentTool === "Text") + { + currentColorPresetModel = textColourPresetsModel + toolSizeAttrName = "Text Size" + + } + else if(currentTool === "Square" || currentTool === "Circle" || currentTool === "Arrow" || currentTool === "Line") + { + currentColorPresetModel = shapesColourPresetsModel + toolSizeAttrName = "Shapes Pen Size" + + } + } + + + function setPenSize(penSize) { + if(currentTool === "Draw" || currentTool === "Laser") + { + currentDrawPenSize = penSize + } + else if(currentTool === "Erase") + { + currentErasePenSize = penSize + } + else if(currentTool === "Square" || currentTool === "Circle" || currentTool === "Arrow" || currentTool === "Line") + { + currentShapePenSize = penSize + } + else if(currentTool === "Text") + { + currentTextSize = penSize + } + } + + // map the local property for currentToolSize to the backend value ... to modify the tool size, we only change the backend + // value binding + + property ListModel currentColorPresetModel: drawColourPresetsModel + + + XsGradientRectangle{ + anchors.fill: parent + } + + // We wrap all the widgets in a top level Item that can forward keyboard + // events back to the viewport for consistent + RowLayout { + anchors.fill: parent + spacing: 0 + + XsToolSelectorTB { + id: toolSelector + Layout.preferredWidth: toolSet.width + framePadding + Layout.maximumWidth: toolSet.width + framePadding + Layout.preferredHeight: parent.height + } + + Item{ id: toolProperties + y: framePadding + Layout.fillWidth: true + Layout.minimumWidth: !isAnyToolSelected? 0 : toolPropLoaderWidth + Layout.preferredHeight: parent.height - y*2 + + Loader{ + width: !isAnyToolSelected? 0 : toolPropLoaderWidth + height: parent.height + anchors.centerIn: parent + clip: true + + sourceComponent: XsToolPropertiesTB{ + root: drawDialog + } + + } + + ColorDialog { + id: colorDialog + title: "Please pick a colour" + onAccepted: { + currentToolColour = colorDialog.currentColor + close() + } + onRejected: { + close() + } + onVisibleChanged: { + if (visible) { + colorDialog.color = currentToolColour + } + } + } + + ListModel{ id: eraseColorPresetModel + ListElement{ + preset: "white" + } + } + ListModel{ id: drawColourPresetsModel + ListElement{ + preset: "#ff0000" //- "red" + } + ListElement{ + preset: "#ffa000" //- "orange" + } + ListElement{ + preset: "#ffff00" //- "yellow" + } + ListElement{ + preset: "#28dc00" //- "green" + } + ListElement{ + preset: "#0050ff" //- "blue" + } + ListElement{ + preset: "#8c00ff" //- "violet" + } + // ListElement{ + // preset: "#ff64ff" //- "pink" + // } + ListElement{ + preset: "#ffffff" //- "white" + } + ListElement{ + preset: "#000000" //- "black" + } + } + ListModel{ id: textColourPresetsModel + ListElement{ + preset: "#ff0000" //- "red" + } + ListElement{ + preset: "#ffa000" //- "orange" + } + ListElement{ + preset: "#ffff00" //- "yellow" + } + ListElement{ + preset: "#28dc00" //- "green" + } + ListElement{ + preset: "#0050ff" //- "blue" + } + ListElement{ + preset: "#8c00ff" //- "violet" + } + ListElement{ + preset: "#ffffff" //- "white" + } + ListElement{ + preset: "#000000" //- "black" + } + } + ListModel{ id: shapesColourPresetsModel + ListElement{ + preset: "#ff0000" //- "red" + } + ListElement{ + preset: "#ffa000" //- "orange" + } + ListElement{ + preset: "#ffff00" //- "yellow" + } + ListElement{ + preset: "#28dc00" //- "green" + } + ListElement{ + preset: "#0050ff" //- "blue" + } + ListElement{ + preset: "#8c00ff" //- "violet" + } + ListElement{ + preset: "#ffffff" //- "white" + } + ListElement{ + preset: "#000000" //- "black" + } + } + } + + XsToolActionsTB{ + id: toolActionSection + Layout.fillWidth: true + Layout.minimumWidth: displayBtn.x + displayBtn.width/2.2 + framePadding/2 + Layout.preferredWidth: displayBtn.x + displayBtn.width + framePadding/2 + Layout.maximumWidth: displayBtn.x + displayBtn.width + framePadding/2 + Layout.fillHeight: true + } + + Item{ + Layout.fillWidth: true + Layout.minimumWidth: XsStyleSheet.primaryButtonStdHeight + Layout.preferredWidth: XsStyleSheet.primaryButtonStdHeight + Layout.maximumWidth: XsStyleSheet.primaryButtonStdHeight + Layout.fillHeight: true + } + + } + +} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsTextCategoriesTB.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsTextCategoriesTB.qml new file mode 100644 index 000000000..4ae23c621 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsTextCategoriesTB.qml @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 + +import QtQuick + +import QtQuick.Layouts + +import xstudio.qml.bookmarks 1.0 + +import xStudio 1.0 +import xstudio.qml.models 1.0 + +Item{ id: textCategory + + XsModuleData { + id: anno_font_options + modelDataName: "annotations_tool_fonts" + } + + Repeater { + + // Using a repeater here - but 'vp_mouse_wheel_behaviour_attr' only + // has one row by the way. The use of a repeater means the model role + // data are all visible in the XsComboBox instance. + model: anno_font_options + + XsComboBox { + + id: dropdownFonts + + width: parent.width - itemSpacing + height: parent.height + + model: combo_box_options + + property var value_: value ? value : null + onValue_Changed: { + currentIndex = indexOfValue(value_) + } + + onCurrentValueChanged: { + value = currentValue; + } + + Component.onCompleted: currentIndex = indexOfValue(value_) + } + } + + +} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsToolActionsTB.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsToolActionsTB.qml new file mode 100644 index 000000000..982af7d9c --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsToolActionsTB.qml @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + +import QtQuick.Layouts + +import xstudio.qml.bookmarks 1.0 + + + +import xStudio 1.0 +import xstudio.qml.models 1.0 + +Item{ id: toolActionsDiv + + property alias displayBtn: displayBtn + + XsAttributeValue { + id: __action_attr + attributeTitle: "action_attribute" + model: annotations_model_data + } + + property alias action_attribute: __action_attr.value + + ListView{ id: toolActionUndoRedo + + width: XsStyleSheet.primaryButtonStdWidth * model.count + height: XsStyleSheet.primaryButtonStdHeight + + anchors.verticalCenter: parent.verticalCenter + + spacing: itemSpacing + interactive: false + orientation: ListView.Horizontal + + model: ListModel{ + id: modelUndoRedo + ListElement{ + action: "Undo" + icon: "qrc:///anno_icons/undo.svg" + } + ListElement{ + action: "Redo" + icon: "qrc:///anno_icons/redo.svg" + } + ListElement{ + action: "Clear" + icon: "qrc:///anno_icons/delete.svg" + } + } + + delegate: XsPrimaryButton{ + text: "" //model.action + imgSrc: model.icon + width: toolActionUndoRedo.width/modelUndoRedo.count - toolActionUndoRedo.spacing + height: toolActionUndoRedo.height + + onClicked: { + action_attribute = model.action + } + } + } + + + // Sigh - hooking up the draw mode backen attr to the combo box here + // is horrible! Need something better than this! + XsModuleData { + id: annotations_tool_draw_mode_model + modelDataName: "annotations_tool_draw_mode" + } + + XsAttributeValue { + id: __display_mode + attributeTitle: "Display Mode" + model: annotations_tool_draw_mode_model + } + property alias display_mode: __display_mode.value + + XsButtonWithImageAndText{ id: displayBtn + iconText: display_mode.substring(display_mode.length-6, display_mode.length) + width: XsStyleSheet.primaryButtonStdWidth*2.2 + height: XsStyleSheet.primaryButtonStdHeight + + anchors.verticalCenter: parent.verticalCenter + anchors.left: toolActionUndoRedo.right + anchors.leftMargin: itemSpacing //framePadding + iconSrc: display_mode == "Always" ? "qrc:///anno_icons/check_circle.svg" : "qrc:///anno_icons/pause_circle.svg" + textDiv.visible: true + textDiv.font.bold: false + textDiv.font.pixelSize: XsStyleSheet.fontSize + paddingSpace: 4 + + onClicked:{ + if(displayMenu.visible) displayMenu.visible = false + else{ + displayMenu.x = x + displayMenu.y = y + height + displayMenu.visible = true + } + } + } + + XsPopupMenu { + id: displayMenu + visible: false + menu_model_name: "displayMenu" + toolActionsDiv + } + XsMenuModelItem { + text: "Always" + menuPath: "" + menuItemPosition: 1 + menuModelName: displayMenu.menu_model_name + onActivated: { + display_mode = "Always" + } + } + XsMenuModelItem { + text: "When Paused" + menuPath: "" + menuItemPosition: 2 + menuModelName: displayMenu.menu_model_name + onActivated: { + display_mode = "Only When Paused" + } + } + +} + diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsToolPropertiesTB.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsToolPropertiesTB.qml new file mode 100644 index 000000000..58c943d45 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsToolPropertiesTB.qml @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Dialogs + +import QtQml 2.15 +import xstudio.qml.bookmarks 1.0 + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 + +Item{ + id: toolProperties + property var root + + Rectangle{ id: row1_categories + width: visible? row2_controls.gridItemWidth*1 : 0 + height: XsStyleSheet.primaryButtonStdHeight + anchors.verticalCenter: parent.verticalCenter + + color: "transparent" + visible: (currentTool == "Text") + + XsTextCategoriesTB { + id: textCategories + anchors.fill: parent + visible: (currentTool === "Text") + } + } + + XsModuleData { + id: annotations_model_data + modelDataName: "annotations_tool_settings" + } + Connections { + target: annotations_model_data // this bubbles up from XsSessionWindow + function onJsonChanged() { + setPropertyIndeces() + } + } + + function setPropertyIndeces() { + draw_pen_size.index = annotations_model_data.searchRecursive("Draw Pen Size", "title") + } + + /* Here we locate particular nodes in the annotations_model_data giving + convenient access to backend data. Seems crazy but this is the QML way! */ + XsModelProperty { + id: draw_pen_size + role: "value" + } + + property alias drawPenSize: draw_pen_size.value + + Grid{id: row2_controls + x: row1_categories.visible? row1_categories.x + row1_categories.width : row1_categories.x + z: 1 + + columns: currentTool === "Text"? 4 : currentTool === "Erase"? 2 : 3 + spacing: itemSpacing + flow: Grid.TopToBottom + width: gridItemWidth*columns + height: XsStyleSheet.primaryButtonStdHeight + anchors.verticalCenter: parent.verticalCenter + + property real gridItemWidth: 80 //toolPropLoaderWidth/(columns+1) > 80 ? 80 : toolPropLoaderWidth/(columns+1) + + XsIntegerAttrControl { + id: sizeProp + visible: enabled + width: row2_controls.gridItemWidth + height: visible? XsStyleSheet.primaryButtonStdHeight : 0 + text: (currentTool=="Square" || currentTool === "Circle" || currentTool === "Arrow" || currentTool === "Line")? "Width" : "Size" + enabled: isAnyToolSelected + attr_group_model: annotations_model_data + attr_title: toolSizeAttrName + } + + XsIntegerAttrControl{ + id: opacityProp + visible: isAnyToolSelected + width: row2_controls.gridItemWidth + height: visible? XsStyleSheet.primaryButtonStdHeight : 0 + text: "Opacity" + attr_group_model: annotations_model_data + attr_title: "Pen Opacity" + enabled: isAnyToolSelected && currentTool != "Erase" + } + XsIntegerAttrControl { + id: bgOpacityProp + visible: currentTool === "Text" + width: row2_controls.gridItemWidth + height: visible? XsStyleSheet.primaryButtonStdHeight : 0 + text: "BG Opa." + attr_group_model: annotations_model_data + attr_title: "Text Background Opacity" + } + + XsViewerMenuButton{ id: colorProp + visible: isAnyToolSelected && currentTool !== "Erase" + width: visible? row2_controls.gridItemWidth : 0 + height: XsStyleSheet.primaryButtonStdHeight + text: "Colour " + shortText: "Col " + enabled: (isAnyToolSelected && currentTool !== "Erase") + showBorder: isMouseHovered + isActive: isPressed + + property bool isPressed: false + property bool isMouseHovered: colorMArea.containsMouse + + MouseArea{ + id: colorMArea + propagateComposedEvents: true + hoverEnabled: true + anchors.fill: parent + onClicked: { + parent.isPressed = false + colorDialog.open() + } + onPressed: { + parent.isPressed = true + } + onReleased: { + parent.isPressed = false + } + } + Item{id:centerItem; anchors.centerIn: parent} + Rectangle{ id: colorPreviewDuplicate + // opacity: (!isAnyToolSelected || currentTool === "Erase")? (parent.enabled?1:0.5): 0 + color: currentTool === "Erase" ? "white" : parent.enabled ? currentToolColour ? currentToolColour : "grey" : "grey" + border.width: 1 + border.color: parent.enabled? "black" : "dark grey" + anchors.left: centerItem.right + anchors.leftMargin: 15 + anchors.right: parent.right + anchors.rightMargin: 4 + height: XsStyleSheet.primaryButtonStdHeight/1.6; + anchors.verticalCenter: centerItem.verticalCenter + } + } + + } + + XsColourPresetsTB{ id: row3_colourpresets + visible: (isAnyToolSelected && currentTool !== "Erase") + + x: row2_controls.x + row2_controls.width + itemSpacing*2 + onXChanged: { + toolPropLoaderWidth = x + width + } + y: framePadding + width: visible? row2_controls.gridItemWidth : 0 + height: parent.height - y*2 + onWidthChanged: { + toolPropLoaderWidth = x + width + } + } + + + + Component.onCompleted: { + toolPropLoaderWidth = row3_colourpresets.x + row3_colourpresets.width + setPropertyIndeces() + } + +} + diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsToolSelectorTB.qml b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsToolSelectorTB.qml new file mode 100644 index 000000000..55fb2c207 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/dockedTB/XsToolSelectorTB.qml @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick + +import QtQuick.Layouts + +import xstudio.qml.bookmarks 1.0 + + + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import xstudio.qml.helpers 1.0 + +Item{ + id: toolSetBg + + property int colCount: tool_choices.length + property int rowCount: 1 + + property alias toolSet: toolSet + + property real itemWidth: XsStyleSheet.primaryButtonStdWidth + property real itemHeight: XsStyleSheet.primaryButtonStdHeight + + /* This connects to the backend annotations tool object and exposes its + ui data via model data */ + XsModuleData { + id: annotations_tool_types + modelDataName: "annotations_tool_settings" + } + + XsAttributeValue { + id: tool_types_value + attributeTitle: "Active Tool" + model: annotations_tool_types + } + XsAttributeValue { + id: tool_types_choices + role: "combo_box_options" + attributeTitle: "Active Tool" + model: annotations_tool_types + } + + property alias current_tool: tool_types_value.value + + // Un-comment this when Laser is implemented in combo_box_options + property alias tool_choices: tool_types_choices.value + + property var toolImages: [ + "qrc:///anno_icons/draw_brush.svg", + "qrc:///anno_icons/draw_laser.svg", + "qrc:///anno_icons/draw_shape_square.svg", + "qrc:///anno_icons/draw_shape_circle.svg", + "qrc:///anno_icons/draw_shape_arrow.svg", + "qrc:///anno_icons/draw_shape_line.svg", + "qrc:///anno_icons/draw_text.svg", + "qrc:///anno_icons/draw_eraser.svg" + ] + + GridView{ + + id: toolSet + + x: framePadding + y: framePadding*2 + anchors.verticalCenter: parent.verticalCenter + + width: itemWidth*colCount + height: itemHeight*rowCount + + cellWidth: itemWidth + cellHeight: itemHeight + + interactive: false + flow: GridView.FlowLeftToRight + + // read only convenience binding to backend. + currentIndex: tool_choices ? tool_choices.indexOf(current_tool) : undefined + + model: tool_choices // this is 'role data' from the backend attr + + delegate: toolSetDelegate + + Component{ + id: toolSetDelegate + + Rectangle{ + width: toolSet.cellWidth + height: toolSet.cellHeight + color: "transparent" + + XsPrimaryButton{ id: toolBtn + + // x: index>=rowCount? width/2:0 + width: parent.width - itemSpacing + height: parent.height - itemSpacing + + clip: true + isToolTipEnabled: false + + isActive: current_tool===text + anchors.top: parent.top + + text: tool_choices[index] + imgSrc: toolImages[index] + + onClicked: { + if(isActive) + { + //Disables tool by setting the 'value' of the 'active tool' + // attribute in the plugin backend to 'None' + current_tool = "None" + } + else + { + current_tool = text + } + } + + } + + } + } + + } + +} + diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/arrow_forward.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/arrow_forward.svg new file mode 100644 index 000000000..99a89fd35 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/arrow_forward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/brush.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/brush.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/brush.svg rename to src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/brush.svg diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/check.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/check.svg new file mode 100644 index 000000000..1655d12bf --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/check_circle.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/check_circle.svg new file mode 100644 index 000000000..fc9a8160d --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/check_circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/radio_button_unchecked.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/circle.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/radio_button_unchecked.svg rename to src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/circle.svg diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/colorize.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/colorize.svg new file mode 100644 index 000000000..1909bcde4 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/colorize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/crop_square.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/crop_square.svg new file mode 100644 index 000000000..05469f2cf --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/crop_square.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/delete.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/delete.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/delete.svg rename to src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/delete.svg diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/horizontal_rule.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/horizontal_rule.svg new file mode 100644 index 000000000..dab1a8348 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/horizontal_rule.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/ink_eraser.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/ink_eraser.svg new file mode 100644 index 000000000..4b3643ba7 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/ink_eraser.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/pause_circle.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/pause_circle.svg new file mode 100644 index 000000000..30e5f4aed --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/pause_circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/play_circle.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/play_circle.svg new file mode 100644 index 000000000..0940d04de --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/play_circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/redo.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/redo.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/redo.svg rename to src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/redo.svg diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/shapes.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/shapes.svg new file mode 100644 index 000000000..59afd4fb3 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/shapes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/star_rate.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/star_rate.svg new file mode 100644 index 000000000..b817447b6 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/star_rate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/stylus_laser_pointer.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/stylus_laser_pointer.svg new file mode 100644 index 000000000..cf15cb796 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/stylus_laser_pointer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/text_fields.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/text_fields.svg new file mode 100644 index 000000000..551a7d337 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/text_fields.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/title.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/title.svg new file mode 100644 index 000000000..94e462161 --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/title.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/undo.svg b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/undo.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/undo.svg rename to src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/icons/undo.svg diff --git a/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/qmldir b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/qmldir new file mode 100644 index 000000000..ed52b68af --- /dev/null +++ b/src/plugin/viewport_overlay/annotations/src/qml/AnnotationsTool.2/qmldir @@ -0,0 +1,23 @@ +module AnnotationsTool + +XsDrawingTools 2.0 XsDrawingTools.qml +XsToolActions 2.0 XsToolActions.qml +XsToolProperties 2.0 XsToolProperties.qml +XsToolSelector 2.0 XsToolSelector.qml +XsColourPresets 2.0 XsColourPresets.qml +XsTextCategories 2.0 XsTextCategories.qml + +XsDrawingToolsLR 2.0 dockedLR/XsDrawingToolsLR.qml +XsToolActionsLR 2.0 dockedLR/XsToolActionsLR.qml +XsToolDisplayLR 2.0 dockedLR/XsToolDisplayLR.qml +XsToolPropertiesLR 2.0 dockedLR/XsToolPropertiesLR.qml +XsToolSelectorLR 2.0 dockedLR/XsToolSelectorLR.qml +XsColourPresetsLR 2.0 dockedLR/XsColourPresetsLR.qml +XsTextCategoriesLR 2.0 dockedLR/XsTextCategoriesLR.qml + +XsDrawingToolsTB 2.0 dockedTB/XsDrawingToolsTB.qml +XsToolActionsTB 2.0 dockedTB/XsToolActionsTB.qml +XsToolPropertiesTB 2.0 dockedTB/XsToolPropertiesTB.qml +XsToolSelectorTB 2.0 dockedTB/XsToolSelectorTB.qml +XsColourPresetsTB 2.0 dockedTB/XsColourPresetsTB.qml +XsTextCategoriesTB 2.0 dockedTB/XsTextCategoriesTB.qml \ No newline at end of file diff --git a/src/plugin/viewport_overlay/annotations/src/qml/CMakeLists.txt b/src/plugin/viewport_overlay/annotations/src/qml/CMakeLists.txt deleted file mode 100644 index 43e48f394..000000000 --- a/src/plugin/viewport_overlay/annotations/src/qml/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -project(basic_viewport_ui VERSION 0.1.0 LANGUAGES CXX) - -if(WIN32) -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationsTool.1/ DESTINATION ${CMAKE_INSTALL_PREFIX}/plugin/qml/AnnotationsTool.1) -else() -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationsTool.1/ DESTINATION share/xstudio/plugin/qml/AnnotationsTool.1) -endif() - -add_custom_target(COPY_ANNO_QML ALL) - -add_custom_command(TARGET COPY_ANNO_QML POST_BUILD - COMMAND ${CMAKE_COMMAND} -E - copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationsTool.1 ${CMAKE_BINARY_DIR}/bin/plugin/qml/AnnotationsTool.1) - diff --git a/src/plugin/viewport_overlay/annotations/test/CMakeLists.txt b/src/plugin/viewport_overlay/annotations/test/CMakeLists.txt index a73825679..66caa1970 100644 --- a/src/plugin/viewport_overlay/annotations/test/CMakeLists.txt +++ b/src/plugin/viewport_overlay/annotations/test/CMakeLists.txt @@ -1,7 +1,7 @@ include(CTest) SET(LINK_DEPS - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/plugin/viewport_overlay/audio_waveform/src/CMakeLists.txt b/src/plugin/viewport_overlay/audio_waveform/src/CMakeLists.txt new file mode 100644 index 000000000..203efa044 --- /dev/null +++ b/src/plugin/viewport_overlay/audio_waveform/src/CMakeLists.txt @@ -0,0 +1,11 @@ + +SET(LINK_DEPS + xstudio::module + xstudio::plugin_manager + xstudio::ui::opengl::viewport + Imath::Imath +) + +find_package(Imath) + +create_plugin_with_alias(audio_waveform_overlay xstudio::viewport::audio_waveform_overlay ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/plugin/viewport_overlay/audio_waveform/src/audio_waveform_overlay.cpp b/src/plugin/viewport_overlay/audio_waveform/src/audio_waveform_overlay.cpp new file mode 100644 index 000000000..9c923ae2d --- /dev/null +++ b/src/plugin/viewport_overlay/audio_waveform/src/audio_waveform_overlay.cpp @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "audio_waveform_overlay.hpp" +#include "xstudio/plugin_manager/plugin_base.hpp" +#include "xstudio/media_reader/image_buffer.hpp" +#include "xstudio/global_store/global_store.hpp" +#include "xstudio/utility/blind_data.hpp" +#include "xstudio/ui/viewport/viewport_helpers.hpp" +#include "xstudio/utility/helpers.hpp" + +#ifdef __apple__ +#include +#else +#include +#include +#endif + +using namespace xstudio; +using namespace xstudio::ui::viewport; + +namespace { +const char *vertex_shader = R"( + #version 330 core + layout (location = 0) in float ypos; + uniform mat4 to_coord_system; + uniform mat4 to_canvas; + uniform float hscale; + uniform float vscale; + uniform float v_pos; + uniform int offset; + + void main() + { + vec4 rpos = vec4(-1.0 + float(gl_VertexID-offset)*hscale, v_pos+ypos*vscale*10.0, vec2(0.0, 1.0)); + gl_Position = rpos*to_canvas; + } + )"; + +const char *frag_shader = R"( + #version 330 core + out vec4 FragColor; + uniform vec3 line_colour; + + void main(void) + { + FragColor = vec4(line_colour, 1.0); + } + + )"; +} // namespace + +AudioWaveformOverlayRenderer::~AudioWaveformOverlayRenderer() { + if (vbo_) + glDeleteBuffers(1, &vbo_); + if (vao_) + glDeleteBuffers(1, &vao_); +} + +void AudioWaveformOverlayRenderer::render_image_overlay( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + const float viewport_du_dpixel, + const xstudio::media_reader::ImageBufPtr &frame, + const bool have_alpha_buffer) { + + if (!shader_) + init_overlay_opengl(); + + auto render_data = + frame.plugin_blind_data(utility::Uuid("873c508b-276b-44e3-82d0-15db2f039aa7")); + if (!render_data) + return; + + const auto *data = dynamic_cast(render_data.get()); + if (!data) + return; + + glBindVertexArray(vao_); + // 2. copy our vertices array in a buffer for OpenGL to use + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData( + GL_ARRAY_BUFFER, + data->verts_.size() * sizeof(float), + data->verts_.data(), + GL_STREAM_DRAW); + // 3. then set our vertex module pointers + glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, sizeof(float), nullptr); + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + const int n_samps = data->verts_.size() / data->num_chans; + + utility::JsonStore shader_params; + shader_params["to_canvas"] = transform_window_to_viewport_space; + shader_params["hscale"] = 2.0f / float(n_samps); + shader_params["vscale"] = data->vscale; + shader_params["line_colour"] = data->line_colour; + shader_->set_shader_parameters(shader_params); + shader_->use(); + glEnableVertexAttribArray(0); + + for (int c = 0; c < data->num_chans; ++c) { + utility::JsonStore es; + es["v_pos"] = data->v_pos + data->chan_spacing * c; + es["offset"] = c * n_samps; + shader_->set_shader_parameters(es); + // the actual draw! + glDrawArrays(GL_LINE_STRIP, c * n_samps, n_samps); + } + shader_->stop_using(); + glDisableVertexAttribArray(0); + glBindVertexArray(0); +} + +void AudioWaveformOverlayRenderer::init_overlay_opengl() { + + glGenBuffers(1, &vbo_); + glGenVertexArrays(1, &vao_); + + shader_ = std::make_unique(vertex_shader, frag_shader); +} + + +AudioWaveformOverlay::AudioWaveformOverlay( + caf::actor_config &cfg, const utility::JsonStore &init_settings) + : plugin::HUDPluginBase(cfg, "Audio Waveform", init_settings, 0.0f) { + + vertical_scale_ = + add_float_attribute("Vertical Scale", "Vertical Scale", 0.1f, 0.01f, 1.0f, 0.01f); + add_hud_settings_attribute(vertical_scale_); + vertical_scale_->set_tool_tip("Sets the vertical scaling of the waveform"); + + horizontal_scale_ = + add_float_attribute("Horizontal Scale", "Horizontal Scale", 50.0f, 10.0f, 100.0f, 1.0f); + add_hud_settings_attribute(horizontal_scale_); + horizontal_scale_->set_tool_tip("Sets the horizontal scaling of the waveform - the units " + "are milliseconds of audio shown on the screen"); + + chan_position_spacing_ = add_float_attribute( + "Chan Position Spacing", "Chan Position Spacing", 0.05f, 0.0f, 1.0f, 0.01f); + add_hud_settings_attribute(chan_position_spacing_); + chan_position_spacing_->set_tool_tip("Vertical spacing between channels"); + + vertical_position_ = add_float_attribute( + "Vertical Position", "Vertical Position", -0.8f, -1.0f, 1.0f, 0.01f); + add_hud_settings_attribute(vertical_position_); + vertical_position_->set_tool_tip("Vertical position for drawing the waveform"); + + separate_channels_ = + add_boolean_attribute("Show Channels Separately", "Show Channels Separately", false); + add_hud_settings_attribute(separate_channels_); + separate_channels_->set_tool_tip( + "Shows the waveforms of each channel, or combine channels if not selected."); + + line_colour_ = add_colour_attribute( + "Line Colour", "Line Colour", utility::ColourTriplet(1.0f, 1.0f, 0.0f)); + add_hud_settings_attribute(line_colour_); + line_colour_->set_tool_tip("The colour of the waveform line"); + + // Registering preference path allows these values to persist between sessions + vertical_scale_->set_preference_path("/plugin/audio_waveform/vertical_scale"); + horizontal_scale_->set_preference_path("/plugin/audio_waveform/horizontal_scale"); + chan_position_spacing_->set_preference_path("/plugin/audio_waveform/chan_position_spacing"); + vertical_position_->set_preference_path("/plugin/audio_waveform/vertical_position"); + line_colour_->set_preference_path("/plugin/audio_waveform/line_colour"); + + // get the global audio output actor and join its event group. This means we + // receive the broadcasted Audiobuffers + auto global_audio_actor = + system().registry().template get(audio_output_registry); + utility::join_event_group(this, global_audio_actor); + + message_handler_ext_ = { + [=](utility::event_atom, + module::change_attribute_event_atom, + const float volume, + const bool muted, + const bool repitch, + const bool scrubbing) {}, + [=](utility::event_atom, + playhead::sound_audio_atom, + const std::vector &audio_buffers, + const utility::Uuid &sub_playhead, + const bool scrubbing, + const timebase::flicks) {}, + [=](utility::event_atom, + playhead::position_atom, + const timebase::flicks playhead_position, + const bool forward, + const float velocity, + const bool playing, + utility::time_point when_position_changed) {}, + [=](utility::event_atom, + playhead::position_atom, + const timebase::flicks playhead_position, + const bool forward, + const float velocity, + const bool playing, + utility::time_point when_position_changed) {}, + [=](utility::event_atom, + audio::audio_samples_atom, + const std::vector &audio_buffers, + timebase::flicks playhead_position, + const utility::Uuid &playhead_uuid) { + latest_audio_buffers_[playhead_uuid] = audio_buffers; + }}; + + make_behavior(); + // we need to keep track of which playhead is driving which viewport + listen_to_playhead_events(); +} + +AudioWaveformOverlay::~AudioWaveformOverlay() = default; + +void AudioWaveformOverlay::attribute_changed( + const utility::Uuid &attribute_uuid, const int /*role*/ +) { + + redraw_viewport(); +} + +utility::BlindDataObjectPtr AudioWaveformOverlay::onscreen_render_data( + const media_reader::ImageBufPtr &image, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid, + const bool is_hero_image) const { + + auto r = utility::BlindDataObjectPtr(); + if (!visible()) + return r; + + auto p = latest_audio_buffers_.find(playhead_uuid); + if (p == latest_audio_buffers_.end()) + return r; + const auto &latest_audio_buffers = p->second; + + // check our sample buffers to get sample rate & num channels + int nc = 0; + uint64_t sample_rate = 0; + for (const auto &aud_buf : latest_audio_buffers) { + + if (aud_buf && !sample_rate && !nc) { + nc = aud_buf->num_channels(); + sample_rate = aud_buf->sample_rate(); + } + } + + if (!sample_rate) + return r; + + float millisecs = horizontal_scale_->value(); + int samps_needed = int(round(millisecs * float(sample_rate) / 1000.0f)); + + std::vector verts(samps_needed * (separate_channels_->value() ? nc : 1)); + + // this gives us the ref timestamp for the start of the window of samples that + // we will draw to the screen + timebase::flicks tt = + image.timeline_timestamp() - + timebase::to_flicks((millisecs / 1000.0 - image.frame_id().rate().to_seconds()) * 0.5); + + for (const auto &aud_buf : latest_audio_buffers) { + + if (aud_buf) { + // reference timeline timestamp for first sample + timebase::flicks when_samples_play = + aud_buf.timeline_timestamp() + std::chrono::duration_cast( + aud_buf->time_delta_to_video_frame()); + const int nsamp = aud_buf->num_samples(); + + if (separate_channels_->value()) { + + // offset *into* the samples that we're generating + for (int c = 0; c < nc; ++c) { + int offset = timebase::to_seconds(when_samples_play - tt) * sample_rate; + int n = 0; + if (offset < 0) { + n = -offset; + offset = 0; + } + int16_t *samp_data = (int16_t *)aud_buf->buffer(); + samp_data += n * nc + c; + while (offset < samps_needed && n < nsamp) { + + verts[offset + samps_needed * c] = float(*samp_data) * 0.000030518f; + n++; + offset++; + samp_data += nc; + } + } + + } else { + + int offset = timebase::to_seconds(when_samples_play - tt) * sample_rate; + int n = 0; + if (offset < 0) { + n = -offset; + offset = 0; + } + int16_t *samp_data = (int16_t *)aud_buf->buffer(); + samp_data += n * nc; + while (offset < samps_needed && n < nsamp) { + + float f = 0.0f; + int c = nc; + while (c--) { + f += *(samp_data++); + } + verts[offset] = f * 0.000030518f; + n++; + offset++; + } + } + } + } + + r.reset(new WaveFormData( + verts, + separate_channels_->value() ? nc : 1, + vertical_scale_->value(), + chan_position_spacing_->value(), + vertical_position_->value(), + line_colour_->value())); + + return r; +} + +/*void AudioWaveformOverlay::viewport_playhead_changed(const std::string &viewport_name, +caf::actor playhead) { if (playhead) { request(playhead, infinite, utility::uuid_atom_v).then( + [=](utility::Uuid &playhead_uuid) { + }, + [=](caf::error &err) {}); + } +}*/ + +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>( + {std::make_shared>( + utility::Uuid("873c508b-276b-44e3-82d0-15db2f039aa7"), + "AudioWaveformOverlay", + plugin_manager::PluginFlags::PF_HEAD_UP_DISPLAY | + plugin_manager::PluginFlags::PF_VIEWPORT_OVERLAY, + true, + "Ted Waine", + "Audio Waveform Overlay")})); +} +} diff --git a/src/plugin/viewport_overlay/audio_waveform/src/audio_waveform_overlay.hpp b/src/plugin/viewport_overlay/audio_waveform/src/audio_waveform_overlay.hpp new file mode 100644 index 000000000..0b82be04c --- /dev/null +++ b/src/plugin/viewport_overlay/audio_waveform/src/audio_waveform_overlay.hpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/ui/opengl/shader_program_base.hpp" +#include "xstudio/ui/opengl/opengl_text_rendering.hpp" +#include "xstudio/plugin_manager/hud_plugin.hpp" + +namespace xstudio { +namespace ui { + namespace viewport { + + class WaveFormData : public utility::BlindDataObject { + public: + WaveFormData( + const std::vector &v, + const int _num_chans, + const float _vscale, + const float _chan_spacing, + const float _v_pos, + const utility::ColourTriplet _line_colour) + : verts_(std::move(v)), + num_chans(_num_chans), + vscale(_vscale), + chan_spacing(_chan_spacing), + v_pos(_v_pos), + line_colour(_line_colour) {} + ~WaveFormData() = default; + + const std::vector verts_; + const int num_chans; + const float vscale; + const float chan_spacing; + const float v_pos; + const utility::ColourTriplet line_colour; + }; + + class AudioWaveformOverlayRenderer : public plugin::ViewportOverlayRenderer { + + public: + void render_image_overlay( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + const float viewport_du_dpixel, + const xstudio::media_reader::ImageBufPtr &frame, + const bool have_alpha_buffer) override; + + ~AudioWaveformOverlayRenderer(); + + void init_overlay_opengl(); + + std::unique_ptr shader_; + GLuint vbo_ = {0}; + GLuint vao_ = {0}; + }; + + class AudioWaveformOverlay : public plugin::HUDPluginBase { + + public: + AudioWaveformOverlay( + caf::actor_config &cfg, const utility::JsonStore &init_settings); + + ~AudioWaveformOverlay(); + + protected: + utility::BlindDataObjectPtr onscreen_render_data( + const media_reader::ImageBufPtr &, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid, + const bool is_hero_image) const override; + + plugin::ViewportOverlayRendererPtr + make_overlay_renderer(const std::string &viewport_name) override { + return plugin::ViewportOverlayRendererPtr(new AudioWaveformOverlayRenderer()); + } + + caf::message_handler message_handler_extensions() override { + return message_handler_ext_.or_else( + plugin::HUDPluginBase::message_handler_extensions()); + } + + void attribute_changed(const utility::Uuid &attr_uuid, const int role) override; + + private: + module::FloatAttribute *vertical_scale_; + module::FloatAttribute *horizontal_scale_; + module::FloatAttribute *chan_position_spacing_; + module::FloatAttribute *vertical_position_; + module::BooleanAttribute *separate_channels_; + module::ColourAttribute *line_colour_; + + utility::Uuid mask_hotkey_; + caf::message_handler message_handler_ext_; + + std::unordered_map> + latest_audio_buffers_; + }; + + } // namespace viewport +} // namespace ui +} // namespace xstudio diff --git a/src/plugin/viewport_overlay/audio_waveform/test/CMakeLists.txt b/src/plugin/viewport_overlay/audio_waveform/test/CMakeLists.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugin/viewport_overlay/basic_viewport_mask/src/CMakeLists.txt b/src/plugin/viewport_overlay/basic_viewport_mask/src/CMakeLists.txt index 5926057e2..3336591fb 100644 --- a/src/plugin/viewport_overlay/basic_viewport_mask/src/CMakeLists.txt +++ b/src/plugin/viewport_overlay/basic_viewport_mask/src/CMakeLists.txt @@ -8,6 +8,6 @@ SET(LINK_DEPS find_package(Imath) -create_plugin_with_alias(basic_viewport_masking xstudio::viewport::basic_viewport_masking 0.1.0 "${LINK_DEPS}") +create_plugin_with_alias(basic_viewport_masking xstudio::viewport::basic_viewport_masking ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") -add_subdirectory(qml) \ No newline at end of file +add_plugin_qml(${PROJECT_NAME} qml) \ No newline at end of file diff --git a/src/plugin/viewport_overlay/basic_viewport_mask/src/basic_viewport_masking.cpp b/src/plugin/viewport_overlay/basic_viewport_mask/src/basic_viewport_masking.cpp index 02f7b4e1d..fed997062 100644 --- a/src/plugin/viewport_overlay/basic_viewport_mask/src/basic_viewport_masking.cpp +++ b/src/plugin/viewport_overlay/basic_viewport_mask/src/basic_viewport_masking.cpp @@ -7,8 +7,12 @@ #include "xstudio/ui/viewport/viewport_helpers.hpp" #include "xstudio/utility/helpers.hpp" -#include +#ifdef __apple__ +#include +#else #include +#include +#endif using namespace xstudio; using namespace xstudio::ui::viewport; @@ -17,21 +21,23 @@ namespace { const char *vertex_shader = R"( #version 330 core layout (location = 0) in vec4 aPos; - out vec2 viewportCoordinate; + out vec2 normImageCoordinate; uniform mat4 to_coord_system; uniform mat4 to_canvas; + uniform float image_aspect; void main() { - vec4 rpos = aPos*to_coord_system; - gl_Position = aPos*to_canvas; - viewportCoordinate = rpos.xy; + vec4 rpos = aPos; + rpos.y = rpos.y/image_aspect; + gl_Position = (rpos*to_coord_system*to_canvas); + normImageCoordinate = rpos.xy; } )"; const char *frag_shader = R"( #version 330 core - in vec2 viewportCoordinate; + in vec2 normImageCoordinate; out vec4 FragColor; uniform float image_aspect; uniform float mask_safety; @@ -74,7 +80,7 @@ const char *frag_shader = R"( void main(void) { - vec2 uv = vec2(viewportCoordinate.x, viewportCoordinate.y); + vec2 uv = vec2(normImageCoordinate.x, normImageCoordinate.y); // here the maske is 'fitted' to the image width ... the image is always // fitted into the viewport coordinates -1.0 to 1.0 @@ -86,7 +92,7 @@ const char *frag_shader = R"( )"; } // namespace -void BasicMaskRenderer::render_opengl( +void BasicMaskRenderer::render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, @@ -106,18 +112,20 @@ void BasicMaskRenderer::render_opengl( } utility::JsonStore shader_params; - shader_params["to_coord_system"] = transform_viewport_to_image_space; - shader_params["to_canvas"] = transform_window_to_viewport_space; - shader_params["viewport_du_dx"] = viewport_du_dpixel; + shader_params["to_coord_system"] = transform_viewport_to_image_space.inverse(); + shader_params["to_canvas"] = transform_window_to_viewport_space; + shader_params["viewport_du_dx"] = viewport_du_dpixel; + shader_params["image_transform_matrix"] = frame.layout_transform(); + shader_params["image_aspect"] = image_aspect(frame); shader_->set_shader_parameters(shader_params); shader_->use(); glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE); glBlendEquation(GL_FUNC_ADD); glDisable(GL_DEPTH_TEST); glBindVertexArray(vertex_array_object_); - glDrawArrays(GL_QUADS, 0, 4); + glDrawArrays(GL_TRIANGLES, 0, 6); shader_->stop_using(); glBindVertexArray(0); @@ -142,7 +150,7 @@ void BasicMaskRenderer::render_opengl( (viewport_du_dpixel * 24.0f)) * ((100.0f - mask_safety) / 100.0f); - text_renderer_->precompute_text_rendering_vertex_layout( + std::ignore = text_renderer_->precompute_text_rendering_vertex_layout( precomputed_text_vertex_buffer_, text_, text_position, @@ -167,23 +175,10 @@ void BasicMaskRenderer::init_overlay_opengl() { glGenBuffers(1, &vertex_buffer_object_); glGenVertexArrays(1, &vertex_array_object_); - static std::array vertices = { - -1.0, - 1.0, - 0.0f, - 1.0f, - 1.0, - 1.0, - 0.0f, - 1.0f, - 1.0, - -1.0, - 0.0f, - 1.0f, - -1.0, - -1.0, - 0.0f, - 1.0f}; + // NOLINT + static std::array vertices = {-1.0, 1.0, 0.0f, 1.0f, 1.0, 1.0, 0.0f, 1.0f, + 1.0, -1.0, 0.0f, 1.0f, 1.0, -1.0, 0.0f, 1.0f, + -1.0, -1.0, 0.0f, 1.0f, -1.0, 1.0, 0.0f, 1.0f}; glBindVertexArray(vertex_array_object_); // 2. copy our vertices array in a buffer for OpenGL to use @@ -197,22 +192,16 @@ void BasicMaskRenderer::init_overlay_opengl() { shader_ = std::make_unique(vertex_shader, frag_shader); text_renderer_ = std::make_unique( - utility::xstudio_root("/fonts/Overpass-Regular.ttf"), 96); + utility::xstudio_resources_dir("fonts/Overpass-Regular.ttf"), 96); } BasicViewportMasking::BasicViewportMasking( caf::actor_config &cfg, const utility::JsonStore &init_settings) - : plugin::StandardPlugin(cfg, "BasicViewportMasking", init_settings) { - // The function Attribute::expose_in_ui_attrs_group allows you to declare - // a XsModuleAttributes or XsModuleAttributesModel item in QML code - by - // setting the 'attributesGroupNames' attrubte on these items to match the - // group name that we set here, we get a connection between QML and this - // class that allows us to update QML when the backend attribute (owned by - // this class) changes or vice/versa. + : plugin::HUDPluginBase(cfg, "Mask", init_settings, 0.0f) { mask_aspect_ratio_ = add_float_attribute("Mask Aspect Ratio", "Aspect Ratio", 1.78f, 1.33f, 2.40f, 0.01f); - mask_aspect_ratio_->expose_in_ui_attrs_group("viewport_mask_settings"); + add_hud_settings_attribute(mask_aspect_ratio_); mask_aspect_ratio_->set_tool_tip("Sets the mask aspect ratio"); aspect_ratio_presets_ = add_string_choice_attribute( @@ -224,33 +213,33 @@ BasicViewportMasking::BasicViewportMasking( line_thickness_ = add_float_attribute("Line Thickness", "Thickness", 0.0f, 1.0f, 20.0f, 0.25f); - line_thickness_->expose_in_ui_attrs_group("viewport_mask_settings"); + add_hud_settings_attribute(line_thickness_); line_thickness_->set_tool_tip("Sets the thickness of the masking lines"); line_thickness_->set_redraw_viewport_on_change(true); line_opacity_ = add_float_attribute("Line Opacity", "Line Opac.", 1.0f, 0.0f, 1.0f, 0.1f); - line_opacity_->expose_in_ui_attrs_group("viewport_mask_settings"); + add_hud_settings_attribute(line_opacity_); line_opacity_->set_tool_tip( "Sets the opacity of the masking lines. Set to zero to hide the lines"); mask_opacity_ = add_float_attribute("Mask Opacity", "Opac.", 1.0f, 0.0f, 1.0f, 0.1f); - mask_opacity_->expose_in_ui_attrs_group("viewport_mask_settings"); + add_hud_settings_attribute(mask_opacity_); mask_opacity_->set_tool_tip("Sets the opacity of the black masking overlay. Set to zero to " "hids the mask completely."); safety_percent_ = add_float_attribute("Safety Percent", "Safety", 0.0f, 0.0f, 20.0f, 0.1f); - safety_percent_->expose_in_ui_attrs_group("viewport_mask_settings"); + add_hud_settings_attribute(safety_percent_); safety_percent_->set_tool_tip( "Sets the percentage of the image that is outside the mask area."); mask_label_size_ = add_float_attribute("Label Size", "Label Size", 16.0f, 10.0f, 40.0f, 1.0f); - mask_label_size_->expose_in_ui_attrs_group("viewport_mask_settings"); + add_hud_settings_attribute(mask_label_size_); mask_label_size_->set_tool_tip("Sets the font size of the mask label."); show_mask_label_ = add_boolean_attribute("Show Mask Label", "Label", true); - show_mask_label_->expose_in_ui_attrs_group("viewport_mask_settings"); + add_hud_settings_attribute(show_mask_label_); show_mask_label_->set_tool_tip( "Toggles a text label indicating the name of the current masking."); @@ -259,44 +248,11 @@ BasicViewportMasking::BasicViewportMasking( "Render Method", "QML", std::vector({"QML", "OpenGL"})); - mask_render_method_->expose_in_ui_attrs_group("viewport_mask_settings"); - - // The 'any_toolbar' attribute group is referenced - // by the xStudio toolbar - for every attribute in these groups, a toolbar - // widget is added to control that attribute. Only certain attribute types - // are supported in this way, however: StringChoice, Float and Boolean. - // But, as below, you can always code your own toolbox widget to control - // your attribute - mask_selection_ = - add_string_choice_attribute("Mask", "Mk", "Off", {"Off", "On"}, {"Off", "On"}); - mask_selection_->set_tool_tip("Toggles the mask on / off, use the settings to customize " - "the mask. You can use the M hotkey to toggle on / off"); - mask_selection_->expose_in_ui_attrs_group("viewport_mask_settings"); - - make_attribute_visible_in_viewport_toolbar(mask_selection_); - - // here we set custom QML code to implement a custom widget that is inserted - // into the viewer toolbox. In this case, we have extended the widget for - // a stringChoice attribute to include an extra 'Mask Settings ...' option - // that then opens a custom dialog to change some of our attributes declared - // here - mask_selection_->set_role_data( - module::Attribute::QmlCode, - R"( - import BasicViewportMask 1.0 - BasicViewportMaskButton { - id: control - anchors.fill: parent - } - )"); - - // This value is used to order the positions of the toolbox widgets by the - // toolbox - mask_selection_->set_role_data(module::Attribute::ToolbarPosition, 1.0f); + add_hud_settings_attribute(mask_render_method_); // Here we declare QML code to instantiate the actual item that draws // the overlay on the viewport. - qml_viewport_overlay_code( + hud_element_qml( R"( import BasicViewportMask 1.0 BasicViewportMaskOverlay { @@ -314,7 +270,7 @@ BasicViewportMasking::BasicViewportMasking( show_mask_label_->set_preference_path("/plugin/basic_masking/show_mask_label"); safety_percent_->set_preference_path("/plugin/basic_masking/safety_percent"); mask_label_size_->set_preference_path("/plugin/basic_masking/mask_label_size"); - mask_selection_->set_preference_path("/plugin/basic_masking/mask_selection"); + mask_render_method_->set_preference_path("/plugin/basic_masking/mask_render_method"); } BasicViewportMasking::~BasicViewportMasking() = default; @@ -325,21 +281,26 @@ void BasicViewportMasking::register_hotkeys() { int('M'), ui::NoModifier, "Toggle Mask", - "Toggles viewport masking. Find mask settings in the toolbar under the 'Mask' button"); + "Toggles viewport masking. Find mask settings in the toolbar under the 'Mask' button", + false, + "Viewer"); } -utility::BlindDataObjectPtr BasicViewportMasking::prepare_overlay_data( - const media_reader::ImageBufPtr &image, const bool /*offscreen*/) const { +utility::BlindDataObjectPtr BasicViewportMasking::onscreen_render_data( + const media_reader::ImageBufPtr &image, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid, + const bool is_hero_image) const { auto r = utility::BlindDataObjectPtr(); - - if (render_opengl_ && image && mask_selection_->value() == "On") { + if (visible() && mask_render_method_->value() == "OpenGL" && image) { try { auto image_dims = image->image_size_in_pixels(); utility::JsonStore j; - j["image_aspect"] = float(image_dims.x * image->pixel_aspect() / image_dims.y); + j["image_aspect"] = + float(image_dims.x * image.frame_id().pixel_aspect() / image_dims.y); j["mask_aspect_ratio"] = mask_aspect_ratio_->value(); j["mask_safety"] = safety_percent_->value(); j["mask_opac"] = mask_opacity_->value(); @@ -364,9 +325,7 @@ void BasicViewportMasking::attribute_changed( const utility::Uuid &attribute_uuid, const int /*role*/ ) { - if (attribute_uuid == mask_render_method_->uuid()) { - render_opengl_ = mask_render_method_->value() == "OpenGL"; - } else if (attribute_uuid == aspect_ratio_presets_->uuid()) { + if (attribute_uuid == aspect_ratio_presets_->uuid()) { if (aspect_ratio_presets_->value() != "...") { mask_aspect_ratio_->set_value(std::stof(aspect_ratio_presets_->value())); } @@ -385,9 +344,9 @@ void BasicViewportMasking::attribute_changed( } void BasicViewportMasking::hotkey_pressed( - const utility::Uuid &hotkey_uuid, const std::string &) { + const utility::Uuid &hotkey_uuid, const std::string &, const std::string &) { if (hotkey_uuid == mask_hotkey_) { - mask_selection_->set_value(mask_selection_->value() == "On" ? "Off" : "On"); + hud_data_->set_value(!hud_data_->value()); } } @@ -398,7 +357,8 @@ plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { {std::make_shared>( utility::Uuid("4006826a-6ff2-41ec-8ef2-d7a40bfd65e4"), "BasicViewportMasking", - plugin_manager::PluginFlags::PF_VIEWPORT_OVERLAY, + plugin_manager::PluginFlags::PF_HEAD_UP_DISPLAY | + plugin_manager::PluginFlags::PF_VIEWPORT_OVERLAY, true, "Ted Waine", "Basic Viewport Masking Plugin")})); diff --git a/src/plugin/viewport_overlay/basic_viewport_mask/src/basic_viewport_masking.hpp b/src/plugin/viewport_overlay/basic_viewport_mask/src/basic_viewport_masking.hpp index 8a4a1792e..e2d1a244c 100644 --- a/src/plugin/viewport_overlay/basic_viewport_mask/src/basic_viewport_masking.hpp +++ b/src/plugin/viewport_overlay/basic_viewport_mask/src/basic_viewport_masking.hpp @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include "xstudio/plugin_manager/plugin_base.hpp" #include "xstudio/ui/opengl/shader_program_base.hpp" #include "xstudio/ui/opengl/opengl_text_rendering.hpp" +#include "xstudio/plugin_manager/hud_plugin.hpp" namespace xstudio { namespace ui { @@ -24,7 +24,7 @@ namespace ui { class BasicMaskRenderer : public plugin::ViewportOverlayRenderer { public: - void render_opengl( + void render_image_overlay( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, @@ -43,7 +43,7 @@ namespace ui { float ma_ = 0.0f; }; - class BasicViewportMasking : public plugin::StandardPlugin { + class BasicViewportMasking : public plugin::HUDPluginBase { public: BasicViewportMasking( caf::actor_config &cfg, const utility::JsonStore &init_settings); @@ -55,15 +55,21 @@ namespace ui { ) override; void hotkey_pressed( - const utility::Uuid &hotkey_uuid, const std::string &context) override; + const utility::Uuid &hotkey_uuid, + const std::string &context, + const std::string &window) override; protected: void register_hotkeys() override; - utility::BlindDataObjectPtr prepare_overlay_data( - const media_reader::ImageBufPtr &, const bool /*offscreen*/) const override; + utility::BlindDataObjectPtr onscreen_render_data( + const media_reader::ImageBufPtr &, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid, + const bool is_hero_image) const override; - plugin::ViewportOverlayRendererPtr make_overlay_renderer(const int) override { + plugin::ViewportOverlayRendererPtr + make_overlay_renderer(const std::string &viewport_name) override { return plugin::ViewportOverlayRendererPtr(new BasicMaskRenderer()); } @@ -82,12 +88,7 @@ namespace ui { module::BooleanAttribute *show_mask_label_; - module::BooleanAttribute *mask_enabled_; - module::StringChoiceAttribute *mask_selection_; - utility::Uuid mask_hotkey_; - - bool render_opengl_ = false; }; } // namespace viewport diff --git a/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/BasicViewportMask.1/BasicViewportMaskButton.qml b/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/BasicViewportMask.1/BasicViewportMaskButton.qml deleted file mode 100644 index 69faad8ce..000000000 --- a/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/BasicViewportMask.1/BasicViewportMaskButton.qml +++ /dev/null @@ -1,214 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.12 - -import xStudio 1.0 - -XsToolbarItem { - - id: control - property var value_: value ? value : "" - title_: "Mask (M)" - hovered: mouse_area.containsMouse - showHighlighted: mouse_area.containsMouse | mouse_area.pressed | (activated != undefined && activated) - property int iconsize: XsStyle.menuItemHeight *.66 - - MouseArea { - id: mouse_area - anchors.fill: parent - hoverEnabled: true - onClicked: { - toggleShow() - widget_clicked() - } - } - - property alias popup_win: popup - - Rectangle { - - id: popup - width: combo_group.width - height: combo_group.height + settings_button.height + 20 - visible: false - z: 10000 - - onVisibleChanged: { - if (!visible) { - sessionWidget.deactivateWidgetHider() - } - } - - // we have to make this a child of the sessionWidget to allow us to - // use a mouseArea that covers the sessionWidget to close this pop-up - // when the user clicks outside the pop-up. - parent: sessionWidget - - border { - color: XsStyle.menuBorderColor - width: XsStyle.menuBorderWidth - } - color: XsStyle.mainBackground - radius: XsStyle.menuRadius - - ListView { - - id: combo_group - - // N.B the 'combo_box_options' attribute is propagated via the - // XsToolBox which instantiates the BasicViewportMaskButton - model: combo_box_options - height: count * XsStyle.menuItemHeight - width: 100 - y: 5 - - delegate: Rectangle { - - id: checkBoxItem - // Icon (radio or checkbox) - anchors.left: parent.left - height: XsStyle.menuItemHeight - width: popup.width - - property var checked: thetext.text === control.value_ - property var highlighted: mouse_area.containsMouse - - color: highlighted ? XsStyle.highlightColor : "transparent" - gradient: styleGradient.accent_gradient - - Text { - id: thetext - anchors.left: iconbox.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 7 - text: (combo_box_options && index >= 0) ? combo_box_options[index] : "" - color: XsStyle.controlColor - font.family: XsStyle.controlTitleFontFamily - font.pixelSize: XsStyle.popupControlFontSize - } - - Rectangle { - - id: iconbox - width: iconsize - height: iconsize - - // color: menuItem.checked?XsStyle.highlightColor:"transparent" - color: checkBoxItem.checked ? XsStyle.highlightColor : "transparent" - - anchors.left: parent.left - anchors.leftMargin: 7 - anchors.verticalCenter: parent.verticalCenter - // visible: menuItem.checkable - border.width: 1 // mycheckableDecorated?1:0 - border.color: checkBoxItem.checked ? XsStyle.hoverColor : checkBoxItem.highlighted ? XsStyle.hoverColor : XsStyle.mainColor - - radius: iconsize / 2 - Image { - id: checkIcon - visible: checkBoxItem.checked - sourceSize.height: XsStyle.menuItemHeight - sourceSize.width: XsStyle.menuItemHeight - source: "check" - width: iconsize // 2 - height: iconsize // 2 - anchors.centerIn: parent - } - ColorOverlay{ - id: colorolay - anchors.fill: checkIcon - source:checkIcon - visible: checkBoxItem.checked - color: checkBoxItem.highlighted?XsStyle.hoverColor:XsStyle.hoverColor - antialiasing: true - } - } - - MouseArea { - id: mouse_area - hoverEnabled: true - anchors.fill: parent - onClicked: { - value = thetext.text - popup.visible = false - } - - } - - } - - } - - Rectangle { - - id: divider - anchors.margins: 5 - anchors.left: parent.left - anchors.right: parent.right - anchors.top: combo_group.bottom - height: XsStyle.menuBorderWidth - color: XsStyle.menuBorderColor - } - - Rectangle { - - id: settings_button - anchors.left: parent.left - anchors.right: parent.right - anchors.top: divider.bottom - anchors.margins: 5 - height: XsStyle.menuItemHeight - property var highlighted: mouse_area2.containsMouse - color: highlighted ? XsStyle.highlightColor : "transparent" - gradient: styleGradient.accent_gradient - - Text { - id: thetext - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "Settings ..." - color: XsStyle.controlColor - font.family: XsStyle.controlTitleFontFamily - font.pixelSize: XsStyle.popupControlFontSize - } - - MouseArea { - id: mouse_area2 - hoverEnabled: true - anchors.fill: parent - onClicked: { - launchSettingsDlg() - popup.visible = false - } - } - } - - - } - - function toggleShow() { - if(popup.visible) { - popup.visible = false - } - else{ - var popup_coords = mapToGlobal(0,0) - popup_coords = sessionWidget.mapFromGlobal(popup_coords.x,popup_coords.y) - popup.y = popup_coords.y-popup.height - popup.x = popup_coords.x - popup.visible = true - sessionWidget.activateWidgetHider(hideMe) - } - } - - function hideMe() { - popup.visible = false - } - - function launchSettingsDlg() { - dynamic_widget = Qt.createQmlObject('import xStudio 1.0; XsModuleAttributesDialog { title: \"Viewport Mask Settings"; attributesGroupNames: "viewport_mask_settings"}', settings_button) - dynamic_widget.raise() - dynamic_widget.show() - } - -} diff --git a/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/BasicViewportMask.1/BasicViewportMaskOverlay.qml b/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/BasicViewportMask.1/BasicViewportMaskOverlay.qml index 8777b6f7f..e7e4bc166 100644 --- a/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/BasicViewportMask.1/BasicViewportMaskOverlay.qml +++ b/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/BasicViewportMask.1/BasicViewportMaskOverlay.qml @@ -1,142 +1,192 @@ // SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.3 -import QtQuick 2.14 + +import QtQuick.Layouts +import QtQuick import QuickFuture 1.0 import QuickPromise 1.0 -import xStudio 1.0 -import xstudio.qml.module 1.0 -Rectangle { +// These imports are necessary to have access to custom QML types that are +// part of the xSTUDIO UI implementation. +import xStudio 1.0 +import xstudio.qml.models 1.0 - id: control - color: "transparent" - width: viewport.width - height: viewport.height +// Our Overlay is based on a transparent rectangle that simply fills the +// xSTUDIO view. Within this we draw the overlay graphics as required. +Item { - property var imageBox: viewport.imageBoundaryInViewport + // Note that viewport overlays are instanced by the Viewport QML instance + // which has the id 'view' and is visible to us here. To learn more + // about the viewport see files Xsview.qml and qml_view.cpp from + // the xSTUDIO source code. - onImageBoxChanged: { - computeMask() + id: control + width: view.width + height: view.height + property var imageBoxes: view.imageBoundariesInViewport + visible: renderMethod == "QML" && maskEnabled + + // access the attribute group that contains all the settings for the mask. + // For HUD plugins this is the name of the plugin ("Mask") plus " Settings" + XsModuleData { + id: mask_settings + modelDataName: "Mask Settings" } - XsModuleAttributes { - - id: viewport_mask_settings - attributesGroupNames: "viewport_mask_settings" - onAttrAdded: control.computeMask() - onValueChanged: control.computeMask() + /* We can make connections to a single attribute in the group using the + attribute title */ + XsAttributeValue { + id: mask_aspect_ratio + attributeTitle: "Mask Aspect Ratio" + model: mask_settings } - - XsModuleAttributes { - - id: current_toolbar - attributesGroupNames: viewport.name + "_toolbar" + // make an alias so the aspect is accessible as a regular property to + // set/get the value + property alias maskAspectRatio: mask_aspect_ratio.value + + XsAttributeValue { + id: safety_percent + attributeTitle: "Safety Percent" + model: mask_settings } + property alias safetyPercent: safety_percent.value - // Properties on the XsModuleAttributes items are created at runtime after - // this item is 'completed' and therefore we need to map to local variables - // with checks as follows: - property var mask_aspect_ratio: viewport_mask_settings.mask_aspect_ratio ? viewport_mask_settings.mask_aspect_ratio : 0.0 - property var safety_percent: viewport_mask_settings.safety_percent ? viewport_mask_settings.safety_percent : 0.0 - property var mask_opacity: viewport_mask_settings.mask_opacity ? viewport_mask_settings.mask_opacity : 0.0 - property var mask_line_opacity: viewport_mask_settings.line_opacity ? viewport_mask_settings.line_opacity : 0.0 - property var mask_line_thickness: viewport_mask_settings.line_thickness ? viewport_mask_settings.line_thickness : 0.0 - property var label_size: viewport_mask_settings.label_size ? viewport_mask_settings.label_size : 10.0 - property var mask_name: mask_aspect_ratio.toFixed(2) - property var show_mask_label: viewport_mask_settings.show_mask_label ? viewport_mask_settings.show_mask_label : false - property var mask_render_method: viewport_mask_settings.mask_render_method ? viewport_mask_settings.mask_render_method : "OpenGL" - property var mask_is_on: viewport_mask_settings.mask == "On" - - property var l: 0.0 - property var b: 0.0 - property var r: width - property var t: height - - visible: mask_render_method == "QML" && mask_is_on - - function computeMask() { - - // assuming mask should be 'width fitted' - l = imageBox.x - r = imageBox.x + imageBox.width - var h = imageBox.width/mask_aspect_ratio - b = imageBox.y + (imageBox.height-h)/2.0 - t = imageBox.y + imageBox.height/2 + h/2.0 - - if (safety_percent != 0.0) { - var xd = (r-l)*safety_percent/200.0 - l = l + xd - r = r - xd - var yd = (t-b)*safety_percent/200.0 - b = b + yd - t = t - yd - } + property var mask_name: maskAspectRatio.toFixed(2) + XsAttributeValue { + id: _mask_enabled + attributeTitle: "Mask" + model: mask_settings } + property alias maskEnabled: _mask_enabled.value - Rectangle { - id: top_masking_rect - opacity: mask_opacity - color: "black" - x: 0 - y: 0 - z: -1 - width: control.width - height: b + XsAttributeValue { + id: mask_opacity + attributeTitle: "Mask Opacity" + model: mask_settings } + property alias maskOpacity: mask_opacity.value - Rectangle { - id: bottom_masking_rect - opacity: mask_opacity - color: "black" - x: 0 - y: t - width: control.width - height: control.height-t + XsAttributeValue { + id: mask_line_opacity + attributeTitle: "Line Opacity" + model: mask_settings } + property alias maskLineOpacity: mask_line_opacity.value - Rectangle { - id: left_masking_rect - opacity: mask_opacity - color: "black" - x: 0 - y: b - width: l - height: t-b + XsAttributeValue { + id: mask_line_thickness + attributeTitle: "Line Thickness" + model: mask_settings } + property alias maskLineThickness: mask_line_thickness.value - Rectangle { - id: right_masking_rect - opacity: mask_opacity - color: "black" - x: r - y: b - width: control.width-x - height: t-b + XsAttributeValue { + id: label_size + attributeTitle: "Label Size" + model: mask_settings } + property alias labelSize: label_size.value - Rectangle { - id: lines - opacity: mask_line_opacity - color: "transparent" - border.color: "white" - border.width: mask_line_thickness - x: l-mask_line_thickness/2 - y: b-mask_line_thickness/2 - width: r-l+mask_line_thickness - height: t-b+mask_line_thickness + XsAttributeValue { + id: show_mask_label + attributeTitle: "Show Mask Label" + model: mask_settings } + property alias showMaskLabel: show_mask_label.value - Text { - text: mask_name - opacity: mask_line_opacity - visible: show_mask_label - color: "white" - font.pixelSize: label_size - anchors.left: lines.left - anchors.bottom: lines.top - anchors.bottomMargin: 4 + XsAttributeValue { + id: render_method + attributeTitle: "Mask Render Method" + model: mask_settings + } + property alias renderMethod: render_method.value + + property bool mask_defined: maskAspectRatio > 0.0 + + property var safety: safetyPercent/200.0 + + Repeater { + + model: imageBoxes + Item { + + // Viewport class provides imageBoxes - the coordinates of each + // image within the viewport, in viewport pixels + property var imageBox: imageBoxes[index] ? imageBoxes[index] : Qt.QRectF() + + x: imageBox.x + y: imageBox.y + height: imageBox.height + width: imageBox.width + + property var l: width*safety + property var b: (height-(width*(1.0-safety*maskAspectRatio)/maskAspectRatio))/2.0 + property var r: width*(1.0 - safety) + property var t: (height+(width*(1.0-safety*maskAspectRatio)/maskAspectRatio))/2.0 + + Rectangle { + id: bottom_masking_rect + opacity: maskOpacity + color: "black" + x: 0 + y: 0 + width: parent.width + height: b + } + + Rectangle { + id: top_masking_rect + opacity: maskOpacity + color: "black" + x: 0 + y: t + width: parent.width + height: parent.height-t + } + + Rectangle { + id: left_masking_rect + opacity: maskOpacity + color: "black" + x: 0 + y: b + width: l + height: t-b + } + + Rectangle { + id: right_masking_rect + opacity: maskOpacity + color: "black" + x: r + y: b + width: parent.width-r + height: t-b + } + + Rectangle { + id: lines + opacity: maskLineOpacity + color: "transparent" + border.color: "white" + border.width: maskLineThickness + x: l-maskLineThickness/2 + y: b-maskLineThickness/2 + width: r-l+maskLineThickness + height: t-b+maskLineThickness + } + + Text { + text: mask_name + opacity: maskLineOpacity + visible: showMaskLabel + color: "white" + font.pixelSize: labelSize + anchors.left: lines.left + anchors.bottom: lines.top + anchors.bottomMargin: 4 + } + } } } diff --git a/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/BasicViewportMask.1/check.svg b/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/BasicViewportMask.1/check.svg old mode 100755 new mode 100644 diff --git a/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/BasicViewportMask.1/qmldir b/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/BasicViewportMask.1/qmldir index e385613df..7d967ebf1 100644 --- a/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/BasicViewportMask.1/qmldir +++ b/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/BasicViewportMask.1/qmldir @@ -1,4 +1,3 @@ module BasicViewportMask -BasicViewportMaskButton 1.0 BasicViewportMaskButton.qml BasicViewportMaskOverlay 1.0 BasicViewportMaskOverlay.qml \ No newline at end of file diff --git a/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/CMakeLists.txt b/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/CMakeLists.txt deleted file mode 100644 index 2dd4119c1..000000000 --- a/src/plugin/viewport_overlay/basic_viewport_mask/src/qml/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -project(basic_viewport_ui VERSION 0.1.0 LANGUAGES CXX) - -if(WIN32) -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/BasicViewportMask.1/ DESTINATION ${CMAKE_INSTALL_PREFIX}/plugin/qml/BasicViewportMask.1) -else() -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/BasicViewportMask.1/ DESTINATION share/xstudio/plugin/qml/BasicViewportMask.1) -endif() - -add_custom_target(COPY_BVP_QML ALL) - -add_custom_command(TARGET COPY_BVP_QML POST_BUILD - COMMAND ${CMAKE_COMMAND} -E - copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/BasicViewportMask.1 ${CMAKE_BINARY_DIR}/bin/plugin/qml/BasicViewportMask.1) - diff --git a/src/plugin/viewport_overlay/media_metadata_hud/src/CMakeLists.txt b/src/plugin/viewport_overlay/media_metadata_hud/src/CMakeLists.txt new file mode 100644 index 000000000..d058c9c46 --- /dev/null +++ b/src/plugin/viewport_overlay/media_metadata_hud/src/CMakeLists.txt @@ -0,0 +1,13 @@ + +SET(LINK_DEPS + xstudio::module + xstudio::plugin_manager + xstudio::ui::opengl::viewport + Imath::Imath +) + +find_package(Imath) + +create_plugin_with_alias(media_metadata_hud xstudio::viewport::media_metadata_hud ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") + +add_plugin_qml(${PROJECT_NAME} qml) \ No newline at end of file diff --git a/src/plugin/viewport_overlay/media_metadata_hud/src/media_metadata_hud.cpp b/src/plugin/viewport_overlay/media_metadata_hud/src/media_metadata_hud.cpp new file mode 100644 index 000000000..118cb87dd --- /dev/null +++ b/src/plugin/viewport_overlay/media_metadata_hud/src/media_metadata_hud.cpp @@ -0,0 +1,1097 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "media_metadata_hud.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/chrono.hpp" +#include "xstudio/media_reader/image_buffer_set.hpp" + +#ifdef __apple__ +#include +#else +#include +#include +#endif + +using namespace xstudio; +using namespace xstudio::ui::viewport; + +namespace { +const char *vertex_shader = R"( + #version 330 core + layout (location = 0) in vec4 aPos; + uniform mat4 tform; + uniform vec2 bdb_min; + uniform vec2 bdb_max; + + void main() + { + vec4 rpos = aPos; + rpos.x = bdb_min.x + (bdb_max.x - bdb_min.x)*(rpos.x + 1.0f)*0.5f; + rpos.y = bdb_min.y + (bdb_max.y - bdb_min.y)*(rpos.y + 1.0f)*0.5f; + gl_Position = (rpos*tform); + } + )"; + +const char *frag_shader = R"( + #version 330 core + out vec4 FragColor; + uniform float opacity; + + void main(void) + { + FragColor = vec4(0.0, 0.0, 0.0, opacity); + } + + )"; + +inline size_t __hash_combine(size_t lhs, size_t rhs) { + lhs ^= rhs + 0x9e3779b9 + (lhs << 6) + (lhs >> 2); + return lhs; +} + +} // namespace + +void MediaMetadataRenderer::render_image_overlay( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + const float viewport_du_dpixel, + const xstudio::media_reader::ImageBufPtr &frame, + const bool have_alpha_buffer) { + + auto t0 = utility::clock::now(); + + if (!text_renderer_) { + init_overlay_opengl(); + } + + auto render_data = + frame.plugin_blind_data(utility::Uuid("f3e7c2db-2578-45d6-8ad5-743779057a63")); + const auto *data = dynamic_cast(render_data.get()); + if (!data) { + return; + } + + + // the gl viewport corresponds to the parent window size. + // TODO: For Qt6 viewport dims will be passed into this function, we can't + // read viewport like this + std::array gl_viewport; + glGetIntegerv(GL_VIEWPORT, gl_viewport.data()); + const auto viewport_width = (float)gl_viewport[2]; + const auto viewport_height = (float)gl_viewport[3]; + + if (display_settings_ != data->display_settings_) { + + display_settings_ = data->display_settings_; + } + glDisable(GL_BLEND); + + + // draw BG boxes + if (display_settings_->bg_opacity > 0.0f) { + glBindVertexArray(vao_); + utility::JsonStore bdb_param; + bdb_param["opacity"] = display_settings_->bg_opacity; + shader_->use(); + glEnableVertexAttribArray(0); + glEnable(GL_BLEND); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE); + glBlendEquation(GL_FUNC_ADD); + auto p = data->positions_.begin(); + for (const auto &bdb : data->bdbs_) { + Imath::M44f m2; + // 'anchor' position of text: -1.0,-1.0 is top left of viewport, + // 1.0, 1.0 is bottom right + Imath::V4f anchor = *p; + // the gl viewport is set to the entire window size, not the viewport + // within the window. So we use 'transform_window_to_viewport_space' + // matrx here ... + anchor *= transform_window_to_viewport_space; + + // Now scale by viewport size, with a reference size of 1920 which + // we used earlier to define the text size in pixels + m2.scale(Imath::V3f(viewport_width / 1920.0f, -viewport_height / 1920.0f, 1.0f)); + m2.translate(Imath::V3f(-anchor.x / anchor.w, -anchor.y / anchor.w, 0.0f)); + bdb_param["bdb_min"] = bdb.min; + bdb_param["bdb_max"] = bdb.max; + bdb_param["tform"] = m2.inverse(); + shader_->set_shader_parameters(bdb_param); + glDrawArrays(GL_TRIANGLES, 0, 6); + p++; + } + glDisable(GL_BLEND); + shader_->stop_using(); + glDisableVertexAttribArray(0); + glBindVertexArray(0); + } + + // draw text + auto p = data->positions_.begin(); + for (const auto &v : data->verts_) { + + Imath::V4f anchor = *p; + p++; + anchor *= transform_window_to_viewport_space; + + Imath::M44f m2; + m2.scale(Imath::V3f(viewport_width / 1920.0f, -viewport_height / 1920.0f, 1.0f)); + m2.translate(Imath::V3f(-anchor.x / anchor.w, -anchor.y / anchor.w, 0.0f)); + + text_renderer_->render_text( + *v, + Imath::M44f(), + m2, + display_settings_->text_colour, + 1.0 / 1920.0, + display_settings_->font_size, + display_settings_->text_opacity); + } +} + +void MediaMetadataRenderer::init_overlay_opengl() { + + text_renderer_ = std::make_unique( + utility::xstudio_resources_dir("fonts/VeraMono.ttf"), 96); + + // Set up the geometry used at draw time ... it couldn't be more simple, + // it's just two triangles to make a rectangle + glGenBuffers(1, &vbo_); + glGenVertexArrays(1, &vao_); + + static std::array vertices = { + // 1st triangle + -1.0f, + 1.0f, + 0.0f, + 1.0f, // top left + 1.0f, + 1.0f, + 0.0f, + 1.0f, // top right + 1.0f, + -1.0f, + 0.0f, + 1.0f, // bottom right + // 2nd triangle + 1.0f, + -1.0f, + 0.0f, + 1.0f, // bottom right + -1.0f, + 1.0f, + 0.0f, + 1.0f, // top left + -1.0f, + -1.0f, + 0.0f, + 1.0f // bottom left + }; + + glBindVertexArray(vao_); + // 2. copy our vertices array in a buffer for OpenGL to use + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices.data(), GL_STATIC_DRAW); + // 3. then set our vertex module pointers + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr); + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + shader_ = std::make_unique(vertex_shader, frag_shader); +} + +MediaMetadataHUD::MediaMetadataHUD( + caf::actor_config &cfg, const utility::JsonStore &init_settings) + : plugin::HUDPluginBase(cfg, "Media Metadata", init_settings, 2.0f), + overlay_font_(utility::xstudio_resources_dir("fonts/VeraMono.ttf"), 96) { + + bg_opacity_ = + add_float_attribute("Background Opacity", "Bg. Opac.", 1.0f, 0.0f, 1.0f, 0.1f); + add_hud_settings_attribute(bg_opacity_); + bg_opacity_->set_tool_tip("Sets the opacity of the background behind the overlay."); + bg_opacity_->set_role_data( + module::Attribute::PreferencePath, "/plugin/media_metadata/bg_opacity"); + + text_size_ = add_float_attribute("Text Size", "Text Size", 16.0f, 10.0f, 40.0f, 0.1f); + add_hud_settings_attribute(text_size_); + text_size_->set_tool_tip("Sets the text size of the metadata overlay."); + text_size_->set_role_data( + module::Attribute::PreferencePath, "/plugin/media_metadata/text_size"); + + text_opacity_ = add_float_attribute("Text Opacity", "Txt. Opac.", 1.0f, 0.0f, 1.0f, 0.1f); + add_hud_settings_attribute(text_opacity_); + text_opacity_->set_tool_tip("Sets the opacity of the text."); + text_opacity_->set_role_data( + module::Attribute::PreferencePath, "/plugin/media_metadata/text_opacity"); + + text_colour_ = add_colour_attribute( + "Text Colour", "Txt. Clr.", utility::ColourTriplet(1.0f, 1.0f, 1.0f)); + add_hud_settings_attribute(text_colour_); + text_colour_->set_role_data( + module::Attribute::PreferencePath, "/plugin/media_metadata/text_colour"); + + text_spacing_ = add_float_attribute("Text Spacing", "Txt. Spg.", 1.0f, 1.0f, 3.0f, 0.1f); + add_hud_settings_attribute(text_spacing_); + text_spacing_->set_role_data( + module::Attribute::PreferencePath, "/plugin/media_metadata/text_spacing"); + + inset_ = add_float_attribute("Caption Inset %", "Inset", 0.0f, 0.0f, 20.0f, 0.1f); + add_hud_settings_attribute(inset_); + inset_->set_role_data( + module::Attribute::PreferencePath, "/plugin/media_metadata/caption_inset"); + + hide_missing_fields_ = + add_boolean_attribute("Hide Field if Data is Missing", "Hide Field", false); + add_hud_settings_attribute(hide_missing_fields_); + hide_missing_fields_->set_role_data( + module::Attribute::PreferencePath, "/plugin/media_metadata/hide_missing_fields"); + + current_profile_ = add_string_choice_attribute( + "Current Profile", "Prfl", "Default", {"Default"}, {"Default"}); + current_profile_->expose_in_ui_attrs_group("metadata_hud_attrs", true); + current_profile_->set_role_data( + module::Attribute::PreferencePath, + "/plugin/media_metadata/metadata_hud_current_profile"); + + search_string_ = add_string_attribute("Search String", "Srch. strg", ""); + search_string_->expose_in_ui_attrs_group("metadata_hud_attrs", true); + + full_metadata_set_ = add_json_attribute("Full Onscreen Image Metadata"); + full_metadata_set_->expose_in_ui_attrs_group("metadata_hud_attrs", true); + + profile_data_ = add_json_attribute("Profile Fields Data"); + profile_data_->expose_in_ui_attrs_group("metadata_hud_attrs", true); + + profile_values_ = add_json_attribute("Profile Metadata Values"); + profile_values_->expose_in_ui_attrs_group("metadata_hud_attrs", true); + + profile_fields_ = add_json_attribute("Profile Fields"); + profile_fields_->expose_in_ui_attrs_group("metadata_hud_attrs", true); + + config_ = add_json_attribute("Configuration"); + config_->set_role_data( + module::Attribute::PreferencePath, "/plugin/media_metadata/metadata_hud_config"); + config_->set_value(R"({"Default":[]})"_json); + config_data_ = config_->value(); + + action_attr_ = add_action_attribute("Action", "Action"); + action_attr_->expose_in_ui_attrs_group("metadata_hud_attrs", true); + + set_custom_settings_qml( + R"( + import MediaMetadataHUD 1.0 + MediaMetadataHUDSettings { + } + )"); + + // call this to initialise display_settings_ member ptr + attribute_changed(utility::Uuid(), module::Attribute::Value); +} + +void MediaMetadataHUD::register_hotkeys() { + + show_viewer_hotkey_ = register_hotkey( + int('Q'), + ui::NoModifier, + "Show Metadata Viewrt", + "Shows or hides the pop-out Media Metadata Panel"); +} + +void MediaMetadataHUD::hotkey_pressed( + const utility::Uuid &hotkey_uuid, + const std::string &context, + const std::string & /*window*/) { + + // This shortcut should be active even when no grading panel is visible + if (hotkey_uuid == show_viewer_hotkey_ && context.find("viewport") == 0) { + show_metadata_viewer_for_viewport(context); + } +} + +void MediaMetadataHUD::attribute_changed(const utility::Uuid &attribute_uuid, const int role) { + if (attribute_uuid == action_attr_->uuid()) { + + const auto action_data = + action_attr_->get_role_data>(module::Attribute::Value); + if (action_data.size() == 1) { + if (action_data[0] == "DELETE CURRENT_PROFILE") { + delete_current_profile(); + } + } else if (action_data.size() == 2) { + if (action_data[0] == "ADD PROFILE") { + add_new_profile(action_data[1]); + } else if (action_data[0] == "SHOWING METADATA VIEWER") { + metadata_viewer_viewport_name_ = action_data[1]; + } else if (action_data[0] == "HIDING METADATA VIEWER") { + metadata_viewer_viewport_name_.clear(); + } else if (action_data[0] == "TOGGLE METADATA FIELD SELECTION") { + profile_toggle_field(action_data[1]); + } + } else if (action_data.size() == 3) { + if (action_data[0] == "CHANGE FIELD LABEL") { + set_profile_field_label(action_data[1], action_data[2]); + } else if (action_data[0] == "CHANGE SCREEN POSITION") { + set_field_screen_position(action_data[1], action_data[2]); + } + } + + // clear the attr ready for next 'action' (without notifying ourselves) + action_attr_->set_role_data( + module::Attribute::Value, std::vector(), false); + + } else if (attribute_uuid == search_string_->uuid()) { + + search_filter_string_ = utility::to_lower(search_string_->value()); + update_full_metadata_set(true); + + } else if (attribute_uuid == config_->uuid() && role == module::Attribute::Value) { + + // config has been updated from prefs (or maybe and API call) + config_data_ = config_->value(); + StringVec profiles; + for (auto p = config_data_.begin(); p != config_data_.end(); ++p) { + profiles.push_back(p.key()); + } + current_profile_->set_role_data(module::Attribute::StringChoices, profiles); + if (config_data_.contains(current_profile_->value())) { + profile_data_store_ = config_data_[current_profile_->value()]; + profile_changed(); + } + + } else if (attribute_uuid == current_profile_->uuid() && role == module::Attribute::Value) { + + // config has been updated from prefs (or maybe and API call) + if (config_data_.contains(current_profile_->value())) { + profile_data_store_ = config_data_[current_profile_->value()]; + profile_changed(); + } + + } else { + + // update display settings + DisplaySettings *display_settings = new DisplaySettings; + display_settings->text_colour = text_colour_->value(); + display_settings->font_size = text_size_->value(); + display_settings->text_opacity = text_opacity_->value(); + display_settings->bg_opacity = bg_opacity_->value(); + display_settings->text_spacing = text_spacing_->value(); + display_settings_.reset(display_settings); + render_data_per_metadata_cache_.clear(); + render_data_per_image_cache_.clear(); + } + redraw_viewport(); + plugin::HUDPluginBase::attribute_changed(attribute_uuid, role); +} + +MediaMetadataHUD::~MediaMetadataHUD() = default; + +void MediaMetadataHUD::media_due_on_screen_soon( + const media::AVFrameIDsAndTimePoints &frame_ids) { + + // this callback is made during playback - it gives us a list of frame IDs, one for each + // individual piece of media that is expected to be put on the screen in the immediate + // future + for (const auto &p : frame_ids) { + update_media_metadata_for_media(*(p.second)); + } +} + +utility::BlindDataObjectPtr MediaMetadataHUD::onscreen_render_data( + const media_reader::ImageBufPtr &image, + const std::string &viewport_name, + const utility::Uuid &playhead_uuid, + const bool is_hero_image) const { + + // this method is called *just* before the given image is drawn into the + // given named viewport. + // This method is thread-safe as far as our plugin is concerned, but it + // blocks the viewport redraw so it must be very fast . + + // We return a pointer to our own data that has already been pre-fetched + // elsewhere. + + auto r = utility::BlindDataObjectPtr(); + + + if (visible() && is_hero_image) { + + // do we need to capture image metadata? + const auto key = + to_string(image.frame_id().key()) + to_string(image.frame_id().source_uuid()); + + auto p = render_data_per_image_cache_.find(key); + if (p != render_data_per_image_cache_.end()) { + p->second->last_used_ = utility::clock::now(); + return p->second; + } + + return (const_cast(this))->make_onscreen_data(image, key); + } + + return r; +} + +MediaMetadataPtr MediaMetadataHUD::make_onscreen_data( + const media_reader::ImageBufPtr &image, const std::string &key) { + + try { + + static const utility::JsonStore empty_store; + + const auto p = media_metadata_.find(image.frame_id().source_uuid()); + const utility::JsonStore &media_metadata = + p != media_metadata_.end() ? *(p->second) : empty_store; + const utility::JsonStore image_metadata = image.metadata(); + if (p != media_metadata_.end()) { + p->second.last_used_ = utility::clock::now(); + } + + + size_t hash = __hash_combine( + std::hash{}(media_metadata.ref()), + std::hash{}(image_metadata.ref())); + + auto cache_entry = render_data_per_metadata_cache_.find(hash); + if (cache_entry != render_data_per_metadata_cache_.end()) { + cache_entry->second->last_used_ = utility::clock::now(); + return cache_entry->second; + } + + + const bool hide_missing_fields = hide_missing_fields_->value(); + std::map positioned_text; + + for (const auto &q : profile_essential_data_) { + + std::string v; + if (!q.label.empty()) { + v = q.label + ": "; + } + if (q.is_from_image) { + if (image_metadata.contains(q.real_metadata_path) && + !image_metadata.at(q.real_metadata_path).is_null()) { + if (image_metadata.at(q.real_metadata_path).is_string()) { + v += image_metadata.at(q.real_metadata_path).get(); + } else { + v += image_metadata.at(q.real_metadata_path).dump(); + } + } else if (hide_missing_fields) { + continue; + } else { + v += "--"; + } + } else { + if (media_metadata.contains(q.real_metadata_path) && + !media_metadata.at(q.real_metadata_path).is_null()) { + if (media_metadata.at(q.real_metadata_path).is_string()) { + v += media_metadata.at(q.real_metadata_path).get(); + } else { + v += media_metadata.at(q.real_metadata_path).dump(); + } + } else if (hide_missing_fields) { + continue; + } else { + v += "--"; + } + } + + if (positioned_text.find(q.screen_position) == positioned_text.end()) { + positioned_text[q.screen_position].reset(new std::string()); + } else { + *(positioned_text[q.screen_position]) += "\n"; + } + *(positioned_text[q.screen_position]) += v; + } + + MediaMetadataPtr result(new MediaMetadata(display_settings_)); + + for (const auto &p : positioned_text) { + + Justification justification = JustifyLeft; + VerticalJustification vjust = JustifyTop; + + Imath::V4f anchor; + const float inset = inset_->value() / 100.0f; + if (p.first == plugin::TopLeft) { + anchor = Imath::V4f(-1.0 + inset, 1.0 - inset, 0.0, 1.0f); + } else if (p.first == plugin::TopCenter) { + justification = JustifyCentre; + anchor = Imath::V4f(0.0, 1.0 - inset, 0.0, 1.0f); + } else if (p.first == plugin::TopRight) { + justification = JustifyRight; + anchor = Imath::V4f(1.0 - inset, 1.0 - inset, 0.0, 1.0f); + } else if (p.first == plugin::BottomLeft) { + anchor = Imath::V4f(inset - 1.0, inset - 1.0, 0.0, 1.0f); + vjust = JustifyBottom; + } else if (p.first == plugin::BottomCenter) { + justification = JustifyCentre; + anchor = Imath::V4f(0.0, inset - 1.0, 0.0, 1.0f); + vjust = JustifyBottom; + } else if (p.first == plugin::BottomRight) { + justification = JustifyRight; + anchor = Imath::V4f(1.0 - inset, inset - 1.0, 0.0, 1.0f); + vjust = JustifyBottom; + } + + Imath::Box2f bdba = overlay_font_.precompute_text_rendering_vertex_layout( + result->make_empty_vec_for_text_render_vtxs(anchor), + *(p.second), + Imath::V2f(0.0f, 0.0f), + 0.0f, + display_settings_->font_size, + justification, + display_settings_->text_spacing, + true, + vjust); + + result->add_bounding_box(bdba); + } + + if (p != media_metadata_.end()) { + render_data_per_image_cache_[key] = result; + render_data_per_metadata_cache_[hash] = result; + } + trim_cache(); + + return result; + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + return MediaMetadataPtr(); +} + +void MediaMetadataHUD::trim_cache() { + + while (render_data_per_image_cache_.size() > 16) { + auto p = render_data_per_image_cache_.begin(); + auto q = p; + auto tp = p->second->last_used_; + while (p != render_data_per_image_cache_.end()) { + if (p->second->last_used_ < tp) { + tp = p->second->last_used_; + q = p; + } + p++; + } + render_data_per_image_cache_.erase(q); + } + + while (render_data_per_metadata_cache_.size() > 16) { + auto p = render_data_per_metadata_cache_.begin(); + auto q = p; + auto tp = p->second->last_used_; + while (p != render_data_per_metadata_cache_.end()) { + if (p->second->last_used_ < tp) { + tp = p->second->last_used_; + q = p; + } + p++; + } + render_data_per_metadata_cache_.erase(q); + } + + while (media_metadata_.size() > 16) { + auto p = media_metadata_.begin(); + auto q = p; + auto tp = p->second.last_used_; + while (p != media_metadata_.end()) { + if (p->second.last_used_ < tp) { + tp = p->second.last_used_; + q = p; + } + p++; + } + media_metadata_.erase(q); + } +} + + +void MediaMetadataHUD::images_going_on_screen( + const media_reader::ImageBufDisplaySetPtr &image_set, + const std::string viewport_name, + const bool playhead_playing) { + + // this method is called a few tens of milliseconds before the given image + // set will actually draw to the screen. It does not block viewport redraw + // but we want it to be fast so that any on-screen data that we are showing + // is updated and ready. + + on_screen_images_[viewport_name] = image_set->hero_image(); + + if (!playhead_playing) { + if (metadata_viewer_viewport_name_ == viewport_name) { + update_full_metadata_set(); + update_metadata_values_for_profile(); + } + } + + if (visible()) { + update_media_metadata_for_media(image_set->hero_image().frame_id(), false); + } +} + +void MediaMetadataHUD::update_profile_metadata() { + + // The contents of the metadata_selection_ attr should be a json array, + // each element should be an array of 2 elements. The first is the metadata + // path/key and the second is the user display name of the metata field. + + // We need to separate these required metadata into string vectors of + // metadata paths for each of Image, Media and Media Metadata. We also + // need to record the ordering of the metadata items so we can re-assemble + // the metadata from Image, Media and Media Metadata into one set for display. + + media_selected_metadata_fields_.clear(); + media_source_selected_metadata_fields_.clear(); + image_selected_metadata_fields_.clear(); + profile_essential_data_.clear(); + + for (size_t i = 0; i < profile_data_store_.size(); ++i) { + const nlohmann::json &field_data = profile_data_store_[i]; + const auto field_path = field_data["metadata_field"].get(); + const auto field_name = field_data["metadata_field_label"].get(); + const plugin::HUDElementPosition screen_pos = + (plugin::HUDElementPosition)field_data["screen_position"].get(); + + std::string stripped_path; + bool is_image_metadata = false; + if (field_path.find("Pipeline Metadata/") == 0) { + + stripped_path = std::string(field_path, 17); + media_selected_metadata_fields_.push_back(stripped_path); + + } else if (field_path.find("Media Metadata/") == 0) { + + stripped_path = std::string(field_path, 14); + media_source_selected_metadata_fields_.push_back(stripped_path); + + } else if (field_path.find("Frame Metadata/") == 0) { + + stripped_path = std::string(field_path, 15); + image_selected_metadata_fields_.push_back(stripped_path); + is_image_metadata = true; + } + + ProfileFieldItem item; + item.is_from_image = is_image_metadata; + item.real_metadata_path = stripped_path; + item.label = field_name; + item.screen_position = screen_pos; + profile_essential_data_.push_back(std::move(item)); + } + + media_metadata_.clear(); + // update our cache for on-screen media + for (auto p : on_screen_images_) { + update_media_metadata_for_media( + p.second.frame_id(), p.first == metadata_viewer_viewport_name_); + } +} + +void MediaMetadataHUD::update_media_metadata_for_media( + const media::AVFrameID frame_id, const bool update_profile_attr) { + + const auto media_source_actor = frame_id.media_source_actor(); + + if (media_metadata_.find(media_source_actor.uuid()) != media_metadata_.end() || + !media_source_actor) { + // already have up-to-date metadata for this media item + return; + } + + const auto media_actor = frame_id.media_actor(); + + utility::JsonStore *result = new utility::JsonStore; + mail(json_store::get_json_atom_v, media_selected_metadata_fields_) + .request(media_actor.actor(), infinite) + .then( + [=](utility::JsonStore selected_media_metadata) mutable { + result->merge(selected_media_metadata); + + mail(json_store::get_json_atom_v, media_source_selected_metadata_fields_) + .request(media_source_actor.actor(), infinite) + .then( + [=](utility::JsonStore selected_media_source_metadata) mutable { + result->merge(selected_media_source_metadata); + media_metadata_[media_source_actor.uuid()].reset(result); + if (update_profile_attr) + update_metadata_values_for_profile(true); + }, + [=](error &err) mutable { + media_metadata_[media_source_actor.uuid()].reset(result); + if (update_profile_attr) + update_metadata_values_for_profile(true); + }); + }, + [=](error &err) mutable { + media_metadata_[media_source_actor.uuid()].reset(result); + if (update_profile_attr) + update_metadata_values_for_profile(true); + }); +} + +void MediaMetadataHUD::update_metadata_values_for_profile(const bool force_update) { + + auto q = on_screen_images_.find(metadata_viewer_viewport_name_); + if (q == on_screen_images_.end()) + return; + + const media_reader::ImageBufPtr &image = q->second; + + if (image == previous_metadata_source_image_ && !force_update) + return; + + nlohmann::json result = nlohmann::json::array(); + const auto p = media_metadata_.find(image.frame_id().source_uuid()); + if (p != media_metadata_.end()) { + p->second.last_used_ = utility::clock::now(); + } + + static const utility::JsonStore empty_store; + const utility::JsonStore &media_metadata = + p != media_metadata_.end() ? *(p->second) : empty_store; + const utility::JsonStore image_metadata = image.metadata(); + + for (const auto &q : profile_essential_data_) { + + if (q.is_from_image) { + if (image_metadata.contains(q.real_metadata_path)) { + result.push_back(image_metadata[q.real_metadata_path]); + } else { + result.push_back("--"); + } + } else { + if (media_metadata.contains(q.real_metadata_path)) { + result.push_back(media_metadata[q.real_metadata_path]); + } else { + result.push_back("--"); + } + } + } + + profile_values_->set_value(result, false); +} + +void MediaMetadataHUD::profile_changed() { + + StringVec profile_selected_fields; + StringVec profile_selected_field_labels; + for (size_t i = 0; i < profile_data_store_.size(); ++i) { + const nlohmann::json &field_data = profile_data_store_[i]; + profile_selected_fields.push_back(field_data["metadata_field"].get()); + profile_selected_field_labels.push_back( + field_data["metadata_field_label"].get()); + } + + profile_data_->set_value(profile_data_store_); + profile_fields_->set_value(profile_selected_fields); + + if (config_data_[current_profile_->value()] != profile_data_store_) { + config_data_[current_profile_->value()] = profile_data_store_; + config_->set_value(config_data_, false); + } + + update_profile_metadata(); +} + +void MediaMetadataHUD::connect_to_viewport( + const std::string &viewport_name, + const std::string &viewport_toolbar_name, + bool connect, + caf::actor viewport) { + + // Here we can add attrs to show up in the viewer context menu (right click) + std::string viewport_context_menu_model_name = viewport_name + "_context_menu"; + if (viewport_menus_.find(viewport_name) == viewport_menus_.end()) { + viewport_menus_[viewport_name] = insert_menu_item( + viewport_context_menu_model_name, + "Metadata Viewer", + "", + 100.0f, + nullptr, + false, + show_viewer_hotkey_, + viewport_name); + } + Module::connect_to_viewport(viewport_name, viewport_toolbar_name, connect, viewport); +} + +void MediaMetadataHUD::menu_item_activated( + const utility::JsonStore &menu_item_data, const std::string &user_data) { + utility::Uuid menu_item_id = menu_item_data.value("uuid", utility::Uuid()); + for (const auto &p : viewport_menus_) { + if (p.second == menu_item_id) { + show_metadata_viewer_for_viewport(p.first); + } + } +} + +void MediaMetadataHUD::show_metadata_viewer_for_viewport(const std::string viewport_name) { + // reveal metadata viewer for a given viewport. The viewport name is + // 'user_data' which was passed in when we create menu items aboc + if (metadata_viewers_.find(viewport_name) == metadata_viewers_.end()) { + metadata_viewers_[viewport_name] = add_qml_code_attribute( + viewport_name + " metadata viewer attr", + R"( + import MediaMetadataHUD 1.0 + MediaMetadataViewerWindow { + } + )"); + metadata_viewers_[viewport_name]->set_role_data(module::Attribute::Enabled, false); + + // this is the 'magic'! The data in this attr will be injected into a QML model + // which then automatically executes the code above. + metadata_viewers_[viewport_name]->expose_in_ui_attrs_group( + viewport_name + " dynamic widgets", true); + metadata_viewers_[viewport_name]->set_role_data(module::Attribute::Enabled, true); + + } else { + metadata_viewers_[viewport_name]->set_role_data( + module::Attribute::Enabled, + !metadata_viewers_[viewport_name]->get_role_data(module::Attribute::Enabled)); + } +} + + +void MediaMetadataHUD::profile_toggle_field(const std::string &field) { + + for (auto p = profile_data_store_.begin(); p != profile_data_store_.end(); ++p) { + if (p.value()["metadata_field"].get() == field) { + profile_data_store_.erase(p); + profile_changed(); + return; + } + } + + nlohmann::json field_data; + field_data["metadata_field"] = field; + field_data["screen_position"] = plugin::TopLeft; + if (field.rfind("/") != std::string::npos) { + field_data["metadata_field_label"] = std::string(field, field.rfind("/") + 1); + } else { + field_data["metadata_field_label"] = field; + } + profile_data_store_.push_back(field_data); + + profile_changed(); +} + +void MediaMetadataHUD::set_profile_field_label( + const std::string &field, const std::string &field_label) { + for (auto p = profile_data_store_.begin(); p != profile_data_store_.end(); ++p) { + if (p.value()["metadata_field"].get() == field) { + p.value()["metadata_field_label"] = field_label; + break; + } + } + profile_changed(); +} + +void MediaMetadataHUD::set_field_screen_position( + const std::string &field, const std::string &screen_position) { + + static const std::map positions( + {{"BottomLeft", plugin::BottomLeft}, + {"BottomCenter", plugin::BottomCenter}, + {"BottomRight", plugin::BottomRight}, + {"TopLeft", plugin::TopLeft}, + {"TopCenter", plugin::TopCenter}, + {"TopRight", plugin::TopRight}, + {"FullScreen", plugin::FullScreen}}); + + + auto q = positions.find(screen_position); + if (q != positions.end()) { + for (auto p = profile_data_store_.begin(); p != profile_data_store_.end(); ++p) { + if (p.value()["metadata_field"].get() == field) { + p.value()["screen_position"] = q->second; + break; + } + } + } + profile_changed(); +} + + +void MediaMetadataHUD::add_new_profile(const std::string &name) { + + if (!config_data_.contains(name)) { + config_data_[name] = nlohmann::json::array(); + profile_data_store_ = nlohmann::json::array(); + StringVec profiles; + for (auto p = config_data_.begin(); p != config_data_.end(); ++p) { + profiles.push_back(p.key()); + } + config_->set_value(config_data_, false); + current_profile_->set_role_data(module::Attribute::StringChoices, profiles); + current_profile_->set_value(name); + profile_changed(); + } +} + +void MediaMetadataHUD::delete_current_profile() { + + if (config_data_.contains(current_profile_->value()) && config_data_.size() > 1) { + + config_data_.erase(config_data_.find(current_profile_->value())); + StringVec profiles; + for (auto p = config_data_.begin(); p != config_data_.end(); ++p) { + profiles.push_back(p.key()); + } + config_->set_value(config_data_, false); + current_profile_->set_role_data(module::Attribute::StringChoices, profiles, false); + current_profile_->set_value(profiles.front()); + profile_changed(); + } +} + + +void MediaMetadataHUD::update_full_metadata_set(const bool full_rebuild) { + + // lambda to get metadata from a given actor and then run new_metadata + // with the actor type + auto get_metadata_from_actor = [=](caf::actor a, const std::string who) { + mail(json_store::get_json_atom_v, std::string("/metadata")) + .request(a, infinite) + .then( + [=](const utility::JsonStore &source_metadata) mutable { + new_metadata(who, source_metadata, full_rebuild); + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + new_metadata(who); + }); + }; + + auto p = on_screen_images_.find(metadata_viewer_viewport_name_); + if (p == on_screen_images_.end() || !p->second) { + // set metadata to nothing + full_metadata_set_->set_value(nlohmann::json()); + return; + } + const media_reader::ImageBufPtr &image = p->second; + + if (image->media_key() != metadata_frame_key_ || full_rebuild) { + + metadata_frame_key_ = image->media_key(); + new_metadata("Frame Metadata", p->second.metadata(), full_rebuild); + } + + if (metadata_media_id_ != image.frame_id().source_uuid() || full_rebuild) { + + metadata_media_id_ = image.frame_id().source_uuid(); + auto media_source_actor = + caf::actor_cast(image.frame_id().media_source_addr()); + if (media_source_actor) { + get_metadata_from_actor(media_source_actor, "Media Metadata"); + } else { + new_metadata("Media Metadata"); + } + auto media_actor = caf::actor_cast(image.frame_id().media_addr()); + if (media_actor) { + get_metadata_from_actor(media_actor, "Pipeline Metadata"); + } else { + new_metadata("Pipeline Metadata"); + } + } +} + +void flatten( + nlohmann::json &result, + const nlohmann::json &obj, + const std::string key, + const std::string real_path, + const std::string &search_filter_string) { + if (obj.is_null()) + return; + // special case + if (obj.is_object() && !obj.is_array()) { + for (auto p = obj.begin(); p != obj.end(); ++p) { + if (p.key() == "streams" && p.value().is_array()) { + for (int i = 0; i < p.value().size(); ++i) { + flatten( + result, + p.value()[i], + key + fmt::format("{}.{}", p.key(), i) + ".", + real_path + "/" + fmt::format("{}", i), + search_filter_string); + } + } else { + flatten( + result, + p.value(), + key + p.key() + ".", + real_path + "/" + p.key(), + search_filter_string); + } + } + } else if (!key.empty()) { + + std::string vstring; + if (obj.is_string()) { + vstring = obj.get(); + } else { + vstring = obj.dump(2); + } + + if (!search_filter_string.empty() && + utility::to_lower(vstring).find(search_filter_string) == std::string::npos && + utility::to_lower(real_path).find(search_filter_string) == std::string::npos) + return; + + result[std::string(key, 0, key.size() - 1)]["value"] = vstring; + result[std::string(key, 0, key.size() - 1)]["path"] = real_path; + } +} + +void MediaMetadataHUD::new_metadata( + const std::string metadata_owner, const utility::JsonStore &metadata, const bool force) { + + // This merges new metadata coming from 'metadata_owner' (which might be Image, Media, Media + // Metadata or Layout) into the full set, and then updates the attr holding the full set to + // drive the metadata viewer UI window + if (!metadata_set_[metadata_owner] || *metadata_set_[metadata_owner] != metadata || force) { + + metadata_set_[metadata_owner].reset(new utility::JsonStore(metadata)); + utility::JsonStore j; + for (const auto &p : metadata_set_) { + + std::string path = p.first; + + if (path == "Layout") { + j[p.first] = p.second->ref(); + continue; + } + + if (path != "Frame Metadata") { + path += "/metadata"; + } + try { + + nlohmann::json jj; + flatten(jj, p.second->ref(), "", path, search_filter_string_); + + j[p.first] = jj; + } catch (std::exception &e) { + spdlog::debug("{} {}", __PRETTY_FUNCTION__, e.what()); + } + } + full_metadata_set_->set_value(j); + } +} + +extern "C" { +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { + return new plugin_manager::PluginFactoryCollection( + std::vector>( + {std::make_shared>( + utility::Uuid("f3e7c2db-2578-45d6-8ad5-743779057a63"), + "MediaMetadataHUD", + plugin_manager::PluginFlags::PF_HEAD_UP_DISPLAY | + plugin_manager::PluginFlags::PF_VIEWPORT_OVERLAY, + true, + "Ted Waine", + "Plugin to display per-frame metadata (e.g. EXR header data)")})); +} +} diff --git a/src/plugin/viewport_overlay/media_metadata_hud/src/media_metadata_hud.hpp b/src/plugin/viewport_overlay/media_metadata_hud/src/media_metadata_hud.hpp new file mode 100644 index 000000000..7ab054520 --- /dev/null +++ b/src/plugin/viewport_overlay/media_metadata_hud/src/media_metadata_hud.hpp @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "xstudio/ui/opengl/shader_program_base.hpp" +#include "xstudio/ui/opengl/opengl_text_rendering.hpp" +#include "xstudio/plugin_manager/hud_plugin.hpp" + +namespace xstudio { +namespace ui { + namespace viewport { + + struct DisplaySettings { + utility::ColourTriplet text_colour; + float font_size; + float text_opacity; + float text_spacing; + float bg_opacity; + }; + typedef std::shared_ptr DisplaySettingsPtr; + + typedef std::vector StringVec; + typedef std::shared_ptr StringPtr; + + class JsonStorePtr : public std::shared_ptr { + using Base = std::shared_ptr; + + public: + JsonStorePtr() = default; + + JsonStorePtr(const JsonStorePtr &o) + : Base(static_cast(o)), last_used_(o.last_used_) {} + + JsonStorePtr &operator=(const JsonStorePtr &o) { + Base &b = static_cast(*this); + b = static_cast(o); + last_used_ = o.last_used_; + return *this; + } + + void reset(utility::JsonStore *p) { + Base &b = static_cast(*this); + b.reset(p); + last_used_ = utility::clock::now(); + } + + utility::time_point last_used_; + }; + + /* To carry data from xstudio backend to the UI draw routine, we need + to make a custom class based on BlindDataObject. This allows us to + set-up all the data we need to draw to the screen in the backen thread + ready for the UI thread. Some careful consideration is made to optimise + this machanism so as not to slow down playback loops and draw + routines which are crucial to performance etc. + */ + class MediaMetadata : public utility::BlindDataObject { + public: + MediaMetadata(const DisplaySettingsPtr &d) + : display_settings_(d), last_used_(utility::clock::now()) {} + + ~MediaMetadata() = default; + + const DisplaySettingsPtr display_settings_; + + std::vector &make_empty_vec_for_text_render_vtxs(Imath::V4f &pos) { + positions_.push_back(pos); + auto p = std::make_shared>(); + verts_.push_back(p); + return *p; + } + + void add_bounding_box(const Imath::Box2f &bdb) { bdbs_.push_back(bdb); } + + std::vector>> verts_; + std::vector positions_; + std::vector bdbs_; + utility::time_point last_used_; + }; + + typedef std::shared_ptr MediaMetadataPtr; + + /* We defined a separate class to take care of rendering graphics into + the xstudio viewport. Be aware the instance(s) of this class runs in + a separate thread to the main plugin class instance, don't share + data directly. Rather, we use our MediaMetadata class to pass data from + the plugin class to the renderer. + */ + class MediaMetadataRenderer : public plugin::ViewportOverlayRenderer { + + public: + void render_image_overlay( + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + const float viewport_du_dpixel, + const xstudio::media_reader::ImageBufPtr &frame, + const bool have_alpha_buffer) override; + + void init_overlay_opengl(); + + std::unique_ptr shader_; + GLuint vbo_; + GLuint vao_; + std::unique_ptr text_renderer_; + DisplaySettingsPtr display_settings_; + }; + + class MediaMetadataHUD : public plugin::HUDPluginBase { + public: + MediaMetadataHUD(caf::actor_config &cfg, const utility::JsonStore &init_settings); + + ~MediaMetadataHUD(); + + protected: + utility::BlindDataObjectPtr onscreen_render_data( + const media_reader::ImageBufPtr &, + const std::string & /*viewport_name*/, + const utility::Uuid &playhead_uuid, + const bool is_hero_image) const override; + + void images_going_on_screen( + const media_reader::ImageBufDisplaySetPtr &image_set, + const std::string viewport_name, + const bool playhead_playing) override; + + plugin::ViewportOverlayRendererPtr + make_overlay_renderer(const std::string &viewport_name) override { + return plugin::ViewportOverlayRendererPtr(new MediaMetadataRenderer()); + } + + void + attribute_changed(const utility::Uuid &attribute_uuid, const int role) override; + + void register_hotkeys() override; + + void hotkey_pressed( + const utility::Uuid &hotkey_uuid, + const std::string &context, + const std::string &window) override; + + void media_due_on_screen_soon(const media::AVFrameIDsAndTimePoints &) override; + + void connect_to_viewport( + const std::string &viewport_name, + const std::string &viewport_toolbar_name, + bool connect, + caf::actor viewport) override; + + void menu_item_activated( + const utility::JsonStore &menu_item_data, + const std::string &user_data) override; + + private: + void show_metadata_viewer_for_viewport(const std::string viewport_name); + void update_profile_metadata(); + void update_full_metadata_set(const bool full_rebuild = false); + void new_metadata( + const std::string who, + const utility::JsonStore &metadata = utility::JsonStore(), + const bool force = false); + void update_media_metadata_for_media( + const media::AVFrameID frame_id, const bool update_profile_attr = false); + + void update_metadata_values_for_profile(const bool force_update = false); + void trim_cache(); + + MediaMetadataPtr + make_onscreen_data(const media_reader::ImageBufPtr &image, const std::string &key); + + void profile_changed(); + void profile_toggle_field(const std::string &field); + void + set_profile_field_label(const std::string &field, const std::string &field_label); + void set_field_screen_position( + const std::string &field, const std::string &screen_position); + void add_new_profile(const std::string &name); + void delete_current_profile(); + + module::FloatAttribute *text_size_; + module::FloatAttribute *text_opacity_; + module::FloatAttribute *text_spacing_; + module::FloatAttribute *bg_opacity_; + module::FloatAttribute *inset_; + module::ColourAttribute *text_colour_; + module::StringAttribute *search_string_; + module::StringChoiceAttribute *current_profile_; + module::BooleanAttribute *hide_missing_fields_; + + module::JsonAttribute *full_metadata_set_; + module::JsonAttribute *profile_data_; + module::JsonAttribute *profile_values_; + module::JsonAttribute *config_; + module::JsonAttribute *profile_fields_; + + utility::JsonStore config_data_; + utility::JsonStore profile_data_store_; + + module::ActionAttribute *action_attr_; + + std::string metadata_viewer_viewport_name_; + std::string search_filter_string_; + media::MediaKey metadata_frame_key_; + utility::Uuid metadata_media_id_; + + StringVec media_selected_metadata_fields_; + StringVec media_source_selected_metadata_fields_; + StringVec image_selected_metadata_fields_; + + // Using this because it should be faster to iterate over than + // a JsonStore + struct ProfileFieldItem { + bool is_from_image; + std::string real_metadata_path; + std::string label; + plugin::HUDElementPosition screen_position; + }; + std::vector profile_essential_data_; + + StringPtr field_names_; + + std::map media_metadata_; + + std::map render_data_per_image_cache_; + std::map render_data_per_metadata_cache_; + + std::map on_screen_images_; + media_reader::ImageBufPtr previous_metadata_source_image_; + std::map metadata_set_; + DisplaySettingsPtr display_settings_; + + SDFBitmapFont overlay_font_; + + // menu management + utility::Uuid show_viewer_hotkey_; + std::map viewport_menus_; + std::map metadata_viewers_; + }; + + } // namespace viewport +} // namespace ui +} // namespace xstudio diff --git a/src/plugin/viewport_overlay/media_metadata_hud/src/qml/.clang-tidy b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/.clang-tidy new file mode 100644 index 000000000..e4a0ac09c --- /dev/null +++ b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/.clang-tidy @@ -0,0 +1,33 @@ +--- +Checks: '-*,modernize-*,-modernize-use-trailing-return-type,-modernize-use-using,clang-diagnostic-gnu-include-next,-modernize-avoid-c-arrays' +WarningsAsErrors: '' +HeaderFilterRegex: '/xstudio/include/.*' +AnalyzeTemporaryDtors: false +FormatStyle: none +User: al +CheckOptions: + - key: google-readability-braces-around-statements.ShortStatementLines + value: '1' + - key: google-readability-function-size.StatementThreshold + value: '800' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '10' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '2' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: modernize-pass-by-value.IncludeStyle + value: llvm + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: readability-identifier-naming.MethodCase + value: camelBack +... + + diff --git a/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MediaMetadataHUDConfig.qml b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MediaMetadataHUDConfig.qml new file mode 100644 index 000000000..bcf2d6c32 --- /dev/null +++ b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MediaMetadataHUDConfig.qml @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQuick.Window +import Qt.labs.qmlmodels + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import "." +// This is an adapted version of XsAttributesPanel, allowing us to add a dynamic +// list of masks that the user can enable/disable + +XsWindow { + + id: dialog + width: 500 + height: 500 + title: "Metadata Picker" + property var windowVisible: visible + + ColumnLayout { + anchors.fill: parent + + MediaMetadataViewer { + Layout.fillWidth: true + Layout.fillHeight: true + select_mode: true + } + + XsSimpleButton { + + Layout.alignment: Qt.AlignRight|Qt.AlignVCenter + Layout.rightMargin: 10 + Layout.bottomMargin: 10 + text: qsTr("Close") + width: XsStyleSheet.primaryButtonStdWidth*2 + onClicked: { + dialog.close() + } + } + } + +} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MediaMetadataHUDSettings.qml b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MediaMetadataHUDSettings.qml new file mode 100644 index 000000000..ccf318055 --- /dev/null +++ b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MediaMetadataHUDSettings.qml @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQuick.Window +import Qt.labs.qmlmodels + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import "." + +// This is an adapted version of XsAttributesPanel, allowing us to add a dynamic +// list of masks that the user can enable/disable + +XsWindow { + + id: dialog + width: 500 + height: thelayout.childrenRect.height+30 + title: "Metadata HUD Settings" + property real cwidth: 50 + property real cwidth2: 50 + + minimumWidth: 540 + minimumHeight: 460 + + XsModuleData { + id: settings_attrs + modelDataName: "Media Metadata Settings" + } + + XsModuleData { + id: metadata_attrs + modelDataName: "metadata_hud_attrs" + } + + function setCWidth(ww) { + if (ww > cwidth) cwidth = ww + } + + XsAttributeValue { + id: __action + attributeTitle: "Action" + model: metadata_attrs + } + property alias actionAttr: __action.value + + // make an alias so the mask options are accessible as an array + property alias settings_attrs: settings_attrs + + Loader { + id: loader + } + + Component { + id: metadata_viewer + MediaMetadataHUDConfig { + } + } + + function addNewProfile(new_name, button) { + if (button == "Add") { + // setting the action attr is a way to callback to C++ side + actionAttr = ["ADD PROFILE", new_name] + } + } + + function deleteProfile(button) { + if (button == "Remove") { + // setting the action attr is a way to callback to C++ side + actionAttr = ["DELETE CURRENT_PROFILE"] + } + } + + + ColumnLayout { + + id: thelayout + anchors.left: parent.left + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: 10 + spacing: 10 + + RowLayout { + spacing: 10 + Layout.fillWidth: true + Rectangle { + Layout.fillWidth: true + height: 1 + color: XsStyleSheet.menuBorderColor + } + XsText { + text: "Metadata HUD Profile" + Layout.alignment: Qt.AlignHCenter + } + Rectangle { + Layout.fillWidth: true + height: 1 + color: XsStyleSheet.menuBorderColor + } + } + + RowLayout { + + Layout.fillWidth: true + Layout.margins: 10 + + ColumnLayout { + + Layout.margins: 5 + + Rectangle { + + height: 20 + color: "transparent" + width: label_metrics.width + onWidthChanged: setCWidth(width) + Layout.alignment: Qt.AlignRight + + XsText { + id: thetext + anchors.fill: parent + text: "Current Profile" + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + y: index*20 + } + + TextMetrics { + id: label_metrics + font: thetext.font + text: thetext.text + } + } + + } + + RowLayout { + + Layout.fillWidth: true + Layout.margins: 5 + + XsAttrComboBox { + id: current_profile + Layout.preferredHeight: 20 + Layout.preferredWidth: 150 + attr_title: "Current Profile" + attr_model_name: "metadata_hud_attrs" + } + + XsPrimaryButton { + + id: addBtn + Layout.preferredHeight: 20 + Layout.preferredWidth: 20 + imgSrc: "qrc:/icons/add.svg" + onClicked: { + dialogHelpers.textInputDialog( + addNewProfile, + "Add new HUD Metadata Profile", + "Please provide a name for a new Metadata HUD profile", + "New Profile", + ["Cancel", "Add"]) + } + XsToolTip { + visible: addBtn.isHovered + text: "Add a new Metadata HUD profile, to show a specific set of media metadata on screen that you can configure." + maxWidth: dialog.width*0.75 + } + } + + XsPrimaryButton { + + id: delBtn + Layout.preferredHeight: 20 + Layout.preferredWidth: 20 + imgSrc: "qrc:/icons/delete.svg" + enabled: current_profile.count > 1 + onClicked: { + dialogHelpers.multiChoiceDialog( + deleteProfile, + "Remove Profile", + "Are you sure that you want to remove profile \"" + current_profile.currentText + "\"?", + ["Cancel", "Remove"], + undefined) + } + XsToolTip { + visible: delBtn.isHovered + text: "Delete the current HUD profile from your list." + } + } + + XsPrimaryButton { + id: configBtn + Layout.preferredHeight: 20 + Layout.preferredWidth: 20 + imgSrc: "qrc:/icons/settings.svg" + onClicked: { + loader.sourceComponent = metadata_viewer + loader.item.visible = true + } + XsToolTip { + x: configBtn.width + y: configBtn.height + visible: configBtn.isHovered + text: "Configure the current profile." + } + } + + } + } + + + Item { + Layout.preferredHeight: 20 + } + + RowLayout { + spacing: 10 + Layout.fillWidth: true + Rectangle { + Layout.fillWidth: true + height: 1 + color: XsStyleSheet.menuBorderColor + } + XsText { + text: "Display Settings" + Layout.alignment: Qt.AlignHCenter + } + Rectangle { + Layout.fillWidth: true + height: 1 + color: XsStyleSheet.menuBorderColor + } + } + + RowLayout { + + Layout.fillWidth: true + Layout.margins: 10 + + ColumnLayout { + + Layout.margins: 5 + + Repeater { + + // N.B the 'combo_box_options' attribute is propagated via the + // XsToolBox which instantiates the BasicViewportMaskButton + model: settings_attrs + + Rectangle { + + height: 20 + color: "transparent" + width: label_metrics.width + onWidthChanged: setCWidth(width) + Layout.alignment: Qt.AlignRight + + XsText { + id: thetext + anchors.fill: parent + text: title ? title : "" + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + y: index*20 + } + + TextMetrics { + id: label_metrics + font: thetext.font + text: thetext.text + } + } + + } + } + + ColumnLayout { + + Layout.fillWidth: true + Layout.margins: 5 + + Repeater { + + model: settings_attrs + y: 5 + delegate: chooser + + DelegateChooser { + id: chooser + role: "type" + + DelegateChoice { + roleValue: "FloatScrubber"; + XsFloatAttributeSlider { + height: 20 + Layout.fillWidth: true + } + } + + DelegateChoice { + roleValue: "IntegerValue"; + XsIntAttributeSlider { + height: 20 + Layout.fillWidth: true + } + } + + DelegateChoice { + roleValue: "ColourAttribute"; + XsColourChooser { + height: 20 + Layout.fillWidth: true + } + } + + + DelegateChoice { + roleValue: "OnOffToggle"; + Rectangle { + color: "transparent" + height: 20 + Layout.fillWidth: true + XsBoolAttributeCheckBox { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.topMargin: 2 + anchors.bottomMargin: 2 + } + } + } + + DelegateChoice { + roleValue: "ComboBox"; + Rectangle { + height: 20 + Layout.fillWidth: true + color: "transparent" + XsComboBox { + model: combo_box_options + anchors.fill: parent + property var value_: value ? value : null + onValue_Changed: { + currentIndex = indexOfValue(value_) + } + Component.onCompleted: currentIndex = indexOfValue(value_) + onCurrentValueChanged: { + if (value != currentValue) { + value = currentValue; + } + } + } + } + } + + } + + } + } + } + + Item { + Layout.preferredHeight: 20 + } + + XsSimpleButton { + text: qsTr("Close") + width: XsStyleSheet.primaryButtonStdWidth*2 + Layout.alignment: Qt.AlignVCenter|Qt.AlignRight + onClicked: { + dialog.close() + } + } + } + +} diff --git a/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MediaMetadataViewer.qml b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MediaMetadataViewer.qml new file mode 100644 index 000000000..7784cc0ec --- /dev/null +++ b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MediaMetadataViewer.qml @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQuick.Window +import Qt.labs.qmlmodels + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import "." +// This is an adapted version of XsAttributesPanel, allowing us to add a dynamic +// list of masks that the user can enable/disable + +Item { + + id: dialog + property bool select_mode: false + + XsModuleData { + id: metadata_attrs + modelDataName: "metadata_hud_attrs" + } + + XsAttributeValue { + id: __full_metadata_set + attributeTitle: "Full Onscreen Image Metadata" + model: metadata_attrs + } + property alias full_metadata_set: __full_metadata_set.value + + + XsAttributeValue { + id: __profile_field_values + attributeTitle: "Profile Metadata Values" + model: metadata_attrs + } + property alias profile_field_values: __profile_field_values.value + + + XsAttributeValue { + id: __profile_data + attributeTitle: "Profile Fields Data" + model: metadata_attrs + } + property alias profile_data: __profile_data.value + + + XsAttributeValue { + id: __profile_field_paths + attributeTitle: "Profile Fields" + model: metadata_attrs + } + property alias profile_field_paths: __profile_field_paths.value + + + XsAttributeValue { + id: __search_string + attributeTitle: "Search String" + model: metadata_attrs + } + property alias search_string: __search_string.value + + XsAttributeValue { + id: __action + attributeTitle: "Action" + model: metadata_attrs + } + property alias actionAttr: __action.value + + // when this viewer is shown, we set the 'Action' attribute to the + // name of the viewport that this metadata viewer was launched from. + // The backend plugin will then fetch the metadata for the on-screen + // image and the media that it came from. + property var parentVis: windowVisible + onParentVisChanged: { + if (parentVis) { + actionAttr = ["SHOWING METADATA VIEWER", view.name] + } else { + actionAttr = ["HIDING METADATA VIEWER", ""] + } + } + + property var things: select_mode ? ["Pipeline Metadata", "Media Metadata", "Frame Metadata", "Layout"] : ["Pipeline Metadata", "Media Metadata", "Frame Metadata"] + + XsToolTip { + id: toolTip + maxWidth: parent.width*0.8 + } + + ColumnLayout { + + anchors.fill: parent + anchors.margins: 0 + spacing: 0 + + TabBar { + id: tabBar + + Layout.fillWidth: true + background: Rectangle { + color: palette.base + } + + Repeater { + model: things + + TabButton { + + width: implicitWidth + + contentItem: XsText { + text: things[index] + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.bold: tabBar.currentIndex == index + } + + background: Rectangle { + border.color: hovered? palette.highlight : "transparent" + color: tabBar.currentIndex == index ? XsStyleSheet.panelTitleBarColor : Qt.darker(XsStyleSheet.panelTitleBarColor, 1.5) + } + } + + } + + } + + /*Item { + Layout.preferredHeight: 10 + }*/ + + ColumnLayout { + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: 8 + + XsSearchButton { + id: searchBtn + Layout.preferredWidth: isExpanded? btnWidth*6 : btnWidth + Layout.preferredHeight: XsStyleSheet.widgetStdHieght + 4 + visible: tabBar.currentIndex != 3 + isExpanded: false + hint: "Search metadata fields ..." + // awkward two-way binding again + onTextChanged: { + if (backendV != text) { + search_string = text + } + } + property var backendV: search_string + onBackendVChanged: { + if (backendV != text) { + text = backendV + } + } + } + + Flickable { + id: flickable + clip: true + + Layout.fillWidth: true + Layout.fillHeight: true + + contentWidth: loader.item ? loader.item.width : 0 + contentHeight: loader.item ? loader.item.height : 0 + + property var tabIdx: tabBar.currentIndex + onTabIdxChanged: { + loader.sourceComponent = undefined + loader.sourceComponent = tabBar.currentIndex == 3 ? selected_metadata : metadata_group + } + + Component { + id: metadata_group + MetadataGroup { + metadataSet: full_metadata_set + setKey: things[tabBar.currentIndex] + width: flickable.width + } + } + + Component { + id: selected_metadata + SelectedMetadata { + width: flickable.width + } + } + + Loader { + id: loader + } + + ScrollBar.vertical: XsScrollBar { + // policy: ScrollBar.AlwaysOn + visible: flickable.height < flickable.contentHeight + policy: ScrollBar.AlwaysOn + width: 8 + } + + } + + } + + } + +} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MediaMetadataViewerWindow.qml b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MediaMetadataViewerWindow.qml new file mode 100644 index 000000000..424166796 --- /dev/null +++ b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MediaMetadataViewerWindow.qml @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQuick.Window +import Qt.labs.qmlmodels + +import xStudio 1.0 +import xstudio.qml.models 1.0 +import "." +// This is an adapted version of XsAttributesPanel, allowing us to add a dynamic +// list of masks that the user can enable/disable + +XsWindow { + + id: dialog + width: 500 + height: 500 + title: "Metadata Viewer" + property var windowVisible: visible + + ColumnLayout { + anchors.fill: parent + + MediaMetadataViewer { + Layout.fillWidth: true + Layout.fillHeight: true + select_mode: false + } + + XsSimpleButton { + + Layout.alignment: Qt.AlignRight|Qt.AlignVCenter + Layout.rightMargin: 10 + Layout.bottomMargin: 10 + text: qsTr("Close") + width: XsStyleSheet.primaryButtonStdWidth*2 + onClicked: { + dialog.close() + } + } + } + +} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MetadataGroup.qml b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MetadataGroup.qml new file mode 100644 index 000000000..e3f9481f9 --- /dev/null +++ b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/MetadataGroup.qml @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQuick.Window +import Qt.labs.qmlmodels + +import xStudio 1.0 +import xstudio.qml.models 1.0 + + +Item { + + id: top_level_item + property var root: metadataSet ? setKey ? metadataSet[setKey] : undefined : undefined + property var setKey + property var metadataSet + + property var keys: root == undefined ? [] : Object.keys(root) + property var num_keys: keys.length + height: the_layout.childrenRect.height + + property real cwidth: 50 + + function setCWidth(ww) { + if (ww > cwidth) cwidth = ww + } + + property var rowHeight: 20 + + function addMetadataField(path) { + if (button == "Add") { + // setting the action attr is a way to callback to C++ side + actionAttr = ["ADD METADATA FIELD", path] + } + } + + XsText { + x: 40 + y: 40 + text: "No " + setKey << " for on-screen item." + visible: keys.length == 0 + } + + ColumnLayout { + id: the_layout + width: parent.width + + Repeater { + model: num_keys + RowLayout { + + id: sub_layout + property var key: keys[index] + property var path: root[key]["path"] + property var value: root[key]["value"] + + Layout.fillWidth: true + XsCheckBoxSimple { + visible: select_mode + Layout.preferredHeight: rowHeight + Layout.preferredWidth: rowHeight + property var checkedOverride: profile_field_paths.includes(path) + onCheckedOverrideChanged: { + // when you click, qml sets checked and kills any + // binding we try to set-up on that attr + checked = checkedOverride + } + onClicked: { + actionAttr = ["TOGGLE METADATA FIELD SELECTION", path] + } + } + + Item { + width: 14 + } + + XsText { + Layout.preferredWidth: cwidth + Layout.preferredHeight: rowHeight + id: pname + text: key + font.family: XsStyleSheet.altFixedWidthFontFamily + font.pixelSize: 12 + font.weight: Font.Medium + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + TextMetrics { + font: pname.font + text: pname.text + onWidthChanged: setCWidth(width) + } + } + + XsText { + + id: valueText + Layout.preferredHeight: rowHeight + Layout.fillWidth: true + text: value.replace(/(\r\n|\n|\r)/gm, ""); + font.family: XsStyleSheet.altFixedWidthFontFamily + font.pixelSize: 12 + elide: Text.ElideRight + font.weight: Font.Medium + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + MouseArea { + id: ma + anchors.fill: parent + hoverEnabled: true + onContainsMouseChanged: { + if (containsMouse) { + if (valueText.truncated) { + var pt = mapToItem(toolTip.parent, mouseX, mouseY); + toolTip.x = pt.x + toolTip.y = pt.y + toolTip.text = value + toolTip.show() + } else { + toolTip.hide() + } + } else if (valueText.truncated && toolTip.text == value) { + toolTip.hide() + } + } + } + + Rectangle { + anchors.fill: parent + visible: valueText.truncated && ma.containsMouse + color: "transparent" + border.color: palette.highlight + } + } + } + } + + } +} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/SelectedMetadata.qml b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/SelectedMetadata.qml new file mode 100644 index 000000000..3cc8a63ab --- /dev/null +++ b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/SelectedMetadata.qml @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache-2.0 +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQuick.Window +import QtQuick.Effects +import Qt.labs.qmlmodels + +import xStudio 1.0 +import xstudio.qml.models 1.0 + + +Item { + + id: top_level_item + height: the_layout.childrenRect.height + + + property real cwidth: 50 + + function setCWidth(ww) { + if (ww > cwidth) cwidth = ww + } + + property var rowHeight: 20 + + property var editedField + function setFieldLabel(new_name, button) { + if (button == "Set") { + actionAttr = ["CHANGE FIELD LABEL", editedField, new_name] + } + } + function deleteField(button) { + if (button == "Remove") { + actionAttr = ["TOGGLE METADATA FIELD SELECTION", editedField] + } + } + + property var screenPositions: ["BottomLeft", "BottomCenter", "BottomRight", "TopLeft", "TopCenter", "TopRight", "FullScreen"] + + + property var num_items: profile_data.length + + ColumnLayout { + id: the_layout + width: parent.width + + Repeater { + model: num_items + RowLayout { + + id: sub_layout + property var vname: profile_data[index]["metadata_field_label"] + property var screen_position: profile_data[index]["screen_position"] + property var value: profile_field_values[index] + Layout.fillWidth: true + + Item { + + Layout.preferredWidth: cwidth + 200 + Layout.preferredHeight: rowHeight + property var my_index: index + id: parentItem + + XsText { + height: rowHeight + id: pname + text: vname + font.family: XsStyleSheet.altFixedWidthFontFamily + font.pixelSize: 12 + font.weight: Font.Medium + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + TextMetrics { + font: pname.font + text: pname.text + onWidthChanged: setCWidth(width + 20) + } + } + + XsPrimaryButton { + + //opacity: ma2.containsMouse + id: editButton + anchors.right: deleteButton.left + anchors.margins: 8 + width: 20 + height: 20 + imgSrc: "qrc:/icons/edit.svg" + onClicked: { + editedField = profile_data[index]["metadata_field"] + dialogHelpers.textInputDialog( + setFieldLabel, + "Set Metadata Field Label", + "Provide a new label for this metadata field.", + vname, + ["Cancel", "Set"]) + } + } + + XsPrimaryButton { + + id: deleteButton + //opacity: ma2.containsMouse + anchors.right: screenPos.left + anchors.margins: 8 + width: 20 + height: 20 + imgSrc: "qrc:/icons/delete.svg" + onClicked: { + editedField = profile_data[index]["metadata_field"] + dialogHelpers.multiChoiceDialog( + deleteField, + "Remove Metata Field", + "Do you want to remove the field " + (vname == "" ? "(unlabelled)" : vname) + " with current value \"" + value + "\" ?", + ["Cancel", "Remove"], + undefined) + } + } + + XsComboBox { + + id: screenPos + model: screenPositions + width: 100 + height: 20 + anchors.right: parent.right + currentIndex: screen_position + + onActivated: { + editedField = profile_data[parentItem.my_index]["metadata_field"] + actionAttr = ["CHANGE SCREEN POSITION", editedField, screenPositions[index]] + } + + } + + /*MouseArea { + id: ma2 + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + }*/ + + + + } + + XsText { + + id: valueText + Layout.preferredHeight: rowHeight + Layout.fillWidth: true + text: value ? value.replace(/(\r\n|\n|\r)/gm, "") : "" + font.family: XsStyleSheet.altFixedWidthFontFamily + font.pixelSize: 12 + elide: Text.ElideRight + font.weight: Font.Medium + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + MouseArea { + id: ma + anchors.fill: parent + hoverEnabled: true + onContainsMouseChanged: { + if (containsMouse) { + if (valueText.truncated) { + var pt = mapToItem(toolTip.parent, mouseX, mouseY); + toolTip.x = pt.x + toolTip.y = pt.y + toolTip.text = value + toolTip.show() + } else { + toolTip.hide() + } + } else if (valueText.truncated && toolTip.text == value) { + toolTip.hide() + } + } + } + + Rectangle { + anchors.fill: parent + visible: valueText.truncated && ma.containsMouse + color: "transparent" + border.color: palette.highlight + } + } + } + } + + } +} \ No newline at end of file diff --git a/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/qmldir b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/qmldir new file mode 100644 index 000000000..86896f906 --- /dev/null +++ b/src/plugin/viewport_overlay/media_metadata_hud/src/qml/MediaMetadataHUD.1/qmldir @@ -0,0 +1,5 @@ +module MediaMetadataHUD + +MediaMetadataHUDSettings 1.0 MediaMetadataHUDSettings.qml +MediaMetadataViewer 1.0 MediaMetadataViewer.qml +MediaMetadataViewerWindow 1.0 MediaMetadataViewerWindow.qml \ No newline at end of file diff --git a/src/plugin/viewport_overlay/media_metadata_hud/test/CMakeLists.txt b/src/plugin/viewport_overlay/media_metadata_hud/test/CMakeLists.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugin_manager/src/CMakeLists.txt b/src/plugin_manager/src/CMakeLists.txt index 0c53ecac2..44e5abd4f 100644 --- a/src/plugin_manager/src/CMakeLists.txt +++ b/src/plugin_manager/src/CMakeLists.txt @@ -1,13 +1,14 @@ SET(LINK_DEPS - caf::core - xstudio::broadcast + CAF::core xstudio::module xstudio::utility ) -if(UNIX AND NOT APPLE) - list(APPEND LINK_DEPS stdc++fs dl) +if(UNIX) + list(APPEND LINK_DEPS dl) # Link against stdc++fs for Linux + if (NOT APPLE) + list(APPEND LINK_DEPS stdc++fs) # Link against stdc++fs for Linux + endif() endif() -create_component(plugin_manager 0.1.0 "${LINK_DEPS}") - +create_component(plugin_manager ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/plugin_manager/src/hud_plugin.cpp b/src/plugin_manager/src/hud_plugin.cpp new file mode 100644 index 000000000..e618098ee --- /dev/null +++ b/src/plugin_manager/src/hud_plugin.cpp @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include + +#include "xstudio/plugin_manager/hud_plugin.hpp" +#include "xstudio/utility/string_helpers.hpp" +#include "xstudio/utility/helpers.hpp" + +using namespace xstudio::utility; +using namespace xstudio::plugin; +using namespace xstudio; + +HUDPluginBase::HUDPluginBase( + caf::actor_config &cfg, + std::string name, + const utility::JsonStore &init_settings, + const float toolbar_position) + : plugin::StandardPlugin(cfg, name, init_settings) { + + hud_data_ = add_boolean_attribute(name, name, false); + hud_data_->expose_in_ui_attrs_group("hud_element_toggles"); + + // add a preference path using the plugin name so that the status of the + // HUD plugin (on or off) is preserved between sessions + static std::regex whitespace(R"(\s)"); + static std::regex specials(R"([^_a-zA-Z0-9])"); + plugin_underscore_name_ = std::regex_replace(utility::to_lower(name), whitespace, "_"); + plugin_underscore_name_ = std::regex_replace(plugin_underscore_name_, specials, ""); + + hud_data_->set_preference_path(fmt::format("/plugin/{}/enabled", plugin_underscore_name_)); + + // toolbar position is used to sort the items in the HUD menu pop-up in + // the viewport toolbar + hud_data_->set_role_data( + module::Attribute::ToolbarPosition, + init_settings.is_null() + ? toolbar_position + : init_settings.value("hud_menu_item_position", toolbar_position)); + + // this means the 'settings' button will not be visible in the HUD pop-up menu against this + // HUD tool. (See XsHudToolbarButton .. visibility on the settings button is hooked to + // 'disabled_value' property that comes through via the model data) + hud_data_->set_role_data(module::Attribute::DisabledValue, true); + add_hud_settings_attribute(hud_data_); + + message_handler_ = { + [=](ui::viewport::hud_settings_atom, bool hud_enabled) { + globally_enabled_ = hud_enabled; + redraw_viewport(); + }, + [=](ui::viewport::hud_settings_atom, + const std::string qml_code, + HUDElementPosition position) -> bool { + hud_element_qml(qml_code, position); + return true; + }}; +} + +void HUDPluginBase::add_hud_settings_attribute(module::Attribute *attr) { + attr->expose_in_ui_attrs_group(Module::name() + " Settings"); + // this means the 'settings' button WILL be visible! + hud_data_->set_role_data(module::Attribute::DisabledValue, false); +} + +void HUDPluginBase::attribute_changed(const utility::Uuid &attribute_uuid, const int role) { + if (hud_item_position_ && hud_item_position_->uuid() == attribute_uuid && + role == module::Attribute::Value) { + + std::vector groups; + if (hud_data_->has_role_data(module::Attribute::UIDataModels)) { + + auto central_models_data_actor = + system().registry().template get(global_ui_model_data_registry); + + // a bit ugly, but if the hud is in 'HUD Bottom Left', say, then + // and user wants it 'HUD Top Right' then we have to erase 'Hud Bottom Left' + // from the UIDataModels attribute role data but preserve other groups + // that it might belong to + static const auto positions = utility::map_value_to_vec(position_names_); + groups = hud_data_->get_role_data>( + module::Attribute::UIDataModels); + auto p = groups.begin(); + while (p != groups.end()) { + if (std::find(positions.begin(), positions.end(), *p) != positions.end()) { + // We need to tell the central models actor that the attribute + // is being taken out of the group (e.g. 'HUD Bottom Left') + anon_mail( + ui::model_data::deregister_model_data_atom_v, + *p, + hud_data_->uuid(), + self()) + .send(central_models_data_actor); + p = groups.erase(p); + } else { + p++; + } + } + } + groups.push_back(hud_item_position_->value()); + + hud_data_->set_role_data(module::Attribute::UIDataModels, groups); + } + + plugin::StandardPlugin::attribute_changed(attribute_uuid, role); +} + +void HUDPluginBase::set_custom_settings_qml(const std::string &code) { + + hud_data_->set_role_data(module::Attribute::UserData, code); +} + +void HUDPluginBase::hud_element_qml( + const std::string qml_code, const HUDElementPosition position) { + + hud_data_->set_role_data(module::Attribute::QmlCode, qml_code); + + if (position == FullScreen) { + hud_data_->expose_in_ui_attrs_group("hud_elements_fullscreen"); + return; + } else if (!hud_item_position_) { + // add settings attribute for the position of the HUD element + hud_item_position_ = add_string_choice_attribute( + "Position On Screen", + "Position On Screen", + position_names_[position], + utility::map_value_to_vec(position_names_)); + add_hud_settings_attribute(hud_item_position_); + hud_item_position_->set_preference_path( + fmt::format("/plugin/{}/hud_item_position", plugin_underscore_name_)); + } + + if (hud_item_position_) { + attribute_changed(hud_item_position_->uuid(), module::Attribute::Value); + } +} \ No newline at end of file diff --git a/src/plugin_manager/src/plugin_base.cpp b/src/plugin_manager/src/plugin_base.cpp index e0d470a9b..0045e10ce 100644 --- a/src/plugin_manager/src/plugin_base.cpp +++ b/src/plugin_manager/src/plugin_base.cpp @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 +#include + #include "xstudio/plugin_manager/plugin_base.hpp" #include "xstudio/utility/helpers.hpp" -#include "xstudio/media_reader/image_buffer.hpp" +#include "xstudio/media_reader/image_buffer_set.hpp" using namespace xstudio; using namespace xstudio::bookmark; @@ -15,6 +17,26 @@ StandardPlugin::StandardPlugin( join_studio_events(); + set_default_handler( + [this](caf::scheduled_actor *, caf::message &msg) -> caf::skippable_result { + + spdlog::warn("{} got unexpected message from {} {}", + Module::name(), + to_string(current_sender()), + to_string(msg)); + + if (current_sender()) { + mail(utility::name_atom_v).request(caf::actor_cast(current_sender()), infinite).then( + [=](std::string __name) { + std::cerr << "Message came from " << __name << "\n"; + }, + [=](caf::error &err) { + std::cerr << "Can't get name of sender.\n"; + }); + } + return message{}; + }); + message_handler_ = { [=](utility::event_atom, session::session_atom, caf::actor session) { @@ -31,28 +53,33 @@ StandardPlugin::StandardPlugin( playhead_media_events_group_ = caf::actor_addr(); } }, + [=](ui::viewport::prepare_overlay_render_data_atom, const media_reader::ImageBufPtr &image, - const bool offscreen) -> utility::BlindDataObjectPtr { - return prepare_overlay_data(image, offscreen); + const std::string &viewport_name, + const utility::Uuid &playhead_id, + const bool hero_image) -> utility::BlindDataObjectPtr { + return onscreen_render_data(image, viewport_name, playhead_id, hero_image); }, - [=](ui::viewport::prepare_overlay_render_data_atom, - const media_reader::ImageBufPtr &image, - const std::string &viewport_name) -> utility::BlindDataObjectPtr { - return onscreen_render_data(image, viewport_name); + [=](playhead::colour_pipeline_lookahead_atom, + const media::AVFrameIDsAndTimePoints &frame_ids_for_colour_mgmnt_lookeahead) { + media_due_on_screen_soon(frame_ids_for_colour_mgmnt_lookeahead); }, [=](playhead::show_atom, - const std::vector &images, + const media_reader::ImageBufDisplaySetPtr &image_set, const std::string &viewport_name, - const bool playing) { images_going_on_screen(images, viewport_name, playing); }, + const bool playing) { + __images_going_on_screen(image_set, viewport_name, playing); + images_going_on_screen(image_set, viewport_name, playing); + }, - [=](ui::viewport::overlay_render_function_atom, const int viewer_index) - -> ViewportOverlayRendererPtr { return make_overlay_renderer(viewer_index); }, + [=](ui::viewport::overlay_render_function_atom, const std::string &viewport_name) + -> ViewportOverlayRendererPtr { return make_overlay_renderer(viewport_name); }, - [=](ui::viewport::pre_render_gpu_hook_atom, const int viewer_index) - -> GPUPreDrawHookPtr { return make_pre_draw_gpu_hook(viewer_index); }, + [=](ui::viewport::pre_render_gpu_hook_atom, const std::string &viewport_name) + -> GPUPreDrawHookPtr { return make_pre_draw_gpu_hook(viewport_name); }, [=](bookmark::build_annotation_atom, const utility::JsonStore &data) -> result { @@ -64,8 +91,10 @@ StandardPlugin::StandardPlugin( } }, - [=](module::current_viewport_playhead_atom, caf::actor_addr live_playhead) -> bool { - current_viewed_playhead_changed(live_playhead); + [=](module::current_viewport_playhead_atom, + const std::string &viewport_name, + caf::actor live_playhead) -> bool { + // this comes from a viewport, if we are an overlay actor return true; }, @@ -83,57 +112,81 @@ StandardPlugin::StandardPlugin( const int playhead_logical_frame, const int media_frame, const int media_logical_frame, - const utility::Timecode &timecode) { - on_screen_frame_changed( - playhead_position, - playhead_logical_frame, - media_frame, - media_logical_frame, - timecode); - - playhead_logical_frame_ = playhead_logical_frame; - }, + const utility::Timecode &timecode) {}, [=](utility::event_atom, bookmark::get_bookmarks_atom, const std::vector> &) {}, - [=](utility::event_atom, utility::event_atom) { join_studio_events(); }}; + [=](utility::event_atom, utility::event_atom) { join_studio_events(); }, + [=](utility::event_atom, + ui::viewport::viewport_playhead_atom, + const std::string &viewport_name, + caf::actor playhead) { + // the playhead of the given viewport has changed + viewport_playhead_changed(viewport_name, playhead); + }, + + [=](utility::event_atom, + ui::viewport::viewport_playhead_atom, + caf::actor live_playhead) { + // this comes from 'global_playhead_events_actor' when the main + // playhead driving viewports changes + current_viewed_playhead_changed(live_playhead); + }, + + [=](utility::event_atom, + playhead::show_atom, + caf::actor media, + caf::actor media_source, + const std::string &viewport_name) { + // the onscreen media for the given viewport has changed + on_screen_media_changed(media); + }, + [=](ui::viewport::turn_off_overlay_interaction_atom, const utility::Uuid requester_id) { + if (requester_id != uuid()) { + turn_off_overlay_interaction(); + } + }}; } +void StandardPlugin::on_exit() { playhead_events_actor_ = caf::actor(); } + void StandardPlugin::on_screen_media_changed(caf::actor media) { - if (media) { - request(media, infinite, utility::name_atom_v) - .then( - [=](const std::string name) mutable { - request( - media, infinite, media::current_media_source_atom_v, media::MT_IMAGE) - .then( + if (!media) + return; + mail(media::current_media_source_atom_v, media::MT_IMAGE) + .request(media, infinite) + .then( - [=](utility::UuidActor source) mutable { - request(source.actor(), infinite, media::media_reference_atom_v) - .then( - - [=](const utility::MediaReference &media_ref) mutable { - on_screen_media_changed(media, media_ref, name); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - } + [=](utility::UuidActor source) mutable { + mail(media::media_reference_atom_v) + .request(source.actor(), infinite) + .then( + [=](const utility::MediaReference &media_ref) mutable { + mail(colour_pipeline::get_colour_pipe_params_atom_v) + .request(source.actor(), infinite) + .then( + [=](utility::JsonStore params) { + on_screen_media_changed(media, media_ref, params); + }, + [=](error &err) mutable { + spdlog::debug( + "{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + }, + [=](error &err) mutable { + spdlog::debug("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + }, + [=](error &err) mutable { + spdlog::debug("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); } void StandardPlugin::session_changed(caf::actor session) { - request(session, infinite, bookmark::get_bookmark_atom_v) + mail(bookmark::get_bookmark_atom_v) + .request(session, infinite) .then( [=](caf::actor bookmark_manager) { bookmark_manager_ = bookmark_manager; }, [=](error &err) mutable { @@ -148,14 +201,16 @@ void StandardPlugin::join_studio_events() { if (!system().registry().template get(studio_registry)) { // studio not created yet. Retry in 100ms - delayed_anon_send( - caf::actor_cast(this), - std::chrono::milliseconds(100), - utility::event_atom_v, - utility::event_atom_v); + anon_mail(utility::event_atom_v, utility::event_atom_v) + .delay(std::chrono::milliseconds(100)) + .send(this); + return; } + playhead_events_actor_ = + system().registry().template get(global_playhead_events_actor); + // join studio events, so we know when a new session has been created auto grp = utility::request_receive( *sys, @@ -170,37 +225,134 @@ void StandardPlugin::join_studio_events() { system().registry().template get(studio_registry), session::session_atom_v)); - // fetch the current viewed playhead from the viewport so we can 'listen' to it - // for position changes, current media changes etc. - auto playhead_events_actor = - system().registry().template get(global_playhead_events_actor); - if (playhead_events_actor) { - request(playhead_events_actor, infinite, ui::viewport::viewport_playhead_atom_v) - .then( - [=](caf::actor playhead) { - current_viewed_playhead_changed( - caf::actor_cast(playhead)); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); - } - + anon_mail(broadcast::join_broadcast_atom_v, caf::actor_cast(this)) + .send(playhead_events_actor_); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } } +void StandardPlugin::__images_going_on_screen( + const media_reader::ImageBufDisplaySetPtr &image_set, + const std::string viewport_name, + const bool playhead_playing) { + + // skip viewports whose name doesn't start with 'viewport' - this lets us + // ignore offscreen and quickview viewports + if (viewport_name.find("viewport") != 0) + return; + + if (image_set && + image_set->hero_image().frame_id().source_uuid() != last_source_uuid_[viewport_name]) { + + last_source_uuid_[viewport_name] = image_set->hero_image().frame_id().source_uuid(); + auto media_source = + caf::actor_cast(image_set->hero_image().frame_id().media_source_addr()); + mail(utility::parent_atom_v) + .request(media_source, infinite) + .then( + [=](caf::actor media_actor) { on_screen_media_changed(media_actor); }, + [=](caf::error &err) {}); + } +} + -void StandardPlugin::current_viewed_playhead_changed(caf::actor_addr viewed_playhead_addr) { +void StandardPlugin::listen_to_playhead_events(const bool listen) { + + // fetch the current viewed playhead from the viewport so we can 'listen' to it + // for position changes, current media changes etc. + + // join the global playhead events group - this tells us when the playhead that should + // be on screen changes, among other things + joined_playhead_events_ = listen; + if (listen) { + + if (!playhead_events_actor_) { + playhead_events_actor_ = + system().registry().template get(global_playhead_events_actor); + } + + anon_mail(broadcast::join_broadcast_atom_v, caf::actor_cast(this)) + .send(playhead_events_actor_); + + // this call means we get event messages when the on-screen media + // changes + mail(ui::viewport::viewport_playhead_atom_v) + .request(playhead_events_actor_, infinite) + .then( + [=](caf::actor ph) { current_viewed_playhead_changed(ph); }, + [=](caf::error &err) { + + }); + + // get all the existing viewports.. + mail(ui::viewport::viewport_atom_v) + .request(playhead_events_actor_, infinite) + .then( + [=](std::vector viewports) { + for (auto &vp : viewports) { + // get the viewport name + mail(utility::name_atom_v) + .request(vp, infinite) + .then( + [=](const std::string &viewport_name) { + // get the playhead attached to the viewport + mail(ui::viewport::viewport_playhead_atom_v, viewport_name) + .request(vp, infinite) + .then( + [=](caf::actor playhead) { + // call our notification method + viewport_playhead_changed( + viewport_name, playhead); + }, + [=](caf::error &err) {}); + }, + [=](caf::error &err) {}); + } + }, + [=](caf::error &err) {}); + + } else { + anon_mail(broadcast::leave_broadcast_atom_v, caf::actor_cast(this)) + .send(playhead_events_actor_); + joined_playhead_events_ = false; + current_viewed_playhead_changed(caf::actor()); + } +} + + +void StandardPlugin::start_stop_playback(const std::string viewport_name, bool play) { + + scoped_actor sys{system()}; + try { + + auto playhead = utility::request_receive( + *sys, + playhead_events_actor_, + ui::viewport::viewport_playhead_atom_v, + viewport_name); + if (playhead) { + utility::request_receive(*sys, playhead, playhead::play_atom_v, play); + } + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + +void StandardPlugin::set_viewport_cursor(const std::string cursor_name) { + anon_mail(ui::viewport::viewport_cursor_atom_v, cursor_name).send(playhead_events_actor_); + + // anon_mail(ui::viewport::viewport_cursor_atom_v, + // cursor_name).send(playhead_events_actor_); +} + + +void StandardPlugin::current_viewed_playhead_changed(caf::actor viewed_playhead) { // here we join the playhead events group of the new playhead that is // attached to the viewport - auto viewed_playhead = caf::actor_cast(viewed_playhead_addr); - active_viewport_playhead_ = viewed_playhead_addr; - auto self = caf::actor_cast(this); if (caf::actor_cast(playhead_media_events_group_)) { utility::leave_broadcast( @@ -209,7 +361,8 @@ void StandardPlugin::current_viewed_playhead_changed(caf::actor_addr viewed_play if (viewed_playhead) { - request(viewed_playhead, infinite, playhead::media_events_group_atom_v) + mail(playhead::media_events_group_atom_v) + .request(viewed_playhead, infinite) .then( [=](caf::actor playhead_media_events_broadcast_group) { utility::join_broadcast(this, playhead_media_events_broadcast_group); @@ -218,17 +371,18 @@ void StandardPlugin::current_viewed_playhead_changed(caf::actor_addr viewed_play // this kicks the playhead into re-broadcasting us an updated list of // bookmarks for this new playhead - anon_send(viewed_playhead, bookmark::get_bookmarks_atom_v); + anon_mail(bookmark::get_bookmarks_atom_v).send(viewed_playhead); // this kicks the playhead to re-broadcast its position, media frame and // so-on - anon_send(viewed_playhead, playhead::redraw_viewport_atom_v); + anon_mail(playhead::redraw_viewport_atom_v).send(viewed_playhead); }, [=](error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); - request(viewed_playhead, infinite, playhead::media_atom_v) + mail(playhead::media_atom_v) + .request(viewed_playhead, infinite) .then( [=](caf::actor current_media_actor) { on_screen_media_changed(current_media_actor); @@ -236,27 +390,87 @@ void StandardPlugin::current_viewed_playhead_changed(caf::actor_addr viewed_play [=](error &err) mutable { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); - - // make sure we have synced the bookmarks info from the playhead - try { - scoped_actor sys{system()}; - playhead_logical_frame_ = utility::request_receive( - *sys, viewed_playhead, playhead::logical_frame_atom_v); - } catch ([[maybe_unused]] std::exception &e) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } } } void StandardPlugin::qml_viewport_overlay_code(const std::string &code) { if (!viewport_overlay_qml_code_) { - viewport_overlay_qml_code_ = add_qml_code_attribute("OverlayCode", code); + viewport_overlay_qml_code_ = add_boolean_attribute( + Module::name() + " Overlay", Module::name() + " Overlay", true); + viewport_overlay_qml_code_->set_role_data(module::Attribute::QmlCode, code); viewport_overlay_qml_code_->expose_in_ui_attrs_group("viewport_overlay_plugins"); } else { - viewport_overlay_qml_code_->set_value(code); + viewport_overlay_qml_code_->set_role_data(module::Attribute::QmlCode, code); + } +} + +utility::Uuid StandardPlugin::create_bookmark_on_frame( + const media::AVFrameID &frame_details, + const std::string &bookmark_subject, + const bookmark::BookmarkDetail &detail, + const bool bookmark_entire_duration) { + + utility::Uuid result; + + scoped_actor sys{system()}; + try { + + auto media_source = caf::actor_cast(frame_details.media_source_addr()); + auto media = + utility::request_receive(*sys, media_source, utility::parent_atom_v); + + if (media) { + + auto media_uuid = frame_details.media_uuid(); + + auto media_ref = + utility::request_receive>( + *sys, media, media::media_reference_atom_v, media_uuid) + .second; + + auto new_bookmark = utility::request_receive( + *sys, + bookmark_manager_, + bookmark::add_bookmark_atom_v, + utility::UuidActor(media_uuid, media)); + + bookmark::BookmarkDetail bmd = detail; + + if (bookmark_entire_duration) { + + bmd.start_ = timebase::k_flicks_low; + bmd.duration_ = timebase::k_flicks_max; + + } else { + + // this will make a bookmark of single frame duration on the current frame + bmd.start_ = (frame_details.frame() - frame_details.first_frame()) * + media_ref.rate().to_flicks(); + bmd.duration_ = timebase::flicks(0); + } + + bmd.subject_ = bookmark_subject; + + if (!bmd.category_) { + + auto default_category = utility::request_receive( + *sys, bookmark_manager_, bookmark::default_category_atom_v); + bmd.category_ = default_category; + } + + utility::request_receive( + *sys, new_bookmark.actor(), bookmark::bookmark_detail_atom_v, bmd); + + result = new_bookmark.uuid(); + } + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } + return result; } + utility::Uuid StandardPlugin::create_bookmark_on_current_media( const std::string &viewport_name, const std::string &bookmark_subject, @@ -266,16 +480,29 @@ utility::Uuid StandardPlugin::create_bookmark_on_current_media( utility::Uuid result; scoped_actor sys{system()}; - auto ph_events = system().registry().template get(global_playhead_events_actor); try { - auto vp = utility::request_receive( - *sys, ph_events, ui::viewport::viewport_atom_v, viewport_name); - auto playhead_addr = utility::request_receive( - *sys, vp, ui::viewport::viewport_playhead_atom_v); - auto playhead = caf::actor_cast(playhead_addr); + + + caf::actor playhead; + try { + auto vp = utility::request_receive( + *sys, playhead_events_actor_, ui::viewport::viewport_atom_v, viewport_name); + playhead = utility::request_receive( + *sys, vp, ui::viewport::viewport_playhead_atom_v); + } catch (...) { + // couldn't find the specified viewport or its playhead. Instead + // get the 'global active' playhead from the global_playhead_events_actor + playhead = utility::request_receive( + *sys, playhead_events_actor_, ui::viewport::viewport_playhead_atom_v); + } + + if (!playhead) { + throw std::runtime_error("Couldn't find viewport/playhead."); + } auto media = utility::request_receive(*sys, playhead, playhead::media_atom_v); + if (media) { auto media_uuid = @@ -329,36 +556,115 @@ utility::Uuid StandardPlugin::create_bookmark_on_current_media( return result; } +void StandardPlugin::cancel_other_drawing_tools() { + + // get all viewports + mail(ui::viewport::viewport_atom_v) + .request(playhead_events_actor_, infinite) + .then( + [=](const std::vector &viewports) { + for (auto &vp : viewports) { + mail(ui::viewport::turn_off_overlay_interaction_atom_v, uuid()).send(vp); + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); +} + +utility::UuidList +StandardPlugin::get_bookmarks_on_current_media(const std::string &viewport_name) { + + utility::UuidList result; + + scoped_actor sys{system()}; + try { + auto vp = utility::request_receive( + *sys, playhead_events_actor_, ui::viewport::viewport_atom_v, viewport_name); + auto playhead = utility::request_receive( + *sys, vp, ui::viewport::viewport_playhead_atom_v); + auto media_actor = + utility::request_receive(*sys, playhead, playhead::media_atom_v); + + result = utility::request_receive( + *sys, media_actor, bookmark::get_bookmarks_atom_v); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + return result; +} + +bookmark::BookmarkDetail StandardPlugin::get_bookmark_detail(const utility::Uuid &bookmark_id) { + + bookmark::BookmarkDetail result; + + scoped_actor sys{system()}; + try { + auto uuid_actor = utility::request_receive( + *sys, bookmark_manager_, bookmark::get_bookmark_atom_v, bookmark_id); + result = utility::request_receive( + *sys, uuid_actor.actor(), bookmark::bookmark_detail_atom_v); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + return result; +} + +AnnotationBasePtr StandardPlugin::get_bookmark_annotation(const utility::Uuid &bookmark_id) { + + AnnotationBasePtr result; + + scoped_actor sys{system()}; + try { + auto uuid_actor = utility::request_receive( + *sys, bookmark_manager_, bookmark::get_bookmark_atom_v, bookmark_id); + result = utility::request_receive( + *sys, uuid_actor.actor(), bookmark::get_annotation_atom_v); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + return result; +} + void StandardPlugin::update_bookmark_annotation( const utility::Uuid bookmark_id, - std::shared_ptr annotation_data, + AnnotationBasePtr annotation_data, const bool annotation_is_empty) { - request(bookmark_manager_, infinite, bookmark::get_bookmark_atom_v, bookmark_id) + + mail(bookmark::get_bookmark_atom_v, bookmark_id) + .request(bookmark_manager_, infinite) .then( [=](utility::UuidActor &bm) { if (!annotation_is_empty) { - anon_send(bm.actor(), bookmark::add_annotation_atom_v, annotation_data); + anon_mail(bookmark::add_annotation_atom_v, annotation_data) + .send(bm.actor()); } else { - request(bm.actor(), infinite, bookmark::bookmark_detail_atom_v) + mail(bookmark::bookmark_detail_atom_v) + .request(bm.actor(), infinite) .then( [=](const bookmark::BookmarkDetail &detail) { if (!detail.note_ || *(detail.note_) == "") { // bookmark has no note, and the annotation is empty. Delete // the bookmark altogether - request( - bookmark_manager_, - infinite, - bookmark::remove_bookmark_atom_v, - bookmark_id); + // ?????????? + // request( + // bookmark_manager_, + // infinite, + // bookmark::remove_bookmark_atom_v, + // bookmark_id); + + mail(bookmark::remove_bookmark_atom_v, bookmark_id) + .send(bookmark_manager_); } else { - anon_send( - bm.actor(), - bookmark::add_annotation_atom_v, - annotation_data); + anon_mail(bookmark::add_annotation_atom_v, annotation_data) + .send(bm.actor()); } }, [=](error &err) mutable { @@ -373,12 +679,20 @@ void StandardPlugin::update_bookmark_annotation( void StandardPlugin::update_bookmark_detail( const utility::Uuid bookmark_id, const bookmark::BookmarkDetail &bmd) { - request(bookmark_manager_, infinite, bookmark::get_bookmark_atom_v, bookmark_id) - .then( - [=](utility::UuidActor &bm) { - anon_send(bm.actor(), bookmark::bookmark_detail_atom_v, bmd); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - }); + + scoped_actor sys{system()}; + try { + auto uuid_actor = utility::request_receive( + *sys, bookmark_manager_, bookmark::get_bookmark_atom_v, bookmark_id); + auto result = utility::request_receive( + *sys, uuid_actor.actor(), bookmark::bookmark_detail_atom_v, bmd); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + +void StandardPlugin::remove_bookmark(const utility::Uuid &bookmark_id) { + + mail(bookmark::remove_bookmark_atom_v, bookmark_id).send(bookmark_manager_); + // request(bookmark_manager_, infinite, bookmark::remove_bookmark_atom_v, bookmark_id); } diff --git a/src/plugin_manager/src/plugin_manager.cpp b/src/plugin_manager/src/plugin_manager.cpp index c2d5c28e3..0a8736128 100644 --- a/src/plugin_manager/src/plugin_manager.cpp +++ b/src/plugin_manager/src/plugin_manager.cpp @@ -1,8 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 -#ifdef __linux__ +#ifndef _WIN32 #include #endif - #include #include @@ -47,41 +46,23 @@ PluginManager::PluginManager(std::list plugin_paths) size_t PluginManager::load_plugins() { // scan for .so or .dll for each path. size_t loaded = 0; - spdlog::debug("Loading Plugins"); for (const auto &path : plugin_paths_) { try { // read dir content.. for (const auto &entry : fs::directory_iterator(path)) { - if (not fs::is_regular_file(entry.status()) or - not(entry.path().extension() == ".so" || - entry.path().extension() == ".dll")) + if (not fs::is_regular_file(entry.status())) continue; -#ifdef __linux__ - // only want .so - // clear any errors.. - dlerror(); +#ifdef _WIN32 - // open .so - void *hndl = dlopen(entry.path().c_str(), RTLD_NOW); - if (hndl == nullptr) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, dlerror()); + if (entry.path().extension() != ".dll") continue; - } - - plugin_factory_collection_ptr pfcp; - *(void **)(&pfcp) = dlsym(hndl, "plugin_factory_collection_ptr"); - if (pfcp == nullptr) { - spdlog::debug("{} {}", __PRETTY_FUNCTION__, dlerror()); - dlclose(hndl); - continue; - } -#elif defined(_WIN32) // open .dll std::string dllPath = entry.path().string(); - HMODULE hndl = LoadLibraryA(dllPath.c_str()); + + HMODULE hndl = LoadLibraryA(dllPath.c_str()); if (hndl == nullptr) { DWORD errorCode = GetLastError(); LPSTR buffer = nullptr; @@ -109,6 +90,27 @@ size_t PluginManager::load_plugins() { FreeLibrary(hndl); continue; } +#else + if (entry.path().extension() != ".so" && entry.path().extension() != ".dylib") + continue; + // only want .so / .dylib + // clear any errors.. + dlerror(); + + // open .so + void *hndl = dlopen(entry.path().c_str(), RTLD_NOW); + if (hndl == nullptr) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, dlerror()); + continue; + } + + plugin_factory_collection_ptr pfcp; + *(void **)(&pfcp) = dlsym(hndl, "plugin_factory_collection_ptr"); + if (pfcp == nullptr) { + spdlog::debug("{} {}", __PRETTY_FUNCTION__, dlerror()); + dlclose(hndl); + continue; + } #endif PluginFactoryCollection *pfc = nullptr; diff --git a/src/plugin_manager/src/plugin_manager_actor.cpp b/src/plugin_manager/src/plugin_manager_actor.cpp index 863112288..0f7002d1f 100644 --- a/src/plugin_manager/src/plugin_manager_actor.cpp +++ b/src/plugin_manager/src/plugin_manager_actor.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include "xstudio/atoms.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" @@ -9,6 +10,7 @@ #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/string_helpers.hpp" +#include "xstudio/plugin_manager/hud_plugin.hpp" using namespace xstudio; using namespace xstudio::utility; @@ -22,8 +24,7 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base system().registry().put(plugin_manager_registry, this); - - manager_.emplace_front_path(xstudio_root("/plugin")); + manager_.emplace_front_path(xstudio_plugin_dir()); // use env var 'XSTUDIO_PLUGIN_PATH' to extend the folders searched for // xstudio plugins @@ -53,13 +54,14 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base const JsonStore & /*change*/, const std::string & /*path*/, const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full).delegate(actor_cast(this)); }, // helper for dealing with URI's [=](data_source::use_data_atom, const caf::uri &uri, - const FrameRate &media_rate) -> result { + const FrameRate &media_rate, + const bool create_playlist) -> result { // send to resident enabled datasource plugins auto actors = std::vector(); @@ -75,7 +77,12 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base auto rp = make_response_promise(); fan_out_request( - actors, infinite, data_source::use_data_atom_v, uri, media_rate) + actors, + infinite, + data_source::use_data_atom_v, + uri, + media_rate, + create_playlist) .then( [=](const std::vector results) mutable { for (const auto &i : results) { @@ -164,12 +171,8 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base const FrameRate &media_rate) -> result { auto rp = make_response_promise(); - request( - caf::actor_cast(this), - infinite, - data_source::use_data_atom_v, - uri, - media_rate) + mail(data_source::use_data_atom_v, uri, media_rate, playlist ? false : true) + .request(caf::actor_cast(this), infinite) .then( [=](const UuidActorVector &results) mutable { // uri can contain playlist or media currently. @@ -180,19 +183,16 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base try { auto type = request_receive(*sys, i.actor(), type_atom_v); - if (type == "Media" && playlist) { - anon_send( - playlist, - playlist::add_media_atom_v, - i, - utility::Uuid()); + if (type == "Media" and playlist) { + anon_mail(playlist::add_media_atom_v, i, utility::Uuid()) + .send(playlist); } else if (type == "Playlist" && session) { - anon_send( - session, + anon_mail( session::add_playlist_atom_v, i.actor(), utility::Uuid(), - false); + false) + .send(session); } // spdlog::warn("type {}", type); } catch (const std::exception &err) { @@ -263,7 +263,8 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base }, [=](spawn_plugin_atom atom, const utility::Uuid &uuid) { - delegate(actor_cast(this), atom, uuid, utility::JsonStore()); + return mail(atom, uuid, utility::JsonStore()) + .delegate(actor_cast(this)); }, [=](spawn_plugin_atom, @@ -312,12 +313,45 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base [=](spawn_plugin_base_atom, const std::string name, - const utility::JsonStore &json) -> result { + const utility::JsonStore &json, + const std::string class_name) -> result { /*if (base_plugins_.find(name) == base_plugins_.end()) { base_plugins_[name] = spawn(name, json); link_to(base_plugins_[name]); }*/ - return spawn(name, json); // base_plugins_[name]; + caf::actor result; + if (class_name == "HUDPlugin") { + result = spawn(name, json); // base_plugins_[name]; + } else if (class_name == "ViewportLayoutPlugin") { + // slightly awkward. We want to spawn an instance of + // ViewportLayoutPlugin class to back a Python plugin for + // managing viewport layouts. To avoid making the plugin_manager + // component link-dependent on the ui::viewport component we + // spawn via the viewport_layouts_manager + std::vector details = manager_.plugin_detail(); + for (auto &detail : details) { + if (detail.name_ == "DefaultViewportLayout") { + try { + auto j = json; + j["name"] = name; + j["is_python"] = true; + result = + manager_.spawn(*scoped_actor(system()), detail.uuid_, j); + + } catch (std::exception &e) { + return make_error(xstudio_error::error, e.what()); + } + } + } + if (!result) { + return make_error( + xstudio_error::error, "Failed to spawn base ViewportLayoutPlugin"); + } + } else { + result = spawn(name, json); // base_plugins_[name]; + } + link_to(result); + return result; }, [=](spawn_plugin_base_atom, const std::string name) -> result { @@ -360,22 +394,16 @@ PluginManagerActor::PluginManagerActor(caf::actor_config &cfg) : caf::event_base if (manager_.factories().at(uuid).factory()->resident()) enable_resident(uuid, enabled); - send( - event_group_, - utility::event_atom_v, - utility::detail_atom_v, - manager_.plugin_detail()); + mail(utility::event_atom_v, utility::detail_atom_v, manager_.plugin_detail()) + .send(event_group_); return true; }, [=](json_store::update_atom) -> int { int result = manager_.load_plugins(); - send( - event_group_, - utility::event_atom_v, - utility::detail_atom_v, - manager_.plugin_detail()); + mail(utility::event_atom_v, utility::detail_atom_v, manager_.plugin_detail()) + .send(event_group_); return result; }, diff --git a/src/plugin_manager/test/CMakeLists.txt b/src/plugin_manager/test/CMakeLists.txt index 6cc6c148b..311553813 100644 --- a/src/plugin_manager/test/CMakeLists.txt +++ b/src/plugin_manager/test/CMakeLists.txt @@ -7,6 +7,7 @@ target_link_libraries(plugin_manager_test xstudio::plugin_manager xstudio::json_store xstudio::global_store + xstudio::media ${GTEST_LDFLAGS} ) target_compile_definitions(plugin_manager_test @@ -39,7 +40,7 @@ target_link_libraries(plugin_test PUBLIC xstudio::plugin_manager xstudio::utility - caf::core + CAF::core ) set_target_properties(${name} PROPERTIES diff --git a/src/plugin_manager/test/plugin_test.cpp b/src/plugin_manager/test/plugin_test.cpp index a4f4f5baf..a7649df03 100644 --- a/src/plugin_manager/test/plugin_test.cpp +++ b/src/plugin_manager/test/plugin_test.cpp @@ -42,7 +42,7 @@ class TestPlugin : public PluginFactory { [[nodiscard]] bool resident() const override { return false; } [[nodiscard]] std::string author() const override { return "author"; } [[nodiscard]] std::string description() const override { return "description"; } - [[nodiscard]] semver::version version() const override { return semver::version("0.1.0"); } + [[nodiscard]] semver::version version() const override { return semver::version("${XSTUDIO_GLOBAL_VERSION}"); } [[nodiscard]] caf::actor spawn( caf::blocking_actor &sys, diff --git a/src/pyside2_module/src/CMakeLists.txt b/src/pyside2_module/src/CMakeLists.txt deleted file mode 100644 index afc36ce9c..000000000 --- a/src/pyside2_module/src/CMakeLists.txt +++ /dev/null @@ -1,185 +0,0 @@ -project(__pyside2_xstudio VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) - -# Enable policy to run automoc on generated files. -if(POLICY CMP0071) - cmake_policy(SET CMP0071 NEW) -endif() - -if (CLANG_PATH) - set(ENV{CLANG_INSTALL_DIR} $CLANG_PATH) -endif() - -find_package(Shiboken2 REQUIRED) -find_package(PythonLibs REQUIRED) -find_package(PySide2 REQUIRED) -find_package(Python COMPONENTS Interpreter) - -get_target_property(PYSIDE2_INCLUDE_DIR PySide2::pyside2 INTERFACE_INCLUDE_DIRECTORIES) -get_target_property(SHIBOKEN2_INCLUDE_DIRS Shiboken2::libshiboken INTERFACE_INCLUDE_DIRECTORIES) -list(GET SHIBOKEN2_INCLUDE_DIRS 0 SHIBOKEN2_INCLUDE_ROOT_DIR) - -get_target_property(PYSIDE2_LIBS PySide2::pyside2 INTERFACE_LINK_LIBRARIES) - -find_package(OpenGL REQUIRED) -find_package(GLEW REQUIRED) -find_package(Qt5 COMPONENTS Core Gui Widgets OpenGL Quick Qml QuickWidgets Network REQUIRED) -find_package(OpenSSL) -find_package(ZLIB) - -set(CMAKE_AUTOUIC ON) -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTORCC ON) - -# Set up the options to pass to shiboken. -set(WRAPPED_HEADER ${CMAKE_CURRENT_SOURCE_DIR}/wrappedclasses.h) -set(TYPESYSTEM_FILE ${CMAKE_CURRENT_SOURCE_DIR}/typesystem.xml) - -# Get all relevant Qt include dirs, to pass them on to shiboken. -get_property(QT_CORE_INCLUDE_DIRS TARGET Qt5::Core PROPERTY INTERFACE_INCLUDE_DIRECTORIES) -get_property(QT_GUI_INCLUDE_DIRS TARGET Qt5::Gui PROPERTY INTERFACE_INCLUDE_DIRECTORIES) -get_property(QT_WIDGETS_INCLUDE_DIRS TARGET Qt5::Widgets PROPERTY INTERFACE_INCLUDE_DIRECTORIES) -get_property(QT_QUICKWIDGETS_INCLUDE_DIRS TARGET Qt5::QuickWidgets PROPERTY INTERFACE_INCLUDE_DIRECTORIES) -get_property(QT_QUICK_INCLUDE_DIRS TARGET Qt5::Quick PROPERTY INTERFACE_INCLUDE_DIRECTORIES) -get_property(QT_NETWORK_INCLUDE_DIRS TARGET Qt5::Network PROPERTY INTERFACE_INCLUDE_DIRECTORIES) -get_property(QT_QML_INCLUDE_DIRS TARGET Qt5::Qml PROPERTY INTERFACE_INCLUDE_DIRECTORIES) -set(QT_INCLUDE_DIRS ${QT_CORE_INCLUDE_DIRS} ${QT_GUI_INCLUDE_DIRS} ${QT_WIDGETS_INCLUDE_DIRS} - ${QT_QUICKWIDGETS_INCLUDE_DIRS} ${QT_QUICK_INCLUDE_DIRS} ${QT_NETWORK_INCLUDE_DIRS} ${QT_QML_INCLUDE_DIRS}) -set(INCLUDES "") -foreach(INCLUDE_DIR ${QT_INCLUDE_DIRS}) - list(APPEND INCLUDES "-I${INCLUDE_DIR}") -endforeach() - -list(APPEND INCLUDES "-I/opt/rh/devtoolset-9/root/usr/lib/gcc/x86_64-redhat-linux/9/include") - -set(SHIBOKEN_OPTIONS --generator-set=shiboken --enable-parent-ctor-heuristic - --enable-pyside-extensions --enable-return-value-heuristic --use-isnull-as-nb_nonzero - --avoid-protected-hack - ${INCLUDES} - -I${CMAKE_CURRENT_SOURCE_DIR} - -T${CMAKE_CURRENT_SOURCE_DIR} - -T${PYSIDE2_INCLUDE_DIR}/../../share/PySide2/typesystems - --output-directory=${CMAKE_CURRENT_BINARY_DIR} - ) - -# Specify which sources will be generated by shiboken, and their dependencies. -set(GENERATED_SOURCES - ${CMAKE_CURRENT_BINARY_DIR}/__pyside2_xstudio/__pyside2_xstudio_module_wrapper.cpp - ${CMAKE_CURRENT_BINARY_DIR}/__pyside2_xstudio/qmlviewport_wrapper.cpp - ${CMAKE_CURRENT_BINARY_DIR}/__pyside2_xstudio/threadedviewportwindow_wrapper.cpp - ${CMAKE_CURRENT_BINARY_DIR}/__pyside2_xstudio/plainviewport_wrapper.cpp) - -set(GENERATED_SOURCES_DEPENDENCIES - ${WRAPPED_HEADER} - ${TYPESYSTEM_FILE} - ) - -set(SHIBOKEN_PATH shiboken2) - -# Add custom target to run shiboken. -add_custom_command(OUTPUT ${GENERATED_SOURCES} - COMMAND ${SHIBOKEN_PATH} - ${SHIBOKEN_OPTIONS} ${WRAPPED_HEADER} ${TYPESYSTEM_FILE} - DEPENDS ${GENERATED_SOURCES_DEPENDENCIES} - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMENT "Running generator for ${TYPESYSTEM_FILE}.") - -set(SOURCES - threaded_viewport.cpp - plain_viewport.cpp - qml_viewport.cpp - caf_system.cpp - ../../../../ui/qml/xstudio/qml.qrc - ${GENERATED_SOURCES} -) - -add_library(${PROJECT_NAME} SHARED ${SOURCES}) - -if(WIN32) - set(EXE_EXTENSION ".exe") -else() - set(EXE_EXTENSION ".bin") -endif() - -set_target_properties(${PROJECT_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" - OUTPUT_NAME "${PROJECT_NAME}${EXE_EXTENSION}" - LINK_DEPENDS_NO_SHARED true -) - -target_link_libraries(${PROJECT_NAME} - PRIVATE - xstudio::caf_utility - xstudio::colour_pipeline - xstudio::global - xstudio::ui::viewport - xstudio::ui::opengl::viewport - xstudio::ui::qt::viewport_widget - xstudio::ui::qml::module - xstudio::ui::qml::global_store - xstudio::ui::qml::helper - xstudio::ui::qml::playhead - xstudio::ui::qml::bookmark - xstudio::ui::qml::session - xstudio::ui::qml::studio - xstudio::ui::qml::viewport - xstudio::ui::qml::quickfuture - xstudio::ui::qml::tag - xstudio::utility - xstudio::module - PRIVATE - caf::core - $<$:GLdispatch> - Qt5::Core - Qt5::Gui - Qt5::Widgets - Qt5::Quick - Qt5::QuickWidgets - Qt5::Network - Qt5::Qml - OpenSSL::SSL - ZLIB::ZLIB - Shiboken2::libshiboken - PySide2::pyside2 -) - -default_options(${PROJECT_NAME}) - -include_directories("${PYSIDE2_INCLUDE_DIR}/QtGui") -include_directories("${PYSIDE2_INCLUDE_DIR}/QtCore") -include_directories("${PYSIDE2_INCLUDE_DIR}/QtWidgets") -include_directories("${PYSIDE2_INCLUDE_DIR}/QtQuick") -include_directories("${PYSIDE2_INCLUDE_DIR}/QtQuickWidgets") -include_directories("${PYSIDE2_INCLUDE_DIR}/QtNetwork") -include_directories("${PYSIDE2_INCLUDE_DIR}/QtQml") -include_directories("${PYSIDE2_INCLUDE_DIR}/PySide2") -include_directories(${SHIBOKEN_PYTHON_INCLUDE_DIRS}) - -set(PYTHONVP "python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}") - -set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME ${PROJECT_NAME}) - -if(INSTALL_XSTUDIO) - set_target_properties( - ${PROJECT_NAME} - PROPERTIES - #PREFIX "${PYTHON_MODULE_PREFIX}" - can't get this to work, module isn't found when you do import - PREFIX "" - #SUFFIX "${PYTHON_MODULE_EXTENSION}" - SUFFIX ".so" - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/python/lib/${PYTHONVP}/site-packages/xstudio/gui" - ) -else() - set_target_properties( - ${PROJECT_NAME} - PROPERTIES - #PREFIX "${PYTHON_MODULE_PREFIX}" - PREFIX "" - #SUFFIX "${PYTHON_MODULE_EXTENSION}" - SUFFIX ".so" - ) - - install(TARGETS ${PROJECT_NAME} - LIBRARY DESTINATION lib/python/xstudio/gui) - -endif(INSTALL_XSTUDIO) diff --git a/src/pyside2_module/src/caf_system.cpp b/src/pyside2_module/src/caf_system.cpp deleted file mode 100644 index f211a4a9a..000000000 --- a/src/pyside2_module/src/caf_system.cpp +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "xstudio/atoms.hpp" -#include "xstudio/thumbnail/thumbnail.hpp" -#include "xstudio/caf_utility/caf_setup.hpp" -#include "xstudio/event/event.hpp" -#include "xstudio/global/global_actor.hpp" -#include "xstudio/global_store/global_store.hpp" -#include "xstudio/media/media.hpp" -#include "xstudio/media/media_actor.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" -#include "xstudio/utility/caf_helpers.hpp" -#include "xstudio/tag/tag.hpp" -#include "xstudio/plugin_manager/plugin_manager.hpp" -#include "xstudio/bookmark/bookmark.hpp" -#include "xstudio/shotgun_client/shotgun_client.hpp" -#include "xstudio/ui/mouse.hpp" -#include "xstudio/utility/serialise_headers.hpp" - -#include "caf_system.hpp" - -using namespace xstudio; - -static std::shared_ptr s_instance_ = nullptr; - -CafSys *CafSys::instance() { - if (!s_instance_) { - s_instance_.reset(new CafSys()); - } - return s_instance_.get(); -} - -utility::JsonStore load_preferences() { - - auto prefs = utility::JsonStore(); - if (not global_store::preference_load_defaults( - prefs, utility::xstudio_root("/preference"))) { - spdlog::error( - "Failed to load application preferences {}", utility::xstudio_root("/preference")); - std::exit(EXIT_FAILURE); - } - return prefs; -} - -CafSys::CafSys() { - - ACTOR_INIT_GLOBAL_META() - - caf::core::init_global_meta_objects(); - caf::io::middleman::init_global_meta_objects(); - - const char *args[] = {"--caf.scheduler.policy=sharing"}; - conf = new caf_utility::caf_config(1, const_cast(args)); - - the_system_ = new caf::actor_system(*conf); - utility::start_logger(spdlog::level::info); - - // preferences are required to set the comms port range, which is - // required to enable xStudio python API - auto prefs = load_preferences(); - - // Create the global actor that manages highest level objects - global_actor_ = the_system_->spawn(prefs); - - { - // This bit is wrong - I'm doing this to kick the global actor - // to enable the API. I don't know the correct mechanism for this - caf::scoped_actor self{*(the_system_)}; - self->send(global_actor_, json_store::update_atom_v, prefs); - } -} - -CafSys::~CafSys() { - - { - caf::scoped_actor self{*(the_system_)}; - self->send_exit(global_actor_, caf::exit_reason::user_shutdown); - } - - // cancel actors talking to them selves. - the_system_->clock().cancel_all(); - utility::stop_logger(); - delete the_system_; - delete conf; -} diff --git a/src/pyside2_module/src/caf_system.hpp b/src/pyside2_module/src/caf_system.hpp deleted file mode 100644 index 3d593523c..000000000 --- a/src/pyside2_module/src/caf_system.hpp +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include - -namespace xstudio::caf_utility { -class caf_config; -} - -namespace xstudio { - -class CafSys { - - public: - static CafSys *instance(); - ~CafSys(); - - caf::actor_system &system() { return *the_system_; } - caf::actor &global_actor() { return global_actor_; } - - - private: - CafSys(); - caf::actor_system *the_system_; - caf_utility::caf_config *conf; - caf::actor global_actor_; -}; - -} // namespace xstudio \ No newline at end of file diff --git a/src/pyside2_module/src/plain_viewport.cpp b/src/pyside2_module/src/plain_viewport.cpp deleted file mode 100644 index 1fa40da64..000000000 --- a/src/pyside2_module/src/plain_viewport.cpp +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "xstudio/ui/qt/viewport_widget.hpp" -#include "xstudio/atoms.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/caf_helpers.hpp" -#include "xstudio/ui/mouse.hpp" - -CAF_PUSH_WARNINGS -#include "plain_viewport.hpp" -#include "caf_system.hpp" -#include -CAF_POP_WARNINGS - -using namespace xstudio; - -PlainViewport::PlainViewport(QWidget *parent) : QWidget(parent) { - - // As far as I can tell caf only allows config to be modified - // through cli args. We prefer the 'sharing' policy rather than - // 'stealing'. The latter results in multiple threads spinning - // aggressively pushing CPU load very high during playback. - - caf::scoped_actor self{CafSys::instance()->system()}; - - // Tell the global_actor to create the 'studio' top level object that manages the 'session' - // object(s) - utility::request_receive( - *self, CafSys::instance()->global_actor(), global::create_studio_atom_v, "XStudio"); - - // Create a session object - auto session = utility::request_receive( - *self, CafSys::instance()->global_actor(), session::session_atom_v); - - // Create the xstudio viewport widget/actor - viewport_widget_ = new ui::qt::ViewportGLWidget(this); - viewport_widget_->init(CafSys::instance()->system()); - - QVBoxLayout *layout = new QVBoxLayout((QWidget *)this); - layout->addWidget(viewport_widget_, 1); - - // Get the session to create a playlist - auto playlist = utility::request_receive( - *self, session, session::add_playlist_atom_v, "Test") - .second.actor(); - - // ask the playlist to create a playhead - caf::actor playhead = utility::request_receive( - *self, playlist, playlist::create_playhead_atom_v) - .actor(); - - // pass the playhead to the viewport - it will 'attach' itself to the playhead - // so that it is receiving video frames for display - viewport_widget_->set_playhead(playhead); -} - -PlainViewport::~PlainViewport() {} - -void PlainViewport::resizeEvent(QResizeEvent *event) { qDebug() << event; } diff --git a/src/pyside2_module/src/qml_viewport.cpp b/src/pyside2_module/src/qml_viewport.cpp deleted file mode 100644 index 81b98e35e..000000000 --- a/src/pyside2_module/src/qml_viewport.cpp +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "xstudio/ui/qt/viewport_widget.hpp" -#include "xstudio/atoms.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/caf_helpers.hpp" -#include "xstudio/ui/mouse.hpp" - -#include "xstudio/ui/qml/module_ui.hpp" //NOLINT -#include "xstudio/ui/qml/module_menu_ui.hpp" //NOLINT -#include "xstudio/ui/qml/embedded_python_ui.hpp" //NOLINT -#include "xstudio/ui/qml/helper_ui.hpp" //NOLINT -#include "xstudio/ui/qml/bookmark_ui.hpp" //NOLINT -#include "xstudio/ui/qml/studio_ui.hpp" //NOLINT -#include "xstudio/ui/qml/thumbnail_provider_ui.hpp" -#include "xstudio/ui/qml/qml_viewport.hpp" //NOLINT -#include "xstudio/ui/mouse.hpp" - -#include "QuickFuture" - -Q_DECLARE_METATYPE(QList) -Q_DECLARE_METATYPE(QFuture) -Q_DECLARE_METATYPE(QFuture>) - -CAF_PUSH_WARNINGS -#include "qml_viewport.hpp" -#include "caf_system.hpp" -#include -CAF_POP_WARNINGS - -using namespace xstudio; - -PySideQmlViewport::PySideQmlViewport(QWidget *parent) : QQuickWidget(parent) { - - // As far as I can tell caf only allows config to be modified - // through cli args. We prefer the 'sharing' policy rather than - // 'stealing'. The latter results in multiple threads spinning - // aggressively pushing CPU load very high during playback. - - caf::scoped_actor self{CafSys::instance()->system()}; - - // Tell the global_actor to create the 'studio' top level object that manages the 'session' - // object(s) - utility::request_receive( - *self, CafSys::instance()->global_actor(), global::create_studio_atom_v, "XStudio"); - - // Create a session object - auto session = utility::request_receive( - *self, CafSys::instance()->global_actor(), session::session_atom_v); - - // Get the session to create a playlist - auto playlist = utility::request_receive( - *self, session, session::add_playlist_atom_v, "Test") - .second.actor(); - - // ask the playlist to create a playhead - caf::actor playhead = utility::request_receive( - *self, playlist, playlist::create_playhead_atom_v) - .actor(); - - // Register our custom types - qmlRegisterType("xstudio.qml.studio", 1, 0, "Studio"); - qmlRegisterType("xstudio.qml.semver", 1, 0, "SemVer"); - qmlRegisterType( - "xstudio.qml.cursor_pos_provider", 1, 0, "CursorPosProvider"); - qmlRegisterType("xstudio.qml.viewport", 1, 0, "Viewport"); - qmlRegisterType("xstudio.qml.playlist", 1, 0, "Playlist"); - qmlRegisterType("xstudio.qml.media", 1, 0, "Media"); - qmlRegisterType("xstudio.qml.media_source", 1, 0, "MediaSource"); - qmlRegisterType("xstudio.qml.media_stream", 1, 0, "MediaStream"); - qmlRegisterType("xstudio.qml.bookmarks", 1, 0, "Bookmarks"); - qmlRegisterType("xstudio.qml.bookmarks", 1, 0, "BookmarkDetail"); - - qmlRegisterType("xstudio.qml.session", 1, 0, "Session"); - qmlRegisterType("xstudio.qml.session", 1, 0, "ContainerGroupUI"); - qmlRegisterType("xstudio.qml.session", 1, 0, "ContainerDividerUI"); - qmlRegisterType("xstudio.qml.timeline", 1, 0, "TimelineUI"); - qmlRegisterType("xstudio.qml.subset", 1, 0, "SubsetUI"); - qmlRegisterType("xstudio.qml.contact_sheet", 1, 0, "ContactSheetUI"); - qmlRegisterType("xstudio.qml.uuid", 1, 0, "QMLUuid"); - qmlRegisterType("xstudio.qml.clipboard", 1, 0, "Clipboard"); - - qmlRegisterType("xstudio.qml.module", 1, 0, "XsModuleAttributesModel"); - qmlRegisterType("xstudio.qml.module", 1, 0, "XsModuleAttributes"); - qmlRegisterType("xstudio.qml.module", 1, 0, "XsModuleMenu"); - - qRegisterMetaType("MediaUI*"); - // qRegisterMetaType("BookmarkDetailUI*"); - qRegisterMetaType("const BookmarkDetailUI*"); - - QuickFuture::registerType(); - QuickFuture::registerType>(); - - // Add a CafSystemObject to the application - this is QObject that simply - // holds a reference to the actor system so that we can access the system - // in Qt main loop - new CafSystemObject(QCoreApplication::instance(), CafSys::instance()->system()); - const QUrl url(QStringLiteral("qrc:/main_viewport_only.qml")); - - engine()->addImageProvider(QLatin1String("thumbnail"), new ThumbnailProvider); - engine()->rootContext()->setContextProperty( - "applicationDirPath", QGuiApplication::applicationDirPath()); - - auto helper = new Helpers(engine()); - engine()->rootContext()->setContextProperty("helpers", helper); - - engine()->addImportPath("qrc:///"); - engine()->addImportPath("qrc:///extern"); - - // gui plugins.. - engine()->addImportPath(QStringFromStd(xstudio_root("/plugin/qml"))); - engine()->addPluginPath(QStringFromStd(xstudio_root("/plugin/qml"))); - - setSource(url); -} - -void PySideQmlViewport::resizeEvent(QResizeEvent *event) { - - rootObject()->setWidth(event->size().width()); - rootObject()->setHeight(event->size().height()); - QQuickWidget::resizeEvent(event); -} diff --git a/src/pyside6_module/src/CMakeLists.txt b/src/pyside6_module/src/CMakeLists.txt new file mode 100644 index 000000000..f05a86dbd --- /dev/null +++ b/src/pyside6_module/src/CMakeLists.txt @@ -0,0 +1,189 @@ +project(__pyside6_xstudio VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) + +# Enable policy to run automoc on generated files. +if(POLICY CMP0071) + cmake_policy(SET CMP0071 NEW) +endif() + +if (CLANG_PATH) + set(ENV{CLANG_INSTALL_DIR} $CLANG_PATH) +endif() + +find_package(Shiboken6 REQUIRED) +find_package(PySide6 REQUIRED) +find_package(Python COMPONENTS Interpreter Development) + +get_target_property(PySide6_INCLUDE_DIR PySide6::pyside6 INTERFACE_INCLUDE_DIRECTORIES) +get_target_property(SHIBOKEN6_INCLUDE_DIRS Shiboken6::libshiboken INTERFACE_INCLUDE_DIRECTORIES) +list(GET SHIBOKEN6_INCLUDE_DIRS 0 SHIBOKEN6_INCLUDE_ROOT_DIR) + +get_target_property(PySide6_LIBS PySide6::pyside6 INTERFACE_LINK_LIBRARIES) + +find_package(OpenGL REQUIRED) +find_package(GLEW REQUIRED) +find_package(Qt6 COMPONENTS Core Gui Widgets OpenGL Quick Qml QuickWidgets OpenGLWidgets Network REQUIRED) +find_package(OpenSSL) +find_package(ZLIB) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +# Set up the options to pass to shiboken. +set(WRAPPED_HEADER ${CMAKE_CURRENT_SOURCE_DIR}/wrappedclasses.h) +set(TYPESYSTEM_FILE ${CMAKE_CURRENT_SOURCE_DIR}/typesystem.xml) + +# Get all relevant Qt include dirs, to pass them on to shiboken. +get_property(QT_CORE_INCLUDE_DIRS TARGET Qt6::Core PROPERTY INTERFACE_INCLUDE_DIRECTORIES) +get_property(QT_GUI_INCLUDE_DIRS TARGET Qt6::Gui PROPERTY INTERFACE_INCLUDE_DIRECTORIES) +get_property(QT_WIDGETS_INCLUDE_DIRS TARGET Qt6::Widgets PROPERTY INTERFACE_INCLUDE_DIRECTORIES) +get_property(QT_QUICKWIDGETS_INCLUDE_DIRS TARGET Qt6::QuickWidgets PROPERTY INTERFACE_INCLUDE_DIRECTORIES) +get_property(QT_QUICK_INCLUDE_DIRS TARGET Qt6::Quick PROPERTY INTERFACE_INCLUDE_DIRECTORIES) +get_property(QT_NETWORK_INCLUDE_DIRS TARGET Qt6::Network PROPERTY INTERFACE_INCLUDE_DIRECTORIES) +get_property(QT_QML_INCLUDE_DIRS TARGET Qt6::Qml PROPERTY INTERFACE_INCLUDE_DIRECTORIES) +get_property(QT_OPNGLWIDGETS_INCLUDE_DIRS TARGET Qt6::OpenGLWidgets PROPERTY INTERFACE_INCLUDE_DIRECTORIES) +get_property(QT_OPENGL_INCLUDE_DIRS TARGET Qt6::OpenGL PROPERTY INTERFACE_INCLUDE_DIRECTORIES) +set(QT_INCLUDE_DIRS ${QT_CORE_INCLUDE_DIRS} ${QT_GUI_INCLUDE_DIRS} ${QT_WIDGETS_INCLUDE_DIRS} + ${QT_QUICKWIDGETS_INCLUDE_DIRS} ${QT_QUICK_INCLUDE_DIRS} ${QT_NETWORK_INCLUDE_DIRS} ${QT_QML_INCLUDE_DIRS} + ${QT_OPENGL_INCLUDE_DIRS} ${QT_OPNGLWIDGETS_INCLUDE_DIRS}) +set(INCLUDES "") +foreach(INCLUDE_DIR ${QT_INCLUDE_DIRS}) + list(APPEND INCLUDES "-I${INCLUDE_DIR}") +endforeach() + +list(APPEND INCLUDES "-I/opt/rh/devtoolset-9/root/usr/lib/gcc/x86_64-redhat-linux/9/include") + +set(SHIBOKEN_OPTIONS --generator-set=shiboken --enable-parent-ctor-heuristic + --enable-pyside-extensions --enable-return-value-heuristic --use-isnull-as-nb_nonzero + --avoid-protected-hack + ${INCLUDES} + -I${CMAKE_CURRENT_SOURCE_DIR} + -T${CMAKE_CURRENT_SOURCE_DIR} + -T${PySide6_INCLUDE_DIR}/../../share/PySide6/typesystems + --output-directory=${CMAKE_CURRENT_BINARY_DIR} + ) + +# Specify which sources will be generated by shiboken, and their dependencies. +set(GENERATED_SOURCES + ${CMAKE_CURRENT_BINARY_DIR}/__pyside6_xstudio/__pyside6_xstudio_module_wrapper.cpp + ${CMAKE_CURRENT_BINARY_DIR}/__pyside6_xstudio/pysideqmlviewport_wrapper.cpp + ${CMAKE_CURRENT_BINARY_DIR}/__pyside6_xstudio/xstudiopyapp_wrapper.cpp + ${CMAKE_CURRENT_BINARY_DIR}/__pyside6_xstudio/plainviewport_wrapper.cpp) + +set(GENERATED_SOURCES_DEPENDENCIES + ${WRAPPED_HEADER} + ${TYPESYSTEM_FILE} + ) + +set(SHIBOKEN_PATH shiboken6) + +# Add custom target to run shiboken. +add_custom_command(OUTPUT ${GENERATED_SOURCES} + COMMAND ${SHIBOKEN_PATH} + ${SHIBOKEN_OPTIONS} ${WRAPPED_HEADER} ${TYPESYSTEM_FILE} + DEPENDS ${GENERATED_SOURCES_DEPENDENCIES} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Running generator for ${TYPESYSTEM_FILE}.") + +set(SOURCES + plain_viewport.cpp + qml_viewport.cpp + xstudio_app.cpp + ../../../../ui/qml/xstudio/qml.qrc + ${GENERATED_SOURCES} +) + +add_library(${PROJECT_NAME} SHARED ${SOURCES}) + +if(WIN32) + set(EXE_EXTENSION ".exe") +else() + set(EXE_EXTENSION ".bin") +endif() + +set_target_properties(${PROJECT_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + OUTPUT_NAME "${PROJECT_NAME}${EXE_EXTENSION}" + LINK_DEPENDS_NO_SHARED true +) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + xstudio::caf_utility + xstudio::colour_pipeline + xstudio::global + xstudio::ui::viewport + xstudio::ui::opengl::viewport + xstudio::ui::qt::viewport_widget + xstudio::ui::qml::conform + xstudio::ui::qml::embedded_python + xstudio::ui::qml::global_store + xstudio::ui::qml::helper + xstudio::ui::qml::log + xstudio::ui::qml::bookmark + xstudio::ui::qml::session + xstudio::ui::qml::studio + xstudio::ui::qml::viewport + xstudio::utility + xstudio::module + PRIVATE + CAF::core + $<$:GLdispatch> + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::OpenGL + Qt6::OpenGLWidgets + Qt6::Quick + Qt6::QuickWidgets + Qt6::Network + Qt6::Qml + OpenSSL::SSL + ZLIB::ZLIB + Shiboken6::libshiboken + PySide6::pyside6 +) + +default_options(${PROJECT_NAME}) + +include_directories("${PySide6_INCLUDE_DIR}/QtGui") +include_directories("${PySide6_INCLUDE_DIR}/QtCore") +include_directories("${PySide6_INCLUDE_DIR}/QtOpenGL") +include_directories("${PySide6_INCLUDE_DIR}/QtOpenGLWidgets") +include_directories("${PySide6_INCLUDE_DIR}/QtWidgets") +include_directories("${PySide6_INCLUDE_DIR}/QtQuick") +include_directories("${PySide6_INCLUDE_DIR}/QtQuickWidgets") +include_directories("${PySide6_INCLUDE_DIR}/QtNetwork") +include_directories("${PySide6_INCLUDE_DIR}/QtQml") +include_directories("${PySide6_INCLUDE_DIR}/PySide6") +include_directories(${SHIBOKEN_PYTHON_INCLUDE_DIRS}) + +set(PYTHONVP "python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}") + +set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME ${PROJECT_NAME}) + +if(INSTALL_XSTUDIO) + set_target_properties( + ${PROJECT_NAME} + PROPERTIES + #PREFIX "${PYTHON_MODULE_PREFIX}" - can't get this to work, module isn't found when you do import + PREFIX "" + #SUFFIX "${PYTHON_MODULE_EXTENSION}" + SUFFIX ".so" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/python/lib/${PYTHONVP}/site-packages/xstudio/gui" + ) +else() + set_target_properties( + ${PROJECT_NAME} + PROPERTIES + #PREFIX "${PYTHON_MODULE_PREFIX}" + PREFIX "" + #SUFFIX "${PYTHON_MODULE_EXTENSION}" + SUFFIX ".so" + ) + + install(TARGETS ${PROJECT_NAME} + LIBRARY DESTINATION lib/python/xstudio/gui) + +endif(INSTALL_XSTUDIO) diff --git a/src/pyside6_module/src/plain_viewport.cpp b/src/pyside6_module/src/plain_viewport.cpp new file mode 100644 index 000000000..3a81a7c37 --- /dev/null +++ b/src/pyside6_module/src/plain_viewport.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "xstudio/global/xstudio_actor_system.hpp" //NOLINT +#include "xstudio/ui/qml/helper_ui.hpp" //NOLINT +#include "xstudio/ui/qt/viewport_widget.hpp" //NOLINT + +CAF_PUSH_WARNINGS +#include "plain_viewport.hpp" +#include +CAF_POP_WARNINGS + +using namespace xstudio; + +PlainViewport::PlainViewport(QWidget *parent, const QString window_id) : QWidget(parent) { + + // Create the xstudio viewport widget/actor + viewport_widget_ = new ui::qt::ViewportGLWidget(this, true, window_id); + viewport_widget_->init(CafActorSystem::system()); + + QVBoxLayout *layout = new QVBoxLayout((QWidget *)this); + layout->addWidget(viewport_widget_, 1); +} + +PlainViewport::~PlainViewport() {} + +void PlainViewport::resizeEvent(QResizeEvent *event) { + + viewport_widget_->resizeGL(event->size().width(), event->size().height()); +} + +QString PlainViewport::name() { return viewport_widget_->name(); } diff --git a/src/pyside2_module/src/plain_viewport.hpp b/src/pyside6_module/src/plain_viewport.hpp similarity index 83% rename from src/pyside2_module/src/plain_viewport.hpp rename to src/pyside6_module/src/plain_viewport.hpp index 206c1e7b2..a0a317589 100644 --- a/src/pyside2_module/src/plain_viewport.hpp +++ b/src/pyside6_module/src/plain_viewport.hpp @@ -18,11 +18,16 @@ class PlainViewport : public QWidget { Q_OBJECT public: - PlainViewport(QWidget *parent); + PlainViewport(QWidget *parent, const QString window_id); + ~PlainViewport() override; void resizeEvent(QResizeEvent *event) override; + public slots: + + QString name(); + private: xstudio::ui::qt::ViewportGLWidget *viewport_widget_; }; \ No newline at end of file diff --git a/src/pyside6_module/src/qml_viewport.cpp b/src/pyside6_module/src/qml_viewport.cpp new file mode 100644 index 000000000..732bd017d --- /dev/null +++ b/src/pyside6_module/src/qml_viewport.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "xstudio/global/xstudio_actor_system.hpp" //NOLINT +#include "xstudio/ui/qml/studio_ui.hpp" //NOLINT + +CAF_PUSH_WARNINGS +#include "qml_viewport.hpp" +#include +CAF_POP_WARNINGS + +using namespace xstudio; + +PySideQmlViewport::PySideQmlViewport(QWidget *parent) : QQuickWidget(parent) { + + ui::qml::setup_xstudio_qml_emgine(engine(), CafActorSystem::system()); + + setResizeMode(QQuickWidget::SizeRootObjectToView); + + const QUrl url(QStringLiteral("qrc:/views/viewport/XsViewportWidget.qml")); + setSource(url); +} + +QString PySideQmlViewport::name() { return rootObject()->property("name").toString(); } diff --git a/src/pyside2_module/src/qml_viewport.hpp b/src/pyside6_module/src/qml_viewport.hpp similarity index 84% rename from src/pyside2_module/src/qml_viewport.hpp rename to src/pyside6_module/src/qml_viewport.hpp index 36be46091..780a06a0a 100644 --- a/src/pyside2_module/src/qml_viewport.hpp +++ b/src/pyside6_module/src/qml_viewport.hpp @@ -12,5 +12,6 @@ class PySideQmlViewport : public QQuickWidget { PySideQmlViewport(QWidget *parent = nullptr); ~PySideQmlViewport() override = default; - void resizeEvent(QResizeEvent *event) override; + public slots: + QString name(); }; \ No newline at end of file diff --git a/src/pyside2_module/src/threaded_viewport.cpp b/src/pyside6_module/src/threaded_viewport.cpp similarity index 98% rename from src/pyside2_module/src/threaded_viewport.cpp rename to src/pyside6_module/src/threaded_viewport.cpp index 62c7268a7..4b4d766c8 100644 --- a/src/pyside2_module/src/threaded_viewport.cpp +++ b/src/pyside6_module/src/threaded_viewport.cpp @@ -3,8 +3,13 @@ #include "xstudio/media_reader/media_reader.hpp" #include "threaded_viewport.hpp" + +#ifdef __apple__ +#include +#else +#include #include -#include +#endif #define WIDTH 2000 #define HEIGHT 1000 diff --git a/src/pyside2_module/src/threaded_viewport.hpp b/src/pyside6_module/src/threaded_viewport.hpp similarity index 100% rename from src/pyside2_module/src/threaded_viewport.hpp rename to src/pyside6_module/src/threaded_viewport.hpp diff --git a/src/pyside2_module/src/typesystem.xml b/src/pyside6_module/src/typesystem.xml similarity index 86% rename from src/pyside2_module/src/typesystem.xml rename to src/pyside6_module/src/typesystem.xml index 0f7998004..644a3af81 100644 --- a/src/pyside2_module/src/typesystem.xml +++ b/src/pyside6_module/src/typesystem.xml @@ -1,5 +1,5 @@ - + @@ -7,8 +7,8 @@ - + diff --git a/src/pyside2_module/src/wrappedclasses.h b/src/pyside6_module/src/wrappedclasses.h similarity index 80% rename from src/pyside2_module/src/wrappedclasses.h rename to src/pyside6_module/src/wrappedclasses.h index 3e073b4b9..e7f03f39b 100644 --- a/src/pyside2_module/src/wrappedclasses.h +++ b/src/pyside6_module/src/wrappedclasses.h @@ -3,6 +3,6 @@ #include #include -#include +#include #endif // WRAPPEDCLASSES_H \ No newline at end of file diff --git a/src/pyside6_module/src/xstudio_app.cpp b/src/pyside6_module/src/xstudio_app.cpp new file mode 100644 index 000000000..a799a71ff --- /dev/null +++ b/src/pyside6_module/src/xstudio_app.cpp @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "xstudio/global/xstudio_actor_system.hpp" //NOLINT +#include "xstudio/ui/qml/helper_ui.hpp" //NOLINT + +CAF_PUSH_WARNINGS +#include "xstudio_app.hpp" +#include +CAF_POP_WARNINGS + +using namespace xstudio; + +XstudioPyApp::XstudioPyApp(QWidget *parent) : QObject(parent) { + + // make sure our logger is started + utility::start_logger(spdlog::level::info); + + // this call sets up the CAF actor system, if it hasn't already happened + CafActorSystem::instance(); + + // this call sets up all the xstudio core components + CafActorSystem::global_actor(false); +} + +XstudioPyApp::~XstudioPyApp() { CafActorSystem::exit(); } diff --git a/src/pyside6_module/src/xstudio_app.hpp b/src/pyside6_module/src/xstudio_app.hpp new file mode 100644 index 000000000..41d5ae966 --- /dev/null +++ b/src/pyside6_module/src/xstudio_app.hpp @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +class XstudioPyApp : public QObject { + Q_OBJECT + + public: + XstudioPyApp(QWidget *parent = nullptr); + ~XstudioPyApp(); +}; \ No newline at end of file diff --git a/src/python_module/src/CMakeLists.txt b/src/python_module/src/CMakeLists.txt index c47f3b85b..74f378b31 100644 --- a/src/python_module/src/CMakeLists.txt +++ b/src/python_module/src/CMakeLists.txt @@ -1,7 +1,7 @@ -project(__pybind_xstudio VERSION 0.1.0 LANGUAGES CXX) +project(__pybind_xstudio VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) -find_package(pybind11 CONFIG REQUIRED) -#find_package(caf CONFIG REQUIRED) +find_package(Python COMPONENTS Interpreter Development) +find_package(pybind11 CONFIG) find_package(Python COMPONENTS Interpreter) set(PYTHONVP "python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}") @@ -33,6 +33,19 @@ default_options_local(${PROJECT_NAME}) set_python_to_proper_build_type() +if (APPLE) + # Note - for Apple we install into the vcpkg installation structure at build time. + # Our install commands will copy the entire python3.X folder into the app bundle + # when making the re-locatable .app structure + if (${CMAKE_BUILD_TYPE} STREQUAL "Debug") + set(OUTPUT_DIR "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/debug/lib/${PYTHONVP}/site-packages/xstudio/core") + else() + set(OUTPUT_DIR "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/${PYTHONVP}/site-packages/xstudio/core") + endif() +else() + set(OUTPUT_DIR "${CMAKE_BINARY_DIR}/bin/python/lib/${PYTHONVP}/site-packages/xstudio/core") +endif() + target_link_libraries( ${PROJECT_NAME} PUBLIC @@ -41,8 +54,9 @@ target_link_libraries( xstudio::global_store_static xstudio::json_store_static xstudio::timeline_static - caf::core - caf::io + xstudio::media_static + CAF::core + CAF::io ) if(INSTALL_XSTUDIO) @@ -51,7 +65,7 @@ if(INSTALL_XSTUDIO) PROPERTIES PREFIX "${PYTHON_MODULE_PREFIX}" SUFFIX "${PYTHON_MODULE_EXTENSION}" - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/python/lib/${PYTHONVP}/site-packages/xstudio/core" + LIBRARY_OUTPUT_DIRECTORY ${OUTPUT_DIR} ) else() set_target_properties( @@ -67,6 +81,5 @@ else() endif(INSTALL_XSTUDIO) if(WIN32) -install(TARGETS ${PROJECT_NAME} DESTINATION "${CMAKE_INSTALL_PREFIX}/python/xstudio/core") +install(TARGETS ${PROJECT_NAME} DESTINATION python/xstudio/core) endif() - diff --git a/src/python_module/src/py_atoms.cpp b/src/python_module/src/py_atoms.cpp index 0d556e7c9..58a009a67 100644 --- a/src/python_module/src/py_atoms.cpp +++ b/src/python_module/src/py_atoms.cpp @@ -18,10 +18,10 @@ using namespace xstudio::bookmark; using namespace xstudio::broadcast; using namespace xstudio::colour_pipeline; using namespace xstudio::data_source; -using namespace xstudio::event; using namespace xstudio::global; using namespace xstudio::global_store; using namespace xstudio::history; +using namespace xstudio::http_client; using namespace xstudio::json_store; using namespace xstudio::media; using namespace xstudio::media_cache; @@ -33,14 +33,14 @@ using namespace xstudio::playhead; using namespace xstudio::playlist; using namespace xstudio::plugin_manager; using namespace xstudio::session; -using namespace xstudio::sync; -using namespace xstudio::tag; +using namespace xstudio::shotgun_client; using namespace xstudio::thumbnail; using namespace xstudio::timeline; -using namespace xstudio::ui; using namespace xstudio::ui::keypress_monitor; +using namespace xstudio::ui::model_data; using namespace xstudio::ui::qml; using namespace xstudio::ui::viewport; +using namespace xstudio::ui; using namespace xstudio::utility; using namespace xstudio; @@ -57,16 +57,15 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::broadcast, join_broadcast_atom); ADD_ATOM(xstudio::broadcast, leave_broadcast_atom); ADD_ATOM(xstudio::broadcast, broadcast_down_atom); - ADD_ATOM(xstudio::sync, authorise_connection_atom); - ADD_ATOM(xstudio::sync, get_sync_atom); - ADD_ATOM(xstudio::sync, request_connection_atom); ADD_ATOM(xstudio::media_hook, get_media_hook_atom); + ADD_ATOM(xstudio::media_hook, get_clip_hook_atom); ADD_ATOM(xstudio::media_hook, gather_media_sources_atom); ADD_ATOM(xstudio::media_metadata, get_metadata_atom); ADD_ATOM(xstudio::timeline, active_range_atom); ADD_ATOM(xstudio::timeline, available_range_atom); + ADD_ATOM(xstudio::timeline, bake_atom); ADD_ATOM(xstudio::timeline, duration_atom); ADD_ATOM(xstudio::timeline, erase_item_at_frame_atom); ADD_ATOM(xstudio::timeline, erase_item_atom); @@ -74,7 +73,10 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::timeline, insert_item_atom); ADD_ATOM(xstudio::timeline, item_atom); ADD_ATOM(xstudio::timeline, item_name_atom); + ADD_ATOM(xstudio::timeline, item_lock_atom); ADD_ATOM(xstudio::timeline, item_flag_atom); + ADD_ATOM(xstudio::timeline, item_marker_atom); + ADD_ATOM(xstudio::timeline, item_prop_atom); ADD_ATOM(xstudio::timeline, focus_atom); ADD_ATOM(xstudio::timeline, move_item_atom); ADD_ATOM(xstudio::timeline, move_item_at_frame_atom); @@ -83,6 +85,8 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::timeline, split_item_atom); ADD_ATOM(xstudio::timeline, split_item_at_frame_atom); ADD_ATOM(xstudio::timeline, trimmed_range_atom); + ADD_ATOM(xstudio::timeline, item_selection_atom); + ADD_ATOM(xstudio::timeline, item_type_atom); ADD_ATOM(xstudio::thumbnail, cache_path_atom); ADD_ATOM(xstudio::thumbnail, cache_stats_atom); @@ -109,6 +113,7 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::playlist, create_subset_atom); ADD_ATOM(xstudio::playlist, create_timeline_atom); ADD_ATOM(xstudio::playlist, duplicate_container_atom); + ADD_ATOM(xstudio::playlist, expanded_atom); ADD_ATOM(xstudio::playlist, get_change_event_group_atom); ADD_ATOM(xstudio::playlist, get_container_atom); ADD_ATOM(xstudio::playlist, get_media_atom); @@ -131,12 +136,14 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::playlist, select_all_media_atom); ADD_ATOM(xstudio::playlist, select_media_atom); ADD_ATOM(xstudio::playlist, set_playlist_in_viewer_atom); - ADD_ATOM(xstudio::playlist, sort_alphabetically_atom); + ADD_ATOM(xstudio::playlist, sort_by_media_display_info_atom); ADD_ATOM(xstudio::session, session_atom); ADD_ATOM(xstudio::session, session_request_atom); ADD_ATOM(xstudio::session, add_playlist_atom); ADD_ATOM(xstudio::session, get_playlist_atom); - ADD_ATOM(xstudio::session, current_playlist_atom); + ADD_ATOM(xstudio::session, get_push_playlist_atom); + ADD_ATOM(xstudio::session, active_media_container_atom); + ADD_ATOM(xstudio::session, viewport_active_media_container_atom); ADD_ATOM(xstudio::session, get_playlists_atom); ADD_ATOM(xstudio::session, media_rate_atom); ADD_ATOM(xstudio::session, load_uris_atom); @@ -145,6 +152,7 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::session, path_atom); ADD_ATOM(xstudio::session, remove_serialise_target_atom); ADD_ATOM(xstudio::session, export_atom); + ADD_ATOM(xstudio::session, import_atom); ADD_ATOM(xstudio::media_reader, clear_precache_queue_atom); ADD_ATOM(xstudio::media_reader, get_image_atom); ADD_ATOM(xstudio::media_reader, get_thumbnail_atom); @@ -158,7 +166,6 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::media_reader, push_image_atom); ADD_ATOM(xstudio::media_reader, retire_readers_atom); ADD_ATOM(xstudio::media_reader, supported_atom); - ADD_ATOM(xstudio::media_cache, cached_frames_atom); ADD_ATOM(xstudio::media_cache, count_atom); ADD_ATOM(xstudio::media_cache, erase_atom); ADD_ATOM(xstudio::media_cache, keys_atom); @@ -175,9 +182,6 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::module, attribute_role_data_atom); ADD_ATOM(xstudio::module, attribute_value_atom); ADD_ATOM(xstudio::module, module_ui_events_group_atom); - ADD_ATOM(xstudio::module, full_attributes_description_atom); - ADD_ATOM(xstudio::module, request_full_attributes_description_atom); - ADD_ATOM(xstudio::module, request_menu_attributes_description_atom); ADD_ATOM(xstudio::module, change_attribute_request_atom); ADD_ATOM(xstudio::module, change_attribute_value_atom); ADD_ATOM(xstudio::module, attribute_deleted_event_atom); @@ -188,17 +192,18 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::module, disconnect_from_ui_atom); ADD_ATOM(xstudio::module, join_module_attr_events_atom); ADD_ATOM(xstudio::module, leave_module_attr_events_atom); - ADD_ATOM(xstudio::module, remove_attrs_from_ui_atom); ADD_ATOM(xstudio::module, grab_all_keyboard_input_atom); ADD_ATOM(xstudio::module, grab_all_mouse_input_atom); ADD_ATOM(xstudio::module, attribute_uuids_atom); + ADD_ATOM(xstudio::module, module_add_menu_item_atom); + ADD_ATOM(xstudio::module, module_remove_menu_item_atom); ADD_ATOM(xstudio::module, remove_attribute_atom); + ADD_ATOM(xstudio::module, set_node_data_atom); ADD_ATOM(xstudio::global, exit_atom); ADD_ATOM(xstudio::global, api_exit_atom); ADD_ATOM(xstudio::global, busy_atom); ADD_ATOM(xstudio::global, create_studio_atom); - ADD_ATOM(xstudio::global, get_api_mode_atom); ADD_ATOM(xstudio::global, get_application_mode_atom); ADD_ATOM(xstudio::global, get_global_audio_cache_atom); ADD_ATOM(xstudio::global, get_global_image_cache_atom); @@ -212,10 +217,12 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::global, remote_session_name_atom); ADD_ATOM(xstudio::global, status_atom); ADD_ATOM(xstudio::global, get_actor_from_registry_atom); + ADD_ATOM(xstudio::global, authenticate_atom); ADD_ATOM(xstudio::media, acquire_media_detail_atom); ADD_ATOM(xstudio::media, add_media_source_atom); ADD_ATOM(xstudio::media, add_media_stream_atom); + ADD_ATOM(xstudio::media, current_media_atom); ADD_ATOM(xstudio::media, current_media_source_atom); ADD_ATOM(xstudio::media, current_media_stream_atom); ADD_ATOM(xstudio::media, get_edit_list_atom); @@ -232,6 +239,8 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::media, rescan_atom); ADD_ATOM(xstudio::media, media_reference_atom); ADD_ATOM(xstudio::media, source_offset_frames_atom); + ADD_ATOM(xstudio::media, media_display_info_atom); + ADD_ATOM(xstudio::global_store, autosave_atom); ADD_ATOM(xstudio::global_store, do_autosave_atom); ADD_ATOM(xstudio::global_store, save_atom); @@ -252,6 +261,7 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::utility, type_atom); ADD_ATOM(xstudio::utility, uuid_atom); ADD_ATOM(xstudio::utility, version_atom); + ADD_ATOM(xstudio::utility, notification_atom); ADD_ATOM(xstudio::json_store, get_json_atom); ADD_ATOM(xstudio::json_store, jsonstore_change_atom); ADD_ATOM(xstudio::json_store, patch_atom); @@ -272,7 +282,6 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::playhead, get_selection_atom); ADD_ATOM(xstudio::playhead, selection_changed_atom); ADD_ATOM(xstudio::playhead, jump_atom); - ADD_ATOM(xstudio::playhead, scrub_frame_atom); ADD_ATOM(xstudio::playhead, key_child_playhead_atom); ADD_ATOM(xstudio::playhead, key_playhead_index_atom); ADD_ATOM(xstudio::playhead, logical_frame_atom); @@ -297,7 +306,6 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::playhead, dropped_frame_atom); ADD_ATOM(xstudio::playhead, simple_loop_end_atom); ADD_ATOM(xstudio::playhead, simple_loop_start_atom); - ADD_ATOM(xstudio::playhead, skip_through_sources_atom); ADD_ATOM(xstudio::playhead, source_atom); ADD_ATOM(xstudio::playhead, step_atom); ADD_ATOM(xstudio::playhead, use_loop_range_atom); @@ -310,36 +318,36 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::playhead, monitored_atom); ADD_ATOM(xstudio::playhead, media_logical_frame_atom); // ADD_ATOM(xstudio::audio, push_samples_atom); - // ADD_ATOM(xstudio::http_client, http_get_atom); - // ADD_ATOM(xstudio::http_client, http_get_simple_atom); - // ADD_ATOM(xstudio::http_client, http_post_atom); - // ADD_ATOM(xstudio::http_client, http_post_simple_atom); - // ADD_ATOM(xstudio::http_client, http_put_atom); - // ADD_ATOM(xstudio::http_client, http_put_simple_atom); - // ADD_ATOM(xstudio::http_client, http_delete_atom); - // ADD_ATOM(xstudio::http_client, http_delete_simple_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_acquire_authentication_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_acquire_token_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_authenticate_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_credential_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_entity_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_entity_filter_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_entity_search_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_host_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_info_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_link_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_preferences_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_projects_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_refresh_token_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_schema_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_schema_entity_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_schema_entity_fields_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_authentication_source_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_text_search_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_update_entity_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_create_entity_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_upload_atom); - // ADD_ATOM(xstudio::shotgun_client, shotgun_image_atom); + ADD_ATOM(xstudio::http_client, http_get_atom); + ADD_ATOM(xstudio::http_client, http_get_simple_atom); + ADD_ATOM(xstudio::http_client, http_post_atom); + ADD_ATOM(xstudio::http_client, http_post_simple_atom); + ADD_ATOM(xstudio::http_client, http_put_atom); + ADD_ATOM(xstudio::http_client, http_put_simple_atom); + ADD_ATOM(xstudio::http_client, http_delete_atom); + ADD_ATOM(xstudio::http_client, http_delete_simple_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_acquire_authentication_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_acquire_token_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_authenticate_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_credential_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_entity_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_entity_filter_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_entity_search_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_host_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_info_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_link_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_preferences_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_projects_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_refresh_token_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_schema_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_schema_entity_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_schema_entity_fields_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_authentication_source_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_text_search_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_update_entity_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_create_entity_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_upload_atom); + ADD_ATOM(xstudio::shotgun_client, shotgun_image_atom); ADD_ATOM(xstudio::plugin_manager, add_path_atom); ADD_ATOM(xstudio::plugin_manager, spawn_plugin_atom); ADD_ATOM(xstudio::plugin_manager, spawn_plugin_base_atom); @@ -359,11 +367,6 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::bookmark, bookmark_change_atom); ADD_ATOM(xstudio::bookmark, render_annotations_atom); - ADD_ATOM(xstudio::tag, get_tag_atom); - ADD_ATOM(xstudio::tag, get_tags_atom); - ADD_ATOM(xstudio::tag, add_tag_atom); - ADD_ATOM(xstudio::tag, remove_tag_atom); - ADD_ATOM(xstudio::history, undo_atom); ADD_ATOM(xstudio::history, redo_atom); ADD_ATOM(xstudio::history, log_atom); @@ -371,9 +374,15 @@ void py_config::add_atoms() { ADD_ATOM(xstudio::ui::viewport, viewport_playhead_atom); ADD_ATOM(xstudio::ui::viewport, quickview_media_atom); + ADD_ATOM(xstudio::ui::viewport, viewport_atom); + ADD_ATOM(xstudio::ui::viewport, hud_settings_atom); + ADD_ATOM(xstudio::ui::viewport, viewport_layout_atom); + ADD_ATOM(xstudio::ui, show_message_box_atom); ADD_ATOM(xstudio::ui::keypress_monitor, register_hotkey_atom); ADD_ATOM(xstudio::ui::keypress_monitor, hotkey_event_atom); + + ADD_ATOM(xstudio::ui::model_data, menu_node_activated_atom); } } // namespace caf::python diff --git a/src/python_module/src/py_config.hpp b/src/python_module/src/py_config.hpp index 751c32b38..3c986eee9 100644 --- a/src/python_module/src/py_config.hpp +++ b/src/python_module/src/py_config.hpp @@ -8,6 +8,8 @@ #include #include +#define PYBIND11_DETAILED_ERROR_MESSAGES + CAF_PUSH_WARNINGS #include "pybind11_json/pybind11_json.hpp" #include @@ -90,8 +92,8 @@ using cpp_binding_ptr = std::unique_ptr; template class has_register_struct { private: template - static auto test(U *x) -> decltype( - register_class(x, std::declval(), std::declval())); + static auto test(U *x) -> decltype(register_class( + x, std::declval(), std::declval())); static auto test(...) -> std::false_type; @@ -117,8 +119,8 @@ template class has_to_string { template class has_register_class { private: template - static auto test(U *x) -> decltype( - register_class(x, std::declval(), std::declval())); + static auto test(U *x) -> decltype(register_class( + x, std::declval(), std::declval())); static auto test(...) -> std::false_type; @@ -216,6 +218,30 @@ class py_config : public caf::actor_system_config { void add_types(); void add_messages(); + class int_py_binding : public py_binding { + public: + using py_binding::py_binding; + void append(message_builder &xs, py::handle x) const override { + // Awkward! PyBind chucks an error if you try to cast a python int + // (which is actually a long or long long) to a C int if the python + // int value > INT_MAX. + long foo = 12412; + int64_t a = PyLong_AsLong(x.ptr()); + int b = int(a); + if (a != int64_t(b)) { + xs.append(a); + } else { + xs.append(b); + } + } + }; + + void add_int_py() { + auto ptr = new int_py_binding("int"); + py_bindings_.emplace("int", py_binding_ptr{ptr}); + bindings_.emplace(std::move("int"), ptr); + } + template void add_py(std::string name) { auto ptr = new default_py_binding(name); py_bindings_.emplace(name, py_binding_ptr{ptr}); diff --git a/src/python_module/src/py_context.cpp b/src/python_module/src/py_context.cpp index 0bc22cdfb..2d3a0eb13 100644 --- a/src/python_module/src/py_context.cpp +++ b/src/python_module/src/py_context.cpp @@ -1,11 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 -#ifdef __GNUC__ // Check if GCC compiler is being used #pragma GCC diagnostic ignored "-Wattributes" -#endif #include "xstudio/utility/logging.hpp" #include "xstudio/utility/caf_helpers.hpp" #include "xstudio/utility/helpers.hpp" +#include "xstudio/atoms.hpp" #include "xstudio/global/global_actor.hpp" #include "py_opaque.hpp" @@ -14,6 +13,40 @@ namespace caf::python { +/* This actor receives event messages from a broadcast group. It then grabs the +GIL, converts the message to a PyTuple and then calls a python callback function. +This is the mechanism that lets us receive and process live broadcast messages +in a python process (running in xSTUDIO's embedded interpreter, or in an +independent interpreter)*/ +class EventToPythonThreadLockerActor : public caf::event_based_actor { + public: + EventToPythonThreadLockerActor( + caf::actor_config &cfg, + const xstudio::utility::Uuid &uuid, + caf::actor events_source, + py_context *context) + : caf::event_based_actor(cfg), uuid_(uuid), context_(context) { + + behavior_.assign( + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) { + // TODO: self clean up + }, + [=](message &_msg) { context_->execute_event_callback(_msg, uuid_); }); + + // join the events broadcast + mail(xstudio::broadcast::join_broadcast_atom_v, caf::actor_cast(this)) + .send(events_source); + } + + ~EventToPythonThreadLockerActor() = default; + + caf::behavior behavior_; + py_context *context_; + const xstudio::utility::Uuid uuid_; + + caf::behavior make_behavior() override { return behavior_; } +}; + inline void set_py_exception_fill(std::ostream &) { // end of recursion } @@ -34,7 +67,16 @@ py_context::py_context(int argc, char **argv) py_local_system_(*this), system_(xstudio::utility::ActorSystemSingleton::actor_system_ref(py_local_system_)), self_(system_), - remote_() {} + remote_() { + + thread_pauser_ = new xstudio::embedded_python::PythonThreadLocker(); + thread_pauser_ptr_.reset(thread_pauser_); +} + +py_context::~py_context() { + // shutdown system + disconnect(); +} std::optional py_context::py_build_message(const py::args &xs) { if (xs.size() < 2) { @@ -68,7 +110,7 @@ void py_context::py_send(const py::args &xs) { auto dest = (*i).cast(); auto msg = py_build_message(xs); if (msg) - self_->send(dest, *msg); + self_->mail(*msg).send(dest); } caf::actor py_context::py_spawn(const py::args &xs) { @@ -102,51 +144,97 @@ caf::actor py_context::py_remote_spawn(const py::args &xs) { return caf::actor(); } -void py_context::py_join(const py::args &xs) { - if (xs.size() < 1) { - set_py_exception("Too few arguments to call CAF.join"); - return; - } - auto i = xs.begin(); - auto grp = (*i).cast(); +// void py_context::py_join(const py::args &xs) { +// if (xs.size() < 1) { +// set_py_exception("Too few arguments to call CAF.join"); +// return; +// } +// auto i = xs.begin(); +// auto grp = (*i).cast(); + +// if (grp) { +// self_->join(grp); +// // spdlog::warn("{}", self_->joined_groups().size()); +// } +// } + +// void py_context::py_leave(const py::args &xs) { +// if (xs.size() < 1) { +// set_py_exception("Too few arguments to call CAF.leave"); +// return; +// } +// auto i = xs.begin(); +// auto grp = (*i).cast(); +// if (grp) +// self_->leave(grp); +// } + +void py_context::erase_func(py::function &callback_func) { + + // lock the mutex that will pause the main thread until we have finished + // what we're doing here + std::lock_guard l(thread_pauser_->mutex_); - if (grp) { - self_->join(grp); - // spdlog::warn("{}", self_->joined_groups().size()); - } -} + try { -void py_context::py_leave(const py::args &xs) { - if (xs.size() < 1) { - set_py_exception("Too few arguments to call CAF.leave"); - return; + + if (embedded_python_actor_) { + // send a message to main python thread to release the GIL and wait + // until it can unlock our mutex + self_->mail(xstudio::embedded_python::python_eval_atom_v, thread_pauser_ptr_) + .send(embedded_python_actor_); + } + + // acquire the GIL! + py::gil_scoped_acquire gil; + + // erase + callback_func = py::function(); + + } catch (std::exception &e) { + + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } - auto i = xs.begin(); - auto grp = (*i).cast(); - if (grp) - self_->leave(grp); } -py::tuple py_context::py_tuple_from_wrapped_message(const py::args &xs) { +void py_context::execute_event_callback( + const caf::message &msg, const xstudio::utility::Uuid &callback_id) { + + // lock the mutex that will pause the main thread until we have finished + // what we're doing here + std::lock_guard l(thread_pauser_->mutex_); + + auto p = message_callback_funcs_.find(callback_id); + if (p == message_callback_funcs_.end()) + return; + auto &callback_func = p->second; - auto i = xs.begin(); try { - if (i == xs.end()) { - throw std::runtime_error("Empty args passed to tuple_from_message"); + + + if (embedded_python_actor_) { + // send a message to main python thread to release the GIL and wait + // until it can unlock our mutex + self_->mail(xstudio::embedded_python::python_eval_atom_v, thread_pauser_ptr_) + .send(embedded_python_actor_); } - auto msg = (*i).cast(); - py::tuple result(msg.size()); + // acquire the GIL! + py::gil_scoped_acquire gil; + + // build our CAF message data into a python tuple + py::tuple result(msg.size()); for (size_t i = 0; i < msg.size(); ++i) { auto tid = msg.type_at(i); - if (auto meta_obj = detail::global_meta_object(tid)) { - auto kvp = portable_bindings().find(to_string(meta_obj->type_name)); + if (auto meta_obj = detail::global_meta_object(tid); + not meta_obj.type_name.empty()) { + auto kvp = portable_bindings().find(std::string(meta_obj.type_name)); if (kvp == portable_bindings().end()) { set_py_exception( R"(Unable to add element of type B ")", - to_string(meta_obj->type_name), + std::string(meta_obj.type_name), R"(" to message: type is unknown to CAF)"); - return py::tuple{}; + return; } auto obj = kvp->second->to_object(msg, i); PyTuple_SetItem(result.ptr(), static_cast(i), obj.release().ptr()); @@ -157,13 +245,16 @@ py::tuple py_context::py_tuple_from_wrapped_message(const py::args &xs) { " from message: ", "could not get portable name of ", tid); - return py::tuple{}; + return; } } - return result; + + // run the callback + callback_func(result); + } catch (std::exception &e) { - set_py_exception(e.what()); - return py::tuple{}; + + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } @@ -204,12 +295,12 @@ py::tuple py_context::tuple_from_message( for (size_t i = 0; i < msg.size(); ++i) { auto tid = msg.type_at(i); - if (auto meta_obj = detail::global_meta_object(tid)) { - auto kvp = portable_bindings().find(to_string(meta_obj->type_name)); + if (auto meta_obj = detail::global_meta_object(tid); not meta_obj.type_name.empty()) { + auto kvp = portable_bindings().find(std::string(meta_obj.type_name)); if (kvp == portable_bindings().end()) { set_py_exception( R"(Unable to add element of type C ")", - to_string(meta_obj->type_name), + std::string(meta_obj.type_name), R"(" to message: type is unknown to CAF)"); return py::tuple{}; } @@ -275,14 +366,63 @@ py_context::py_dequeue_with_timeout(xstudio::utility::absolute_receive_timeout t return tuple_from_message(ptr->mid, ptr->sender, std::move(ptr->content())); } +xstudio::utility::Uuid py_context::py_add_message_callback(const py::args &xs) { + + xstudio::utility::Uuid uuid = xstudio::utility::Uuid::generate(); + + if (xs.size() == 2) { + + auto i = xs.begin(); + auto remote_actor = (*i).cast(); + i++; + auto callback_func = (*i).cast(); + auto addr = caf::actor_cast(remote_actor); + + // spawn a listener actor to receive the event messages. THis will + // run the Python callback (after acquiring the GIL) + message_callback_handler_actors_[uuid] = + self_->spawn(uuid, remote_actor, this); + message_callback_funcs_[uuid] = callback_func; + + } else { + throw std::runtime_error("Set message callback expecting tuple of size 2 " + "(remote_event_group_actor, callack_func)."); + } + return uuid; +} + +void py_context::py_remove_message_callback(const xstudio::utility::Uuid &id) { + + auto p = message_callback_handler_actors_.find(id); + if (p != message_callback_handler_actors_.end()) { + self_->send_exit(p->second, caf::exit_reason::user_shutdown); + message_callback_handler_actors_.erase(p); + } else { + throw std::runtime_error( + "py_remove_message_callback - callback handler ID not recognised."); + } +} + void py_context::disconnect() { - host_ = ""; - port_ = 0; - remote_ = actor(); + + host_ = ""; + port_ = 0; + remote_ = actor(); + embedded_python_actor_ = caf::actor(); + for (auto p : message_callback_handler_actors_) { + self_->send_exit(p.second, caf::exit_reason::user_shutdown); + } + message_callback_handler_actors_.clear(); + message_callback_funcs_.clear(); } bool py_context::connect_remote(std::string host, uint16_t port) { disconnect(); + + // This will be null in xstudio_python case, but we expect that + embedded_python_actor_ = + system_.registry().template get(xstudio::embedded_python_registry); + auto actor = system_.middleman().remote_actor(host, port); if (actor) { remote_ = *actor; @@ -293,10 +433,15 @@ bool py_context::connect_remote(std::string host, uint16_t port) { } bool py_context::connect_local(caf::actor actor) { + disconnect(); + + // This will be null in xstudio_python case, but we expect that + embedded_python_actor_ = + system_.registry().template get(xstudio::embedded_python_registry); + remote_ = actor; return static_cast(actor); } - -} // namespace caf::python +} // namespace caf::python \ No newline at end of file diff --git a/src/python_module/src/py_context.hpp b/src/python_module/src/py_context.hpp index e7575eb07..bb7f3541d 100644 --- a/src/python_module/src/py_context.hpp +++ b/src/python_module/src/py_context.hpp @@ -17,6 +17,8 @@ CAF_PUSH_WARNINGS CAF_POP_WARNINGS #include "xstudio/utility/caf_helpers.hpp" +#include "xstudio/utility/uuid.hpp" +#include "xstudio/embedded_python/python_thread_locker.hpp" namespace caf::python { @@ -28,10 +30,7 @@ class py_context : public py_config { public: py_context(int argc = 0, char **argv = nullptr); - virtual ~py_context() { - // shutdown system - disconnect(); - } + virtual ~py_context(); std::optional py_build_message(const py::args &xs); void py_send(const py::args &xs); @@ -41,9 +40,15 @@ class py_context : public py_config { void py_send_exit(const py::args &xs); py::tuple tuple_from_message(const message_id mid, const strong_actor_ptr sender, const message &msg); - py::tuple py_tuple_from_wrapped_message(const py::args &xs); + void + execute_event_callback(const caf::message &msg, const xstudio::utility::Uuid &callback_id); + void erase_func(py::function &callback_func); + py::tuple py_dequeue(); py::tuple py_dequeue_with_timeout(xstudio::utility::absolute_receive_timeout timeout); + xstudio::utility::Uuid py_add_message_callback(const py::args &xs); + void py_remove_message_callback(const xstudio::utility::Uuid &id); + actor py_self() { return self_; } actor py_remote() { return remote_; } actor py_spawn(const py::args &xs); @@ -60,6 +65,10 @@ class py_context : public py_config { std::string host() { return host_; } uint16_t port() { return port_; } + void pause_main_python_thread(); + void unpause_main_python_thread(); + + public: std::string host_; uint16_t port_; @@ -81,10 +90,17 @@ class py_context : public py_config { // the python module is instanced. actor_system &system_; - scoped_actor self_; actor remote_; + actor embedded_python_actor_; py::function my_func; std::thread my_thread; + + xstudio::embedded_python::PythonThreadLocker *thread_pauser_; + xstudio::utility::BlindDataObjectPtr thread_pauser_ptr_; + + std::map message_callback_handler_actors_; + std::map message_callback_funcs_; + std::map plugin_registry_; }; } // namespace caf::python diff --git a/src/python_module/src/py_link.cpp b/src/python_module/src/py_link.cpp index f72068277..d14c9ba81 100644 --- a/src/python_module/src/py_link.cpp +++ b/src/python_module/src/py_link.cpp @@ -46,13 +46,18 @@ void py_link(py::module_ &m) { &caf::python::py_context::py_dequeue_with_timeout, "Receives the next message") .def( - "tuple_from_message", - &caf::python::py_context::py_tuple_from_wrapped_message, - "Convert an XStudioExtensions.CafMessage to tuple") + "add_message_callback", + &caf::python::py_context::py_add_message_callback, + "Add a python callback function, called every time the given Actor's event group " + "generates a message. ") + .def( + "remove_message_callback", + &caf::python::py_context::py_remove_message_callback, + "Remove a python callback function. ") .def("spawn", &caf::python::py_context::py_spawn, "Spawn actor") .def("remote_spawn", &caf::python::py_context::py_remote_spawn, "Spawn remote actor") - .def("join", &caf::python::py_context::py_join, "Join event group") - .def("leave", &caf::python::py_context::py_leave, "Leave event group") + // .def("join", &caf::python::py_context::py_join, "Join event group") + // .def("leave", &caf::python::py_context::py_leave, "Leave event group") .def("self", &caf::python::py_context::py_self, "Returns the global self handle") .def("remote", &caf::python::py_context::py_remote, "Returns the remote handle") .def("set_remote", &caf::python::py_context::py_set_remote, "Set the remote handle") diff --git a/src/python_module/src/py_messages.cpp b/src/python_module/src/py_messages.cpp index 0bce6319f..1292fbbe9 100644 --- a/src/python_module/src/py_messages.cpp +++ b/src/python_module/src/py_messages.cpp @@ -6,7 +6,6 @@ // #include // #include // #include -#include "xstudio/utility/helpers.hpp" #include "py_opaque.hpp" @@ -16,12 +15,14 @@ #include "xstudio/ui/mouse.hpp" #include "xstudio/utility/caf_helpers.hpp" #include "xstudio/utility/container.hpp" +#include "xstudio/utility/helpers.hpp" #include "xstudio/utility/media_reference.hpp" #include "xstudio/utility/remote_session_file.hpp" #include "xstudio/utility/serialise_headers.hpp" #include "xstudio/utility/timecode.hpp" #include "xstudio/utility/uuid.hpp" #include "xstudio/utility/frame_range.hpp" +#include "xstudio/utility/notification_handler.hpp" #include "py_config.hpp" @@ -40,31 +41,36 @@ extern void register_mediapointer_class(py::module &m, const std::string &name); extern void register_mediakey_class(py::module &m, const std::string &name); extern void register_jsonstore_class(py::module &m, const std::string &name); extern void register_FrameRate_class(py::module &m, const std::string &name); -extern void register_group_down_msg(py::module &m, const std::string &name); extern void register_URI_class(py::module &m, const std::string &name); extern void register_FrameRateDuration_class(py::module &m, const std::string &name); +extern void register_colour_triplet_class(py::module &m, const std::string &name); extern void register_uuid_actor_class(py::module &m, const std::string &name); extern void register_uuid_actor_vector_class(py::module &m, const std::string &name); -extern void register_uuidvec_class(py::module &m, const std::string &name); extern void register_item_class(py::module &m, const std::string &name); extern void register_frame_range_class(py::module &m, const std::string &name); extern void register_frame_list_class(py::module &m, const std::string &name); +extern void register_notification_class(py::module &m, const std::string &name); +extern void register_stringvec_class(py::module &m, const std::string &name); using namespace xstudio; void py_config::add_messages() { - add_message_type( - "group_down_msg", "caf::group_down_msg", ®ister_group_down_msg); add_message_type("URI", "caf::uri", ®ister_URI_class); add_message_type("MediaType", "xstudio::media::MediaType", nullptr); + add_message_type( + "LogLevel", "spdlog::level::level_enum", nullptr); + add_message_type( + "HUDElementPosition", "xstudio::plugin::HUDElementPosition", nullptr); add_message_type( "StreamDetail", "xstudio::media::StreamDetail", ®ister_streamdetail_class); add_message_type( "PluginDetail", "xstudio::plugin_manager::PluginDetail", ®ister_plugindetail_class); add_message_type>( "PluginDetailVec", "std::vector", nullptr); - add_message_type( - "CompareMode", "xstudio::playhead::CompareMode", nullptr); + add_message_type( + "AssemblyMode", "xstudio::playhead::AssemblyMode", nullptr); + add_message_type( + "AutoAlignMode", "xstudio::playhead::AutoAlignMode", nullptr); add_message_type("StatusType", "xstudio::global::StatusType", nullptr); add_message_type("LoopMode", "xstudio::playhead::LoopMode", nullptr); add_message_type( @@ -104,6 +110,15 @@ void py_config::add_messages() { add_message_type>>( "std::vector>", "std::vector>", nullptr); + add_message_type>>( + "std::pair>", + "std::pair>", + nullptr); + add_message_type("timebase::flicks", "timebase::flicks", nullptr); add_message_type>>( @@ -114,8 +129,6 @@ void py_config::add_messages() { add_message_type( "xstudio::utility::time_point", "xstudio::utility::time_point", nullptr); - add_message_type>( - "UuidVec", "std::vector", ®ister_uuidvec_class); add_message_type( "absolute_receive_timeout", "xstudio::utility::absolute_receive_timeout", @@ -128,6 +141,8 @@ void py_config::add_messages() { "FrameRateDuration", "xstudio::utility::FrameRateDuration", ®ister_FrameRateDuration_class); + add_message_type( + "ColourTriplet", "xstudio::utility::ColourTriplet", ®ister_colour_triplet_class); add_message_type( "JsonStore", "xstudio::utility::JsonStore", ®ister_jsonstore_class); add_message_type( @@ -162,6 +177,9 @@ void py_config::add_messages() { add_message_type( "Item", "xstudio::timeline::Item", ®ister_item_class); + add_message_type( + "ItemType", "xstudio::timeline::ItemType", nullptr); + add_message_type( "FrameRange", "xstudio::utility::FrameRange", ®ister_frame_range_class); @@ -169,5 +187,13 @@ void py_config::add_messages() { "std::pair", "std::pair", nullptr); + + add_message_type( + "Notification", "xstudio::utility::Notification", ®ister_notification_class); + + add_message_type>( + "std::vector", + "std::vector", + nullptr); } -} // namespace caf::python +} // namespace caf::python \ No newline at end of file diff --git a/src/python_module/src/py_opaque.hpp b/src/python_module/src/py_opaque.hpp index a2d55214e..7bfcd07b6 100644 --- a/src/python_module/src/py_opaque.hpp +++ b/src/python_module/src/py_opaque.hpp @@ -8,5 +8,6 @@ CAF_POP_WARNINGS #include "xstudio/utility/uuid.hpp" +PYBIND11_MAKE_OPAQUE(std::vector) PYBIND11_MAKE_OPAQUE(std::vector) PYBIND11_MAKE_OPAQUE(std::vector) diff --git a/src/python_module/src/py_playhead.cpp b/src/python_module/src/py_playhead.cpp index 2023cdcc2..509aaa18c 100644 --- a/src/python_module/src/py_playhead.cpp +++ b/src/python_module/src/py_playhead.cpp @@ -22,9 +22,16 @@ using namespace xstudio; namespace py = pybind11; void py_playhead(py::module_ &m) { - py::enum_(m, "CompareMode") - .value("CM_STRING", playhead::CompareMode::CM_STRING) - .value("CM_AB", playhead::CompareMode::CM_AB) + py::enum_(m, "AssemblyMode") + .value("AM_STRING", playhead::AssemblyMode::AM_STRING) + .value("AM_ONE", playhead::AssemblyMode::AM_ONE) + .value("AM_ALL", playhead::AssemblyMode::AM_ALL) + .export_values(); + + py::enum_(m, "AutoAlignMode") + .value("AAM_ALIGN_OFF", playhead::AutoAlignMode::AAM_ALIGN_OFF) + .value("AAM_ALIGN_FRAMES", playhead::AutoAlignMode::AAM_ALIGN_FRAMES) + .value("AAM_ALIGN_TRIM", playhead::AutoAlignMode::AAM_ALIGN_TRIM) .export_values(); py::enum_(m, "LoopMode") diff --git a/src/python_module/src/py_plugin.cpp b/src/python_module/src/py_plugin.cpp index 6d02551f5..015e29c49 100644 --- a/src/python_module/src/py_plugin.cpp +++ b/src/python_module/src/py_plugin.cpp @@ -35,4 +35,14 @@ void py_plugin(py::module_ &m) { .value("PF_UTILITY", plugin_manager::PluginFlags::PF_UTILITY) .value("PF_CONFORM", plugin_manager::PluginFlags::PF_CONFORM) .export_values(); + + py::enum_(m, "HUDElementPosition") + .value("BottomLeft", plugin::HUDElementPosition::BottomLeft) + .value("BottomCenter", plugin::HUDElementPosition::BottomCenter) + .value("BottomRight", plugin::HUDElementPosition::BottomRight) + .value("TopLeft", plugin::HUDElementPosition::TopLeft) + .value("TopCenter", plugin::HUDElementPosition::TopCenter) + .value("TopRight", plugin::HUDElementPosition::TopRight) + .value("FullScreen", plugin::HUDElementPosition::FullScreen) + .export_values(); } \ No newline at end of file diff --git a/src/python_module/src/py_register.cpp b/src/python_module/src/py_register.cpp index 69d279313..b5f6e4364 100644 --- a/src/python_module/src/py_register.cpp +++ b/src/python_module/src/py_register.cpp @@ -31,6 +31,7 @@ CAF_POP_WARNINGS #include "xstudio/utility/timecode.hpp" #include "xstudio/utility/uuid.hpp" #include "xstudio/utility/frame_range.hpp" +#include "xstudio/utility/notification_handler.hpp" namespace py = pybind11; @@ -38,10 +39,6 @@ namespace py = pybind11; using namespace xstudio; namespace caf::python { -void register_group_down_msg(py::module &m, const std::string &name) { - py::class_(m, name.c_str()).def(py::init<>()); -} - void register_playlistitem_class(py::module &m, const std::string &name) { // auto str_impl = [](const utility::PlaylistItem& x) { return to_string(x); }; py::class_(m, name.c_str()) @@ -62,18 +59,111 @@ void register_abs_class(py::module &m, const std::string &name) { } void register_uuid_class(py::module &m, const std::string &name) { - auto str_impl = [](const utility::Uuid &x) { return to_string(x); }; + auto str_impl = [](const utility::Uuid &x) { return to_string(x); }; + auto hash_impl = [](const utility::Uuid &x) { + std::hash h; + return h(x); + }; py::class_(m, name.c_str()) .def(py::init<>()) .def(py::init()) .def("generate", &utility::Uuid::generate) .def("generate_in_place", &utility::Uuid::generate_in_place) + .def("generate_from_name", &utility::Uuid::generate_in_place_from_name) .def("is_null", &utility::Uuid::is_null) .def(py::self == py::self) .def(py::self != py::self) + .def("__hash__", hash_impl) .def("__str__", str_impl); } +void register_notification_class(py::module &m, const std::string &name) { + py::class_(m, name.c_str()) + .def(py::init<>()) + .def("type", [](const utility::Notification &x) { return x.type(); }) + .def("uuid", [](const utility::Notification &x) { return x.uuid(); }) + .def("text", [](const utility::Notification &x) { return x.text(); }) + .def("progress", [](const utility::Notification &x) { return x.progress(); }) + .def( + "progress_maximum", + [](const utility::Notification &x) { return x.progress_maximum(); }) + .def( + "progress_minimum", + [](const utility::Notification &x) { return x.progress_minimum(); }) + .def( + "progress_percentage", + [](const utility::Notification &x) { return x.progress_percentage(); }) + .def( + "progress_text_percentage", + [](const utility::Notification &x) { return x.progress_text_percentage(); }) + .def( + "progress_text_range", + [](const utility::Notification &x) { return x.progress_text_range(); }) + + .def( + "set_type", + [](utility::Notification &x, const utility::NotificationType value) { + x.type(value); + }) + .def( + "set_uuid", + [](utility::Notification &x, const utility::Uuid &value) { x.uuid(value); }) + .def( + "set_text", + [](utility::Notification &x, const std::string &value) { x.text(value); }) + .def( + "set_progress", + [](utility::Notification &x, const float value) { x.progress(value); }) + .def( + "set_progress_minimum", + [](utility::Notification &x, const float value) { x.progress_maximum(value); }) + .def( + "set_progress_maximum", + [](utility::Notification &x, const float value) { x.progress_maximum(value); }) + .def( + "set_expires_in", + [](utility::Notification &x, const int seconds) { + x.expires_in(std::chrono::seconds(seconds)); + }) + + + .def( + "InfoNotification", + [](const std::string &text, const int seconds) { + return utility::Notification::InfoNotification( + text, std::chrono::seconds(seconds)); + }) + .def( + "WarnNotification", + [](const std::string &text, const int seconds) { + return utility::Notification::WarnNotification( + text, std::chrono::seconds(seconds)); + }) + .def( + "ProcessingNotification", + [](const std::string &text) { + return utility::Notification::ProcessingNotification(text); + }) + .def( + "ProgressPercentageNotification", + [](const std::string &text, const float progress = 0.0f, const int seconds = 600) { + return utility::Notification::ProgressPercentageNotification( + text, progress, std::chrono::seconds(seconds)); + }) + .def( + "ProgressRangeNotification", + [](const std::string &text, + const float progress = 0.0f, + const float progress_min = 0.0f, + const float progress_max = 100.0f, + const int seconds = 600) { + return utility::Notification::ProgressRangeNotification( + text, progress, progress_min, progress_max, std::chrono::seconds(seconds)); + }) + + ; +} + void register_plugindetail_class(py::module &m, const std::string &name) { py::class_(m, name.c_str()) .def(py::init<>()) @@ -194,13 +284,17 @@ void register_mediareference_class(py::module &m, const std::string &name) { .def("set_rate", &utility::MediaReference::set_rate) .def( "uri", - py::overload_cast<>(&utility::MediaReference::uri, py::const_), - "URI of mediareference") + py::overload_cast( + &utility::MediaReference::uri, py::const_), + "URI of mediareference", + py::arg("fpf") = utility::MediaReference::FramePadFormat::FPF_XSTUDIO) .def("uri_from_frame", &utility::MediaReference::uri_from_frame) .def("timecode", &utility::MediaReference::timecode) .def("offset", &utility::MediaReference::offset) .def("set_offset", &utility::MediaReference::set_offset) + .def("start_frame_offset", &utility::MediaReference::start_frame_offset) + .def("set_start_frame_offset", &utility::MediaReference::set_start_frame_offset) .def("frame", [](const utility::MediaReference &x, int i) { auto f = x.frame(i); if (f) @@ -220,6 +314,7 @@ void register_bookmark_detail_class(py::module &m, const std::string &name) { .def_readwrite("visible", &bookmark::BookmarkDetail::visible_) .def_readwrite("start", &bookmark::BookmarkDetail::start_) .def_readwrite("duration", &bookmark::BookmarkDetail::duration_) + //.def_readwrite("user_data", &bookmark::BookmarkDetail::user_data_) .def_readwrite("author", &bookmark::BookmarkDetail::author_) .def_readwrite("owner", &bookmark::BookmarkDetail::owner_) .def_readwrite("note", &bookmark::BookmarkDetail::note_) @@ -239,7 +334,7 @@ void register_mediakey_class(py::module &m, const std::string &name) { } void register_jsonstore_class(py::module &m, const std::string &name) { - auto str_impl = [](const utility::JsonStore &x) { return to_string(x); }; + auto str_impl = [](const utility::JsonStore &x) -> std::string { return x.dump(); }; auto get_preferences_impl = [](const utility::JsonStore &x, const std::set &context = std::set()) { @@ -274,7 +369,8 @@ void register_jsonstore_class(py::module &m, const std::string &name) { .def("remove", &utility::JsonStore::remove) .def("empty", &utility::JsonStore::empty) .def("get_preferences", get_preferences_impl) - .def("get_values", get_preference_values_impl); + .def("get_values", get_preference_values_impl) + .def("parse_string", &utility::JsonStore::parse_string); } void register_FrameRate_class(py::module &m, const std::string &name) { @@ -307,7 +403,7 @@ void register_FrameRateDuration_class(py::module &m, const std::string &name) { auto str_impl = [](const utility::FrameRateDuration &x) { return to_string(x); }; py::class_(m, name.c_str()) .def(py::init<>()) - .def(py::init()) + .def(py::init()) .def( "frames", &utility::FrameRateDuration::frames, @@ -321,15 +417,31 @@ void register_FrameRateDuration_class(py::module &m, const std::string &name) { .def("__str__", str_impl); } +void register_colour_triplet_class(py::module &m, const std::string &name) { + auto str_impl = [](const utility::ColourTriplet &x) { return to_string(x); }; + py::class_(m, name.c_str()) + .def(py::init<>()) + .def(py::init()) + .def_property("red", &utility::ColourTriplet::red, &utility::ColourTriplet::setRed) + .def_property( + "green", &utility::ColourTriplet::green, &utility::ColourTriplet::setGreen) + .def_property("blue", &utility::ColourTriplet::blue, &utility::ColourTriplet::setBlue) + .def("__str__", str_impl) + .def("__repr__", str_impl); +} + void register_actor_class(py::module &m, const std::string &name) { auto str_impl = [](const caf::actor &x) { return to_string(x); }; - py::class_(m, name.c_str()).def(py::init()).def("__str__", str_impl); + py::class_(m, name.c_str()) + .def(py::init()) + .def("__bool__", [](const caf::actor &x) { return bool(x); }) + .def("__str__", str_impl); } -void register_group_class(py::module &m, const std::string &name) { - auto str_impl = [](const caf::group &x) { return to_string(x); }; - py::class_(m, name.c_str()).def(py::init()).def("__str__", str_impl); -} +// void register_group_class(py::module &m, const std::string &name) { +// auto str_impl = [](const caf::group &x) { return to_string(x); }; +// py::class_(m, name.c_str()).def(py::init()).def("__str__", str_impl); +// } void register_addr_class(py::module &m, const std::string &name) { auto str_impl = [](const caf::actor_addr &x) { return to_string(x); }; @@ -375,28 +487,6 @@ void register_uuid_actor_vector_class(py::module &m, const std::string &name) { py::keep_alive<0, 1>()); } -void register_uuidvec_class(py::module &m, const std::string &name) { - // auto str_impl = [](const std::vector& x) { return - // to_string(x); - // }; - py::class_>(m, name.c_str()) - .def(py::init()) - // .def("__str__", str_impl) - .def("clear", &std::vector::clear) - .def( - "push_back", - py::overload_cast( - &std::vector::push_back)) - .def("pop_back", &std::vector::pop_back) - .def("__len__", &std::vector::size) - .def( - "__iter__", - [](std::vector &v) { - return py::make_iterator(v.begin(), v.end()); - }, - py::keep_alive<0, 1>()); -} - void register_item_class(py::module &m, const std::string &name) { // auto str_impl = [](const timeline::Item &x) { return to_string(x); }; py::class_(m, name.c_str()) @@ -408,11 +498,11 @@ void register_item_class(py::module &m, const std::string &name) { .def("uuid_actor", &timeline::Item::uuid_actor) .def("enabled", &timeline::Item::enabled) .def("transparent", &timeline::Item::transparent) - .def("rate", &timeline::Item::rate) .def("name", &timeline::Item::name) .def("flag", &timeline::Item::flag) .def("prop", &timeline::Item::prop) + .def("locked", &timeline::Item::locked) .def("active_range", &timeline::Item::active_range) .def("active_duration", &timeline::Item::active_duration) @@ -425,13 +515,25 @@ void register_item_class(py::module &m, const std::string &name) { .def("trimmed_range", &timeline::Item::trimmed_range) .def("trimmed_duration", &timeline::Item::trimmed_duration) .def("trimmed_start", &timeline::Item::trimmed_start) - + .def( + "frame_at_index", + [](timeline::Item &v, const int frame) { return v.frame_at_index(frame); }) + .def("range_at_index", &timeline::Item::range_at_index) + .def( + "item_at_frame", + [](timeline::Item &v, const int frame) { + auto tmp = v.item_at_frame(frame); + if (tmp) + return std::optional(*(tmp->first)); + return std::optional(); + }) .def( "resolve_time", &timeline::Item::resolve_time, - py::arg("time") = utility::FrameRate(), - py::arg("media_type") = media::MediaType::MT_IMAGE, - py::arg("focus") = utility::UuidSet()) + py::arg("time") = utility::FrameRate(), + py::arg("media_type") = media::MediaType::MT_IMAGE, + py::arg("focus") = utility::UuidSet(), + py::arg("must_have_focus") = false) .def("children", py::overload_cast<>(&timeline::Item::children), "Get children") .def("__len__", [](timeline::Item &v) { return v.size(); }); diff --git a/src/python_module/src/py_remote_session_file.cpp b/src/python_module/src/py_remote_session_file.cpp index 3b8c05b22..21bf608b0 100644 --- a/src/python_module/src/py_remote_session_file.cpp +++ b/src/python_module/src/py_remote_session_file.cpp @@ -31,13 +31,11 @@ void py_remote_session_file(py::module_ &m) { .def("filepath", &utility::RemoteSessionFile::filepath) .def("host", &utility::RemoteSessionFile::host) .def("port", &utility::RemoteSessionFile::port) - .def("pid", &utility::RemoteSessionFile::pid) - .def("sync", &utility::RemoteSessionFile::sync); + .def("pid", &utility::RemoteSessionFile::pid); py::class_(m, "RemoteSessionManager") .def(py::init()) .def("first_api", &utility::RemoteSessionManager::first_api) - .def("first_sync", &utility::RemoteSessionManager::first_sync) .def("find", &utility::RemoteSessionManager::find) .def("__len__", &utility::RemoteSessionManager::size) .def("empty", &utility::RemoteSessionManager::empty); diff --git a/src/python_module/src/py_types.cpp b/src/python_module/src/py_types.cpp index d44ee8dec..f239be3b2 100644 --- a/src/python_module/src/py_types.cpp +++ b/src/python_module/src/py_types.cpp @@ -10,12 +10,13 @@ namespace caf::python { void register_addr_class(py::module &m, const std::string &name); -void register_group_class(py::module &m, const std::string &name); +// void register_group_class(py::module &m, const std::string &name); void register_actor_class(py::module &m, const std::string &name); void py_config::add_types() { // allow CAF to convert native Python types to C++ types - add_py("int"); + // add_py("int"); + add_int_py(); add_py("bool"); add_py("float"); add_py("double"); @@ -23,7 +24,7 @@ void py_config::add_types() { // create Python bindings for builtin CAF types add_cpp("actor", "caf::actor", register_actor_class); - add_cpp("group", "caf::actor", register_group_class); + // add_cpp("group", "caf::actor", register_group_class); add_cpp("addr", "caf::actor_addr", register_addr_class); add_cpp("message", "caf::message"); add_cpp("error", "caf::error"); @@ -35,7 +36,11 @@ void py_config::add_types() { add_cpp("double", "double", nullptr); add_cpp("int32_t", "int32_t", nullptr); add_cpp("uint64_t", "uint64_t", nullptr); + add_cpp("int64_t", "int64_t", nullptr); + add_cpp("int", "int", nullptr); add_cpp("str", "std::string", nullptr); - add_cpp>("strvec", "std::vector", nullptr); + add_cpp>("VectorString", "std::vector", nullptr); + add_cpp>( + "VectorUuid", "std::vector", nullptr); } } // namespace caf::python diff --git a/src/python_module/src/py_utility.cpp b/src/python_module/src/py_utility.cpp index d5f80870a..094d0a6cc 100644 --- a/src/python_module/src/py_utility.cpp +++ b/src/python_module/src/py_utility.cpp @@ -27,4 +27,13 @@ void py_utility(py::module_ &m) { .value("TSM_REMAPPED", utility::TimeSourceMode::REMAPPED) .value("TSM_DYNAMIC", utility::TimeSourceMode::DYNAMIC) .export_values(); -} \ No newline at end of file + + py::enum_(m, "NotificationType") + .value("NT_UNKNOWN", utility::NotificationType::NT_UNKNOWN) + .value("NT_INFO", utility::NotificationType::NT_INFO) + .value("NT_WARN", utility::NotificationType::NT_WARN) + .value("NT_PROCESSING", utility::NotificationType::NT_PROCESSING) + .value("NT_PROGRESS_RANGE", utility::NotificationType::NT_PROGRESS_RANGE) + .value("NT_PROGRESS_PERCENTAGE", utility::NotificationType::NT_PROGRESS_PERCENTAGE) + .export_values(); +} diff --git a/src/python_module/src/py_xstudio.cpp b/src/python_module/src/py_xstudio.cpp index 21abba7dd..bb8b35008 100644 --- a/src/python_module/src/py_xstudio.cpp +++ b/src/python_module/src/py_xstudio.cpp @@ -1,7 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -#ifdef __GNUC__ // Check if GCC compiler is being used #pragma GCC diagnostic ignored "-Wattributes" -#endif #include "py_opaque.hpp" @@ -28,6 +26,19 @@ CAF_POP_WARNINGS #include "xstudio/utility/serialise_headers.hpp" #include "xstudio/media_reader/pixel_info.hpp" +#ifdef __apple__ +namespace caf { +namespace detail { + + template <> struct int_types_by_size<16> { + using unsigned_type = __uint128_t; + using signed_type = __int128; + }; + +} // namespace detail +} // namespace caf +#endif + using namespace xstudio; namespace py = pybind11; @@ -92,6 +103,22 @@ PYBIND11_MODULE(__pybind_xstudio, m) { .value("ST_BUSY", global::StatusType::ST_BUSY) .export_values(); + py::enum_(m, "LogLevel") + .value("SPDLOG_LEVEL_TRACE", spdlog::level::level_enum::trace) + .value("SPDLOG_LEVEL_DEBUG", spdlog::level::level_enum::debug) + .value("SPDLOG_LEVEL_INFO", spdlog::level::level_enum::info) + .value("SPDLOG_LEVEL_WARN", spdlog::level::level_enum::warn) + .value("SPDLOG_LEVEL_ERROR", spdlog::level::level_enum::err) + .value("SPDLOG_LEVEL_CRITICAL", spdlog::level::level_enum::critical) + .value("SPDLOG_LEVEL_OFF", spdlog::level::level_enum::off) + .export_values(); + + py::enum_(m, "FramePadFormat") + .value("FPF_XSTUDIO", utility::MediaReference::FramePadFormat::FPF_XSTUDIO) + .value("FPF_NUKE", utility::MediaReference::FramePadFormat::FPF_NUKE) + .value("FPF_SHAKE", utility::MediaReference::FramePadFormat::FPF_SHAKE) + .export_values(); + py::enum_(m, "AttributeRole") .value("Type", module::Attribute::Role::Type) .value("Enabled", module::Attribute::Role::Enabled) @@ -115,7 +142,7 @@ PYBIND11_MODULE(__pybind_xstudio, m) { .value("DefaultValue", module::Attribute::Role::DefaultValue) .value("AbbrValue", module::Attribute::Role::AbbrValue) .value("UuidRole", module::Attribute::Role::UuidRole) - .value("Groups", module::Attribute::Role::Groups) + .value("UIDataModels", module::Attribute::Role::UIDataModels) .value("MenuPaths", module::Attribute::Role::MenuPaths) .value("ToolbarPosition", module::Attribute::Role::ToolbarPosition) .value("OverrideValue", module::Attribute::Role::OverrideValue) @@ -129,8 +156,18 @@ PYBIND11_MODULE(__pybind_xstudio, m) { .value("TextContainerBox", module::Attribute::Role::TextContainerBox) .value("Colour", module::Attribute::Role::Colour) .value("HotkeyUuid", module::Attribute::Role::HotkeyUuid) + .value("IconPath", module::Attribute::Role::IconPath) + .value("CallbackData", module::Attribute::Role::CallbackData) .export_values(); + // set XSTUDIO_LOCAL_PLUGIN_PATH so we can load python plugins that are + // provided as part of the xstudio install/distribution + m.add_object( + "XSTUDIO_LOCAL_PLUGIN_PATH", py::cast(utility::xstudio_resources_dir("plugin-python"))); + + py::bind_vector>(m, "VectorString"); + py::bind_vector>(m, "VectorUuid"); + py_remote_session_file(m); py_playhead(m); py_plugin(m); diff --git a/src/python_module/test/CMakeLists.txt b/src/python_module/test/CMakeLists.txt index a73825679..66caa1970 100644 --- a/src/python_module/test/CMakeLists.txt +++ b/src/python_module/test/CMakeLists.txt @@ -1,7 +1,7 @@ include(CTest) SET(LINK_DEPS - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/scanner/src/CMakeLists.txt b/src/scanner/src/CMakeLists.txt index 083e5d55c..8b69b7cf6 100644 --- a/src/scanner/src/CMakeLists.txt +++ b/src/scanner/src/CMakeLists.txt @@ -4,8 +4,8 @@ find_package(OpenSSL) SET(LINK_DEPS xstudio::utility xstudio::json_store - caf::core + CAF::core OpenSSL::SSL ) -create_component(scanner 0.1.0 "${LINK_DEPS}") +create_component(scanner ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/scanner/src/scanner_actor.cpp b/src/scanner/src/scanner_actor.cpp index e27b864f4..73d03bcec 100644 --- a/src/scanner/src/scanner_actor.cpp +++ b/src/scanner/src/scanner_actor.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include #include #include @@ -53,7 +54,7 @@ media::MediaStatus check_media_status(const MediaReference &mr) { } } - } catch ([[maybe_unused]] std::exception &e) { + } catch (std::exception &e) { ms = media::MediaStatus::MS_UNREADABLE; } @@ -82,10 +83,21 @@ std::string get_checksum(const std::string &path) { try { myfile.exceptions(std::ifstream::failbit | std::ifstream::badbit); myfile.open(path, std::ios::in | std::ios::binary); - myfile.read((char *)buf.data(), 1024); - myfile.seekg(-1024, std::ios::end); - myfile.read((char *)buf.data() + 1024, 1024); + auto short_file = false; + + try { + myfile.read((char *)buf.data(), 1024); + } catch (...) { + // shortfile.. + short_file = true; + } + if (not short_file) { + myfile.seekg(-1024, std::ios::end); + myfile.read((char *)buf.data() + 1024, 1024); + } + myfile.close(); + } catch (const std::exception &err) { spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, path, err.what()); return std::string(); @@ -223,15 +235,16 @@ ScannerActor::ScannerActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) behavior_.assign( [=](media::media_status_atom, const MediaReference &mr, caf::actor dest) { - anon_send(dest, media::media_status_atom_v, check_media_status(mr)); + anon_mail(media::media_status_atom_v, check_media_status(mr)).send(dest); }, [=](media::checksum_atom atom, const caf::actor &media_source) { - request(media_source, infinite, media::media_reference_atom_v) + mail(media::media_reference_atom_v) + .request(media_source, infinite) .then( [=](const MediaReference &result) mutable { - anon_send( - caf::actor_cast(this), atom, media_source, result); + anon_mail(atom, media_source, result) + .send(caf::actor_cast(this)); }, [=](const caf::error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -241,36 +254,36 @@ ScannerActor::ScannerActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) [=](media::checksum_atom atom, const caf::actor &media_source, const MediaReference &mr) { - request(helper, infinite, atom, mr) + mail(atom, mr) + .request(helper, infinite) .then( [=](const std::pair &result) mutable { - anon_send(media_source, atom, result); + anon_mail(atom, result).send(media_source); }, [=](const caf::error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); }, - [=](media::rescan_atom atom, const MediaReference &mr) { delegate(helper, atom, mr); }, + [=](media::rescan_atom atom, const MediaReference &mr) { + return mail(atom, mr).delegate(helper); + }, [=](media::checksum_atom atom, const MediaReference &mr) { - delegate(helper, atom, mr); + return mail(atom, mr).delegate(helper); }, [=](media::relink_atom atom, const std::pair &pin, - const caf::uri &path) { delegate(helper, atom, pin, path); }, + const caf::uri &path) { return mail(atom, pin, path).delegate(helper); }, [=](media::relink_atom atom, const caf::actor &media_source, const caf::uri &path) { - request(media_source, infinite, media::checksum_atom_v) + mail(media::checksum_atom_v) + .request(media_source, infinite) .then( [=](const std::pair &result) mutable { - anon_send( - caf::actor_cast(this), - atom, - media_source, - result, - path); + anon_mail(atom, media_source, result, path) + .send(caf::actor_cast(this)); }, [=](const caf::error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -281,12 +294,14 @@ ScannerActor::ScannerActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) const caf::actor &media_source, const std::pair &pin, const caf::uri &path) { - request(helper, infinite, atom, pin, path) + mail(atom, pin, path) + .request(helper, infinite) .then( [=](const caf::uri &result) mutable { if (not result.empty()) { // get mr, and then over write.. - request(media_source, infinite, media::media_reference_atom_v) + mail(media::media_reference_atom_v) + .request(media_source, infinite) .then( [=](MediaReference mr) mutable { if (not mr.container()) { @@ -298,8 +313,8 @@ ScannerActor::ScannerActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) mr.set_uri(tmp[0].first); } else mr.set_uri(result); - anon_send( - media_source, media::media_reference_atom_v, mr); + anon_mail(media::media_reference_atom_v, mr) + .send(media_source); }, [=](const caf::error &err) { spdlog::warn( diff --git a/src/scanner/test/CMakeLists.txt b/src/scanner/test/CMakeLists.txt index 603cc6c4c..7d3368948 100644 --- a/src/scanner/test/CMakeLists.txt +++ b/src/scanner/test/CMakeLists.txt @@ -2,6 +2,7 @@ include(CTest) SET(LINK_DEPS xstudio::scanner + xstudio::media ) create_tests("${LINK_DEPS}") diff --git a/src/session/src/CMakeLists.txt b/src/session/src/CMakeLists.txt index a4303039c..4bacb99d1 100644 --- a/src/session/src/CMakeLists.txt +++ b/src/session/src/CMakeLists.txt @@ -2,8 +2,7 @@ SET(LINK_DEPS xstudio::playlist xstudio::utility xstudio::bookmark - xstudio::tag - caf::core + CAF::core ) -create_component(session 0.1.0 "${LINK_DEPS}") +create_component(session ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/session/src/session.cpp b/src/session/src/session.cpp index 0f4c59256..bbf66d78a 100644 --- a/src/session/src/session.cpp +++ b/src/session/src/session.cpp @@ -18,10 +18,12 @@ Session::Session(const JsonStore &jsn, caf::uri filepath) : Container(static_cast(jsn["container"])), filepath_(std::move(filepath)) { - session_file_mtime_ = get_file_mtime(filepath_); - media_rate_ = jsn["media_rate"]; - playhead_rate_ = jsn["playhead_rate"]; - playlists_ = PlaylistTree(static_cast(jsn["playlists"])); + session_file_mtime_ = get_file_mtime(filepath_); + media_rate_ = jsn["media_rate"]; + playhead_rate_ = jsn["playhead_rate"]; + playlists_ = PlaylistTree(static_cast(jsn["playlists"])); + current_playlist_uuid_ = jsn.value("current_playlist_uuid", utility::Uuid()); + viewed_playlist_uuid_ = jsn.value("viewed_playlist_uuid", utility::Uuid()); } JsonStore Session::serialise() const { @@ -32,7 +34,9 @@ JsonStore Session::serialise() const { jsn["playhead_rate"] = playhead_rate_; // identify actors that are media.. - jsn["playlists"] = playlists_.serialise(); + jsn["playlists"] = playlists_.serialise(); + jsn["current_playlist_uuid"] = current_playlist_uuid_; + jsn["viewed_playlist_uuid"] = viewed_playlist_uuid_; return jsn; } diff --git a/src/session/src/session_actor.cpp b/src/session/src/session_actor.cpp index fc27d7101..e63800c6a 100644 --- a/src/session/src/session_actor.cpp +++ b/src/session/src/session_actor.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -13,9 +14,7 @@ #include "xstudio/json_store/json_store_actor.hpp" #include "xstudio/bookmark/bookmarks_actor.hpp" #include "xstudio/playlist/playlist_actor.hpp" -#include "xstudio/tag/tag_actor.hpp" #include "xstudio/session/session_actor.hpp" -#include "xstudio/tag/tag_actor.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/thumbnail/thumbnail.hpp" @@ -26,10 +25,113 @@ using namespace xstudio::global_store; using namespace xstudio::session; using namespace nlohmann; using namespace caf; +using namespace std::chrono_literals; + namespace fs = std::filesystem; namespace { +class SessionIOActor : public caf::event_based_actor { + public: + SessionIOActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) {} + const char *name() const override { return NAME.c_str(); } + + caf::message_handler message_handler() { + return caf::message_handler{ + [=](save_atom, + const JsonStore &js, + const caf::uri &path, + const bool update_path, + const size_t hash) -> caf::result { + size_t new_hash = 0; + + try { + auto data = js.dump(2); + + auto resolve_link = false; + new_hash = std::hash{}(data); + + // no change in hash, so skip save (autosave) + if (new_hash == hash) { + return new_hash; + } + + // fix something ? + auto ppath = utility::posix_path_to_uri(utility::uri_to_posix_path(path)); + + // try and save, we are already looking at this file + if (update_path) { + // same path as session, are we allowed ? + resolve_link = true; + } + + auto save_path = uri_to_posix_path(ppath); + if (resolve_link && fs::exists(save_path) && fs::is_symlink(save_path)) +#ifdef _WIN32 + save_path = fs::canonical(save_path).string(); +#else + save_path = fs::canonical(save_path); +#endif + + + // compress data. + if (to_lower(path_to_string(fs::path(save_path).extension())) == ".xsz") { + zstr::ofstream o(save_path + ".tmp"); + try { + o.exceptions(std::ifstream::failbit | std::ifstream::badbit); + // if(not o.is_open()) + // throw std::runtime_error(); + o << std::setw(4) << data << std::endl; + o.close(); + } catch (const std::exception &) { + // remove failed file + if (o.is_open()) { + o.close(); + fs::remove(save_path + ".tmp"); + } + throw std::runtime_error("Failed to open file"); + } + } else { + // this maybe a symlink in which case we should resolve it. + std::ofstream o(save_path + ".tmp"); + try { + o.exceptions(std::ifstream::failbit | std::ifstream::badbit); + // if(not o.is_open()) + // throw std::runtime_error(); + o << std::setw(4) << data << std::endl; + o.close(); + } catch (const std::exception &) { + // remove failed file + if (o.is_open()) { + o.close(); + fs::remove(save_path + ".tmp"); + } + throw std::runtime_error("Failed to open file"); + } + } + + // rename tmp to final name + fs::rename(save_path + ".tmp", save_path); + + const std::string t = utility::to_string(utility::sysclock::now()); + spdlog::info("Session saved as {} at {}", save_path, t); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + return make_error(xstudio_error::error, err.what()); + } + + return new_hash; + }}; + } + + caf::behavior make_behavior() override { return message_handler(); } + + private: + inline static const std::string NAME = "SessionIOActor"; +}; + + // offload media actor copy as it blocks the session actor.. class MediaCopyActor : public caf::event_based_actor { @@ -58,7 +160,7 @@ class MediaCopyActor : public caf::event_based_actor { const utility::Uuid &dst, const utility::Uuid &src, const utility::UuidVector &media, - const bool remove_source = false, + bool remove_source = false, const bool force_duplicate = false, const utility::Uuid &uuid_before = utility::Uuid(), const bool into = false); @@ -98,7 +200,7 @@ void MediaCopyActor::copy_media_to( const utility::Uuid &dst, const utility::Uuid &src, const utility::UuidVector &media, - const bool remove_source, + bool remove_source, const bool force_duplicate, const utility::Uuid &uuid_before, const bool) { @@ -108,17 +210,20 @@ void MediaCopyActor::copy_media_to( // now find src.. caf::scoped_actor sys(system()); caf::actor src_actor; + caf::actor src_playlist; if (not src.is_null()) { - if (playlists_.count(src)) - src_actor = playlists_[src]; - else { + if (playlists_.count(src)) { + src_actor = playlists_[src]; + src_playlist = playlists_[src]; + } else { for (const auto &i : playlists_) { try { auto result = request_receive>( *sys, i.second, playlist::get_container_atom_v, true); for (const auto &ii : result) { if (ii.uuid() == src) { - src_actor = ii.actor(); + src_actor = ii.actor(); + src_playlist = i.second; break; } } @@ -131,6 +236,17 @@ void MediaCopyActor::copy_media_to( } } + if (dst == src && !force_duplicate) { + // We're being asked to move media between same src and dest with + // no copy ... + mail(playlist::move_media_atom_v, media, uuid_before) + .request(src_actor, infinite) + .then( + [=](bool) mutable { rp.deliver(media); }, + [=](caf::error &err) mutable { rp.deliver(err); }); + return; + } + // find target caf::actor target; caf::actor target_playlist; @@ -160,6 +276,12 @@ void MediaCopyActor::copy_media_to( } } + if (src_playlist == target_playlist) { + // copying between sibling subsets/timelines OR from parent playlist + // to child subset/timeline + remove_source = false; + } + // we should have target.. if (not target) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, "Invalid destination uuid"); @@ -225,12 +347,11 @@ void MediaCopyActor::copy_media_to( // add to subgroup if (target_playlist != target) { - spdlog::warn("add dup to sub"); - anon_send( - target, + anon_mail( playlist::add_media_atom_v, new_media.second.uuid(), - uuid_before); + uuid_before) + .send(target); } // remove source @@ -241,16 +362,17 @@ void MediaCopyActor::copy_media_to( } else { // just adding to subgroup.. // we don't delete as this from the parent - anon_send( - target, playlist::add_media_atom_v, i.uuid(), uuid_before); + anon_mail(playlist::add_media_atom_v, i.uuid(), uuid_before) + .send(target); result.push_back(i.uuid()); // but we do from the sibling if (remove_source and src_actor) { - anon_send( - src_actor, playlist::remove_media_atom_v, i.uuid()); + anon_mail(playlist::remove_media_atom_v, i.uuid()) + .send(src_actor); } } } + // all done just do removal.. if (not removal_list.empty()) { fan_out_request( @@ -291,7 +413,8 @@ class LoadUrisActor : public caf::event_based_actor { caf::behavior behavior_; caf::actor session_; std::vector uris_; - bool load_uris(const bool single_playlist = false); + UuidActorVector + load_uris(const bool single_playlist = false, const bool make_subsets = false); }; LoadUrisActor::LoadUrisActor( @@ -300,29 +423,34 @@ LoadUrisActor::LoadUrisActor( behavior_.assign( [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](load_uris_atom, const bool single_playlist) -> result { + [=](load_uris_atom, const bool single_playlist) -> result { return load_uris(single_playlist); - }); + }, + [=](load_uris_atom, const bool single_playlist, const bool make_subsets) + -> result { return load_uris(single_playlist, make_subsets); }); } -bool LoadUrisActor::load_uris(const bool single_playlist) { +UuidActorVector LoadUrisActor::load_uris(const bool single_playlist, const bool make_subsets) { bool has_files = false; caf::actor playlist; if (single_playlist) { - request(session_, infinite, add_playlist_atom_v, std::string("Untitled Playlist")) + mail(add_playlist_atom_v, std::string("")) + .request(session_, infinite) .then( [=](UuidUuidActor playlist) { for (const auto &i : uris_) { fs::path p(uri_to_posix_path(i)); - if (not is_session(p.string())) - anon_send( - playlist.second.actor(), - playlist::add_media_atom_v, - i, - true, - Uuid()); + if (not is_session(p.string())) { + if (make_subsets) { + anon_mail(playlist::add_media_with_subsets_atom_v, i, Uuid()) + .send(playlist.second.actor()); + } else { + anon_mail(playlist::add_media_atom_v, i, true, Uuid()) + .send(playlist.second.actor()); + } + } } }, [=](error &err) mutable { @@ -335,20 +463,21 @@ bool LoadUrisActor::load_uris(const bool single_playlist) { fs::path p(uri_to_posix_path(i)); if (fs::is_directory(p)) { #ifdef _WIN32 - request( - session_, infinite, add_playlist_atom_v, std::string(p.filename().string())) + mail(add_playlist_atom_v, std::string(p.filename().string())) + .request(session_, infinite) #else - request(session_, infinite, add_playlist_atom_v, std::string(p.filename())) + mail(add_playlist_atom_v, std::string(p.filename())) + .request(session_, infinite) #endif - .then( [=](UuidUuidActor playlist) { - anon_send( - playlist.second.actor(), - playlist::add_media_atom_v, - i, - true, - Uuid()); + if (make_subsets) { + anon_mail(playlist::add_media_with_subsets_atom_v, i, Uuid()) + .send(playlist.second.actor()); + } else { + anon_mail(playlist::add_media_atom_v, i, true, Uuid()) + .send(playlist.second.actor()); + } }, [=](error &err) mutable { spdlog::error("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -356,25 +485,22 @@ bool LoadUrisActor::load_uris(const bool single_playlist) { } else { if (is_session(p.string())) - anon_send(session_, merge_session_atom_v, i); + anon_mail(merge_session_atom_v, i).send(session_); else has_files = true; } } if (has_files) { - request(session_, infinite, add_playlist_atom_v, std::string("Untitled Playlist")) + mail(add_playlist_atom_v, std::string("")) + .request(session_, infinite) .then( [=](UuidUuidActor playlist) { for (const auto &i : uris_) { fs::path p(uri_to_posix_path(i)); if (!fs::is_directory(p) and not is_session(p.string())) - anon_send( - playlist.second.actor(), - playlist::add_media_atom_v, - i, - true, - Uuid()); + anon_mail(playlist::add_media_atom_v, i, true, Uuid()) + .send(playlist.second.actor()); } }, [=](error &err) mutable { @@ -382,7 +508,7 @@ bool LoadUrisActor::load_uris(const bool single_playlist) { }); } } - return true; + return UuidActorVector(); } } // namespace @@ -416,14 +542,6 @@ SessionActor::SessionActor( join_event_group(this, bookmarks_); link_to(bookmarks_); - if (not jsn.count("tags") or jsn["tags"].is_null()) { - tags_ = spawn(); - } else { - tags_ = spawn(static_cast(jsn["tags"])); - } - join_event_group(this, tags_); - link_to(tags_); - for (const auto &[key, value] : jsn["actors"].items()) { if (value["base"]["container"]["type"] == "Playlist") { try { @@ -440,16 +558,34 @@ SessionActor::SessionActor( init(); check_media_hook_plugin_version(jsn, path); + + if (!base_.current_playlist_uuid().is_null()) { + anon_mail(active_media_container_atom_v, base_.current_playlist_uuid()).send(this); + } + if (!base_.viewed_playlist_uuid().is_null()) { + anon_mail(viewport_active_media_container_atom_v, base_.viewed_playlist_uuid()) + .send(this); + } } SessionActor::SessionActor(caf::actor_config &cfg, const std::string &name) : caf::event_based_actor(cfg), base_(name) { try { + auto prefs = GlobalStoreHelper(system()); + JsonStore j; + join_broadcast(this, prefs.get_group(j)); base_.set_playhead_rate( - FrameRate(1.0 / prefs.value("/core/session/play_rate"))); - base_.set_media_rate(FrameRate(1.0 / prefs.value("/core/session/media_rate"))); + FrameRate(preference_value(j, "/core/session/play_rate"))); + base_.set_media_rate( + FrameRate(preference_value(j, "/core/session/media_rate"))); + base_.set_push_to_current_playlist( + preference_value(j, "/core/session/pushed_media_playlist_behaviour") == + "Push to Current Playlist"); + base_.set_push_playlist_name( + preference_value(j, "/core/session/pushed_media_playlist_name")); + } catch (...) { } @@ -463,10 +599,6 @@ SessionActor::SessionActor(caf::actor_config &cfg, const std::string &name) join_event_group(this, bookmarks_); link_to(bookmarks_); - tags_ = spawn(); - join_event_group(this, tags_); - link_to(tags_); - init(); } @@ -474,61 +606,82 @@ void SessionActor::init() { print_on_create(this, base_); print_on_exit(this, base_); - event_group_ = spawn(this); - link_to(event_group_); - - // monitor serilise targets. - set_down_handler([=](down_msg &msg) { - // find in playhead list.. - // if they don't unsubscribe we blow their data..! - auto target = caf::actor_cast(msg.source); - if (serialise_targets_.count(target)) { - anon_send(json_store_, json_store::erase_json_atom_v, serialise_targets_[target]); - serialise_targets_.erase(target); - demonitor(msg.source); - } - }); + ioactor_ = spawn(); + link_to(ioactor_); + + // // monitor serilise targets. + // set_down_handler([=](down_msg &msg) { + // // find in playhead list.. + // // if they don't unsubscribe we blow their data..! + // auto target = caf::actor_cast(msg.source); + // if (msg.source == viewedContainer_.actor()) { + // demonitor(viewedContainer_.actor()); + // viewedContainer_ = UuidActor(); + // base_.set_viewed_playlist_uuid(utility::Uuid()); + // mail( + // utility::event_atom_v, + // session::viewport_active_media_container_atom_v, + // viewedContainer_) + // .send(base_.event_group()); + // } else if (msg.source == inspectedContainer_.actor()) { + // demonitor(inspectedContainer_.actor()); + // inspectedContainer_ = UuidActor(); + // base_.set_current_playlist_uuid(utility::Uuid()); + // mail( + // utility::event_atom_v, + // session::active_media_container_atom_v, + // inspectedContainer_) + // .send(base_.event_group()); + // } + // if (serialise_targets_.count(target)) { + // anon_mail(json_store::erase_json_atom_v, serialise_targets_[target]) + // .send(json_store_); + // serialise_targets_.erase(target); + // demonitor(msg.source); + // } + // }); behavior_.assign(message_handler() + .or_else(base_.container_message_handler(this)) + .or_else(notification_.message_handler(this, base_.event_group())) .or_else(bookmark::BookmarksActor::default_event_handler()) - .or_else(playlist::PlaylistActor::default_event_handler()) - .or_else(tag::TagActor::default_event_handler())); + .or_else(playlist::PlaylistActor::default_event_handler())); - anon_send(caf::actor_cast(this), bookmark::associate_bookmark_atom_v); + anon_mail(bookmark::associate_bookmark_atom_v).send(caf::actor_cast(this)); + + auto playhead_events_actor = + system().registry().template get(global_playhead_events_actor); + + anon_mail(broadcast::join_broadcast_atom_v, caf::actor_cast(this)) + .send(playhead_events_actor); } caf::message_handler SessionActor::message_handler() { return { [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), - base_.make_ignore_error_handler(), make_get_version_handler(), + make_ignore_error_handler(), [=](bookmark::associate_bookmark_atom) -> result { auto rp = make_response_promise(); associate_bookmarks(rp); - delayed_anon_send( - caf::actor_cast(this), - std::chrono::milliseconds(1000), - bookmark::associate_bookmark_atom_v); + anon_mail(bookmark::associate_bookmark_atom_v) + .delay(std::chrono::milliseconds(1000)) + .send(caf::actor_cast(this)); return rp; }, + [=](name_atom, const std::string &name_template, const bool) -> std::string { + return get_next_name(name_template); + }, + [=](add_playlist_atom atom, const Uuid &uuid_before) { - delegate( - actor_cast(this), atom, "Untitled Playlist", uuid_before, false); + return mail(atom, get_next_name("Playlist {}"), uuid_before, false) + .delegate(actor_cast(this)); }, [=](add_playlist_atom atom, const std::string name) { - delegate(actor_cast(this), atom, name, Uuid(), false); + return mail(atom, name, Uuid(), false).delegate(actor_cast(this)); }, [=](add_playlist_atom, caf::actor actor, const Uuid &uuid_before, const bool into) @@ -561,10 +714,70 @@ caf::message_handler SessionActor::message_handler() { // gather sources for media and return new sources. [=](media_hook::gather_media_sources_atom atom, const caf::actor &media) { - delegate(caf::actor_cast(this), atom, media, base_.media_rate()); + return mail(atom, media, base_.media_rate()) + .delegate(caf::actor_cast(this)); + }, + + [=](timeline::item_selection_atom) -> UuidActorVector { return selection_; }, + + [=](timeline::item_selection_atom, const UuidActorVector &selection) { + selection_ = selection; + }, + + [=](get_push_playlist_atom) { + return mail(get_push_playlist_atom_v, "__DEFAULT_PUSH_PLAYLIST__") + .delegate(actor_cast(this)); }, - [=](tag::get_tag_atom) -> caf::actor { return tags_; }, + [=](get_push_playlist_atom, std::string playlist_name) -> result { + // get a playlist for 'pushing' media to (from external process) + + auto rp = make_response_promise(); + + bool dont_use_current = false; + if (playlist_name == "__DEFAULT_PUSH_PLAYLIST__") { + playlist_name = base_.push_playlist_name(); + } else { + // a specific playlist is requested + dont_use_current = true; + } + + if (base_.push_to_current_playlist() && inspectedContainer_ && !dont_use_current) { + rp.deliver(inspectedContainer_.actor()); + } else { + mail(get_playlist_atom_v, playlist_name) + .request(actor_cast(this), infinite) + .then( + [=](caf::actor playlist) mutable { + if (playlist) { + rp.deliver(playlist); + if (!inspectedContainer_) { + // no playlist selected, so select this one + anon_mail(active_media_container_atom_v, playlist) + .send(this); + } + } else { + + mail(add_playlist_atom_v, playlist_name) + .request(actor_cast(this), infinite) + .then( + [=](UuidUuidActor new_playlist) mutable { + rp.deliver(new_playlist.second.actor()); + if (!inspectedContainer_) { + // no playlist selected, so select this one + anon_mail( + active_media_container_atom_v, + new_playlist.second) + .send(this); + } + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + } + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + } + return rp; + }, [=](get_playlist_atom) -> result { // gets the first playlist @@ -609,6 +822,7 @@ caf::message_handler SessionActor::message_handler() { [=](get_playlist_atom, const std::string &name) -> caf::actor { std::function recursive_name_search; + recursive_name_search = [&recursive_name_search]( const PlaylistTree &tree, const std::string &name) -> Uuid { @@ -660,59 +874,39 @@ caf::message_handler SessionActor::message_handler() { }, [=](global_store::save_atom atom) { - delegate( - actor_cast(this), - atom, - base_.filepath(), - std::vector(), - static_cast(0), - true); + return mail( + atom, + base_.filepath(), + std::vector(), + static_cast(0), + true) + .delegate(actor_cast(this)); }, [=](global_store::save_atom atom, const caf::uri &path) { - delegate( - actor_cast(this), - atom, - path, - std::vector(), - static_cast(0), - true); + return mail(atom, path, std::vector(), static_cast(0), true) + .delegate(actor_cast(this)); }, [=](global_store::save_atom atom, const caf::uri &path, const size_t hash) { - delegate( - actor_cast(this), - atom, - path, - std::vector(), - hash, - true); + return mail(atom, path, std::vector(), hash, true) + .delegate(actor_cast(this)); }, [=](global_store::save_atom atom, const caf::uri &path, const size_t hash, const bool update_path) { - delegate( - actor_cast(this), - atom, - path, - std::vector(), - hash, - update_path); + return mail(atom, path, std::vector(), hash, update_path) + .delegate(actor_cast(this)); }, [=](global_store::save_atom atom, const caf::uri &path, const std::vector &containers) { - delegate( - actor_cast(this), - atom, - path, - containers, - static_cast(0), - false); + return mail(atom, path, containers, static_cast(0), false) + .delegate(actor_cast(this)); }, [=](global_store::save_atom, @@ -725,24 +919,36 @@ caf::message_handler SessionActor::message_handler() { auto rp = make_response_promise(); if (containers.empty()) { - request( - actor_cast(this), - std::chrono::seconds(60), - utility::serialise_atom_v) + mail(utility::serialise_atom_v) + .request(actor_cast(this), std::chrono::seconds(60)) .then( [=](const utility::JsonStore &js) mutable { - save_json_to(rp, js, path, update_path, hash); + mail(save_atom_v, js, path, update_path, hash) + .request(ioactor_, infinite) + .then( + [=](size_t r) mutable { + rp.deliver(r); + if (update_path) { + base_.set_filepath(path); + mail( + utility::event_atom_v, + path_atom_v, + std::make_pair( + base_.filepath(), + base_.session_file_mtime())) + .send(base_.event_group()); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); } else { - request( - actor_cast(this), - std::chrono::seconds(60), - utility::serialise_atom_v, - containers) + mail(utility::serialise_atom_v, containers) + .request(actor_cast(this), std::chrono::seconds(60)) .then( [=](const utility::JsonStore &js) mutable { - save_json_to(rp, js, path, false, hash); + rp.delegate(ioactor_, save_atom_v, js, path, false, hash); + // save_json_to(rp, js, path, false, hash); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); } @@ -750,31 +956,51 @@ caf::message_handler SessionActor::message_handler() { return rp; }, + [=](json_store::get_json_atom atom) { return mail(atom).delegate(json_store_); }, + [=](json_store::get_json_atom atom, const std::string &path) { - delegate(json_store_, atom, path); + return mail(atom, path).delegate(json_store_); }, [=](json_store::set_json_atom atom, const JsonStore &json, const std::string &path) { - delegate(json_store_, atom, json, path); + return mail(atom, json, path).delegate(json_store_); }, [=](load_uris_atom, const std::vector &uris, const bool single_playlist) { auto loader = system().spawn(actor_cast(this), uris); - anon_send(loader, load_uris_atom_v, single_playlist); + return mail(load_uris_atom_v, single_playlist).delegate(loader); + }, + + [=](load_uris_atom, + const std::vector &uris, + const bool single_playlist, + const bool make_subsets) { + auto loader = system().spawn(actor_cast(this), uris); + return mail(load_uris_atom_v, single_playlist, make_subsets).delegate(loader); }, [=](media_rate_atom) -> FrameRate { return base_.media_rate(); }, [=](media_rate_atom, const FrameRate &rate) { base_.set_media_rate(rate); - send(event_group_, utility::event_atom_v, media_rate_atom_v, rate); - base_.send_changed(event_group_, this); + mail(utility::event_atom_v, media_rate_atom_v, rate).send(base_.event_group()); + base_.send_changed(); // force all playlists to ave the same new media rate ? for (auto &i : playlists_) { - anon_send(i.second, media_rate_atom_v, rate); + anon_mail(media_rate_atom_v, rate).send(i.second); } }, + + // if (serialise_targets_.count(target)) { + // anon_mail(json_store::erase_json_atom_v, serialise_targets_[target]) + // .send(json_store_); + // serialise_targets_.erase(target); + // demonitor(msg.source); + // } + + // NOTHING SEEMS TO USE THIS AND ITS NOT POSSIBLE TO ADD ANYTHING TO IT.. + [=](remove_serialise_target_atom, const caf::actor target, const bool remove_data) -> bool { @@ -782,13 +1008,17 @@ caf::message_handler SessionActor::message_handler() { auto target_addr = caf::actor_cast(target); if (serialise_targets_.count(target_addr)) { if (remove_data) { - anon_send( - json_store_, - json_store::erase_json_atom_v, - serialise_targets_[target_addr]); + anon_mail(json_store::erase_json_atom_v, serialise_targets_[target_addr]) + .send(json_store_); } serialise_targets_.erase(target_addr); - demonitor(target); + + if (auto it = serialise_monitor_.find(target_addr); + it != std::end(serialise_monitor_)) { + it->second.dispose(); + serialise_monitor_.erase(it); + } + return true; } @@ -803,9 +1033,9 @@ caf::message_handler SessionActor::message_handler() { try { for (const auto &uuid : uuids) pls.push_back(playlists_[uuid]); - delegate(actor_cast(this), atom, name, before, pls); } catch (...) { } + return mail(atom, name, before, pls).delegate(actor_cast(this)); }, [=](merge_playlist_atom, @@ -815,13 +1045,8 @@ caf::message_handler SessionActor::message_handler() { auto rp = make_response_promise(); // create merge destination - request( - actor_cast(this), - infinite, - add_playlist_atom_v, - name, - before, - false) + mail(add_playlist_atom_v, name, before, false) + .request(actor_cast(this), infinite) .then( [=](const utility::UuidUuidActor &merged) mutable { // fanout.. @@ -836,13 +1061,12 @@ caf::message_handler SessionActor::message_handler() { for (const auto &i : cr) mcr.push_back(i); - request( - merged.second.actor(), - infinite, + mail( playlist::insert_container_atom_v, mcr, utility::Uuid(), false) + .request(merged.second.actor(), infinite) .then( [=](const UuidVector &) mutable { // now add containers/actors. and remap sources. @@ -868,7 +1092,8 @@ caf::message_handler SessionActor::message_handler() { [=](merge_session_atom, caf::actor session) -> result { // piggy back off of duplicate.. auto rp = make_response_promise(); - request(session, infinite, playlist::get_container_atom_v) + mail(playlist::get_container_atom_v) + .request(session, infinite) .then( [=](const PlaylistTree &pt) mutable { duplicate_container( @@ -901,17 +1126,10 @@ caf::message_handler SessionActor::message_handler() { const bool into) { auto copy_actor = system().spawn( actor_cast(this), bookmarks_, playlists_); - delegate( - copy_actor, - playlist::copy_media_atom_v, - dst, - src, - media, - true, - false, - before, - into); + return mail(playlist::copy_media_atom_v, dst, src, media, true, false, before, into) + .delegate(copy_actor); }, + [=](playlist::copy_media_atom, const Uuid &dst, const UuidVector &media, @@ -920,16 +1138,16 @@ caf::message_handler SessionActor::message_handler() { const bool into) { auto copy_actor = system().spawn( actor_cast(this), bookmarks_, playlists_); - delegate( - copy_actor, - playlist::copy_media_atom_v, - dst, - utility::Uuid(), - media, - false, - force_duplicate, - before, - into); + return mail( + playlist::copy_media_atom_v, + dst, + utility::Uuid(), + media, + false, + force_duplicate, + before, + into) + .delegate(copy_actor); }, [=](path_atom) -> std::pair { @@ -938,11 +1156,11 @@ caf::message_handler SessionActor::message_handler() { [=](path_atom, const caf::uri &uri) -> bool { base_.set_filepath(uri); - send( - event_group_, + mail( utility::event_atom_v, path_atom_v, - std::make_pair(base_.filepath(), base_.session_file_mtime())); + std::make_pair(base_.filepath(), base_.session_file_mtime())) + .send(base_.event_group()); return true; }, @@ -950,17 +1168,20 @@ caf::message_handler SessionActor::message_handler() { [=](playhead::playhead_rate_atom, const FrameRate &rate) { base_.set_playhead_rate(rate); - base_.send_changed(event_group_, this); + base_.send_changed(); }, + [=](utility::event_atom, utility::notification_atom, const utility::JsonStore &) {}, + [=](playlist::create_divider_atom, const std::string &name, const Uuid &uuid_before, const bool into) -> result { auto i = base_.insert_divider(name, uuid_before, into); if (i) { - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, playlist::create_divider_atom_v, *i); + base_.send_changed(); + mail(utility::event_atom_v, playlist::create_divider_atom_v, *i) + .send(base_.event_group()); return *i; } @@ -983,8 +1204,9 @@ caf::message_handler SessionActor::message_handler() { const Uuid &uuid_before) -> result { auto i = base_.insert_group(name, uuid_before); if (i) { - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, playlist::create_group_atom_v, *i); + base_.send_changed(); + mail(utility::event_atom_v, playlist::create_group_atom_v, *i) + .send(base_.event_group()); return *i; } @@ -1027,7 +1249,8 @@ caf::message_handler SessionActor::message_handler() { system().registry().template get(offscreen_viewport_registry); std::vector d; - request(bookmarks_, infinite, bookmark::bookmark_detail_atom_v, d) + mail(bookmark::bookmark_detail_atom_v, d) + .request(bookmarks_, infinite) .then( [=](std::vector bd) mutable { std::sort( @@ -1047,15 +1270,15 @@ caf::message_handler SessionActor::message_handler() { caf::actor owner = d.owner_->actor(); sprintf(buf.data(), path.c_str(), idx); caf::uri u = posix_path_to_uri(buf.data()); - request( - offscreen_renderer, - infinite, + // std::cerr << "buf.data() " << buf.data() << "\n"; + mail( ui::viewport::render_viewport_to_image_atom_v, owner, *(d.logical_start_frame_), 1920, 1080, u) + .request(offscreen_renderer, infinite) .then( [=](bool) mutable {}, [=](caf::error &err) { @@ -1094,6 +1317,36 @@ caf::message_handler SessionActor::message_handler() { return rp; }, + [=](media::get_media_source_atom, const Uuid &uuid) -> result { + if (playlists_.empty()) { + return make_error(xstudio_error::error, "No Playlists"); + } + + auto rp = make_response_promise(); + + fan_out_request( + playlists(), infinite, media::get_media_source_atom_v, uuid, true) + .then( + [=](std::vector results) mutable { + caf::actor result; + + for (const auto &i : results) { + if (i) { + result = i; + break; + } + } + + if (result) + rp.deliver(result); + else + rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); + }, + + [=](error &err) mutable { rp.deliver(std::move(err)); }); + return rp; + }, + [=](playlist::get_media_atom, const Uuid &uuid) -> result { if (playlists_.empty()) { return make_error(xstudio_error::error, "Invalid uuid"); @@ -1125,7 +1378,7 @@ caf::message_handler SessionActor::message_handler() { }, [=](playlist::move_container_atom atom, const Uuid &uuid, const Uuid &uuid_before) { - delegate(actor_cast(this), atom, uuid, uuid_before, false); + return mail(atom, uuid, uuid_before, false).delegate(actor_cast(this)); }, [=](playlist::copy_container_to_atom, @@ -1159,13 +1412,9 @@ caf::message_handler SessionActor::message_handler() { bool changed = base_.move_container(uuid, uuid_before, into); if (changed) { - base_.send_changed(event_group_, this); - send( - event_group_, - utility::event_atom_v, - playlist::move_container_atom_v, - uuid, - uuid_before); + base_.send_changed(); + mail(utility::event_atom_v, playlist::move_container_atom_v, uuid, uuid_before) + .send(base_.event_group()); } return changed; @@ -1176,13 +1425,9 @@ caf::message_handler SessionActor::message_handler() { const Uuid &uuid) -> bool { bool result = base_.reflag_container(flag, uuid); if (result) { - base_.send_changed(event_group_, this); - send( - event_group_, - utility::event_atom_v, - playlist::reflag_container_atom_v, - uuid, - flag); + base_.send_changed(); + mail(utility::event_atom_v, playlist::reflag_container_atom_v, uuid, flag) + .send(base_.event_group()); } return result; }, @@ -1226,17 +1471,14 @@ caf::message_handler SessionActor::message_handler() { } if (result) { - base_.send_changed(event_group_, this); - send( - event_group_, - utility::event_atom_v, - playlist::remove_container_atom_v, - cuuid); - send( - event_group_, + base_.send_changed(); + mail(utility::event_atom_v, playlist::remove_container_atom_v, cuuid) + .send(base_.event_group()); + mail( utility::event_atom_v, playlist::remove_container_atom_v, - removed_playlist_uuids); + removed_playlist_uuids) + .send(base_.event_group()); } return result; @@ -1250,48 +1492,223 @@ caf::message_handler SessionActor::message_handler() { // also propergate to actor is there is one.. auto found = base_.containers().cfind(uuid); if (found and playlists_.count((*found)->value().uuid())) - anon_send(playlists_[(*found)->value().uuid()], name_atom_v, name); + anon_mail(name_atom_v, name).send(playlists_[(*found)->value().uuid()]); - base_.send_changed(event_group_, this); - send( - event_group_, - utility::event_atom_v, - playlist::rename_container_atom_v, - uuid, - name); + base_.send_changed(); + mail(utility::event_atom_v, playlist::rename_container_atom_v, uuid, name) + .send(base_.event_group()); } return result; }, - [=](session::current_playlist_atom) -> result { - auto a = actor_cast(current_playlist_); - if (a) - return a; - - return make_error(xstudio_error::error, "No current playlist"); + [=](session::active_media_container_atom, const UuidActor &playlist) -> bool { + if (playlist != inspectedContainer_) { + + if (inspectedContainer_) + inspected_monitor_.dispose(); + + inspectedContainer_ = playlist; + if (inspectedContainer_) { + inspected_monitor_ = monitor( + inspectedContainer_.actor(), + [this, addr = inspectedContainer_.actor().address()](const error &) { + inspectedContainer_ = UuidActor(); + base_.set_current_playlist_uuid(utility::Uuid()); + mail( + utility::event_atom_v, + session::active_media_container_atom_v, + inspectedContainer_) + .send(base_.event_group()); + }); + } + base_.set_current_playlist_uuid(inspectedContainer_.uuid()); + mail( + utility::event_atom_v, + session::active_media_container_atom_v, + inspectedContainer_) + .send(base_.event_group()); + + if (!viewedContainer_) { + // if the viewed playlist is not set, default it to the inspected playlist + anon_mail( + session::viewport_active_media_container_atom_v, inspectedContainer_) + .send(this); + } + } + return true; }, - [=](session::current_playlist_atom, caf::actor actor, bool broadcast) { - current_playlist_ = actor_cast(actor); + [=](session::viewport_active_media_container_atom, const UuidActor &playlist) -> bool { + // Sets the active Playlist/Subset/Timeline ... either the 'viewed' one driving the + // viewport or the 'current' one that shows up in the MediaList, for example + if (playlist != viewedContainer_) { + + if (viewedContainer_) + viewed_monitor_.dispose(); + + viewedContainer_ = playlist; + if (viewedContainer_) { + viewed_monitor_ = monitor( + viewedContainer_.actor(), + [this, addr = viewedContainer_.actor().address()](const error &) { + viewedContainer_ = UuidActor(); + base_.set_viewed_playlist_uuid(utility::Uuid()); + mail( + utility::event_atom_v, + session::viewport_active_media_container_atom_v, + viewedContainer_) + .send(base_.event_group()); + }); + + // we need to ensure that the Playlist/Subset/Timeline has a playhead and + // that this playhead is broadcast to viewport so they can attach to it. + mail(playlist::create_playhead_atom_v) + .request(viewedContainer_.actor(), infinite) + .then( + [=](utility::UuidActor &playhead) { + // this is a bit nasty - I need to know if this SessionActor + // is the *current* session (it could be a session that's being + // imported) + mail(session::session_atom_v) + .request( + system().registry().template get( + studio_registry), + infinite) + .then( + [=](caf::actor session) { + // If we are not THE active session. Don't try and + // switch + // the global playhead ... + if (caf::actor_cast(this) != session) + return; + + // this actually broadcasts, via the global playhead + // events actor, the new playhead to all viewports + // so they can attach to the playhead + auto playhead_events_actor = + system().registry().template get( + global_playhead_events_actor); + + anon_mail( + ui::viewport::viewport_playhead_atom_v, + playhead.actor()) + .send(playhead_events_actor); + }, + [=](caf::error &err) {}); + + + mail( + utility::event_atom_v, + ui::viewport::viewport_playhead_atom_v, + playhead.uuid()) + .send(base_.event_group()); + }, + [=](caf::error &err) { - if (broadcast) { - send( - event_group_, + }); + } + base_.set_viewed_playlist_uuid(viewedContainer_.uuid()); + mail( utility::event_atom_v, - session::current_playlist_atom_v, - actor); + session::viewport_active_media_container_atom_v, + playlist) + .send(base_.event_group()); + } + return true; + }, + + [=](media::current_media_atom) -> UuidActorVector { + auto result = UuidActorVector(); + + for (const auto &i : selectedMedia_) + result.emplace_back(UuidActor(i.first, i.second)); + + return result; + }, + + [=](media::current_media_atom, const UuidActorVector &media) { + selectedMedia_.clear(); + for (const auto &i : media) { + selectedMedia_.push_back(std::make_pair(i.uuid(), i.actor_addr())); } }, - [=](session::current_playlist_atom, caf::actor actor) { - current_playlist_ = actor_cast(actor); - send(event_group_, utility::event_atom_v, session::current_playlist_atom_v, actor); + [=](session::active_media_container_atom) -> UuidActor { return inspectedContainer_; }, + + [=](session::viewport_active_media_container_atom) -> UuidActor { + return viewedContainer_; + }, + + [=](session::active_media_container_atom, utility::Uuid playlist_uuid) { + mail(get_playlist_atom_v, playlist_uuid) + .request(caf::actor_cast(this), infinite) + .then( + [=](caf::actor playlist) { + anon_mail( + session::active_media_container_atom_v, + UuidActor(playlist_uuid, playlist)) + .send(this); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + }, + + [=](session::viewport_active_media_container_atom, utility::Uuid playlist_uuid) { + mail(get_playlist_atom_v, playlist_uuid) + .request(caf::actor_cast(this), infinite) + .then( + [=](caf::actor playlist) { + anon_mail( + session::viewport_active_media_container_atom_v, + UuidActor(playlist_uuid, playlist)) + .send(this); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + }, + + [=](session::active_media_container_atom, caf::actor actor) { + mail(utility::uuid_atom_v) + .request(actor, infinite) + .then( + [=](const utility::Uuid &playlist_uuid) mutable { + anon_mail( + session::active_media_container_atom_v, + UuidActor(playlist_uuid, actor)) + .send(this); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + }, + + [=](session::viewport_active_media_container_atom, caf::actor actor) { + mail(utility::uuid_atom_v) + .request(actor, infinite) + .then( + [=](const utility::Uuid &playlist_uuid) mutable { + anon_mail( + session::viewport_active_media_container_atom_v, + UuidActor(playlist_uuid, actor)) + .send(this); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); }, - [=](utility::event_atom, playlist::add_media_atom, const UuidActor &ua) { - send(event_group_, utility::event_atom_v, playlist::add_media_atom_v, ua); + [=](utility::event_atom, playlist::add_media_atom, const UuidActorVector &uav) { + mail(utility::event_atom_v, playlist::add_media_atom_v, uav) + .send(base_.event_group()); }, + [=](utility::event_atom, + media::media_display_info_atom, + const utility::JsonStore &, + caf::actor_addr &) {}, + [=](utility::event_atom, playlist::remove_media_atom, const UuidVector &) {}, // still exist ? @@ -1305,11 +1722,8 @@ caf::message_handler SessionActor::message_handler() { // allow actor to push it's serialisation data. auto target_addr = caf::actor_cast(actor); if (serialise_targets_.count(target_addr)) { - anon_send( - json_store_, - json_store::set_json_atom_v, - data, - serialise_targets_[target_addr]); + anon_mail(json_store::set_json_atom_v, data, serialise_targets_[target_addr]) + .send(json_store_); return true; } @@ -1324,17 +1738,17 @@ caf::message_handler SessionActor::message_handler() { auto rp = make_response_promise(); // flush abitary data to our store. - request( - caf::actor_cast(this), infinite, utility::serialise_atom_v, true) + mail(utility::serialise_atom_v, true) + .request(caf::actor_cast(this), infinite) .then( [=](const bool) mutable { auto stores = std::make_shared>(); - request( - system().registry().template get(global_store_registry), - infinite, - utility::serialise_atom_v, - "SESSION") + mail(utility::serialise_atom_v, "SESSION") + .request( + system().registry().template get( + global_store_registry), + infinite) .then( [=](const JsonStore &result) mutable { (*stores)["global_store"] = result; @@ -1348,7 +1762,8 @@ caf::message_handler SessionActor::message_handler() { rp.deliver(std::move(err)); }); - request(json_store_, infinite, json_store::get_json_atom_v) + mail(json_store::get_json_atom_v) + .request(json_store_, infinite) .then( [=](const JsonStore &result) mutable { (*stores)["store"] = result; @@ -1360,7 +1775,8 @@ caf::message_handler SessionActor::message_handler() { rp.deliver(std::move(err)); }); - request(bookmarks_, infinite, utility::serialise_atom_v) + mail(utility::serialise_atom_v) + .request(bookmarks_, infinite) .then( [=](const JsonStore &result) mutable { (*stores)["bookmarks"] = result; @@ -1372,22 +1788,11 @@ caf::message_handler SessionActor::message_handler() { rp.deliver(std::move(err)); }); - request(tags_, infinite, utility::serialise_atom_v) - .then( - [=](const JsonStore &result) mutable { - (*stores)["tags"] = result; - check_save_serialise_payload(stores, rp); - }, - [=](error &err) mutable { - spdlog::warn( - "{} tags {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(std::move(err)); - }); - - request( - system().registry().template get(media_hook_registry), - infinite, - utility::serialise_atom_v) + mail(utility::serialise_atom_v) + .request( + system().registry().template get( + media_hook_registry), + infinite) .then( [=](const JsonStore &result) mutable { (*stores)["media_hook_versions"] = result; @@ -1428,6 +1833,23 @@ caf::message_handler SessionActor::message_handler() { return rp; }, + [=](json_store::update_atom, + const JsonStore &change, + const std::string &path, + const JsonStore &) { + try { + if (path == "/core/session/pushed_media_playlist_behaviour/value") { + base_.set_push_to_current_playlist( + change.get() == "Push to Current Playlist"); + } else if (path == "/core/session/pushed_media_playlist_name/value") { + base_.set_push_playlist_name(change.get()); + } + } catch (std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + }, + [=](json_store::update_atom, const JsonStore &) {}, + [=](utility::serialise_atom, const std::vector &containers) -> result { caf::scoped_actor sys(system()); @@ -1460,6 +1882,9 @@ caf::message_handler SessionActor::message_handler() { clients.push_back(playlists_[i]); } + auto bookmarks = + request_receive(*sys, bookmarks_, utility::serialise_atom_v); + if (not clients.empty()) { auto rp = make_response_promise(); fan_out_request(clients, infinite, serialise_atom_v) @@ -1471,6 +1896,7 @@ caf::message_handler SessionActor::message_handler() { jsn["actors"] = {}; jsn["global_store"] = global_store_json; jsn["store"] = local_json; + jsn["bookmarks"] = bookmarks; for (const auto &j : json) { jsn["actors"][static_cast( j["base"]["container"]["uuid"])] = j; @@ -1487,23 +1913,88 @@ caf::message_handler SessionActor::message_handler() { jsn["actors"] = {}; jsn["global_store"] = global_store_json; jsn["store"] = local_json; + jsn["bookmarks"] = bookmarks; return result(jsn); } catch (const std::exception &err) { return make_error(xstudio_error::error, err.what()); } }, + [=](ui::open_quickview_window_atom, + const utility::UuidActorVector &media_items, + std::string compare_mode) { + // forward to the studio actor + anon_mail( + ui::open_quickview_window_atom_v, + media_items, + compare_mode, + utility::JsonStore(), + utility::JsonStore()) + .send(home_system().registry().get(studio_registry)); + }, [=](ui::open_quickview_window_atom, const utility::UuidActorVector &media_items, std::string compare_mode, - bool force) { + const utility::JsonStore in_point, + const utility::JsonStore out_point) { // forward to the studio actor - anon_send( - home_system().registry().get(studio_registry), + anon_mail( ui::open_quickview_window_atom_v, media_items, compare_mode, - force); + in_point, + out_point) + .send(home_system().registry().get(studio_registry)); + }, + [=](utility::event_atom, + ui::viewport::viewport_atom, + const std::string &viewport_name, + caf::actor viewport) { + // event from 'global_playhead_events_actor' + // a new viewport has been created + }, + + [=](utility::event_atom, + ui::viewport::viewport_playhead_atom, + const std::string &viewport_name, + caf::actor playhead) { + // event from 'global_playhead_events_actor' + // the playhead of the given viewport has changed + }, + + [=](utility::event_atom, + ui::viewport::viewport_playhead_atom, + caf::actor live_playhead) { + // the main, globally active playhead that pushes images to the + // viewport (apart from QuickView windows) has changed.... + + if (live_playhead) { + // we want to get the playlist/subset/timeline that owns this + // new playhead + mail(utility::parent_atom_v) + .request(live_playhead, infinite) + .then( + [=](caf::actor_addr playhead_owner) { + // the playhead owner is a Playlist, Subset or Timeline + anon_mail( + session::viewport_active_media_container_atom_v, + caf::actor_cast(playhead_owner)) + .send(this); + }, + [=](caf::error &err) {}); + } else { + anon_mail(session::viewport_active_media_container_atom_v, UuidActor()) + .send(this); + } + }, + + [=](utility::event_atom, + playhead::show_atom, + caf::actor media, + caf::actor media_source, + const std::string &viewport_name) { + // event from 'global_playhead_events_actor' + // the onscreen media for the given viewport has changed }}; } @@ -1518,8 +2009,6 @@ void SessionActor::check_save_serialise_payload( return; if (not payload->count("bookmarks")) return; - if (not payload->count("tags")) - return; if (not payload->count("media_hook_versions")) return; if (not payload->count("actors")) @@ -1532,7 +2021,6 @@ void SessionActor::check_save_serialise_payload( jsn["global_store"] = (*payload)["global_store"]; jsn["store"] = (*payload)["store"]; jsn["bookmarks"] = (*payload)["bookmarks"]; - jsn["tags"] = (*payload)["tags"]; jsn["media_hook_versions"] = (*payload)["media_hook_versions"]; rp.deliver(jsn); @@ -1555,11 +2043,13 @@ void SessionActor::sync_to_json_store(caf::typed_response_promise &rp) { rp.deliver(true); for (const auto &i : *actors) { - request(i.first, infinite, utility::serialise_atom_v) + mail(utility::serialise_atom_v) + .request(i.first, infinite) .then( [=](const JsonStore &json) mutable { auto target = caf::actor_cast(current_sender()); - request(json_store_, infinite, json_store::set_json_atom_v, json, i.second) + mail(json_store::set_json_atom_v, json, i.second) + .request(json_store_, infinite) .then( [=](const bool) mutable { (*actors).erase(target); @@ -1588,10 +2078,14 @@ void SessionActor::create_playlist( std::string name, const utility::Uuid &uuid_before, const bool into) { + + if (name.empty()) + name = get_next_name("Playlist {}"); + auto actor = spawn( name, utility::Uuid(), caf::actor_cast(this)); - anon_send(actor, media_rate_atom_v, base_.media_rate()); - anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); + anon_mail(media_rate_atom_v, base_.media_rate()).send(actor); + anon_mail(playhead::playhead_rate_atom_v, base_.playhead_rate()).send(actor); create_container(actor, rp, uuid_before, into); } @@ -1600,8 +2094,8 @@ void SessionActor::insert_playlist( caf::actor actor, const utility::Uuid &uuid_before, const bool into) { - anon_send(actor, media_rate_atom_v, base_.media_rate()); - anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); + anon_mail(media_rate_atom_v, base_.media_rate()).send(actor); + anon_mail(playhead::playhead_rate_atom_v, base_.playhead_rate()).send(actor); create_container(actor, rp, uuid_before, into); } @@ -1611,7 +2105,8 @@ void SessionActor::create_container( const Uuid &uuid_before, const bool into) { - request(actor, infinite, detail_atom_v) + mail(detail_atom_v) + .request(actor, infinite) .await( [=](const ContainerDetail &detail) mutable { playlists_[detail.uuid_] = actor; @@ -1622,12 +2117,9 @@ void SessionActor::create_container( if (not cuuid) { cuuid = base_.insert_container(tmp); } - base_.send_changed(event_group_, this); - send( - event_group_, - utility::event_atom_v, - add_playlist_atom_v, - UuidActor(detail.uuid_, actor)); + base_.send_changed(); + mail(utility::event_atom_v, add_playlist_atom_v, UuidActor(detail.uuid_, actor)) + .send(base_.event_group()); rp.deliver(std::make_pair(*cuuid, UuidActor(detail.uuid_, actor))); }, @@ -1648,13 +2140,16 @@ void SessionActor::duplicate_container( const bool into, const bool rename, const bool kill_source) { + // find all actor children.. (Timeline/SUBSET/CONTACTSHEET) // quick clone of divider.. // clone structure PlaylistTree new_tree(tree); + // regenerate uuids.. new_tree.reset_uuid(true); + duplicate_tree(new_tree, source_session, rename); std::function &)> flatten_tree; @@ -1681,7 +2176,7 @@ void SessionActor::duplicate_container( base_.insert_container(new_tree, uuid_before, into); } - base_.send_changed(event_group_, this); + base_.send_changed(); if (kill_source) send_exit(source_session, caf::exit_reason::user_shutdown); @@ -1700,10 +2195,12 @@ void SessionActor::duplicate_tree( if (tree.value().type() == "ContainerDivider") { tree.value().set_uuid(Uuid::generate()); - send(event_group_, utility::event_atom_v, playlist::create_divider_atom_v, tree.uuid()); + mail(utility::event_atom_v, playlist::create_divider_atom_v, tree.uuid()) + .send(base_.event_group()); } else if (tree.value().type() == "ContainerGroup") { tree.value().set_uuid(Uuid::generate()); - send(event_group_, utility::event_atom_v, playlist::create_group_atom_v, tree.uuid()); + mail(utility::event_atom_v, playlist::create_group_atom_v, tree.uuid()) + .send(base_.event_group()); } else if (tree.value().type() == "Playlist") { // need to issue a duplicate action, as we actors are blackboxes.. // try not to confuse this with duplicating a container, as opposed to the actor.. @@ -1732,11 +2229,11 @@ void SessionActor::duplicate_tree( if (rename) { auto name = request_receive(*sys, result.actor(), name_atom_v); tree.value().set_name(name + " - copy"); - send(result.actor(), name_atom_v, tree.value().name()); + mail(name_atom_v, tree.value().name()).send(result.actor()); } link_to(result.actor()); - send(event_group_, utility::event_atom_v, add_playlist_atom_v, result); + mail(utility::event_atom_v, add_playlist_atom_v, result).send(base_.event_group()); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); // set to invalid uuid ? @@ -1781,13 +2278,8 @@ void SessionActor::copy_containers_to( for (const auto &i : cr) mcr.push_back(i); - request( - dst_actor, - infinite, - playlist::insert_container_atom_v, - mcr, - uuid_before, - into) + mail(playlist::insert_container_atom_v, mcr, uuid_before, into) + .request(dst_actor, infinite) .then( [=](const utility::UuidVector &newitems) mutable { // now add containers/actors. and remap sources. @@ -1850,13 +2342,8 @@ void SessionActor::move_containers_to( for (const auto &i : cr) mcr.push_back(i); - request( - dst_actor, - infinite, - playlist::insert_container_atom_v, - mcr, - uuid_before, - into) + mail(playlist::insert_container_atom_v, mcr, uuid_before, into) + .request(dst_actor, infinite) .then( [=](const utility::UuidVector &newitems) mutable { // now add containers/actors. and remap sources. @@ -1873,98 +2360,52 @@ void SessionActor::move_containers_to( } } -void SessionActor::save_json_to( - caf::typed_response_promise &rp, - const utility::JsonStore &js, - const caf::uri &path, - const bool update_path, - const size_t hash) { - size_t new_hash = 0; +std::string SessionActor::get_next_name(const std::string &name_template) const { + auto result = name_template; - try { - auto data = js.dump(2); + auto merged_tree = base_.containers(); - auto resolve_link = false; - new_hash = std::hash{}(data); + caf::scoped_actor sys(system()); - // no change in hash, so skip save (autosave) - if (new_hash == hash) { - return rp.deliver(new_hash); - } + // pity we don't sync children.. like timelines.. + for (const auto &p : playlists()) { + auto pt = request_receive(*sys, p, playlist::get_container_atom_v); + merged_tree.insert(pt); + } - // fix something ? - auto ppath = utility::posix_path_to_uri(utility::uri_to_posix_path(path)); + // No name supplied ... we want to create a new playlist called + // 'Playlist 1' or, if 'Playlist 1' already exists 'Playlist 2' etc. + std::function recursive_name_search; - // try and save, we are already looking at this file - if (update_path) { - // same path as session, are we allowed ? - resolve_link = true; + recursive_name_search = + [&recursive_name_search](const PlaylistTree &tree, const std::string &name) -> Uuid { + if (tree.name() == name) { + return tree.value_uuid(); } - - auto save_path = uri_to_posix_path(ppath); - if (resolve_link && fs::exists(save_path) && fs::is_symlink(save_path)) -#ifdef _WIN32 - save_path = fs::canonical(save_path).string(); -#else - save_path = fs::canonical(save_path); -#endif - - - // compress data. - if (to_lower(path_to_string(fs::path(save_path).extension())) == ".xsz") { - zstr::ofstream o(save_path + ".tmp"); - try { - o.exceptions(std::ifstream::failbit | std::ifstream::badbit); - // if(not o.is_open()) - // throw std::runtime_error(); - o << std::setw(4) << data << std::endl; - o.close(); - } catch (const std::exception &) { - // remove failed file - if (o.is_open()) { - o.close(); - fs::remove(save_path + ".tmp"); - } - throw std::runtime_error("Failed to open file"); - } - } else { - // this maybe a symlink in which case we should resolve it. - std::ofstream o(save_path + ".tmp"); - try { - o.exceptions(std::ifstream::failbit | std::ifstream::badbit); - // if(not o.is_open()) - // throw std::runtime_error(); - o << std::setw(4) << data << std::endl; - o.close(); - } catch (const std::exception &) { - // remove failed file - if (o.is_open()) { - o.close(); - fs::remove(save_path + ".tmp"); - } - throw std::runtime_error("Failed to open file"); - } + // also search for other children of session.. + for (auto i : tree.children_ref()) { + auto uuid = recursive_name_search(i, name); + if (uuid) + return uuid; } + return Uuid(); + }; - // rename tmp to final name - fs::rename(save_path + ".tmp", save_path); + int n = 1; - if (update_path) { - base_.set_filepath(path); - send( - event_group_, - utility::event_atom_v, - path_atom_v, - std::make_pair(base_.filepath(), base_.session_file_mtime())); - } + while (true) { + result = fmt::format(fmt::runtime(name_template), n); + if (result == name_template) + break; - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - return rp.deliver(make_error(xstudio_error::error, err.what())); + if (not recursive_name_search(merged_tree, result)) + break; + + n++; } - rp.deliver(new_hash); + return result; } void SessionActor::associate_bookmarks(caf::typed_response_promise &rp) { @@ -2000,11 +2441,8 @@ void SessionActor::check_media_hook_plugin_version( media_hook_plugin_serialised_info = jsn["media_hook_versions"]; } - request( - hooks, - infinite, - media_hook::check_media_hook_plugin_versions_atom_v, - media_hook_plugin_serialised_info) + mail(media_hook::check_media_hook_plugin_versions_atom_v, media_hook_plugin_serialised_info) + .request(hooks, infinite) .then( [=](bool hook_plugin_versions_ok) { if (!hook_plugin_versions_ok) { @@ -2024,7 +2462,8 @@ void SessionActor::check_media_hook_plugin_version( [=](const std::vector> media_ua) mutable { for (const auto &v : media_ua) { for (const auto &m : v) { - anon_send(m.actor(), media_hook::get_media_hook_atom_v); + anon_mail(media_hook::get_media_hook_atom_v) + .send(m.actor()); } } }, @@ -2064,7 +2503,8 @@ void SessionActor::gather_media_sources_media_hook( auto hook = system().registry().template get(media_hook_registry); if (hook) { - request(hook, infinite, media_hook::gather_media_sources_atom_v, media, media_rate) + mail(media_hook::gather_media_sources_atom_v, media, media_rate) + .request(hook, infinite) .then( [=](const UuidActorVector &dsources) mutable { sources.insert(sources.end(), dsources.begin(), dsources.end()); @@ -2087,7 +2527,8 @@ void SessionActor::gather_media_sources_data_source( auto pm = system().registry().template get(plugin_manager_registry); if (pm) { - request(pm, infinite, data_source::use_data_atom_v, media, media_rate) + mail(data_source::use_data_atom_v, media, media_rate) + .request(pm, infinite) .then( [=](const UuidActorVector &dsources) mutable { sources.insert(sources.end(), dsources.begin(), dsources.end()); @@ -2116,7 +2557,8 @@ void SessionActor::gather_media_sources_add_media( if (sources.empty()) return rp.deliver(sources); - request(media, infinite, media::get_media_source_names_atom_v) + mail(media::get_media_source_names_atom_v) + .request(media, infinite) .then( [=](const std::vector> ¤t_names) mutable { @@ -2136,11 +2578,8 @@ void SessionActor::gather_media_sources_add_media( } } - request( - media, - infinite, - media::add_media_source_atom_v, - deduped_sources) + mail(media::add_media_source_atom_v, deduped_sources) + .request(media, infinite) .then( [=](const bool) mutable { rp.deliver(deduped_sources); }, [=](const caf::error &err) mutable { @@ -2158,4 +2597,4 @@ void SessionActor::gather_media_sources_add_media( spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); rp.deliver(err); }); -} +} \ No newline at end of file diff --git a/src/shotgun_client/src/CMakeLists.txt b/src/shotgun_client/src/CMakeLists.txt index 66e7081fd..613729599 100644 --- a/src/shotgun_client/src/CMakeLists.txt +++ b/src/shotgun_client/src/CMakeLists.txt @@ -1,11 +1,10 @@ SET(LINK_DEPS xstudio::utility - xstudio::broadcast xstudio::http_client - caf::core + CAF::core ) find_package(OpenSSL) find_package(ZLIB) -create_component(shotgun_client 0.1.0 "${LINK_DEPS}") +create_component(shotgun_client ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/shotgun_client/src/shotgun_client_actor.cpp b/src/shotgun_client/src/shotgun_client_actor.cpp index aaaa65eac..92bef09c0 100644 --- a/src/shotgun_client/src/shotgun_client_actor.cpp +++ b/src/shotgun_client/src/shotgun_client_actor.cpp @@ -1,4 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 +#include #include "xstudio/atoms.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" @@ -50,148 +51,35 @@ void ShotgunClientActor::init() { }, [=](shotgun_acquire_token_atom) -> result> { - // spdlog::warn("shotgun_acquire_token_atom"); - auto rp = make_response_promise>(); - - if (not base_.refresh_token().empty()) { - // spdlog::warn("not base_.refresh_token().empty()"); - request_refresh_queue_.push(rp); - if (request_refresh_queue_.size() > 1) - return rp; - request(actor_cast(this), infinite, shotgun_refresh_token_atom_v) - .then( - [=](const std::pair &tokens) mutable { - // spdlog::info("refresh from token"); - base_.set_authenticated(true); - while (not request_refresh_queue_.empty()) { - request_refresh_queue_.front().deliver(tokens); - request_refresh_queue_.pop(); - } - }, - [=](error &) mutable { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - // try using login - base_.expire_refresh_token(); - while (not request_refresh_queue_.empty()) { - request_refresh_queue_.front().delegate( - actor_cast(this), shotgun_acquire_token_atom_v); - request_refresh_queue_.pop(); - } - }); - } else { - if (not base_.failed_authentication()) { - acquire_token(rp); - } else { - // spdlog::info("refresh from login"); - // request secret. - request( - actor_cast(secret_source_), - infinite, - shotgun_acquire_authentication_atom_v, - base_.failed_authentication() ? "Authentication failed, try again." - : "") - .then( - [=](const AuthenticateShotgun &auth) mutable { - base_.set_credentials_method(auth); - rp.delegate( - actor_cast(this), shotgun_acquire_token_atom_v); - }, - [=](error &err) mutable { - base_.set_authenticated(false); - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(std::move(err)); - }); - } - } + acquire_token_primary(rp); return rp; }, - [=](shotgun_authenticate_atom, const AuthenticateShotgun &auth) { + [=](shotgun_authenticate_atom, + const AuthenticateShotgun &auth) -> result> { + // spdlog::error("shotgun_authenticate_atom"); + auto rp = make_response_promise>(); base_.set_credentials_method(auth); - delegate(actor_cast(this), shotgun_acquire_token_atom_v); + acquire_token_primary(rp); + return rp; }, [=](shotgun_link_atom, const std::string &link) -> result { auto rp = make_response_promise(); - // spdlog::warn("shotgun_link_atom"); - if (not base_.authenticated()) { - request(actor_cast(this), infinite, shotgun_acquire_token_atom_v) - .then( - [=](const std::pair) mutable { - rp.delegate( - actor_cast(this), shotgun_link_atom_v, link); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - } else { - request( - http_, - infinite, - http_get_atom_v, - base_.scheme_host_port(), - link, - base_.get_auth_headers()) - .then( - [=](const httplib::Response &response) mutable { - try { - auto jsn = nlohmann::json::parse(response.body); - - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std::pair< - std::string, - std::string>) mutable { - rp.delegate( - actor_cast(this), - shotgun_link_atom_v, - link); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(JsonStore(std::move(jsn))); - }); - return; - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - rp.deliver(JsonStore(std::move(jsn))); - } catch (const std::exception &err) { - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - } - - + authenticate(rp, [=]() { request_link(link, rp); }); return rp; }, - [=](shotgun_update_entity_atom atom, + [=](shotgun_update_entity_atom, const std::string &entity, const int record_id, - const JsonStore &body) { - delegate( - actor_cast(this), - atom, - entity, - record_id, - body, - std::vector()); + const JsonStore &body) -> result { + auto rp = make_response_promise(); + authenticate(rp, [=]() { + update_entity(entity, record_id, body, std::vector(), rp); + }); + return rp; }, [=](shotgun_update_entity_atom, @@ -200,121 +88,15 @@ void ShotgunClientActor::init() { const JsonStore &body, const std::vector &fields) -> result { auto rp = make_response_promise(); - - request( - http_, - infinite, - http_put_atom_v, - base_.scheme_host_port(), - std::string("/api/v1/entity/" + entity + "/" + std::to_string(record_id)), - base_.get_auth_headers(), - body.dump(), - httplib::Params( - {{"options[fields]", fields.empty() ? "*" : join_as_string(fields, ",")}}), - base_.content_type_json()) - .then( - [=](const httplib::Response &response) mutable { - try { - auto jsn = nlohmann::json::parse(response.body); - - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std:: - pair) mutable { - rp.delegate( - actor_cast(this), - shotgun_update_entity_atom_v, - entity, - record_id, - body, - fields); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(JsonStore(std::move(jsn))); - }); - return; - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - rp.deliver(JsonStore(std::move(jsn))); - } catch (const std::exception &err) { - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - + authenticate(rp, [=]() { update_entity(entity, record_id, body, fields, rp); }); return rp; }, [=](shotgun_create_entity_atom, const std::string &entity, const JsonStore &body) -> result { - // spdlog::warn("shotgun_create_entity_atom"); auto rp = make_response_promise(); - request( - http_, - infinite, - http_post_atom_v, - base_.scheme_host_port(), - std::string("/api/v1/entity/" + entity), - base_.get_auth_headers(), - body.dump(), - base_.content_type_json()) - .then( - [=](const httplib::Response &response) mutable { - try { - auto jsn = nlohmann::json::parse(response.body); - - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std:: - pair) mutable { - rp.delegate( - actor_cast(this), - shotgun_create_entity_atom_v, - entity, - body); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(JsonStore(std::move(jsn))); - }); - return; - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - rp.deliver(JsonStore(std::move(jsn))); - } catch (const std::exception &err) { - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - + authenticate(rp, [=]() { create_entity(entity, body, rp); }); return rp; }, @@ -322,186 +104,69 @@ void ShotgunClientActor::init() { const std::string &entity, const int record_id) -> result { auto rp = make_response_promise(); - request( - http_, - infinite, - http_delete_atom_v, - base_.scheme_host_port(), - std::string("/api/v1/entity/" + entity + "/") + std::to_string(record_id), - base_.get_auth_headers(), - "", - base_.content_type_json()) - .then( - [=](const httplib::Response &response) mutable { - try { - if (response.body == "") - rp.deliver(JsonStore()); - else { - auto jsn = nlohmann::json::parse(response.body); - - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std::pair< - std::string, - std::string>) mutable { - rp.delegate( - actor_cast(this), - shotgun_delete_entity_atom_v, - entity, - record_id); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(JsonStore(std::move(jsn))); - }); - return; - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - rp.deliver(JsonStore(std::move(jsn))); - } - } catch (const std::exception &err) { - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - + authenticate(rp, [=]() { delete_entity(entity, record_id, rp); }); return rp; }, - [=](shotgun_entity_atom atom, const std::string &entity, const int record_id) { - delegate( - actor_cast(this), - atom, - entity, - record_id, - std::vector()); + [=](shotgun_entity_atom, const std::string &entity, const int record_id) { + auto rp = make_response_promise(); + authenticate(rp, [=]() { + request_entity(entity, record_id, std::vector(), rp); + }); + return rp; }, [=](shotgun_entity_atom, const std::string &entity, const int record_id, const std::vector &fields) -> result { - // spdlog::warn("shotgun_entity_atom"); auto rp = make_response_promise(); - - if (not base_.authenticated()) { - request(actor_cast(this), infinite, shotgun_acquire_token_atom_v) - .then( - [=](const std::pair) mutable { - rp.delegate( - actor_cast(this), - shotgun_entity_atom_v, - entity, - record_id, - fields); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - } else { - // requires authentication.. - request( - http_, - infinite, - http_get_atom_v, - base_.scheme_host_port(), - std::string("/api/v1/entity/" + entity + "/" + std::to_string(record_id)), - base_.get_auth_headers(), - httplib::Params( - {{"fields", fields.empty() ? "*" : join_as_string(fields, ",")}})) - .then( - [=](const httplib::Response &response) mutable { - try { - auto jsn = nlohmann::json::parse(response.body); - - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std::pair< - std::string, - std::string>) mutable { - rp.delegate( - actor_cast(this), - shotgun_entity_atom_v, - entity, - record_id, - fields); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(JsonStore(std::move(jsn))); - }); - return; - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - rp.deliver(JsonStore(std::move(jsn))); - } catch (const std::exception &err) { - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - } + authenticate(rp, [=]() { request_entity(entity, record_id, fields, rp); }); return rp; }, - [=](shotgun_entity_filter_atom atom, const std::string &entity) { - delegate( - actor_cast(this), - atom, - entity, - JsonStore(), - std::vector(), - std::vector(), - 1, - 1000); + [=](shotgun_entity_filter_atom, const std::string &entity) -> result { + auto rp = make_response_promise(); + authenticate(rp, [=]() { + request_entity_filter( + entity, + JsonStore(), + std::vector(), + std::vector(), + 1, + 1000, + rp); + }); + return rp; }, - [=](shotgun_entity_filter_atom atom, + [=](shotgun_entity_filter_atom, const std::string &entity, - const JsonStore &filter) { - delegate( - actor_cast(this), - atom, - entity, - filter, - std::vector(), - std::vector(), - 1, - 1000); + const JsonStore &filter) -> result { + auto rp = make_response_promise(); + authenticate(rp, [=]() { + request_entity_filter( + entity, + filter, + std::vector(), + std::vector(), + 1, + 1000, + rp); + }); + return rp; }, - [=](shotgun_entity_filter_atom atom, + [=](shotgun_entity_filter_atom, const std::string &entity, const JsonStore &filter, const std::vector &fields, - const std::vector &sort) { - delegate(actor_cast(this), atom, entity, filter, fields, sort, 1, 1000); + const std::vector &sort) -> result { + auto rp = make_response_promise(); + authenticate(rp, [=]() { + request_entity_filter(entity, filter, fields, sort, 1, 1000, rp); + }); + return rp; }, [=](shotgun_entity_filter_atom, @@ -511,143 +176,23 @@ void ShotgunClientActor::init() { const std::vector &sort, const int page, const int page_size) -> result { - // spdlog::warn("shotgun_entity_filter_atom"); - auto rp = make_response_promise(); - - if (not base_.authenticated()) { - request(actor_cast(this), infinite, shotgun_acquire_token_atom_v) - .then( - [=](const std::pair) mutable { - rp.delegate( - actor_cast(this), - shotgun_entity_filter_atom_v, - entity, - filter, - fields, - sort, - page, - page_size); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - } else { - try { - httplib::Params params; - - if (page) { - params.insert(std::make_pair("number", std::to_string(page))); - params.insert(std::make_pair("size", std::to_string(page_size))); - } - - if (fields.empty()) { - params.insert(std::make_pair("fields", "*")); - } else { - params.insert(std::make_pair("fields", join_as_string(fields, ","))); - } - - if (not sort.empty()) { - params.insert(std::make_pair("sort", join_as_string(sort, ","))); - } - - if (not filter.empty()) { - // should be an array.. - for (const auto &[key, value] : filter.items()) { - if (value.is_array()) - params.insert(std::make_pair( - "filter[" + key + "]", - join_as_string( - value.get>(), ","))); - else - params.insert(std::make_pair( - "filter[" + key + "]", value.get())); - } - } - - /* - curl -X GET - https://yoursite.shotgunstudio.com/api/v1/entity/assets?filter[sg_status_list]=ip&filter[sg_asset_type]=Prop,Character,Environment&filter[shots.Shot.code]=bunny_010_0020 - \ - */ - // requires authentication.. - - request( - http_, - infinite, - http_get_atom_v, - base_.scheme_host_port(), - std::string("/api/v1/entity/" + entity), - base_.get_auth_headers(), - params) - .then( - [=](const httplib::Response &response) mutable { - try { - auto jsn = nlohmann::json::parse(response.body); - - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std::pair< - std::string, - std::string>) mutable { - rp.delegate( - actor_cast(this), - shotgun_entity_filter_atom_v, - entity, - filter, - fields, - sort, - page, - page_size); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(JsonStore(std::move(jsn))); - }); - return; - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - rp.deliver(JsonStore(std::move(jsn))); - } catch (const std::exception &err) { - rp.deliver(make_error( - sce::response_error, err.what() + response.body)); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - } catch (const std::exception &err) { - return make_error(sce::args_error, err.what()); - } - } + authenticate(rp, [=]() { + request_entity_filter(entity, filter, fields, sort, page, page_size, rp); + }); return rp; }, - [=](shotgun_entity_search_atom atom, + [=](shotgun_entity_search_atom, const std::string &entity, const JsonStore &conditions, const std::vector &fields) { - delegate( - actor_cast(this), - atom, - entity, - conditions, - fields, - std::vector(), - 1, - 1000); + auto rp = make_response_promise(); + authenticate(rp, [=]() { + request_entity_search( + entity, conditions, fields, std::vector(), 1, 1000, rp); + }); + return rp; }, [=](shotgun_entity_search_atom, @@ -658,94 +203,9 @@ void ShotgunClientActor::init() { const int page, const int page_size) -> result { auto rp = make_response_promise(); - // spdlog::warn("shotgun_entity_search_atom"); - - if (not base_.authenticated()) { - request(actor_cast(this), infinite, shotgun_acquire_token_atom_v) - .then( - [=](const std::pair) mutable { - rp.delegate( - actor_cast(this), - shotgun_entity_search_atom_v, - entity, - conditions, - fields, - sort, - page, - page_size); - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - } else { - auto jsn = R"({"page":{}})"_json; - jsn["page"]["size"] = page_size; - jsn["page"]["number"] = page; - jsn["fields"] = fields; - if (not sort.empty()) - jsn["sort"] = sort; - jsn["filters"] = conditions; - - // requires authentication.. - request( - http_, - infinite, - http_post_atom_v, - base_.scheme_host_port(), - std::string("/api/v1/entity/" + entity + "/_search"), - base_.get_auth_headers(), - jsn.dump(), - base_.content_type_hash()) - .then( - [=](const httplib::Response &response) mutable { - try { - // validate / authentication error. - auto jsn = nlohmann::json::parse(response.body); - - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std::pair< - std::string, - std::string>) mutable { - rp.delegate( - actor_cast(this), - shotgun_entity_search_atom_v, - entity, - conditions, - fields, - sort, - page, - page_size); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(JsonStore(std::move(jsn))); - }); - return; - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - rp.deliver(JsonStore(std::move(jsn))); - - } catch (const std::exception &err) { - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - } + authenticate(rp, [=]() { + request_entity_search(entity, conditions, fields, sort, page, page_size, rp); + }); return rp; }, @@ -761,386 +221,154 @@ void ShotgunClientActor::init() { [=](shotgun_info_atom) -> result { auto rp = make_response_promise(); - // spdlog::warn("shotgun_info_atom"); - request( - http_, - infinite, - http_get_simple_atom_v, - base_.scheme_host_port(), - "/api/v1/", - base_.get_headers()) - .then( - [=](const std::string &body) mutable { - try { - rp.deliver(JsonStore(nlohmann::json::parse(body))); - } catch (const std::exception &err) { - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - + request_info(rp); return rp; }, [=](shotgun_preferences_atom) -> result { auto rp = make_response_promise(); - // spdlog::warn("shotgun_preferences_atom"); - - // requires authentication.. - request( - http_, - infinite, - http_get_atom_v, - base_.scheme_host_port(), - "/api/v1/preferences", - base_.get_auth_headers()) - .then( - [=](const httplib::Response &response) mutable { - try { - auto jsn = nlohmann::json::parse(response.body); - - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std:: - pair) mutable { - rp.delegate( - actor_cast(this), - shotgun_preferences_atom_v); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(JsonStore(std::move(jsn))); - }); - return; - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - rp.deliver(JsonStore(std::move(jsn))); - - } catch (const std::exception &err) { - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - + authenticate(rp, [=]() { request_preference(rp); }); return rp; }, - [=](shotgun_groups_atom, const int project_id) { + [=](shotgun_groups_atom, const int project_id) -> result { + auto rp = make_response_promise(); + auto filter = FilterBy().And( StatusList("sg_status").is_not("Archive"), RelationType("sg_project").is(R"({"type": "Project", "id": 0})"_json)); auto jfilter = JsonStore(static_cast(filter)); jfilter["conditions"][1][2]["id"] = project_id; + authenticate(rp, [=]() { + request_entity_search( + "groups", + jfilter, + std::vector({"code", "id"}), + std::vector({"code"}), + 1, + 4999, + rp); + }); + + return rp; + }, + + [=](shotgun_projects_atom, + const std::vector &fields, + const std::vector &sort) -> result { + auto rp = make_response_promise(); + + auto filter = FilterBy().And( + StatusList("sg_status").is_not("Archive"), Text("sg_type").is_not("Template")); - delegate( - actor_cast(this), - shotgun_entity_search_atom_v, - "groups", - jfilter, - std::vector({"code", "id"}), - std::vector({"code"}), - 1, - 4999); + authenticate(rp, [=]() { + request_entity_search( + "projects", + JsonStore(static_cast(filter)), + fields, + sort, + 1, + 4999, + rp); + }); + return rp; }, [=](shotgun_projects_atom) { + auto rp = make_response_promise(); + auto filter = FilterBy().And( StatusList("sg_status").is_not("Archive"), Text("sg_type").is_not("Template")); - delegate( - actor_cast(this), - shotgun_entity_search_atom_v, - "projects", - JsonStore(static_cast(filter)), - std::vector({"name"}), - std::vector({"name"}), - 1, - 4999); + + authenticate(rp, [=]() { + request_entity_search( + "projects", + JsonStore(static_cast(filter)), + std::vector({"name"}), + std::vector({"name"}), + 1, + 4999, + rp); + }); + return rp; }, - [=](shotgun_refresh_token_atom atom) { - delegate(actor_cast(this), atom, false); + [=](shotgun_refresh_token_atom) -> result> { + auto rp = make_response_promise>(); + refresh_token(true, rp); + return rp; }, [=](shotgun_refresh_token_atom, bool force) -> result> { - // check we need one.. - if (not base_.token().empty() and not force and - base_.token_lifetime() > std::chrono::seconds(30)) { - return std::make_pair(base_.token(), base_.refresh_token()); - } - auto rp = make_response_promise>(); - // spdlog::warn("REFRESH TOKEN"); - request( - http_, - infinite, - http_post_simple_atom_v, - base_.scheme_host_port(), - "/api/v1/auth/access_token", - base_.get_headers(), - base_.get_auth_refresh_params()) - .then( - [=](const std::string &body) mutable { - try { - auto j = nlohmann::json::parse(body); - // should have valid token - base_.set_token( - j["token_type"], - j["access_token"], - j["refresh_token"], - j["expires_in"]); - // auto refresh - // delayed_anon_send( - // actor_cast(this), - // std::chrono::seconds(1), - // shotgun_acquire_token_atom_v - // ); - // spdlog::error("REFRESH TOKEN SUCCESS"); - send( - event_group_, - utility::event_atom_v, - shotgun_acquire_token_atom_v, - std::make_pair(base_.token(), base_.refresh_token())); - rp.deliver(std::make_pair(base_.token(), base_.refresh_token())); - } catch (const std::exception &err) { - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + refresh_token(force, rp); return rp; }, - [=](shotgun_schema_atom atom) { delegate(actor_cast(this), atom, -1); }, + [=](shotgun_schema_atom) -> result { + auto rp = make_response_promise(); + authenticate(rp, [=]() { request_schema(-1, rp); }); + return rp; + }, [=](shotgun_schema_atom, const int id) -> result { auto rp = make_response_promise(); - // requires authentication.. - // spdlog::warn("shotgun_schema_atom"); - httplib::Params params; - if (id != -1) - params.insert(std::make_pair("project_id", std::to_string(id))); - - request( - http_, - infinite, - http_get_atom_v, - base_.scheme_host_port(), - "/api/v1/schema", - base_.get_auth_headers(), - params) - .then( - [=](const httplib::Response &response) mutable { - try { - auto jsn = nlohmann::json::parse(response.body); - - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std:: - pair) mutable { - rp.delegate( - actor_cast(this), - shotgun_schema_atom_v, - id); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(JsonStore(std::move(jsn))); - }); - return; - } - - } catch (const std::exception &err) { - spdlog::warn( - "{} {} {}", __PRETTY_FUNCTION__, err.what(), jsn.dump(2)); - } - rp.deliver(JsonStore(std::move(jsn))); - } catch (const std::exception &err) { - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - + authenticate(rp, [=]() { request_schema(id, rp); }); return rp; }, - [=](shotgun_schema_entity_atom atom, const std::string &entity) { - delegate(actor_cast(this), atom, entity, -1); + [=](shotgun_schema_entity_atom, const std::string &entity) -> result { + auto rp = make_response_promise(); + authenticate(rp, [=]() { request_schema_entity(entity, -1, rp); }); + return rp; }, [=](shotgun_schema_entity_atom, const std::string &entity, const int id) -> result { - // spdlog::warn("shotgun_schema_entity_atom"); auto rp = make_response_promise(); - - httplib::Params params; - if (id != -1) - params.insert(std::make_pair("project_id", std::to_string(id))); - - // requires authentication.. - request( - http_, - infinite, - http_get_atom_v, - base_.scheme_host_port(), - "/api/v1/schema/" + entity, - base_.get_auth_headers(), - params) - .then( - [=](const httplib::Response &response) mutable { - try { - auto jsn = nlohmann::json::parse(response.body); - - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std:: - pair) mutable { - rp.delegate( - actor_cast(this), - shotgun_schema_entity_atom_v, - id); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(JsonStore(std::move(jsn))); - }); - return; - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - rp.deliver(JsonStore(std::move(jsn))); - } catch (const std::exception &err) { - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - + authenticate(rp, [=]() { request_schema_entity(entity, id, rp); }); return rp; }, - [=](shotgun_schema_entity_fields_atom atom, const std::string &entity) { - delegate(actor_cast(this), atom, entity, "", -1); + [=](shotgun_schema_entity_fields_atom, const std::string &entity) -> result { + auto rp = make_response_promise(); + authenticate(rp, [=]() { request_schema_entity_fields(entity, "", -1, rp); }); + return rp; }, - [=](shotgun_schema_entity_fields_atom atom, + [=](shotgun_schema_entity_fields_atom, const std::string &entity, - const std::string &field) { - delegate(actor_cast(this), atom, entity, field, -1); + const std::string &field) -> result { + auto rp = make_response_promise(); + authenticate(rp, [=]() { request_schema_entity_fields(entity, field, -1, rp); }); + return rp; }, - [=](shotgun_schema_entity_fields_atom atom, const std::string &entity, const int id) { - delegate(actor_cast(this), atom, entity, "", id); + [=](shotgun_schema_entity_fields_atom, + const std::string &entity, + const int id) -> result { + auto rp = make_response_promise(); + authenticate(rp, [=]() { request_schema_entity_fields(entity, "", id, rp); }); + return rp; }, [=](shotgun_schema_entity_fields_atom, const std::string &entity, const std::string &field, const int id) -> result { - // spdlog::warn("shotgun_schema_entity_fields_atom"); auto rp = make_response_promise(); - - httplib::Params params; - if (id != -1) - params.insert(std::make_pair("project_id", std::to_string(id))); - - auto path = std::string("/api/v1/schema/" + entity + "/fields"); - if (not field.empty()) - path += "/" + field; - - // requires authentication.. - request( - http_, - infinite, - http_get_atom_v, - base_.scheme_host_port(), - path, - base_.get_auth_headers(), - params) - .then( - [=](const httplib::Response &response) mutable { - try { - auto jsn = nlohmann::json::parse(response.body); - - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std:: - pair) mutable { - rp.delegate( - actor_cast(this), - shotgun_schema_entity_fields_atom_v, - entity, - field, - id); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(JsonStore(std::move(jsn))); - }); - return; - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - rp.deliver(JsonStore(std::move(jsn))); - } catch (const std::exception &err) { - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - + authenticate(rp, [=]() { request_schema_entity_fields(entity, field, id, rp); }); return rp; }, - [=](shotgun_text_search_atom atom, + [=](shotgun_text_search_atom, const std::string &text, - const JsonStore &conditions) { - delegate(actor_cast(this), atom, text, conditions, 1, 49); + const JsonStore &conditions) -> result { + auto rp = make_response_promise(); + authenticate(rp, [=]() { request_text_search(text, conditions, 1, 49, rp); }); + return rp; }, [=](shotgun_text_search_atom, @@ -1148,68 +376,9 @@ void ShotgunClientActor::init() { const JsonStore &conditions, const int page, const int page_size) -> result { - // spdlog::warn("shotgun_text_search_atom"); - auto rp = make_response_promise(); - auto jsn = R"({"page":{},"entity_types":{}})"_json; - jsn["text"] = text; - jsn["page"]["size"] = page_size; - jsn["page"]["number"] = page; - jsn["entity_types"] = conditions; - - // requires authentication.. - request( - http_, - infinite, - http_post_atom_v, - base_.scheme_host_port(), - std::string("/api/v1/entity/_text_search"), - base_.get_auth_headers(), - jsn.dump(), - base_.content_type_hash()) - .then( - [=](const httplib::Response &response) mutable { - try { - auto jsn = nlohmann::json::parse(response.body); - - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std:: - pair) mutable { - rp.delegate( - actor_cast(this), - shotgun_text_search_atom_v, - text, - conditions, - page, - page_size); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(JsonStore(std::move(jsn))); - }); - return; - } - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - rp.deliver(JsonStore(std::move(jsn))); - } catch (const std::exception &err) { - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - + auto rp = make_response_promise(); + authenticate( + rp, [=]() { request_text_search(text, conditions, page, page_size, rp); }); return rp; }, @@ -1220,66 +389,7 @@ void ShotgunClientActor::init() { const std::string &field, const std::string &name) -> result { auto rp = make_response_promise(); - - httplib::Params params({{"filename", name}}); - auto url = std::string(fmt::format("/api/v1/entity/{}/{}/", entity, record_id)); - if (not field.empty()) - url += field + "/"; - url += "_upload"; - - request( - http_, - infinite, - http_get_atom_v, - base_.scheme_host_port(), - url, - base_.get_auth_headers(), - params) - .then( - [=](const httplib::Response &response) mutable { - try { - auto jsn = nlohmann::json::parse(response.body); - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std:: - pair) mutable { - rp.delegate( - actor_cast(this), - shotgun_upload_atom_v, - entity, - record_id, - field, - name); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(JsonStore(std::move(jsn))); - }); - return; - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - rp.deliver(JsonStore(std::move(jsn))); - } catch (const std::exception &err) { - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](const error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(err); - }); - + upload_start(entity, record_id, field, name, rp); return rp; }, @@ -1288,41 +398,8 @@ void ShotgunClientActor::init() { const std::string &url, const std::string &content_type, const std::vector &data) -> result { - auto rp = make_response_promise(); - auto uri = caf::make_uri(url); - if (uri) { - auto host_port = to_string(*(uri->authority_only())); - auto path = url.substr(host_port.size()); - auto headers = httplib::Headers({{"Content-Type", content_type}}); - - request( - http_, - infinite, - http_put_atom_v, - host_port, - path, - headers, - std::string(reinterpret_cast(data.data()), data.size()), - content_type) - .then( - [=](const httplib::Response &response) mutable { - try { - auto jsn = nlohmann::json::parse(response.body); - if (response.status != 200) { - return rp.deliver( - make_error(sce::response_error, jsn.dump(2))); - } - rp.deliver(jsn); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - } else { - rp.deliver(make_error(sce::response_error, "Invalid uri")); - } - + auto rp = make_response_promise(); + upload_transfer(url, content_type, data, rp); return rp; }, @@ -1331,43 +408,7 @@ void ShotgunClientActor::init() { const std::string &path, const JsonStore &info) -> result { auto rp = make_response_promise(); - - // spdlog::warn("{}", info.dump(2)); - - request( - http_, - infinite, - http_post_atom_v, - base_.scheme_host_port(), - path, - base_.get_auth_headers(), - info.dump(2), - "application/json") - .then( - [=](const httplib::Response &response) mutable { - try { - if (response.status != 201) { - spdlog::warn( - "{} {} {}", - __PRETTY_FUNCTION__, - response.status, - response.body); - return rp.deliver( - make_error(sce::response_error, response.body)); - } - auto jsn = JsonStore(R"({"status": "Success", "body": ""})"_json); - jsn["body"] = response.body; - rp.deliver(jsn); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(std::move(err)); - }); - + upload_finish(path, info, rp); return rp; }, @@ -1380,63 +421,7 @@ void ShotgunClientActor::init() { const std::string &content_type) -> result { // request upload url. auto rp = make_response_promise(); - request( - caf::actor_cast(this), - infinite, - shotgun_upload_atom_v, - entity, - record_id, - field, - name) - .then( - [=](const JsonStore &upload_req) mutable { - // should have destination for upload and completion url. - auto upload_link = - upload_req.at("links").at("upload").get(); - // trim prefix.. - // upload.. - // spdlog::warn("{}", upload_req.dump(2)); - request( - caf::actor_cast(this), - infinite, - shotgun_upload_atom_v, - upload_link, - content_type, - data) - .then( - [=](const JsonStore &upload_resp) mutable { - // if good... - // spdlog::warn("{}", upload_resp.dump(2)); - - auto info = JsonStore(); - info["upload_info"] = upload_req.at("data"); - info["upload_data"]["display_name"] = name; - if (upload_req["data"]["storage_service"] == "sg") - info["upload_info"]["upload_id"] = - upload_resp["data"]["upload_id"]; - - // finalise upload.. - request( - caf::actor_cast(this), - infinite, - shotgun_upload_atom_v, - upload_resp.at("links") - .at("complete_upload") - .get(), - info) - .then( - [=](const JsonStore &final) mutable { - // spdlog::warn("{}", final.dump(2)); - rp.deliver(true); - }, - [=](error &err) mutable { - rp.deliver(std::move(err)); - }); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - + upload(entity, record_id, field, name, data, content_type, rp); return rp; }, @@ -1445,67 +430,7 @@ void ShotgunClientActor::init() { const int id, const std::string &property) -> result { auto rp = make_response_promise(); - - request( - http_, - infinite, - http_get_atom_v, - base_.scheme_host_port(), - std::string( - fmt::format("/api/v1/entity/{}/{}/{}?alt=original", entity, id, property)), - base_.get_auth_headers()) - // base_.get_auth_headers("video/webm")) - .then( - [=](const httplib::Response &response) mutable { - if (response.status == 200) { - return rp.deliver(response.body); - } - - try { - auto jsn = nlohmann::json::parse(response.body); - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std:: - pair) mutable { - rp.delegate( - actor_cast(this), - shotgun_attachment_atom_v, - entity, - id, - property); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(std::move(err)); - }); - } else { - // missing thumbnail - rp.deliver(make_error(sce::response_error, response.body)); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(sce::response_error, err.what())); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(sce::response_error, err.what())); - } - }, - [=](error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - rp.deliver(std::move(err)); - }); - + authenticate(rp, [=]() { request_attachment(entity, id, property, rp); }); return rp; }, @@ -1514,164 +439,873 @@ void ShotgunClientActor::init() { const int record_id, const bool thumbnail) -> result { auto rp = make_response_promise(); + authenticate(rp, [=]() { request_image(entity, record_id, thumbnail, rp); }); + return rp; + }, - request( - http_, - infinite, - http_get_atom_v, - base_.scheme_host_port(), - std::string(fmt::format( - "/api/v1/entity/{}/{}/image?alt={}", - entity, - record_id, - (thumbnail ? "thumbnail" : "original"))), - base_.get_auth_headers()) + [=](shotgun_image_atom, + const std::string &entity, + const int record_id, + const bool thumbnail, + const bool as_buffer) -> result { + auto rp = make_response_promise(); + authenticate(rp, [=]() { + request_image_buffer(entity, record_id, thumbnail, as_buffer, rp); + }); + return rp; + }); +} + +void ShotgunClientActor::acquire_token_primary( + caf::typed_response_promise> rp) { + if (not base_.refresh_token().empty()) { + // spdlog::warn("not base_.refresh_token().empty()"); + if (request_refresh_queue_.empty()) { + request_refresh_queue_.push(rp); + mail(shotgun_refresh_token_atom_v) + .request(actor_cast(this), infinite) .then( - [=](const httplib::Response &response) mutable { - if (response.status == 200) - return rp.deliver(response.body); - - try { - auto jsn = nlohmann::json::parse(response.body); - try { - if (not jsn["errors"][0]["status"].is_null() and - jsn["errors"][0]["status"].get() == 401) { - // try and authorise.. - request( - actor_cast(this), - infinite, - shotgun_acquire_token_atom_v) - .then( - [=](const std:: - pair) mutable { - rp.delegate( - actor_cast(this), - shotgun_image_atom_v, - entity, - record_id, - thumbnail); - }, - [=](error &err) mutable { - spdlog::warn( - "{} {}", - __PRETTY_FUNCTION__, - to_string(err)); - rp.deliver(std::move(err)); - }); - } else { - // missing thumbnail - rp.deliver(make_error(sce::response_error, response.body)); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(sce::response_error, err.what())); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - rp.deliver(make_error(sce::response_error, err.what())); + [=](const std::pair &tokens) mutable { + // spdlog::info("refresh from token"); + base_.set_authenticated(true); + while (not request_refresh_queue_.empty()) { + request_refresh_queue_.front().deliver(tokens); + request_refresh_queue_.pop(); + } + }, + [=](error &) mutable { + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + // try using login + base_.expire_refresh_token(); + while (not request_refresh_queue_.empty()) { + request_refresh_queue_.front().delegate( + actor_cast(this), shotgun_acquire_token_atom_v); + request_refresh_queue_.pop(); } + }); + } else { + request_refresh_queue_.push(rp); + } + } else { + if (not base_.failed_authentication()) { + acquire_token(rp); + } else { + // spdlog::info("refresh from login"); + // request secret. + mail( + shotgun_acquire_authentication_atom_v, + base_.failed_authentication() ? "Authentication failed, try again." : "") + .request(actor_cast(secret_source_), infinite) + .then( + [=](const AuthenticateShotgun &auth) mutable { + base_.set_credentials_method(auth); + acquire_token_primary(rp); }, [=](error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + base_.set_authenticated(false); rp.deliver(std::move(err)); }); - - return rp; } - - //, - // TODO: Ahead Fix - // [=](shotgun_image_atom, - // const std::string &entity, - // const int record_id, - // const bool thumbnail, - // const bool as_buffer) -> result { - // auto rp = make_response_promise(); - - // request( - // actor_cast(this), - // infinite, - // shotgun_image_atom_v, - // entity, - // record_id, - // thumbnail) - // .then( - // [=](const std::string &data) mutable { - // // request conversion.. - // auto thumbgen = system().registry().template get( - // thumbnail_manager_registry); - // if (thumbgen) { - // std::vector bytedata(data.size()); - // std::memcpy(bytedata.data(), data.data(), data.size()); - // //rp.delegate(thumbgen, media_reader::get_thumbnail_atom_v, - // bytedata); - // } else { - // rp.deliver(make_error( - // sce::response_error, "Thumbnail manager not available")); - // } - // }, - // [=](error &err) mutable { rp.deliver(std::move(err)); }); - - // return rp; - // } - ); + } } void ShotgunClientActor::acquire_token( caf::typed_response_promise> rp) { - request_acquire_queue_.push(rp); - if (request_acquire_queue_.size() > 1) - return; - - // spdlog::warn("acquire_token"); - request( - http_, - infinite, - http_post_simple_atom_v, + + if (request_acquire_queue_.empty()) { + request_acquire_queue_.push(rp); + + // spdlog::error("REQUEST ACCESS TOKEN"); + mail( + http_post_simple_atom_v, + base_.scheme_host_port(), + "/api/v1/auth/access_token", + base_.get_headers(), + base_.get_auth_request_params()) + .request(http_, infinite) + .then( + [=](const std::string &body) mutable { + try { + auto j = nlohmann::json::parse(body); + // should have valid token + base_.set_token( + j["token_type"], + j["access_token"], + j["refresh_token"], + j["expires_in"]); + base_.set_authenticated(true); + mail( + utility::event_atom_v, + shotgun_acquire_token_atom_v, + std::make_pair(base_.token(), base_.refresh_token())) + .send(event_group_); + + while (not request_acquire_queue_.empty()) { + request_acquire_queue_.front().deliver( + std::make_pair(base_.token(), base_.refresh_token())); + request_acquire_queue_.pop(); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + // spurious result ? + while (not request_acquire_queue_.empty()) { + request_acquire_queue_.front().deliver( + make_error(sce::response_error, err.what())); + request_acquire_queue_.pop(); + } + base_.set_authenticated(false); + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + base_.set_failed_authentication(); + base_.set_authenticated(false); + while (not request_acquire_queue_.empty()) { + request_acquire_queue_.front().delegate( + actor_cast(this), shotgun_acquire_token_atom_v); + request_acquire_queue_.pop(); + } + }); + } else { + // spdlog::warn("request access token, add to queue"); + request_acquire_queue_.push(rp); + } +} + +void ShotgunClientActor::refresh_token( + const bool force, caf::typed_response_promise> rp) { + // check we need one.. + if (not base_.token().empty() and not force and + base_.token_lifetime() > std::chrono::seconds(30)) { + // spdlog::warn("REUSE TOKEN {} {}", base_.token(), base_.refresh_token()); + rp.deliver(std::make_pair(base_.token(), base_.refresh_token())); + } else { + // spdlog::error("REFRESH TOKEN"); + mail( + http_post_simple_atom_v, + base_.scheme_host_port(), + "/api/v1/auth/access_token", + base_.get_headers(), + base_.get_auth_refresh_params()) + .request(http_, infinite) + .then( + [=](const std::string &body) mutable { + try { + auto j = nlohmann::json::parse(body); + // should have valid token + base_.set_token( + j["token_type"], + j["access_token"], + j["refresh_token"], + j["expires_in"]); + mail( + utility::event_atom_v, + shotgun_acquire_token_atom_v, + std::make_pair(base_.token(), base_.refresh_token())) + .send(event_group_); + rp.deliver(std::make_pair(base_.token(), base_.refresh_token())); + } catch (const std::exception &err) { + // spdlog::error("FAILED REFRESH"); + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + } +} + +void ShotgunClientActor::request_image( + const std::string &entity, + const int record_id, + const bool thumbnail, + caf::typed_response_promise rp) { + + mail( + http_get_atom_v, + base_.scheme_host_port(), + std::string(fmt::format( + "/api/v1/entity/{}/{}/image?alt={}", + entity, + record_id, + (thumbnail ? "thumbnail" : "original"))), + base_.get_auth_headers()) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + if (response.status == 200) + return rp.deliver(response.body); + + try { + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate(jsn, rp, [=]() { + request_image(entity, record_id, thumbnail, rp); + })) { + // missing thumbnail + rp.deliver(make_error(sce::response_error, response.body)); + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(std::move(err)); + }); +} + +void ShotgunClientActor::request_attachment( + const std::string &entity, + const int record_id, + const std::string &property, + caf::typed_response_promise rp) { + + std::string path; + + if (property.empty()) + path = std::string(fmt::format("/api/v1/entity/{}/{}?alt=original", entity, record_id)); + else + path = std::string( + fmt::format("/api/v1/entity/{}/{}/{}?alt=original", entity, record_id, property)), + + mail(http_get_atom_v, base_.scheme_host_port(), path, base_.get_auth_headers()) + // base_.get_auth_headers("video/webm")) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + if (response.status == 200) { + return rp.deliver(response.body); + } + + try { + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate(jsn, rp, [=]() { + request_attachment(entity, record_id, property, rp); + })) { + // missing thumbnail + rp.deliver(make_error(sce::response_error, response.body)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(std::move(err)); + }); +} + +void ShotgunClientActor::request_text_search( + const std::string &text, + const utility::JsonStore &conditions, + const int page, + const int page_size, + caf::typed_response_promise rp) { + + auto jsn = R"({"page":{},"entity_types":{}})"_json; + jsn["text"] = text; + jsn["page"]["size"] = page_size; + jsn["page"]["number"] = page; + jsn["entity_types"] = conditions; + + // requires authentication.. + mail( + http_post_atom_v, + base_.scheme_host_port(), + std::string("/api/v1/entity/_text_search"), + base_.get_auth_headers(), + jsn.dump(), + base_.content_type_hash()) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate(jsn, rp, [=]() { + request_text_search(text, conditions, page, page_size, rp); + })) { + rp.deliver(JsonStore(std::move(jsn))); + } + } catch (const std::exception &err) { + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} + +void ShotgunClientActor::request_schema_entity_fields( + const std::string &entity, + const std::string &field, + const int id, + caf::typed_response_promise rp) { + + httplib::Params params; + if (id != -1) + params.insert(std::make_pair("project_id", std::to_string(id))); + + auto path = std::string("/api/v1/schema/" + entity + "/fields"); + if (not field.empty()) + path += "/" + field; + + // requires authentication.. + mail(http_get_atom_v, base_.scheme_host_port(), path, base_.get_auth_headers(), params) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate(jsn, rp, [=]() { + request_schema_entity_fields(entity, field, id, rp); + })) { + rp.deliver(JsonStore(std::move(jsn))); + } + } catch (const std::exception &err) { + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} + +void ShotgunClientActor::request_entity_filter( + const std::string &entity, + const JsonStore &filter, + const std::vector &fields, + const std::vector &sort, + const int page, + const int page_size, + caf::typed_response_promise rp) { + + try { + httplib::Params params; + + if (page) { + params.insert(std::make_pair("number", std::to_string(page))); + params.insert(std::make_pair("size", std::to_string(page_size))); + } + + if (fields.empty()) { + params.insert(std::make_pair("fields", "*")); + } else { + params.insert(std::make_pair("fields", join_as_string(fields, ","))); + } + + if (not sort.empty()) { + params.insert(std::make_pair("sort", join_as_string(sort, ","))); + } + + if (not filter.empty()) { + // should be an array.. + for (const auto &[key, value] : filter.items()) { + if (value.is_array()) + params.insert(std::make_pair( + "filter[" + key + "]", + join_as_string(value.get>(), ","))); + else + params.insert( + std::make_pair("filter[" + key + "]", value.get())); + } + } + + /* + curl -X GET + https://yoursite.shotgunstudio.com/api/v1/entity/assets?filter[sg_status_list]=ip&filter[sg_asset_type]=Prop,Character,Environment&filter[shots.Shot.code]=bunny_010_0020 + \ + */ + // requires authentication.. + + mail( + http_get_atom_v, + base_.scheme_host_port(), + std::string("/api/v1/entity/" + entity), + base_.get_auth_headers(), + params) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate(jsn, rp, [=]() { + request_entity_filter( + entity, filter, fields, sort, page, page_size, rp); + })) { + rp.deliver(JsonStore(std::move(jsn))); + } + } catch (const std::exception &err) { + rp.deliver(make_error(sce::response_error, err.what() + response.body)); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + } catch (const std::exception &err) { + rp.deliver(make_error(sce::args_error, err.what())); + } +} + +void ShotgunClientActor::request_entity( + const std::string &entity, + const int record_id, + const std::vector &fields, + caf::typed_response_promise rp) { + + mail( + http_get_atom_v, + base_.scheme_host_port(), + std::string("/api/v1/entity/" + entity + "/" + std::to_string(record_id)), + base_.get_auth_headers(), + httplib::Params({{"fields", fields.empty() ? "*" : join_as_string(fields, ",")}})) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate(jsn, rp, [=]() { + request_entity(entity, record_id, fields, rp); + })) { + rp.deliver(JsonStore(std::move(jsn))); + } + } catch (const std::exception &err) { + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} + +void ShotgunClientActor::request_entity_search( + const std::string &entity, + const JsonStore &conditions, + const std::vector &fields, + const std::vector &sort, + const int page, + const int page_size, + caf::typed_response_promise rp) { + + auto jsn = R"({"page":{}})"_json; + jsn["page"]["size"] = page_size; + jsn["page"]["number"] = page; + jsn["fields"] = fields; + if (not sort.empty()) + jsn["sort"] = sort; + jsn["filters"] = conditions; + + // requires authentication.. + mail( + http_post_atom_v, base_.scheme_host_port(), - "/api/v1/auth/access_token", - base_.get_headers(), - base_.get_auth_request_params()) + std::string("/api/v1/entity/" + entity + "/_search"), + base_.get_auth_headers(), + jsn.dump(), + base_.content_type_hash()) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + // validate / authentication error. + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate(jsn, rp, [=]() { + request_entity_search( + entity, conditions, fields, sort, page, page_size, rp); + })) { + rp.deliver(JsonStore(std::move(jsn))); + } + + } catch (const std::exception &err) { + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} + +void ShotgunClientActor::request_schema_entity( + const std::string &entity, + const int id, + caf::typed_response_promise rp) { + + httplib::Params params; + if (id != -1) + params.insert(std::make_pair("project_id", std::to_string(id))); + + // requires authentication.. + mail( + http_get_atom_v, + base_.scheme_host_port(), + "/api/v1/schema/" + entity, + base_.get_auth_headers(), + params) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate( + jsn, rp, [=]() { request_schema_entity(entity, id, rp); })) { + rp.deliver(JsonStore(std::move(jsn))); + } + } catch (const std::exception &err) { + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} + +void ShotgunClientActor::request_schema( + const int id, caf::typed_response_promise rp) { + + // requires authentication.. + // spdlog::warn("shotgun_schema_atom"); + httplib::Params params; + if (id != -1) + params.insert(std::make_pair("project_id", std::to_string(id))); + + mail( + http_get_atom_v, + base_.scheme_host_port(), + "/api/v1/schema", + base_.get_auth_headers(), + params) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate( + jsn, rp, [=]() { request_schema(id, rp); })) { + rp.deliver(JsonStore(std::move(jsn))); + } + } catch (const std::exception &err) { + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} + +void ShotgunClientActor::request_link( + const std::string &link, caf::typed_response_promise rp) { + // spdlog::warn("shotgun_link_atom"); + mail(http_get_atom_v, base_.scheme_host_port(), link, base_.get_auth_headers()) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate( + jsn, rp, [=]() { request_link(link, rp); })) { + rp.deliver(JsonStore(std::move(jsn))); + } + } catch (const std::exception &err) { + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} + +void ShotgunClientActor::update_entity( + const std::string &entity, + const int record_id, + const JsonStore &body, + const std::vector &fields, + caf::typed_response_promise rp) { + + mail( + http_put_atom_v, + base_.scheme_host_port(), + std::string("/api/v1/entity/" + entity + "/" + std::to_string(record_id)), + base_.get_auth_headers(), + body.dump(), + httplib::Params( + {{"options[fields]", fields.empty() ? "*" : join_as_string(fields, ",")}}), + base_.content_type_json()) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate(jsn, rp, [=]() { + update_entity(entity, record_id, body, fields, rp); + })) { + rp.deliver(JsonStore(std::move(jsn))); + } + } catch (const std::exception &err) { + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} + +void ShotgunClientActor::delete_entity( + const std::string &entity, + const int record_id, + caf::typed_response_promise rp) { + mail( + http_delete_atom_v, + base_.scheme_host_port(), + std::string("/api/v1/entity/" + entity + "/") + std::to_string(record_id), + base_.get_auth_headers(), + "", + base_.content_type_json()) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + if (response.body == "") + rp.deliver(JsonStore()); + else { + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate( + jsn, rp, [=]() { delete_entity(entity, record_id, rp); })) { + rp.deliver(JsonStore(std::move(jsn))); + } + } + } catch (const std::exception &err) { + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} + +void ShotgunClientActor::create_entity( + const std::string &entity, + const JsonStore &body, + caf::typed_response_promise rp) { + mail( + http_post_atom_v, + base_.scheme_host_port(), + std::string("/api/v1/entity/" + entity), + base_.get_auth_headers(), + body.dump(), + base_.content_type_json()) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate( + jsn, rp, [=]() { create_entity(entity, body, rp); })) { + rp.deliver(JsonStore(std::move(jsn))); + } + } catch (const std::exception &err) { + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} + +void ShotgunClientActor::request_info(caf::typed_response_promise rp) { + + // spdlog::warn("shotgun_info_atom"); + mail(http_get_simple_atom_v, base_.scheme_host_port(), "/api/v1/", base_.get_headers()) + .request(http_, infinite) .then( [=](const std::string &body) mutable { try { - auto j = nlohmann::json::parse(body); - // should have valid token - base_.set_token( - j["token_type"], - j["access_token"], - j["refresh_token"], - j["expires_in"]); - base_.set_authenticated(true); - send( - event_group_, - utility::event_atom_v, - shotgun_acquire_token_atom_v, - std::make_pair(base_.token(), base_.refresh_token())); + rp.deliver(JsonStore(nlohmann::json::parse(body))); + } catch (const std::exception &err) { + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} - while (not request_acquire_queue_.empty()) { - request_acquire_queue_.front().deliver( - std::make_pair(base_.token(), base_.refresh_token())); - request_acquire_queue_.pop(); +void ShotgunClientActor::request_preference( + caf::typed_response_promise rp) { + mail( + http_get_atom_v, + base_.scheme_host_port(), + "/api/v1/preferences", + base_.get_auth_headers()) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate( + jsn, rp, [=]() { request_preference(rp); })) { + rp.deliver(JsonStore(std::move(jsn))); } } catch (const std::exception &err) { - // spurious result ? - while (not request_acquire_queue_.empty()) { - request_acquire_queue_.front().deliver( - make_error(sce::response_error, err.what())); - request_acquire_queue_.pop(); + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} + +void ShotgunClientActor::upload_start( + const std::string &entity, + const int record_id, + const std::string &field, + const std::string &name, + caf::typed_response_promise rp) { + httplib::Params params({{"filename", name}}); + auto url = std::string(fmt::format("/api/v1/entity/{}/{}/", entity, record_id)); + if (not field.empty()) + url += field + "/"; + url += "_upload"; + + mail(http_get_atom_v, base_.scheme_host_port(), url, base_.get_auth_headers(), params) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + auto jsn = nlohmann::json::parse(response.body); + if (not check_failed_authenticate(jsn, rp, [=]() { + upload_start(entity, record_id, field, name, rp); + })) { + rp.deliver(JsonStore(std::move(jsn))); } - base_.set_authenticated(false); + } catch (const std::exception &err) { + rp.deliver(make_error(sce::response_error, err.what())); } }, - [=](error &err) mutable { + [=](const error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - base_.set_failed_authentication(); - base_.set_authenticated(false); - while (not request_acquire_queue_.empty()) { - request_acquire_queue_.front().delegate( - actor_cast(this), shotgun_acquire_token_atom_v); - request_acquire_queue_.pop(); + rp.deliver(err); + }); +} + + +void ShotgunClientActor::upload_transfer( + const std::string &url, + const std::string &content_type, + const std::vector &data, + caf::typed_response_promise rp) { + auto uri = caf::make_uri(url); + if (uri) { + auto host_port = to_string(*(uri->authority_only())); + auto path = url.substr(host_port.size()); + auto headers = httplib::Headers({{"Content-Type", content_type}}); + + mail( + http_put_atom_v, + host_port, + path, + headers, + std::string(reinterpret_cast(data.data()), data.size()), + content_type) + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + auto jsn = nlohmann::json::parse(response.body); + if (response.status != 200) { + rp.deliver(make_error(sce::response_error, jsn.dump(2))); + } else + rp.deliver(jsn); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(sce::response_error, err.what())); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + } else { + rp.deliver(make_error(sce::response_error, "Invalid uri")); + } +} +void ShotgunClientActor::upload_finish( + const std::string &path, + const JsonStore &info, + caf::typed_response_promise rp) { + mail( + http_post_atom_v, + base_.scheme_host_port(), + path, + base_.get_auth_headers(), + info.dump(2), + "application/json") + .request(http_, infinite) + .then( + [=](const httplib::Response &response) mutable { + try { + if (response.status != 201) { + spdlog::warn( + "{} {} {}", __PRETTY_FUNCTION__, response.status, response.body); + return rp.deliver(make_error(sce::response_error, response.body)); + } + auto jsn = JsonStore(R"({"status": "Success", "body": ""})"_json); + jsn["body"] = response.body; + rp.deliver(jsn); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + rp.deliver(make_error(sce::response_error, err.what())); } + }, + [=](error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(std::move(err)); }); } + +void ShotgunClientActor::upload( + const std::string &entity, + const int record_id, + const std::string &field, + const std::string &name, + const std::vector &data, + const std::string &content_type, + caf::typed_response_promise rp) { + mail(shotgun_upload_atom_v, entity, record_id, field, name) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &upload_req) mutable { + // should have destination for upload and completion url. + auto upload_link = upload_req.at("links").at("upload").get(); + // trim prefix.. + // upload.. + // spdlog::warn("{}", upload_req.dump(2)); + mail(shotgun_upload_atom_v, upload_link, content_type, data) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &upload_resp) mutable { + // if good... + // spdlog::warn("{}", upload_resp.dump(2)); + + auto info = JsonStore(); + info["upload_info"] = upload_req.at("data"); + info["upload_data"]["display_name"] = name; + if (upload_req["data"]["storage_service"] == "sg") + info["upload_info"]["upload_id"] = + upload_resp["data"]["upload_id"]; + + // finalise upload.. + mail( + shotgun_upload_atom_v, + upload_resp.at("links") + .at("complete_upload") + .get(), + info) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &final) mutable { + // spdlog::warn("{}", final.dump(2)); + rp.deliver(true); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} + +void ShotgunClientActor::request_image_buffer( + const std::string &entity, + const int record_id, + const bool thumbnail, + const bool as_buffer, + caf::typed_response_promise rp) { + mail(shotgun_image_atom_v, entity, record_id, thumbnail) + .request(actor_cast(this), infinite) + .then( + [=](const std::string &data) mutable { + // request conversion.. + auto thumbgen = + system().registry().template get(thumbnail_manager_registry); + if (thumbgen) { + std::vector bytedata(data.size()); + std::memcpy(bytedata.data(), data.data(), data.size()); + rp.delegate(thumbgen, media_reader::get_thumbnail_atom_v, bytedata); + } else { + rp.deliver( + make_error(sce::response_error, "Thumbnail manager not available")); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); +} diff --git a/src/shotgun_client/test/CMakeLists.txt b/src/shotgun_client/test/CMakeLists.txt index 3d621266b..c18cd059b 100644 --- a/src/shotgun_client/test/CMakeLists.txt +++ b/src/shotgun_client/test/CMakeLists.txt @@ -4,7 +4,7 @@ SET(LINK_DEPS xstudio::shotgun_client xstudio::media xstudio::utility - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/studio/src/CMakeLists.txt b/src/studio/src/CMakeLists.txt index fdb40b57f..9614db972 100644 --- a/src/studio/src/CMakeLists.txt +++ b/src/studio/src/CMakeLists.txt @@ -1,8 +1,7 @@ SET(LINK_DEPS xstudio::utility xstudio::session - xstudio::broadcast - caf::core + CAF::core ) create_component(studio ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/studio/src/studio_actor.cpp b/src/studio/src/studio_actor.cpp index 6a9487871..508734759 100644 --- a/src/studio/src/studio_actor.cpp +++ b/src/studio/src/studio_actor.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include #include "xstudio/atoms.hpp" @@ -28,43 +29,22 @@ StudioActor::StudioActor(caf::actor_config &cfg, const std::string &name) init(); } -void StudioActor::init() { - // launch global actors.. - // preferences first.. - // this will need more configuration - spdlog::debug("Created StudioActor {}", base_.name()); - - session_ = spawn("New Session"); - link_to(session_); - - system().registry().put(studio_registry, this); - - auto event_group_ = spawn(this); - link_to(event_group_); - - behavior_.assign( +caf::message_handler StudioActor::message_handler() { + return caf::message_handler{ [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), make_get_version_handler(), [=](session::session_atom) -> caf::actor { return session_; }, - [=](bookmark::get_bookmark_atom atom) { delegate(session_, atom); }, + [=](bookmark::get_bookmark_atom atom) { return mail(atom).delegate(session_); }, [=](session::session_atom, caf::actor session) -> bool { unlink_from(session_); send_exit(session_, caf::exit_reason::user_shutdown); session_ = session; link_to(session_); - send(event_group_, utility::event_atom_v, session::session_atom_v, session_); + mail(utility::event_atom_v, session::session_atom_v, session_) + .send(base_.event_group()); return true; }, @@ -72,8 +52,8 @@ void StudioActor::init() { const std::string &path, const JsonStore &js) -> bool { // need to chat to UI ? - send( - event_group_, utility::event_atom_v, session::session_request_atom_v, path, js); + mail(utility::event_atom_v, session::session_request_atom_v, path, js) + .send(base_.event_group()); return true; }, @@ -88,23 +68,24 @@ void StudioActor::init() { // Request (from somewhere) to open light viewers for list of media items. // Forward to UI via event group so UI can handle it. if (studio_ui_actor) { - anon_send( - studio_ui_actor, + anon_mail( utility::event_atom_v, ui::show_message_box_atom_v, message_title, message_body, close_button, - timeout_seconds); + timeout_seconds) + .send(studio_ui_actor); } }, - // [&](session::create_player_atom atom, const std::string &name) {// delegate(session_, - // atom, name);// }, + // [&](session::create_player_atom atom, const std::string &name) {// mail(// atom, + // name).delegate(session_);// }, [=](utility::serialise_atom) -> result { if (session_) { auto rp = make_response_promise(); - request(session_, caf::infinite, utility::serialise_atom_v) + mail(utility::serialise_atom_v) + .request(session_, caf::infinite) .then( [=](const JsonStore &_jsn) mutable { JsonStore jsn; @@ -125,42 +106,40 @@ void StudioActor::init() { [=](ui::open_quickview_window_atom atom, const utility::UuidActorVector &media_items, std::string compare_mode) { - delegate(actor_cast(this), atom, media_items, compare_mode, false); + return mail( + atom, + media_items, + compare_mode, + utility::JsonStore(), + utility::JsonStore()) + .delegate(actor_cast(this)); }, [=](ui::open_quickview_window_atom, const utility::UuidActorVector &media_items, std::string compare_mode, - bool force) { - bool do_quickview = force; - if (!do_quickview) { - try { - auto prefs = global_store::GlobalStoreHelper(system()); - do_quickview = - prefs.value("/core/session/quickview_all_incoming_media"); - } catch (...) { - } - } + const utility::JsonStore in_point, + const utility::JsonStore out_point) { + caf::actor studio_ui_actor = + system().registry().template get(studio_ui_registry); - if (do_quickview) { - - caf::actor studio_ui_actor = - system().registry().template get(studio_ui_registry); - - if (studio_ui_actor) { - // forward to StudioUI instance - anon_send( - studio_ui_actor, - utility::event_atom_v, - ui::open_quickview_window_atom_v, - media_items, - compare_mode); - } else { - // UI hasn't started up yet, store the request - QuickviewRequest request; - request.media_actors = media_items; - request.compare_mode = compare_mode; - quickview_requests_.push_back(request); - } + if (studio_ui_actor) { + // forward to StudioUI instance + anon_mail( + utility::event_atom_v, + ui::open_quickview_window_atom_v, + media_items, + compare_mode, + in_point, + out_point) + .send(studio_ui_actor); + } else { + // UI hasn't started up yet, store the request + QuickviewRequest request; + request.media_actors = media_items; + request.compare_mode = compare_mode; + request.in_point = in_point; + request.out_point = out_point; + quickview_requests_.push_back(request); } }, [=](ui::open_quickview_window_atom, caf::actor studio_ui_actor) { @@ -168,15 +147,30 @@ void StudioActor::init() { // so we can send it any pending requests for quickviewers for (const auto &r : quickview_requests_) { - anon_send( - studio_ui_actor, + anon_mail( utility::event_atom_v, ui::open_quickview_window_atom_v, r.media_actors, - r.compare_mode); + r.compare_mode, + r.in_point, + r.out_point) + .send(studio_ui_actor); } quickview_requests_.clear(); - }); + }}; +} + + +void StudioActor::init() { + // launch global actors.. + // preferences first.. + // this will need more configuration + spdlog::debug("Created StudioActor {}", base_.name()); + + session_ = spawn("New Session"); + link_to(session_); + + system().registry().put(studio_registry, this); } void StudioActor::on_exit() { diff --git a/src/studio/test/CMakeLists.txt b/src/studio/test/CMakeLists.txt index a73825679..66caa1970 100644 --- a/src/studio/test/CMakeLists.txt +++ b/src/studio/test/CMakeLists.txt @@ -1,7 +1,7 @@ include(CTest) SET(LINK_DEPS - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/subset/src/CMakeLists.txt b/src/subset/src/CMakeLists.txt index 98bae501f..26a58d219 100644 --- a/src/subset/src/CMakeLists.txt +++ b/src/subset/src/CMakeLists.txt @@ -2,7 +2,7 @@ SET(LINK_DEPS xstudio::playhead xstudio::utility xstudio::audio_output - caf::core + CAF::core ) -create_component(subset 0.1.0 "${LINK_DEPS}") +create_component(subset ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") diff --git a/src/subset/src/subset.cpp b/src/subset/src/subset.cpp index bc21018f4..24360b7ff 100644 --- a/src/subset/src/subset.cpp +++ b/src/subset/src/subset.cpp @@ -8,8 +8,8 @@ using namespace xstudio::subset; using namespace xstudio::utility; -Subset::Subset(const std::string &name, const std::string &type) - : Container(name, type), playhead_rate_(timebase::k_flicks_24fps) {} +Subset::Subset(const std::string &name, const std::string &type, const utility::Uuid &uuid) + : Container(name, type, uuid), playhead_rate_(timebase::k_flicks_24fps) {} Subset::Subset(const JsonStore &jsn) : Container(static_cast(jsn["container"])), diff --git a/src/subset/src/subset_actor.cpp b/src/subset/src/subset_actor.cpp index b8baac394..ad19700fb 100644 --- a/src/subset/src/subset_actor.cpp +++ b/src/subset/src/subset_actor.cpp @@ -23,158 +23,157 @@ SubsetActor::SubsetActor( playlist_(caf::actor_cast(playlist)), base_(static_cast(jsn["base"])) { - anon_send(this, playhead::source_atom_v, playlist, UuidUuidMap()); + anon_mail(playhead::source_atom_v, playlist, UuidUuidMap()).send(this); + + if (jsn.contains("playhead")) { + playhead_serialisation_ = jsn["playhead"]; + } + + jsn_handler_ = json_store::JsonStoreHandler( + dynamic_cast(this), + base_.event_group(), + utility::Uuid::generate(), + not jsn.count("store") or jsn["store"].is_null() + ? JsonStore() + : static_cast(jsn["store"])); + + if (jsn.contains("selection_actor")) { + try { + + selection_actor_ = system().spawn( + static_cast(jsn["selection_actor"]), + caf::actor_cast(this)); + link_to(selection_actor_); + + } catch (const std::exception &e) { + spdlog::error("{}", e.what()); + } + } // need to scan playlist to relink our media.. init(); } -SubsetActor::SubsetActor(caf::actor_config &cfg, caf::actor playlist, const std::string &name) +SubsetActor::SubsetActor( + caf::actor_config &cfg, + caf::actor playlist, + const std::string &name, + const utility::Uuid &uuid, + const std::string &override_type) : caf::event_based_actor(cfg), playlist_(caf::actor_cast(playlist)), - base_(name) { + base_(name, override_type, uuid) { + + jsn_handler_ = json_store::JsonStoreHandler( + dynamic_cast(this), base_.event_group()); + init(); } +void SubsetActor::monitor_media(const caf::actor &actor) { + // make sure we don't double monitor.. + auto act_addr = caf::actor_cast(actor); + + if (auto sit = monitor_.find(act_addr); sit == std::end(monitor_)) { + monitor_[act_addr] = monitor(actor, [this, addr = actor.address()](const error &) { + if (auto mit = monitor_.find(caf::actor_cast(addr)); + mit != std::end(monitor_)) + monitor_.erase(mit); + + for (auto it = std::begin(actors_); it != std::end(actors_); ++it) { + if (addr == it->second) { + spdlog::debug("Remove media {}", to_string(it->first)); + remove_media(it->second, it->first); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + mail( + utility::event_atom_v, + playlist::remove_media_atom_v, + UuidVector({it->first})) + .send(base_.event_group()); + base_.send_changed(); + break; + } + } + }); + } +} + void SubsetActor::add_media( caf::actor actor, const utility::Uuid &uuid, const utility::Uuid &before_uuid) { - base_.insert_media(uuid, before_uuid); - actors_[uuid] = actor; - monitor(actor); + if (not base_.contains_media(uuid)) { + base_.insert_media(uuid, before_uuid); + actors_[uuid] = actor; + monitor_media(actor); + } } bool SubsetActor::remove_media(caf::actor actor, const utility::Uuid &uuid) { bool result = false; if (base_.remove_media(uuid)) { - demonitor(actor); + if (auto it = monitor_.find(caf::actor_cast(actor)); + it != std::end(monitor_)) { + it->second.dispose(); + monitor_.erase(it); + } + actors_.erase(uuid); result = true; } return result; } - - -void SubsetActor::init() { - print_on_create(this, base_); - print_on_exit(this, base_); - - event_group_ = spawn(this); - change_event_group_ = spawn(this); - link_to(event_group_); - link_to(change_event_group_); - - auto selection_actor_ = spawn( - "SubsetPlayheadSelectionActor", caf::actor_cast(this)); - link_to(selection_actor_); - - set_down_handler([=](down_msg &msg) { - // find in playhead list.. - for (auto it = std::begin(actors_); it != std::end(actors_); ++it) { - if (msg.source == it->second) { - spdlog::debug("Remove media {}", to_string(it->first)); - remove_media(it->second, it->first); - send(event_group_, utility::event_atom_v, change_atom_v); - send( - event_group_, - utility::event_atom_v, - playlist::remove_media_atom_v, - UuidVector({it->first})); - base_.send_changed(event_group_, this); - break; - } - } - }); - - - behavior_.assign( +caf::message_handler SubsetActor::message_handler() { + return caf::message_handler{ [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), [=](get_change_event_group_atom) -> caf::actor { return change_event_group_; }, [=](session::media_rate_atom atom) { - delegate(caf::actor_cast(playlist_), atom); + return mail(atom).delegate(caf::actor_cast(playlist_)); }, - [=](duplicate_atom) -> result { // clone ourself.. - auto actor = spawn( - caf::actor_cast(playlist_), base_.name()); - anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); - // get uuid from actor.. - try { - caf::scoped_actor sys(system()); - // get uuid.. - Uuid uuid = request_receive(*sys, actor, utility::uuid_atom_v); - - // maybe not be safe.. as ordering isn't implicit.. - std::vector media_actors; - for (const auto &i : base_.media()) - media_actors.emplace_back(UuidActor(i, actors_[i])); - - request_receive( - *sys, actor, playlist::add_media_atom_v, media_actors, Uuid()); - - return UuidActor(uuid, actor); + auto rp = make_response_promise(); + auto uuid = utility::Uuid::generate(); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return make_error(xstudio_error::error, "Invalid uuid"); - }, - - [=](media::get_edit_list_atom, const Uuid &uuid) -> result { - std::vector actors; - for (const auto &i : base_.media()) - actors.push_back(actors_[i]); - - if (not actors.empty()) { - auto rp = make_response_promise(); + auto actor = spawn( + caf::actor_cast(playlist_), base_.name(), uuid); + anon_mail(playhead::playhead_rate_atom_v, base_.playhead_rate()) + .send(caf::actor_cast(actor)); - fan_out_request( - actors, infinite, media::get_edit_list_atom_v, Uuid()) - .then( - [=](std::vector sections) mutable { - utility::EditList ordered_sections; - for (const auto &i : base_.media()) { - for (const auto &ii : sections) { - const auto &[ud, rt, tc] = ii.section_list()[0]; - if (ud == i) { - if (uuid.is_null()) - ordered_sections.push_back(ii.section_list()[0]); - else - ordered_sections.push_back({uuid, rt, tc}); - break; - } - } - } - rp.deliver(ordered_sections); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - return rp; - } + mail(json_store::get_json_atom_v) + .request(jsn_handler_.json_actor(), infinite) + .then( + [=](const utility::JsonStore &meta) mutable { + anon_mail(json_store::set_json_atom_v, meta) + .send(caf::actor_cast(actor)); + + // maybe not be safe.. as ordering isn't implicit.. + std::vector media_actors; + for (const auto &i : base_.media()) + media_actors.emplace_back(UuidActor(i, actors_[i])); + + mail(playlist::add_media_atom_v, media_actors, Uuid()) + .request(caf::actor_cast(actor), infinite) + .then( + [=](const bool meta) mutable { + rp.deliver(UuidActor(uuid, actor)); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); - return result(utility::EditList()); + return rp; }, [=](playhead::playhead_rate_atom) -> FrameRate { return base_.playhead_rate(); }, [=](playhead::playhead_rate_atom, const FrameRate &rate) { base_.set_playhead_rate(rate); - base_.send_changed(event_group_, this); + base_.send_changed(); }, [=](playhead::source_atom) -> caf::actor { @@ -183,8 +182,13 @@ void SubsetActor::init() { // set source (playlist), triggers relinking [=](playhead::source_atom, caf::actor playlist, const UuidUuidMap &swap) -> bool { - for (const auto &i : actors_) - demonitor(i.second); + for (const auto &i : actors_) { + if (auto it = monitor_.find(caf::actor_cast(i.second)); + it != std::end(monitor_)) { + it->second.dispose(); + monitor_.erase(it); + } + } actors_.clear(); playlist_ = caf::actor_cast(playlist); @@ -218,16 +222,40 @@ void SubsetActor::init() { base_.swap_media(i, ii); } actors_[ii] = amap[ii]; - monitor(amap[ii]); + monitor_media(amap[ii]); } } catch (const std::exception &e) { spdlog::error("Failed to init Subset {}", e.what()); base_.clear(); } - base_.send_changed(event_group_, this); + base_.send_changed(); return true; }, + [=](playlist::add_media_atom atom, + const caf::uri &path, + const bool recursive, + const utility::Uuid &uuid_before) -> result> { + auto rp = make_response_promise>(); + mail(atom, path, recursive, uuid_before) + .request(caf::actor_cast(playlist_), infinite) + .then( + [=](const std::vector new_media) mutable { + for (const auto &m : new_media) { + add_media(m.actor(), m.uuid(), uuid_before); + } + mail(utility::event_atom_v, playlist::add_media_atom_v, new_media) + .send(base_.event_group()); + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + mail(utility::event_atom_v, utility::change_atom_v) + .send(change_event_group_); + rp.deliver(new_media); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + return rp; + }, + [=](add_media_atom atom, UuidActor ua, // the MediaActor const utility::UuidList &final_ordered_uuid_list, @@ -289,14 +317,14 @@ void SubsetActor::init() { const Uuid &before_uuid) -> bool { try { add_media(actor, uuid, before_uuid); - send( - event_group_, + mail( utility::event_atom_v, playlist::add_media_atom_v, - UuidActor(uuid, actor)); - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); + UuidActorVector({UuidActor(uuid, actor)})) + .send(base_.event_group()); + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + mail(utility::event_atom_v, utility::change_atom_v).send(change_event_group_); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -328,14 +356,13 @@ void SubsetActor::init() { // add_media(actor, uuid, before_uuid); - // send( - // event_group_, - // utility::event_atom_v, + // mail(// utility::event_atom_v, // playlist::add_media_atom_v, - // UuidActor(uuid, actor)); - // base_.send_changed(event_group_, this); - // send(event_group_, utility::event_atom_v, change_atom_v); - // send(change_event_group_, utility::event_atom_v, utility::change_atom_v); + // UuidActorVector({UuidActor(uuid, actor)})).send(// base_.event_group()); + // base_.send_changed(); + // mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + // mail(utility::event_atom_v, + // utility::change_atom_v).send(change_event_group_); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); rp.deliver(false); @@ -350,8 +377,8 @@ void SubsetActor::init() { // get actor from playlist.. auto rp = make_response_promise(); - request( - caf::actor_cast(playlist_), infinite, playlist::get_media_atom_v, uuid) + mail(playlist::get_media_atom_v, uuid) + .request(caf::actor_cast(playlist_), infinite) .then( [=](caf::actor actor) mutable { rp.delegate( @@ -361,14 +388,14 @@ void SubsetActor::init() { actor, before_uuid); // add_media(actor, uuid, before_uuid); - // send(event_group_, utility::event_atom_v, change_atom_v); - // send(change_event_group_, utility::event_atom_v, - // utility::change_atom_v); send( - // event_group_, - // utility::event_atom_v, + // mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + // mail(utility::event_atom_v, + // utility::change_atom_v).send(change_base_.event_group()); mail(// + // utility::event_atom_v, // playlist::add_media_atom_v, - // UuidActor(uuid, actor)); - // base_.send_changed(event_group_, this); + // UuidActorVector({UuidActor(uuid, actor)})).send(// + // base_.event_group()); + // base_.send_changed(); // rp.deliver(true); }, [=](error &err) mutable { @@ -379,13 +406,24 @@ void SubsetActor::init() { return rp; }, + [=](playlist::add_media_atom, const std::vector &media) -> bool { + for (const auto &m : media) { + add_media(m.actor(), m.uuid(), utility::Uuid()); + } + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + mail(utility::event_atom_v, utility::change_atom_v).send(change_event_group_); + return true; + }, + [=](playlist::add_media_atom, const UuidVector &media_uuids, const utility::Uuid &uuid_before) -> result { // need actors.. auto rp = make_response_promise(); - request(caf::actor_cast(playlist_), infinite, playlist::get_media_atom_v) + mail(playlist::get_media_atom_v) + .request(caf::actor_cast(playlist_), infinite) .then( [=](const std::vector &actors) mutable { bool changed = false; @@ -394,23 +432,22 @@ void SubsetActor::init() { if (ii.uuid() == i) { changed = true; add_media(ii.actor(), i, uuid_before); - send( - event_group_, + mail( utility::event_atom_v, playlist::add_media_atom_v, - UuidActor(i, ii.actor())); + UuidActorVector({UuidActor(i, ii.actor())})) + .send(base_.event_group()); break; } } } if (changed) { - send(event_group_, utility::event_atom_v, change_atom_v); - send( - change_event_group_, - utility::event_atom_v, - utility::change_atom_v); - base_.send_changed(event_group_, this); + mail(utility::event_atom_v, change_atom_v) + .send(base_.event_group()); + mail(utility::event_atom_v, utility::change_atom_v) + .send(change_event_group_); + base_.send_changed(); } rp.deliver(changed); }, @@ -423,16 +460,37 @@ void SubsetActor::init() { }, + [=](media::current_media_source_atom) + -> caf::result>>> { + auto rp = make_response_promise< + std::vector>>>(); + if (not actors_.empty()) { + fan_out_request( + map_value_to_vec(actors_), infinite, media::current_media_source_atom_v) + .then( + [=](const std::vector< + std::pair>> + details) mutable { rp.deliver(details); }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + } else { + rp.deliver( + std::vector>>()); + } + return rp; + }, + + [=](playlist::add_media_atom, const std::vector &media_actors, const utility::Uuid &uuid_before) -> result { for (const auto &i : media_actors) { add_media(i.actor(), i.uuid(), uuid_before); - send(event_group_, utility::event_atom_v, playlist::add_media_atom_v, i); } - send(event_group_, utility::event_atom_v, change_atom_v); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); - base_.send_changed(event_group_, this); + mail(utility::event_atom_v, playlist::add_media_atom_v, media_actors) + .send(base_.event_group()); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + mail(utility::event_atom_v, utility::change_atom_v).send(change_event_group_); + base_.send_changed(); return true; }, @@ -441,16 +499,28 @@ void SubsetActor::init() { return playhead_; auto uuid = utility::Uuid::generate(); auto actor = spawn( - std::string("Subset Playhead"), selection_actor_, uuid); + std::string("Subset Playhead"), + playhead::GLOBAL_AUDIO, + selection_actor_, + uuid, + caf::actor_cast(this)); link_to(actor); - anon_send(actor, playhead::playhead_rate_atom_v, base_.playhead_rate()); + anon_mail(playhead::playhead_rate_atom_v, base_.playhead_rate()).send(actor); + playhead_ = UuidActor(uuid, actor); + + if (!playhead_serialisation_.is_null()) { + anon_mail(module::deserialise_atom_v, playhead_serialisation_) + .send(playhead_.actor()); + } + return playhead_; }, [=](playlist::get_playhead_atom) { - delegate(caf::actor_cast(this), playlist::create_playhead_atom_v); + return mail(playlist::create_playhead_atom_v) + .delegate(caf::actor_cast(this)); }, [=](playlist::get_media_atom) -> std::vector { @@ -503,18 +573,18 @@ void SubsetActor::init() { [=](playlist::get_media_uuid_atom) -> UuidVector { return base_.media_vector(); }, [=](playlist::move_media_atom atom, const Uuid &uuid, const Uuid &uuid_before) { - delegate( - actor_cast(this), atom, utility::UuidVector({uuid}), uuid_before); + return mail(atom, utility::UuidVector({uuid}), uuid_before) + .delegate(actor_cast(this)); }, [=](playlist::move_media_atom atom, const UuidList &media_uuids, const Uuid &uuid_before) { - delegate( - actor_cast(this), - atom, - utility::UuidVector(media_uuids.begin(), media_uuids.end()), - uuid_before); + return mail( + atom, + utility::UuidVector(media_uuids.begin(), media_uuids.end()), + uuid_before) + .delegate(actor_cast(this)); }, [=](playlist::move_media_atom, @@ -525,28 +595,27 @@ void SubsetActor::init() { result |= base_.move_media(uuid, uuid_before); } if (result) { - base_.send_changed(event_group_, this); - send( - event_group_, + base_.send_changed(); + mail( utility::event_atom_v, playlist::move_media_atom_v, media_uuids, - uuid_before); - send(event_group_, utility::event_atom_v, change_atom_v); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); + uuid_before) + .send(base_.event_group()); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + mail(utility::event_atom_v, utility::change_atom_v).send(change_event_group_); } return result; }, [=](playlist::remove_media_atom atom, const utility::UuidList &uuids) { - delegate( - actor_cast(this), - atom, - utility::UuidVector(uuids.begin(), uuids.end())); + return mail(atom, utility::UuidVector(uuids.begin(), uuids.end())) + .delegate(actor_cast(this)); }, [=](playlist::remove_media_atom atom, const Uuid &uuid) { - delegate(actor_cast(this), atom, utility::UuidVector({uuid})); + return mail(atom, utility::UuidVector({uuid})) + .delegate(actor_cast(this)); }, [=](playlist::remove_media_atom, const utility::UuidVector &uuids) -> bool { @@ -560,81 +629,209 @@ void SubsetActor::init() { } if (not removed.empty()) { - send(event_group_, utility::event_atom_v, change_atom_v); - send( - event_group_, - utility::event_atom_v, - playlist::remove_media_atom_v, - removed); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); - base_.send_changed(event_group_, this); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + mail(utility::event_atom_v, playlist::remove_media_atom_v, removed) + .send(base_.event_group()); + mail(utility::event_atom_v, utility::change_atom_v).send(change_event_group_); + base_.send_changed(); } return not removed.empty(); }, [=](playlist::selection_actor_atom) -> caf::actor { return selection_actor_; }, - [=](playlist::sort_alphabetically_atom) { sort_alphabetically(); }, - - [=](get_next_media_atom, - const utility::Uuid &after_this_uuid, - int skip_by) -> result { - const utility::UuidList media = base_.media(); - if (skip_by > 0) { - auto i = std::find(media.begin(), media.end(), after_this_uuid); - if (i == media.end()) { - // not found! - return make_error( - xstudio_error::error, - "playlist::get_next_media_atom called with uuid that is not in " - "subset"); - } - while (skip_by--) { - i++; - if (i == media.end()) { - i--; - break; + [=](playlist::sort_by_media_display_info_atom, + const int sort_column_index, + const bool ascending) { sort_by_media_display_info(sort_column_index, ascending); }, + + [=](playlist::filter_media_atom, + const std::string &filter_string) -> result { + // for each media item in the playlist, check if any fields in the + // 'media display info' for the media matches filter_string. If + // it does, include it in the result. We have to do some awkward + // shenanegans thanks to async requests ... the usual stuff!! + + if (filter_string.empty()) + return base_.media(); + + auto rp = make_response_promise(); + // share ptr to store result for each media piece (in order) + auto vv = std::make_shared>(base_.media().size()); + // keep count of responses with this + auto ct = std::make_shared(base_.media().size()); + const auto filter_string_lower = utility::to_lower(filter_string); + auto check_deliver = [=]() mutable { + (*ct)--; + if (*ct == 0) { + utility::UuidList r; + for (const auto &v : *vv) { + if (!v.is_null()) { + r.push_back(v); + } } + rp.deliver(r); } - if (actors_.count(*i)) - return UuidActor(*i, actors_[*i]); + }; - } else { - auto i = std::find(media.rbegin(), media.rend(), after_this_uuid); - if (i == media.rend()) { - // not found! - return make_error( - xstudio_error::error, - "playlist::get_next_media_atom called with uuid that is not in " - "playlist"); - } - while (skip_by++) { - i++; - if (i == media.rend()) { - i--; - break; - } - } - if (actors_.count(*i)) - return UuidActor(*i, actors_[*i]); + int idx = 0; + for (const auto &uuid : base_.media()) { + if (!actors_.count(uuid)) { + check_deliver(); + continue; + } + UuidActor media(uuid, actors_[uuid]); + mail(media::media_display_info_atom_v) + .request(media.actor(), infinite) + .then( + [=](const utility::JsonStore &media_display_info) mutable { + if (media_display_info.is_array()) { + for (int i = 0; i < media_display_info.size(); ++i) { + std::string data = media_display_info[i].dump(); + if (utility::to_lower(data).find(filter_string_lower) != + std::string::npos) { + (*vv)[idx] = media.uuid(); + break; + } + } + } + check_deliver(); + }, + [=](caf::error &err) mutable { check_deliver(); }); + idx++; } + return rp; + }, + + [=](get_next_media_atom, + const utility::Uuid &after_this_uuid, + int skip_by, + const std::string &filter_string) -> result { + auto rp = make_response_promise(); - return make_error( - xstudio_error::error, - "playlist::get_next_media_atom called with uuid for which no media actor " - "exists"); + mail(playlist::filter_media_atom_v, filter_string) + .request(caf::actor_cast(this), infinite) + .then( + [=](const utility::UuidList &media) mutable { + if (skip_by > 0) { + auto i = std::find(media.begin(), media.end(), after_this_uuid); + if (i == media.end()) { + // not found! + rp.deliver(make_error( + xstudio_error::error, + "playlist::get_next_media_atom called with uuid that is " + "not in " + "subset")); + } + while (skip_by--) { + i++; + if (i == media.end()) { + i--; + break; + } + } + if (actors_.count(*i)) + rp.deliver(UuidActor(*i, actors_[*i])); + + } else { + auto i = std::find(media.rbegin(), media.rend(), after_this_uuid); + if (i == media.rend()) { + // not found! + rp.deliver(make_error( + xstudio_error::error, + "playlist::get_next_media_atom called with uuid that is " + "not in " + "playlist")); + } + while (skip_by++) { + i++; + if (i == media.rend()) { + i--; + break; + } + } + + if (actors_.count(*i)) + rp.deliver(UuidActor(*i, actors_[*i])); + } + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + return rp; }, + // [=](json_store::get_json_atom atom, const std::string &path) { + // return mail(atom, path).delegate(caf::actor_cast(playlist_)); + // }, + + // [=](json_store::set_json_atom atom, const JsonStore &json, const std::string &path) { + // return mail(atom, json, path).delegate(caf::actor_cast(playlist_)); + // }, + [=](session::session_atom) { - delegate(caf::actor_cast(playlist_), session::session_atom_v); + return mail(session::session_atom_v) + .delegate(caf::actor_cast(playlist_)); }, - [=](utility::serialise_atom) -> JsonStore { - JsonStore jsn; - jsn["base"] = base_.serialise(); - return jsn; - }); + [=](utility::serialise_atom) -> result { + auto rp = make_response_promise(); + JsonStore j; + j["base"] = SubsetActor::serialise(); + + mail(json_store::get_json_atom_v, "") + .request(jsn_handler_.json_actor(), infinite) + .then([=](const JsonStore &meta) mutable { + j["store"] = meta; + mail(utility::serialise_atom_v) + .request(selection_actor_, infinite) + .then( + [=](const utility::JsonStore &selection_state) mutable { + j["selection_actor"] = selection_state; + if (playhead_) { + mail(utility::serialise_atom_v) + .request(playhead_.actor(), infinite) + .then( + [=](const utility::JsonStore + &playhead_state) mutable { + playhead_serialisation_ = playhead_state; + j["playhead"] = playhead_state; + rp.deliver(j); + }, + [=](caf::error &err) mutable { + spdlog::warn( + "{} {}", + __PRETTY_FUNCTION__, + to_string(err)); + rp.deliver(j); + }); + + } else { + if (!playhead_serialisation_.is_null()) { + j["playhead"] = playhead_serialisation_; + } + rp.deliver(j); + } + }, + [=](caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + rp.deliver(j); + }); + }); + return rp; + }}; +} + +void SubsetActor::init() { + print_on_create(this, base_); + print_on_exit(this, base_); + + change_event_group_ = spawn(this); + link_to(change_event_group_); + + if (!selection_actor_) { + selection_actor_ = spawn( + "SubsetPlayheadSelectionActor", caf::actor_cast(this)); + link_to(selection_actor_); + } } void SubsetActor::add_media( @@ -667,10 +864,11 @@ void SubsetActor::add_media( } add_media(actor, ua.uuid(), before_uuid); - send(event_group_, utility::event_atom_v, playlist::add_media_atom_v, ua); - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); + mail(utility::event_atom_v, playlist::add_media_atom_v, UuidActorVector({ua})) + .send(base_.event_group()); + base_.send_changed(); + mail(utility::event_atom_v, change_atom_v).send(base_.event_group()); + mail(utility::event_atom_v, utility::change_atom_v).send(change_event_group_); rp.deliver(ua); } catch (const std::exception &err) { @@ -678,48 +876,62 @@ void SubsetActor::add_media( } } -void SubsetActor::sort_alphabetically() { +void SubsetActor::sort_by_media_display_info( + const int sort_column_index, const bool ascending) { using SourceAndUuid = std::pair; - auto media_names_vs_uuids = + auto sort_keys_vs_uuids = std::make_shared>>(); + int idx = 0; for (const auto &i : base_.media()) { // Pro tip: because i is a reference, it's the reference that is captured in our lambda // below and therefore it is 'unstable' so we make a copy here and use that in the // lambda as this is object-copied in the capture instead. UuidActor media_actor(i, actors_[i]); + idx++; - request(media_actor.actor(), infinite, media::media_reference_atom_v, utility::Uuid()) + mail(media::media_display_info_atom_v) + .request(media_actor.actor(), infinite) .await( - [=](const std::pair &m_ref) mutable { - std::string path = uri_to_posix_path(m_ref.second.uri()); - path = std::string(path, path.rfind("/") + 1); - path = to_lower(path); + [=](const utility::JsonStore &media_display_info) mutable { + // media_display_info should be an array of arrays. Each + // array is the data shown in the media list columns. + // So info_set_idx corresponds to the media list (in the UI) + // from which the sort was requested. And the info_item_idx + // corresponds to the column of that media list. - (*media_names_vs_uuids).push_back(std::make_pair(path, media_actor.uuid())); + // default sort key keeps current sorting but should always + // put it after the last element that did have a sort key + std::string sort_key = fmt::format("ZZZZZZ{}", idx); - if (media_names_vs_uuids->size() == base_.media().size()) { + if (media_display_info.is_array() && + sort_column_index < media_display_info.size()) { + sort_key = media_display_info[sort_column_index].dump(); + } + + (*sort_keys_vs_uuids) + .push_back(std::make_pair(sort_key, media_actor.uuid())); + + if (sort_keys_vs_uuids->size() == base_.media().size()) { std::sort( - media_names_vs_uuids->begin(), - media_names_vs_uuids->end(), - [](const SourceAndUuid &a, const SourceAndUuid &b) -> bool { - return a.first < b.first; + sort_keys_vs_uuids->begin(), + sort_keys_vs_uuids->end(), + [ascending]( + const SourceAndUuid &a, const SourceAndUuid &b) -> bool { + return ascending ? a.first < b.first : a.first > b.first; }); utility::UuidList ordered_uuids; - for (const auto &p : (*media_names_vs_uuids)) { + for (const auto &p : (*sort_keys_vs_uuids)) { ordered_uuids.push_back(p.second); } - anon_send( - caf::actor_cast(this), - move_media_atom_v, - ordered_uuids, - utility::Uuid()); + anon_mail(move_media_atom_v, ordered_uuids, utility::Uuid()) + .send(caf::actor_cast(this)); } }, [=](error &err) mutable { diff --git a/src/subset/test/CMakeLists.txt b/src/subset/test/CMakeLists.txt index 362d2075a..eebddc23b 100644 --- a/src/subset/test/CMakeLists.txt +++ b/src/subset/test/CMakeLists.txt @@ -3,7 +3,7 @@ include(CTest) SET(LINK_DEPS xstudio::subset xstudio::global - caf::core + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/subset/test/subset_actor_test.cpp b/src/subset/test/subset_actor_test.cpp index 2f7715e94..9b40877b8 100644 --- a/src/subset/test/subset_actor_test.cpp +++ b/src/subset/test/subset_actor_test.cpp @@ -44,7 +44,7 @@ TEST(SubsetActorTest, Test) { utility::FrameRate(timebase::k_flicks_24fps), su1))})); - f.self->anon_send(playlist, add_media_atom_v, media, Uuid()); + anon_mail(add_media_atom_v, media, Uuid()).send(playlist); auto media_uuid = request_receive(*(f.self), media, uuid_atom_v); @@ -60,7 +60,7 @@ TEST(SubsetActorTest, Test) { utility::FrameRate(timebase::k_flicks_24fps), su2))})); - f.self->anon_send(playlist, add_media_atom_v, media2, Uuid()); + anon_mail(add_media_atom_v, media2, Uuid()).send(playlist); // spawn subset.. auto subset = f.self->spawn(playlist, "Test"); diff --git a/src/sync/src/CMakeLists.txt b/src/sync/src/CMakeLists.txt deleted file mode 100644 index 79babf3b5..000000000 --- a/src/sync/src/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -SET(LINK_DEPS - xstudio::utility - xstudio::global_store - caf::core - caf::io -) - -create_component(sync 0.1.0 "${LINK_DEPS}") diff --git a/src/sync/src/sync_actor.cpp b/src/sync/src/sync_actor.cpp deleted file mode 100644 index dd244a4cd..000000000 --- a/src/sync/src/sync_actor.cpp +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/sync/sync_actor.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" - -using namespace caf; -using namespace xstudio; -using namespace xstudio::sync; -using namespace xstudio::utility; - -SyncActor::SyncActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { init(); } - -void SyncActor::init() { - // launch global actors.. - // preferences first.. - // this will need more configuration - spdlog::debug("Created SyncActor"); - print_on_exit(this, "SyncActor"); - - behavior_.assign( - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](global::get_api_mode_atom) -> std::string { return "SYNC"; }, - - [=](global::get_application_mode_atom _req) { - delegate(system().registry().template get(global_registry), _req); - }); -} - -void SyncActor::on_exit() {} diff --git a/src/sync/src/sync_gateway_actor.cpp b/src/sync/src/sync_gateway_actor.cpp deleted file mode 100644 index da43d5ba4..000000000 --- a/src/sync/src/sync_gateway_actor.cpp +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/global_store/global_store.hpp" -#include "xstudio/sync/sync_actor.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" - -using namespace caf; -using namespace xstudio; -using namespace xstudio::sync; -using namespace xstudio::utility; -using namespace xstudio::global_store; - -SyncGatewayActor::SyncGatewayActor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { - init(); -} - -void SyncGatewayActor::init() { - // launch global actors.. - // preferences first.. - // this will need more configuration - spdlog::debug("Created SyncGatewayActor"); - print_on_exit(this, "SyncGatewayActor"); - - system().registry().put(sync_gateway_registry, this); - - behavior_.assign( - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](authorise_connection_atom _req, const Uuid &lock, const std::string &key) { - delegate( - system().registry().template get(sync_gateway_manager_registry), - _req, - lock, - key); - }, - - [=](global::get_api_mode_atom) -> std::string { return "GATEWAY"; }, - - [=](request_connection_atom _req) { - delegate( - system().registry().template get(sync_gateway_manager_registry), - _req); - }); -} - -void SyncGatewayActor::on_exit() { system().registry().erase(sync_gateway_registry); } diff --git a/src/sync/src/sync_gateway_manager_actor.cpp b/src/sync/src/sync_gateway_manager_actor.cpp deleted file mode 100644 index 37ddbee11..000000000 --- a/src/sync/src/sync_gateway_manager_actor.cpp +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include -#include -#include -#include - -#include -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/global_store/global_store.hpp" -#include "xstudio/sync/sync_actor.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" - -using namespace caf; -using namespace xstudio; -using namespace xstudio::sync; -using namespace xstudio::utility; -using namespace xstudio::global_store; -using namespace std::chrono_literals; - -SyncGatewayManagerActor::SyncGatewayManagerActor(caf::actor_config &cfg) - : caf::event_based_actor(cfg) { - init(); -} - -void SyncGatewayManagerActor::init() { - // launch global actors.. - // preferences first.. - // this will need more configuration - spdlog::debug("Created SyncGatewayManagerActor"); - print_on_exit(this, "SyncGatewayManagerActor"); - - system().registry().put(sync_gateway_manager_registry, this); - - set_down_handler([=](down_msg &msg) { - // find in playhead list.. - for (auto it = std::begin(clients_); it != std::end(clients_); ++it) { - if (msg.source == it->second) { - spdlog::debug("Remove sync actor {}", to_string(it->first)); - demonitor(it->second); - clients_.erase(it); - break; - } - } - }); - - behavior_.assign( - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](authorise_connection_atom, - const Uuid &lock, - const std::string &key) -> caf::result { - if (not lock_key_.count(lock)) - return make_error( - xstudio_error::error, fmt::format("Invalid lock {}", to_string(lock))); - if (clients_.count(lock)) - return make_error( - xstudio_error::error, fmt::format("Already connected {}", to_string(lock))); - - if (lock_key_[lock] != key) { - lock_key_.erase(lock); - return make_error( - xstudio_error::error, - fmt::format("Invalid key {} {}, lock invalidated.", to_string(lock), key)); - } - lock_key_.erase(lock); - auto act = spawn(); - monitor(act); - clients_.insert({lock, act}); - return act; - }, - - [=](get_sync_atom) -> caf::actor { - Uuid lock = Uuid::generate(); - auto act = spawn(); - monitor(act); - clients_.insert({lock, act}); - return act; - }, - - [=](request_connection_atom) -> Uuid { - Uuid lock = Uuid::generate(); - lock_key_.insert({lock, std::string("key")}); - // stop client hammering.. - std::this_thread::sleep_for(1s); - return lock; - }); -} - -void SyncGatewayManagerActor::on_exit() { - system().registry().erase(sync_gateway_manager_registry); -} diff --git a/src/sync/test/CMakeLists.txt b/src/sync/test/CMakeLists.txt deleted file mode 100644 index 35637d110..000000000 --- a/src/sync/test/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -include(CTest) - -SET(LINK_DEPS - xstudio::sync - caf::core -) - -create_tests("${LINK_DEPS}") diff --git a/src/sync/test/sync_actor_test.cpp b/src/sync/test/sync_actor_test.cpp deleted file mode 100644 index 357a5619a..000000000 --- a/src/sync/test/sync_actor_test.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/sync/sync_actor.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/uuid.hpp" - -using namespace xstudio; -using namespace xstudio::sync; -using namespace xstudio::utility; - -using namespace caf; - -#include "xstudio/utility/serialise_headers.hpp" - - -ACTOR_TEST_SETUP() - - -TEST(SyncActorTest, Test) { fixture f; } diff --git a/src/tag/src/CMakeLists.txt b/src/tag/src/CMakeLists.txt deleted file mode 100644 index dcd0c7b8d..000000000 --- a/src/tag/src/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -SET(LINK_DEPS - xstudio::utility - xstudio::json_store - caf::core -) - -create_component(tag 0.1.0 "${LINK_DEPS}") - diff --git a/src/tag/src/tag.cpp b/src/tag/src/tag.cpp deleted file mode 100644 index 98fe66197..000000000 --- a/src/tag/src/tag.cpp +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include "xstudio/tag/tag.hpp" -#include "xstudio/utility/helpers.hpp" - -using namespace xstudio; -using namespace xstudio::tag; -using namespace xstudio::utility; - -Tag::Tag(const utility::JsonStore &jsn) { - id_ = jsn.at("id"); - link_ = jsn.at("link"); - type_ = jsn.at("type"); - data_ = jsn.at("data"); - unique_ = jsn.at("unique"); - persistent_ = true; -} - -utility::JsonStore Tag::serialise() const { - utility::JsonStore jsn; - - jsn["id"] = id_; - jsn["link"] = link_; - jsn["type"] = type_; - jsn["data"] = data_; - jsn["unique"] = unique_; - - return jsn; -} - - -TagBase::TagBase(const std::string &name) : Container(name, "TagBase") {} - -TagBase::TagBase(const utility::JsonStore &jsn) - : Container(static_cast(jsn["container"])) { - for (const auto &i : jsn["tags"]) - add_tag(Tag(i)); -} - -std::optional TagBase::add_tag(const Tag tag) { - // if unique keys exists we replace with new entry - bool replaced = false; - Tag replaced_tag; - - if (tag.unique()) { - auto uni = tag.unique(); - for (const auto &i : tag_map_) { - if (i.second.unique() == uni) { - replaced = true; - replaced_tag = i.second; - remove_tag(i.first); - break; - } - } - } - - tag_map_[tag.id()] = tag; - if (not link_map_.count(tag.link())) - link_map_[tag.link()] = utility::UuidVector(); - link_map_[tag.link()].push_back(tag.id()); - - if (replaced) - return replaced_tag; - - return {}; -} - -bool TagBase::remove_tag(const utility::Uuid &id) { - if (tag_map_.count(id)) { - std::remove( - link_map_[tag_map_[id].link()].begin(), link_map_[tag_map_[id].link()].end(), id); - tag_map_.erase(tag_map_.find(id)); - return true; - } - return false; -} - -std::optional TagBase::get_tag(const utility::Uuid &id) const { - if (tag_map_.count(id)) - return tag_map_.at(id); - return {}; -} - -std::vector TagBase::get_tags(const utility::Uuid &link) const { - if (link_map_.count(link)) { - std::vector tags; - for (const auto &i : link_map_.at(link)) { - tags.push_back(tag_map_.at(i)); - } - - return tags; - } - - return std::vector(); -} - - -std::vector TagBase::get_tags() const { return map_value_to_vec(tag_map_); } - -JsonStore TagBase::serialise() const { - JsonStore jsn; - - jsn["container"] = Container::serialise(); - jsn["tags"] = nlohmann::json::array(); - - for (const auto &i : tag_map_) { - if (i.second.persistent()) - jsn["tags"].push_back(i.second.serialise()); - } - - return jsn; -} diff --git a/src/tag/src/tag_actor.cpp b/src/tag/src/tag_actor.cpp deleted file mode 100644 index 5f9a86740..000000000 --- a/src/tag/src/tag_actor.cpp +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/broadcast/broadcast_actor.hpp" -#include "xstudio/tag/tag_actor.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" - -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::tag; -using namespace nlohmann; -using namespace caf; - -namespace {} // namespace - - -TagActor::TagActor(caf::actor_config &cfg, const utility::JsonStore &jsn) - : caf::event_based_actor(cfg), base_(static_cast(jsn["base"])) { - - init(); -} - -TagActor::TagActor(caf::actor_config &cfg, const utility::Uuid &uuid) - : caf::event_based_actor(cfg) { - base_.set_uuid(uuid); - - init(); -} - -caf::message_handler TagActor::default_event_handler() { - return { - [=](utility::event_atom, remove_tag_atom, const utility::Uuid &) {}, - [=](utility::event_atom, add_tag_atom, const Tag &) {}, - }; -} - -void TagActor::init() { - print_on_create(this, base_); - print_on_exit(this, base_); - - event_group_ = spawn(this); - link_to(event_group_); - - behavior_.assign( - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - base_.make_ignore_error_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), - base_.make_last_changed_event_handler(event_group_, this), - - [=](get_tag_atom, const utility::Uuid &id) -> result { - auto tag = base_.get_tag(id); - if (tag) - return *tag; - return make_error(xstudio_error::error, "Invalid uuid"); - }, - [=](get_tags_atom, const utility::Uuid &link_id) -> std::vector { - return base_.get_tags(link_id); - }, - [=](get_tags_atom) -> std::vector { return base_.get_tags(); }, - - [=](add_tag_atom, const Tag &tag) -> utility::Uuid { - auto replaced = base_.add_tag(tag); - - if (replaced) - send(event_group_, utility::event_atom_v, remove_tag_atom_v, replaced->id()); - - send(event_group_, utility::event_atom_v, add_tag_atom_v, tag); - - if (tag.persistent()) - base_.send_changed(event_group_, this); - - return tag.id(); - }, - - [=](remove_tag_atom, const utility::Uuid &id) -> bool { - auto result = base_.remove_tag(id); - if (result) { - send(event_group_, utility::event_atom_v, remove_tag_atom_v, id); - base_.send_changed(event_group_, this); - } - return result; - }, - - [=](utility::serialise_atom) -> JsonStore { - JsonStore jsn; - jsn["base"] = base_.serialise(); - return jsn; - }); -} diff --git a/src/tag/test/CMakeLists.txt b/src/tag/test/CMakeLists.txt deleted file mode 100644 index 149f57f26..000000000 --- a/src/tag/test/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -include(CTest) - -SET(LINK_DEPS - xstudio::tag - caf::core -) - -create_tests("${LINK_DEPS}") diff --git a/src/tag/test/tag_actor_test.cpp b/src/tag/test/tag_actor_test.cpp deleted file mode 100644 index 7f99da466..000000000 --- a/src/tag/test/tag_actor_test.cpp +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/playlist/playlist_actor.hpp" -#include "xstudio/bookmark/bookmarks_actor.hpp" -#include "xstudio/utility/helpers.hpp" - -using namespace std::chrono_literals; -using namespace xstudio::utility; -using namespace xstudio::playlist; -using namespace xstudio::bookmark; -using namespace xstudio; - -using namespace caf; - -#include "xstudio/utility/serialise_headers.hpp" - - -ACTOR_TEST_SETUP() - -TEST(BookmarkActorTest, Test) { fixture f; } - -TEST(BookmarkSerializerTest, Test) { - fixture f; - - binary_serializer::container_type buf; - binary_serializer bs{f.system, buf}; -} diff --git a/src/tag/test/tag_test.cpp b/src/tag/test/tag_test.cpp deleted file mode 100644 index a02990ca6..000000000 --- a/src/tag/test/tag_test.cpp +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include - -#include "xstudio/tag/tag.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/uuid.hpp" -#include "xstudio/utility/chrono.hpp" -#include "xstudio/utility/caf_helpers.hpp" - -using namespace xstudio::utility; -using namespace xstudio::tag; - -ACTOR_TEST_MINIMAL() - -TEST(TagTest, Test) { - // Bookmarks b1; - - // EXPECT_TRUE(b1.empty()); - // EXPECT_EQ(b1.size(), 0); - // auto u1 = b1.add_bookmark(); - // EXPECT_FALSE(b1.empty()); - // EXPECT_EQ(b1.size(), 1); - - // auto jsn = b1.serialise(); - - // Bookmarks b2(jsn); - // EXPECT_FALSE(b2.empty()); - // EXPECT_EQ(b2.size(), 1); - - // EXPECT_EQ(u1, b2.get_bookmark(u1)->uuid()); - // auto bm = b2.erase_bookmark(u1); - // EXPECT_TRUE(b2.empty()); - // EXPECT_EQ(b2.size(), 0); -} diff --git a/src/thumbnail/src/CMakeLists.txt b/src/thumbnail/src/CMakeLists.txt index b97ee1795..7d9145612 100644 --- a/src/thumbnail/src/CMakeLists.txt +++ b/src/thumbnail/src/CMakeLists.txt @@ -1,9 +1,32 @@ -SET(LINK_DEPS - caf::core - JPEG::JPEG - xstudio::global_store -) +if (USE_VCPKG) + + find_package(JPEG REQUIRED) + find_package(libjpeg-turbo CONFIG REQUIRED) + + SET(LINK_DEPS + CAF::core + JPEG::JPEG + xstudio::global_store + xstudio::media + ) + + create_component(thumbnail ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") + target_include_directories(thumbnail PRIVATE JPEG::JPEG) + target_link_libraries(thumbnail PRIVATE $,libjpeg-turbo::turbojpeg,libjpeg-turbo::turbojpeg-static>) + +else() + + find_package(JPEG REQUIRED) + + SET(LINK_DEPS + CAF::core + JPEG::JPEG + xstudio::global_store + xstudio::media + ) + + create_component(thumbnail ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") + +endif() -find_package(JPEG REQUIRED) -create_component(thumbnail 0.1.0 "${LINK_DEPS}") diff --git a/src/thumbnail/src/thumbnail_cache_actor.cpp b/src/thumbnail/src/thumbnail_cache_actor.cpp index e589ff898..41ff1f338 100644 --- a/src/thumbnail/src/thumbnail_cache_actor.cpp +++ b/src/thumbnail/src/thumbnail_cache_actor.cpp @@ -49,12 +49,12 @@ ThumbnailCacheActor::ThumbnailCacheActor( [=](media_cache::keys_atom, bool) { if (not erased_keys_.empty() or not new_keys_.empty()) - send( - event_group_, + mail( utility::event_atom_v, media_cache::keys_atom_v, std::vector(new_keys_.begin(), new_keys_.end()), - std::vector(erased_keys_.begin(), erased_keys_.end())); + std::vector(erased_keys_.begin(), erased_keys_.end())) + .send(event_group_); new_keys_.clear(); erased_keys_.clear(); @@ -92,7 +92,9 @@ void ThumbnailCacheActor::update_changes( } if (not update_pending_ and (not new_keys_.empty() or not erased_keys_.empty())) { update_pending_ = true; - delayed_anon_send(this, std::chrono::milliseconds(250), media_cache::keys_atom_v, true); + anon_mail(media_cache::keys_atom_v, true) + .delay(std::chrono::milliseconds(250)) + .send(this); } } diff --git a/src/thumbnail/src/thumbnail_disk_cache_actor.cpp b/src/thumbnail/src/thumbnail_disk_cache_actor.cpp index 81daaf6d2..5ba8d0a61 100644 --- a/src/thumbnail/src/thumbnail_disk_cache_actor.cpp +++ b/src/thumbnail/src/thumbnail_disk_cache_actor.cpp @@ -1,7 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 +#include + #include #include +#ifdef _WIN32 +// required to define INT32 type used by jpeglib +#include +#endif #include #include @@ -40,7 +46,7 @@ boolean empty_stdvector_output_buffer(j_compress_ptr cinfo) { dest->pub_.next_output_byte = reinterpret_cast(dest->vec_->data()) + currentSize; dest->pub_.free_in_buffer = currentSize; - return TRUE; + return boolean(1); } void term_stdvector_destination(j_compress_ptr cinfo) { @@ -107,13 +113,9 @@ ThumbGenMiddleman::ThumbGenMiddleman(caf::actor_config &cfg) : caf::event_based_ if (not global_reader) { rp.deliver(make_error(xstudio_error::error, "No readers available")); } else { - request( - global_reader, - infinite, - media_reader::get_thumbnail_atom_v, - mptr, - thumb_size) - .await( + mail(media_reader::get_thumbnail_atom_v, mptr, thumb_size) + .request(global_reader, infinite) + .then( [=](const ThumbnailBufferPtr &buf) mutable { rp.deliver(buf); }, [=](const caf::error &err) mutable { rp.deliver(err); }); } @@ -177,9 +179,10 @@ TDCHelperActor::encode_save_thumb(const std::string &path, const ThumbnailBuffer return buf.size(); } -std::vector TDCHelperActor::encode_thumb(const ThumbnailBufferPtr &buffer) { +std::vector +TDCHelperActor::encode_thumb(const ThumbnailBufferPtr &buffer, const int quality) { auto result = std::vector(); - int quality = 75; + // Creating a custom deleter for the compressInfo pointer // to ensure ::jpeg_destroy_compress() gets called even if // we throw out of this function. @@ -196,11 +199,11 @@ std::vector TDCHelperActor::encode_thumb(const ThumbnailBufferPtr &bu compressInfo->in_color_space = ::JCS_RGB; compressInfo->err = ::jpeg_std_error(&m_errorMgr); ::jpeg_set_defaults(compressInfo.get()); - ::jpeg_set_quality(compressInfo.get(), quality, TRUE); + ::jpeg_set_quality(compressInfo.get(), quality, boolean(1)); jpeg_stdvector_dest(compressInfo.get(), result); - ::jpeg_start_compress(compressInfo.get(), TRUE); + ::jpeg_start_compress(compressInfo.get(), boolean(1)); size_t row_stride = buffer->width() * 3; for (size_t i = 0; i < buffer->height(); i++) { @@ -247,7 +250,7 @@ ThumbnailBufferPtr TDCHelperActor::decode_thumb(const std::vector &bu reinterpret_cast(buffer.data()), buffer.size()); - int rc = ::jpeg_read_header(decompressInfo.get(), TRUE); + int rc = ::jpeg_read_header(decompressInfo.get(), boolean(1)); if (rc != 1) { throw std::runtime_error("File does not seem to be a normal JPEG"); } @@ -313,11 +316,7 @@ TDCHelperActor::TDCHelperActor(caf::actor_config &cfg) : caf::event_based_actor( try { fs::last_write_time( thumbnail_path(path, thumb), std::filesystem::file_time_type::clock::now()); -#ifdef _WIN32 return read_decode_thumb(thumbnail_path(path, thumb).string()); -#else - return read_decode_thumb(thumbnail_path(path, thumb)); -#endif } catch (const std::exception &err) { return make_error(xstudio_error::error, err.what()); } @@ -325,6 +324,16 @@ TDCHelperActor::TDCHelperActor(caf::actor_config &cfg) : caf::event_based_actor( return ThumbnailBufferPtr(); }, + [=](media_reader::get_thumbnail_atom, + const ThumbnailBufferPtr &buffer, + const int quality) -> result> { + try { + return encode_thumb(buffer, quality); + } catch (const std::exception &err) { + return make_error(xstudio_error::error, err.what()); + } + }, + [=](media_reader::get_thumbnail_atom, const ThumbnailBufferPtr &buffer) -> result> { try { @@ -358,11 +367,8 @@ TDCHelperActor::TDCHelperActor(caf::actor_config &cfg) : caf::event_based_actor( thumb_path.parent_path().string()); } } -#ifdef _WIN32 return encode_save_thumb(thumbnail_path(path, thumb).string(), buffer); -#else - return encode_save_thumb(thumbnail_path(path, thumb), buffer); -#endif + } catch (const std::exception &err) { return make_error(xstudio_error::error, err.what()); } @@ -382,7 +388,8 @@ TDCHelperActor::TDCHelperActor(caf::actor_config &cfg) : caf::event_based_actor( return dcs; }, [=](media_cache::erase_atom atom, const std::string &path, const size_t &thumb) { - delegate(caf::actor_cast(this), atom, path, std::vector{thumb}); + return mail(atom, path, std::vector{thumb}) + .delegate(caf::actor_cast(this)); }, [=](media_cache::erase_atom, const std::string &path, @@ -407,16 +414,19 @@ ThumbnailDiskCacheActor::ThumbnailDiskCacheActor(caf::actor_config &cfg) auto event_group_ = spawn(this); link_to(event_group_); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" pool_ = caf::actor_pool::make( - system().dummy_execution_unit(), + system(), 5, [&] { return system().spawn(); }, caf::actor_pool::round_robin()); link_to(pool_); +#pragma GCC diagnostic pop + + thumb_gen_middleman_ = system().registry().template get(media_reader_registry); - thumb_gen_middleman_ = spawn(); - link_to(thumb_gen_middleman_); behavior_.assign( [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, @@ -425,14 +435,21 @@ ThumbnailDiskCacheActor::ThumbnailDiskCacheActor(caf::actor_config &cfg) return true; }, + // convert to jpg + [=](media_reader::get_thumbnail_atom, + const ThumbnailBufferPtr &buffer, + const int quality) { + return mail(media_reader::get_thumbnail_atom_v, buffer, quality).delegate(pool_); + }, + // convert to jpg [=](media_reader::get_thumbnail_atom, const ThumbnailBufferPtr &buffer) { - delegate(pool_, media_reader::get_thumbnail_atom_v, buffer); + return mail(media_reader::get_thumbnail_atom_v, buffer).delegate(pool_); }, // convert to ThumbnailBufferPtr [=](media_reader::get_thumbnail_atom, const std::vector &buffer) { - delegate(pool_, media_reader::get_thumbnail_atom_v, buffer); + return mail(media_reader::get_thumbnail_atom_v, buffer).delegate(pool_); }, [=](media_reader::get_thumbnail_atom, @@ -443,7 +460,7 @@ ThumbnailDiskCacheActor::ThumbnailDiskCacheActor(caf::actor_config &cfg) auto rp = make_response_promise(); auto thumbkey = ThumbnailKey(mptr, hash, thumb_size); // check for file in cache - // spdlog::warn("{} {} {} {} {}", to_string(mptr.uri_), mptr.frame_, hash, + // spdlog::warn("{} {} {} {} {}", to_string(mptr.uri()), mptr.frame(), hash, // thumbkey.hash(), cache_.cache_.count(thumbkey.hash())); if (cache_.cache_.count(thumbkey.hash())) request_read_of_thumbnail(rp, thumbkey.hash()); @@ -489,7 +506,8 @@ ThumbnailDiskCacheActor::ThumbnailDiskCacheActor(caf::actor_config &cfg) cache_path_pref_ = uri; cache_path_ = fspath; cache_ = DiskCacheStat(); - request(pool_, infinite, cache_stats_atom_v, cache_path_.string()) + mail(cache_stats_atom_v, cache_path_.string()) + .request(pool_, infinite) .then( [=](const DiskCacheStat &dcs) { cache_ = dcs; }, [=](const caf::error &err) { @@ -510,7 +528,8 @@ void ThumbnailDiskCacheActor::on_exit() {} void ThumbnailDiskCacheActor::request_read_of_thumbnail( caf::typed_response_promise rp, const size_t hash) { - request(pool_, infinite, media_reader::get_thumbnail_atom_v, cache_path_.string(), hash) + mail(media_reader::get_thumbnail_atom_v, cache_path_.string(), hash) + .request(pool_, infinite) .then( [=](const ThumbnailBufferPtr &buf) mutable { if (buf) { @@ -534,21 +553,16 @@ void ThumbnailDiskCacheActor::request_generation_of_thumbnail( const bool cache_to_disk) { auto thumbkey = ThumbnailKey(mptr, hash, thumb_size); - request( - thumb_gen_middleman_, infinite, media_reader::get_thumbnail_atom_v, mptr, thumb_size) + mail(media_reader::get_thumbnail_atom_v, mptr, thumb_size) + .request(thumb_gen_middleman_, infinite) .then( [=](const ThumbnailBufferPtr &buf) mutable { rp.deliver(buf); if (cache_to_disk and max_cache_count_ and max_cache_size_) { // add to disk cache, check limits - request( - pool_, - infinite, - media_cache::store_atom_v, - cache_path_.string(), - thumbkey.hash(), - buf) + mail(media_cache::store_atom_v, cache_path_.string(), thumbkey.hash(), buf) + .request(pool_, infinite) .then( [=](const size_t size) { // make room ? update stats.. @@ -568,7 +582,8 @@ void ThumbnailDiskCacheActor::request_generation_of_thumbnail( void ThumbnailDiskCacheActor::evict_thumbnails(const std::vector &hashes) { if (not hashes.empty()) { - request(pool_, infinite, media_cache::erase_atom_v, cache_path_.string(), hashes) + mail(media_cache::erase_atom_v, cache_path_.string(), hashes) + .request(pool_, infinite) .then( [=](const bool) {}, [=](const caf::error &err) { diff --git a/src/thumbnail/src/thumbnail_manager_actor.cpp b/src/thumbnail/src/thumbnail_manager_actor.cpp index c849af536..2275cf50b 100644 --- a/src/thumbnail/src/thumbnail_manager_actor.cpp +++ b/src/thumbnail/src/thumbnail_manager_actor.cpp @@ -1,4 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 +#include + #include #include "xstudio/atoms.hpp" @@ -38,7 +40,7 @@ ThumbnailManagerActor::ThumbnailManagerActor(caf::actor_config &cfg) auto prefs = GlobalStoreHelper(system()); JsonStore j; join_broadcast(this, prefs.get_group(j)); - anon_send(this, json_store::update_atom_v, j); + anon_mail(json_store::update_atom_v, j).send(this); } catch (...) { } @@ -53,42 +55,16 @@ ThumbnailManagerActor::ThumbnailManagerActor(caf::actor_config &cfg) behavior_.assign( [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](media_reader::cancel_thumbnail_request_atom atom, const utility::Uuid job_uuid) { - for (auto i = request_queue_.begin(); i != request_queue_.end(); ++i) { - auto rp = std::get<0>(*i); - const utility::Uuid j_uuid = std::get<5>(*i); - if (j_uuid == job_uuid) { - rp.deliver(make_error(xstudio_error::error, "Cancelled")); - request_queue_.erase(i); - break; - } - } - }, - [=](media_reader::get_thumbnail_atom) { process_queue(); }, [=](media_reader::get_thumbnail_atom atom, const media::AVFrameID &mptr) { - delegate( - caf::actor_cast(this), - atom, - mptr, - size_t(thumb_size_), - size_t(0), - true, - utility::Uuid()); + return mail(atom, mptr, size_t(thumb_size_), size_t(0), true) + .delegate(caf::actor_cast(this)); }, - [=](media_reader::get_thumbnail_atom atom, - const media::AVFrameID &mptr, - const utility::Uuid &job_uuid) { - delegate( - caf::actor_cast(this), - atom, - mptr, - size_t(thumb_size_), - size_t(0), - true, - job_uuid); + [=](media_reader::get_thumbnail_atom atom, const media::AVFrameID &mptr) { + return mail(atom, mptr, size_t(thumb_size_), size_t(0), true) + .delegate(caf::actor_cast(this)); }, [=](utility::clear_atom atom, @@ -105,11 +81,13 @@ ThumbnailManagerActor::ThumbnailManagerActor(caf::actor_config &cfg) else if (disk_cache) target = dsk_cache_; - request(target, infinite, atom) + mail(atom) + .request(target, infinite) .then( [=](const bool) mutable { if (mem_cache and disk_cache) { - request(dsk_cache_, infinite, atom) + mail(atom) + .request(dsk_cache_, infinite) .then( [=](const bool) mutable { rp.deliver(true); }, [=](const caf::error &err) mutable { rp.deliver(err); }); @@ -124,31 +102,16 @@ ThumbnailManagerActor::ThumbnailManagerActor(caf::actor_config &cfg) [=](media_cache::count_atom atom, const bool memory_cache) { if (memory_cache) - delegate(mem_cache_, atom); + return mail(atom).delegate(mem_cache_); else - delegate(dsk_cache_, atom); + return mail(atom).delegate(dsk_cache_); }, [=](media_cache::size_atom atom, const bool memory_cache) { if (memory_cache) - delegate(mem_cache_, atom); + return mail(atom).delegate(mem_cache_); else - delegate(dsk_cache_, atom); - }, - - [=](media_reader::get_thumbnail_atom atom, - const media::AVFrameID &mptr, - const size_t thumb_size, - const size_t hash, - const bool cache_to_disk) { - delegate( - caf::actor_cast(this), - atom, - mptr, - thumb_size, - hash, - cache_to_disk, - Uuid::generate()); + return mail(atom).delegate(dsk_cache_); }, // get thumb from memory cache. @@ -164,52 +127,68 @@ ThumbnailManagerActor::ThumbnailManagerActor(caf::actor_config &cfg) const std::string &key, const size_t thumb_size, const ThumbnailBufferPtr &buf) { - anon_send(mem_cache_, atom, ThumbnailKey(key, thumb_size).hash(), buf); + anon_mail(atom, ThumbnailKey(key, thumb_size).hash(), buf).send(mem_cache_); + }, + + [=](media_reader::get_thumbnail_atom, + const media::AVFrameID &mptr, + const size_t thumb_size, + const size_t hash, + const bool cache_to_disk) -> result { + // try cache.. + // cache key will differ from media keys. + auto rp = make_response_promise(); + + request_buffer(rp, mptr, thumb_size, hash, cache_to_disk); + + return rp; }, [=](media_reader::get_thumbnail_atom, const media::AVFrameID &mptr, const size_t thumb_size, const size_t hash, - const bool cache_to_disk, - const utility::Uuid &job_uuid) -> result { + const bool cache_to_disk) -> result { // try cache.. // cache key will differ from media keys. auto rp = make_response_promise(); - request_buffer(rp, mptr, thumb_size, hash, cache_to_disk, job_uuid); + request_buffer(rp, mptr, thumb_size, hash, cache_to_disk); return rp; }, + // convert to jpg + [=](media_reader::get_thumbnail_atom, + const ThumbnailBufferPtr &buffer, + const int quality) { + return mail(media_reader::get_thumbnail_atom_v, buffer, quality) + .delegate(dsk_cache_); + }, + // convert to jpg [=](media_reader::get_thumbnail_atom, const ThumbnailBufferPtr &buffer) { - delegate(dsk_cache_, media_reader::get_thumbnail_atom_v, buffer); + return mail(media_reader::get_thumbnail_atom_v, buffer).delegate(dsk_cache_); }, // convert from jpg [=](media_reader::get_thumbnail_atom, const std::vector &buffer) { - delegate(dsk_cache_, media_reader::get_thumbnail_atom_v, buffer); + return mail(media_reader::get_thumbnail_atom_v, buffer).delegate(dsk_cache_); }, [=](media_reader::get_thumbnail_atom atom, const media::AVFrameID &mptr, const size_t hash, const bool cache_to_disk) { - delegate( - caf::actor_cast(this), - atom, - mptr, - thumb_size_, - hash, - cache_to_disk); + return mail(atom, mptr, thumb_size_, hash, cache_to_disk) + .delegate(caf::actor_cast(this)); }, [=](json_store::update_atom, const JsonStore & /*change*/, const std::string & /*path*/, const JsonStore &full) { - delegate(actor_cast(this), json_store::update_atom_v, full); + return mail(json_store::update_atom_v, full).delegate(actor_cast(this)); }, [=](json_store::update_atom, const JsonStore &js) mutable { @@ -218,7 +197,7 @@ ThumbnailManagerActor::ThumbnailManagerActor(caf::actor_config &cfg) preference_value(js, "/core/thumbnail/memory_cache/max_count"); if (mem_max_count != new_size_t) { mem_max_count = new_size_t; - anon_send(mem_cache_, media_cache::count_atom_v, mem_max_count); + anon_mail(media_cache::count_atom_v, mem_max_count).send(mem_cache_); spdlog::debug( "set mem_max_count {} {}", __PRETTY_FUNCTION__, mem_max_count); } @@ -228,7 +207,7 @@ ThumbnailManagerActor::ThumbnailManagerActor(caf::actor_config &cfg) 1024 * 1024; if (mem_max_size != new_size_t) { mem_max_size = new_size_t; - anon_send(mem_cache_, media_cache::size_atom_v, mem_max_size); + anon_mail(media_cache::size_atom_v, mem_max_size).send(mem_cache_); spdlog::debug("set mem_max_size {} {}", __PRETTY_FUNCTION__, mem_max_size); } @@ -237,7 +216,7 @@ ThumbnailManagerActor::ThumbnailManagerActor(caf::actor_config &cfg) preference_value(js, "/core/thumbnail/disk_cache/max_count"); if (dsk_max_count != new_size_t) { dsk_max_count = new_size_t; - anon_send(dsk_cache_, media_cache::count_atom_v, dsk_max_count); + anon_mail(media_cache::count_atom_v, dsk_max_count).send(dsk_cache_); spdlog::debug( "set dsk_max_count {} {}", __PRETTY_FUNCTION__, dsk_max_count); } @@ -247,7 +226,7 @@ ThumbnailManagerActor::ThumbnailManagerActor(caf::actor_config &cfg) 1024; if (dsk_max_size != new_size_t) { dsk_max_size = new_size_t; - anon_send(dsk_cache_, media_cache::size_atom_v, dsk_max_size); + anon_mail(media_cache::size_atom_v, dsk_max_size).send(dsk_cache_); spdlog::debug("set dsk_max_size {} {}", __PRETTY_FUNCTION__, dsk_max_size); } @@ -257,10 +236,8 @@ ThumbnailManagerActor::ThumbnailManagerActor(caf::actor_config &cfg) if (cache_path != new_string) { cache_path = new_string; - anon_send( - dsk_cache_, - thumbnail::cache_path_atom_v, - posix_path_to_uri(cache_path)); + anon_mail(thumbnail::cache_path_atom_v, posix_path_to_uri(cache_path)) + .send(dsk_cache_); spdlog::debug("set cache_path {} {}", __PRETTY_FUNCTION__, cache_path); } @@ -278,10 +255,9 @@ ThumbnailManagerActor::ThumbnailManagerActor(caf::actor_config &cfg) void ThumbnailManagerActor::on_exit() { // fufil pending queries. - while (not request_queue_.empty()) { - auto [r, m, t, h, c, i] = request_queue_.front(); - r.deliver(make_error(xstudio_error::error, "System shutting down")); - request_queue_.pop_front(); + auto err = make_error(xstudio_error::error, "System shutting down"); + for (auto &p : request_queue_) { + p->deliver(err); } system().registry().erase(thumbnail_manager_registry); @@ -292,12 +268,12 @@ void ThumbnailManagerActor::request_buffer( const media::AVFrameID &mptr, const size_t thumb_size, const size_t hash, - const bool cache_to_disk, - const utility::Uuid &job_uuid) { + const bool cache_to_disk) { auto key = ThumbnailKey(mptr, hash, thumb_size).hash(); - request(mem_cache_, infinite, media_cache::retrieve_atom_v, key) + mail(media_cache::retrieve_atom_v, key) + .request(mem_cache_, infinite) .then( [=](const ThumbnailBufferPtr &buf) mutable { if (buf) { @@ -305,17 +281,43 @@ void ThumbnailManagerActor::request_buffer( } else { // add to queue, if to many requests or already inflight. bool start_loop = request_queue_.empty(); - request_queue_.emplace_back( - std::make_tuple(rp, mptr, thumb_size, hash, cache_to_disk, job_uuid)); + queue_thumbnail_request(rp, mptr, thumb_size, hash, cache_to_disk); if (start_loop) - anon_send( - caf::actor_cast(this), - media_reader::get_thumbnail_atom_v); + anon_mail(media_reader::get_thumbnail_atom_v) + .send(caf::actor_cast(this)); } }, [=](const caf::error &err) mutable { rp.deliver(err); }); } +void ThumbnailManagerActor::queue_thumbnail_request( + caf::typed_response_promise rp, + const media::AVFrameID &mptr, + const size_t thumb_size, + const size_t hash, + const bool cache_to_disk) { + + for (auto p = request_queue_.begin(); p != request_queue_.end(); ++p) { + if ((*p)->mptr == mptr && (*p)->size == thumb_size && (*p)->hash == hash && + (*p)->cache_to_disk == cache_to_disk) { + // we put it at the end of the queue, so it gets processed next + auto tn_request = *p; + tn_request->response_promises.push_back(rp); + request_queue_.erase(p); + request_queue_.push_back(tn_request); + return; + } + } + + auto tn_request = std::make_shared(); + tn_request->mptr = mptr; + tn_request->size = thumb_size; + tn_request->hash = hash; + tn_request->cache_to_disk = cache_to_disk; + tn_request->response_promises.push_back(rp); + request_queue_.push_back(tn_request); +} + void ThumbnailManagerActor::request_buffer( caf::typed_response_promise rp, const std::string &key, @@ -323,7 +325,8 @@ void ThumbnailManagerActor::request_buffer( auto tkey = ThumbnailKey(key, thumb_size).hash(); - request(mem_cache_, infinite, media_cache::retrieve_atom_v, tkey) + mail(media_cache::retrieve_atom_v, tkey) + .request(mem_cache_, infinite) .then( [=](const ThumbnailBufferPtr &buf) mutable { if (buf) { @@ -341,74 +344,88 @@ void ThumbnailManagerActor::process_queue() { if (!request_queue_.size()) return; - auto r = request_queue_.front(); + ThumbnailRequestPtr tn_request; + + // N.b.We process the requests most recent first. This is because the most + // recent requests are made by the UI to draw thumbnails in the media list. + // If the user scrolls through the media list we get a lot of thumbnail + // requests, but we want to process the most recent first so the thumbnails + // needed to go onscreen are provided immediately and the ones that were + // scrolled past earlier are lower priority. + + // get the last request that isn't already in flight + for (auto p = request_queue_.rbegin(); p != request_queue_.rend(); p++) { + if (!(*p)->in_flight) { + tn_request = (*p); + break; + } + } + + if (!tn_request) + return; - // unpack this tuple! - caf::typed_response_promise response_promise = std::get<0>(r); - media::AVFrameID media_ptr = std::get<1>(r); - const size_t thumb_size = std::get<2>(r); - const size_t hash = std::get<3>(r); - const bool cache_to_disk = std::get<4>(r); - const utility::Uuid job_id = std::get<5>(r); + tn_request->in_flight = true; - auto key = ThumbnailKey(media_ptr, hash, thumb_size).hash(); + auto key = ThumbnailKey(tn_request->mptr, tn_request->hash, tn_request->size).hash(); auto continue_func = [=]() { - for (auto i = request_queue_.begin(); i != request_queue_.end(); ++i) { - const utility::Uuid j_uuid = std::get<5>(*i); - if (j_uuid == job_id) { - request_queue_.erase(i); - break; - } - } // delayed send prevents 'tight loop' in the message queue, // so incoming messages such as cancelled thumbnail requests // are able to be adressed between processing thumbnails + for (auto p = request_queue_.begin(); p != request_queue_.end(); p++) { + if ((*p) == tn_request) { + request_queue_.erase(p); + break; + } + } if (request_queue_.size()) { - delayed_anon_send( - caf::actor_cast(this), - std::chrono::milliseconds(5), - media_reader::get_thumbnail_atom_v); + anon_mail(media_reader::get_thumbnail_atom_v) + .delay(std::chrono::milliseconds(5)) + .send(caf::actor_cast(this)); } }; - request(mem_cache_, infinite, media_cache::retrieve_atom_v, key) + mail(media_cache::retrieve_atom_v, key) + .request(mem_cache_, infinite) .then( [=](const ThumbnailBufferPtr &buf) mutable { if (buf) { - response_promise.deliver(buf); + tn_request->deliver(buf); continue_func(); } else { - request( - dsk_cache_, - infinite, + mail( media_reader::get_thumbnail_atom_v, - media_ptr, - thumb_size, - hash, - cache_to_disk) + tn_request->mptr, + tn_request->size, + tn_request->hash, + tn_request->cache_to_disk) + .request(dsk_cache_, infinite) .then( [=](ThumbnailBufferPtr &buf) mutable { // if valid add to memory cache if (buf) - anon_send( - mem_cache_, + anon_mail( media_cache::store_atom_v, - ThumbnailKey(media_ptr, hash, thumb_size).hash(), - buf); + ThumbnailKey( + tn_request->mptr, + tn_request->hash, + tn_request->size) + .hash(), + buf) + .send(mem_cache_); // deliver buffer. - response_promise.deliver(buf); + tn_request->deliver(buf); continue_func(); }, [=](const caf::error &err) mutable { - response_promise.deliver(err); + tn_request->deliver(err); continue_func(); }); } }, [=](const caf::error &err) mutable { - request_queue_.pop_front(); + tn_request->deliver(err); continue_func(); }); } diff --git a/src/thumbnail/test/CMakeLists.txt b/src/thumbnail/test/CMakeLists.txt index 1ed8d2b7e..b99b75ad0 100644 --- a/src/thumbnail/test/CMakeLists.txt +++ b/src/thumbnail/test/CMakeLists.txt @@ -3,7 +3,8 @@ include(CTest) SET(LINK_DEPS xstudio::thumbnail xstudio::utility - caf::core + xstudio::media + CAF::core ) create_tests("${LINK_DEPS}") diff --git a/src/timeline/src/CMakeLists.txt b/src/timeline/src/CMakeLists.txt index a2f595024..63652e108 100644 --- a/src/timeline/src/CMakeLists.txt +++ b/src/timeline/src/CMakeLists.txt @@ -1,26 +1,30 @@ - -#find_package(OpenTime REQUIRED) -#find_package(OpenTimelineIO REQUIRED) -#find_package(Imath) - - +find_package(Imath REQUIRED) +find_package(fmt REQUIRED) +if(NOT ${OTIO_SUBMODULE}) + find_package(OpenTime REQUIRED) + find_package(OpenTimelineIO REQUIRED) +endif() SET(LINK_DEPS - xstudio::playhead xstudio::utility - caf::core - #OTIO::opentime - #OTIO::opentimelineio - #Imath::Imath + xstudio::media + xstudio::playhead + CAF::core + Imath::Imath + fmt::fmt + OTIO::opentime + OTIO::opentimelineio ) SET(STATIC_LINK_DEPS xstudio::utility_static - caf::core - #Imath::Imath - #OTIO::opentime - #OTIO::opentimelineio + xstudio::media_static + CAF::core + Imath::Imath + fmt::fmt + OTIO::opentime + OTIO::opentimelineio ) -create_component_static(timeline 0.1.0 "${LINK_DEPS}" "${STATIC_LINK_DEPS}") +create_component_static(timeline ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${STATIC_LINK_DEPS}") diff --git a/src/timeline/src/clip.cpp b/src/timeline/src/clip.cpp index 2f07eb906..c7aa9f185 100644 --- a/src/timeline/src/clip.cpp +++ b/src/timeline/src/clip.cpp @@ -1,9 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include #include "xstudio/timeline/clip.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/json_store.hpp" using namespace xstudio::timeline; using namespace xstudio; @@ -29,6 +26,7 @@ Clip::Clip(const utility::JsonStore &jsn) : Container(static_cast(jsn.at("container"))), item_(static_cast(jsn.at("item"))) { + // hack for old data if (jsn.count("media_uuid")) { media_uuid_ = jsn.at("media_uuid"); auto jsn = R"({"media_uuid": null})"_json; @@ -39,6 +37,13 @@ Clip::Clip(const utility::JsonStore &jsn) } } +Clip::Clip(const Item &item, const caf::actor &actor) + : Container(item.name(), "Clip", item.uuid()), item_(item.clone()) { + media_uuid_ = item_.prop().value("media_uuid", utility::Uuid()); + item_.set_actor_addr(caf::actor_cast(actor)); +} + + Clip Clip::duplicate() const { utility::JsonStore jsn; diff --git a/src/timeline/src/clip_actor.cpp b/src/timeline/src/clip_actor.cpp index 3303c4ec0..aae2bc574 100644 --- a/src/timeline/src/clip_actor.cpp +++ b/src/timeline/src/clip_actor.cpp @@ -1,21 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include "xstudio/atoms.hpp" -#include "xstudio/broadcast/broadcast_actor.hpp" -#include "xstudio/media/media.hpp" +#include "xstudio/thumbnail/thumbnail.hpp" #include "xstudio/timeline/clip_actor.hpp" -#include "xstudio/timeline/stack_actor.hpp" -#include "xstudio/timeline/timeline_actor.hpp" -#include "xstudio/timeline/track_actor.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" -#include "xstudio/utility/uuid.hpp" using namespace xstudio; using namespace xstudio::utility; using namespace xstudio::timeline; -using namespace caf; +using namespace std::literals; ClipActor::ClipActor(caf::actor_config &cfg, const JsonStore &jsn) : caf::event_based_actor(cfg), base_(static_cast(jsn.at("base"))) { @@ -30,45 +26,75 @@ ClipActor::ClipActor(caf::actor_config &cfg, const JsonStore &jsn, Item &pitem) base_.item().set_actor_addr(this); base_.item().set_system(&system()); - pitem = base_.item(); + pitem = base_.item().clone(); init(); } +ClipActor::ClipActor(caf::actor_config &cfg, const Item &item) + : caf::event_based_actor(cfg), base_(item, this) { + base_.item().set_system(&system()); + init(); +} + +ClipActor::ClipActor(caf::actor_config &cfg, const Item &item, Item &nitem) + : ClipActor(cfg, item) { + nitem = base_.item().clone(); +} + ClipActor::ClipActor( - caf::actor_config &cfg, - const utility::UuidActor &media, - const std::string &name, - const utility::Uuid &uuid) + caf::actor_config &cfg, const UuidActor &media, const std::string &name, const Uuid &uuid) : caf::event_based_actor(cfg), // playlist_(caf::actor_cast(playlist)), base_(name, uuid, this, media.uuid()) { base_.item().set_system(&system()); - base_.item().set_name(name); + + // already done in base clase ? + // base_.item().set_name(name); if (media.actor()) { media_ = caf::actor_cast(media.actor()); - monitor(media.actor()); + // monitor_media(media.actor()); + join_event_group(this, media.actor()); try { caf::scoped_actor sys(system()); - auto ref = request_receive>( - *sys, media.actor(), media::media_reference_atom_v)[0]; + auto ref = request_receive>( + *sys, media.actor(), media::media_reference_atom_v, Uuid()) + .second; if (name.empty()) { - base_.item().set_name( - fs::path(uri_to_posix_path(ref.uri())).filename().string()); + mail(name_atom_v) + .delay(1s) + .request(media.actor(), infinite) + .then( + [=](const std::string &value) mutable { + anon_mail(item_name_atom_v, value).send(this); + }, + [=](const caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + // base_.item().set_name( + // fs::path(uri_to_posix_path(ref.uri())).filename().string()); } - if (ref.frame_count()) - base_.item().set_available_range(utility::FrameRange(ref.duration())); - else - delayed_send( - caf::actor_cast(this), - std::chrono::milliseconds(100), - media::acquire_media_detail_atom_v); + if (ref.frame_count()) { + // base_.item().set_available_range(FrameRange(ref.duration())); + auto tc_start = static_cast(ref.timecode().total_frames()); + + auto mfr = FrameRange( + FrameRateDuration(tc_start, ref.duration().rate()), ref.duration()); + base_.item().set_range(mfr, mfr); + + auto m_actor = + system().registry().template get(media_hook_registry); + anon_mail(media_hook::get_clip_hook_atom_v, this).send(m_actor); + } else + mail(media::acquire_media_detail_atom_v) + .delay(std::chrono::milliseconds(100)) + .send(caf::actor_cast(this)); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -78,97 +104,215 @@ ClipActor::ClipActor( init(); } +void ClipActor::link_media( + caf::typed_response_promise rp, const UuidActor &media, const bool refresh) { -void ClipActor::init() { - print_on_create(this, base_.name()); - print_on_exit(this, base_.name()); - - event_group_ = spawn(this); - link_to(event_group_); + // spdlog::warn("link_media_atom {}", to_string(media)); + auto old_media_actor = caf::actor_cast(media_); + if (old_media_actor) + leave_event_group(this, old_media_actor); - // we die if our media dies. - set_down_handler([=](down_msg &msg) { - if (msg.source == media_) { - demonitor(caf::actor_cast(media_)); - send_exit(this, caf::exit_reason::user_shutdown); + if (media.actor()) { + // monitor(media.actor()); + join_event_group(this, media.actor()); + media_ = caf::actor_cast(media.actor()); + } else + media_ = caf::actor_addr(); + + auto jsn = base_.set_media_uuid(media.uuid()); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + + // clear ptr cache + image_ptr_cache_.clear(); + audio_ptr_cache_.clear(); + + if (refresh) { + // force update ? + // cache available range ? + if (media.actor()) { + mail(media::acquire_media_detail_atom_v) + .delay(std::chrono::milliseconds(100)) + .send(caf::actor_cast(this)); + + // replace clip name ? + mail(name_atom_v) + .request(media.actor(), infinite) + .then( + [=](const std::string &value) mutable { + anon_mail(item_name_atom_v, value).send(this); + }, + [=](const caf::error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } else { + anon_mail(item_name_atom_v, "NO CLIP").send(this); + auto m_actor = system().registry().template get(media_hook_registry); + anon_mail(media_hook::get_clip_hook_atom_v, this).send(m_actor); } - }); - - // monitor changes in media.. - behavior_.assign( - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), - - [=](link_media_atom, const UuidActorMap &media) -> bool { - if (media.count(base_.media_uuid())) { - auto media_actor = media.at(base_.media_uuid()); - auto addr = caf::actor_cast(media_actor); + } + + rp.deliver(true); +} + +caf::message_handler ClipActor::message_handler() { + return caf::message_handler{ + // replace current media.. + [=](link_media_atom, const UuidActor &media) -> result { + auto rp = make_response_promise(); + link_media(rp, media); + return rp; + }, - if (media_ != addr) { - monitor(media_actor); - join_event_group(this, media_actor); - media_ = addr; + [=](link_media_atom, const UuidActorMap &media, const bool force) -> result { + auto rp = make_response_promise(); + + if (media.count(base_.media_uuid())) { + if (force) { + link_media(rp, UuidActor(base_.media_uuid(), media.at(base_.media_uuid()))); + } else { + auto media_actor = media.at(base_.media_uuid()); + auto addr = caf::actor_cast(media_actor); + + if (media_ != addr) { + // monitor_media(media_actor); + // shouldn't we leave previous events ? + // leave_event_group(this, media_); + join_event_group(this, media_actor); + media_ = addr; + } + rp.deliver(true); } } else { media_ = caf::actor_addr(); + rp.deliver(true); } - return true; + + return rp; }, + [=](playhead::source_atom, + const UuidUuidMap &swap, + const UuidActorMap &media) -> result { + auto rp = make_response_promise(); + + if (swap.count(base_.media_uuid())) { + // spdlog::warn("{} {} {}", base_.item().name(), to_string(base_.media_uuid()), + // to_string(media.at(swap.at(base_.media_uuid())))); + link_media( + rp, + UuidActor( + swap.at(base_.media_uuid()), media.at(swap.at(base_.media_uuid()))), + false); + } else + rp.deliver(false); + + return rp; + }, + + [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) {}, [=](plugin_manager::enable_atom, const bool value) -> JsonStore { auto jsn = base_.item().set_enabled(value); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, [=](item_name_atom, const std::string &value) -> JsonStore { auto jsn = base_.item().set_name(value); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_type_atom) -> ItemType { return base_.item().item_type(); }, + + [=](item_lock_atom, const bool value) -> JsonStore { + auto jsn = base_.item().set_locked(value); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, [=](item_flag_atom, const std::string &value) -> JsonStore { auto jsn = base_.item().set_flag(value); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](rate_atom) -> FrameRate { return base_.item().rate(); }, + + [=](rate_atom atom, const media::MediaType media_type) { + return mail(atom).delegate(caf::actor_cast(this)); + }, + + [=](item_prop_atom, const JsonStore &value) -> JsonStore { + auto jsn = base_.item().set_prop(value); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_prop_atom, const JsonStore &value, const bool merge) -> JsonStore { + auto p = base_.item().prop(); + p.update(value); + auto jsn = base_.item().set_prop(p); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_prop_atom, const JsonStore &value, const std::string &path) -> JsonStore { + auto prop = base_.item().prop(); + try { + auto ptr = nlohmann::json::json_pointer(path); + prop.at(ptr).update(value, true); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + auto jsn = base_.item().set_prop(prop); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, + [=](item_prop_atom) -> JsonStore { return base_.item().prop(); }, + [=](active_range_atom, const FrameRange &fr) -> JsonStore { auto jsn = base_.item().set_active_range(fr); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, [=](available_range_atom, const FrameRange &fr) -> JsonStore { auto jsn = base_.item().set_available_range(fr); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, - [=](active_range_atom) -> std::optional { + [=](active_range_atom) -> std::optional { return base_.item().active_range(); }, - [=](available_range_atom) -> std::optional { + [=](available_range_atom) -> std::optional { return base_.item().available_range(); }, - [=](trimmed_range_atom) -> utility::FrameRange { return base_.item().trimmed_range(); }, + [=](trimmed_range_atom) -> FrameRange { return base_.item().trimmed_range(); }, + + [=](trimmed_range_atom, + const FrameRange &avail, + const FrameRange &active) -> JsonStore { + auto jsn = base_.item().set_range(avail, active); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, [=](history::undo_atom, const JsonStore &hist) -> result { base_.item().undo(hist); @@ -180,37 +324,31 @@ void ClipActor::init() { return true; }, - [=](item_atom) -> Item { return base_.item(); }, + [=](event_atom, item_atom, const JsonStore &update, const bool hidden) { + // where is this coming from?? + }, + + [=](item_atom) -> Item { return base_.item().clone(); }, [=](item_atom, const bool with_state) -> result> { auto rp = make_response_promise>(); - request(caf::actor_cast(this), infinite, utility::serialise_atom_v) + mail(serialise_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](const JsonStore &jsn) mutable { - rp.deliver(std::make_pair(jsn, base_.item())); + rp.deliver(std::make_pair(jsn, base_.item().clone())); }, [=](const caf::error &err) mutable { rp.deliver(err); }); return rp; }, - // [=](duration_atom) -> utility::FrameRateDuration { return base_.duration(); }, + // [=](duration_atom) -> FrameRateDuration { return base_.duration(); }, - // [=](duration_atom, const utility::FrameRateDuration &duration) -> bool { + // [=](duration_atom, const FrameRateDuration &duration) -> bool { // base_.set_duration(duration); // update_edit_list(); // return true; // }, - // [=](media::get_edit_list_atom, const Uuid &uuid) -> utility::EditList { - // auto edit_list = clip_edit_list_; - - // if (not uuid.is_null()) - // edit_list.set_uuid(uuid); - // else - // edit_list.set_uuid(base_.uuid()); - - // return edit_list; - // }, - // [=](media::get_media_pointer_atom, // const int logical_frame) -> result { // if (base_.media_uuid().is_null()) @@ -228,94 +366,225 @@ void ClipActor::init() { // return UuidActor(base_.media_uuid(), caf::actor_cast(media_)); // }, - // [=](start_time_atom) -> utility::FrameRateDuration { return base_.start_time(); }, + // [=](start_time_atom) -> FrameRateDuration { return base_.start_time(); }, - // [=](start_time_atom, const utility::FrameRateDuration &start_time) -> bool { + // [=](start_time_atom, const FrameRateDuration &start_time) -> bool { // base_.set_start_time(start_time); // update_edit_list(); // return true; // }, - // [&](utility::event_atom, utility::change_atom) { - // request(actor_cast(media_), infinite, media::get_edit_list_atom_v, Uuid()) - // .then( - // [&](const utility::EditList &sl) { - // media_edit_list_ = sl; - // update_edit_list(); - // }, - // [=](const error &err) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); - // }); - // }, - - // [=](utility::event_atom, utility::name_atom, const std::string & /*name*/) {}, + // [=](event_atom, name_atom, const std::string & /*name*/) {}, // events from media actor // re-evaluate media reference.., needed for lazy loading - [=](media::acquire_media_detail_atom) { + // [=](media_reference_atom atom, + // const Uuid &uuid) -> caf::result> { + + [=](playlist::reflag_container_atom) -> result> { + auto rp = make_response_promise>(); auto actor = caf::actor_cast(media_); + if (actor) + rp.delegate(actor, playlist::reflag_container_atom_v); + else + rp.deliver(make_error(xstudio_error::error, "No media assigned.")); + + return rp; + }, + + + [=](media::media_reference_atom, + const media::MediaType media_type) -> result> { + auto rp = make_response_promise>(); + auto actor = caf::actor_cast(media_); + if (actor) + rp.delegate(actor, media::media_reference_atom_v, media_type, Uuid()); + else + rp.deliver(make_error(xstudio_error::error, "No media assigned.")); + + return rp; + }, + + [=](media::acquire_media_detail_atom, const Uuid &uuid) { + auto actor = caf::actor_cast(media_); + // spdlog::warn("acquire_media_detail_atom {} {}", to_string(uuid), + // to_string(actor)); if (actor) { - request(actor, infinite, media::media_reference_atom_v) + mail(media::media_reference_atom_v, uuid) + .request(actor, infinite) .then( - [=](const std::vector &refs) { - if (not refs.empty() and refs[0].frame_count()) { - auto jsn = base_.item().set_available_range( - utility::FrameRange(refs[0].duration())); + [=](const std::pair &ref) { + // spdlog::warn("{}",ref.second.frame_count()); + if (ref.second.frame_count()) { + // clear ptr cache + auto tc_start = + static_cast(ref.second.timecode().total_frames()); + + // spdlog::warn("{} {} {}", base_.item().name(), tc_start, + // ref.second.timecode().to_string()); + + // if(base_.item().available_range()) { + // spdlog::warn("before {} available_range start {} duration + // {}", + // base_.item().name(), + // base_.item().available_range()->frame_start().frames(), + // base_.item().available_range()->frame_duration().frames() + // ); + // spdlog::warn("before {} trimmed start {} duration {}", + // base_.item().name(), + // base_.item().trimmed_range().frame_start().frames(), + // base_.item().trimmed_range().frame_duration().frames() + // ); + // } + // else + // spdlog::warn("no available range + // {}",base_.item().name()); + + auto jsn = base_.item().set_available_range(FrameRange( + FrameRateDuration(tc_start, ref.second.duration().rate()), + ref.second.duration())); + + // spdlog::warn("after {} available_range start {} duration {}", + // base_.item().name(), + // base_.item().available_range()->frame_start().frames(), + // base_.item().available_range()->frame_duration().frames() + // ); + // spdlog::warn("after {} trimmed start {} duration {}", + // base_.item().name(), + // base_.item().trimmed_range().frame_start().frames(), + // base_.item().trimmed_range().frame_duration().frames() + // ); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false) + .send(base_.event_group()); + + auto m_actor = system().registry().template get( + media_hook_registry); + anon_mail(media_hook::get_clip_hook_atom_v, this).send(m_actor); } else { // retry ? - delayed_send( - caf::actor_cast(this), - std::chrono::seconds(1), - media::acquire_media_detail_atom_v); + // spdlog::warn("delayed get detail"); + + mail(media::acquire_media_detail_atom_v, uuid) + .delay(std::chrono::seconds(1)) + .send(caf::actor_cast(this)); } }, - [=](const error &err) {}); + [=](const error &err) { + // spdlog::warn("errored get detail"); + // retry not ready ? Or no sources? + mail(media::acquire_media_detail_atom_v, uuid) + .delay(std::chrono::seconds(1)) + .send(caf::actor_cast(this)); + }); } }, - [=](utility::event_atom, + [=](media::acquire_media_detail_atom atom) { + return mail(atom, Uuid()).delegate(caf::actor_cast(this)); + }, + + [=](event_atom, playlist::reflag_container_atom, const Uuid &, const std::tuple &) {}, - [=](utility::event_atom, - bookmark::bookmark_change_atom, - const utility::Uuid &bookmark_uuid) { + [=](event_atom, bookmark::bookmark_change_atom, const Uuid &bookmark_uuid) { // not sure why we want this.. - // send( - // event_group_, - // utility::event_atom_v, + // mail(// event_atom_v, // bookmark::bookmark_change_atom_v, - // bookmark_uuid); + // bookmark_uuid).send(// event_group_); + }, + [=](event_atom, bookmark::bookmark_change_atom, const Uuid &, const UuidList &) {}, + + [=](event_atom, + media_reader::get_thumbnail_atom, + const thumbnail::ThumbnailBufferPtr &buf) {}, + + [=](event_atom, bookmark::remove_bookmark_atom, const Uuid &) {}, + [=](event_atom, bookmark::add_bookmark_atom, const UuidActor &) {}, + + [=](event_atom, media::media_status_atom, const media::MediaStatus ms) {}, + + [=](event_atom, media::media_display_info_atom, const JsonStore &, caf::actor_addr &) { }, - [=](utility::event_atom, bookmark::remove_bookmark_atom, const utility::Uuid &) {}, - [=](utility::event_atom, bookmark::add_bookmark_atom, const utility::UuidActor &) {}, + [=](event_atom, media::media_display_info_atom, const JsonStore &) {}, - [=](utility::event_atom, media::media_status_atom, const media::MediaStatus ms) {}, - [=](utility::event_atom, + // ak this need's to know if the clip uses audio or video SOB. + // we'll use video for the moment.. BUT THIS IS WRONG. + [=](event_atom, media::current_media_source_atom, - const UuidActor &, - const media::MediaType) { + const UuidActor &ua, + const media::MediaType mt) { + // media source has changed, we need to acquire the new available range... + + // spdlog::warn("media::current_media_source_atom {}", to_string(ua.uuid())); + + if (mt == media::MediaType::MT_IMAGE) { + + mail(media::acquire_media_detail_atom_v, ua.uuid()) + .delay(std::chrono::milliseconds(10)) + .send(caf::actor_cast(this)); + } + + image_ptr_cache_.clear(); + audio_ptr_cache_.clear(); + + mail(event_atom_v, change_atom_v).send(base_.event_group()); + }, + + [=](event_atom, change_atom) { + // something has changed. It means we might need to re-generate our + // frame pointers for playback. For example, if media source metadata + // has changed that affects colour management, we need to re-gernerate + // the frame pointers that carry the colour management data to the + // playhead and up to the viewport. image_ptr_cache_.clear(); audio_ptr_cache_.clear(); + mail(event_atom_v, change_atom_v).send(base_.event_group()); + }, + + [=](json_store::update_atom, + const JsonStore &, + const std::string &, + const JsonStore &) {}, + + [=](json_store::update_atom, const JsonStore &) mutable {}, + + [=](event_atom, media::add_media_source_atom, const UuidActorVector &) {}, + + [=](playlist::get_media_atom, const bool) -> UuidUuidActor { + return UuidUuidActor( + base_.uuid(), + UuidActor(base_.media_uuid(), caf::actor_cast(media_))); + }, + + // how do we deal with lazy loading of metadata.. + [=](playlist::get_media_atom, + json_store::get_json_atom, + const Uuid &uuid, + const std::string &path) -> result { + auto rp = make_response_promise(); + auto actor = caf::actor_cast(media_); + if (actor) + rp.delegate(actor, json_store::get_json_atom_v, uuid, path); + else + rp.deliver(make_error(xstudio_error::error, "No media assigned.")); + + return rp; }, - [=](utility::event_atom, utility::change_atom) {}, - [=](utility::event_atom, - media::add_media_source_atom, - const utility::UuidActorVector &) {}, [=](playlist::get_media_atom) -> UuidActor { return UuidActor(base_.media_uuid(), caf::actor_cast(media_)); }, - [=](xstudio::media::current_media_source_atom atom) -> caf::result { + [=](xstudio::media::current_media_source_atom atom, + const media::MediaType mt) -> caf::result { if (media_) { auto rp = make_response_promise(); - rp.delegate(caf::actor_cast(media_), atom); + rp.delegate(caf::actor_cast(media_), atom, mt); return rp; } return make_error(xstudio_error::error, "No media assigned."); @@ -335,64 +604,85 @@ void ClipActor::init() { const std::vector &timepoints, const FrameRate &override_rate) -> result { if (media_) { - auto result = std::make_shared(timepoints.size()); - auto rp = make_response_promise(); + + + // TODO: Optimise and refactor this!! Too slow (often over > 1ms to + // evaulate) + + // the 'result' here is a vector of std::shared_ptr - + // one element per frame timepoint. We initialise it with blank frames and + // the logic below will fill in actual frames where possible + media::AVFrameID blank = *(media::make_blank_frame( + media_type, base_.media_uuid(), Uuid(), base_.uuid())); + blank.set_frame_status(media::FS_NOT_ON_DISK); + + auto blank_ptr = std::make_shared(blank); + + // the result data + auto result = std::make_shared(timepoints.size(), blank_ptr); + + // the response + auto rp = make_response_promise(); auto trimmed_start = base_.item().trimmed_start(); - auto active_start = - (base_.item().active_start() ? *base_.item().active_start() - : trimmed_start); auto available_start = (base_.item().available_start() ? *base_.item().available_start() : trimmed_start); - // spdlog::warn("trs {} avs {} act {}", trimmed_start.to_seconds(), - // available_start.to_seconds(), active_start.to_seconds()); - - // build logical frame ranges. + // build a set of frame ranges for which we haven't already + // generated AvFrameIDs (and stored in our cache) auto ranges = media::LogicalFrameRanges(); - auto indexs = std::vector(); - auto range = std::pair(-1, -1); + // for each range, we have an index into 'result' + static const int invalid_frame = std::numeric_limits::lowest(); + auto indexs = std::vector(); + auto range_not_in_cache = std::pair(invalid_frame, invalid_frame); + + // get a ref to the appropriate AvFrameID cache + auto &frame_ptr_cache = media_type == media::MediaType::MT_IMAGE + ? image_ptr_cache_ + : audio_ptr_cache_; auto index = 0; + // loop over the timeline timepoints for which we've been asked + // for an AVFrameID for (const auto timepoint : timepoints) { + + // convert the timepoint into a logical frame auto frd = FrameRateDuration( FrameRate(timepoint.to_flicks() - available_start.to_flicks()), override_rate); auto logical_frame = frd.frames(base_.item().rate()); - if (media_type == media::MediaType::MT_IMAGE and - image_ptr_cache_.count(logical_frame)) { - (*result)[index] = image_ptr_cache_[logical_frame]; - if (range.first != -1) { - ranges.push_back(range); - range.first = -1; - range.second = -1; - } - } else if ( - media_type == media::MediaType::MT_AUDIO and - audio_ptr_cache_.count(logical_frame)) { - (*result)[index] = audio_ptr_cache_[logical_frame]; - if (range.first != -1) { - ranges.push_back(range); - range.first = -1; - range.second = -1; + // have we cached an AVFrameID for this frame already? + if (frame_ptr_cache.count(logical_frame)) { + (*result)[index] = frame_ptr_cache[logical_frame]; + + if (range_not_in_cache.first != invalid_frame) { + // previous frame was NOT in cache - store the + // range for which we need AvFrameIDs + ranges.push_back(range_not_in_cache); + range_not_in_cache.first = invalid_frame; + range_not_in_cache.second = invalid_frame; } } else { - // extend or flush rane - if (range.first != -1) { - if (logical_frame != range.second + 1) { - ranges.push_back(range); - range.first = -1; - range.second = -1; + + // extend the frame range for which we need to + if (range_not_in_cache.first != invalid_frame) { + if (logical_frame != range_not_in_cache.second + 1) { + + ranges.push_back(range_not_in_cache); + range_not_in_cache.first = invalid_frame; + range_not_in_cache.second = invalid_frame; } else { - range.second = logical_frame; + range_not_in_cache.second = logical_frame; } } - if (range.first == -1) { - range.first = logical_frame; - range.second = logical_frame; + // start a new range to record frames where we don't + // already have AvFrameIDs in the cache + if (range_not_in_cache.first == invalid_frame) { + range_not_in_cache.first = logical_frame; + range_not_in_cache.second = logical_frame; indexs.push_back(index); } } @@ -400,32 +690,51 @@ void ClipActor::init() { index++; } - if (range.first != -1) - ranges.push_back(range); + if (range_not_in_cache.first != invalid_frame) { + ranges.push_back(range_not_in_cache); + } if (indexs.empty()) { + // all our frames were in the cache rp.deliver(*result); } else { - request( - caf::actor_cast(media_), - infinite, + mail( media::get_media_pointers_atom_v, media_type, ranges, - FrameRate()) + FrameRate(), + base_.uuid()) + .request(caf::actor_cast(media_), infinite) .then( [=](const media::AVFrameIDs &mps) mutable { + // spdlog::warn("const media::AVFrameIDs &mps {} {}", + // base_.item().name(), mps.size()); + auto mp = mps.begin(); + int ct = 0; for (size_t i = 0; i < indexs.size(); i++) { + auto ind = indexs[i]; auto rng = ranges[i]; + for (auto ii = rng.first; ii <= rng.second; ii++, ind++) { + + // if we got our logic above correct this shouldn't + // happen as indexs and ranges match in length and + // mps size is the sum of ranges + if (mp == mps.end()) + break; + + std::shared_ptr foo = *mp; + if (media_type == media::MediaType::MT_IMAGE) { - image_ptr_cache_[ii] = *mp; - (*result)[ind] = image_ptr_cache_[ii]; + image_ptr_cache_[ii] = foo; + (*result)[ind] = foo; // image_ptr_cache_[ii]; + + // spdlog::warn("{}", (*mp)->key_); } else if (media_type == media::MediaType::MT_AUDIO) { - audio_ptr_cache_[ii] = *mp; - (*result)[ind] = audio_ptr_cache_[ii]; + audio_ptr_cache_[ii] = foo; + (*result)[ind] = foo; } mp++; } @@ -434,46 +743,12 @@ void ClipActor::init() { rp.deliver(*result); }, [=](error &err) mutable { - for (size_t i = 0; i < indexs.size(); i++) { - auto ind = indexs[i]; - auto rng = ranges[i]; - for (auto ii = rng.first; ii <= rng.second; ii++, ind++) - (*result)[ind] = media::make_blank_frame(media_type); - } - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + // can get an error for missing media - instead + // of passing error up we allow delivery of + // the blank frames // rp.deliver(std::move(err)); rp.deliver(*result); }); - - // for(size_t i = 0; i < indexs.size(); i++) { - // auto ind = indexs[i]; - // auto rng = ranges[i]; - - // try { - // // FrameRate() might need setting correctly.. - // auto mps = request_receive( - // *sys, caf::actor_cast(media_), - // media::get_media_pointers_atom_v, media_type, - // media::LogicalFrameRanges({rng}), FrameRate()); - - // for(auto mp : mps ){ - // if(media_type == media::MediaType::MT_IMAGE) { - // image_ptr_cache_[ind] = mp; - // (*result)[ind] = image_ptr_cache_[ind]; - // } else if(media_type == media::MediaType::MT_AUDIO) { - // audio_ptr_cache_[ind] = mp; - // (*result)[ind] = audio_ptr_cache_[ind]; - // } - // ind++; - // } - - // } catch (const std::exception &err) { - // for(auto ii = rng.first; ii <= rng.second; ii++, ind++) - // (*result)[ind] = make_blank_frame(media_type); - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - // } - // } - // rp.deliver(*result); } return rp; } @@ -506,9 +781,9 @@ void ClipActor::init() { // return audio_ptr_cache_[logical_frame]; // } else { // auto rp = make_response_promise>(); request(caf::actor_cast(media_), - // infinite, media::get_media_pointer_atom_v, media_type, - // logical_frame).then( + // media::AVFrameID>>(); mail(media::get_media_pointer_atom_v, media_type, + // logical_frame).request(caf::actor_cast(media_), // + // infinite).then( // [=](const media::AVFrameID &mp) mutable { // if(media_type == media::MediaType::MT_IMAGE) { // image_ptr_cache_[logical_frame] = @@ -533,14 +808,14 @@ void ClipActor::init() { // }, - [=](xstudio::media::current_media_source_atom atom) { - delegate(caf::actor_cast(media_), atom); - }, - [=](xstudio::playlist::reflag_container_atom atom) { - delegate(caf::actor_cast(media_), atom); - }, + // [=](xstudio::media::current_media_source_atom atom) { + // mail(atom).delegate(caf::actor_cast(media_)); + // }, + // [=](xstudio::playlist::reflag_container_atom atom) { + // mail(atom).delegate(caf::actor_cast(media_)); + // }, - [=](utility::duplicate_atom) -> result { + [=](duplicate_atom) -> result { JsonStore jsn; auto dup = base_.duplicate(); jsn["base"] = dup.serialise(); @@ -551,7 +826,8 @@ void ClipActor::init() { auto rp = make_response_promise(); - request(actor, infinite, link_media_atom_v, media_map) + mail(link_media_atom_v, media_map, false) + .request(actor, infinite) .then( [=](const bool) mutable { rp.deliver(UuidActor(dup.uuid(), actor)); }, [=](const caf::error &err) mutable { @@ -562,33 +838,40 @@ void ClipActor::init() { return rp; }, - [=](utility::serialise_atom) -> JsonStore { + [=](serialise_atom) -> JsonStore { JsonStore jsn; jsn["base"] = base_.serialise(); return jsn; - } + }, - ); + [=](media::get_media_pointers_atom atom, + const media::MediaType media_type, + const TimeSourceMode tsm, + const FrameRate &override_rate) -> caf::result { + // This is required by SubPlayhead actor to make the track + // playable. + return base_.item().get_all_frame_IDs(media_type, tsm, override_rate); + }}; } -// void ClipActor::update_edit_list() { -// ClipList sl = media_edit_list_.section_list(); - -// sl[0].frame_rate_and_duration_ = base_.duration(); -// sl[0].timecode_ = sl[0].timecode_ + base_.start_time().frames(); +void ClipActor::init() { + print_on_create(this, base_.name()); + print_on_exit(this, base_.name()); +} -// clip_edit_list_ = EditList(sl); +// we die if our media dies. +// set_down_handler([=](down_msg &msg) { +// if (msg.source == media_) { +// just unset media. +// anon_send(this, link_media_atom_v, UuidActor()); -// // all we do is adjust the duration ? and the timecode? -// // adjust edit_list based off our start_time and duration.. -// // a media object should really only return one entry, otherwise we've no idea how to -// adjust -// // the STL start_time we set the uuid to ourself as we're the ones that return -// mediapointer. -// // remapped to our start_time/duration. -// send(event_group_, utility::event_atom_v, utility::change_atom_v); +// demonitor(caf::actor_cast(media_)); +// send_exit(this, caf::exit_reason::user_shutdown); +// } +// }); // } + // void ClipActor::deliver_media_pointer( // const int logical_frame, caf::typed_response_promise rp) { // // send request to media actor but offset the logical_frame. @@ -597,9 +880,9 @@ void ClipActor::init() { // rp.deliver(make_error(xstudio_error::error, "No frames left")); // else { // int request_frame = logical_frame + base_.start_time().frames(); -// request( -// actor_cast(media_), infinite, media::get_media_pointer_atom_v, -// media::MT_IMAGE, request_frame) .then( +// mail(media::get_media_pointer_atom_v, +// media::MT_IMAGE, request_frame) .request(// actor_cast(media_), +// infinite).then( // [=](const media::AVFrameID &mptr) mutable { rp.deliver(mptr); }, // [=](error &err) mutable { rp.deliver(std::move(err)); }); // } diff --git a/src/timeline/src/gap.cpp b/src/timeline/src/gap.cpp index 24e218e0e..04fd0a7e8 100644 --- a/src/timeline/src/gap.cpp +++ b/src/timeline/src/gap.cpp @@ -1,8 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include #include "xstudio/timeline/gap.hpp" -#include "xstudio/utility/helpers.hpp" using namespace xstudio; using namespace xstudio::timeline; @@ -10,24 +8,29 @@ using namespace xstudio::utility; Gap::Gap( const std::string &name, - const utility::FrameRateDuration &duration, - const utility::Uuid &_uuid, + const FrameRateDuration &duration, + const Uuid &_uuid, const caf::actor &actor) : Container(name, "Gap", _uuid), item_( ItemType::IT_GAP, - utility::UuidActorAddr(uuid(), caf::actor_cast(actor)), - utility::FrameRange(FrameRateDuration(0, duration.rate()), duration), - utility::FrameRange(FrameRateDuration(0, duration.rate()), duration)) { + UuidActorAddr(uuid(), caf::actor_cast(actor)), + FrameRange(FrameRateDuration(0, duration.rate()), duration), + FrameRange(FrameRateDuration(0, duration.rate()), duration)) { item_.set_name(name); } -Gap::Gap(const utility::JsonStore &jsn) - : Container(static_cast(jsn.at("container"))), - item_(static_cast(jsn.at("item"))) {} +Gap::Gap(const JsonStore &jsn) + : Container(static_cast(jsn.at("container"))), + item_(static_cast(jsn.at("item"))) {} -utility::JsonStore Gap::serialise() const { - utility::JsonStore jsn; +Gap::Gap(const Item &item, const caf::actor &actor) + : Container(item.name(), "Gap", item.uuid()), item_(item.clone()) { + item_.set_actor_addr(caf::actor_cast(actor)); +} + +JsonStore Gap::serialise() const { + JsonStore jsn; jsn["container"] = Container::serialise(); jsn["item"] = item_.serialise(); @@ -37,7 +40,7 @@ utility::JsonStore Gap::serialise() const { Gap Gap::duplicate() const { - utility::JsonStore jsn; + JsonStore jsn; auto dup_container = Container::duplicate(); auto dup_item = item_; diff --git a/src/timeline/src/gap_actor.cpp b/src/timeline/src/gap_actor.cpp index 0e10f5ca6..a7210dba8 100644 --- a/src/timeline/src/gap_actor.cpp +++ b/src/timeline/src/gap_actor.cpp @@ -2,16 +2,13 @@ #include #include "xstudio/atoms.hpp" -#include "xstudio/broadcast/broadcast_actor.hpp" #include "xstudio/timeline/gap_actor.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" -#include "xstudio/utility/uuid.hpp" using namespace xstudio; using namespace xstudio::utility; using namespace xstudio::timeline; -using namespace caf; GapActor::GapActor(caf::actor_config &cfg, const JsonStore &jsn) : caf::event_based_actor(cfg), base_(static_cast(jsn.at("base"))) { @@ -26,15 +23,15 @@ GapActor::GapActor(caf::actor_config &cfg, const JsonStore &jsn, Item &pitem) base_.item().set_actor_addr(this); base_.item().set_system(&system()); - pitem = base_.item(); + pitem = base_.item().clone(); init(); } GapActor::GapActor( caf::actor_config &cfg, const std::string &name, - const utility::FrameRateDuration &duration, - const utility::Uuid &uuid) + const FrameRateDuration &duration, + const Uuid &uuid) : caf::event_based_actor(cfg), base_(name, duration, uuid, this) { base_.item().set_system(&system()); @@ -42,79 +39,135 @@ GapActor::GapActor( init(); } -void GapActor::init() { - print_on_create(this, base_.name()); - print_on_exit(this, base_.name()); +GapActor::GapActor(caf::actor_config &cfg, const Item &item) + : caf::event_based_actor(cfg), base_(item, this) { + base_.item().set_system(&system()); + init(); +} + +GapActor::GapActor(caf::actor_config &cfg, const Item &item, Item &nitem) + : GapActor(cfg, item) { + nitem = base_.item().clone(); +} + +caf::message_handler GapActor::message_handler() { + return caf::message_handler{ + [=](item_lock_atom, const bool value) -> JsonStore { + auto jsn = base_.item().set_locked(value); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, - event_group_ = spawn(this); - link_to(event_group_); - - // monitor changes in media.. - behavior_.assign( - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), [=](item_name_atom, const std::string &value) -> JsonStore { auto jsn = base_.item().set_name(value); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, [=](item_flag_atom, const std::string &value) -> JsonStore { auto jsn = base_.item().set_flag(value); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, + [=](item_type_atom) -> ItemType { return base_.item().item_type(); }, + [=](plugin_manager::enable_atom, const bool value) -> JsonStore { auto jsn = base_.item().set_enabled(value); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, [=](active_range_atom, const FrameRange &fr) -> JsonStore { auto jsn = base_.item().set_active_range(fr); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, [=](available_range_atom, const FrameRange &fr) -> JsonStore { auto jsn = base_.item().set_available_range(fr); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_prop_atom, const JsonStore &value, const bool merge) -> JsonStore { + auto p = base_.item().prop(); + p.update(value); + auto jsn = base_.item().set_prop(p); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_prop_atom, const JsonStore &value) -> JsonStore { + auto jsn = base_.item().set_prop(value); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_prop_atom, const JsonStore &value, const std::string &path) -> JsonStore { + auto prop = base_.item().prop(); + try { + auto ptr = nlohmann::json::json_pointer(path); + prop.at(ptr).update(value); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + auto jsn = base_.item().set_prop(prop); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, + [=](rate_atom) -> FrameRate { return base_.item().rate(); }, - [=](active_range_atom) -> std::optional { + [=](rate_atom atom, const media::MediaType media_type) { + return mail(atom).delegate(caf::actor_cast(this)); + }, + + [=](item_prop_atom) -> JsonStore { return base_.item().prop(); }, + + [=](active_range_atom) -> std::optional { return base_.item().active_range(); }, - [=](available_range_atom) -> std::optional { + [=](available_range_atom) -> std::optional { return base_.item().available_range(); }, - [=](trimmed_range_atom) -> utility::FrameRange { return base_.item().trimmed_range(); }, + [=](trimmed_range_atom) -> FrameRange { return base_.item().trimmed_range(); }, + + [=](trimmed_range_atom, + const FrameRange &avail, + const FrameRange &active) -> JsonStore { + auto jsn = base_.item().set_range(avail, active); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](trimmed_range_atom, + const FrameRange &avail, + const FrameRange &active, + const bool silent) -> JsonStore { return base_.item().set_range(avail, active); }, - [=](link_media_atom, const UuidActorMap &) -> bool { return true; }, + [=](link_media_atom, const UuidActorMap &, const bool) -> bool { return true; }, - [=](item_atom) -> Item { return base_.item(); }, + [=](item_atom) -> Item { return base_.item().clone(); }, [=](item_atom, const bool with_state) -> result> { auto rp = make_response_promise>(); - request(caf::actor_cast(this), infinite, utility::serialise_atom_v) + mail(serialise_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](const JsonStore &jsn) mutable { - rp.deliver(std::make_pair(jsn, base_.item())); + rp.deliver(std::make_pair(jsn, base_.item().clone())); }, [=](const caf::error &err) mutable { rp.deliver(err); }); return rp; @@ -131,9 +184,8 @@ void GapActor::init() { }, [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) {}, - [=](utility::duplicate_atom) -> UuidActor { + [=](duplicate_atom) -> UuidActor { JsonStore jsn; auto dup = base_.duplicate(); jsn["base"] = dup.serialise(); @@ -142,9 +194,19 @@ void GapActor::init() { return UuidActor(dup.uuid(), actor); }, - [=](utility::serialise_atom) -> JsonStore { + [=](playhead::source_atom, + const UuidUuidMap &swap, + const UuidActorMap &media) -> result { return true; }, + + [=](serialise_atom) -> JsonStore { JsonStore jsn; jsn["base"] = base_.serialise(); return jsn; - }); + }}; +} + + +void GapActor::init() { + print_on_create(this, base_.name()); + print_on_exit(this, base_.name()); } diff --git a/src/timeline/src/item.cpp b/src/timeline/src/item.cpp index 1f9888971..8356f7558 100644 --- a/src/timeline/src/item.cpp +++ b/src/timeline/src/item.cpp @@ -1,22 +1,21 @@ // SPDX-License-Identifier: Apache-2.0 -#include #include -#include "xstudio/utility/helpers.hpp" #include "xstudio/timeline/item.hpp" +#include "xstudio/utility/helpers.hpp" using namespace xstudio; using namespace xstudio::timeline; using namespace xstudio::utility; -Item::Item(const utility::JsonStore &jsn, caf::actor_system *system) - : Items(), the_system_(system) { +Item::Item(const JsonStore &jsn, caf::actor_system *system) : Items(), the_system_(system) { uuid_addr_.first = jsn.at("uuid"); item_type_ = jsn.at("type"); enabled_ = jsn.at("enabled"); name_ = jsn.value("name", ""); + locked_ = jsn.value("locked", false); flag_ = jsn.value("flag", ""); - prop_ = jsn.value("prop", JsonStore()); + prop_ = jsn.value("prop", JsonStore(R"({})"_json)); if (jsn.count("actor_addr")) uuid_addr_.second = string_to_actor_addr(jsn.at("actor_addr")); @@ -35,11 +34,28 @@ Item::Item(const utility::JsonStore &jsn, caf::actor_system *system) available_range_ = jsn.at("available_range"); } + for (const auto &i : jsn.value("markers", R"([])"_json)) + markers_.emplace_back(Marker(JsonStore(i))); + for (const auto &i : jsn.at("children")) { - emplace_back(Item(utility::JsonStore(i), the_system_)); + emplace_back(Item(JsonStore(i), the_system_)); } } +bool Item::transparent() const { + auto result = not enabled_; + + if (item_type_ == ItemType::IT_GAP) + result = true; + else if ( + item_type_ == ItemType::IT_CLIP and + (prop_.is_null() or prop_.value("media_uuid", Uuid()).is_null())) + result = true; + + return result; +} + + std::string Item::actor_addr_to_string(const caf::actor_addr &addr) const { std::string str; if (the_system_) { @@ -47,7 +63,7 @@ std::string Item::actor_addr_to_string(const caf::actor_addr &addr) const { binary_serializer bs{system(), buf}; auto e = bs.apply(addr); if (e) - str = utility::make_hex_string(std::begin(buf), std::end(buf)); + str = make_hex_string(std::begin(buf), std::end(buf)); } return str; } @@ -62,16 +78,18 @@ caf::actor_addr Item::string_to_actor_addr(const std::string &str) const { return addr; } -utility::JsonStore Item::serialise(const int depth) const { - utility::JsonStore jsn; +JsonStore Item::serialise(const int depth) const { + JsonStore jsn; jsn["uuid"] = uuid_addr_.first; jsn["actor_addr"] = actor_addr_to_string(uuid_addr_.second); jsn["type"] = item_type_; jsn["enabled"] = enabled_; + jsn["locked"] = locked_; jsn["name"] = name_; jsn["flag"] = flag_; jsn["prop"] = prop_; + jsn["markers"] = serialise_markers(markers_); if (has_available_range_) jsn["available_range"] = available_range_; @@ -90,79 +108,79 @@ utility::JsonStore Item::serialise(const int depth) const { return jsn; } -std::optional Item::active_range() const { +std::optional Item::active_range() const { if (has_active_range_) return active_range_; return {}; } -std::optional Item::available_range() const { +std::optional Item::available_range() const { if (has_available_range_) return available_range_; return {}; } -utility::FrameRange Item::trimmed_range() const { +FrameRange Item::trimmed_range() const { if (has_active_range_) return active_range_; if (has_available_range_) return available_range_; - return utility::FrameRange(); + return FrameRange(); } -utility::FrameRate Item::trimmed_duration() const { +FrameRate Item::trimmed_duration() const { if (has_active_range_) return active_range_.duration(); if (has_available_range_) return available_range_.duration(); - return utility::FrameRate(); + return FrameRate(); } -utility::FrameRate Item::trimmed_start() const { +FrameRate Item::trimmed_start() const { if (has_active_range_) return active_range_.start(); if (has_available_range_) return available_range_.start(); - return utility::FrameRate(); + return FrameRate(); } -std::optional Item::available_duration() const { +std::optional Item::available_duration() const { if (has_available_range_) return available_range_.duration(); return {}; } -std::optional Item::active_duration() const { +std::optional Item::active_duration() const { if (has_active_range_) return active_range_.duration(); return {}; } -std::optional Item::available_start() const { +std::optional Item::available_start() const { if (has_available_range_) return available_range_.start(); return {}; } -std::optional Item::active_start() const { +std::optional Item::active_start() const { if (has_active_range_) return active_range_.start(); return {}; } -std::optional Item::active_frame_duration() const { +std::optional Item::active_frame_duration() const { if (has_active_range_) return active_range_.frame_duration(); return {}; } -std::optional Item::available_frame_duration() const { +std::optional Item::available_frame_duration() const { if (has_available_range_) return available_range_.frame_duration(); return {}; @@ -171,9 +189,9 @@ std::optional Item::available_frame_duration() const // ordering might matter here.. // so watch out if things go wrong. -utility::JsonStore Item::refresh(const int depth) { +JsonStore Item::refresh(const int depth) { if (not depth) - return utility::JsonStore(); + return JsonStore(); auto result = nlohmann::json::array(); @@ -229,9 +247,9 @@ utility::JsonStore Item::refresh(const int depth) { } if (result.empty()) - return utility::JsonStore(); + return JsonStore(); - return utility::JsonStore(result); + return JsonStore(result); } bool Item::valid() const { @@ -330,23 +348,97 @@ bool Item::valid_child(const Item &child) const { return valid; } -utility::UuidActorVector Item::find_all_uuid_actors(const ItemType item_type) const { - utility::UuidActorVector items; +UuidActorVector +Item::find_all_uuid_actors(const ItemType item_type, const bool only_enabled_items) const { + UuidActorVector items; - if (item_type_ == item_type) + if (item_type_ == item_type && (!only_enabled_items || enabled_)) items.push_back(uuid_actor()); for (const auto &i : children()) { - auto citems = i.find_all_uuid_actors(item_type); + auto citems = i.find_all_uuid_actors(item_type, only_enabled_items); items.insert(items.end(), citems.begin(), citems.end()); } return items; } +std::vector> +Item::find_all_items(const ItemType item_type, const ItemType track_type) const { + auto result = std::vector>(); + + auto is_track = (item_type_ == IT_AUDIO_TRACK or item_type_ == IT_VIDEO_TRACK); + + if (item_type_ == item_type) + result.push_back(std::ref(*this)); + + if (track_type == IT_NONE or not is_track or item_type_ == track_type) { + for (const auto &i : children()) { + auto citems = i.find_all_items(item_type, track_type); + result.insert(result.end(), citems.begin(), citems.end()); + } + } + + return result; +} + +std::vector> +Item::find_all_items(const ItemType item_type, const ItemType track_type) { + auto result = std::vector>(); + + auto is_track = (item_type_ == IT_AUDIO_TRACK or item_type_ == IT_VIDEO_TRACK); + + if (item_type_ == item_type) + result.push_back(std::ref(*this)); + + if (track_type == IT_NONE or not is_track or item_type_ == track_type) { + for (auto &i : children()) { + auto citems = i.find_all_items(item_type, track_type); + result.insert(result.end(), citems.begin(), citems.end()); + } + } + + return result; +} + +/*void Item::print( + const timebase::flicks print_range_in, + const timebase::flicks print_range_out, + const timebase::flicks frame_inteval, + const media::MediaType mt, + const UuidSet &focus, + const bool must_have_focus) const +{ + if (transparent()) + return; + + if (print_range_in >= trimmed_duration()) return; + if (print_range_out >= trimmed_start()) return; + + // If TRACK... + const auto ts = trimmed_start(); + + timebase::flicks time = print_range_in; + + // loop over clips + for (const auto &it : *this) { + + const auto td = it.trimmed_duration(); + while (time < td && time <= print_range_out) { + // print clip/gap frame here + result[idx++] = std::pair(it, time + +it.trimmed_start()); time += frame_inteval; + } + + + } + +}*/ + std::optional Item::resolve_time( - const utility::FrameRate &time, + const FrameRate &time, const media::MediaType mt, - const utility::UuidSet &focus) const { + const UuidSet &focus, + const bool must_have_focus) const { if (transparent()) return {}; @@ -357,7 +449,7 @@ std::optional Item::resolve_time( case IT_TIMELINE: // pass to stack if (not empty()) { - auto t = front().resolve_time(time + trimmed_start(), mt, focus); + auto t = front().resolve_time(time + trimmed_start(), mt, focus, must_have_focus); if (t) return *t; } @@ -368,6 +460,7 @@ std::optional Item::resolve_time( // handle transparent.. // needs depth first search ? // most of the logic lives here.. + if (mt == media::MediaType::MT_IMAGE) { std::optional found_item = {}; @@ -376,19 +469,26 @@ std::optional Item::resolve_time( if (it.transparent() or it.item_type() == IT_AUDIO_TRACK) continue; - auto t = it.resolve_time(time + trimmed_start(), mt, focus); + auto requires_focus = must_have_focus and not focus.count(it.uuid()); + auto t = it.resolve_time(time + trimmed_start(), mt, focus, requires_focus); if (t) { + // first found item has result and we're not filtering if (focus.empty()) return *t; - if (focus.count(it.uuid()) and std::get<0>(*t).item_type() == IT_CLIP) + const auto &item = t->first; + + // we are filtering and container is focused + if (focus.count(it.uuid()) and item.item_type() == IT_CLIP) return *t; - if (focus.count(std::get<0>(*t).uuid())) + // item is focused + if (focus.count(item.uuid())) return *t; - if (not found_item and std::get<0>(*t).item_type() == IT_CLIP) + // default first match + if (not must_have_focus and not found_item and item.item_type() == IT_CLIP) found_item = *t; } } @@ -401,18 +501,21 @@ std::optional Item::resolve_time( // we skip video track if (it.transparent() or it.item_type() == IT_VIDEO_TRACK) continue; - auto t = it.resolve_time(time + trimmed_start(), mt, focus); + auto requires_focus = must_have_focus and not focus.count(it.uuid()); + auto t = it.resolve_time(time + trimmed_start(), mt, focus, requires_focus); if (t) { if (focus.empty()) return *t; - if (focus.count(it.uuid()) and std::get<0>(*t).item_type() == IT_CLIP) + const auto &item = t->first; + + if (focus.count(it.uuid()) and item.item_type() == IT_CLIP) return *t; - if (focus.count(std::get<0>(*t).uuid())) + if (focus.count(item.uuid())) return *t; - if (not found_item and std::get<0>(*t).item_type() == IT_CLIP) + if (not must_have_focus and not found_item and item.item_type() == IT_CLIP) found_item = *t; } } @@ -426,17 +529,21 @@ std::optional Item::resolve_time( case IT_VIDEO_TRACK: // sequentail list of items. { - auto ttp = time; + auto requires_focus = must_have_focus and not focus.count(uuid()); + auto ttp = time; // spdlog::warn("{}", trimmed_start()/timebase::k_flicks_24fps); // spdlog::warn("{}", ttp/timebase::k_flicks_24fps); // spdlog::warn("{}", (ttp + trimmed_start())/timebase::k_flicks_24fps); + // spdlog::warn("IT_VIDEO_TRACK {} {}", name(), requires_focus); + const auto ts = trimmed_start(); for (const auto &it : *this) { const auto td = it.trimmed_duration(); + if (ttp + ts >= td) { ttp -= td; } else { - auto t = it.resolve_time(ttp + ts, mt, focus); + auto t = it.resolve_time(ttp + ts, mt, focus, requires_focus); if (t) return *t; break; @@ -445,10 +552,11 @@ std::optional Item::resolve_time( } break; - case IT_GAP: case IT_CLIP: - return std::tuple(*this, time + trimmed_start()); + if (not must_have_focus or focus.count(uuid())) + return std::pair(*this, time + trimmed_start()); break; + case IT_GAP: case IT_NONE: default: break; @@ -456,33 +564,194 @@ std::optional Item::resolve_time( return {}; } +std::vector Item::resolve_time_raw( + const FrameRate &time, + const media::MediaType mt, + const UuidSet &focus, + const bool ignore_disabled) const { + auto result = std::vector(); + + if (ignore_disabled and transparent()) + return result; + + if (time >= trimmed_duration()) + return result; + + switch (item_type_) { + case IT_TIMELINE: + // pass to stack + if (not empty()) { + result = + front().resolve_time_raw(time + trimmed_start(), mt, focus, ignore_disabled); + } + break; + + case IT_STACK: + // capture all items unless focus match found.. + if (mt == media::MediaType::MT_IMAGE) { + auto focus_found = std::vector(); + + for (const auto &it : *this) { + // we skip audio track.. + if ((ignore_disabled and it.transparent()) or it.item_type() == IT_AUDIO_TRACK) + continue; + + auto t = + it.resolve_time_raw(time + trimmed_start(), mt, focus, ignore_disabled); -void Item::set_enabled_direct(const bool &value) { enabled_ = value; } + // track matches focus + if (not focus.empty() and focus.count(it.uuid())) + focus_found.insert(focus_found.end(), t.begin(), t.end()); + else + result.insert(result.end(), t.begin(), t.end()); + } + + // parsed stack or results. + if (not focus.empty()) { + // scan results for focus items + // in which case we only return those items. + for (const auto &i : result) { + if (i.first.item_type() == IT_CLIP and focus.count(i.first.uuid())) + focus_found.push_back(i); + } + + if (not focus_found.empty()) + result = focus_found; + } + } else { + auto focus_found = std::vector(); + + // should this be reversed ? + for (const auto &it : *this) { + // we skip audio track.. + if (ignore_disabled and it.transparent() or it.item_type() == IT_VIDEO_TRACK) + continue; + + auto t = + it.resolve_time_raw(time + trimmed_start(), mt, focus, ignore_disabled); + + // track matches focus + if (not focus.empty() and focus.count(it.uuid())) + focus_found.insert(focus_found.end(), t.begin(), t.end()); + else + result.insert(result.end(), t.begin(), t.end()); + } + + // parsed stack or results. + if (not focus.empty()) { + // scan results for focus items + // in which case we only return those items. + for (const auto &i : result) { + if (i.first.item_type() == IT_CLIP and focus.count(i.first.uuid())) + focus_found.push_back(i); + } + + if (not focus_found.empty()) + result = focus_found; + } + } + // we shouldn't return the container.. + break; + + case IT_AUDIO_TRACK: + case IT_VIDEO_TRACK: + // sequentail list of items. + { + auto ttp = time; + // spdlog::warn("{}", trimmed_start()/timebase::k_flicks_24fps); + // spdlog::warn("{}", ttp/timebase::k_flicks_24fps); + // spdlog::warn("{}", (ttp + trimmed_start())/timebase::k_flicks_24fps); + const auto ts = trimmed_start(); + for (const auto &it : *this) { + const auto td = it.trimmed_duration(); + if (ttp + ts >= td) { + ttp -= td; + } else { + result = it.resolve_time_raw(ttp + ts, mt, focus, ignore_disabled); + break; + } + } + } + break; + + case IT_GAP: + case IT_CLIP: + result.emplace_back(std::pair(*this, time + trimmed_start())); + break; + case IT_NONE: + default: + break; + } + + return result; +} + +void Item::set_enabled_direct(const bool value) { enabled_ = value; } + +void Item::set_locked_direct(const bool value) { locked_ = value; } void Item::set_name_direct(const std::string &value) { name_ = value; } void Item::set_flag_direct(const std::string &value) { flag_ = value; } -void Item::set_prop_direct(const utility::JsonStore &value) { prop_ = value; } +void Item::set_prop_direct(const JsonStore &value) { prop_ = value; } + +void Item::set_markers_direct(const Markers &value) { markers_ = value; } + +bool Item::has_dirty(const JsonStore &event) { + auto result = false; + for (const auto &i : event) { + if (i.at("undo").value("action", ItemAction::IA_NONE) == ItemAction::IA_DIRTY) { + result = true; + break; + } + } + + return result; +} + -utility::JsonStore Item::set_enabled(const bool &value) { +JsonStore Item::set_enabled(const bool value) { if (enabled_ != value) { - utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); - jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IT_ENABLE; + JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); + jsn[0]["undo"]["event_id"] = jsn[0]["redo"]["event_id"] = Uuid::generate(); + jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IA_ENABLE; jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; jsn[0]["undo"]["value"] = enabled_; jsn[0]["redo"]["value"] = value; + + jsn.push_back(R"({"undo":{}, "redo":{}})"_json); + jsn[1]["undo"]["event_id"] = jsn[1]["redo"]["event_id"] = Uuid::generate(); + jsn[1]["undo"]["action"] = jsn[1]["redo"]["action"] = ItemAction::IA_DIRTY; + jsn[1]["undo"]["uuid"] = jsn[1]["redo"]["uuid"] = uuid_addr_.first; + set_enabled_direct(value); return jsn; } - return utility::JsonStore(); + return JsonStore(); } -utility::JsonStore Item::set_name(const std::string &value) { +JsonStore Item::set_locked(const bool value) { + if (locked_ != value) { + JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); + jsn[0]["undo"]["event_id"] = jsn[0]["redo"]["event_id"] = Uuid::generate(); + jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IA_LOCK; + jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; + jsn[0]["undo"]["value"] = locked_; + jsn[0]["redo"]["value"] = value; + set_locked_direct(value); + return jsn; + } + + return JsonStore(); +} + +JsonStore Item::set_name(const std::string &value) { if (name_ != value) { - utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); - jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IT_NAME; + JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); + jsn[0]["undo"]["event_id"] = jsn[0]["redo"]["event_id"] = Uuid::generate(); + jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IA_NAME; jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; jsn[0]["undo"]["value"] = name_; jsn[0]["redo"]["value"] = value; @@ -490,13 +759,14 @@ utility::JsonStore Item::set_name(const std::string &value) { return jsn; } - return utility::JsonStore(); + return JsonStore(); } -utility::JsonStore Item::set_flag(const std::string &value) { +JsonStore Item::set_flag(const std::string &value) { if (flag_ != value) { - utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); - jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IT_FLAG; + JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); + jsn[0]["undo"]["event_id"] = jsn[0]["redo"]["event_id"] = Uuid::generate(); + jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IA_FLAG; jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; jsn[0]["undo"]["value"] = flag_; jsn[0]["redo"]["value"] = value; @@ -504,39 +774,50 @@ utility::JsonStore Item::set_flag(const std::string &value) { return jsn; } - return utility::JsonStore(); + return JsonStore(); } -utility::JsonStore Item::set_prop(const utility::JsonStore &value) { +JsonStore Item::set_prop(const JsonStore &value) { if (prop_ != value) { - utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); - jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IT_PROP; + JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); + jsn[0]["undo"]["event_id"] = jsn[0]["redo"]["event_id"] = Uuid::generate(); + jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IA_PROP; jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; jsn[0]["undo"]["value"] = prop_; jsn[0]["redo"]["value"] = value; + + if (item_type_ == IT_CLIP) { + jsn.push_back(R"({"undo":{}, "redo":{}})"_json); + jsn[1]["undo"]["event_id"] = jsn[1]["redo"]["event_id"] = Uuid::generate(); + jsn[1]["undo"]["action"] = jsn[1]["redo"]["action"] = ItemAction::IA_DIRTY; + jsn[1]["undo"]["uuid"] = jsn[1]["redo"]["uuid"] = uuid_addr_.first; + } + set_prop_direct(value); return jsn; } - return utility::JsonStore(); + return JsonStore(); } void Item::set_actor_addr_direct(const caf::actor_addr &value) { uuid_addr_.second = value; } -utility::JsonStore Item::make_actor_addr_update() const { - utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); +JsonStore Item::make_actor_addr_update() const { + JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); - jsn[0]["redo"]["action"] = ItemAction::IT_ADDR; - jsn[0]["redo"]["uuid"] = uuid_addr_.first; - jsn[0]["redo"]["value"] = actor_addr_to_string(uuid_addr_.second); + jsn[0]["undo"]["event_id"] = jsn[0]["redo"]["event_id"] = Uuid::generate(); + jsn[0]["redo"]["action"] = ItemAction::IA_ADDR; + jsn[0]["redo"]["uuid"] = uuid_addr_.first; + jsn[0]["redo"]["value"] = actor_addr_to_string(uuid_addr_.second); return jsn; } -utility::JsonStore Item::set_actor_addr(const caf::actor_addr &value) { +JsonStore Item::set_actor_addr(const caf::actor_addr &value) { if (uuid_addr_.second != value) { - utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); - jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IT_ADDR; + JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); + jsn[0]["undo"]["event_id"] = jsn[0]["redo"]["event_id"] = Uuid::generate(); + jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IA_ADDR; jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; if (uuid_addr_.second) @@ -553,18 +834,71 @@ utility::JsonStore Item::set_actor_addr(const caf::actor_addr &value) { return jsn; } - return utility::JsonStore(); + return JsonStore(); +} + +JsonStore Item::set_markers(const Markers &value) { + if (value != markers_) { + JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); + jsn[0]["undo"]["event_id"] = jsn[0]["redo"]["event_id"] = Uuid::generate(); + jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IA_MARKER; + jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; + + jsn[0]["undo"]["value"] = serialise_markers(markers_); + jsn[0]["redo"]["value"] = serialise_markers(value); + + set_markers_direct(value); + return jsn; + } + return JsonStore(); +} + +void Item::set_range_direct(const FrameRange &avail, const FrameRange &active) { + set_available_range_direct(avail); + set_active_range_direct(active); } -void Item::set_active_range_direct(const utility::FrameRange &value) { +JsonStore Item::set_range(const FrameRange &avail, const FrameRange &active) { + if (active != active_range_ || avail != available_range_) { + JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); + jsn[0]["undo"]["event_id"] = jsn[0]["redo"]["event_id"] = Uuid::generate(); + jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IA_RANGE; + jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; + + jsn[0]["undo"]["value"] = available_range_; + jsn[0]["undo"]["value2"] = has_available_range_; + jsn[0]["undo"]["value3"] = active_range_; + jsn[0]["undo"]["value4"] = has_active_range_; + + jsn[0]["redo"]["value"] = avail; + jsn[0]["redo"]["value2"] = true; + jsn[0]["redo"]["value3"] = active; + jsn[0]["redo"]["value4"] = true; + + jsn.push_back(R"({"undo":{}, "redo":{}})"_json); + jsn[1]["undo"]["event_id"] = jsn[1]["redo"]["event_id"] = Uuid::generate(); + jsn[1]["undo"]["action"] = jsn[1]["redo"]["action"] = ItemAction::IA_DIRTY; + jsn[1]["undo"]["uuid"] = jsn[1]["redo"]["uuid"] = uuid_addr_.first; + + set_range_direct(avail, active); + return jsn; + } + + return JsonStore(); +} + + +void Item::set_active_range_direct(const FrameRange &value) { has_active_range_ = true; active_range_ = value; } -utility::JsonStore Item::set_active_range(const utility::FrameRange &value) { +JsonStore Item::set_active_range(const FrameRange &value) { if (value != active_range_) { - utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); - jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IT_ACTIVE; + JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); + jsn[0]["undo"]["event_id"] = jsn[0]["redo"]["event_id"] = Uuid::generate(); + + jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IA_ACTIVE; jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; jsn[0]["undo"]["value"] = active_range_; @@ -572,23 +906,30 @@ utility::JsonStore Item::set_active_range(const utility::FrameRange &value) { jsn[0]["redo"]["value"] = value; jsn[0]["redo"]["value2"] = true; + + jsn.push_back(R"({"undo":{}, "redo":{}})"_json); + jsn[1]["undo"]["event_id"] = jsn[1]["redo"]["event_id"] = Uuid::generate(); + jsn[1]["undo"]["action"] = jsn[1]["redo"]["action"] = ItemAction::IA_DIRTY; + jsn[1]["undo"]["uuid"] = jsn[1]["redo"]["uuid"] = uuid_addr_.first; + set_active_range_direct(value); return jsn; } - return utility::JsonStore(); + return JsonStore(); } -void Item::set_available_range_direct(const utility::FrameRange &value) { +void Item::set_available_range_direct(const FrameRange &value) { has_available_range_ = true; available_range_ = value; } -utility::JsonStore Item::set_available_range(const utility::FrameRange &value) { +JsonStore Item::set_available_range(const FrameRange &value) { if (value != available_range_) { - utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); + JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); - jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IT_AVAIL; + jsn[0]["undo"]["event_id"] = jsn[0]["redo"]["event_id"] = Uuid::generate(); + jsn[0]["undo"]["action"] = jsn[0]["redo"]["action"] = ItemAction::IA_AVAIL; jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; jsn[0]["undo"]["value"] = available_range_; @@ -597,36 +938,48 @@ utility::JsonStore Item::set_available_range(const utility::FrameRange &value) { jsn[0]["redo"]["value"] = value; jsn[0]["redo"]["value2"] = true; + jsn.push_back(R"({"undo":{}, "redo":{}})"_json); + jsn[1]["undo"]["event_id"] = jsn[1]["redo"]["event_id"] = Uuid::generate(); + jsn[1]["undo"]["action"] = jsn[1]["redo"]["action"] = ItemAction::IA_DIRTY; + jsn[1]["undo"]["uuid"] = jsn[1]["redo"]["uuid"] = uuid_addr_.first; + set_available_range_direct(value); return jsn; } - return utility::JsonStore(); + return JsonStore(); } Items::iterator Item::insert_direct(Items::iterator position, const Item &val) { auto it = Items::insert(position, val); it->set_system(the_system_); - if (recursive_bind_ and item_event_callback_) - it->bind_item_event_func(item_event_callback_, recursive_bind_); + if (recursive_bind_post_ and item_post_event_callback_) + it->bind_item_post_event_func(item_post_event_callback_, recursive_bind_post_); + if (recursive_bind_pre_ and item_pre_event_callback_) + it->bind_item_pre_event_func(item_pre_event_callback_, recursive_bind_pre_); return it; } -utility::JsonStore -Item::insert(Items::iterator position, const Item &value, const utility::JsonStore &blind) { - utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); +JsonStore Item::insert(Items::iterator position, const Item &value, const JsonStore &blind) { + JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); auto index = std::distance(begin(), position); + jsn[0]["undo"]["event_id"] = jsn[0]["redo"]["event_id"] = Uuid::generate(); jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; - jsn[0]["undo"]["action"] = ItemAction::IT_REMOVE; + jsn[0]["undo"]["action"] = ItemAction::IA_REMOVE; jsn[0]["undo"]["index"] = index; jsn[0]["undo"]["item_uuid"] = value.uuid(); - jsn[0]["redo"]["action"] = ItemAction::IT_INSERT; + jsn[0]["redo"]["action"] = ItemAction::IA_INSERT; jsn[0]["redo"]["index"] = index; jsn[0]["redo"]["item"] = value.serialise(); jsn[0]["redo"]["blind"] = blind; + jsn.push_back(R"({"undo":{}, "redo":{}})"_json); + jsn[1]["undo"]["event_id"] = jsn[1]["redo"]["event_id"] = Uuid::generate(); + jsn[1]["undo"]["action"] = jsn[1]["redo"]["action"] = ItemAction::IA_DIRTY; + jsn[1]["undo"]["uuid"] = jsn[1]["redo"]["uuid"] = uuid_addr_.first; + insert_direct(position, value); return jsn; @@ -634,21 +987,27 @@ Item::insert(Items::iterator position, const Item &value, const utility::JsonSto Items::iterator Item::erase_direct(Items::iterator position) { return Items::erase(position); } -utility::JsonStore Item::erase(Items::iterator position, const utility::JsonStore &blind) { - utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); +JsonStore Item::erase(Items::iterator position, const JsonStore &blind) { + JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); auto index = std::distance(begin(), position); + jsn[0]["undo"]["event_id"] = jsn[0]["redo"]["event_id"] = Uuid::generate(); jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; - jsn[0]["undo"]["action"] = ItemAction::IT_INSERT; + jsn[0]["undo"]["action"] = ItemAction::IA_INSERT; jsn[0]["undo"]["index"] = index; jsn[0]["undo"]["item"] = position->serialise(); jsn[0]["undo"]["blind"] = blind; - jsn[0]["redo"]["action"] = ItemAction::IT_REMOVE; + jsn[0]["redo"]["action"] = ItemAction::IA_REMOVE; jsn[0]["redo"]["index"] = index; jsn[0]["redo"]["item_uuid"] = position->uuid(); + jsn.push_back(R"({"undo":{}, "redo":{}})"_json); + jsn[1]["undo"]["event_id"] = jsn[1]["redo"]["event_id"] = Uuid::generate(); + jsn[1]["undo"]["action"] = jsn[1]["redo"]["action"] = ItemAction::IA_DIRTY; + jsn[1]["undo"]["uuid"] = jsn[1]["redo"]["uuid"] = uuid_addr_.first; + erase_direct(position); return jsn; @@ -662,21 +1021,22 @@ void Item::splice_direct( Items::splice(pos, other, first, last); } -utility::JsonStore Item::splice( +JsonStore Item::splice( Items::const_iterator pos, Items &other, Items::const_iterator first, Items::const_iterator last) { - utility::JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); + JsonStore jsn(R"([{"undo":{}, "redo":{}}])"_json); auto dst_index = std::distance(cbegin(), pos); auto start_index = std::distance(cbegin(), first); auto count = std::distance(first, last); + jsn[0]["undo"]["event_id"] = jsn[0]["redo"]["event_id"] = Uuid::generate(); jsn[0]["undo"]["uuid"] = jsn[0]["redo"]["uuid"] = uuid_addr_.first; - jsn[0]["redo"]["action"] = ItemAction::IT_SPLICE; + jsn[0]["redo"]["action"] = ItemAction::IA_SPLICE; jsn[0]["redo"]["dst"] = dst_index; // dst jsn[0]["redo"]["first"] = start_index; // frst jsn[0]["redo"]["count"] = count; @@ -691,11 +1051,16 @@ utility::JsonStore Item::splice( undo_dst += count; } - jsn[0]["undo"]["action"] = ItemAction::IT_SPLICE; + jsn[0]["undo"]["action"] = ItemAction::IA_SPLICE; jsn[0]["undo"]["dst"] = undo_dst; jsn[0]["undo"]["first"] = undo_first; jsn[0]["undo"]["count"] = count; + jsn.push_back(R"({"undo":{}, "redo":{}})"_json); + jsn[1]["undo"]["event_id"] = jsn[1]["redo"]["event_id"] = Uuid::generate(); + jsn[1]["undo"]["action"] = jsn[1]["redo"]["action"] = ItemAction::IA_DIRTY; + jsn[1]["undo"]["uuid"] = jsn[1]["redo"]["uuid"] = uuid_addr_.first; + // std::cerr << "first " << undo_first << std::endl; // std::cerr << " dst " << undo_dst << std::endl; // std::cerr << std::endl << std::flush; @@ -706,67 +1071,93 @@ utility::JsonStore Item::splice( } // apply update that occurred outside our tree. -bool Item::update(const utility::JsonStore &event) { - bool changed = false; +std::set Item::update(const JsonStore &event) { + auto result = std::set(); for (const auto &i : event) - changed |= process_event(i.at("redo")); - return changed; + if (process_event(i.at("redo"))) + result.insert(i.at("redo").at("event_id").get()); + return result; } -void Item::undo(const utility::JsonStore &event) { - for (const auto &i : event) - process_event(i.at("undo")); +void Item::undo(const JsonStore &event) { + // reverse ordering.. + for (auto it = event.crbegin(); it != event.crend(); ++it) + process_event(it->at("undo")); } -void Item::redo(const utility::JsonStore &event) { +void Item::redo(const JsonStore &event) { for (const auto &i : event) process_event(i.at("redo")); } -bool Item::process_event(const utility::JsonStore &event) { - // spdlog::warn("{}", event.dump(2)); +bool Item::process_event(const JsonStore &event) { + // spdlog::warn("{} {}", name(), event.dump(2)); if (Uuid(event.at("uuid")) == uuid_addr_.first) { + if (item_pre_event_callback_) + item_pre_event_callback_(event, *this); + switch (static_cast(event.at("action"))) { - case IT_ENABLE: + case IA_ENABLE: set_enabled_direct(event.at("value")); break; - case IT_NAME: + case IA_LOCK: + set_locked_direct(event.at("value")); + break; + case IA_NAME: set_name_direct(event.at("value")); break; - case IT_FLAG: + case IA_FLAG: set_flag_direct(event.at("value")); break; - case IT_PROP: + case IA_PROP: set_prop_direct(event.at("value")); break; - case IT_ACTIVE: + case IA_MARKER: { + auto tmp = Markers(); + for (const auto &i : event.at("value")) + tmp.emplace_back(Marker(JsonStore(i))); + set_markers_direct(tmp); + } break; + case IA_ACTIVE: set_active_range_direct(event.at("value")); has_active_range_ = event.at("value2"); break; - case IT_AVAIL: + case IA_AVAIL: + set_available_range_direct(event.at("value")); + has_available_range_ = event.at("value2"); + break; + case IA_RANGE: set_available_range_direct(event.at("value")); has_available_range_ = event.at("value2"); + set_active_range_direct(event.at("value3")); + has_active_range_ = event.at("value4"); break; - case IT_INSERT: { - // spdlog::warn("IT_INSERT {}", event.dump(2)); + case IA_INSERT: { auto index = event.at("index").get(); - if (index == 0 or index <= size()) { + if (index >= 0 and index <= size()) { insert_direct( std::next(begin(), index), Item(JsonStore(event.at("item")), the_system_)); } else { spdlog::error( - "IT_INSERT - INVALID INDEX {} {} {}", size(), index, event.dump(2)); + "IA_INSERT - INVALID INDEX uuid {} name {} type {} index {} track size {} " + "{}", + to_string(uuid()), + name(), + to_string(item_type()), + index, + size(), + event.dump(2)); } } break; - case IT_REMOVE: { + case IA_REMOVE: { // spdlog::warn("IT_REMOVE {}", event.dump(2)); auto index = event.at("index").get(); if (index < size()) { erase_direct(std::next(begin(), index)); } else { spdlog::error( - "IT_REMOVE - INVALID INDEX {} {} {} {} {} {}", + "IA_REMOVE - INVALID INDEX {} {} {} {} {} {}", to_string(uuid()), name(), to_string(item_type()), @@ -775,7 +1166,7 @@ bool Item::process_event(const utility::JsonStore &event) { event.dump(2)); } } break; - case IT_SPLICE: { + case IA_SPLICE: { // spdlog::warn("IT_SPLICE {}", event.dump(2)); auto dst = event.at("dst").get(); auto first = event.at("first").get(); @@ -787,11 +1178,11 @@ bool Item::process_event(const utility::JsonStore &event) { splice_direct(it1, *this, it2, it3); } else { spdlog::error( - "IT_SPLICE - INVALID INDEX {} {} {} {}", size(), first, dst, event.dump(2)); + "IA_SPLICE - INVALID INDEX {} {} {} {}", size(), first, dst, event.dump(2)); } } break; - case IT_ADDR: + case IA_ADDR: if (event.at("value").is_null()) set_actor_addr_direct(caf::actor_addr()); else @@ -801,8 +1192,8 @@ bool Item::process_event(const utility::JsonStore &event) { default: break; } - if (item_event_callback_) - item_event_callback_(event, *this); + if (item_post_event_callback_) + item_post_event_callback_(event, *this); } else { // child ? for (auto &i : *this) { @@ -815,15 +1206,42 @@ bool Item::process_event(const utility::JsonStore &event) { return true; } -void Item::bind_item_event_func(ItemEventFunc fn, const bool recursive) { - recursive_bind_ = recursive; - item_event_callback_ = [fn](auto &&PH1, auto &&PH2) { +// make copty safe.. ICK +void Item::unbind() { + item_pre_event_callback_ = nullptr; + item_post_event_callback_ = nullptr; + for (auto &i : *this) + i.unbind(); +} + +void Item::reset_actor(const bool recursive) { + set_actor_addr_direct(caf::actor_addr()); + if (recursive) + for (auto &i : *this) + i.reset_actor(recursive); +} + +void Item::bind_item_pre_event_func(ItemEventFunc fn, const bool recursive) { + recursive_bind_pre_ = recursive; + item_pre_event_callback_ = [fn](auto &&PH1, auto &&PH2) { return fn(std::forward(PH1), std::forward(PH2)); }; - if (recursive_bind_) { + if (recursive_bind_pre_) { for (auto &i : *this) - i.bind_item_event_func(fn, recursive_bind_); + i.bind_item_pre_event_func(fn, recursive_bind_pre_); + } +} + +void Item::bind_item_post_event_func(ItemEventFunc fn, const bool recursive) { + recursive_bind_post_ = recursive; + item_post_event_callback_ = [fn](auto &&PH1, auto &&PH2) { + return fn(std::forward(PH1), std::forward(PH2)); + }; + + if (recursive_bind_post_) { + for (auto &i : *this) + i.bind_item_post_event_func(fn, recursive_bind_post_); } } @@ -849,8 +1267,8 @@ Item::item_at_frame(const int track_frame) const { return {}; } -utility::FrameRange Item::range_at_index(const int item_index) const { - auto result = utility::FrameRange(); +FrameRange Item::range_at_index(const int item_index) const { + auto result = FrameRange(); result.set_rate(trimmed_range().rate()); auto start = trimmed_range().start(); @@ -892,8 +1310,419 @@ int Item::frame_at_index(const int item_index, const int item_frame) const { return frame_at_index(item_index) + dur; } +std::optional Item::frame_at_item_frame( + const Uuid &item_uuid, const int item_local_frame, const bool skip_disabled) const { + std::vector result; + auto item = find_item(children(), item_uuid); + auto item_top_left = top_left(item_uuid); + if (item_top_left && item && (!skip_disabled || (*item)->enabled())) { + int f = item_local_frame - (*item)->trimmed_frame_start().frames(); + if (f >= 0 && f <= (*item)->trimmed_frame_duration().frames()) { + return item_top_left->first + item_local_frame - + (*item)->trimmed_frame_start().frames(); + } + } + return {}; +} + std::optional Item::item_at_index(const int index) const { if (index < 0 or index >= static_cast(size())) return {}; return std::next(begin(), index); } + + +std::optional Item::top_left(const Uuid &_uuid) const { + + if (_uuid == uuid()) { + return top_left(); + } else { + auto b = box(_uuid); + if (b) + return b->first; + } + + return {}; +} + +timeline::Point Item::top_left() const { return std::make_pair(0, 0); } + +std::optional Item::bottom_right(const Uuid &_uuid) const { + if (_uuid == uuid()) { + return bottom_right(); + } else { + auto b = box(_uuid); + if (b) + return b->second; + } + + return {}; +} + +timeline::Point Item::bottom_right() const { + auto y = 0; + switch (item_type_) { + case IT_TIMELINE: + y = 1; + if (not empty()) + y = front().bottom_right().second; + break; + + case IT_STACK: + // sum height of children. + for (const auto &i : children()) + y += i.bottom_right().second; + y = std::max(y, 1); + break; + + case IT_AUDIO_TRACK: + case IT_VIDEO_TRACK: + // sum height of children. + y = 1; + for (const auto &i : children()) + y = std::max(y, i.bottom_right().second); + break; + + default: + y = 1; + break; + } + + return std::make_pair(trimmed_frame_duration().frames(), y); +} + +std::optional Item::box(const Uuid &_uuid) const { + if (_uuid == uuid()) { + return box(); + } else { + if (not empty()) { + switch (item_type_) { + case IT_TIMELINE: + return front().box(_uuid); + break; + + case IT_STACK: + // we need to increment y + { + int y = 0; + for (const auto &i : children()) { + auto b = i.box(_uuid); + if (b) { + b->first.second += y; + b->second.second += y; + return *b; + break; + } + y++; + } + } + break; + + case IT_AUDIO_TRACK: + case IT_VIDEO_TRACK: { + int x = 0; + for (const auto &i : children()) { + auto b = i.box(_uuid); + if (b) { + b->first.first += x; + b->second.first += x; + return *b; + break; + } + x += i.trimmed_frame_duration().frames(); + } + } break; + + case IT_GAP: + case IT_CLIP: + default: + break; + } + } + } + + return {}; +} + +Box Item::box() const { return std::make_pair(top_left(), bottom_right()); } + +// merge gaps, prune trailing gaps, remove null clips. +void Item::merge_gaps(const bool purge_empty_clips) { + auto previous = end(); + auto it = begin(); + + while (it != end()) { + switch (it->item_type_) { + case IT_GAP: + if (previous != end() and previous->item_type_ == IT_GAP) { + // merge.. + previous->set_available_range(FrameRange( + previous->trimmed_start(), + previous->trimmed_duration() + it->trimmed_duration(), + previous->rate())); + previous->set_active_range(*(previous->available_range())); + + it = erase_direct(it); + } else { + previous = it; + it++; + } + break; + + case IT_CLIP: + if (it->prop().value("media_uuid", Uuid()).is_null() and purge_empty_clips) { + if (previous != end() and previous->item_type_ == IT_GAP) { + previous->set_available_range(FrameRange( + previous->trimmed_start(), + previous->trimmed_duration() + it->trimmed_duration(), + previous->rate())); + previous->set_active_range(*(previous->available_range())); + + it = erase_direct(it); + } else { + auto gap = Item(IT_GAP, "Gap"); + gap.set_available_range( + FrameRange(FrameRate(), it->trimmed_duration(), it->rate())); + gap.set_active_range(*(gap.available_range())); + + previous = insert_direct(it, gap); + it = erase_direct(it); + } + + } else { + previous = it; + it++; + } + break; + + case IT_AUDIO_TRACK: + case IT_VIDEO_TRACK: + case IT_TIMELINE: + // purge empty tracks from head and tail? + case IT_STACK: + it->merge_gaps(purge_empty_clips); + previous = it; + it++; + break; + + default: + previous = it; + it++; + break; + } + } + + // trim end gap + if (not empty() and back().item_type_ == IT_GAP) + erase_direct(std::next(begin(), size() - 1)); +} + +void Item::reset_uuid(const bool recursive) { + set_uuid(Uuid::generate()); + for (auto &i : markers_) + i.reset_uuid(); + + if (recursive) { + for (auto &i : children()) + i.reset_uuid(recursive); + } +} + +void Item::reset_media_uuid() { + if (item_type_ == IT_CLIP) { + if (not prop_.is_null()) + prop_["media_uuid"] = Uuid(); + } else { + for (auto &i : children()) + i.reset_media_uuid(); + } +} + +namespace xstudio::timeline { +/* Doing sync requests to the clip actors to build our FrameTimeMap can +get a bit ugly. To help with this we have this helper that processes the +responses from the clip actors and self destroys once all the expected responses +come in*/ +class BuildFrameIDsHelper { + + public: + BuildFrameIDsHelper( + caf::typed_response_promise rp, + const int rcount, + Item *parent, + const media::MediaType media_type) + : rp_(rp), media_type_(media_type) { + + base_rate_ = parent->rate(); + parent_actor_ = caf::actor_cast(parent->actor()); + count_ = size_t(rcount); + blank_frame = media::make_blank_frame(media_type); + result = new media::FrameTimeMap; + } + + void add_blank_frame(timebase::flicks timeline_tp); + void add_frame(caf::actor, timebase::flicks, const FrameRate &clip_tp); + + void incref() { refcount_++; } + + void decref() { + refcount_--; + if (!refcount_) { + if (rp_.pending()) { + rp_.deliver(make_error( + xstudio_error::error, "Timeline Item failed to complete frame IDs build.")); + } + delete this; + } + } + + void request_clip_frames(); + + void decrement_count(const size_t n = 1); + + private: + // store for the result + media::FrameTimeMap *result; + size_t count_; + int refcount_ = {0}; + FrameRate base_rate_; + caf::event_based_actor *parent_actor_ = nullptr; + caf::actor current_clip_actor_; + std::vector timeline_timepoints_; + std::vector clip_timepoints_; + const media::MediaType media_type_; + caf::typed_response_promise rp_; + // blank frame + std::shared_ptr blank_frame; +}; +} // namespace xstudio::timeline + +caf::typed_response_promise Item::get_all_frame_IDs( + const media::MediaType media_type, + const TimeSourceMode tsm, + const FrameRate &override_rate, + const UuidSet &focus_list) { + auto foo = caf::actor_cast(actor()); + auto rp = foo->make_response_promise(); + + if (!available_range()) { + rp.deliver(media::FrameTimeMapPtr()); + return rp; + } + + // First, get our frame range + const int start_frame = available_range()->frame_start().frames(override_rate); + const int end_frame = + start_frame + available_range()->frame_duration().frames(override_rate); + const int num_frames = end_frame - start_frame; + + BuildFrameIDsHelper *helper = new BuildFrameIDsHelper(rp, num_frames, this, media_type); + helper->incref(); + + const timebase::flicks delta = override_rate.to_flicks(); + timebase::flicks timepoint = start_frame * delta; + + // TODO: this needs optimisation - for multi-track, long timlines we see + // this taking 100s of milliseconds. The problem is the recursive call into + // resolve_time on every frame, recurses from Timeline->Stack->Video Track->clip/gap + // + // One solution that might work is to get an Item to 'print' itself into + // a result vector, eliminating function calls on every frame as a Clip will + // just do a simple loop to print itself into the result. + for (auto i = start_frame; i < end_frame; i++) { + + std::optional clip_item = + resolve_time(FrameRate(timepoint), media_type, focus_list); + + if (clip_item) + helper->add_frame(clip_item->first.actor(), timepoint, clip_item->second); + else + helper->add_blank_frame(timepoint); + timepoint += delta; + } + + helper->request_clip_frames(); + + // in case start_frame = end_frame, i.e. nothing to process this + // call will ensure we deliver ont the RP + helper->decrement_count(0); + helper->decref(); + + return rp; +} + +void BuildFrameIDsHelper::add_blank_frame(timebase::flicks timeline_tp) { + if (current_clip_actor_) { + request_clip_frames(); + current_clip_actor_ = caf::actor(); + timeline_timepoints_.clear(); + clip_timepoints_.clear(); + } + (*result)[timeline_tp] = blank_frame; + decrement_count(); +} + +void BuildFrameIDsHelper::add_frame( + caf::actor clip_actor, timebase::flicks timeline_tp, const FrameRate &clip_tp) { + + if (clip_actor != current_clip_actor_) { + + // we've moved to a new clip ... + request_clip_frames(); + current_clip_actor_ = clip_actor; + timeline_timepoints_.clear(); + clip_timepoints_.clear(); + } + + timeline_timepoints_.emplace_back(timeline_tp); + clip_timepoints_.emplace_back(clip_tp); +} + +void BuildFrameIDsHelper::request_clip_frames() { + + if (!current_clip_actor_) { + incref(); + for (const auto &tp : timeline_timepoints_) { + (*result)[tp] = blank_frame; + } + decrement_count(timeline_timepoints_.size()); + decref(); + return; + } + + const auto timeline_timepoints_cpy = timeline_timepoints_; + + incref(); + + parent_actor_ + ->mail(media::get_media_pointer_atom_v, media_type_, clip_timepoints_, base_rate_) + .request(current_clip_actor_, infinite) + .then( + [=](const media::AVFrameIDs &mps) mutable { + int idx = 0; + for (const auto &mp : mps) { + (*result)[timeline_timepoints_cpy[idx++]] = mp; + } + decrement_count(timeline_timepoints_cpy.size()); + decref(); + }, + + [=](error &err) mutable { + for (const auto &tp : timeline_timepoints_cpy) { + (*result)[tp] = blank_frame; + } + decrement_count(timeline_timepoints_cpy.size()); + decref(); + }); +} + +void BuildFrameIDsHelper::decrement_count(const size_t n) { + + if (n >= count_ && rp_.pending()) { + media::FrameTimeMapPtr r(result); + rp_.deliver(r); + + } else { + count_ -= n; + } +} \ No newline at end of file diff --git a/src/timeline/src/marker.cpp b/src/timeline/src/marker.cpp new file mode 100644 index 000000000..4d6b85ce7 --- /dev/null +++ b/src/timeline/src/marker.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "xstudio/timeline/marker.hpp" + +using namespace xstudio; +using namespace xstudio::timeline; +using namespace xstudio::utility; + +Marker::Marker(const JsonStore &jsn) { + uuid_ = jsn.at("uuid"); + range_ = jsn.at("range"); + name_ = jsn.value("name", ""); + flag_ = jsn.value("flag", ""); + prop_ = jsn.value("prop", JsonStore()); +} + +JsonStore Marker::serialise() const { + JsonStore jsn; + + jsn["uuid"] = uuid_; + jsn["range"] = range_; + jsn["name"] = name_; + jsn["flag"] = flag_; + jsn["prop"] = prop_; + + return jsn; +} diff --git a/src/timeline/src/stack.cpp b/src/timeline/src/stack.cpp index 590930175..e78d89868 100644 --- a/src/timeline/src/stack.cpp +++ b/src/timeline/src/stack.cpp @@ -1,24 +1,30 @@ // SPDX-License-Identifier: Apache-2.0 -#include #include "xstudio/timeline/stack.hpp" -#include "xstudio/utility/helpers.hpp" using namespace xstudio; using namespace xstudio::timeline; using namespace xstudio::utility; -Stack::Stack(const std::string &name, const utility::Uuid &uuid_, const caf::actor &actor) +Stack::Stack( + const std::string &name, const FrameRate &rate, const Uuid &uuid_, const caf::actor &actor) : Container(name, "Stack", uuid_), item_( ItemType::IT_STACK, - utility::UuidActorAddr(uuid(), caf::actor_cast(actor))) { + UuidActorAddr(uuid(), caf::actor_cast(actor)), + {}, + FrameRange(FrameRateDuration(0, rate))) { item_.set_name(name); } Stack::Stack(const JsonStore &jsn) - : Container(static_cast(jsn.at("container"))), - item_(static_cast(jsn.at("item"))) {} + : Container(static_cast(jsn.at("container"))), + item_(static_cast(jsn.at("item"))) {} + +Stack::Stack(const Item &item, const caf::actor &actor) + : Container(item.name(), "Stack", item.uuid()), item_(item.clone()) { + item_.set_actor_addr(caf::actor_cast(actor)); +} JsonStore Stack::serialise() const { JsonStore jsn; @@ -30,7 +36,7 @@ JsonStore Stack::serialise() const { } Stack Stack::duplicate() const { - utility::JsonStore jsn; + JsonStore jsn; auto dup_container = Container::duplicate(); auto dup_item = item_; diff --git a/src/timeline/src/stack_actor.cpp b/src/timeline/src/stack_actor.cpp index 29d4c7abb..c3176c757 100644 --- a/src/timeline/src/stack_actor.cpp +++ b/src/timeline/src/stack_actor.cpp @@ -2,64 +2,82 @@ #include #include "xstudio/atoms.hpp" -#include "xstudio/broadcast/broadcast_actor.hpp" #include "xstudio/timeline/clip_actor.hpp" #include "xstudio/timeline/stack_actor.hpp" #include "xstudio/timeline/gap_actor.hpp" -#include "xstudio/timeline/timeline_actor.hpp" #include "xstudio/timeline/track_actor.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" -#include "xstudio/utility/uuid.hpp" using namespace xstudio; using namespace xstudio::utility; using namespace xstudio::timeline; -using namespace caf; -caf::actor StackActor::deserialise(const utility::JsonStore &value, const bool replace_item) { - auto key = utility::Uuid(value.at("base").at("item").at("uuid")); +caf::actor StackActor::deserialise(const JsonStore &value, const bool replace_item) { + auto key = Uuid(value.at("base").at("item").at("uuid")); auto actor = caf::actor(); auto type = value.at("base").at("container").at("type").get(); + auto item = Item(); + if (type == "Track") { - auto item = Item(); - actor = spawn(static_cast(value), item); - add_item(UuidActor(key, actor)); - if (replace_item) { - auto itemit = find_uuid(base_.item().children(), key); - (*itemit) = item; - } + actor = spawn(static_cast(value), item); } else if (type == "Clip") { - auto item = Item(); - actor = spawn(static_cast(value), item); - add_item(UuidActor(key, actor)); - if (replace_item) { - auto itemit = find_uuid(base_.item().children(), key); - (*itemit) = item; - } + actor = spawn(static_cast(value), item); } else if (type == "Gap") { - auto item = Item(); - actor = spawn(static_cast(value), item); - add_item(UuidActor(key, actor)); - if (replace_item) { - auto itemit = find_uuid(base_.item().children(), key); - (*itemit) = item; - } + actor = spawn(static_cast(value), item); } else if (type == "Stack") { - auto item = Item(); - actor = spawn(static_cast(value), item); + actor = spawn(static_cast(value), item); + } + + if (actor) { add_item(UuidActor(key, actor)); if (replace_item) { auto itemit = find_uuid(base_.item().children(), key); - (*itemit) = item; + if (itemit != base_.item().end()) { + (*itemit) = item; + } else { + spdlog::warn( + "{} Invalid item to replace {} {}", + __PRETTY_FUNCTION__, + to_string(key), + value.dump(2)); + } } } + return actor; } -StackActor::StackActor(caf::actor_config &cfg, const utility::JsonStore &jsn) - : caf::event_based_actor(cfg), base_(static_cast(jsn.at("base"))) { +void StackActor::deserialise() { + for (auto &i : base_.item().children()) { + switch (i.item_type()) { + case IT_CLIP: { + auto actor = spawn(i, i); + add_item(UuidActor(i.uuid(), actor)); + } break; + case IT_GAP: { + auto actor = spawn(i, i); + add_item(UuidActor(i.uuid(), actor)); + } break; + case IT_STACK: { + auto actor = spawn(i, i); + add_item(UuidActor(i.uuid(), actor)); + } break; + case IT_AUDIO_TRACK: + case IT_VIDEO_TRACK: { + auto actor = spawn(i, i); + add_item(UuidActor(i.uuid(), actor)); + } break; + default: + break; + } + } +} + + +StackActor::StackActor(caf::actor_config &cfg, const JsonStore &jsn) + : caf::event_based_actor(cfg), base_(static_cast(jsn.at("base"))) { base_.item().set_actor_addr(this); @@ -71,15 +89,14 @@ StackActor::StackActor(caf::actor_config &cfg, const utility::JsonStore &jsn) } } base_.item().set_system(&system()); - base_.item().bind_item_event_func([this](const utility::JsonStore &event, Item &item) { - item_event_callback(event, item); - }); + base_.item().bind_item_post_event_func( + [this](const JsonStore &event, Item &item) { item_event_callback(event, item); }); init(); } -StackActor::StackActor(caf::actor_config &cfg, const utility::JsonStore &jsn, Item &pitem) - : caf::event_based_actor(cfg), base_(static_cast(jsn.at("base"))) { +StackActor::StackActor(caf::actor_config &cfg, const JsonStore &jsn, Item &pitem) + : caf::event_based_actor(cfg), base_(static_cast(jsn.at("base"))) { base_.item().set_actor_addr(this); @@ -91,48 +108,57 @@ StackActor::StackActor(caf::actor_config &cfg, const utility::JsonStore &jsn, It } } base_.item().set_system(&system()); - base_.item().bind_item_event_func([this](const utility::JsonStore &event, Item &item) { - item_event_callback(event, item); - }); - pitem = base_.item(); + base_.item().bind_item_post_event_func( + [this](const JsonStore &event, Item &item) { item_event_callback(event, item); }); + pitem = base_.item().clone(); init(); } StackActor::StackActor( - caf::actor_config &cfg, const std::string &name, const utility::Uuid &uuid) - : caf::event_based_actor(cfg), base_(name, uuid, this) { + caf::actor_config &cfg, const std::string &name, const FrameRate &rate, const Uuid &uuid) + : caf::event_based_actor(cfg), base_(name, rate, uuid, this) { base_.item().set_system(&system()); base_.item().set_name(name); - base_.item().bind_item_event_func([this](const utility::JsonStore &event, Item &item) { - item_event_callback(event, item); - }); + base_.item().bind_item_post_event_func( + [this](const JsonStore &event, Item &item) { item_event_callback(event, item); }); + init(); +} + +StackActor::StackActor(caf::actor_config &cfg, const Item &item) + : caf::event_based_actor(cfg), base_(item, this) { + base_.item().set_system(&system()); + deserialise(); init(); } +StackActor::StackActor(caf::actor_config &cfg, const Item &item, Item &nitem) + : StackActor(cfg, item) { + nitem = base_.item().clone(); +} + void StackActor::on_exit() { for (const auto &i : actors_) send_exit(i.second, caf::exit_reason::user_shutdown); } // trigger actor creation -void StackActor::item_event_callback(const utility::JsonStore &event, Item &item) { +void StackActor::item_event_callback(const JsonStore &event, Item &item) { switch (static_cast(event.at("action"))) { - case IT_INSERT: { - auto cuuid = utility::Uuid(event.at("item").at("uuid")); + case IA_INSERT: { + auto cuuid = Uuid(event.at("item").at("uuid")); // spdlog::warn("{} {} {} {}", find_uuid(base_.item().children(), cuuid) != // base_.item().cend(), actors_.count(cuuid), not event["blind"].is_null(), // event.dump(2)); needs to be child.. auto child_item_it = find_uuid(base_.item().children(), cuuid); - if (child_item_it != base_.item().cend() and not actors_.count(cuuid) and + if (child_item_it != base_.item().end() and not actors_.count(cuuid) and not event.at("blind").is_null()) { // our child // spdlog::warn("RECREATE MATCH"); - auto actor = deserialise(utility::JsonStore(event.at("blind")), false); - add_item(UuidActor(cuuid, actor)); + auto actor = deserialise(JsonStore(event.at("blind")), false); // spdlog::warn("{}",to_string(caf::actor_cast(actor))); // spdlog::warn("{}",to_string(caf::actor_cast(child_item_it->actor()))); child_item_it->set_actor_addr(actor); @@ -141,105 +167,126 @@ void StackActor::item_event_callback(const utility::JsonStore &event, Item &item // item actor_addr will be wrong.. in ancestors // send special update.. - send( - event_group_, - event_atom_v, - item_atom_v, - child_item_it->make_actor_addr_update(), - true); + mail(event_atom_v, item_atom_v, child_item_it->make_actor_addr_update(), true) + .send(base_.event_group()); } } break; - case IT_REMOVE: { - auto cuuid = utility::Uuid(event.at("item_uuid")); + case IA_REMOVE: { + auto cuuid = Uuid(event.at("item_uuid")); // child destroyed if (actors_.count(cuuid)) { // spdlog::warn("destroy // {}",to_string(caf::actor_cast(actors_[cuuid]))); - demonitor(actors_[cuuid]); + if (auto mit = monitor_.find(caf::actor_cast(actors_[cuuid])); + mit != std::end(monitor_)) { + mit->second.dispose(); + monitor_.erase(mit); + } + send_exit(actors_[cuuid], caf::exit_reason::user_shutdown); actors_.erase(cuuid); } } break; - case IT_ENABLE: - case IT_ACTIVE: - case IT_AVAIL: - case IT_SPLICE: - case IT_ADDR: + case IA_LOCK: + case IA_ENABLE: + case IA_ACTIVE: + case IA_RANGE: + case IA_AVAIL: + case IA_SPLICE: + case IA_ADDR: case IA_NONE: default: break; } } -void StackActor::init() { - print_on_create(this, base_.name()); - print_on_exit(this, base_.name()); +caf::message_handler StackActor::message_handler() { + return caf::message_handler{ + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) { + // should we handle child down ? + }, - event_group_ = spawn(this); - link_to(event_group_); + [=](event_atom, notification_atom, const JsonStore &) {}, - set_down_handler([=](down_msg &msg) { - // if a child dies we won't have enough information to recreate it. - // we still need to report it up the chain though. - for (auto it = std::begin(actors_); it != std::end(actors_); ++it) { - if (msg.source == it->second) { - demonitor(it->second); - actors_.erase(it); + [=](link_media_atom, const UuidActorMap &media, const bool force) -> result { + auto rp = make_response_promise(); - // remove from base. - auto it = find_actor_addr(base_.item().children(), msg.source); + if (actors_.empty()) { + rp.deliver(true); + } else { + // pool direct children for state. + fan_out_request( + map_value_to_vec(actors_), infinite, link_media_atom_v, media, force) + .await( + [=](std::vector items) mutable { rp.deliver(true); }, + [=](error &err) mutable { + spdlog::warn( + "{} {} {}", + __PRETTY_FUNCTION__, + to_string(err), + base_.item().name()); + rp.deliver(false); + }); + } + return rp; + }, - if (it != base_.item().end()) { - auto jsn = base_.item().erase(it); - auto more = base_.item().refresh(); - if (not more.is_null()) - jsn.insert(jsn.begin(), more.begin(), more.end()); + [=](item_marker_atom, insert_item_atom, const Marker &value) -> JsonStore { + auto markers = base_.item().markers(); + markers.push_back(value); + auto jsn = base_.item().set_markers(markers); - send(event_group_, event_atom_v, item_atom_v, jsn, false); - } - break; - } - } - }); - - behavior_.assign( - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); - [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) { - // should we handle child down ? + return jsn; }, - [=](link_media_atom, const UuidActorMap &media) -> result { - auto rp = make_response_promise(); + [=](item_marker_atom, insert_item_atom, const std::vector &value) -> JsonStore { + auto markers = base_.item().markers(); + markers.insert(markers.end(), value.begin(), value.end()); + auto jsn = base_.item().set_markers(markers); - // pool direct children for state. - fan_out_request( - map_value_to_vec(actors_), infinite, link_media_atom_v, media) - .await( - [=](std::vector items) mutable { rp.deliver(true); }, - [=](error &err) mutable { rp.deliver(err); }); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); - return rp; + return jsn; + }, + + [=](item_marker_atom, const std::vector &markers) -> JsonStore { + auto jsn = base_.item().set_markers(markers); + + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + + return jsn; + }, + + [=](item_type_atom) -> ItemType { return base_.item().item_type(); }, + + [=](rate_atom) -> FrameRate { return base_.item().rate(); }, + + [=](rate_atom atom, const media::MediaType media_type) { + return mail(atom).delegate(caf::actor_cast(this)); + }, + + [=](item_marker_atom) -> std::vector { + std::vector result( + base_.item().markers().begin(), base_.item().markers().end()); + return result; }, - [=](item_atom) -> Item { return base_.item(); }, + [=](item_atom) -> Item { return base_.item().clone(); }, [=](item_atom, const bool with_state) -> result> { auto rp = make_response_promise>(); - request(caf::actor_cast(this), infinite, utility::serialise_atom_v) + mail(serialise_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](const JsonStore &jsn) mutable { - rp.deliver(std::make_pair(jsn, base_.item())); + rp.deliver(std::make_pair(jsn, base_.item().clone())); }, [=](const caf::error &err) mutable { rp.deliver(err); }); return rp; @@ -248,14 +295,21 @@ void StackActor::init() { [=](item_flag_atom, const std::string &value) -> JsonStore { auto jsn = base_.item().set_flag(value); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_lock_atom, const bool value) -> JsonStore { + auto jsn = base_.item().set_locked(value); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, [=](item_name_atom, const std::string &value) -> JsonStore { auto jsn = base_.item().set_name(value); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, @@ -265,39 +319,71 @@ void StackActor::init() { } auto it = base_.item().cbegin(); std::advance(it, index); - return *it; + return (*it).clone(); + }, + + [=](item_prop_atom, const JsonStore &value, const bool merge) -> JsonStore { + auto p = base_.item().prop(); + p.update(value); + auto jsn = base_.item().set_prop(p); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_prop_atom, const JsonStore &value) -> JsonStore { + auto jsn = base_.item().set_prop(value); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_prop_atom, const JsonStore &value, const std::string &path) -> JsonStore { + auto prop = base_.item().prop(); + try { + auto ptr = nlohmann::json::json_pointer(path); + prop.at(ptr).update(value); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + auto jsn = base_.item().set_prop(prop); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; }, + [=](item_prop_atom) -> JsonStore { return base_.item().prop(); }, + [=](plugin_manager::enable_atom, const bool value) -> JsonStore { auto jsn = base_.item().set_enabled(value); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, [=](active_range_atom, const FrameRange &fr) -> JsonStore { auto jsn = base_.item().set_active_range(fr); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, [=](available_range_atom, const FrameRange &fr) -> JsonStore { auto jsn = base_.item().set_available_range(fr); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, - [=](active_range_atom) -> std::optional { + [=](active_range_atom) -> std::optional { return base_.item().active_range(); }, - [=](available_range_atom) -> std::optional { + [=](available_range_atom) -> std::optional { return base_.item().available_range(); }, - [=](trimmed_range_atom) -> utility::FrameRange { return base_.item().trimmed_range(); }, + [=](trimmed_range_atom) -> FrameRange { return base_.item().trimmed_range(); }, // should these be reflected upward ? [=](history::undo_atom, const JsonStore &hist) -> result { @@ -332,30 +418,44 @@ void StackActor::init() { return rp; }, + // check events processes + [=](item_atom, event_atom, const std::set &events) -> bool { + auto result = true; + for (const auto &i : events) { + if (not events_processed_.contains(i)) { + result = false; + break; + } + } + return result; + }, + // handle child change events. [=](event_atom, item_atom, const JsonStore &update, const bool hidden) { - if (base_.item().update(update)) { + auto event_ids = base_.item().update(update); + if (not event_ids.empty()) { + events_processed_.insert(event_ids.begin(), event_ids.end()); auto more = base_.item().refresh(); if (not more.is_null()) { more.insert(more.begin(), update.begin(), update.end()); - send(event_group_, event_atom_v, item_atom_v, more, hidden); + mail(event_atom_v, item_atom_v, more, hidden).send(base_.event_group()); return; } } - send(event_group_, event_atom_v, item_atom_v, update, hidden); + mail(event_atom_v, item_atom_v, update, hidden).send(base_.event_group()); }, [=](insert_item_atom, const int index, const UuidActorVector &uav) -> result { auto rp = make_response_promise(); - insert_items(index, uav, rp); + insert_items(rp, index, uav); return rp; }, [=](insert_item_atom, - const utility::Uuid &before_uuid, + const Uuid &before_uuid, const UuidActorVector &uav) -> result { auto rp = make_response_promise(); @@ -370,7 +470,7 @@ void StackActor::init() { } if (rp.pending()) - insert_items(index, uav, rp); + insert_items(rp, index, uav); return rp; }, @@ -378,14 +478,12 @@ void StackActor::init() { [=](move_item_atom, const int src_index, const int count, const int dst_index) -> result { auto rp = make_response_promise(); - move_items(src_index, count, dst_index, rp); + move_items(rp, src_index, count, dst_index); return rp; }, - [=](move_item_atom, - const utility::Uuid &src_uuid, - const int count, - const utility::Uuid &before_uuid) -> result { + [=](move_item_atom, const Uuid &src_uuid, const int count, const Uuid &before_uuid) + -> result { // check src is valid. auto rp = make_response_promise(); auto sitb = find_uuid(base_.item().children(), src_uuid); @@ -402,32 +500,45 @@ void StackActor::init() { } if (rp.pending()) move_items( + rp, std::distance(base_.item().begin(), sitb), count, - std::distance(base_.item().begin(), dit), - rp); + std::distance(base_.item().begin(), dit)); } return rp; }, - [=](remove_item_atom, - const int index) -> result>> { - auto rp = make_response_promise>>(); - remove_items(index, 1, rp); + [=](clear_atom) -> result { + auto rp = make_response_promise(); + mail(remove_item_atom_v, 0, (int)base_.item().size(), true) + .request(caf::actor_cast(this), infinite) + .then( + [=](const std::pair> &) mutable { + rp.deliver(true); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); return rp; }, [=](remove_item_atom, const int index, - const int count) -> result>> { + const bool) -> result>> { auto rp = make_response_promise>>(); - remove_items(index, count, rp); + remove_items(rp, index); + return rp; + }, + + [=](remove_item_atom, const int index, const int count, const bool) + -> result>> { + auto rp = make_response_promise>>(); + remove_items(rp, index, count); return rp; }, [=](remove_item_atom, - const utility::Uuid &uuid) -> result>> { + const Uuid &uuid, + const bool) -> result>> { auto rp = make_response_promise>>(); auto it = find_uuid(base_.item().children(), uuid); @@ -436,24 +547,25 @@ void StackActor::init() { rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); if (rp.pending()) - remove_items(std::distance(base_.item().begin(), it), 1, rp); + remove_items(rp, std::distance(base_.item().begin(), it)); return rp; }, - [=](erase_item_atom, const int index) -> result { + [=](erase_item_atom, const int index, const bool) -> result { auto rp = make_response_promise(); - erase_items(index, 1, rp); + erase_items(rp, index); return rp; }, - [=](erase_item_atom, const int index, const int count) -> result { + [=](erase_item_atom, const int index, const int count, const bool) + -> result { auto rp = make_response_promise(); - erase_items(index, count, rp); + erase_items(rp, index, count); return rp; }, - [=](erase_item_atom, const utility::Uuid &uuid) -> result { + [=](erase_item_atom, const Uuid &uuid, const bool) -> result { auto rp = make_response_promise(); auto it = find_uuid(base_.item().children(), uuid); @@ -462,12 +574,37 @@ void StackActor::init() { rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); if (rp.pending()) - erase_items(std::distance(base_.item().begin(), it), 1, rp); + erase_items(rp, std::distance(base_.item().begin(), it)); return rp; }, - [=](utility::duplicate_atom) -> result { + [=](playhead::source_atom, + const UuidUuidMap &swap, + const UuidActorMap &media) -> result { + auto rp = make_response_promise(); + + if (actors_.empty()) { + rp.deliver(true); + } else { + fan_out_request( + map_value_to_vec(actors_), infinite, playhead::source_atom_v, swap, media) + .await( + [=](std::vector items) mutable { rp.deliver(true); }, + [=](error &err) mutable { + spdlog::warn( + "{} {} {}", + __PRETTY_FUNCTION__, + to_string(err), + base_.item().name()); + rp.deliver(err); + }); + } + + return rp; + }, + + [=](duplicate_atom) -> result { auto rp = make_response_promise(); JsonStore jsn; auto dup = base_.duplicate(); @@ -484,8 +621,8 @@ void StackActor::init() { scoped_actor sys{system()}; for (const auto &i : base_.children()) { - auto ua = request_receive( - *sys, actors_[i.uuid()], utility::duplicate_atom_v); + auto ua = + request_receive(*sys, actors_[i.uuid()], duplicate_atom_v); request_receive( *sys, actor, insert_item_atom_v, -1, UuidActorVector({ua})); } @@ -495,7 +632,11 @@ void StackActor::init() { return rp; }, - [=](utility::serialise_atom) -> result { + [=](event_atom, change_atom) { + mail(event_atom_v, change_atom_v).send(base_.event_group()); + }, + + [=](serialise_atom) -> result { if (not actors_.empty()) { auto rp = make_response_promise(); @@ -521,30 +662,62 @@ void StackActor::init() { jsn["actors"] = {}; return result(jsn); - }); + }}; } -void StackActor::add_item(const utility::UuidActor &ua) { +void StackActor::init() { + print_on_create(this, base_.name()); + print_on_exit(this, base_.name()); +} + +void StackActor::add_item(const UuidActor &ua) { // joining must be in sync scoped_actor sys{system()}; try { - auto grp = - request_receive(*sys, ua.actor(), utility::get_event_group_atom_v); + auto grp = request_receive(*sys, ua.actor(), get_event_group_atom_v); auto joined = request_receive(*sys, grp, broadcast::join_broadcast_atom_v, this); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } - // join_event_group(this, ua.second); - monitor(ua.actor()); + auto act_addr = caf::actor_cast(ua.actor()); + + if (auto sit = monitor_.find(act_addr); sit == std::end(monitor_)) { + monitor_[act_addr] = + monitor(ua.actor(), [this, addr = ua.actor().address()](const error &) { + if (auto mit = monitor_.find(caf::actor_cast(addr)); + mit != std::end(monitor_)) + monitor_.erase(mit); + + for (auto it = std::begin(actors_); it != std::end(actors_); ++it) { + if (addr == it->second) { + actors_.erase(it); + + // remove from base. + auto it = find_actor_addr(base_.item().children(), addr); + + if (it != base_.item().end()) { + auto jsn = base_.item().erase(it); + auto more = base_.item().refresh(); + if (not more.is_null()) + jsn.insert(jsn.begin(), more.begin(), more.end()); + + mail(event_atom_v, item_atom_v, jsn, false) + .send(base_.event_group()); + } + + break; + } + } + }); + } + actors_[ua.uuid()] = ua.actor(); } void StackActor::insert_items( - const int index, - const UuidActorVector &uav, - caf::typed_response_promise rp) { + caf::typed_response_promise rp, const int index, const UuidActorVector &uav) { // validate items can be inserted. fan_out_request(vector_to_caf_actor_vector(uav), infinite, item_atom_v) .then( @@ -566,13 +739,14 @@ void StackActor::insert_items( // insert items.. // our list will be out of order.. auto changes = JsonStore(R"([])"_json); - for (const auto &ua : uav) { + for (auto uit = uav.rbegin(); uit != uav.rend(); ++uit) { // find item.. auto found = false; for (const auto &i : items) { - if (ua.uuid() == i.uuid()) { + if (uit->uuid() == i.uuid()) { auto tmp = base_.item().insert(it, i); - changes.insert(changes.begin(), tmp.begin(), tmp.end()); + it = std::next(base_.item().begin(), index); + changes.insert(changes.end(), tmp.begin(), tmp.end()); found = true; break; } @@ -586,25 +760,47 @@ void StackActor::insert_items( // add changes to stack auto more = base_.item().refresh(); if (not more.is_null()) - changes.insert(changes.begin(), more.begin(), more.end()); + changes.insert(changes.end(), more.begin(), more.end()); - send(event_group_, event_atom_v, item_atom_v, changes, false); + mail(event_atom_v, item_atom_v, changes, false).send(base_.event_group()); rp.deliver(changes); }, [=](const caf::error &err) mutable { rp.deliver(err); }); } void StackActor::remove_items( + caf::typed_response_promise>> rp, const int index, - const int count, - caf::typed_response_promise>> - rp) { + const int count) { + + try { + rp.deliver(remove_items(index, count)); + } catch (const std::exception &err) { + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +void StackActor::erase_items( + caf::typed_response_promise rp, const int index, const int count) { + + try { + auto result = remove_items(index, count); + for (const auto &i : result.second) + send_exit(i.actor(), caf::exit_reason::user_shutdown); + rp.deliver(result.first); + } catch (const std::exception &err) { + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +std::pair> +StackActor::remove_items(const int index, const int count) { std::vector items; JsonStore changes(R"([])"_json); if (index < 0 or index + count - 1 >= static_cast(base_.item().size())) - rp.deliver(make_error(xstudio_error::error, "Invalid index / count")); + throw std::runtime_error("Invalid index / count"); else { scoped_actor sys{system()}; @@ -612,14 +808,20 @@ void StackActor::remove_items( auto it = std::next(base_.item().begin(), i); if (it != base_.item().end()) { auto item = *it; - demonitor(item.actor()); + + if (auto mit = monitor_.find(caf::actor_cast(item.actor())); + mit != std::end(monitor_)) { + mit->second.dispose(); + monitor_.erase(mit); + } + actors_.erase(item.uuid()); auto blind = request_receive(*sys, item.actor(), serialise_atom_v); auto tmp = base_.item().erase(it, blind); changes.insert(changes.end(), tmp.begin(), tmp.end()); - items.push_back(item); + items.push_back(item.clone()); } } @@ -627,30 +829,18 @@ void StackActor::remove_items( if (not more.is_null()) changes.insert(changes.begin(), more.begin(), more.end()); - send(event_group_, event_atom_v, item_atom_v, changes, false); - - rp.deliver(std::make_pair(changes, items)); + mail(event_atom_v, item_atom_v, changes, false).send(base_.event_group()); } -} -void StackActor::erase_items( - const int index, const int count, caf::typed_response_promise rp) { - - request(caf::actor_cast(this), infinite, remove_item_atom_v, index, count) - .then( - [=](const std::pair> &hist_item) mutable { - for (const auto &i : hist_item.second) - send_exit(i.actor(), caf::exit_reason::user_shutdown); - rp.deliver(hist_item.first); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + return std::make_pair(changes, items); } + void StackActor::move_items( + caf::typed_response_promise rp, const int src_index, const int count, - const int dst_index, - caf::typed_response_promise rp) { + const int dst_index) { // don't allow mixing audio / video tracks ? @@ -666,7 +856,7 @@ void StackActor::move_items( if (not more.is_null()) changes.insert(changes.begin(), more.begin(), more.end()); - send(event_group_, event_atom_v, item_atom_v, changes, false); + mail(event_atom_v, item_atom_v, changes, false).send(base_.event_group()); rp.deliver(changes); } } diff --git a/src/timeline/src/timeline.cpp b/src/timeline/src/timeline.cpp index c82bd2c75..cdb3ad225 100644 --- a/src/timeline/src/timeline.cpp +++ b/src/timeline/src/timeline.cpp @@ -3,23 +3,25 @@ #include "xstudio/timeline/timeline.hpp" #include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/json_store.hpp" using namespace xstudio::timeline; using namespace xstudio::utility; -Timeline::Timeline(const std::string &name, const utility::Uuid &_uuid, const caf::actor &actor) +Timeline::Timeline( + const std::string &name, const FrameRate &rate, const Uuid &_uuid, const caf::actor &actor) : Container(name, "Timeline", _uuid), item_( ItemType::IT_TIMELINE, - utility::UuidActorAddr(uuid(), caf::actor_cast(actor))) { + UuidActorAddr(uuid(), caf::actor_cast(actor)), + {}, + FrameRange(FrameRateDuration(0, rate))) { item_.set_name(name); } Timeline::Timeline(const JsonStore &jsn) - : Container(static_cast(jsn.at("container"))), - item_(static_cast(jsn.at("item"))), - media_list_(static_cast(jsn.at("media"))) {} + : Container(static_cast(jsn.at("container"))), + item_(static_cast(jsn.at("item"))), + media_list_(static_cast(jsn.at("media"))) {} JsonStore Timeline::serialise() const { JsonStore jsn; @@ -32,7 +34,7 @@ JsonStore Timeline::serialise() const { } Timeline Timeline::duplicate() const { - utility::JsonStore jsn; + JsonStore jsn; auto dup_container = Container::duplicate(); auto dup_item = item_; diff --git a/src/timeline/src/timeline_actor.cpp b/src/timeline/src/timeline_actor.cpp index 84f9a6b4f..bca9cf378 100644 --- a/src/timeline/src/timeline_actor.cpp +++ b/src/timeline/src/timeline_actor.cpp @@ -1,18 +1,21 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include -#ifdef BUILD_OTIO #include #include #include #include +#include #include #include -#endif +#include +#include + +#include #include "xstudio/atoms.hpp" #include "xstudio/bookmark/bookmark_actor.hpp" -#include "xstudio/broadcast/broadcast_actor.hpp" #include "xstudio/history/history_actor.hpp" #include "xstudio/media/media_actor.hpp" #include "xstudio/playhead/playhead_actor.hpp" @@ -25,25 +28,66 @@ #include "xstudio/utility/chrono.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" -#include "xstudio/utility/uuid.hpp" using namespace xstudio; using namespace xstudio::utility; using namespace xstudio::timeline; -using namespace caf; -caf::actor -TimelineActor::deserialise(const utility::JsonStore &value, const bool replace_item) { - auto key = utility::Uuid(value.at("base").at("item").at("uuid")); +namespace { + +auto __sysclock_now() { +#ifdef _MSC_VER + /*auto tp = sysclock::now(); + return + std::chrono::duration_cast(tp.time_since_epoch()).count();*/ + return sysclock::now(); +#elif defined(__linux__) + return sysclock::now(); +#elif defined(__apple__) + return utility::clock::now(); +#endif +} + +const static auto COLOUR_JPOINTER = nlohmann::json::json_pointer("/xstudio/colour"); +const static auto LOCKED_JPOINTER = nlohmann::json::json_pointer("/xstudio/locked"); +const static auto CONFORM_JPOINTER = nlohmann::json::json_pointer("/xstudio/conform_track"); +const static auto ENABLED_JPOINTER = nlohmann::json::json_pointer("/xstudio/enabled"); +const static auto MEDIA_COLOUR_JPOINTER = nlohmann::json::json_pointer("/xstudio/media_colour"); + +} // namespace + +caf::actor TimelineActor::deserialise(const JsonStore &value, const bool replace_item) { + auto actor = caf::actor(); if (value.at("base").at("container").at("type") == "Stack") { + auto key = Uuid(value.at("base").at("item").at("uuid")); auto item = Item(); - actor = spawn(static_cast(value), item); + actor = spawn(static_cast(value), item); add_item(UuidActor(key, actor)); if (replace_item) { auto itemit = find_uuid(base_.item().children(), key); - (*itemit) = item; + + if (itemit != base_.item().end()) { + (*itemit) = item; + } else { + spdlog::warn( + "{} Invalid item to replace {} {}", + __PRETTY_FUNCTION__, + to_string(key), + value.dump(2)); + } + } + } else if (value.at("base").at("container").at("type") == "PlayheadSelection") { + + try { + + selection_actor_ = system().spawn( + static_cast(value), caf::actor_cast(this)); + link_to(selection_actor_); + + } catch (const std::exception &e) { + spdlog::error("{}", e.what()); } } @@ -51,22 +95,23 @@ TimelineActor::deserialise(const utility::JsonStore &value, const bool replace_i } // trigger actor creation -void TimelineActor::item_event_callback(const utility::JsonStore &event, Item &item) { +void TimelineActor::item_post_event_callback(const JsonStore &event, Item &item) { switch (static_cast(event.at("action"))) { - case IT_INSERT: { - auto cuuid = utility::Uuid(event.at("item").at("uuid")); + case IA_INSERT: { + auto cuuid = Uuid(event.at("item").at("uuid")); // spdlog::warn("{} {} {} {}", find_uuid(base_.item().children(), cuuid) != // base_.item().cend(), actors_.count(cuuid), not event["blind"].is_null(), // event.dump(2)); needs to be child.. auto child_item_it = find_uuid(base_.item().children(), cuuid); - if (child_item_it != base_.item().cend() and not actors_.count(cuuid) and + if (child_item_it != base_.item().cend() and not item_actors_.count(cuuid) and not event.at("blind").is_null()) { // our child // spdlog::warn("RECREATE MATCH"); - auto actor = deserialise(utility::JsonStore(event.at("blind")), false); + auto actor = deserialise(JsonStore(event.at("blind")), false); add_item(UuidActor(cuuid, actor)); + child_item_it = find_uuid(base_.item().children(), cuuid); // spdlog::warn("{}",to_string(caf::actor_cast(actor))); // spdlog::warn("{}",to_string(caf::actor_cast(child_item_it->actor()))); child_item_it->set_actor_addr(actor); @@ -75,134 +120,406 @@ void TimelineActor::item_event_callback(const utility::JsonStore &event, Item &i // item actor_addr will be wrong.. in ancestors // send special update.. - send( - event_group_, - event_atom_v, - item_atom_v, - child_item_it->make_actor_addr_update(), - true); + mail(event_atom_v, item_atom_v, child_item_it->make_actor_addr_update(), true) + .send(base_.event_group()); } - spdlog::warn("TimelineActor IT_INSERT"); + // spdlog::warn("TimelineActor IT_INSERT"); // rebuilt child.. trigger relink } break; - case IT_REMOVE: { - auto cuuid = utility::Uuid(event.at("item_uuid")); + case IA_REMOVE: { + auto cuuid = Uuid(event.at("item_uuid")); // child destroyed - if (actors_.count(cuuid)) { + if (item_actors_.count(cuuid)) { // spdlog::warn("destroy // {}",to_string(caf::actor_cast(actors_[cuuid]))); - demonitor(actors_[cuuid]); - send_exit(actors_[cuuid], caf::exit_reason::user_shutdown); - actors_.erase(cuuid); + if (auto mit = monitor_.find(caf::actor_cast(item_actors_[cuuid])); + mit != std::end(monitor_)) { + mit->second.dispose(); + monitor_.erase(mit); + } + + send_exit(item_actors_[cuuid], caf::exit_reason::user_shutdown); + item_actors_.erase(cuuid); } } break; - case IT_ENABLE: - case IT_ACTIVE: - case IT_AVAIL: - case IT_SPLICE: - case IT_ADDR: + case IA_PROP: case IA_NONE: + case IA_ENABLE: + case IA_ADDR: + case IA_RANGE: + case IA_ACTIVE: + case IA_AVAIL: + case IA_SPLICE: + case IA_NAME: + case IA_FLAG: + case IA_LOCK: default: break; } } -#ifdef BUILD_OTIO -namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; +void TimelineActor::item_pre_event_callback(const JsonStore &event, Item &item) { + try { + switch (static_cast(event.at("action"))) { + case IA_REMOVE: { + auto cuuid = Uuid(event.at("item_uuid")); + // spdlog::warn("{}", event.dump(2)); + // child destroyed + if (item_actors_.count(cuuid)) { + } else { + // watch for clip deletion events + // we'll want to check for media cleanup required. + auto citem = item.item_at_index(event.value("index", 0)); + if (citem and (*citem)->item_type() == IT_CLIP) { + auto media_uuid = (*citem)->prop().value("media_uuid", Uuid()); + // get a count of all references to this media.. + // more than one then nothing to do. + auto media_clips = find_media_clips(base_.children(), media_uuid); + if (media_clips.size() == 1) + anon_mail(playlist::remove_media_atom_v, UuidVector({media_uuid})) + .delay(std::chrono::milliseconds(100)) + .send(this); + } else { + // we need to find all clip items under this item that's being removed. + // collect a map of all media_uuids about to be removed. + auto mmap = std::map(); + auto clips = (*citem)->find_all_items(IT_CLIP); + for (const auto &i : clips) { + auto media_uuid = i.get().prop().value("media_uuid", Uuid()); + if (not media_uuid.is_null()) { + if (not mmap.count(media_uuid)) + mmap[media_uuid] = 0; + mmap[media_uuid]++; + } + } + + for (const auto &i : mmap) { + auto media_clips = find_media_clips(base_.children(), i.first); + if (media_clips.size() == i.second) { + anon_mail(playlist::remove_media_atom_v, UuidVector({i.first})) + .delay(std::chrono::milliseconds(500)) + .send(this); + } + } + } + } + } break; + case IA_PROP: { + const auto item_media_uuid = item.prop().value("media_uuid", Uuid()); + const auto new_item_media_uuid = event.at("value").value("media_uuid", Uuid()); + if (not item_media_uuid.is_null() and item_media_uuid != new_item_media_uuid) { + auto media_clips = find_media_clips(base_.children(), item_media_uuid); + if (media_clips.size() == 1) + anon_mail(playlist::remove_media_atom_v, UuidVector({item_media_uuid})) + .delay(std::chrono::milliseconds(100)) + .send(this); + } + // spdlog::warn("item_pre_event_callback {} {} {} {}", + // to_string(item_media_uuid),to_string(new_item_media_uuid),item.name(), + // event.dump(2)); + } break; + + case IA_INSERT: { + auto new_item = Item(JsonStore(event.at("item"))); + if (new_item.item_type() == IT_CLIP) { + auto media_uuid = new_item.prop().value("media_uuid", Uuid()); + // make sure media exists in timeline. + if (media_uuid and not media_actors_.count(media_uuid)) { + // request media from playlist.. + caf::scoped_actor sys(system()); + try { + // get uuid.. + auto mactor = request_receive( + *sys, + caf::actor_cast(playlist_), + playlist::get_media_atom_v, + media_uuid); + add_media(mactor, media_uuid, Uuid()); + mail( + event_atom_v, + playlist::add_media_atom_v, + UuidActorVector({UuidActor(media_uuid, mactor)})) + .send(base_.event_group()); + base_.send_changed(); + mail(event_atom_v, change_atom_v).send(base_.event_group()); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + } + } break; + + + default: + break; + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} + +namespace otio = opentimelineio::OPENTIMELINEIO_VERSION; // std::vector> const& children() const noexcept // { // return _children; // } +// { +// "OTIO_SCHEMA": "Marker.2", +// "metadata": { +// "fcp_xml": { +// "comment": "1001,1017-1142,1158" +// } +// }, +// "name": "064_BMC_0020", +// "color": "RED", +// "marked_range": { +// "OTIO_SCHEMA": "TimeRange.1", +// "duration": { +// "OTIO_SCHEMA": "RationalTime.1", +// "rate": 24.0, +// "value": 70.0 +// }, +// "start_time": { +// "OTIO_SCHEMA": "RationalTime.1", +// "rate": 24.0, +// "value": 0.0 +// } +// } +// } +std::vector process_markers( + const std::vector> &markers, + const FrameRate &timeline_rate) { + auto result = std::vector(); + + for (const auto &om : markers) { + auto m = Marker(om->name()); + + if (colors::wpf_named_color_converter::is_named(om->color())) + m.set_flag(colors::to_ahex_str(colors::color( + colors::value_of(colors::wpf_named_color_converter::value(om->color()))))); + + auto marker_metadata = JsonStore(); + try { + otio::ErrorStatus err; + auto marker_metadata = nlohmann::json::parse(om->to_json_string(&err, {}, 0)); + if (marker_metadata.count("metadata")) { + m.set_prop(JsonStore(marker_metadata.at("metadata"))); + if (m.prop().contains(COLOUR_JPOINTER)) { + m.set_flag(m.prop().at(COLOUR_JPOINTER).get()); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + + auto marked_range = om->marked_range(); + m.set_range(FrameRange( + FrameRateDuration( + static_cast(marked_range.start_time().value()), + FrameRate(fps_to_flicks(marked_range.start_time().rate()))), + FrameRateDuration( + static_cast(marked_range.duration().value()), + FrameRate(fps_to_flicks(marked_range.duration().rate()))))); + + result.emplace_back(m); + } + + return result; +} void process_item( const std::vector> &items, blocking_actor *self, + const caf::actor &timeline_actor, caf::actor &parent, - const std::map &media_lookup) { + const caf::uri &path, // otio path + const std::map &media_lookup, + const FrameRate &timeline_rate) { + + auto fcp_locked_path = nlohmann::json::json_pointer("/fcp_xml/locked"); + auto fcp_enabled_path = nlohmann::json::json_pointer("/fcp_xml/enabled"); + auto fcp_track_name_path = nlohmann::json::json_pointer("/fcp_xml/@MZ.TrackName"); + // let the fun begin.. for (auto i : items) { if (auto ii = dynamic_cast(&(*i))) { // spdlog::warn("Track"); - if (ii->kind() == otio::Track::Kind::video) { - // spdlog::warn("Video Track"); - - auto uuid = Uuid::generate(); - auto actor = - self->spawn(ii->name(), media::MediaType::MT_IMAGE, uuid); - auto source_range = ii->source_range(); - if (source_range) - self->request( - actor, - infinite, - active_range_atom_v, - FrameRange(FrameRateDuration( - static_cast(source_range->duration().value()), - source_range->duration().rate()))) - .receive([=](const JsonStore &) {}, [=](const error &err) {}); + auto media_type = media::MediaType::MT_IMAGE; + if (ii->kind() == otio::Track::Kind::audio) + media_type = media::MediaType::MT_AUDIO; - self->request( - parent, - infinite, - insert_item_atom_v, - -1, - UuidActorVector({UuidActor(uuid, actor)})) - .receive([=](const JsonStore &) {}, [=](const error &err) {}); + auto locked = false; + auto enabled = ii->enabled(); + auto name = ii->name(); + auto metadata = JsonStore(); + auto flag = std::string(); + auto conform_track = false; - process_item(ii->children(), self, actor, media_lookup); - } else if (ii->kind() == otio::Track::Kind::audio) { - // spdlog::warn("Audio Track"); - auto uuid = Uuid::generate(); - auto actor = - self->spawn(ii->name(), media::MediaType::MT_AUDIO, uuid); - auto source_range = ii->source_range(); + try { + // "fcp_xml": { + // "@MZ.TrackName": "Cut Ref QT", + // "@MZ.TrackTargeted": "1", + // "@TL.SQTrackExpanded": "0", + // "@TL.SQTrackExpandedHeight": "45", + // "@TL.SQTrackShy": "0", + // "enabled": "TRUE", + // "locked": "FALSE" + // } + otio::ErrorStatus err; + auto track_metadata = nlohmann::json::parse(ii->to_json_string(&err, {}, 0)); - if (source_range) - self->request( - actor, - infinite, - active_range_atom_v, - FrameRange(FrameRateDuration( - static_cast(source_range->duration().value()), - source_range->duration().rate()))) - .receive([=](const JsonStore &) {}, [=](const error &err) {}); + if (track_metadata.count("metadata")) + metadata = JsonStore(track_metadata.at("metadata")); + + if (metadata.contains(fcp_locked_path) and + metadata.at(fcp_locked_path).get() == "TRUE") + locked = true; + + if (metadata.contains(fcp_enabled_path) and + metadata.at(fcp_enabled_path).get() == "FALSE") + enabled = false; + + if (metadata.contains(fcp_track_name_path) and + metadata.at(fcp_track_name_path).get() != "") + name = metadata.at(fcp_track_name_path).get(); + + if (metadata.contains(COLOUR_JPOINTER)) + flag = metadata.at(COLOUR_JPOINTER).get(); + + if (metadata.contains(LOCKED_JPOINTER)) + locked = metadata.at(LOCKED_JPOINTER).get(); + + if (metadata.contains(CONFORM_JPOINTER)) + conform_track = metadata.at(CONFORM_JPOINTER).get(); + + if (metadata.contains(ENABLED_JPOINTER)) + enabled = metadata.at(ENABLED_JPOINTER).get(); - self->request( - parent, - infinite, - insert_item_atom_v, - -1, - UuidActorVector({UuidActor(uuid, actor)})) + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + auto uuid = Uuid::generate(); + auto actor = self->spawn(name, timeline_rate, media_type, uuid); + + if (locked) + self->mail(item_lock_atom_v, locked) + .request(actor, infinite) .receive([=](const JsonStore &) {}, [=](const error &err) {}); - process_item(ii->children(), self, actor, media_lookup); + if (conform_track) { + self->mail(item_prop_atom_v) + .request(timeline_actor, infinite) + .receive( + [=](JsonStore &prop) { + prop["conform_track_uuid"] = uuid; + self->mail(item_prop_atom_v, prop) + .request(timeline_actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + }, + [=](const error &err) {}); } + + if (not enabled) + self->mail(plugin_manager::enable_atom_v, enabled) + .request(actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + if (not flag.empty()) + self->mail(item_flag_atom_v, flag) + .request(actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + anon_mail(item_prop_atom_v, metadata, "").send(actor); + + auto source_range = ii->source_range(); + if (source_range) + self->mail( + active_range_atom_v, + FrameRange(FrameRateDuration( + static_cast(source_range->duration().value()), + FrameRate(fps_to_flicks(source_range->duration().rate()))))) + .request(actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + self->mail(insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, actor)})) + .request(parent, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + process_item( + ii->children(), self, timeline_actor, actor, path, media_lookup, timeline_rate); } else if (auto ii = dynamic_cast(&(*i))) { - auto uuid = Uuid::generate(); - auto actor = self->spawn(ii->name(), utility::FrameRateDuration(), uuid); + auto uuid = Uuid::generate(); + auto actor = + self->spawn(ii->name(), FrameRateDuration(0, timeline_rate), uuid); auto source_range = ii->source_range(); + try { + auto locked = false; + auto enabled = ii->enabled(); + auto metadata = JsonStore(); + auto flag = std::string(); + otio::ErrorStatus err; + auto ometadata = nlohmann::json::parse(ii->to_json_string(&err, {}, 0)); + + if (ometadata.count("metadata")) + metadata = JsonStore(ometadata.at("metadata")); + + if (metadata.contains(fcp_locked_path) and + metadata.at(fcp_locked_path).get() == "TRUE") + locked = true; + + if (metadata.contains(fcp_enabled_path) and + metadata.at(fcp_enabled_path).get() == "FALSE") + enabled = false; + + if (metadata.contains(COLOUR_JPOINTER)) + flag = metadata.at(COLOUR_JPOINTER).get(); + + if (metadata.contains(LOCKED_JPOINTER)) + locked = metadata.at(LOCKED_JPOINTER).get(); + + if (metadata.contains(ENABLED_JPOINTER)) + enabled = metadata.at(ENABLED_JPOINTER).get(); + + if (locked) + self->mail(item_lock_atom_v, locked) + .request(actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + if (not enabled) + self->mail(plugin_manager::enable_atom_v, enabled) + .request(actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + if (not flag.empty()) + self->mail(item_flag_atom_v, flag) + .request(actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + anon_mail(item_prop_atom_v, metadata, "").send(actor); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + if (source_range) - self->request( - actor, - infinite, + self->mail( active_range_atom_v, FrameRange(FrameRateDuration( static_cast(source_range->duration().value()), - source_range->duration().rate()))) + FrameRate(fps_to_flicks(source_range->duration().rate()))))) + .request(actor, infinite) .receive([=](const JsonStore &) {}, [=](const error &err) {}); - self->request( - parent, - infinite, - insert_item_atom_v, - -1, - UuidActorVector({UuidActor(uuid, actor)})) + self->mail(insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, actor)})) + .request(parent, infinite) .receive([=](const JsonStore &) {}, [=](const error &err) {}); } else if (auto ii = dynamic_cast(&(*i))) { @@ -217,111 +534,318 @@ void process_item( if (auto active = otio::SerializableObject::Retainer( dynamic_cast(ii->media_reference()))) { active_path = active->target_url(); + } else if ( + auto active = otio::SerializableObject::Retainer( + dynamic_cast(ii->media_reference()))) { + active_path = active->target_url_base() + active->name_prefix() + "{:0" + + std::to_string(active->frame_zero_padding()) + "d}" + + active->name_suffix(); + } + + // active_path maybe relative.. + if (not active_path.empty() and not caf::make_uri(active_path)) { + // not uri.... + // assume relative ? + auto tmp = uri_to_posix_path(path); + auto const pos = tmp.find_last_of('/'); + active_path = "file://" + tmp.substr(0, pos + 1) + active_path; } if (active_path.empty() or not media_lookup.count(active_path)) { - // spdlog::warn("ERRRR {}", active_path); // missing media.. actor = self->spawn(UuidActor(), ii->name(), uuid); } else { actor = self->spawn(media_lookup.at(active_path), ii->name(), uuid); + } - self->request( - actor, - infinite, - available_range_atom_v, - FrameRange( - FrameRateDuration( - static_cast(ii->available_range().start_time().value()), - ii->available_range().start_time().rate()), - FrameRateDuration( - static_cast(ii->available_range().duration().value()), - ii->available_range().duration().rate()))) - .receive([=](const JsonStore &) {}, [=](const error &err) {}); + try { + auto locked = false; + auto enabled = ii->enabled(); + auto metadata = JsonStore(); + auto flag = std::string(); + auto media_flag = std::string(); + otio::ErrorStatus err; + auto ometadata = nlohmann::json::parse(ii->to_json_string(&err, {}, 0)); + if (ometadata.count("metadata")) + metadata = JsonStore(ometadata.at("metadata")); + + if (metadata.contains(fcp_locked_path) and + metadata.at(fcp_locked_path).get() == "TRUE") + locked = true; + + if (metadata.contains(fcp_enabled_path) and + metadata.at(fcp_enabled_path).get() == "FALSE") + enabled = false; + + if (metadata.contains(COLOUR_JPOINTER)) + flag = metadata.at(COLOUR_JPOINTER).get(); + + if (metadata.contains(MEDIA_COLOUR_JPOINTER)) + media_flag = metadata.at(MEDIA_COLOUR_JPOINTER).get(); + + if (metadata.contains(LOCKED_JPOINTER)) + locked = metadata.at(LOCKED_JPOINTER).get(); + + if (metadata.contains(ENABLED_JPOINTER)) + enabled = metadata.at(ENABLED_JPOINTER).get(); + + if (locked) + self->mail(item_lock_atom_v, locked) + .request(actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + if (not enabled) + self->mail(plugin_manager::enable_atom_v, enabled) + .request(actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + if (not flag.empty()) + self->mail(item_flag_atom_v, flag) + .request(actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + if (not metadata.is_null()) + anon_mail(item_prop_atom_v, metadata, "").send(actor); + + // set media colour + if (not media_flag.empty() and not active_path.empty() and + media_lookup.count(active_path)) { + self->mail( + playlist::reflag_container_atom_v, + std::tuple, std::optional>( + media_flag, {})) + .request(media_lookup.at(active_path).actor(), infinite) + .receive([=](const bool) {}, [=](const error &err) {}); + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } auto source_range = ii->source_range(); if (source_range) { - self->request( - actor, - infinite, + self->mail( active_range_atom_v, FrameRange( FrameRateDuration( static_cast(source_range->start_time().value()), - source_range->start_time().rate()), + FrameRate(fps_to_flicks(source_range->start_time().rate()))), FrameRateDuration( static_cast(source_range->duration().value()), - source_range->duration().rate()))) + FrameRate(fps_to_flicks(source_range->duration().rate()))))) + .request(actor, infinite) .receive([=](const JsonStore &) {}, [=](const error &err) {}); } - self->request( - parent, - infinite, - insert_item_atom_v, - -1, - UuidActorVector({UuidActor(uuid, actor)})) + self->mail(insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, actor)})) + .request(parent, infinite) .receive([=](const JsonStore &) {}, [=](const error &err) {}); } else if (auto ii = dynamic_cast(&(*i))) { // spdlog::warn("Stack"); + // timeline where marker live.. + auto uuid = Uuid::generate(); + // spdlog::warn("SPAWN stack {}", timeline_rate.to_fps()); + auto actor = self->spawn(ii->name(), timeline_rate, uuid); + + if (not ii->enabled()) + self->mail(plugin_manager::enable_atom_v, false) + .request(actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + auto markers = process_markers(ii->markers(), timeline_rate); + + if (not markers.empty()) + anon_mail(item_marker_atom_v, insert_item_atom_v, markers).send(actor); + + try { + auto locked = false; + auto enabled = ii->enabled(); + auto metadata = JsonStore(); + auto flag = std::string(); + otio::ErrorStatus err; + auto ometadata = nlohmann::json::parse(ii->to_json_string(&err, {}, 0)); + if (ometadata.count("metadata")) + metadata = JsonStore(ometadata.at("metadata")); + + if (metadata.contains(fcp_locked_path) and + metadata.at(fcp_locked_path).get() == "TRUE") + locked = true; + + if (metadata.contains(fcp_enabled_path) and + metadata.at(fcp_enabled_path).get() == "FALSE") + enabled = false; + + if (metadata.contains(COLOUR_JPOINTER)) + flag = metadata.at(COLOUR_JPOINTER).get(); + + if (metadata.contains(LOCKED_JPOINTER)) + locked = metadata.at(LOCKED_JPOINTER).get(); + + if (metadata.contains(ENABLED_JPOINTER)) + enabled = metadata.at(ENABLED_JPOINTER).get(); + + if (locked) + self->mail(item_lock_atom_v, locked) + .request(actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + if (not enabled) + self->mail(plugin_manager::enable_atom_v, enabled) + .request(actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + if (not flag.empty()) + self->mail(item_flag_atom_v, flag) + .request(actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + anon_mail(item_prop_atom_v, metadata, "").send(actor); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } - auto uuid = Uuid::generate(); - auto actor = self->spawn(ii->name(), uuid); auto source_range = ii->source_range(); if (source_range) - self->request( - actor, - infinite, + self->mail( active_range_atom_v, FrameRange(FrameRateDuration( static_cast(source_range->duration().value()), - source_range->duration().rate()))) + FrameRate(fps_to_flicks(source_range->duration().rate()))))) + .request(actor, infinite) .receive([=](const JsonStore &) {}, [=](const error &err) {}); - self->request( - parent, - infinite, - insert_item_atom_v, - -1, - UuidActorVector({UuidActor(uuid, actor)})) + self->mail(insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, actor)})) + .request(parent, infinite) .receive([=](const JsonStore &) {}, [=](const error &err) {}); - process_item(ii->children(), self, parent, media_lookup); + process_item( + ii->children(), + self, + timeline_actor, + parent, + path, + media_lookup, + timeline_rate); } } } - void timeline_importer( blocking_actor *self, - caf::response_promise rp, + caf::typed_response_promise rp, const caf::actor &playlist, const UuidActor &dst, + const caf::uri &path, const std::string &data) { otio::ErrorStatus error_status; otio::SerializableObject::Retainer timeline; + // notify processing.. + auto notify_uuid = Uuid(); + { + auto notify = Notification::ProgressRangeNotification("Loading timeline"); + anon_mail(notification_atom_v, notify).send(dst.actor()); + notify_uuid = notify.uuid(); + } + timeline = otio::SerializableObject::Retainer( dynamic_cast(otio::Timeline::from_json_string(data, &error_status))); if (otio::is_error(error_status)) { + auto failnotify = Notification::WarnNotification("Failed Loading timeline"); + failnotify.uuid(notify_uuid); + anon_mail(notification_atom_v, failnotify).send(dst.actor()); + return rp.deliver(false); } - // spdlog::warn("{}", data); - // timeline loaded, convert to native timeline. // iterate over media, and add to playlist. const std::vector> clips = (timeline->find_clips()); + auto timeline_rate = FrameRate(); + auto timeline_start_frame = 0; + + if (timeline->global_start_time()) { + timeline_rate = FrameRate(fps_to_flicks(timeline->global_start_time()->rate())); + timeline_start_frame = static_cast(timeline->global_start_time()->value()); + } else { + // need to determine timeline rate somehow.. + // rate of first clip ? + for (const auto &cl : clips) { + auto source_range = cl->source_range(); + if (source_range) { + timeline_rate = FrameRate(fps_to_flicks(source_range->start_time().rate())); + break; + } + } + } + + { + auto notify = + Notification::ProgressRangeNotification("Loading timeline", 0, 0, clips.size()); + notify.uuid(notify_uuid); + anon_mail(notification_atom_v, notify).send(dst.actor()); + } + + + self->mail( + available_range_atom_v, + FrameRange( + FrameRateDuration(timeline_start_frame, timeline_rate), + FrameRateDuration(0, timeline_rate))) + .request(dst.actor(), infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + auto timeline_metadata = JsonStore(R"({})"_json); + try { + otio::ErrorStatus err; + auto metadata = nlohmann::json::parse(timeline->to_json_string(&err, {}, 0)); + if (metadata.count("metadata")) + timeline_metadata = metadata.at("metadata"); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + timeline_metadata["path"] = to_string(path); + + anon_mail(item_prop_atom_v, timeline_metadata).send(dst.actor()); + + // this maps Media actors to the url for the active media ref. We use this + // to check if a clip can re-use Media that we already have, or whether we + // need to create a new Media item. + std::map existing_media_url_map; std::map target_url_map; + spdlog::info("Processing {} clips", clips.size()); + // fill our map with media that's already in the parent playlist + self->mail(playlist::get_media_atom_v) + .request(playlist, infinite) + .receive( + [&](const std::vector &all_media_in_playlist) mutable { + for (auto &media : all_media_in_playlist) { + self->mail(media::media_reference_atom_v, Uuid()) + .request(media.actor(), infinite) + .receive( + [&](const std::pair &ref) mutable { + existing_media_url_map[uri_to_posix_path(ref.second.uri())] = + media; + }, + [=](error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } + }, + [=](error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); + + float count = 0.0; for (const auto &cl : clips) { + count++; + anon_mail(notification_atom_v, notify_uuid, count).send(dst.actor()); + const auto &name = cl->name(); // spdlog::warn("{} {}", name, cl->active_media_reference_key()); @@ -331,24 +855,41 @@ void timeline_importer( if (auto active = otio::SerializableObject::Retainer( dynamic_cast(cl->media_reference()))) { active_path = active->target_url(); + } else if ( + auto active = otio::SerializableObject::Retainer( + dynamic_cast(cl->media_reference()))) { + + active_path = active->target_url_base() + active->name_prefix() + "{:0" + + std::to_string(active->frame_zero_padding()) + "d}" + + active->name_suffix(); } - // spdlog::warn("{} {}", active_key, active_path); + // active_path maybe relative.. + if (not caf::make_uri(active_path)) { + // not uri.... + // assume relative ? + auto tmp = uri_to_posix_path(path); + auto const pos = tmp.find_last_of('/'); + active_path = "file://" + tmp.substr(0, pos + 1) + active_path; + } // WARNING this may inadvertantly skip auxiliary sources we want.. if (active_path.empty() or target_url_map.count(active_path)) { - // spdlog::warn("SKIP"); + // spdlog::warn("SKIP {}", active_path); + continue; + } else if (existing_media_url_map.count(active_path)) { + // spdlog::warn("SKIP EXISTING {}", active_path); + target_url_map[active_path] = existing_media_url_map[active_path]; continue; } - auto clip_metadata = JsonStore(); try { otio::ErrorStatus err; auto clip_meta = nlohmann::json::parse(cl->to_json_string(&err, {}, 0)); - if (clip_meta.count("metadata")) { + if (clip_meta.count("metadata")) clip_metadata = JsonStore(clip_meta.at("metadata")); - } + } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -358,20 +899,26 @@ void timeline_importer( // create media sources. for (const auto &mr : cl->media_references()) { - // spdlog::warn("{} {}", mr.first, mr.second->name()); // try and dynamic cast to if (auto ext = otio::SerializableObject::Retainer( dynamic_cast(mr.second))) { - // spdlog::warn("{}", ext->target_url()); auto uri = caf::make_uri(ext->target_url()); + if (!uri) { + // not uri.... + // assume relative ? + auto tmp = uri_to_posix_path(path); + auto const pos = tmp.find_last_of('/'); + uri = posix_path_to_uri(tmp.substr(0, pos + 1) + ext->target_url()); + } + if (uri) { auto extname = ext->name(); - auto source_uuid = utility::Uuid::generate(); + auto source_uuid = Uuid::generate(); auto rate = FrameRate(); auto ar = ext->available_range(); if (ar) { - rate = FrameRate(ar->start_time().rate()); + rate = FrameRate(fps_to_flicks(ar->start_time().rate())); } auto source_metadata = JsonStore(); @@ -392,11 +939,57 @@ void timeline_importer( source_uuid); if (not source_metadata.is_null()) - anon_send( - source, - json_store::set_json_atom_v, - source_metadata, - "/metadata/timeline"); + anon_mail( + json_store::set_json_atom_v, source_metadata, "/metadata/timeline") + .send(source); + + sources.emplace_back(UuidActor(source_uuid, source)); + } + } else if ( + auto ext = otio::SerializableObject::Retainer( + dynamic_cast(mr.second))) { + + auto uri = caf::make_uri( + ext->target_url_base() + ext->name_prefix() + "{:0" + + std::to_string(ext->frame_zero_padding()) + "d}" + ext->name_suffix()); + if (!uri) { + uri = posix_path_to_uri( + ext->target_url_base() + ext->name_prefix() + "{:0" + + std::to_string(ext->frame_zero_padding()) + "d}" + ext->name_suffix()); + } + if (uri) { + auto extname = ext->name(); + auto source_uuid = Uuid::generate(); + auto rate = FrameRate(); + auto ar = ext->available_range(); + if (ar) { + rate = FrameRate(fps_to_flicks(ar->start_time().rate())); + } + + auto source_metadata = JsonStore(); + try { + otio::ErrorStatus err; + auto ext_meta = nlohmann::json::parse(ext->to_json_string(&err, {}, 0)); + if (ext_meta.count("metadata")) { + source_metadata = JsonStore(ext_meta.at("metadata")); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + auto source = self->spawn( + extname.empty() ? std::string("ImageSequenceReference") : extname, + *uri, + FrameList( + std::to_string(ext->start_frame()) + "-" + + std::to_string(ext->end_frame())), + rate, + source_uuid); + + if (not source_metadata.is_null()) + anon_mail( + json_store::set_json_atom_v, source_metadata, "/metadata/timeline") + .send(source); sources.emplace_back(UuidActor(source_uuid, source)); } @@ -412,19 +1005,14 @@ void timeline_importer( UuidActor(uuid, self->spawn(name, uuid, sources)); if (not clip_metadata.is_null()) - anon_send( - target_url_map[active_path].actor(), - json_store::set_json_atom_v, - clip_metadata, - "/metadata/timeline"); - - anon_send( - target_url_map[active_path].actor(), - media::current_media_source_atom_v, - sources.front().uuid()); + anon_mail(json_store::set_json_atom_v, clip_metadata, "/metadata/timeline") + .send(target_url_map[active_path].actor()); + + anon_mail(media::current_media_source_atom_v, sources.front().uuid()) + .send(target_url_map[active_path].actor()); } - // otio::RationalTime dur = cl->duration(); + otio::RationalTime dur = cl->duration(); // std::cout << "Name: " << cl->name() << " ["; // std::cout << dur.value() << "/" << dur.rate() << "]" << std::endl; // trigger population of additional sources ? May conflict with timeline ? @@ -439,20 +1027,22 @@ void timeline_importer( new_media.push_back(i.second); // trigger additional sources. - utility::UuidList media_uuids; + UuidList media_uuids; for (const auto &i : target_url_map) { media_uuids.push_back(i.second.uuid()); } // add to playlist/timeline - self->request(dst.actor(), infinite, playlist::add_media_atom_v, new_media, Uuid()) + self->mail(playlist::add_media_atom_v, new_media, Uuid()) + .request(dst.actor(), infinite) .receive([=](const UuidActor &) {}, [=](const error &err) {}); } // process timeline. caf::actor history_actor; - self->request(dst.actor(), infinite, history::history_atom_v) + self->mail(history::history_atom_v) + .request(dst.actor(), infinite) .receive( [&](const UuidActor &ua) mutable { history_actor = ua.actor(); }, [=](error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); @@ -460,7 +1050,7 @@ void timeline_importer( // disable history whilst populating // should we clear it ? if (history_actor) { - anon_send(history_actor, plugin_manager::enable_atom_v, false); + anon_mail(plugin_manager::enable_atom_v, false).send(history_actor); } // build timeline, for fun and profit.. @@ -478,35 +1068,69 @@ void timeline_importer( // get timeline stack.. auto stack_actor = caf::actor(); - self->request(dst.actor(), infinite, item_atom_v, 0) + self->mail(item_atom_v, 0) + .request(dst.actor(), infinite) .receive( [&](const Item &item) mutable { stack_actor = item.actor(); }, [=](error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }); if (stack_actor) { - process_item(tracks, self, stack_actor, target_url_map); + // set stack actor rate via avaliable range + self->mail( + available_range_atom_v, + FrameRange( + FrameRateDuration(0, timeline_rate), FrameRateDuration(0, timeline_rate))) + .request(stack_actor, infinite) + .receive([=](const JsonStore &) {}, [=](const error &err) {}); + + // process markers on top level stack.. + auto stack = timeline->tracks(); + + auto markers = process_markers(stack->markers(), timeline_rate); + + if (not markers.empty()) + anon_mail(item_marker_atom_v, insert_item_atom_v, markers).send(stack_actor); + + process_item( + tracks, self, dst.actor(), stack_actor, path, target_url_map, timeline_rate); } // enable history, we've finished. if (history_actor) { - anon_send(history_actor, plugin_manager::enable_atom_v, true); + anon_mail(plugin_manager::enable_atom_v, true).send(history_actor); } + // spdlog::warn("imported"); + + auto successnotify = Notification::InfoNotification("Timeline Loaded"); + successnotify.uuid(notify_uuid); + anon_mail(notification_atom_v, successnotify).send(dst.actor()); + rp.deliver(true); } -#endif BUILD_OTIO - TimelineActor::TimelineActor( - caf::actor_config &cfg, const utility::JsonStore &jsn, const caf::actor &playlist) + caf::actor_config &cfg, const JsonStore &jsn, const caf::actor &playlist) : caf::event_based_actor(cfg), - base_(static_cast(jsn["base"])), + base_(static_cast(jsn["base"])), playlist_(playlist ? caf::actor_cast(playlist) : caf::actor_addr()) { base_.item().set_actor_addr(this); // parse and generate tracks/stacks. if (playlist) - anon_send(this, playhead::source_atom_v, playlist, UuidUuidMap()); + anon_mail(playhead::source_atom_v, playlist, UuidUuidMap()).send(this); + + if (jsn.contains("playhead")) { + playhead_serialisation_ = jsn["playhead"]; + } + + jsn_handler_ = json_store::JsonStoreHandler( + dynamic_cast(this), + base_.event_group(), + Uuid::generate(), + not jsn.count("store") or jsn["store"].is_null() + ? JsonStore() + : static_cast(jsn["store"])); for (const auto &[key, value] : jsn["actors"].items()) { try { @@ -517,9 +1141,13 @@ TimelineActor::TimelineActor( } base_.item().set_system(&system()); - base_.item().bind_item_event_func([this](const utility::JsonStore &event, Item &item) { - item_event_callback(event, item); - }); + + base_.item().bind_item_pre_event_func( + [this](const JsonStore &event, Item &item) { item_pre_event_callback(event, item); }, + true); + base_.item().bind_item_post_event_func( + [this](const JsonStore &event, Item &item) { item_post_event_callback(event, item); }); + init(); } @@ -527,136 +1155,116 @@ TimelineActor::TimelineActor( TimelineActor::TimelineActor( caf::actor_config &cfg, const std::string &name, - const utility::Uuid &uuid, - const caf::actor &playlist) + const FrameRate &rate, + const Uuid &uuid, + const caf::actor &playlist, + const bool with_tracks) : caf::event_based_actor(cfg), - base_(name, uuid, this), + base_(name, rate, uuid, this), playlist_(playlist ? caf::actor_cast(playlist) : caf::actor_addr()) { // create default stack - auto suuid = Uuid::generate(); - auto stack = spawn("Stack", suuid); - anon_send( - this, insert_item_atom_v, 0, UuidActorVector({UuidActor(suuid, stack)})); - base_.item().set_system(&system()); - base_.item().set_name(name); - base_.item().bind_item_event_func([this](const utility::JsonStore &event, Item &item) { - item_event_callback(event, item); - }); - init(); -} + auto stack_item = Item(IT_STACK, "Stack", rate); -caf::message_handler TimelineActor::default_event_handler() { - return { - [=](utility::event_atom, item_atom, const Item &) {}, - [=](utility::event_atom, item_atom, const JsonStore &, const bool) {}, - }; -} + if (with_tracks) { + stack_item.insert(stack_item.end(), Item(IT_VIDEO_TRACK, "Video Track", rate)); + stack_item.insert(stack_item.end(), Item(IT_AUDIO_TRACK, "Audio Track", rate)); + auto vuuid = stack_item.front().uuid(); + auto pjsn = R"({})"_json; + pjsn["conform_track_uuid"] = vuuid; + base_.item().set_prop(JsonStore(pjsn)); + } + auto stack = spawn(stack_item, stack_item); -void TimelineActor::init() { - print_on_create(this, base_.name()); - print_on_exit(this, base_.name()); + base_.item().set_system(&system()); + base_.item().set_name(name); - event_group_ = spawn(this); - link_to(event_group_); + add_item(UuidActor(stack_item.uuid(), stack)); + base_.item().insert(base_.item().begin(), stack_item); + base_.item().refresh(); - auto change_event_group_ = spawn(this); - link_to(change_event_group_); + base_.item().bind_item_pre_event_func( + [this](const JsonStore &event, Item &item) { item_pre_event_callback(event, item); }, + true); + base_.item().bind_item_post_event_func( + [this](const JsonStore &event, Item &item) { item_post_event_callback(event, item); }); - auto history_uuid = Uuid::generate(); - auto history_ = spawn>(history_uuid); - link_to(history_); - - auto selection_actor_ = spawn( - "SubsetPlayheadSelectionActor", caf::actor_cast(this)); - link_to(selection_actor_); - - set_down_handler([=](down_msg &msg) { - // find in playhead list.. - for (auto it = std::begin(actors_); it != std::end(actors_); ++it) { - // if a child dies we won't have enough information to recreate it. - // we still need to report it up the chain though. - - if (msg.source == it->second) { - demonitor(it->second); - - // if media.. - if (base_.remove_media(it->first)) { - send(event_group_, utility::event_atom_v, change_atom_v); - send( - event_group_, - utility::event_atom_v, - playlist::remove_media_atom_v, - UuidVector({it->first})); - base_.send_changed(event_group_, this); - } + jsn_handler_ = json_store::JsonStoreHandler( + dynamic_cast(this), base_.event_group()); - actors_.erase(it); + init(); +} - // remove from base. - auto it = find_actor_addr(base_.item().children(), msg.source); +caf::message_handler TimelineActor::default_event_handler() { + return caf::message_handler( + { + [=](event_atom, item_atom, const Item &) {}, + [=](event_atom, item_atom, const JsonStore &, const bool) {}, + }) + .or_else(NotificationHandler::default_event_handler()) + .or_else(json_store::JsonStoreHandler::default_event_handler()) + .or_else(Container::default_event_handler()); +} - if (it != base_.item().end()) { - auto jsn = base_.item().erase(it); - auto more = base_.item().refresh(); - if (not more.is_null()) - jsn.insert(jsn.begin(), more.begin(), more.end()); - send(event_group_, event_atom_v, item_atom_v, jsn, false); - } - break; - } - } - }); +caf::message_handler TimelineActor::message_handler() { + return caf::message_handler{ + [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - // update_edit_list_ = true; + [=](history::history_atom) -> UuidActor { return UuidActor(history_uuid_, history_); }, - behavior_.assign( - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), + [=](link_media_atom, const bool force) -> result { + auto rp = make_response_promise(); - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) {}, + if (item_actors_.empty()) { + rp.deliver(true); + } else { + // pool direct children for state. + fan_out_request( + map_value_to_vec(item_actors_), + infinite, + link_media_atom_v, + media_actors_, + force) + .await( + [=](std::vector items) mutable { rp.deliver(true); }, + [=](error &err) mutable { + spdlog::warn( + "{} {} {}", + __PRETTY_FUNCTION__, + to_string(err), + base_.item().name()); + rp.deliver(false); + }); + } - [=](history::history_atom) -> UuidActor { return UuidActor(history_uuid, history_); }, + return rp; + }, - [=](link_media_atom, const UuidActorMap &media) -> result { + [=](link_media_atom, const UuidActorMap &media, const bool force) -> result { auto rp = make_response_promise(); - - // pool direct children for state. - fan_out_request( - map_value_to_vec(actors_), infinite, link_media_atom_v, media) - .await( - [=](std::vector items) mutable { rp.deliver(true); }, - [=](error &err) mutable { rp.deliver(err); }); + if (item_actors_.empty()) { + rp.deliver(true); + } else { + // pool direct children for state. + fan_out_request( + map_value_to_vec(item_actors_), infinite, link_media_atom_v, media, force) + .await( + [=](std::vector items) mutable { rp.deliver(true); }, + [=](error &err) mutable { rp.deliver(err); }); + } return rp; }, + [=](active_range_atom, const FrameRange &fr) -> JsonStore { auto jsn = base_.item().set_active_range(fr); if (not jsn.is_null()) { - send(event_group_, event_atom_v, item_atom_v, jsn, false); -#ifdef _MSC_VER - auto tp = sysclock::now(); - auto micros = - std::chrono::duration_cast(tp.time_since_epoch()) - .count(); - // using nano_sys = std::chrono::time_point; - anon_send(history_, history::log_atom_v, micros, jsn); -#else - anon_send(history_, history::log_atom_v, sysclock::now(), jsn); -#endif + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + anon_mail(history::log_atom_v, __sysclock_now(), jsn).send(history_); } return jsn; }, @@ -664,62 +1272,82 @@ void TimelineActor::init() { [=](item_flag_atom, const std::string &value) -> JsonStore { auto jsn = base_.item().set_flag(value); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_lock_atom, const bool value) -> JsonStore { + auto jsn = base_.item().set_locked(value); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, [=](item_name_atom, const std::string &value) -> JsonStore { auto jsn = base_.item().set_name(value); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, + [=](item_prop_atom, const JsonStore &value, const bool merge) -> JsonStore { + auto p = base_.item().prop(); + p.update(value); + auto jsn = base_.item().set_prop(p); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_prop_atom, const JsonStore &value) -> JsonStore { + auto jsn = base_.item().set_prop(value); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_prop_atom, const JsonStore &value, const std::string &path) -> JsonStore { + auto prop = base_.item().prop(); + try { + auto ptr = nlohmann::json::json_pointer(path); + prop.at(ptr).update(value); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + auto jsn = base_.item().set_prop(prop); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_prop_atom) -> JsonStore { return base_.item().prop(); }, + [=](available_range_atom, const FrameRange &fr) -> JsonStore { auto jsn = base_.item().set_available_range(fr); if (not jsn.is_null()) { - send(event_group_, event_atom_v, item_atom_v, jsn, false); - -#ifdef _MSC_VER - auto tp = sysclock::now(); - auto micros = - std::chrono::duration_cast(tp.time_since_epoch()) - .count(); - anon_send(history_, history::log_atom_v, micros, jsn); -#else - anon_send(history_, history::log_atom_v, sysclock::now(), jsn); -#endif + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + anon_mail(history::log_atom_v, __sysclock_now(), jsn).send(history_); } return jsn; }, - [=](active_range_atom) -> std::optional { + [=](active_range_atom) -> std::optional { return base_.item().active_range(); }, - [=](available_range_atom) -> std::optional { + [=](available_range_atom) -> std::optional { return base_.item().available_range(); }, - [=](trimmed_range_atom) -> utility::FrameRange { return base_.item().trimmed_range(); }, + [=](trimmed_range_atom) -> FrameRange { return base_.item().trimmed_range(); }, - [=](item_atom) -> Item { return base_.item(); }, + [=](item_atom) -> Item { return base_.item().clone(); }, [=](plugin_manager::enable_atom, const bool value) -> JsonStore { auto jsn = base_.item().set_enabled(value); if (not jsn.is_null()) { - send(event_group_, event_atom_v, item_atom_v, jsn, false); -#ifdef _MSC_VER - auto tp = sysclock::now(); - auto micros = - std::chrono::duration_cast(tp.time_since_epoch()) - .count(); - using nano_sys = std::chrono:: - time_point; - anon_send(history_, history::log_atom_v, micros, jsn); -#else - anon_send(history_, history::log_atom_v, sysclock::now(), jsn); -#endif + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + anon_mail(history::log_atom_v, __sysclock_now(), jsn).send(history_); } return jsn; }, @@ -730,80 +1358,95 @@ void TimelineActor::init() { } auto it = base_.item().cbegin(); std::advance(it, index); - return *it; + return (*it).clone(); + }, + + // search for item in children. + [=](item_atom, const Uuid &id) -> result { + auto item = find_item(base_.item().children(), id); + if (item) + return (**item).clone(); + + return make_error(xstudio_error::error, "Invalid uuid"); }, - [=](utility::event_atom, utility::change_atom, const bool) { + [=](event_atom, change_atom, const bool) { content_changed_ = false; - // send(event_group_, event_atom_v, item_atom_v, base_.item()); - send(event_group_, utility::event_atom_v, change_atom_v); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); + // mail(event_atom_v, item_atom_v, base_.item()).send(base_.event_group()); + mail(event_atom_v, change_atom_v).send(base_.event_group()); + mail(event_atom_v, change_atom_v).send(change_event_group_); + + + // Ted - TODO - this WIP stuff allows comparing of video tracks within + // a timeline. + auto video_tracks = base_.item().find_all_uuid_actors(IT_VIDEO_TRACK, true); + if (video_tracks != video_tracks_) { + video_tracks_ = video_tracks; + if (playhead_) { + // now set this (and its video tracks) as the source for + // the timeple playhead + + anon_mail( + playhead::source_atom_v, + UuidActor(base_.uuid(), caf::actor_cast(this)), + video_tracks_) + .send(playhead_.actor()); + } + } }, - [=](utility::event_atom, utility::change_atom) { + [=](event_atom, change_atom) { if (not content_changed_) { content_changed_ = true; - delayed_send( - this, - std::chrono::milliseconds(50), - utility::event_atom_v, - change_atom_v, - true); + mail(event_atom_v, change_atom_v, true) + .delay(std::chrono::milliseconds(50)) + .send(this); } }, - // handle child change events. - // [=](event_atom, item_atom, const Item &item) { - // // it's possibly one of ours.. so try and substitue the record - // if(base_.item().replace_child(item)) { - // base_.item().refresh(); - // send(this, utility::event_atom_v, change_atom_v); - // } - // }, + // check events processes + [=](item_atom, event_atom, const std::set &events) -> bool { + auto result = true; + for (const auto &i : events) { + if (not events_processed_.contains(i)) { + result = false; + break; + } + } + return result; + }, // handle child change events. [=](event_atom, item_atom, const JsonStore &update, const bool hidden) { - if (base_.item().update(update)) { + auto event_ids = base_.item().update(update); + if (not event_ids.empty()) { + events_processed_.insert(event_ids.begin(), event_ids.end()); auto more = base_.item().refresh(); if (not more.is_null()) { more.insert(more.begin(), update.begin(), update.end()); - send(event_group_, event_atom_v, item_atom_v, more, hidden); - if (not hidden) { -#ifdef _WIN32 - auto tp = sysclock::now(); - auto micros = std::chrono::duration_cast( - tp.time_since_epoch()) - .count(); - anon_send(history_, history::log_atom_v, micros, more); -#else - anon_send(history_, history::log_atom_v, sysclock::now(), more); -#endif - } - send(this, utility::event_atom_v, change_atom_v); + mail(event_atom_v, item_atom_v, more, hidden).send(base_.event_group()); + if (not hidden) + anon_mail(history::log_atom_v, __sysclock_now(), more).send(history_); + + mail(event_atom_v, change_atom_v).send(this); return; } } - send(event_group_, event_atom_v, item_atom_v, update, hidden); - if (not hidden) { -#ifdef _WIN32 - auto tp = sysclock::now(); - auto micros = - std::chrono::duration_cast(tp.time_since_epoch()) - .count(); - anon_send(history_, history::log_atom_v, micros, update); -#else - anon_send(history_, history::log_atom_v, sysclock::now(), update); -#endif - } - send(this, utility::event_atom_v, change_atom_v); + mail(event_atom_v, item_atom_v, update, hidden).send(base_.event_group()); + if (not hidden) + anon_mail(history::log_atom_v, __sysclock_now(), update).send(history_); + + if (base_.item().has_dirty(update)) + mail(event_atom_v, change_atom_v).send(this); }, // loop with timepoint [=](history::undo_atom, const sys_time_point &key) -> result { auto rp = make_response_promise(); - request(history_, infinite, history::undo_atom_v, key) + mail(history::undo_atom_v, key) + .request(history_, infinite) .then( [=](const JsonStore &hist) mutable { rp.delegate( @@ -817,7 +1460,8 @@ void TimelineActor::init() { [=](history::redo_atom, const sys_time_point &key) -> result { auto rp = make_response_promise(); - request(history_, infinite, history::redo_atom_v, key) + mail(history::redo_atom_v, key) + .request(history_, infinite) .then( [=](const JsonStore &hist) mutable { rp.delegate( @@ -827,28 +1471,30 @@ void TimelineActor::init() { return rp; }, - [=](history::undo_atom, const utility::sys_time_duration &duration) -> result { + [=](history::undo_atom, const sys_time_duration &duration) -> result { auto rp = make_response_promise(); - request(history_, infinite, history::undo_atom_v, duration) + mail(history::undo_atom_v, duration) + .request(history_, infinite) .then( [=](const std::vector &hist) mutable { auto count = std::make_shared(0); for (const auto &h : hist) { - request( - caf::actor_cast(this), - infinite, - history::undo_atom_v, - h) + mail(history::undo_atom_v, h) + .request(caf::actor_cast(this), infinite) .then( [=](const bool) mutable { (*count)++; - if (*count == hist.size()) + if (*count == hist.size()) { rp.deliver(true); + mail(event_atom_v, change_atom_v).send(this); + } }, [=](const error &err) mutable { (*count)++; - if (*count == hist.size()) + if (*count == hist.size()) { rp.deliver(true); + mail(event_atom_v, change_atom_v).send(this); + } }); } }, @@ -856,28 +1502,30 @@ void TimelineActor::init() { return rp; }, - [=](history::redo_atom, const utility::sys_time_duration &duration) -> result { + [=](history::redo_atom, const sys_time_duration &duration) -> result { auto rp = make_response_promise(); - request(history_, infinite, history::redo_atom_v, duration) + mail(history::redo_atom_v, duration) + .request(history_, infinite) .then( [=](const std::vector &hist) mutable { auto count = std::make_shared(0); for (const auto &h : hist) { - request( - caf::actor_cast(this), - infinite, - history::redo_atom_v, - h) + mail(history::redo_atom_v, h) + .request(caf::actor_cast(this), infinite) .then( [=](const bool) mutable { (*count)++; - if (*count == hist.size()) + if (*count == hist.size()) { rp.deliver(true); + mail(event_atom_v, change_atom_v).send(this); + } }, [=](const error &err) mutable { (*count)++; - if (*count == hist.size()) + if (*count == hist.size()) { rp.deliver(true); + mail(event_atom_v, change_atom_v).send(this); + } }); } }, @@ -887,7 +1535,8 @@ void TimelineActor::init() { [=](history::undo_atom) -> result { auto rp = make_response_promise(); - request(history_, infinite, history::undo_atom_v) + mail(history::undo_atom_v) + .request(history_, infinite) .then( [=](const JsonStore &hist) mutable { rp.delegate( @@ -899,7 +1548,8 @@ void TimelineActor::init() { [=](history::redo_atom) -> result { auto rp = make_response_promise(); - request(history_, infinite, history::redo_atom_v) + mail(history::redo_atom_v) + .request(history_, infinite) .then( [=](const JsonStore &hist) mutable { rp.delegate( @@ -915,33 +1565,31 @@ void TimelineActor::init() { base_.item().undo(hist); auto inverted = R"([])"_json; - for (const auto &i : hist) { + for (auto it = hist.crbegin(); it != hist.crend(); ++it) { auto ev = R"({})"_json; - ev["redo"] = i.at("undo"); - ev["undo"] = i.at("redo"); + ev["redo"] = it->at("undo"); + ev["undo"] = it->at("redo"); inverted.emplace_back(ev); } - // send(event_group_, event_atom_v, item_atom_v, JsonStore(inverted), true); + // mail(event_atom_v, item_atom_v, JsonStore(inverted), + // true).send(base_.event_group()); - if (not actors_.empty()) { + if (not item_actors_.empty()) { // push to children.. fan_out_request( - map_value_to_vec(actors_), infinite, history::undo_atom_v, hist) + map_value_to_vec(item_actors_), infinite, history::undo_atom_v, hist) .await( [=](std::vector updated) mutable { - anon_send(this, link_media_atom_v, media_actors_); - send( - event_group_, - event_atom_v, - item_atom_v, - JsonStore(inverted), - true); + anon_mail(link_media_atom_v, media_actors_, false).send(this); + mail(event_atom_v, item_atom_v, JsonStore(inverted), true) + .send(base_.event_group()); rp.deliver(true); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); } else { - send(event_group_, event_atom_v, item_atom_v, JsonStore(inverted), true); + mail(event_atom_v, item_atom_v, JsonStore(inverted), true) + .send(base_.event_group()); rp.deliver(true); } return rp; @@ -951,28 +1599,81 @@ void TimelineActor::init() { auto rp = make_response_promise(); base_.item().redo(hist); - // send(event_group_, event_atom_v, item_atom_v, hist, true); + // mail(event_atom_v, item_atom_v, hist, true).send(base_.event_group()); - if (not actors_.empty()) { + if (not item_actors_.empty()) { // push to children.. fan_out_request( - map_value_to_vec(actors_), infinite, history::redo_atom_v, hist) + map_value_to_vec(item_actors_), infinite, history::redo_atom_v, hist) .await( [=](std::vector updated) mutable { rp.deliver(true); - anon_send(this, link_media_atom_v, media_actors_); + anon_mail(link_media_atom_v, media_actors_, false).send(this); - send(event_group_, event_atom_v, item_atom_v, hist, true); + mail(event_atom_v, item_atom_v, hist, true) + .send(base_.event_group()); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); } else { - send(event_group_, event_atom_v, item_atom_v, hist, true); + mail(event_atom_v, item_atom_v, hist, true).send(base_.event_group()); rp.deliver(true); } return rp; }, + [=](bake_atom, const UuidVector &uuids) -> result { + auto rp = make_response_promise(); + + bake(rp, UuidSet(uuids.begin(), uuids.end())); + + return rp; + }, + + [=](bake_atom, + const FrameRate &time, + const FrameRate &duration) -> result>> { + auto result = std::vector>(); + + for (auto i = time; i <= time + duration; i += base_.item().rate()) { + result.emplace_back(base_.item().resolve_time( + i, media::MediaType::MT_IMAGE, base_.focus_list())); + } + + return result; + }, + + [=](bake_atom, const FrameRate &time) -> result { + auto ri = + base_.item().resolve_time(time, media::MediaType::MT_IMAGE, base_.focus_list()); + if (ri) + return *ri; + + return make_error(xstudio_error::error, "No clip resolved"); + }, + + [=](item_selection_atom) -> UuidActorVector { return selection_; }, + + [=](item_type_atom) -> ItemType { return base_.item().item_type(); }, + + [=](item_selection_atom, const UuidActorVector &selection) { selection_ = selection; }, + + [=](media_frame_to_timeline_frames_atom, + const Uuid &media_uuid, + const int mediaFrame, + const bool skipDisabled) -> std::vector { + std::vector result; + auto media_clips = find_media_clips(base_.children(), media_uuid); + for (const auto &clip_uuid : media_clips) { + auto frame = + base_.item().frame_at_item_frame(clip_uuid, mediaFrame, skipDisabled); + if (frame) { + result.push_back(*frame); + } + } + return result; + }, + [=](insert_item_atom, const int index, const UuidActorVector &uav) -> result { @@ -981,12 +1682,12 @@ void TimelineActor::init() { if (not base_.item().empty() or uav.size() > 1) rp.deliver(make_error(xstudio_error::error, "Only one child allowed")); else - insert_items(index, uav, rp); + insert_items(rp, index, uav); return rp; }, [=](insert_item_atom, - const utility::Uuid &before_uuid, + const Uuid &before_uuid, const UuidActorVector &uav) -> result { auto rp = make_response_promise(); @@ -1005,29 +1706,45 @@ void TimelineActor::init() { } if (rp.pending()) - insert_items(index, uav, rp); + insert_items(rp, index, uav); } return rp; }, [=](remove_item_atom, - const int index) -> result>> { + const int index, + const bool) -> result>> { auto rp = make_response_promise>>(); - remove_items(index, 1, rp); + remove_items(rp, index); return rp; }, - [=](remove_item_atom, - const int index, - const int count) -> result>> { + [=](remove_item_atom, const int index, const int count, const bool) + -> result>> { auto rp = make_response_promise>>(); - remove_items(index, count, rp); + remove_items(rp, index, count); + return rp; + }, + + // delegate to deep child.. + [=](remove_item_atom, const Uuid &uuid, const bool add_gap, const bool) + -> result>> { + auto rp = make_response_promise>>(); + + auto actor = find_parent_actor(base_.item(), uuid); + + if (not actor) + rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); + else + rp.delegate(actor, remove_item_atom_v, uuid, add_gap); + return rp; }, [=](remove_item_atom, - const utility::Uuid &uuid) -> result>> { + const Uuid &uuid, + const bool) -> result>> { auto rp = make_response_promise>>(); auto it = find_uuid(base_.item().children(), uuid); @@ -1036,24 +1753,40 @@ void TimelineActor::init() { rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); if (rp.pending()) - remove_items(std::distance(base_.item().begin(), it), 1, rp); + remove_items(rp, std::distance(base_.item().begin(), it)); return rp; }, - [=](erase_item_atom, const int index) -> result { + [=](erase_item_atom, const int index, const bool) -> result { auto rp = make_response_promise(); - erase_items(index, 1, rp); + erase_items(rp, index); return rp; }, - [=](erase_item_atom, const int index, const int count) -> result { + [=](erase_item_atom, const int index, const int count, const bool) + -> result { auto rp = make_response_promise(); - erase_items(index, count, rp); + erase_items(rp, index, count); return rp; }, - [=](erase_item_atom, const utility::Uuid &uuid) -> result { + // delegate to deep child.. + [=](erase_item_atom, const Uuid &uuid, const bool add_gap, const bool) + -> result { + auto rp = make_response_promise(); + + auto actor = find_parent_actor(base_.item(), uuid); + + if (not actor) + rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); + else + rp.delegate(actor, erase_item_atom_v, uuid, add_gap); + + return rp; + }, + + [=](erase_item_atom, const Uuid &uuid, const bool) -> result { auto rp = make_response_promise(); auto it = find_uuid(base_.item().children(), uuid); @@ -1062,89 +1795,60 @@ void TimelineActor::init() { rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); if (rp.pending()) - erase_items(std::distance(base_.item().begin(), it), 1, rp); + erase_items(rp, std::distance(base_.item().begin(), it)); return rp; }, // emulate subset - [=](playlist::sort_alphabetically_atom) { sort_alphabetically(); }, + [=](playlist::sort_by_media_display_info_atom, + const int sort_column_index, + const bool ascending) { sort_by_media_display_info(sort_column_index, ascending); }, - [=](media::get_edit_list_atom, media::MediaType, const Uuid &) -> utility::EditList { - // Edit list actor (from A/B compare in PlaheadActor) sends this - // message, we will return empty edit list as getting Timelines to - // construct EditLists seems pointless at this stage and instead we - // will get rid of EditListActor - return utility::EditList(); - }, - - [=](media::source_offset_frames_atom) -> int { - // needed when retime actor wraps a timeline - return 0; - }, - - [=](media::source_offset_frames_atom, const int) -> bool { - // needed when retime actor wraps a timeline - return false; - }, - - [=](timeline::duration_atom, const timebase::flicks &new_duration) -> bool { + [=](duration_atom, const timebase::flicks &new_duration) -> bool { // attempt by playhead to force trim the duration (to support compare // modes for sources of different lenght). Here we ignore it. return false; }, - [=](media::get_edit_list_atom, const Uuid &uuid) -> result { - std::vector actors; - for (const auto &i : base_.media()) - actors.push_back(actors_[i]); - - if (not actors.empty()) { - auto rp = make_response_promise(); - - fan_out_request( - actors, infinite, media::get_edit_list_atom_v, Uuid()) - .then( - [=](std::vector sections) mutable { - utility::EditList ordered_sections; - for (const auto &i : base_.media()) { - for (const auto &ii : sections) { - const auto &[ud, rt, tc] = ii.section_list()[0]; - if (ud == i) { - if (uuid.is_null()) - ordered_sections.push_back(ii.section_list()[0]); - else - ordered_sections.push_back({uuid, rt, tc}); - break; - } - } - } - rp.deliver(ordered_sections); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - - return rp; - } - - return result(utility::EditList()); - }, - [=](media::get_media_pointer_atom, const media::MediaType media_type, const int logical_frame) -> result { // get actors attached to our media.. if (not base_.empty()) { auto rp = make_response_promise(); - deliver_media_pointer(logical_frame, media_type, rp); + deliver_media_pointer(rp, logical_frame, media_type); return rp; } return result(make_error(xstudio_error::error, "No media")); }, - [=](playlist::get_media_uuid_atom) -> UuidVector { return base_.media_vector(); }, + [=](playlist::add_media_atom atom, + const caf::uri &path, + const bool recursive, + const Uuid &uuid_before) -> result> { + auto rp = make_response_promise>(); + mail(atom, path, recursive, uuid_before) + .request(caf::actor_cast(playlist_), infinite) + .then( + [=](const std::vector new_media) mutable { + for (const auto &m : new_media) { + add_media(m.actor(), m.uuid(), uuid_before); + } + mail(event_atom_v, playlist::add_media_atom_v, new_media) + .send(base_.event_group()); + base_.send_changed(); + mail(event_atom_v, change_atom_v).send(base_.event_group()); + mail(event_atom_v, change_atom_v).send(change_event_group_); + rp.deliver(new_media); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + return rp; + }, + [=](playlist::add_media_atom, const UuidActorVector &uav, const Uuid &before_uuid, @@ -1168,43 +1872,41 @@ void TimelineActor::init() { } // dispatch to playlist - request( - caf::actor_cast(playlist_), - infinite, - playlist::add_media_atom_v, - uav, - Uuid()) + mail(playlist::add_media_atom_v, uav, Uuid()) + .request(caf::actor_cast(playlist_), infinite) .then( [=](const bool) mutable { rp.deliver(true); - anon_send( - caf::actor_cast(playlist_), - media_hook::gather_media_sources_atom_v, - vector_to_uuid_vector(uav)); + anon_mail( + media_hook::gather_media_sources_atom_v, vector_to_uuid_vector(uav)) + .send(caf::actor_cast(playlist_)); // just one vent to trigger rebuild ? - send( - event_group_, - utility::event_atom_v, + mail( + event_atom_v, playlist::add_media_atom_v, - UuidActor(uav[0].uuid(), uav[0].actor())); + UuidActorVector({UuidActor(uav[0].uuid(), uav[0].actor())})) + .send(base_.event_group()); - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); - send( - change_event_group_, utility::event_atom_v, utility::change_atom_v); + base_.send_changed(); + mail(event_atom_v, change_atom_v).send(base_.event_group()); + mail(event_atom_v, change_atom_v).send(change_event_group_); }, [=](const caf::error &err) mutable { rp.deliver(err); }); return rp; }, + [=](session::media_rate_atom atom) { + return mail(atom).delegate(caf::actor_cast(playlist_)); + }, + [=](playlist::add_media_atom, const UuidActor &ua, const Uuid &before_uuid) -> result { auto rp = make_response_promise(); - add_media(ua, before_uuid, rp); + add_media(rp, ua, before_uuid); return rp; }, @@ -1214,8 +1916,8 @@ void TimelineActor::init() { // get actor from playlist.. auto rp = make_response_promise(); - request( - caf::actor_cast(playlist_), infinite, playlist::get_media_atom_v, uuid) + mail(playlist::get_media_atom_v, uuid) + .request(caf::actor_cast(playlist_), infinite) .then( [=](caf::actor actor) mutable { rp.delegate( @@ -1224,16 +1926,6 @@ void TimelineActor::init() { uuid, actor, before_uuid); - // add_media(actor, uuid, before_uuid); - // send(event_group_, utility::event_atom_v, change_atom_v); - // send(change_event_group_, utility::event_atom_v, - // utility::change_atom_v); send( - // event_group_, - // utility::event_atom_v, - // playlist::add_media_atom_v, - // UuidActor(uuid, actor)); - // base_.send_changed(event_group_, this); - // rp.deliver(true); }, [=](error &err) mutable { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); @@ -1249,14 +1941,14 @@ void TimelineActor::init() { const Uuid &before_uuid) -> bool { try { add_media(actor, uuid, before_uuid); - send( - event_group_, - utility::event_atom_v, + mail( + event_atom_v, playlist::add_media_atom_v, - UuidActor(uuid, actor)); - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); + UuidActorVector({UuidActor(uuid, actor)})) + .send(base_.event_group()); + base_.send_changed(); + mail(event_atom_v, change_atom_v).send(base_.event_group()); + mail(event_atom_v, change_atom_v).send(change_event_group_); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -1271,7 +1963,7 @@ void TimelineActor::init() { caf::scoped_actor sys(system()); try { // get uuid.. - Uuid uuid = request_receive(*sys, actor, utility::uuid_atom_v); + Uuid uuid = request_receive(*sys, actor, uuid_atom_v); // check playlist owns it.. request_receive( *sys, @@ -1293,97 +1985,237 @@ void TimelineActor::init() { return rp; }, - [=](playlist::get_next_media_atom, - const utility::Uuid &after_this_uuid, - int skip_by) -> result { - const utility::UuidList media = base_.media(); - - if (skip_by > 0) { - auto i = std::find(media.begin(), media.end(), after_this_uuid); - if (i == media.end()) { - // not found! - return make_error( - xstudio_error::error, - fmt::format( - "playlist::get_next_media_atom called with uuid that is not in " - "timeline {}", - to_string(after_this_uuid))); + [=](playlist::add_media_atom atom, + UuidActor ua, // the MediaActor + const UuidList &final_ordered_uuid_list, + Uuid before) -> result { + auto rp = make_response_promise(); + // This handler lets us build a playlist of a given order, even when + // we add the media in an out of order way. We generate Uuids for the + // MediaActors that will be used to build the playlist *before* the + // actual MediaActors are built and added - we pass in the ordered + // list of Uuids when adding each MediaActor so we can insert it + // into the correct point as the playlist is being built. + // + // This is used in ShotgunDataSourceActor, for example + + const UuidList media = base_.media(); + + // get an iterator to the uuid of the next MediaActor item that has + // been (or will be) added to this playlist + auto next = std::find( + final_ordered_uuid_list.begin(), final_ordered_uuid_list.end(), ua.uuid()); + if (next != final_ordered_uuid_list.end()) + next++; + + while (next != final_ordered_uuid_list.end()) { + + // has 'next' already been added to this playlist? + auto q = std::find(media.begin(), media.end(), *next); + if (q != media.end()) { + // yes - we know where to insert the incoming MediaActor + before = *q; + break; } - while (skip_by--) { - i++; - if (i == media.end()) { - i--; - break; + // keep looking + next++; + } + + // Note we can't use mail(add_media_atom_v, ua, before) + // to enact the adding, because it might happen *after* we get + // another of these add_media messages which would then mess up the + // ordering algorithm + add_media(rp, ua, before); + return rp; + }, + + [=](playlist::filter_media_atom, const std::string &filter_string) -> result { + // for each media item in the playlist, check if any fields in the + // 'media display info' for the media matches filter_string. If + // it does, include it in the result. We have to do some awkward + // shenanegans thanks to async requests ... the usual stuff!! + + if (filter_string.empty()) + return base_.media(); + + auto rp = make_response_promise(); + // share ptr to store result for each media piece (in order) + auto vv = std::make_shared>(base_.media().size()); + // keep count of responses with this + auto ct = std::make_shared(base_.media().size()); + const auto filter_string_lower = to_lower(filter_string); + + auto check_deliver = [=]() mutable { + (*ct)--; + if (*ct == 0) { + UuidList r; + for (const auto &v : *vv) { + if (!v.is_null()) { + r.push_back(v); + } } + rp.deliver(r); } - if (media_actors_.count(*i)) - return UuidActor(*i, media_actors_[*i]); + }; + + int idx = 0; + for (const auto &uuid : base_.media()) { + if (!media_actors_.count(uuid)) { + check_deliver(); + continue; + } + UuidActor media(uuid, media_actors_[uuid]); + mail(media::media_display_info_atom_v) + .request(media.actor(), infinite) + .then( + [=](const JsonStore &media_display_info) mutable { + if (media_display_info.is_array()) { + for (int i = 0; i < media_display_info.size(); ++i) { + std::string data = media_display_info[i].dump(); + if (to_lower(data).find(filter_string_lower) != + std::string::npos) { + (*vv)[idx] = media.uuid(); + break; + } + } + } + check_deliver(); + }, + [=](caf::error &err) mutable { check_deliver(); }); + idx++; + } + return rp; + }, + + [=](playlist::get_next_media_atom, + const Uuid &after_this_uuid, + int skip_by, + const std::string &filter_string) -> result { + auto rp = make_response_promise(); + + mail(playlist::filter_media_atom_v, filter_string) + .request(caf::actor_cast(this), infinite) + .then( + [=](const UuidList &media) mutable { + if (skip_by > 0) { + auto i = std::find(media.begin(), media.end(), after_this_uuid); + if (i == media.end()) { + // not found! + rp.deliver(make_error( + xstudio_error::error, + fmt::format( + "playlist::get_next_media_atom called with uuid that " + "is not in " + "timeline {}", + to_string(after_this_uuid)))); + } + while (skip_by--) { + i++; + if (i == media.end()) { + i--; + break; + } + } + if (media_actors_.count(*i)) + rp.deliver(UuidActor(*i, media_actors_[*i])); + + } else { + auto i = std::find(media.rbegin(), media.rend(), after_this_uuid); + if (i == media.rend()) { + // not found! + rp.deliver(make_error( + xstudio_error::error, + fmt::format( + "playlist::get_next_media_atom called with uuid that " + "is not in " + "playlist", + to_string(after_this_uuid)))); + } + while (skip_by++) { + i++; + if (i == media.rend()) { + i--; + break; + } + } + + if (media_actors_.count(*i)) + rp.deliver(UuidActor(*i, media_actors_[*i])); + } + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + + + return rp; + }, + + [=](global_store::save_atom, + const caf::uri &path, + const std::string &type, + const std::string &schema) -> result { + auto rp = make_response_promise(); - } else { - auto i = std::find(media.rbegin(), media.rend(), after_this_uuid); - if (i == media.rend()) { - // not found! - return make_error( - xstudio_error::error, - fmt::format( - "playlist::get_next_media_atom called with uuid that is not in " - "playlist", - to_string(after_this_uuid))); - } - while (skip_by++) { - i++; - if (i == media.rend()) { - i--; - break; - } - } + mail(session::export_atom_v) + .request(caf::actor_cast(this), infinite) + .then( + [=](const std::string &result) { + export_otio(rp, result, path, type, schema); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); - if (media_actors_.count(*i)) - return UuidActor(*i, media_actors_[*i]); - } + return rp; + }, - return make_error( - xstudio_error::error, - fmt::format( - "playlist::get_next_media_atom called with uuid for which no media actor " - "exists {}", - to_string(after_this_uuid))); + [=](session::export_atom) -> result { + // convert timeline to otio string + auto rp = make_response_promise(); + export_otio_as_string(rp); + return rp; }, [=](playlist::create_playhead_atom) -> UuidActor { if (playhead_) return playhead_; - auto uuid = utility::Uuid::generate(); - - /*auto actor = spawn( - std::string("Timeline Playhead"), selection_actor_, uuid);*/ + auto uuid = Uuid::generate(); // N.B. for now we're not using the 'selection_actor_' as this - // feeds the playhead a list of selected media which the playhead + // drives the playhead a list of selected media which the playhead // will play. It will ignore this timeline completely if we do that. // We want to play this timeline, not the media in the timeline // that is selected. auto playhead_actor = spawn( - std::string("Timeline Playhead"), caf::actor(), uuid); + std::string("Timeline Playhead"), + playhead::GLOBAL_AUDIO, + selection_actor_, + uuid, + caf::actor_cast(this)); link_to(playhead_actor); - anon_send(playhead_actor, playhead::playhead_rate_atom_v, base_.rate()); + anon_mail(playhead::playhead_rate_atom_v, base_.rate()).send(playhead_actor); - // now make this timeline the (only) source for the playhead - anon_send( - playhead_actor, + // now make this timeline and its vide tracks the source for the playhead + video_tracks_ = base_.item().find_all_uuid_actors(IT_VIDEO_TRACK, true); + anon_mail( playhead::source_atom_v, - std::vector({caf::actor_cast(this)})); + UuidActor(base_.uuid(), caf::actor_cast(this)), + video_tracks_) + .send(playhead_actor); playhead_ = UuidActor(uuid, playhead_actor); + if (!playhead_serialisation_.is_null()) { + anon_mail(module::deserialise_atom_v, playhead_serialisation_) + .send(playhead_.actor()); + } return playhead_; }, [=](playlist::get_playhead_atom) { - delegate(caf::actor_cast(this), playlist::create_playhead_atom_v); + return mail(playlist::create_playhead_atom_v) + .delegate(caf::actor_cast(this)); }, + [=](playlist::get_change_event_group_atom) -> caf::actor { return change_event_group_; }, @@ -1398,7 +2230,7 @@ void TimelineActor::init() { auto rp = make_response_promise>(); // collect media data.. - fan_out_request(actors, infinite, utility::detail_atom_v) + fan_out_request(actors, infinite, detail_atom_v) .then( [=](const std::vector details) mutable { std::vector reordered_details; @@ -1440,18 +2272,15 @@ void TimelineActor::init() { [=](playlist::selection_actor_atom) -> caf::actor { return selection_actor_; }, [=](playlist::move_media_atom atom, const Uuid &uuid, const Uuid &uuid_before) { - delegate( - actor_cast(this), atom, utility::UuidVector({uuid}), uuid_before); + return mail(atom, UuidVector({uuid}), uuid_before) + .delegate(actor_cast(this)); }, [=](playlist::move_media_atom atom, const UuidList &media_uuids, const Uuid &uuid_before) { - delegate( - actor_cast(this), - atom, - utility::UuidVector(media_uuids.begin(), media_uuids.end()), - uuid_before); + return mail(atom, UuidVector(media_uuids.begin(), media_uuids.end()), uuid_before) + .delegate(actor_cast(this)); }, [=](playlist::move_media_atom, @@ -1462,188 +2291,87 @@ void TimelineActor::init() { result |= base_.move_media(uuid, uuid_before); } if (result) { - base_.send_changed(event_group_, this); - send( - event_group_, - utility::event_atom_v, - playlist::move_media_atom_v, - media_uuids, - uuid_before); - send(event_group_, utility::event_atom_v, change_atom_v); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); + base_.send_changed(); + mail(event_atom_v, playlist::move_media_atom_v, media_uuids, uuid_before) + .send(base_.event_group()); + mail(event_atom_v, change_atom_v).send(base_.event_group()); + mail(event_atom_v, change_atom_v).send(change_event_group_); } return result; }, - [=](playlist::remove_media_atom atom, const utility::UuidList &uuids) { - delegate( - actor_cast(this), - atom, - utility::UuidVector(uuids.begin(), uuids.end())); + [=](playlist::remove_media_atom atom, const UuidList &uuids) { + return mail(atom, UuidVector(uuids.begin(), uuids.end())) + .delegate(actor_cast(this)); }, [=](playlist::remove_media_atom atom, const Uuid &uuid) { - delegate(actor_cast(this), atom, utility::UuidVector({uuid})); + return mail(atom, UuidVector({uuid})).delegate(actor_cast(this)); }, - [=](playlist::remove_media_atom, const utility::UuidVector &uuids) -> bool { + [=](media::current_media_source_atom) + -> caf::result>>> { + auto rp = make_response_promise< + std::vector>>>(); + if (not media_actors_.empty()) { + fan_out_request( + map_value_to_vec(media_actors_), + infinite, + media::current_media_source_atom_v) + .then( + [=](const std::vector< + std::pair>> + details) mutable { rp.deliver(details); }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + } else { + rp.deliver( + std::vector>>()); + } + return rp; + }, + + [=](playlist::remove_media_atom, const UuidVector &uuids) -> bool { // this needs to propergate to children somehow.. - utility::UuidVector removed; + UuidVector removed; for (const auto &uuid : uuids) { if (media_actors_.count(uuid) and remove_media(media_actors_[uuid], uuid)) { - spdlog::warn("remove {}", to_string(uuid)); removed.push_back(uuid); + + for (const auto &i : find_media_clips(base_.children(), uuid)) { + // find parent actor of clip and remove.. + auto pa = find_parent_actor(base_.item(), i); + if (pa) + anon_mail(erase_item_atom_v, i, true).send(pa); + } } } if (not removed.empty()) { - send(event_group_, utility::event_atom_v, change_atom_v); - send( - event_group_, - utility::event_atom_v, - playlist::remove_media_atom_v, - removed); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); - base_.send_changed(event_group_, this); + mail(event_atom_v, change_atom_v).send(base_.event_group()); + mail(event_atom_v, playlist::remove_media_atom_v, removed) + .send(base_.event_group()); + mail(event_atom_v, change_atom_v).send(change_event_group_); + base_.send_changed(); } return not removed.empty(); }, + [=](clear_atom) -> bool { + base_.clear(); + for (const auto &i : item_actors_) { + // this->leave(i.second); + unlink_from(i.second); + send_exit(i.second, caf::exit_reason::user_shutdown); + } + item_actors_.clear(); + return true; + }, + [=](rate_atom) -> FrameRate { return base_.item().rate(); }, - // // code for playhead// get edit_list for all tracks/stacks..// this is temporary, - // it'll - // // need heavy changes..// also this only returns edit_lists for images, audio may be - // // different.. - // [=](media::get_edit_list_atom, const Uuid &uuid) -> result { - // if (update_edit_list_) { - // std::vector actors; - // for (const auto &i : base_.tracks()) - // actors.push_back(actors_[i]); - - // if (not actors.empty()) { - // auto rp = make_response_promise(); - - // fan_out_request( - // actors, infinite, media::get_edit_list_atom_v, Uuid()) - // .await( - // [=](std::vector sections) mutable { - // edit_list_.clear(); - // for (const auto &i : base_.tracks()) { - // for (const auto &ii : sections) { - // for (const auto §ion : ii.section_list()) { - // const auto &[ud, rt, tc] = section; - // if (ud == i) { - // if (uuid.is_null()) - // edit_list_.push_back(section); - // else - // edit_list_.push_back({uuid, rt, tc}); - // } - // } - // } - // } - // update_edit_list_ = false; - // rp.deliver(edit_list_); - // }, - // [=](error &err) mutable { rp.deliver(std::move(err)); }); - - // return rp; - // } else { - // edit_list_.clear(); - // update_edit_list_ = false; - // } - // } - - // return result(edit_list_); - // }, - - // [=](media::get_media_pointer_atom, - // const int logical_frame) -> result { - // if (base_.empty()) - // return result(make_error(xstudio_error::error, "No - // media")); - - // auto rp = make_response_promise(); - // if (update_edit_list_) { - // request(actor_cast(this), infinite, media::get_edit_list_atom_v) - // .then( - // [=](const utility::EditList &) mutable { - // deliver_media_pointer(logical_frame, rp); - // }, - // [=](error &err) mutable { rp.deliver(std::move(err)); }); - // } else { - // deliver_media_pointer(logical_frame, rp); - // } - - // return rp; - // }, - - // [=](start_time_atom) -> utility::FrameRateDuration { return base_.start_time(); }, - - // [=](utility::clear_atom) -> bool { - // base_.clear(); - // for (const auto &i : actors_) { - // // this->leave(i.second); - // unlink_from(i.second); - // send_exit(i.second, caf::exit_reason::user_shutdown); - // } - // actors_.clear(); - // return true; - // }, - - // [=](utility::event_atom, utility::change_atom) { - // update_edit_list_ = true; - // send(event_group_, utility::event_atom_v, utility::change_atom_v); - // }, - - // [=](utility::event_atom, utility::name_atom, const std::string & /*name*/) {}, - - // [=](utility::rate_atom) -> FrameRate { return base_.rate(); }, - - // [=](utility::rate_atom, const FrameRate &rate) { base_.set_rate(rate); }, - - // this operation isn't relevant ? - - - // [=](playlist::create_playhead_atom) -> UuidActor { - // if (playhead_) - // return playhead_; - // auto uuid = utility::Uuid::generate(); - // auto actor = spawn( - // std::string("Timeline Playhead"), caf::actor_cast(this), uuid); - // link_to(actor); - // playhead_ = UuidActor(uuid, actor); - - // #ifdef _MSC_VER - // auto tp = sysclock::now(); - // auto micros = - // std::chrono::duration_cast(tp.time_since_epoch()).count(); - // //using nano_sys = std::chrono::time_point; anon_send(actor,playhead::playhead_rate_atom_v, - // caf::make_timestamp(), base_.rate()); - // #else - // anon_send(actor, playhead::playhead_rate_atom_v, base_.rate()); - // #endif - - - // anon_send(actor, playhead::playhead_rate_atom_v, base_.rate()); - - // // this pushes this actor to the playhead as the 'source' that the - // // playhead will play - // send( - // actor, - // utility::event_atom_v, - // playhead::source_atom_v, - // std::vector{caf::actor_cast(this)}); - - // return playhead_; - // }, - - // emulate subset. - - // [=](playlist::selection_actor_atom) -> caf::actor { - // return caf::actor_cast(this); - // }, + [=](rate_atom atom, const media::MediaType media_type) { + return mail(atom).delegate(caf::actor_cast(this)); + }, [=](duplicate_atom) -> result { auto rp = make_response_promise(); @@ -1652,25 +2380,59 @@ void TimelineActor::init() { // clone ourself.. caf::scoped_actor sys(system()); + auto conform_track_uuid = + base_.item().prop().value("conform_track_uuid", Uuid()); + auto video_tracks = base_.item().find_all_uuid_actors(IT_VIDEO_TRACK); + auto conform_track_index = video_tracks.size() - 1; + + if (not conform_track_uuid.is_null()) { + for (size_t i = 0; i < video_tracks.size(); i++) { + if (video_tracks[i].uuid() == conform_track_uuid) { + conform_track_index = i; + break; + } + } + } + JsonStore jsn; auto dup = base_.duplicate(); dup.item().clear(); + // reset conform track uuid + auto tprop = dup.item().prop(); + tprop["conform_track_uuid"] = Uuid(); + dup.item().set_prop(tprop); + jsn["base"] = dup.serialise(); jsn["actors"] = {}; auto actor = spawn(jsn, caf::actor()); + + auto meta = request_receive( + *sys, jsn_handler_.json_actor(), json_store::get_json_atom_v); + request_receive(*sys, actor, json_store::set_json_atom_v, meta); + auto hactor = request_receive(*sys, actor, history::history_atom_v); - anon_send(hactor.actor(), plugin_manager::enable_atom_v, false); + anon_mail(plugin_manager::enable_atom_v, false).send(hactor.actor()); for (const auto &i : base_.children()) { auto ua = request_receive( - *sys, actors_[i.uuid()], utility::duplicate_atom_v); - anon_send(actor, insert_item_atom_v, -1, UuidActorVector({ua})); + *sys, item_actors_[i.uuid()], duplicate_atom_v); + request_receive( + *sys, actor, insert_item_atom_v, -1, UuidActorVector({ua})); + } + + // actor should be populated ? + // find uuid of video track.. + auto new_item = request_receive(*sys, actor, item_atom_v); + auto track = (*(new_item.item_at_index(0)))->item_at_index(conform_track_index); + if (track) { + tprop["conform_track_uuid"] = (*track)->uuid(); + anon_mail(item_prop_atom_v, tprop).send(actor); } // enable history - anon_send(hactor.actor(), plugin_manager::enable_atom_v, true); + anon_mail(plugin_manager::enable_atom_v, true).send(hactor.actor()); rp.deliver(UuidActor(dup.uuid(), actor)); @@ -1682,16 +2444,16 @@ void TimelineActor::init() { return rp; }, - [=](timeline::focus_atom) -> UuidVector { + [=](focus_atom) -> UuidVector { auto tmp = base_.focus_list(); return UuidVector(tmp.begin(), tmp.end()); }, - [=](timeline::focus_atom, const UuidVector &list) { + [=](focus_atom, const UuidVector &list) { base_.set_focus_list(list); // both ? - send(event_group_, utility::event_atom_v, change_atom_v); - send(change_event_group_, utility::event_atom_v, utility::change_atom_v); + mail(event_atom_v, change_atom_v).send(base_.event_group()); + mail(event_atom_v, change_atom_v).send(change_event_group_); }, [=](playhead::source_atom) -> caf::actor { @@ -1702,15 +2464,25 @@ void TimelineActor::init() { [=](playhead::source_atom, caf::actor playlist, const UuidUuidMap &swap) -> result { + // spdlog::warn("playhead::source_atom old {} new {}", to_string(playlist_), + // to_string(playlist)); for(const auto &i: swap) + // spdlog::warn("{} {}", to_string(i.first), to_string(i.second)); + auto rp = make_response_promise(); - for (const auto &i : media_actors_) - demonitor(i.second); + for (const auto &i : media_actors_) { + if (auto mit = monitor_.find(caf::actor_cast(i.second)); + mit != std::end(monitor_)) { + mit->second.dispose(); + monitor_.erase(mit); + } + } media_actors_.clear(); playlist_ = caf::actor_cast(playlist); - request(playlist, infinite, playlist::get_media_atom_v) + mail(playlist::get_media_atom_v) + .request(playlist, infinite) .then( [=](const std::vector &media) mutable { // build map @@ -1739,10 +2511,33 @@ void TimelineActor::init() { base_.swap_media(i, ii); } media_actors_[ii] = amap[ii]; - monitor(amap[ii]); + monitor_media(amap[ii]); } - base_.send_changed(event_group_, this); - rp.deliver(true); + + // for(const auto &i: actors_) + // spdlog::warn("{} {}", to_string(i.first),to_string(i.second)); + + // timeline has only one child, the stack (we hope) + // relink all clips.. + fan_out_request( + map_value_to_vec(item_actors_), + infinite, + playhead::source_atom_v, + swap, + media_actors_) + .await( + [=](std::vector items) mutable { + base_.send_changed(); + rp.deliver(true); + }, + [=](error &err) mutable { + spdlog::warn( + "{} {} {}", + __PRETTY_FUNCTION__, + to_string(err), + base_.item().name()); + rp.deliver(false); + }); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); @@ -1753,253 +2548,65 @@ void TimelineActor::init() { [=](media::get_media_pointers_atom atom, const media::MediaType media_type, - const utility::TimeSourceMode tsm, - const utility::FrameRate &override_rate) -> caf::result { + const TimeSourceMode tsm, + const FrameRate &override_rate) -> caf::result { // This is required by SubPlayhead actor to make the timeline // playable. + return base_.item().get_all_frame_IDs( + media_type, tsm, override_rate, base_.focus_list()); + }, - auto rp = make_response_promise(); - - if (!base_.item().available_range()) { - rp.deliver(media::FrameTimeMap()); - return rp; - } + [=](serialise_atom) -> result { + std::vector actors = map_value_to_vec(item_actors_); + actors.push_back(selection_actor_); + auto rp = make_response_promise(); - // Should this be trimmed_range, active_range or available_range or - // something else? - const int start_frame = - (*base_.item().available_range()).frame_start().frames(override_rate); - const int end_frame = - start_frame + - (*base_.item().available_range()).frame_duration().frames(override_rate); - - // request the sequential AVFrameIDs for this timeline - request( - caf::actor_cast(this), - infinite, - atom, - media_type, - media::LogicalFrameRanges({{ - start_frame, - end_frame, - }}), - override_rate) + // get json. + mail(json_store::get_json_atom_v, "") + .request(jsn_handler_.json_actor(), infinite) .then( - [=](const media::AVFrameIDs &frame_ids) mutable { - auto time_point = timebase::flicks(0); - media::FrameTimeMap reslt; - for (const auto &frame_id : frame_ids) { - auto frame_id_cpy = std::make_shared(*frame_id); - - // use the base rate to set the frame rate - this - // could be varied within this function if it fits - // with the timeline model. For example, supporting - // media of different frame rates in one timeline? - frame_id_cpy->rate_ = base_.rate(); - reslt[time_point] = frame_id_cpy; - - // This is where the frame rate for the current - // frame is actually applied. We can increment - // by anything which allows playheads to play - // media of different rates. - time_point += frame_id_cpy->rate_.to_flicks(); - } - rp.deliver(reslt); + [=](const JsonStore &meta) mutable { + fan_out_request(actors, infinite, serialise_atom_v) + .then( + [=](std::vector json) mutable { + JsonStore jsn; + jsn["base"] = base_.serialise(); + jsn["store"] = meta; + jsn["actors"] = {}; + for (const auto &j : json) + jsn["actors"][static_cast( + j["base"]["container"]["uuid"])] = j; + if (playhead_) { + mail(serialise_atom_v) + .request(playhead_.actor(), infinite) + .then( + [=](const JsonStore &playhead_state) mutable { + playhead_serialisation_ = playhead_state; + jsn["playhead"] = playhead_state; + rp.deliver(jsn); + }, + [=](caf::error &err) mutable { + spdlog::warn( + "{} {}", + __PRETTY_FUNCTION__, + to_string(err)); + rp.deliver(jsn); + }); + + } else { + if (!playhead_serialisation_.is_null()) { + jsn["playhead"] = playhead_serialisation_; + } + rp.deliver(jsn); + } + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); return rp; }, - [=](media::get_media_pointers_atom atom, - const media::MediaType media_type, - const media::LogicalFrameRanges &ranges, - const FrameRate &override_rate) -> caf::result { - // spdlog::stopwatch sw; - auto num_frames = 0; - for (const auto &i : ranges) - num_frames += (i.second - i.first) + 1; - - - auto result = std::make_shared(num_frames); - auto count = std::make_shared(); - *count = 0; - // spdlog::warn("{} {} {} {}", media_type, num_frames, start_frame, - // override_rate.to_fps()); - - caf::scoped_actor sys(system()); - - auto item_tp = std::vector>>(); - item_tp.reserve(num_frames); - - for (const auto &r : ranges) { - for (auto i = r.first; i <= r.second; i++) { - auto ii = base_.item().resolve_time( - FrameRate(i * override_rate.to_flicks()), - media_type, - base_.focus_list()); - if (ii) { - item_tp.emplace_back(*ii); - (*count)++; - } else { - item_tp.emplace_back(); - } - } - } - - // spdlog::error("resolve_time elapsed {:.3}", sw); - - // only blank fraems - auto bf = media::make_blank_frame(media_type); - if (not *count) { - for (auto i = 0; i < num_frames; i++) - (*result)[i] = bf; - // spdlog::error("blank output elapsed {:.3}", sw); - return *result; - } - - auto rp = make_response_promise(); - - auto start = 0; - auto end = 0; - auto tps = std::vector(); - auto act = caf::actor(); - - for (auto i = 0; i < num_frames; i++) { - auto item = item_tp[i]; - - // dispatch on actor change - if (not tps.empty() and (not item or std::get<0>(*item).actor() != act)) { - request( - act, - infinite, - media::get_media_pointer_atom_v, - media_type, - tps, - override_rate) - .then( - [=, s = start, e = end](const media::AVFrameIDs &mps) mutable { - for (auto ii = s; ii <= e; ii++) { - (*result)[ii] = mps[ii - s]; - (*count)--; - // spdlog::error("s {} e {} ii {} c {}", s, e, ii, *count); - if (not *count) { - rp.deliver(*result); - // spdlog::error("get_media_pointers_atom elapsed - // {:.3}", sw); - } - } - }, - - [=, s = start, e = end](error &err) mutable { - for (auto ii = s; ii <= e; ii++) { - (*result)[ii] = bf; - (*count)--; - // spdlog::error("s {} e {} ii {} c {}", s, e, ii, *count); - if (not *count) { - rp.deliver(*result); - // spdlog::error("get_media_pointers_atom elapsed - // {:.3}", sw); - } - } - }); - - start = end = i; - tps.clear(); - act = (item ? std::get<0>(*item).actor() : caf::actor()); - } - - if (not item) { - (*result)[i] = bf; - } else { - if (tps.empty()) { - start = i; - act = std::get<0>(*item).actor(); - } - end = i; - tps.push_back(std::get<1>(*item)); - } - } - - // catch all - if (not tps.empty()) { - request( - act, - infinite, - media::get_media_pointer_atom_v, - media_type, - tps, - override_rate) - .then( - [=, s = start, e = end](const media::AVFrameIDs &mps) mutable { - for (auto ii = s; ii <= e; ii++) { - (*result)[ii] = mps[ii - s]; - (*count)--; - // spdlog::error("s {} e {} ii {} c {}", s, e, ii, *count); - if (not *count) { - rp.deliver(*result); - // spdlog::error("get_media_pointers_atom elapsed {:.3}", - // sw); - } - } - }, - - [=, s = start, e = end](error &err) mutable { - for (auto ii = s; ii <= e; ii++) { - (*result)[ii] = bf; - (*count)--; - // spdlog::error("s {} e {} ii {} c {}", s, e, ii, *count); - if (not *count) { - rp.deliver(*result); - // spdlog::error("get_media_pointers_atom elapsed {:.3}", - // sw); - } - } - }); - } - - // spdlog::error("get_media_pointers dispatched elapsed {:.3}", sw); - return rp; - }, - - // [=](media::get_edit_list_atom, const Uuid &uuid) -> result { - // auto el = utility::EditList(utility::ClipList({utility::EditListSection( - // base_.uuid(), - // base_.item().trimmed_frame_duration(), - // utility::Timecode( - // base_.item().trimmed_frame_duration().frames(), - // base_.rate().to_fps()))})); - // return el; - // }, - - [=](utility::serialise_atom) -> result { - if (not actors_.empty()) { - auto rp = make_response_promise(); - - fan_out_request( - map_value_to_vec(actors_), infinite, serialise_atom_v) - .then( - [=](std::vector json) mutable { - JsonStore jsn; - jsn["base"] = base_.serialise(); - jsn["actors"] = {}; - for (const auto &j : json) - jsn["actors"] - [static_cast(j["base"]["container"]["uuid"])] = - j; - rp.deliver(jsn); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - - return rp; - } - JsonStore jsn; - jsn["base"] = base_.serialise(); - jsn["actors"] = {}; - - return result(jsn); - }, - // *********************************************************** // @@ -2020,20 +2627,8 @@ void TimelineActor::init() { [=](playhead::get_selection_atom) -> UuidList { return UuidList{base_.uuid()}; }, [=](playhead::get_selection_atom, caf::actor requester) { -#ifdef _WIN32 - auto tp = sysclock::now(); - auto micros = - std::chrono::duration_cast(tp.time_since_epoch()) - .count(); - anon_send( - requester, playhead::selection_changed_atom_v, micros, UuidList{base_.uuid()}); -#else - anon_send( - requester, - utility::event_atom_v, - playhead::selection_changed_atom_v, - UuidList{base_.uuid()}); -#endif + anon_mail(event_atom_v, playhead::selection_changed_atom_v, UuidList{base_.uuid()}) + .send(requester); }, [=](playhead::select_next_media_atom, const int skip_by) {}, @@ -2044,63 +2639,210 @@ void TimelineActor::init() { [=](playlist::select_media_atom) {}, - [=](playlist::select_media_atom, utility::Uuid media_uuid) {}, + [=](playlist::select_media_atom, Uuid media_uuid) {}, - [=](playhead::get_selected_sources_atom) -> utility::UuidActorVector { - return utility::UuidActorVector(); + [=](playhead::get_selected_sources_atom) -> UuidActorVector { + return UuidActorVector(); }, [=](session::get_playlist_atom) -> caf::actor { return caf::actor_cast(playlist_); }, - [=](session::import_atom, const std::string &data) -> result { + [=](session::import_atom, const caf::uri &path, const std::string &data, bool clear) + -> result { auto rp = make_response_promise(); - // purge timeline.. ? -#ifdef BUILD_OTIO + if (clear && base_.item().size()) { + // send a clear atom to the stack actor. + auto stack_item = *(base_.item().begin()); + mail(clear_atom_v) + .request(stack_item.actor(), infinite) + .then( + [=](bool) mutable { + rp.delegate( + caf::actor_cast(this), + session::import_atom_v, + path, + data); + }, + [=](caf::error &err) mutable { rp.deliver(err); }); + } else { + rp.delegate( + caf::actor_cast(this), session::import_atom_v, path, data); + } + return rp; + }, + + [=](session::import_atom, + const caf::uri &path, + const std::string &data) -> result { + auto rp = make_response_promise(); + // purge timeline.. ? spawn( timeline_importer, rp, caf::actor_cast(playlist_), UuidActor(base_.uuid(), actor_cast(this)), + path, data); -#endif return rp; + }}; +} + + +void TimelineActor::init() { + print_on_create(this, base_.name()); + print_on_exit(this, base_.name()); + + change_event_group_ = spawn(this); + link_to(change_event_group_); + + history_uuid_ = Uuid::generate(); + history_ = spawn>(history_uuid_); + link_to(history_); + + if (!selection_actor_) { + selection_actor_ = spawn( + "TimelinePlayheadSelectionActor", caf::actor_cast(this)); + link_to(selection_actor_); + } + + // set_down_handler([=](down_msg &msg) { + // // find in playhead list.. + // for (auto it = std::begin(media_actors_); it != std::end(media_actors_); ++it) { + // // if a child dies we won't have enough information to recreate it. + // // we still need to report it up the chain though. + + // if (msg.source == it->second) { + // demonitor(it->second); + + // // if media.. + // if (base_.remove_media(it->first)) { + // mail(event_atom_v, change_atom_v).send(base_.event_group()); + // mail( + // event_atom_v, + // playlist::remove_media_atom_v, + // UuidVector({it->first})) + // .send(base_.event_group()); + // base_.send_changed(); + // } + + // media_actors_.erase(it); + // } + // } + // auto it = find_actor_addr(base_.item().children(), msg.source); + + // if (it != base_.item().end()) { + // auto jsn = base_.item().erase(it); + // auto more = base_.item().refresh(); + // if (not more.is_null()) + // jsn.insert(jsn.begin(), more.begin(), more.end()); + + // mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + // } + // }); + + // update_edit_list_ = true; +} + +void TimelineActor::monitor_media(const caf::actor &actor) { + auto act_addr = caf::actor_cast(actor); + + if (auto it = monitor_.find(act_addr); it == std::end(monitor_)) { + monitor_[act_addr] = monitor(actor, [this, addr = actor.address()](const error &) { + // remove monitored + if (auto it = monitor_.find(caf::actor_cast(addr)); + it != std::end(monitor_)) + monitor_.erase(it); + + // find it + for (auto it = std::begin(media_actors_); it != std::end(media_actors_); ++it) { + // if a child dies we won't have enough information to recreate it. + // we still need to report it up the chain though. + + if (addr == it->second) { + // if media.. + if (base_.remove_media(it->first)) { + mail(event_atom_v, change_atom_v).send(base_.event_group()); + mail( + event_atom_v, + playlist::remove_media_atom_v, + UuidVector({it->first})) + .send(base_.event_group()); + base_.send_changed(); + } + + media_actors_.erase(it); + break; + } + } }); + } } -void TimelineActor::add_item(const utility::UuidActor &ua) { + +void TimelineActor::add_item(const UuidActor &ua) { // join_event_group(this, ua.second); scoped_actor sys{system()}; try { - auto grp = - request_receive(*sys, ua.actor(), utility::get_event_group_atom_v); + auto grp = request_receive(*sys, ua.actor(), get_event_group_atom_v); auto joined = request_receive(*sys, grp, broadcast::join_broadcast_atom_v, this); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } - monitor(ua.actor()); - actors_[ua.uuid()] = ua.actor(); + auto act_addr = caf::actor_cast(ua.actor()); + + if (auto it = monitor_.find(act_addr); it == std::end(monitor_)) { + monitor_[act_addr] = + monitor(ua.actor(), [this, addr = ua.actor().address()](const error &) { + if (auto it = monitor_.find(caf::actor_cast(addr)); + it != std::end(monitor_)) + monitor_.erase(it); + + for (auto it = std::begin(item_actors_); it != std::end(item_actors_); ++it) { + if (addr == it->second) { + item_actors_.erase(it); + + // remove from base. + auto it = find_actor_addr(base_.item().children(), addr); + + if (it != base_.item().end()) { + auto jsn = base_.item().erase(it); + auto more = base_.item().refresh(); + if (not more.is_null()) + jsn.insert(jsn.begin(), more.begin(), more.end()); + + mail(event_atom_v, item_atom_v, jsn, false) + .send(base_.event_group()); + } + + break; + } + } + }); + } + + item_actors_[ua.uuid()] = ua.actor(); } -void TimelineActor::add_media( - caf::actor actor, const utility::Uuid &uuid, const utility::Uuid &before_uuid) { +void TimelineActor::add_media(caf::actor actor, const Uuid &uuid, const Uuid &before_uuid) { + if (actor) { - base_.insert_media(uuid, before_uuid); - media_actors_[uuid] = actor; - monitor(actor); + if (not base_.contains_media(uuid)) { + base_.insert_media(uuid, before_uuid); + media_actors_[uuid] = actor; + monitor_media(actor); + } } else { spdlog::warn("{} Invalid media actor", __PRETTY_FUNCTION__); } } void TimelineActor::add_media( - const utility::UuidActor &ua, - const utility::Uuid &before_uuid, - caf::typed_response_promise rp) { + caf::typed_response_promise rp, const UuidActor &ua, const Uuid &before_uuid) { try { @@ -2127,11 +2869,12 @@ void TimelineActor::add_media( } add_media(actor, ua.uuid(), before_uuid); - send(event_group_, utility::event_atom_v, playlist::add_media_atom_v, ua); - base_.send_changed(event_group_, this); - send(event_group_, utility::event_atom_v, change_atom_v); + mail(event_atom_v, playlist::add_media_atom_v, UuidActorVector({ua})) + .send(base_.event_group()); + base_.send_changed(); + mail(event_atom_v, change_atom_v).send(base_.event_group()); - // send(change_event_group_, utility::event_atom_v, utility::change_atom_v); + // mail(event_atom_v, change_atom_v).send(change_event_group_); rp.deliver(ua); @@ -2140,11 +2883,16 @@ void TimelineActor::add_media( } } -bool TimelineActor::remove_media(caf::actor actor, const utility::Uuid &uuid) { +bool TimelineActor::remove_media(caf::actor actor, const Uuid &uuid) { bool result = false; if (base_.remove_media(uuid)) { - demonitor(actor); + if (auto mit = monitor_.find(caf::actor_cast(actor)); + mit != std::end(monitor_)) { + mit->second.dispose(); + monitor_.erase(mit); + } + media_actors_.erase(uuid); result = true; } @@ -2153,18 +2901,18 @@ bool TimelineActor::remove_media(caf::actor actor, const utility::Uuid &uuid) { } void TimelineActor::on_exit() { - for (const auto &i : actors_) + for (const auto &i : item_actors_) send_exit(i.second, caf::exit_reason::user_shutdown); } void TimelineActor::deliver_media_pointer( + caf::typed_response_promise rp, const int logical_frame, - const media::MediaType media_type, - caf::typed_response_promise rp) { + const media::MediaType media_type) { std::vector actors; for (const auto &i : base_.media()) - actors.push_back(actors_[i]); + actors.push_back(media_actors_[i]); fan_out_request(actors, infinite, media::media_reference_atom_v, Uuid()) .then( @@ -2199,12 +2947,8 @@ void TimelineActor::deliver_media_pointer( if (exceeded_duration) throw std::runtime_error("No frames left"); // send request media atom.. - request( - actors_[m.first], - infinite, - media::get_media_pointer_atom_v, - media_type, - logical_frame - frames) + mail(media::get_media_pointer_atom_v, media_type, logical_frame - frames) + .request(media_actors_[m.first], infinite) .then( [=](const media::AVFrameID &mp) mutable { rp.deliver(mp); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); @@ -2217,48 +2961,61 @@ void TimelineActor::deliver_media_pointer( } -void TimelineActor::sort_alphabetically() { +void TimelineActor::sort_by_media_display_info( + const int sort_column_index, const bool ascending) { - using SourceAndUuid = std::pair; - auto media_names_vs_uuids = - std::make_shared>>(); + using SourceAndUuid = std::pair; + auto sort_keys_vs_uuids = std::make_shared>>(); + int idx = 0; for (const auto &i : base_.media()) { // Pro tip: because i is a reference, it's the reference that is captured in our lambda // below and therefore it is 'unstable' so we make a copy here and use that in the // lambda as this is object-copied in the capture instead. UuidActor media_actor(i, media_actors_[i]); + idx++; - request(media_actor.actor(), infinite, media::media_reference_atom_v, utility::Uuid()) + mail(media::media_display_info_atom_v) + .request(media_actor.actor(), infinite) .await( - [=](const std::pair &m_ref) mutable { - std::string path = uri_to_posix_path(m_ref.second.uri()); - path = std::string(path, path.rfind("/") + 1); - path = to_lower(path); + [=](const JsonStore &media_display_info) mutable { + // media_display_info should be an array of arrays. Each + // array is the data shown in the media list columns. + // So info_set_idx corresponds to the media list (in the UI) + // from which the sort was requested. And the info_item_idx + // corresponds to the column of that media list. - (*media_names_vs_uuids).push_back(std::make_pair(path, media_actor.uuid())); + // default sort key keeps current sorting but should always + // put it after the last element that did have a sort key + std::string sort_key = fmt::format("ZZZZZZ{}", idx); - if (media_names_vs_uuids->size() == base_.media().size()) { + if (media_display_info.is_array() && + sort_column_index < media_display_info.size()) { + sort_key = media_display_info[sort_column_index].dump(); + } + + (*sort_keys_vs_uuids) + .push_back(std::make_pair(sort_key, media_actor.uuid())); + + if (sort_keys_vs_uuids->size() == base_.media().size()) { std::sort( - media_names_vs_uuids->begin(), - media_names_vs_uuids->end(), - [](const SourceAndUuid &a, const SourceAndUuid &b) -> bool { - return a.first < b.first; + sort_keys_vs_uuids->begin(), + sort_keys_vs_uuids->end(), + [ascending]( + const SourceAndUuid &a, const SourceAndUuid &b) -> bool { + return ascending ? a.first < b.first : a.first > b.first; }); - utility::UuidList ordered_uuids; - for (const auto &p : (*media_names_vs_uuids)) { + UuidList ordered_uuids; + for (const auto &p : (*sort_keys_vs_uuids)) { ordered_uuids.push_back(p.second); } - anon_send( - caf::actor_cast(this), - playlist::move_media_atom_v, - ordered_uuids, - utility::Uuid()); + anon_mail(playlist::move_media_atom_v, ordered_uuids, Uuid()) + .send(caf::actor_cast(this)); } }, [=](error &err) mutable { @@ -2268,9 +3025,7 @@ void TimelineActor::sort_alphabetically() { } void TimelineActor::insert_items( - const int index, - const UuidActorVector &uav, - caf::typed_response_promise rp) { + caf::typed_response_promise rp, const int index, const UuidActorVector &uav) { // validate items can be inserted. fan_out_request(vector_to_caf_actor_vector(uav), infinite, item_atom_v) .then( @@ -2315,26 +3070,23 @@ void TimelineActor::insert_items( if (not more.is_null()) changes.insert(changes.begin(), more.begin(), more.end()); - send(event_group_, event_atom_v, item_atom_v, changes, false); - anon_send(history_, history::log_atom_v, sysclock::now(), changes); - send(this, utility::event_atom_v, change_atom_v); + mail(event_atom_v, item_atom_v, changes, false).send(base_.event_group()); + anon_mail(history::log_atom_v, __sysclock_now(), changes).send(history_); + mail(event_atom_v, change_atom_v).send(this); rp.deliver(changes); }, [=](const caf::error &err) mutable { rp.deliver(err); }); } -void TimelineActor::remove_items( - const int index, - const int count, - caf::typed_response_promise>> - rp) { +std::pair> +TimelineActor::remove_items(const int index, const int count) { std::vector items; JsonStore changes(R"([])"_json); if (index < 0 or index + count - 1 >= static_cast(base_.item().size())) - rp.deliver(make_error(xstudio_error::error, "Invalid index / count")); + throw std::runtime_error("Invalid index / count"); else { scoped_actor sys{system()}; @@ -2342,13 +3094,19 @@ void TimelineActor::remove_items( auto it = std::next(base_.item().begin(), i); if (it != base_.item().end()) { auto item = *it; - demonitor(item.actor()); - actors_.erase(item.uuid()); + + if (auto mit = monitor_.find(caf::actor_cast(item.actor())); + mit != std::end(monitor_)) { + mit->second.dispose(); + monitor_.erase(mit); + } + + item_actors_.erase(item.uuid()); auto blind = request_receive(*sys, item.actor(), serialise_atom_v); auto tmp = base_.item().erase(it, blind); changes.insert(changes.end(), tmp.begin(), tmp.end()); - items.push_back(item); + items.push_back(item.clone()); } } @@ -2357,25 +3115,467 @@ void TimelineActor::remove_items( changes.insert(changes.begin(), more.begin(), more.end()); // why was this commented out ? - // send(event_group_, event_atom_v, item_atom_v, changes, false); + // mail(event_atom_v, item_atom_v, changes, false).send(base_.event_group()); + + anon_mail(history::log_atom_v, __sysclock_now(), changes).send(history_); + + mail(event_atom_v, change_atom_v).send(this); + } + + return std::make_pair(changes, items); +} - anon_send(history_, history::log_atom_v, sysclock::now(), changes); - send(this, utility::event_atom_v, change_atom_v); +void TimelineActor::remove_items( + caf::typed_response_promise>> rp, + const int index, + const int count) { - rp.deliver(std::make_pair(changes, items)); + try { + rp.deliver(remove_items(index, count)); + } catch (const std::exception &err) { + rp.deliver(make_error(xstudio_error::error, err.what())); } } void TimelineActor::erase_items( - const int index, const int count, caf::typed_response_promise rp) { + caf::typed_response_promise rp, const int index, const int count) { - request(caf::actor_cast(this), infinite, remove_item_atom_v, index, count) - .then( - [=](const std::pair> &hist_item) mutable { - for (const auto &i : hist_item.second) - send_exit(i.actor(), caf::exit_reason::user_shutdown); - rp.deliver(hist_item.first); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + try { + auto result = remove_items(index, count); + for (const auto &i : result.second) + send_exit(i.actor(), caf::exit_reason::user_shutdown); + rp.deliver(result.first); + + } catch (const std::exception &err) { + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + +// create new track from bake list +void TimelineActor::bake(caf::typed_response_promise rp, const UuidSet &uuids) { + // audio or video ? + // bake for range of timeline + if (uuids.empty()) + return rp.deliver(make_error(xstudio_error::error, "Empty uuid list")); + + auto items = std::vector>(); + auto range = base_.item().trimmed_range(); + + auto first = range.start(); + auto duration = range.duration(); + auto last = range.start() + range.duration(); + auto rate = range.rate(); + + auto mtype = media::MediaType::MT_IMAGE; + + // determine if this is audio or image + // find track container of uuid. + + auto utrack = find_track_from_item(base_.item().children(), *(uuids.begin())); + if (utrack) { + // spdlog::warn("{}", (*utrack)->name()); + if ((*utrack)->item_type() == IT_AUDIO_TRACK) + mtype = media::MediaType::MT_AUDIO; + } + + // spdlog::warn("{} {} {}", range.frame_start().frames(), range.frame_duration().frames(), + // to_string(range.rate())); spdlog::warn("{} {} {} {}", first.to_seconds(), + // duration.to_seconds(), last.to_seconds(), to_string(range.rate())); + + for (auto i = first; i <= last; i += range.rate()) { + auto r = base_.item().resolve_time(i, mtype, uuids, true); + // if(r) + // spdlog::warn("frame {} {} {}", i.to_seconds(), std::get<1>(*r).to_seconds(), + // std::get<0>(*r).name()); + // else + // spdlog::warn("frame {}", i.to_seconds()); + items.emplace_back(r); + } + + // collapse into sequence of clips and gaps. + auto track = Track("Flattened Tracks", mtype); + + for (const auto &i : items) { + if (track.children().empty() or // empty track + (track.children().back().item_type() != IT_GAP and not i) or // change to gap + (track.children().back().item_type() == IT_GAP and i) or // change from gap + (track.children().back().item_type() != IT_GAP and + track.children().back().uuid() != + std::get<0>(*i).uuid()) // change to different clip + ) { + if (not i) { + track.children().emplace_back(Gap("Gap", FrameRateDuration(1, rate)).item()); + } else { + // replace item with copy of i + track.children().emplace_back(std::get<0>(*i)); + // track.children().back().set_uuid(Uuid::generate()); + track.children().back().set_actor_addr(caf::actor_addr()); + track.children().back().set_active_range(FrameRange( + FrameRateDuration( + std::get<1>(*i).to_flicks(), track.children().back().rate()), + FrameRateDuration(1, track.children().back().rate()))); + } + } else { + if (i) { + auto trange = track.children().back().trimmed_range(); + trange.set_duration(trange.duration() + trange.rate()); + track.children().back().set_active_range(trange); + } else { + auto trange = track.children().back().trimmed_range(); + trange.set_duration(trange.duration() + trange.rate()); + track.children().back().set_active_range(trange); + } + } + } + // trim trailing gap.. + if (not track.children().empty() and track.children().back().item_type() == IT_GAP) + track.children().pop_back(); + + // reset uuids + for (auto &i : track.item().children()) { + i.set_uuid(Uuid::generate()); + } + + track.item().refresh(); + + // for(const auto &i: track.item().children()) { + // spdlog::warn("{} {} {}", i.name(), i.trimmed_frame_start().frames(), + // i.trimmed_frame_duration().frames()); + // } + + // create actors + auto track_uuid = track.item().uuid(); + auto track_actor = spawn(track.item()); + + rp.deliver(UuidActor(track_uuid, track_actor)); +} + +void TimelineActor::export_otio_as_string(caf::typed_response_promise rp) { + + otio::ErrorStatus err; + auto jany = std::any(); + auto meta = base_.item().prop(); + + try { + if (not meta.is_object()) + meta = R"({})"_json; + + auto conform_track_uuid = meta.value("conform_track_uuid", Uuid()); + + meta.erase("conform_track_uuid"); + + auto xstudio_meta = + R"({"xstudio": {"colour": "", "enabled": true, "locked": false}})"_json; + xstudio_meta[COLOUR_JPOINTER] = base_.item().flag(); + xstudio_meta[ENABLED_JPOINTER] = base_.item().enabled(); + xstudio_meta[LOCKED_JPOINTER] = base_.item().locked(); + meta.update(xstudio_meta, true); + deserialize_json_from_string(meta.dump(), &jany, &err); + // if(is_error(err)) { + // spdlog::warn("Failed {}", err.full_description); + // } + + otio::SerializableObject::Retainer otimeline(new otio::Timeline( + base_.item().name(), + otio::RationalTime::from_frames( + base_.item().trimmed_frame_start().frames(), base_.item().rate().to_fps()), + std::any_cast(jany))); + + auto to_marker = [=](const Marker &item) mutable { + if (meta = item.prop(); not meta.is_object()) + meta = R"({})"_json; + + xstudio_meta = R"({"xstudio": {"colour": ""}})"_json; + xstudio_meta[COLOUR_JPOINTER] = item.flag(); + meta.update(xstudio_meta, true); + deserialize_json_from_string(meta.dump(), &jany, &err); + + return otio::SerializableObject::Retainer(new otio::Marker( + item.name(), + otio::TimeRange( + otio::RationalTime::from_frames( + item.frame_start().frames(), item.rate().to_fps()), + otio::RationalTime::from_frames( + item.frame_duration().frames(), item.rate().to_fps())), + "GREEN", + std::any_cast(jany))); + }; + + auto to_composition = [=](const Item &item) mutable { + otio::Composition *result = nullptr; + + if (item.item_type() == IT_VIDEO_TRACK or item.item_type() == IT_AUDIO_TRACK) { + if (meta = item.prop(); not meta.is_object()) + meta = R"({})"_json; + + xstudio_meta = + R"({"xstudio": {"colour": "", "enabled": true, "locked": false}})"_json; + xstudio_meta[COLOUR_JPOINTER] = item.flag(); + xstudio_meta[ENABLED_JPOINTER] = item.enabled(); + xstudio_meta[LOCKED_JPOINTER] = item.locked(); + + if (item.uuid() == conform_track_uuid) + xstudio_meta["xstudio"]["conform_track"] = true; + + meta.update(xstudio_meta, true); + deserialize_json_from_string(meta.dump(), &jany, &err); + + result = new otio::Track( + item.name(), + otio::TimeRange( + otio::RationalTime::from_frames( + item.trimmed_frame_start().frames(), item.rate().to_fps()), + otio::RationalTime::from_frames( + item.trimmed_frame_duration().frames(), item.rate().to_fps())), + item.item_type() == IT_VIDEO_TRACK ? otio::Track::Kind::video + : otio::Track::Kind::audio, + std::any_cast(jany)); + result->set_enabled(item.enabled()); + + for (const auto &citem : item.children()) { + if (meta = citem.prop(); not meta.is_object()) + meta = R"({})"_json; + + meta.erase("media_uuid"); + + if (citem.item_type() == IT_GAP) { + xstudio_meta = + R"({"xstudio": {"colour": "", "enabled": true, "locked": false}})"_json; + xstudio_meta[COLOUR_JPOINTER] = citem.flag(); + xstudio_meta[ENABLED_JPOINTER] = citem.enabled(); + xstudio_meta[LOCKED_JPOINTER] = citem.locked(); + meta.update(xstudio_meta, true); + deserialize_json_from_string(meta.dump(), &jany, &err); + + auto gap = new otio::Gap( + otio::RationalTime::from_frames( + citem.trimmed_frame_duration().frames(), citem.rate().to_fps()), + citem.name(), + std::vector(), + std::vector(), + std::any_cast(jany)); + + gap->set_enabled(citem.enabled()); + + result->append_child(gap); + } else if (citem.item_type() == IT_CLIP) { + xstudio_meta = + R"({"xstudio": {"colour": "", "media_colour": "", "enabled": true, "locked": false}})"_json; + xstudio_meta[COLOUR_JPOINTER] = citem.flag(); + xstudio_meta[ENABLED_JPOINTER] = citem.enabled(); + xstudio_meta[LOCKED_JPOINTER] = citem.locked(); + + // need to get the media flag.. + try { + if (citem.actor()) { + caf::scoped_actor sys(system()); + auto flag = std::get<0>( + request_receive>( + *sys, + citem.actor(), + playlist::reflag_container_atom_v)); + if (flag != "#00000000" and flag != "") + xstudio_meta[MEDIA_COLOUR_JPOINTER] = flag; + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + meta.update(xstudio_meta, true); + deserialize_json_from_string(meta.dump(), &jany, &err); + + auto clip = new otio::Clip( + citem.name(), + nullptr, + otio::TimeRange( + otio::RationalTime::from_frames( + citem.trimmed_frame_start().frames(), + citem.rate().to_fps()), + otio::RationalTime::from_frames( + citem.trimmed_frame_duration().frames(), + citem.rate().to_fps())), + std::any_cast(jany)); + + clip->set_enabled(citem.enabled()); + + try { + if (citem.actor()) { + caf::scoped_actor sys(system()); + + auto mr = MediaReference(); + auto mt = item.item_type() == IT_VIDEO_TRACK ? media::MT_IMAGE + : media::MT_AUDIO; + + try { + mr = request_receive>( + *sys, + citem.actor(), + media::media_reference_atom_v, + mt) + .second; + } catch (...) { + // fallback.. + if (mt == media::MT_AUDIO) { + mt = media::MT_IMAGE; + mr = request_receive>( + *sys, + citem.actor(), + media::media_reference_atom_v, + mt) + .second; + } else { + throw; + } + } + + auto msua = request_receive( + *sys, + citem.actor(), + media::current_media_source_atom_v, + mt); + + auto name = request_receive( + *sys, msua.actor(), name_atom_v); + + if (mr.container()) { + auto extref = new otio::ExternalReference( + "file://" + uri_to_posix_path(mr.uri())); + if (citem.available_range()) { + extref->set_available_range(otio::TimeRange( + otio::RationalTime::from_frames( + citem.available_frame_start()->frames() + + mr.start_frame_offset(), + citem.rate().to_fps()), + otio::RationalTime::from_frames( + citem.available_frame_duration()->frames() - + mr.start_frame_offset(), + citem.rate().to_fps()))); + } + extref->set_name(name); + clip->set_media_reference(extref); + } else { + auto path = uri_to_posix_path(mr.uri()); + const static std::regex path_re( + R"(^(.+?\/)([^/]+\.)\{:(\d+)d\}(\..+?)$)"); + std::cmatch m; + if (std::regex_match(path.c_str(), m, path_re)) { + auto base = m[1].str(); + auto prefix = m[2].str(); + auto pad = std::stoi(m[3].str()); + auto suffix = m[4].str(); + + // spdlog::warn("base {}", base); + // spdlog::warn("prefix {}", prefix); + // spdlog::warn("pad {}", pad); + // spdlog::warn("sufix {}", suffix); + + auto extref = new otio::ImageSequenceReference( + "file://" + base, + prefix, + suffix, + citem.available_frame_start()->frames(), + 1, + citem.rate().to_fps(), + pad); + extref->set_name(name); + + if (citem.available_range()) { + extref->set_available_range(otio::TimeRange( + otio::RationalTime::from_frames( + citem.available_frame_start()->frames(), + citem.rate().to_fps()), + otio::RationalTime::from_frames( + citem.available_frame_duration()->frames(), + citem.rate().to_fps()))); + } + clip->set_media_reference(extref); + } + + // ImageSequenceReference( + // std::string const& target_url_base = + // std::string(), std::string const& name_prefix + // = std::string(), std::string const& name_suffix = + // std::string(), int start_frame + // = 1, int frame_step = + // 1, double rate = 1, + // int frame_zero_padding = 0, + // MissingFramePolicy const missing_frame_policy = + // MissingFramePolicy::error, + // optional const& available_range = + // nullopt, AnyDictionary const& metadata = + // AnyDictionary(), optional const& + // available_image_bounds = nullopt); + } + } else { + spdlog::warn("Missing media actor {}", citem.name()); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + result->append_child(clip); + } + } + } + + return result; + }; + + // add tracks.. / etc.. + auto ostack = otimeline->tracks(); + + auto stack = base_.item().front(); + for (const auto &marker : stack.markers()) { + ostack->markers().push_back(to_marker(marker)); + } + + if (meta = base_.item().front().prop(); not meta.is_object()) + meta = R"({})"_json; + + xstudio_meta = R"({"xstudio": {"colour": "", "enabled": true, "locked": false}})"_json; + xstudio_meta[COLOUR_JPOINTER] = base_.item().front().flag(); + xstudio_meta[ENABLED_JPOINTER] = base_.item().front().enabled(); + xstudio_meta[LOCKED_JPOINTER] = base_.item().front().locked(); + meta.update(xstudio_meta, true); + deserialize_json_from_string(meta.dump(), &jany, &err); + ostack->metadata() = std::any_cast(jany); + + for (const auto &track : stack.children()) { + if (track.item_type() == IT_VIDEO_TRACK) + ostack->insert_child(0, to_composition(track)); + else + ostack->append_child(to_composition(track)); + } + + const auto result = otimeline->to_json_string(&err); + + if (not otio::is_error(err)) + rp.deliver(result); + else + rp.deliver(make_error(xstudio_error::error, "Export failed")); + + // and crash.. + otimeline->possibly_delete(); + } catch (const std::exception &err) { + spdlog::warn( + "{} {} {} {}", __PRETTY_FUNCTION__, err.what(), meta.dump(2), jany.type().name()); + rp.deliver(make_error(xstudio_error::error, err.what())); + } +} + + +void TimelineActor::export_otio( + caf::typed_response_promise rp, + const std::string &otio_str, + const caf::uri &path, + const std::string &type, + const std::string &target_schema) { + // build timeline from model.. + // we need clips to return information on current media source.. + + caf::scoped_actor sys(system()); + auto global = system().registry().template get(global_registry); + auto epa = request_receive(*sys, global, global::get_python_atom_v); + rp.delegate(epa, session::export_atom_v, otio_str, path, type, target_schema); } diff --git a/src/timeline/src/track.cpp b/src/timeline/src/track.cpp index 9cd5f332a..11cf718a8 100644 --- a/src/timeline/src/track.cpp +++ b/src/timeline/src/track.cpp @@ -1,10 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #include -#include "xstudio/media/enums.hpp" #include "xstudio/timeline/track.hpp" #include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/frame_range.hpp" using namespace xstudio; using namespace xstudio::media; @@ -13,26 +11,37 @@ using namespace xstudio::utility; Track::Track( const std::string &name, - const media::MediaType media_type, - const utility::Uuid &_uuid, + const FrameRate &rate, + const MediaType media_type, + const Uuid &_uuid, const caf::actor &actor) : Container(name, "Track", _uuid), media_type_(media_type), item_( media_type == MediaType::MT_IMAGE ? ItemType::IT_VIDEO_TRACK : ItemType::IT_AUDIO_TRACK, - utility::UuidActorAddr(uuid(), caf::actor_cast(actor))) { + UuidActorAddr(uuid(), caf::actor_cast(actor)), + {}, + FrameRange(FrameRateDuration(0, rate))) { item_.set_name(name); } Track::Track(const JsonStore &jsn) - : Container(static_cast(jsn.at("container"))), - item_(static_cast(jsn.at("item"))) { + : Container(static_cast(jsn.at("container"))), + item_(static_cast(jsn.at("item"))) { media_type_ = jsn.at("media_type"); } +Track::Track(const Item &item, const caf::actor &actor) + : Container(item.name(), "Track", item.uuid()), item_(item.clone()) { + item_.set_actor_addr(caf::actor_cast(actor)); + media_type_ = + (item_.item_type() == ItemType::IT_VIDEO_TRACK ? MediaType::MT_IMAGE + : MediaType::MT_AUDIO); +} + Track Track::duplicate() const { - utility::JsonStore jsn; + JsonStore jsn; auto dup_container = Container::duplicate(); auto dup_item = item_; @@ -55,4 +64,4 @@ JsonStore Track::serialise() const { return jsn; } -void Track::set_media_type(const media::MediaType media_type) { media_type_ = media_type; } +void Track::set_media_type(const MediaType media_type) { media_type_ = media_type; } diff --git a/src/timeline/src/track_actor.cpp b/src/timeline/src/track_actor.cpp index c4e8968f1..8871cf9f8 100644 --- a/src/timeline/src/track_actor.cpp +++ b/src/timeline/src/track_actor.cpp @@ -2,67 +2,79 @@ #include #include "xstudio/atoms.hpp" -#include "xstudio/broadcast/broadcast_actor.hpp" -#include "xstudio/media/media.hpp" #include "xstudio/timeline/clip_actor.hpp" #include "xstudio/timeline/gap_actor.hpp" #include "xstudio/timeline/stack_actor.hpp" #include "xstudio/timeline/track_actor.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" -#include "xstudio/utility/uuid.hpp" - -// #include "xstudio/media/media_actor.hpp" -// #include "xstudio/atoms.hpp" -// #include "xstudio/playhead/playhead_actor.hpp" -// #include "xstudio/atoms.hpp" -// #include "xstudio/atoms.hpp" using namespace xstudio; using namespace xstudio::utility; using namespace xstudio::timeline; -using namespace caf; -caf::actor TrackActor::deserialise(const utility::JsonStore &value, const bool replace_item) { - auto key = utility::Uuid(value.at("base").at("item").at("uuid")); +caf::actor TrackActor::deserialise(const JsonStore &value, const bool replace_item) { + auto key = Uuid(value.at("base").at("item").at("uuid")); auto actor = caf::actor(); auto type = value.at("base").at("container").at("type").get(); + auto item = Item(); + if (type == "Clip") { - auto item = Item(); - actor = spawn(static_cast(value), item); - add_item(UuidActor(key, actor)); - if (replace_item) { - auto itemit = find_uuid(base_.item().children(), key); - (*itemit) = item; - } + actor = spawn(static_cast(value), item); } else if (type == "Gap") { - auto item = Item(); - actor = spawn(static_cast(value), item); - add_item(UuidActor(key, actor)); - if (replace_item) { - auto itemit = find_uuid(base_.item().children(), key); - (*itemit) = item; - } + actor = spawn(static_cast(value), item); } else if (type == "Stack") { - auto item = Item(); - actor = spawn(static_cast(value), item); + actor = spawn(static_cast(value), item); + } + + if (actor) { add_item(UuidActor(key, actor)); if (replace_item) { auto itemit = find_uuid(base_.item().children(), key); - (*itemit) = item; + if (itemit != base_.item().end()) { + (*itemit) = item; + } else { + spdlog::warn( + "{} Invalid item to replace {} {}", + __PRETTY_FUNCTION__, + to_string(key), + value.dump(2)); + } } } + return actor; } +void TrackActor::deserialise() { + for (auto &i : base_.item().children()) { + switch (i.item_type()) { + case IT_CLIP: { + auto actor = spawn(i, i); + add_item(UuidActor(i.uuid(), actor)); + } break; + case IT_GAP: { + auto actor = spawn(i, i); + add_item(UuidActor(i.uuid(), actor)); + } break; + case IT_STACK: { + auto actor = spawn(i, i); + add_item(UuidActor(i.uuid(), actor)); + } break; + default: + break; + } + } +} + // trigger actor creation -void TrackActor::item_event_callback(const utility::JsonStore &event, Item &item) { +void TrackActor::item_event_callback(const JsonStore &event, Item &item) { switch (static_cast(event.at("action"))) { - case IT_INSERT: { + case IA_INSERT: { // spdlog::warn("TrackActor IT_INSERT {}", event.dump(2)); - auto cuuid = utility::Uuid(event.at("item").at("uuid")); + auto cuuid = Uuid(event.at("item").at("uuid")); // spdlog::warn("{} {} {} {}", find_uuid(base_.item().children(), cuuid) != // base_.item().cend(), actors_.count(cuuid), not event["blind"].is_null(), // event.dump(2)); needs to be child.. @@ -72,58 +84,77 @@ void TrackActor::item_event_callback(const utility::JsonStore &event, Item &item // spdlog::warn("{} {} {}", child_item_it != base_.item().cend(), not // actors_.count(cuuid), not event.at("blind").is_null()); - if (child_item_it != base_.item().cend() and not actors_.count(cuuid) and + if (child_item_it != base_.item().end() and not actors_.count(cuuid) and not event.at("blind").is_null()) { // our child // spdlog::warn("RECREATE MATCH"); - auto actor = deserialise(utility::JsonStore(event.at("blind")), false); - add_item(UuidActor(cuuid, actor)); + // if(child_item_it->item_type() == IT_CLIP) { + // auto media_uuid = child_item_it->prop().value("media_uuid", Uuid()); + // if(media_uuid) { + // // make sure media exists in timeline. + + // } + // } + + // spdlog::warn("child_item_it {}", child_item_it->item_type()); + // spdlog::warn("child_item_it {}", child_item_it->name()); + // spdlog::warn("child_item_it {}", child_item_it->prop().dump(2)); + + auto actor = deserialise(JsonStore(event.at("blind")), false); // spdlog::warn("{}",to_string(caf::actor_cast(actor))); // spdlog::warn("{}",to_string(caf::actor_cast(child_item_it->actor()))); + + // iteraror becomes invalid.. child_item_it->set_actor_addr(actor); + + // change item actor addr // spdlog::warn("TrackActor create // {}",to_string(caf::actor_cast(child_item_it->actor()))); // item actor_addr will be wrong.. in ancestors // send special update.. - send( - event_group_, - event_atom_v, - item_atom_v, - child_item_it->make_actor_addr_update(), - true); + mail(event_atom_v, item_atom_v, child_item_it->make_actor_addr_update(), true) + .send(base_.event_group()); } } break; - case IT_REMOVE: { + case IA_REMOVE: { // spdlog::warn("TrackActor IT_REMOVE {} {}", base_.item().name(), event.dump(2)); - auto cuuid = utility::Uuid(event.at("item_uuid")); + auto cuuid = Uuid(event.at("item_uuid")); // child destroyed if (actors_.count(cuuid)) { // spdlog::warn("destroy // {}",to_string(caf::actor_cast(actors_[cuuid]))); - demonitor(actors_[cuuid]); + + if (auto mit = monitor_.find(caf::actor_cast(actors_[cuuid])); + mit != std::end(monitor_)) { + mit->second.dispose(); + monitor_.erase(mit); + } + send_exit(actors_[cuuid], caf::exit_reason::user_shutdown); actors_.erase(cuuid); } } break; - case IT_ENABLE: - case IT_ACTIVE: - case IT_AVAIL: - case IT_SPLICE: - case IT_ADDR: + case IA_LOCK: + case IA_ENABLE: + case IA_ACTIVE: + case IA_RANGE: + case IA_AVAIL: + case IA_SPLICE: + case IA_ADDR: case IA_NONE: default: break; } } -TrackActor::TrackActor(caf::actor_config &cfg, const utility::JsonStore &jsn) - : caf::event_based_actor(cfg), base_(static_cast(jsn.at("base"))) { +TrackActor::TrackActor(caf::actor_config &cfg, const JsonStore &jsn) + : caf::event_based_actor(cfg), base_(static_cast(jsn.at("base"))) { base_.item().set_actor_addr(this); for (const auto &[key, value] : jsn.at("actors").items()) { @@ -135,16 +166,15 @@ TrackActor::TrackActor(caf::actor_config &cfg, const utility::JsonStore &jsn) } base_.item().set_system(&system()); - base_.item().bind_item_event_func([this](const utility::JsonStore &event, Item &item) { - item_event_callback(event, item); - }); + base_.item().bind_item_post_event_func( + [this](const JsonStore &event, Item &item) { item_event_callback(event, item); }); init(); } -TrackActor::TrackActor(caf::actor_config &cfg, const utility::JsonStore &jsn, Item &pitem) - : caf::event_based_actor(cfg), base_(static_cast(jsn.at("base"))) { +TrackActor::TrackActor(caf::actor_config &cfg, const JsonStore &jsn, Item &pitem) + : caf::event_based_actor(cfg), base_(static_cast(jsn.at("base"))) { base_.item().set_actor_addr(this); for (const auto &[key, value] : jsn.at("actors").items()) { @@ -156,10 +186,9 @@ TrackActor::TrackActor(caf::actor_config &cfg, const utility::JsonStore &jsn, It } base_.item().set_system(&system()); - base_.item().bind_item_event_func([this](const utility::JsonStore &event, Item &item) { - item_event_callback(event, item); - }); - pitem = base_.item(); + base_.item().bind_item_post_event_func( + [this](const JsonStore &event, Item &item) { item_event_callback(event, item); }); + pitem = base_.item().clone(); init(); } @@ -167,81 +196,66 @@ TrackActor::TrackActor(caf::actor_config &cfg, const utility::JsonStore &jsn, It TrackActor::TrackActor( caf::actor_config &cfg, const std::string &name, + const FrameRate &rate, const media::MediaType media_type, - const utility::Uuid &uuid) - : caf::event_based_actor(cfg), base_(name, media_type, uuid, this) { + const Uuid &uuid) + : caf::event_based_actor(cfg), base_(name, rate, media_type, uuid, this) { base_.item().set_system(&system()); base_.item().set_name(name); - base_.item().bind_item_event_func([this](const utility::JsonStore &event, Item &item) { - item_event_callback(event, item); - }); + base_.item().bind_item_post_event_func( + [this](const JsonStore &event, Item &item) { item_event_callback(event, item); }); init(); } -void TrackActor::on_exit() { - for (const auto &i : actors_) - send_exit(i.second, caf::exit_reason::user_shutdown); +TrackActor::TrackActor(caf::actor_config &cfg, const Item &item) + : caf::event_based_actor(cfg), base_(item, this) { + base_.item().set_system(&system()); + deserialise(); + init(); } -void TrackActor::init() { - print_on_create(this, base_.name()); - print_on_exit(this, base_.name()); - - event_group_ = spawn(this); - link_to(event_group_); - // update_edit_list_ = true; - - set_down_handler([=](down_msg &msg) { - // if a child dies we won't have enough information to recreate it. - // we still need to report it up the chain though. - for (auto it = std::begin(actors_); it != std::end(actors_); ++it) { - if (msg.source == it->second) { - - // spdlog::warn("detected death {}", to_string(it->second)); - demonitor(it->second); - actors_.erase(it); - - // remove from base. - auto it = find_actor_addr(base_.item().children(), msg.source); +TrackActor::TrackActor(caf::actor_config &cfg, const Item &item, Item &nitem) + : TrackActor(cfg, item) { + nitem = base_.item().clone(); +} - if (it != base_.item().end()) { - auto jsn = base_.item().erase(it); - auto more = base_.item().refresh(); - if (not more.is_null()) - jsn.insert(jsn.begin(), more.begin(), more.end()); - send(event_group_, event_atom_v, item_atom_v, jsn, false); - } +void TrackActor::on_exit() { + for (const auto &i : actors_) + send_exit(i.second, caf::exit_reason::user_shutdown); +} - break; - } - } - }); - - behavior_.assign( - base_.make_set_name_handler(event_group_, this), - base_.make_get_name_handler(), - base_.make_last_changed_getter(), - base_.make_last_changed_setter(event_group_, this), - base_.make_last_changed_event_handler(event_group_, this), - base_.make_get_uuid_handler(), - base_.make_get_type_handler(), - make_get_event_group_handler(event_group_), - base_.make_get_detail_handler(this, event_group_), +caf::message_handler TrackActor::default_event_handler() { + return caf::message_handler() + .or_else(NotificationHandler::default_event_handler()) + .or_else(Container::default_event_handler()); +} +caf::message_handler TrackActor::message_handler() { + return caf::message_handler{ [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) {}, - [=](link_media_atom, const UuidActorMap &media) -> result { + [=](link_media_atom, const UuidActorMap &media, const bool force) -> result { auto rp = make_response_promise(); - // pool direct children for state. - fan_out_request( - map_value_to_vec(actors_), infinite, link_media_atom_v, media) - .await( - [=](std::vector items) mutable { rp.deliver(true); }, - [=](error &err) mutable { rp.deliver(err); }); + if (actors_.empty()) { + rp.deliver(true); + } else { + // pool direct children for state. + fan_out_request( + map_value_to_vec(actors_), infinite, link_media_atom_v, media, force) + .await( + [=](std::vector items) mutable { rp.deliver(true); }, + [=](error &err) mutable { + spdlog::warn( + "{} {} {}", + __PRETTY_FUNCTION__, + to_string(err), + base_.item().name()); + rp.deliver(false); + }); + } return rp; }, @@ -249,32 +263,40 @@ void TrackActor::init() { [=](plugin_manager::enable_atom, const bool value) -> JsonStore { auto jsn = base_.item().set_enabled(value); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, - [=](item_atom) -> Item { return base_.item(); }, + [=](item_atom) -> Item { return base_.item().clone(); }, [=](item_flag_atom, const std::string &value) -> JsonStore { auto jsn = base_.item().set_flag(value); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_lock_atom, const bool value) -> JsonStore { + auto jsn = base_.item().set_locked(value); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, [=](item_name_atom, const std::string &value) -> JsonStore { auto jsn = base_.item().set_name(value); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, [=](item_atom, const bool with_state) -> result> { auto rp = make_response_promise>(); - request(caf::actor_cast(this), infinite, utility::serialise_atom_v) + mail(serialise_atom_v) + .request(caf::actor_cast(this), infinite) .then( [=](const JsonStore &jsn) mutable { - rp.deliver(std::make_pair(jsn, base_.item())); + rp.deliver(std::make_pair(jsn, base_.item().clone())); }, [=](const caf::error &err) mutable { rp.deliver(err); }); return rp; @@ -286,51 +308,104 @@ void TrackActor::init() { } auto it = base_.item().begin(); std::advance(it, index); - return *it; + return (*it).clone(); + }, + + [=](item_prop_atom, const JsonStore &value, const std::string &path) -> JsonStore { + auto prop = base_.item().prop(); + try { + auto ptr = nlohmann::json::json_pointer(path); + prop.at(ptr).update(value); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + auto jsn = base_.item().set_prop(prop); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_prop_atom, const JsonStore &value, const bool merge) -> JsonStore { + auto p = base_.item().prop(); + p.update(value); + auto jsn = base_.item().set_prop(p); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_prop_atom, const JsonStore &value) -> JsonStore { + auto jsn = base_.item().set_prop(value); + if (not jsn.is_null()) + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); + return jsn; + }, + + [=](item_prop_atom) -> JsonStore { return base_.item().prop(); }, + + [=](rate_atom) -> FrameRate { return base_.item().rate(); }, + + [=](rate_atom atom, const media::MediaType media_type) { + return mail(atom).delegate(caf::actor_cast(this)); }, [=](active_range_atom, const FrameRange &fr) -> JsonStore { auto jsn = base_.item().set_active_range(fr); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, [=](available_range_atom, const FrameRange &fr) -> JsonStore { auto jsn = base_.item().set_available_range(fr); if (not jsn.is_null()) - send(event_group_, event_atom_v, item_atom_v, jsn, false); + mail(event_atom_v, item_atom_v, jsn, false).send(base_.event_group()); return jsn; }, - [=](active_range_atom) -> std::optional { + [=](active_range_atom) -> std::optional { return base_.item().active_range(); }, - [=](available_range_atom) -> std::optional { + [=](available_range_atom) -> std::optional { return base_.item().available_range(); }, - [=](trimmed_range_atom) -> utility::FrameRange { return base_.item().trimmed_range(); }, + [=](trimmed_range_atom) -> FrameRange { return base_.item().trimmed_range(); }, + + // check events processes + [=](item_atom, event_atom, const std::set &events) -> bool { + auto result = true; + for (const auto &i : events) { + if (not events_processed_.contains(i)) { + result = false; + break; + } + } + return result; + }, // handle child change events. [=](event_atom, item_atom, const JsonStore &update, const bool hidden) { - if (base_.item().update(update)) { + auto event_ids = base_.item().update(update); + if (not event_ids.empty()) { + events_processed_.insert(event_ids.begin(), event_ids.end()); auto more = base_.item().refresh(); if (not more.is_null()) { more.insert(more.begin(), update.begin(), update.end()); - send(event_group_, event_atom_v, item_atom_v, more, hidden); + mail(event_atom_v, item_atom_v, more, hidden).send(base_.event_group()); return; } } - send(event_group_, event_atom_v, item_atom_v, update, hidden); + mail(event_atom_v, item_atom_v, update, hidden).send(base_.event_group()); }, [=](history::undo_atom, const JsonStore &hist) -> result { base_.item().undo(hist); if (actors_.empty()) return true; + // push to children.. auto rp = make_response_promise(); @@ -363,7 +438,7 @@ void TrackActor::init() { const int frame, const UuidActorVector &uav) -> result { auto rp = make_response_promise(); - insert_items_at_frame(frame, uav, rp); + insert_items_at_frame(rp, frame, uav); return rp; }, @@ -371,7 +446,7 @@ void TrackActor::init() { const int index, const UuidActorVector &uav) -> result { auto rp = make_response_promise(); - insert_items(index, uav, rp); + insert_items(rp, index, uav); return rp; }, @@ -379,12 +454,12 @@ void TrackActor::init() { -> result { auto rp = make_response_promise(); auto tframe = base_.item().frame_at_index(index, frame); - insert_items_at_frame(tframe, uav, rp); + insert_items_at_frame(rp, tframe, uav); return rp; }, [=](insert_item_atom, - const utility::Uuid &before_uuid, + const Uuid &before_uuid, const UuidActorVector &uav) -> result { auto rp = make_response_promise(); @@ -399,37 +474,55 @@ void TrackActor::init() { } if (rp.pending()) - insert_items(index, uav, rp); + insert_items(rp, index, uav); return rp; }, + [=](item_type_atom) -> ItemType { return base_.item().item_type(); }, [=](remove_item_at_frame_atom, const int frame, - const int duration) -> result>> { + const int duration, + const bool add_gap, + const bool collapse_gaps) -> result>> { auto rp = make_response_promise>>(); - remove_items_at_frame(frame, duration, rp); + remove_items_at_frame(rp, frame, duration, add_gap, collapse_gaps); + return rp; + }, + + [=](remove_item_at_frame_atom, const int frame, const int duration, const bool add_gap) + -> result>> { + auto rp = make_response_promise>>(); + remove_items_at_frame(rp, frame, duration, add_gap, true); return rp; }, [=](remove_item_atom, const int index) -> result>> { auto rp = make_response_promise>>(); - remove_items(index, 1, rp); + remove_items(rp, index, 1, false, true); return rp; }, [=](remove_item_atom, const int index, - const int count) -> result>> { + const bool add_gap) -> result>> { auto rp = make_response_promise>>(); - remove_items(index, count, rp); + remove_items(rp, index, 1, add_gap, true); + return rp; + }, + + [=](remove_item_atom, const int index, const int count, const bool add_gap) + -> result>> { + auto rp = make_response_promise>>(); + remove_items(rp, index, count, add_gap, true); return rp; }, [=](remove_item_atom, - const utility::Uuid &uuid) -> result>> { + const Uuid &uuid, + const bool add_gap) -> result>> { auto rp = make_response_promise>>(); auto it = find_uuid(base_.item().children(), uuid); @@ -438,32 +531,48 @@ void TrackActor::init() { rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); if (rp.pending()) - remove_items(std::distance(base_.item().begin(), it), 1, rp); + remove_items(rp, std::distance(base_.item().begin(), it), 1, add_gap, true); return rp; }, [=](erase_item_at_frame_atom, const int frame, - const int duration) -> result { + const int duration, + const bool add_gap, + const bool collapse_gaps) -> result { auto rp = make_response_promise(); - erase_items_at_frame(frame, duration, rp); + erase_items_at_frame(rp, frame, duration, add_gap, collapse_gaps); + return rp; + }, + + [=](erase_item_at_frame_atom, const int frame, const int duration, const bool add_gap) + -> result { + auto rp = make_response_promise(); + erase_items_at_frame(rp, frame, duration, add_gap, true); return rp; }, [=](erase_item_atom, const int index) -> result { auto rp = make_response_promise(); - erase_items(index, 1, rp); + erase_items(rp, index, 1, false, true); return rp; }, - [=](erase_item_atom, const int index, const int count) -> result { + [=](erase_item_atom, const int index, const bool add_gap) -> result { auto rp = make_response_promise(); - erase_items(index, count, rp); + erase_items(rp, index, 1, add_gap, true); return rp; }, - [=](erase_item_atom, const utility::Uuid &uuid) -> result { + [=](erase_item_atom, const int index, const int count, const bool add_gap) + -> result { + auto rp = make_response_promise(); + erase_items(rp, index, count, add_gap, true); + return rp; + }, + + [=](erase_item_atom, const Uuid &uuid, const bool add_gap) -> result { auto rp = make_response_promise(); auto it = find_uuid(base_.item().children(), uuid); @@ -472,7 +581,7 @@ void TrackActor::init() { rp.deliver(make_error(xstudio_error::error, "Invalid uuid")); if (rp.pending()) - erase_items(std::distance(base_.item().begin(), it), 1, rp); + erase_items(rp, std::distance(base_.item().begin(), it), 1, add_gap, true); return rp; }, @@ -483,7 +592,7 @@ void TrackActor::init() { if (not split_point) rp.deliver(make_error(xstudio_error::error, "Invalid split frame")); else - split_item(split_point->first, split_point->second, rp); + split_item(rp, split_point->first, split_point->second); return rp; }, @@ -494,16 +603,16 @@ void TrackActor::init() { if (it == base_.item().children().end()) return make_error(xstudio_error::error, "Invalid index"); auto rp = make_response_promise(); - split_item(it, frame, rp); + split_item(rp, it, frame); return rp; }, - [=](split_item_atom, const utility::Uuid &uuid, const int frame) -> result { + [=](split_item_atom, const Uuid &uuid, const int frame) -> result { auto it = find_uuid(base_.item().children(), uuid); if (it == base_.item().end()) return make_error(xstudio_error::error, "Invalid uuid"); auto rp = make_response_promise(); - split_item(it, frame, rp); + split_item(rp, it, frame); return rp; }, @@ -511,23 +620,32 @@ void TrackActor::init() { const int frame, const int duration, const int dest_frame, - const bool insert) -> result { + const bool insert, + const bool add_gap) -> result { + auto rp = make_response_promise(); + move_items_at_frame(rp, frame, duration, dest_frame, insert, add_gap); + return rp; + }, + + [=](move_item_atom, + const int src_index, + const int count, + const int dst_index, + const bool add_gap) -> result { auto rp = make_response_promise(); - move_items_at_frame(frame, duration, dest_frame, insert, rp); + move_items(rp, src_index, count, dst_index, add_gap); return rp; }, [=](move_item_atom, const int src_index, const int count, const int dst_index) -> result { auto rp = make_response_promise(); - move_items(src_index, count, dst_index, rp); + move_items(rp, src_index, count, dst_index, false); return rp; }, - [=](move_item_atom, - const utility::Uuid &src_uuid, - const int count, - const utility::Uuid &before_uuid) -> result { + [=](move_item_atom, const Uuid &src_uuid, const int count, const Uuid &before_uuid) + -> result { // check src is valid. auto rp = make_response_promise(); @@ -545,23 +663,24 @@ void TrackActor::init() { } if (rp.pending()) move_items( + rp, std::distance(base_.item().begin(), sitb), count, std::distance(base_.item().begin(), dit), - rp); + false); } return rp; }, - [=](utility::event_atom, utility::change_atom) { + [=](event_atom, change_atom) { // update_edit_list_ = true; - send(event_group_, utility::event_atom_v, utility::change_atom_v); + mail(event_atom_v, change_atom_v).send(base_.event_group()); }, - [=](utility::event_atom, utility::name_atom, const std::string & /*name*/) {}, + [=](event_atom, name_atom, const std::string & /*name*/) {}, - [=](utility::duplicate_atom) -> result { + [=](duplicate_atom) -> result { auto rp = make_response_promise(); JsonStore jsn; auto dup = base_.duplicate(); @@ -578,8 +697,8 @@ void TrackActor::init() { scoped_actor sys{system()}; for (const auto &i : base_.children()) { - auto ua = request_receive( - *sys, actors_[i.uuid()], utility::duplicate_atom_v); + auto ua = + request_receive(*sys, actors_[i.uuid()], duplicate_atom_v); request_receive( *sys, actor, insert_item_atom_v, -1, UuidActorVector({ua})); } @@ -589,7 +708,33 @@ void TrackActor::init() { return rp; }, - [=](utility::serialise_atom) -> result { + [=](playhead::source_atom, + const UuidUuidMap &swap, + const UuidActorMap &media) -> result { + auto rp = make_response_promise(); + + if (actors_.empty()) { + rp.deliver(true); + } else { + fan_out_request( + map_value_to_vec(actors_), infinite, playhead::source_atom_v, swap, media) + .await( + [=](std::vector items) mutable { rp.deliver(true); }, + [=](error &err) mutable { + spdlog::warn( + "{} {} {}", + __PRETTY_FUNCTION__, + to_string(err), + base_.item().name()); + + rp.deliver(err); + }); + } + + return rp; + }, + + [=](serialise_atom) -> result { if (not actors_.empty()) { auto rp = make_response_promise(); @@ -615,113 +760,77 @@ void TrackActor::init() { jsn["actors"] = {}; return result(jsn); + }, + + [=](media::get_media_pointers_atom atom, + const media::MediaType media_type, + const TimeSourceMode tsm, + const FrameRate &override_rate) -> caf::result { + // This is required by SubPlayhead actor to make the track + // playable. + return base_.item().get_all_frame_IDs(media_type, tsm, override_rate); } + }; +} - // code for playhead// get edit_list for all tracks/stacks..// this is temporary, it'll - // need heavy changes..// also this only returns edit_lists for images, audio may be - // different.. - // [=](media::get_edit_list_atom, const Uuid &uuid) -> result { - // if (update_edit_list_) { - // std::vector actors; - // for (const auto &i : base_.clips()) - // actors.push_back(actors_[i]); - - // if (not actors.empty()) { - // auto rp = make_response_promise(); - - // fan_out_request( - // actors, infinite, media::get_edit_list_atom_v, Uuid()) - // .await( - // [=](std::vector sections) mutable { - // edit_list_.clear(); - // for (const auto &i : base_.clips()) { - // for (const auto &ii : sections) { - // if (not ii.section_list().empty()) { - // const auto &[ud, rt, tc] = ii.section_list()[0]; - // if (ud == i) { - // if (uuid.is_null()) - // edit_list_.push_back(ii.section_list()[0]); - // else - // edit_list_.push_back({uuid, rt, tc}); - // break; - // } - // } - // } - // } - // update_edit_list_ = false; - // auto edit_list = edit_list_; - // if (not uuid.is_null()) - // edit_list.set_uuid(uuid); - // else - // edit_list.set_uuid(base_.uuid()); - - // rp.deliver(edit_list); - // }, - // [=](error &err) mutable { rp.deliver(std::move(err)); }); - - // return rp; - // } else { - // edit_list_.clear(); - // update_edit_list_ = false; - // } - // } - - // auto edit_list = edit_list_; - // if (not uuid.is_null()) - // edit_list.set_uuid(uuid); - // else - // edit_list.set_uuid(base_.uuid()); - - // return result(edit_list); - // }, - - // [=](media::get_media_pointer_atom, - // const int logical_frame) -> result { - // if (base_.empty()) - // return result(make_error(xstudio_error::error, "No - // media")); - - // auto rp = make_response_promise(); - // if (update_edit_list_) { - // request(actor_cast(this), infinite, media::get_edit_list_atom_v) - // .then( - // [=](const utility::EditList &) mutable { - // deliver_media_pointer(logical_frame, rp); - // }, - // [=](error &err) mutable { rp.deliver(std::move(err)); }); - // } else { - // deliver_media_pointer(logical_frame, rp); - // } - - // return rp; - // }, - - - ); + +void TrackActor::init() { + print_on_create(this, base_.name()); + print_on_exit(this, base_.name()); } -void TrackActor::add_item(const utility::UuidActor &ua) { +void TrackActor::add_item(const UuidActor &ua) { // join_event_group(this, ua.second); scoped_actor sys{system()}; try { - auto grp = - request_receive(*sys, ua.actor(), utility::get_event_group_atom_v); + auto grp = request_receive(*sys, ua.actor(), get_event_group_atom_v); auto joined = request_receive(*sys, grp, broadcast::join_broadcast_atom_v, this); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } - monitor(ua.actor()); + auto act_addr = caf::actor_cast(ua.actor()); + + if (auto sit = monitor_.find(act_addr); sit == std::end(monitor_)) { + monitor_[act_addr] = + monitor(ua.actor(), [this, addr = ua.actor().address()](const error &) { + if (auto mit = monitor_.find(caf::actor_cast(addr)); + mit != std::end(monitor_)) + monitor_.erase(mit); + + for (auto it = std::begin(actors_); it != std::end(actors_); ++it) { + if (addr == it->second) { + actors_.erase(it); + + // remove from base. + auto it = find_actor_addr(base_.item().children(), addr); + + if (it != base_.item().end()) { + auto jsn = base_.item().erase(it); + auto more = base_.item().refresh(); + if (not more.is_null()) + jsn.insert(jsn.begin(), more.begin(), more.end()); + + mail(event_atom_v, item_atom_v, jsn, false) + .send(base_.event_group()); + } + + break; + } + } + }); + } + actors_[ua.uuid()] = ua.actor(); } void TrackActor::split_item( + caf::typed_response_promise rp, const Items::const_iterator &itemit, - const int frame, - caf::typed_response_promise rp) { + const int frame) { // validate frame is inside item.. // validate item type.. auto item = *itemit; @@ -731,9 +840,10 @@ void TrackActor::split_item( auto orig_duration = trimmed_range.frame_duration().frames(); auto orig_end = orig_start + orig_duration - 1; - if (frame > orig_start and frame < orig_end) { + if (frame > orig_start and frame <= orig_end) { // duplicate item to split. - request(item.actor(), infinite, utility::duplicate_atom_v) + mail(duplicate_atom_v) + .request(item.actor(), infinite) .await( [=](const UuidActor &ua) mutable { // adjust start frames if clip. @@ -750,22 +860,20 @@ void TrackActor::split_item( (orig_duration - item1_range.frame_duration().frames()))); // set range on new item - request( - ua.actor(), infinite, timeline::active_range_atom_v, item2_range) + mail(active_range_atom_v, item2_range) + .request(ua.actor(), infinite) .await( [=](const JsonStore &) mutable { // set range on old item - request( - item.actor(), - infinite, - timeline::active_range_atom_v, - item1_range) + mail(active_range_atom_v, item1_range) + .request(item.actor(), infinite) .await( [=](const JsonStore &) mutable { // insert next to original. - rp.delegate( - caf::actor_cast(this), - insert_item_atom_v, + // we got a backlog of inflight events. + // track will not be in sync + insert_items( + rp, static_cast( std::distance( base_.item().cbegin(), itemit) + @@ -781,6 +889,8 @@ void TrackActor::split_item( }, [=](error &err) mutable { rp.deliver(std::move(err)); }); } else { + // spdlog::warn("{} {} frame {} clip start {} clip end {}", __PRETTY_FUNCTION__, + // "Invalid frame to split on", frame, orig_start, orig_end); rp.deliver(make_error(xstudio_error::error, "Invalid frame to split on")); } } else { @@ -789,9 +899,7 @@ void TrackActor::split_item( } void TrackActor::insert_items( - const int index, - const UuidActorVector &uav, - caf::typed_response_promise rp) { + caf::typed_response_promise rp, const int index, const UuidActorVector &uav) { // validate items can be inserted. fan_out_request(vector_to_caf_actor_vector(uav), infinite, item_atom_v) .then( @@ -842,7 +950,7 @@ void TrackActor::insert_items( if (not more.is_null()) changes.insert(changes.begin(), more.begin(), more.end()); - send(event_group_, event_atom_v, item_atom_v, changes, false); + mail(event_atom_v, item_atom_v, changes, false).send(base_.event_group()); rp.deliver(changes); }, @@ -850,9 +958,7 @@ void TrackActor::insert_items( } void TrackActor::insert_items_at_frame( - const int frame, - const utility::UuidActorVector &uav, - caf::typed_response_promise rp) { + caf::typed_response_promise rp, const int frame, const UuidActorVector &uav) { auto item_frame = base_.item().item_at_frame(frame); if (not item_frame) { @@ -862,14 +968,14 @@ void TrackActor::insert_items_at_frame( auto track_end = base_.item().trimmed_frame_start().frames() + base_.item().trimmed_frame_duration().frames() - 1; auto filler = frame - track_end; - auto gap_uuid = utility::Uuid::generate(); + auto gap_uuid = Uuid::generate(); auto gap_actor = spawn("Gap", FrameRateDuration(filler, base_.item().rate()), gap_uuid); uav_plus_gap.push_back(UuidActor(gap_uuid, gap_actor)); for (const auto &i : uav) uav_plus_gap.push_back(i); - insert_items(base_.item().size(), uav_plus_gap, rp); + insert_items(rp, base_.item().size(), uav_plus_gap); } else { auto cit = item_frame->first; auto cframe = item_frame->second; @@ -877,17 +983,13 @@ void TrackActor::insert_items_at_frame( if (cframe == cit->trimmed_frame_start().frames()) { // simple insertion.. - insert_items(index, uav, rp); + insert_items(rp, index, uav); } else { // complex.. we need to split item - request( - caf::actor_cast(this), - infinite, - split_item_atom_v, - static_cast(index), - static_cast(cframe)) + mail(split_item_atom_v, static_cast(index), static_cast(cframe)) + .request(caf::actor_cast(this), infinite) .then( - [=](const JsonStore &) { insert_items_at_frame(frame, uav, rp); }, + [=](const JsonStore &) { insert_items_at_frame(rp, frame, uav); }, [=](const caf::error &err) mutable { rp.deliver(err); }); } } @@ -897,103 +999,169 @@ void TrackActor::insert_items_at_frame( // build item/count value and pass to remove items // how do we wait for sync of state, from split children.. ? void TrackActor::remove_items_at_frame( + caf::typed_response_promise>> rp, const int frame, const int duration, - caf::typed_response_promise>> - rp) { + const bool add_gap, + const bool collapse_gaps) { auto in_point = base_.item().item_at_frame(frame); + // spdlog::warn("{}",base_.item().size()); + if (not in_point) rp.deliver(make_error(xstudio_error::error, "Invalid frame")); else { if (in_point->second != in_point->first->trimmed_frame_start().frames()) { // split at in point, and recall function. - request( - caf::actor_cast(this), - infinite, + // spdlog::warn("start split {} {}", + // static_cast(std::distance(base_.item().cbegin(), in_point->first)), + // static_cast(in_point->second)); + mail( split_item_atom_v, static_cast(std::distance(base_.item().cbegin(), in_point->first)), static_cast(in_point->second)) + .request(caf::actor_cast(this), infinite) .then( - [=](const JsonStore &) { remove_items_at_frame(frame, duration, rp); }, + [=](const JsonStore &) { + remove_items_at_frame(rp, frame, duration, add_gap, collapse_gaps); + }, [=](const caf::error &err) mutable { rp.deliver(err); }); } else { + // do we need to account for erasing off the end ? // split end point... auto out_point = base_.item().item_at_frame(frame + duration); - if (out_point->second != out_point->first->trimmed_frame_start().frames()) { + if (out_point and + out_point->second != out_point->first->trimmed_frame_start().frames()) { // split at in point, and recall function. - request( - caf::actor_cast(this), - infinite, + // spdlog::warn("start end {} {}", + // static_cast(std::distance(base_.item().cbegin(), out_point->first)), + // static_cast(out_point->second)); + mail( split_item_atom_v, static_cast(std::distance(base_.item().cbegin(), out_point->first)), static_cast(out_point->second)) + .request(caf::actor_cast(this), infinite) .then( - [=](const JsonStore &) { remove_items_at_frame(frame, duration, rp); }, + [=](const JsonStore &) { + remove_items_at_frame(rp, frame, duration, add_gap, collapse_gaps); + }, [=](const caf::error &err) mutable { rp.deliver(err); }); } else { // in and out split now remove items - auto first_index = std::distance(base_.item().cbegin(), in_point->first); - auto last_index = std::distance(base_.item().cbegin(), out_point->first); - remove_items(first_index, last_index - first_index, rp); + auto first_index = 0; + auto last_index = base_.item().size(); + if (in_point) + first_index = std::distance(base_.item().cbegin(), in_point->first); + if (out_point) + last_index = std::distance(base_.item().cbegin(), out_point->first); + + // spdlog::warn("remove {} {} {}", first_index, last_index, last_index - + // first_index); + remove_items(rp, first_index, last_index - first_index, add_gap, collapse_gaps); } } } } -void TrackActor::remove_items( - const int index, - const int count, - caf::typed_response_promise>> - rp) { - +std::pair> TrackActor::remove_items( + const int index, const int count, const bool add_gap, const bool collapse_gaps) { std::vector items; JsonStore changes(R"([])"_json); if (index < 0 or index + count - 1 >= static_cast(base_.item().size())) - rp.deliver(make_error(xstudio_error::error, "Invalid index / count")); + throw std::runtime_error("Invalid index / count"); else { scoped_actor sys{system()}; + auto gap_size = FrameRateDuration(); + for (int i = index + count - 1; i >= index; i--) { auto it = std::next(base_.item().begin(), i); if (it != base_.item().end()) { auto item = *it; - demonitor(item.actor()); + + if (auto mit = monitor_.find(caf::actor_cast(item.actor())); + mit != std::end(monitor_)) { + mit->second.dispose(); + monitor_.erase(mit); + } + actors_.erase(item.uuid()); // need to serialise actor.. auto blind = request_receive(*sys, item.actor(), serialise_atom_v); auto tmp = base_.item().erase(it, blind); + changes.insert(changes.end(), tmp.begin(), tmp.end()); + items.push_back(item.clone()); - changes.insert(changes.begin(), tmp.begin(), tmp.end()); - items.push_back(item); + if (add_gap) { + if (gap_size.frames()) { + gap_size += item.trimmed_frame_duration(); + } else { + gap_size = item.trimmed_frame_duration(); + } + } } } - // reverse order as we deleted back to front. - std::reverse(items.begin(), items.end()); + // add gap and index isn't last entry. + if (gap_size.frames() and index != static_cast(base_.item().size())) { + JsonStore gap_changes(R"([])"_json); + auto uuid = Uuid::generate(); + auto gap = spawn("GAP", gap_size, uuid); + // take ownership + add_item(UuidActor(uuid, gap)); + // where we're going to insert gap.. + auto it = std::next(base_.item().begin(), index); + auto item = request_receive(*sys, gap, item_atom_v); + auto blind = request_receive(*sys, gap, serialise_atom_v); + + auto tmp = base_.item().insert(it, item, blind); + + changes.insert(changes.end(), tmp.begin(), tmp.end()); + } - auto more = base_.item().refresh(); - if (not more.is_null()) - changes.insert(changes.begin(), more.begin(), more.end()); + if (collapse_gaps) { + auto more = merge_gaps(); + if (not more.is_null()) + changes.insert(changes.end(), more.begin(), more.end()); + } else { + auto more = base_.item().refresh(); + if (not more.is_null()) + changes.insert(changes.end(), more.begin(), more.end()); + } + } + std::reverse(items.begin(), items.end()); + return std::make_pair(changes, items); +} - send(event_group_, event_atom_v, item_atom_v, changes, false); - rp.deliver(std::make_pair(changes, items)); +void TrackActor::remove_items( + caf::typed_response_promise>> rp, + const int index, + const int count, + const bool add_gap, + const bool collapse_gaps) { + + try { + auto result = remove_items(index, count, add_gap, collapse_gaps); + mail(event_atom_v, item_atom_v, result.first, false).send(base_.event_group()); + rp.deliver(result); + } catch (const std::exception &err) { + rp.deliver(make_error(xstudio_error::error, err.what())); } } void TrackActor::erase_items_at_frame( - const int frame, const int duration, caf::typed_response_promise rp) { - - request( - caf::actor_cast(this), - infinite, - remove_item_at_frame_atom_v, - frame, - duration) + caf::typed_response_promise rp, + const int frame, + const int duration, + const bool add_gap, + const bool collapse_gaps) { + + mail(remove_item_at_frame_atom_v, frame, duration, add_gap, collapse_gaps) + .request(caf::actor_cast(this), infinite) .then( [=](const std::pair> &hist_item) mutable { for (const auto &i : hist_item.second) @@ -1004,25 +1172,37 @@ void TrackActor::erase_items_at_frame( } void TrackActor::erase_items( - const int index, const int count, caf::typed_response_promise rp) { + caf::typed_response_promise rp, + const int index, + const int count, + const bool add_gap, + const bool collapse_gaps) { - request(caf::actor_cast(this), infinite, remove_item_atom_v, index, count) - .then( - [=](const std::pair> &hist_item) mutable { - for (const auto &i : hist_item.second) - send_exit(i.actor(), caf::exit_reason::user_shutdown); - rp.deliver(hist_item.first); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + try { + auto result = remove_items(index, count, add_gap, collapse_gaps); + + // spdlog::warn("{}", result.first.dump(2)); + + mail(event_atom_v, item_atom_v, result.first, false).send(base_.event_group()); + for (const auto &i : result.second) + send_exit(i.actor(), caf::exit_reason::user_shutdown); + rp.deliver(result.first); + + } catch (const std::exception &err) { + rp.deliver(make_error(xstudio_error::error, err.what())); + } } void TrackActor::move_items( + caf::typed_response_promise rp, const int src_index, const int count, const int dst_index, - caf::typed_response_promise rp) { + const bool add_gap, + const bool replace_with_gap) { + // this has to deal with gap removal / consolidation if (dst_index == src_index or not count) rp.deliver(make_error(xstudio_error::error, "Invalid Move")); @@ -1031,177 +1211,387 @@ void TrackActor::move_items( auto eit = std::next(sit, count); auto dit = std::next(base_.item().begin(), dst_index); + // collect size of possible gap. + auto gap_size = FrameRateDuration(); + if (add_gap or replace_with_gap) { + for (auto it = sit; it != eit; it++) { + if (gap_size.frames()) { + gap_size += it->trimmed_frame_duration(); + } else { + gap_size = it->trimmed_frame_duration(); + } + } + } + + // move done. auto changes = base_.item().splice(dit, base_.item().children(), sit, eit); - auto more = base_.item().refresh(); + + // deal with gaps.. + auto index = src_index; + if (dst_index < src_index) + index += count; + + if (index < static_cast(base_.item().size())) { + scoped_actor sys{system()}; + + if (add_gap and gap_size.frames()) { + + JsonStore gap_changes(R"([])"_json); + + auto uuid = Uuid::generate(); + auto gap = spawn("GAP", gap_size, uuid); + // take ownership + add_item(UuidActor(uuid, gap)); + // where we're going to insert gap.. + auto it = std::next(base_.item().begin(), index); + auto item = request_receive(*sys, gap, item_atom_v); + auto blind = request_receive(*sys, gap, serialise_atom_v); + + auto tmp = base_.item().insert(it, item, blind); + changes.insert(changes.end(), tmp.begin(), tmp.end()); + } + } + + if (replace_with_gap) { + // remove moved entries, and insert gap there.. + auto adjusted_dst = dst_index; + if (dst_index > src_index) + adjusted_dst -= count; + + auto more = remove_items(adjusted_dst, count, true, false); + for (const auto &i : more.second) { + send_exit(i.actor(), caf::exit_reason::user_shutdown); + } + changes.insert(changes.end(), more.first.begin(), more.first.end()); + } + + + auto more = merge_gaps(); + if (not more.is_null()) + changes.insert(changes.end(), more.begin(), more.end()); + + // end gap code... + more = base_.item().refresh(); if (not more.is_null()) changes.insert(changes.begin(), more.begin(), more.end()); - send(event_group_, event_atom_v, item_atom_v, changes, false); + mail(event_atom_v, item_atom_v, changes, false).send(base_.event_group()); + rp.deliver(changes); } } void TrackActor::move_items_at_frame( - const int frame, - const int duration, - const int dest_frame, - const bool insert, - caf::typed_response_promise rp) { + caf::typed_response_promise rp, + const int frame_, + const int duration_, + const int dest_frame_, + const bool insert_, + const bool add_gap_, + const bool replace_with_gap_) { + + // this is gonna be complex.. + // validate input + auto frame = frame_; + auto duration = duration_; + auto dest_frame = dest_frame_; + auto insert = insert_; + auto add_gap = add_gap_; + auto replace_with_gap = replace_with_gap_; + + auto overwrite_self_start = dest_frame > frame and dest_frame < frame + duration; + auto overwrite_self_end = + dest_frame + duration > frame and dest_frame + duration < frame + duration; + auto overwrite_self = overwrite_self_start or overwrite_self_end; + + if (frame_ == dest_frame_) { + return rp.deliver(make_error(xstudio_error::error, "Invalid start frame")); + } + + if (overwrite_self_end) { + // invert logic.. + frame = dest_frame_; + duration = frame_ - dest_frame_; + dest_frame = frame_ + duration_; + insert = true; + add_gap = false; + replace_with_gap = true; + + // handle moving off beginning of track + // spdlog::warn("overwrite_self_end"); + // spdlog::warn("{} {} {}", frame_, duration_, dest_frame_); + + if (frame < 0) { + // spdlog::warn("{} {} {}", frame, duration, dest_frame); + duration += -frame - 1; + // dest_frame += frame; + frame = 0; + } + + // spdlog::warn("{} {} {}", frame, duration, dest_frame); + } else if (overwrite_self_start) { + dest_frame = frame_; + frame = frame_ + duration_; + duration = dest_frame_ - frame_; + + // spdlog::warn("overwrite_self_start"); + // spdlog::warn("{} {} {}", frame_, duration_, dest_frame_); + // spdlog::warn("{} {} {}", frame, duration, dest_frame); + insert = true; + add_gap = false; + replace_with_gap = true; + + // if were overwriting the end of the track we need to pad it. + // use track trimmed duration to work out how big a gap we need, + auto track_duration = base_.item().trimmed_frame_duration().frames(); + auto gap_duration = (frame + duration) - track_duration; + if (gap_duration > 0) { + // insert gap on track end. + auto gap_uuid = Uuid::generate(); + auto gap_actor = spawn( + "Gap", FrameRateDuration(gap_duration, base_.item().rate()), gap_uuid); + + // spdlog::warn("INSERT DEST GAP {}", gap_duration); + + // insert_items(base_.item().size(), uav_plus_gap, rp); + mail(insert_item_atom_v, Uuid(), UuidActorVector({UuidActor(gap_uuid, gap_actor)})) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + move_items_at_frame( + rp, + frame_, + duration_, + dest_frame_, + insert_, + add_gap_, + replace_with_gap_); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + + return; + } + // spdlog::warn("gap_duration {}", gap_duration); + } + + auto start = base_.item().item_at_frame(frame); - // don't support moving in to move range.. - if (dest_frame >= frame and dest_frame <= frame + duration) - rp.deliver(make_error(xstudio_error::error, "Invalid move")); + if (not start) + rp.deliver(make_error(xstudio_error::error, "Invalid start frame")); else { - // this is gonna be complex.. - // validate input - auto start = base_.item().item_at_frame(frame); - - if (not start) - rp.deliver(make_error(xstudio_error::error, "Invalid start frame")); - else { - // split at start ? - if (start->first->trimmed_frame_start().frames() != start->second) { - // split start item - request( - caf::actor_cast(this), - infinite, + // spdlog::warn( + // "check source start {} {} {}", + // frame, + // start->first->trimmed_frame_start().frames(), + // start->second); + + // split at start ? + if (start and start->first->trimmed_frame_start().frames() != start->second) { + // split start item + // spdlog::warn( + // "split source index {} at frame {} (start)", + // static_cast(std::distance(base_.item().cbegin(), start->first)), + // start->second); + mail( + split_item_atom_v, + static_cast(std::distance(base_.item().cbegin(), start->first)), + start->second) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + move_items_at_frame( + rp, frame, duration, dest_frame, insert, add_gap, replace_with_gap); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + + } else { + // split at end frame ? + auto end = base_.item().item_at_frame(frame + duration); + // if (end) + // spdlog::warn( + // "check source end {} {} {}", + // frame + duration, + // end->first->trimmed_frame_start().frames(), + // end->second); + // else + // spdlog::warn("check source end {}", frame + duration); + + if (end and end->first->trimmed_frame_start().frames() != end->second) { + // split end item + // spdlog::warn( + // "split source index {} at frame {} (end)", + // static_cast(std::distance(base_.item().cbegin(), end->first)), + // end->second); + + mail( split_item_atom_v, - static_cast(std::distance(base_.item().cbegin(), start->first)), - start->second) + static_cast(std::distance(base_.item().cbegin(), end->first)), + end->second) + .request(caf::actor_cast(this), infinite) .then( [=](const JsonStore &) mutable { - move_items_at_frame(frame, duration, dest_frame, insert, rp); + move_items_at_frame( + rp, + frame, + duration, + dest_frame, + insert, + add_gap, + replace_with_gap); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); - } else { - // split at end frame ? - auto end = base_.item().item_at_frame(frame + duration); - - if (end->first->trimmed_frame_start().frames() != end->second) { - // split end item - request( - caf::actor_cast(this), - infinite, + // move to frame should insert gap, but we might need to split.. + // either split or inject end.. + // dest might be off end of track which is still valid.. + auto dest = base_.item().item_at_frame(dest_frame); + + // if (dest) + // spdlog::warn( + // "check dest start {} {} {}", + // dest_frame, + // dest->first->trimmed_frame_start().frames(), + // dest->second); + + if (dest and dest->first->trimmed_frame_start().frames() != dest->second) { + // spdlog::warn( + // "split dest index {} at frame {} (start)", + // static_cast(std::distance(base_.item().cbegin(), dest->first)), + // dest->second); + + // split dest start + mail( split_item_atom_v, - static_cast(std::distance(base_.item().cbegin(), end->first)), - end->second) + static_cast(std::distance(base_.item().cbegin(), dest->first)), + dest->second) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + move_items_at_frame( + rp, + frame, + duration, + dest_frame, + insert, + add_gap, + replace_with_gap); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); + } else if ( + not dest and base_.item().trimmed_frame_duration().frames() != dest_frame) { + // check for off end as we'll need gap.. + auto track_end = base_.item().trimmed_frame_start().frames() + + base_.item().trimmed_frame_duration().frames() - 1; + auto filler = dest_frame - track_end - 1; + auto gap_uuid = Uuid::generate(); + auto gap_actor = spawn( + "Gap", FrameRateDuration(filler, base_.item().rate()), gap_uuid); + + // spdlog::warn("INSERT GAP {}", filler); + + // insert_items(base_.item().size(), uav_plus_gap, rp); + mail( + insert_item_atom_v, + Uuid(), + UuidActorVector({UuidActor(gap_uuid, gap_actor)})) + .request(caf::actor_cast(this), infinite) .then( [=](const JsonStore &) mutable { - move_items_at_frame(frame, duration, dest_frame, insert, rp); + move_items_at_frame( + rp, + frame, + duration, + dest_frame, + true, + add_gap, + replace_with_gap); }, [=](error &err) mutable { rp.deliver(std::move(err)); }); } else { - // move to frame should insert gap, but we might need to split.. - // either split or inject end.. - // dest might be off end of track which is still valid.. - auto dest = base_.item().item_at_frame(dest_frame); - - if (dest and dest->first->trimmed_frame_start().frames() != dest->second) { - - // split dest.. - request( - caf::actor_cast(this), - infinite, - split_item_atom_v, - static_cast(std::distance(base_.item().cbegin(), dest->first)), - dest->second) - .then( - [=](const JsonStore &) mutable { - move_items_at_frame( - frame, duration, dest_frame, insert, rp); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); - } else if ( - not dest and - base_.item().trimmed_frame_duration().frames() != dest_frame) { - - // check for off end as we'll need gap.. - auto track_end = base_.item().trimmed_frame_start().frames() + - base_.item().trimmed_frame_duration().frames() - 1; - auto filler = dest_frame - track_end - 1; - auto gap_uuid = utility::Uuid::generate(); - auto gap_actor = spawn( - "Gap", FrameRateDuration(filler, base_.item().rate()), gap_uuid); - - // insert_items(base_.item().size(), uav_plus_gap, rp); - request( - caf::actor_cast(this), - infinite, - insert_item_atom_v, - static_cast(base_.item().size()), - UuidActorVector({UuidActor(gap_uuid, gap_actor)})) - .then( - [=](const JsonStore &) mutable { - move_items_at_frame( - frame, duration, dest_frame, insert, rp); - }, - [=](error &err) mutable { rp.deliver(std::move(err)); }); + // finally ready.. + if (insert or + (not dest and + base_.item().trimmed_frame_duration().frames() == dest_frame)) { + + auto index = std::distance(base_.item().cbegin(), start->first); + auto end_index = end ? std::distance(base_.item().cbegin(), end->first) + : base_.item().size(); + auto count = + end_index - std::distance(base_.item().cbegin(), start->first); + auto dst = dest ? std::distance(base_.item().cbegin(), dest->first) + : base_.item().size(); + + // spdlog::warn("move_items index {} count {} dst {}", index, count, + // dst); + + move_items(rp, index, count, dst, add_gap, replace_with_gap); } else { - // finally ready.. - if (insert or - (not dest and - base_.item().trimmed_frame_duration().frames() == dest_frame)) { - auto index = std::distance(base_.item().cbegin(), start->first); - auto count = std::distance(base_.item().cbegin(), end->first) - - std::distance(base_.item().cbegin(), start->first); - auto dst = dest ? std::distance(base_.item().cbegin(), dest->first) - : base_.item().size(); - - move_items(index, count, dst, rp); + // we need to remove material at destnation + // we may need to split at dst+duration + auto dst_end = base_.item().item_at_frame(dest_frame + duration); + + // if(dst_end) + // spdlog::warn( + // "check dest end {} {} {}", + // dest_frame + duration, + // dst_end->first->trimmed_frame_start().frames(), + // dst_end->second); + + if (dst_end and + dst_end->first->trimmed_frame_start().frames() != dst_end->second) { + // we need to split.. + // split dest.. + // spdlog::warn( + // "split dest index {} at frame {} (end)", + // static_cast( + // std::distance(base_.item().cbegin(), dst_end->first)), + // dst_end->second); + + mail( + split_item_atom_v, + static_cast( + std::distance(base_.item().cbegin(), dst_end->first)), + dst_end->second) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + move_items_at_frame( + rp, + frame, + duration, + dest_frame, + insert, + add_gap, + replace_with_gap); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); } else { - // we need to remove material at destnation - // we may need to split at dst+duration - auto dst_end = base_.item().item_at_frame(dest_frame + duration); - if (dst_end) { - // we need to split.. - // split dest.. - request( - caf::actor_cast(this), - infinite, - split_item_atom_v, - static_cast( - std::distance(base_.item().cbegin(), dst_end->first)), - dst_end->second) - .then( - [=](const JsonStore &) mutable { - move_items_at_frame( - frame, duration, dest_frame, insert, rp); - }, - [=](error &err) mutable { - rp.deliver(std::move(err)); - }); - } else { - // move and prune.. - int move_from_frame = frame; - int move_to_frame = dest_frame + duration; - - // if we remove from in front of start we need to adjust move - // ranges. - if (dest_frame < frame) { - move_from_frame -= duration; - move_to_frame -= duration; - } - - request( - caf::actor_cast(this), - infinite, - erase_item_at_frame_atom_v, - static_cast(dest_frame + duration), - duration) - .then( - [=](const JsonStore &) mutable { - move_items_at_frame( - move_from_frame, - duration, - move_to_frame, - false, - rp); - }, - [=](error &err) mutable { - rp.deliver(std::move(err)); - }); + // move and prune.. + int move_from_frame = frame; + int move_to_frame = dest_frame; + + // if we remove from in front of start we need to adjust move + // ranges. + if (dest_frame < frame) { + move_from_frame -= duration; } + // spdlog::warn("ERASE {} {}", dest_frame, duration); + + // replace source with gap ? + mail(erase_item_at_frame_atom_v, dest_frame, duration, false, false) + .request(caf::actor_cast(this), infinite) + .then( + [=](const JsonStore &) mutable { + move_items_at_frame( + rp, + move_from_frame, + duration, + move_to_frame, + true, + add_gap, + replace_with_gap); + }, + [=](error &err) mutable { rp.deliver(std::move(err)); }); } } } @@ -1210,25 +1600,119 @@ void TrackActor::move_items_at_frame( } } +JsonStore TrackActor::merge_gaps() { + JsonStore changes(R"([])"_json); + scoped_actor sys{system()}; + + // purge trailing gaps also handles track of gap + while (not base_.item().empty() and base_.item().back().item_type() == IT_GAP) { + // prune gaps off end of track. + // spdlog::warn("ERASE TRAILING GAP {}", base_.item().size() - 1); + + auto it = std::next(base_.item().begin(), base_.item().size() - 1); + auto item = *it; + + if (auto mit = monitor_.find(caf::actor_cast(item.actor())); + mit != std::end(monitor_)) { + mit->second.dispose(); + monitor_.erase(mit); + } + + actors_.erase(item.uuid()); + + // need to serialise actor.. + auto blind = request_receive(*sys, item.actor(), serialise_atom_v); + auto tmp = base_.item().erase(it, blind); + send_exit(item.actor(), caf::exit_reason::user_shutdown); + changes.insert(changes.end(), tmp.begin(), tmp.end()); + } + + auto gap_count = 0; + auto gap_duration = FrameRateDuration(); + // look for multiple sequential gaps. + int index = static_cast(base_.item().size()) - 1; + + auto merge_gaps_ = [&](auto it) mutable { + if (gap_count) { + if (gap_count > 1) { + // spdlog::warn("merging {}", gap_count); + scoped_actor sys{system()}; + + auto uuid = Uuid::generate(); + auto gap = spawn("GAP", gap_duration, uuid); + + // take ownership + add_item(UuidActor(uuid, gap)); + + // where we're going to insert gap.. + auto item = request_receive(*sys, gap, item_atom_v); + auto blind = request_receive(*sys, gap, serialise_atom_v); + + auto tmp = base_.item().insert(it, item, blind); + changes.insert(changes.end(), tmp.begin(), tmp.end()); + + while (gap_count) { + // get distance from start. + auto gip = std::next(it, gap_count - 1); + + // spdlog::warn("remove index {}", std::distance(base_.item().begin(), + // gip)); + + auto item = *gip; + + if (auto mit = + monitor_.find(caf::actor_cast(item.actor())); + mit != std::end(monitor_)) { + mit->second.dispose(); + monitor_.erase(mit); + } + + actors_.erase(item.uuid()); + + // need to serialise actor.. + auto blind = + request_receive(*sys, item.actor(), serialise_atom_v); + auto tmp = base_.item().erase(gip, blind); + send_exit(item.actor(), caf::exit_reason::user_shutdown); + changes.insert(changes.end(), tmp.begin(), tmp.end()); + + gap_count--; + } + } else + gap_count = 0; + } + }; + + for (; index >= 0; index--) { + auto it = std::next(base_.item().begin(), index); + + if (it->item_type() == IT_GAP) { + if (not gap_count) + gap_duration = it->trimmed_frame_duration(); + else + gap_duration += it->trimmed_frame_duration(); + + gap_count++; + } else { + merge_gaps_(std::next(it, 1)); + } + } + + merge_gaps_(base_.item().begin()); + + auto more = base_.item().refresh(); + if (not more.is_null()) + changes.insert(changes.end(), more.begin(), more.end()); + + return changes; +} + +// iterate over track removing surplus gaps +void TrackActor::merge_gaps(caf::typed_response_promise rp) { + auto changes = merge_gaps(); + + if (changes.size()) + mail(event_atom_v, item_atom_v, changes, false).send(base_.event_group()); -// void TrackActor::deliver_media_pointer( -// const int logical_frame, caf::typed_response_promise rp) { -// // should be able to use edit_list ? -// try { -// int clip_frame = 0; -// utility::EditListSection clip_at_frame = -// edit_list_.media_frame(logical_frame, clip_frame); -// // send request media atom.. -// request( -// actors_[clip_at_frame.media_uuid_], -// infinite, -// media::get_media_pointer_atom_v, -// clip_frame) -// .then( -// [=](const media::AVFrameID &mp) mutable { rp.deliver(mp); }, -// [=](error &err) mutable { rp.deliver(std::move(err)); }); - -// } catch (const std::exception &e) { -// rp.deliver(make_error(xstudio_error::error, e.what())); -// } -// } + rp.deliver(changes); +} \ No newline at end of file diff --git a/src/timeline/test/CMakeLists.txt b/src/timeline/test/CMakeLists.txt index fda89e8ea..2148fc9b9 100644 --- a/src/timeline/test/CMakeLists.txt +++ b/src/timeline/test/CMakeLists.txt @@ -1,7 +1,7 @@ include(CTest) SET(LINK_DEPS - caf::core + CAF::core xstudio::global xstudio::playlist xstudio::timeline diff --git a/src/timeline/test/clip_test.cpp b/src/timeline/test/clip_test.cpp index b9ac681eb..7e7945643 100644 --- a/src/timeline/test/clip_test.cpp +++ b/src/timeline/test/clip_test.cpp @@ -21,6 +21,23 @@ TEST(ClipTest, Test) { // check initial value EXPECT_EQ(c.item().trimmed_range().duration(), timebase::k_flicks_zero_seconds); + EXPECT_EQ(c.item().top_left().first, 0); + EXPECT_EQ(c.item().top_left().second, 0); + + EXPECT_EQ(c.item().bottom_right().first, 0); + EXPECT_EQ(c.item().bottom_right().second, 1); + + c.item().set_available_range(FrameRange( + FrameRateDuration(1, timebase::k_flicks_24fps), + FrameRateDuration(10, timebase::k_flicks_24fps))); + + EXPECT_EQ(c.item().top_left().first, 0); + EXPECT_EQ(c.item().top_left().second, 0); + + EXPECT_EQ(c.item().bottom_right().first, 10); + EXPECT_EQ(c.item().bottom_right().second, 1); + + Clip c2(c.serialise()); EXPECT_EQ(c2.item().uuid(), c.uuid()); } diff --git a/src/timeline/test/stack_actor_test.cpp b/src/timeline/test/stack_actor_test.cpp index 595c82377..84bd132f4 100644 --- a/src/timeline/test/stack_actor_test.cpp +++ b/src/timeline/test/stack_actor_test.cpp @@ -130,7 +130,7 @@ TEST(NestedStackActorTest, Test) { auto s = f.self->spawn("Stack"); auto cuuid = Uuid::generate(); - auto c = f.self->spawn("ChildStack", cuuid); + auto c = f.self->spawn("ChildStack", utility::FrameRate(), cuuid); { auto result = request_receive( *(f.self), s, insert_item_atom_v, -1, UuidActorVector({UuidActor(cuuid, c)})); @@ -195,7 +195,8 @@ TEST(NestedTrackStackActorTest, Test) { auto s = f.self->spawn("Stack"); auto cuuid = Uuid::generate(); - auto c = f.self->spawn("ChildTrack", media::MediaType::MT_IMAGE, cuuid); + auto c = f.self->spawn( + "ChildTrack", utility::FrameRate(), media::MediaType::MT_IMAGE, cuuid); { auto result = request_receive( *(f.self), s, insert_item_atom_v, -1, UuidActorVector({UuidActor(cuuid, c)})); @@ -257,8 +258,9 @@ TEST(StackActorAddTest, Test) { auto t = f.self->spawn("Test Stack"); { - auto uuid = utility::Uuid::generate(); - auto invalid = f.self->spawn("Invalid Timeline", uuid); + auto uuid = utility::Uuid::generate(); + auto invalid = + f.self->spawn("Invalid Timeline", utility::FrameRate(), uuid); EXPECT_THROW( request_receive( *(f.self), @@ -271,14 +273,15 @@ TEST(StackActorAddTest, Test) { } { auto uuid = utility::Uuid::generate(); - auto valid = f.self->spawn("Valid Track", media::MediaType::MT_IMAGE, uuid); + auto valid = f.self->spawn( + "Valid Track", utility::FrameRate(), media::MediaType::MT_IMAGE, uuid); EXPECT_NO_THROW(request_receive( *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); } { auto uuid = utility::Uuid::generate(); - auto valid = f.self->spawn("Valid Stack", uuid); + auto valid = f.self->spawn("Valid Stack", utility::FrameRate(), uuid); EXPECT_NO_THROW(request_receive( *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); } @@ -315,25 +318,29 @@ TEST(StackActorMoveTest2, Test) { { auto uuid = utility::Uuid::generate(); - auto valid = f.self->spawn("Track 4", media::MediaType::MT_IMAGE, uuid); + auto valid = f.self->spawn( + "Track 4", utility::FrameRate(), media::MediaType::MT_IMAGE, uuid); EXPECT_NO_THROW(request_receive( *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); } { auto uuid = utility::Uuid::generate(); - auto valid = f.self->spawn("Track 3", media::MediaType::MT_IMAGE, uuid); + auto valid = f.self->spawn( + "Track 3", utility::FrameRate(), media::MediaType::MT_IMAGE, uuid); EXPECT_NO_THROW(request_receive( *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); } { auto uuid = utility::Uuid::generate(); - auto valid = f.self->spawn("Track 2", media::MediaType::MT_IMAGE, uuid); + auto valid = f.self->spawn( + "Track 2", utility::FrameRate(), media::MediaType::MT_IMAGE, uuid); EXPECT_NO_THROW(request_receive( *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); } { auto uuid = utility::Uuid::generate(); - auto valid = f.self->spawn("Track 1", media::MediaType::MT_IMAGE, uuid); + auto valid = f.self->spawn( + "Track 1", utility::FrameRate(), media::MediaType::MT_IMAGE, uuid); EXPECT_NO_THROW(request_receive( *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); } diff --git a/src/timeline/test/stack_test.cpp b/src/timeline/test/stack_test.cpp index 3459dbdb9..773e1e095 100644 --- a/src/timeline/test/stack_test.cpp +++ b/src/timeline/test/stack_test.cpp @@ -3,7 +3,9 @@ #include "xstudio/media/media_actor.hpp" #include "xstudio/timeline/stack.hpp" +#include "xstudio/timeline/track.hpp" #include "xstudio/timeline/gap.hpp" +#include "xstudio/timeline/clip.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/uuid.hpp" @@ -152,6 +154,36 @@ TEST(StackTest, Test) { EXPECT_FALSE(s2.item().children().front().enabled()); } +TEST(StackBoxTest, Test) { + Stack s; + + auto g1 = Gap("Gap1", utility::FrameRateDuration(10, timebase::k_flicks_24fps)); + auto g2 = Gap("Gap2", utility::FrameRateDuration(15, timebase::k_flicks_24fps)); + s.item().push_back(g1.item()); + s.item().push_back(g2.item()); + + s.item().refresh(); + + auto box = s.item().box(); + + EXPECT_EQ(box.first.first, 0); + EXPECT_EQ(box.first.second, 0); + EXPECT_EQ(box.second.first, 15); + EXPECT_EQ(box.second.second, 2); + + auto cbox = s.item().box(g1.uuid()); + EXPECT_EQ(cbox->first.first, 0); + EXPECT_EQ(cbox->first.second, 0); + EXPECT_EQ(cbox->second.first, 10); + EXPECT_EQ(cbox->second.second, 1); + + cbox = s.item().box(g2.uuid()); + EXPECT_EQ(cbox->first.first, 0); + EXPECT_EQ(cbox->first.second, 1); + EXPECT_EQ(cbox->second.first, 15); + EXPECT_EQ(cbox->second.second, 2); +} + TEST(StackRefreshTest, Test) { Stack s; @@ -185,3 +217,234 @@ TEST(StackRefreshTest, Test) { // we should be back to original state.. EXPECT_FALSE(s.item().available_range()); } + +TEST(StackResolveTest, Test) { + Stack s; + + auto id1 = FrameRange( + utility::FrameRateDuration(0, timebase::k_flicks_24fps), + utility::FrameRateDuration(10, timebase::k_flicks_24fps)); + auto i1 = Clip("Clip1"); + i1.item().set_available_range(id1); + + auto id2 = FrameRange( + utility::FrameRateDuration(0, timebase::k_flicks_24fps), + utility::FrameRateDuration(15, timebase::k_flicks_24fps)); + auto i2 = Clip("Clip2"); + i2.item().set_available_range(id2); + + auto id3 = utility::FrameRateDuration(20, timebase::k_flicks_24fps); + auto i3 = Gap("Gap1", id3); + + s.item().insert(s.item().end(), i1.item()); + s.item().insert(s.item().end(), i2.item()); + s.item().insert(s.item().end(), i3.item()); + + // we've added gaps so we need to refresh our state. + s.item().refresh(1); + + auto r = s.item().resolve_time(timebase::k_flicks_24fps * 0, media::MediaType::MT_IMAGE); + EXPECT_TRUE(r); + if (r) { + auto [i, t] = *r; + EXPECT_EQ(i.uuid(), i1.item().uuid()); + } + + + auto focus = utility::UuidSet({i2.item().uuid()}); + + // focus test + r = s.item().resolve_time(timebase::k_flicks_24fps * 0, media::MediaType::MT_IMAGE, focus); + EXPECT_TRUE(r); + if (r) { + auto [i, t] = *r; + EXPECT_EQ(i.uuid(), i2.item().uuid()); + } + + // return normally if no focus match + focus = utility::UuidSet({utility::Uuid::generate()}); + r = s.item().resolve_time(timebase::k_flicks_24fps * 0, media::MediaType::MT_IMAGE, focus); + EXPECT_TRUE(r); + if (r) { + auto [i, t] = *r; + EXPECT_EQ(i.uuid(), i1.item().uuid()); + } + + + // test with focus not matching + auto rr = s.item().resolve_time_raw( + timebase::k_flicks_24fps * 0, media::MediaType::MT_IMAGE, focus); + EXPECT_FALSE(rr.empty()); + EXPECT_EQ(rr.size(), 2); + + EXPECT_EQ(std::get<0>(rr[0]).uuid(), i1.item().uuid()); + EXPECT_EQ(std::get<0>(rr[1]).uuid(), i2.item().uuid()); + + // test with focus matching + focus = utility::UuidSet({i2.item().uuid()}); + rr = s.item().resolve_time_raw( + timebase::k_flicks_24fps * 0, media::MediaType::MT_IMAGE, focus); + EXPECT_FALSE(rr.empty()); + EXPECT_EQ(rr.size(), 1); + + EXPECT_EQ(std::get<0>(rr[0]).uuid(), i2.item().uuid()); + + // test with range matching one even with focus matching, but it's off the end + focus = utility::UuidSet({i1.item().uuid()}); + rr = s.item().resolve_time_raw( + timebase::k_flicks_24fps * 14, media::MediaType::MT_IMAGE, focus); + EXPECT_FALSE(rr.empty()); + EXPECT_EQ(rr.size(), 1); + EXPECT_EQ(std::get<0>(rr[0]).uuid(), i2.item().uuid()); +} + +TEST(StackTrackResolveTest, Test) { + Stack s; + + Track tv1("Track V1", MediaType::MT_IMAGE); + Track tv2("Track V2", MediaType::MT_IMAGE); + Track tv3("Track V3", MediaType::MT_IMAGE); + + Track ta1("Track A1", MediaType::MT_AUDIO); + Track ta2("Track A2", MediaType::MT_AUDIO); + Track ta3("Track A3", MediaType::MT_AUDIO); + + auto cd1 = FrameRange( + utility::FrameRateDuration(0, timebase::k_flicks_24fps), + utility::FrameRateDuration(10, timebase::k_flicks_24fps)); + auto c1 = Clip("Clip1"); + c1.item().set_available_range(cd1); + + auto c3 = Clip("Clip3"); + c3.item().set_available_range(cd1); + + auto c4 = Clip("Clip4"); + c4.item().set_available_range(cd1); + + auto cd2 = FrameRange( + utility::FrameRateDuration(0, timebase::k_flicks_24fps), + utility::FrameRateDuration(15, timebase::k_flicks_24fps)); + auto c2 = Clip("Clip2"); + c2.item().set_available_range(cd2); + + auto gd1 = utility::FrameRateDuration(20, timebase::k_flicks_24fps); + auto g1 = Gap("Gap1", gd1); + auto g2 = Gap("Gap1", gd1); + + + tv1.item().insert(tv1.item().end(), c1.item()); + tv1.item().insert(tv1.item().end(), g1.item()); + tv1.item().insert(tv1.item().end(), c2.item()); + tv1.item().refresh(1); + + tv2.item().insert(tv2.item().end(), c3.item()); + tv2.item().insert(tv2.item().end(), g2.item()); + tv2.item().insert(tv2.item().end(), c4.item()); + tv2.item().refresh(1); + + tv3.item().refresh(1); + + ta1.item().refresh(1); + ta2.item().insert(ta2.item().end(), c1.item()); + ta2.item().insert(ta2.item().end(), g1.item()); + ta2.item().insert(ta2.item().end(), c2.item()); + ta2.item().refresh(1); + ta3.item().refresh(1); + + s.item().insert(s.item().end(), tv1.item()); + s.item().insert(s.item().end(), tv2.item()); + s.item().insert(s.item().end(), tv3.item()); + s.item().insert(s.item().end(), ta1.item()); + s.item().insert(s.item().end(), ta2.item()); + s.item().insert(s.item().end(), ta3.item()); + s.item().refresh(); + + auto rr = + s.item().resolve_time_raw(timebase::k_flicks_24fps * 0, media::MediaType::MT_IMAGE); + EXPECT_FALSE(rr.empty()); + EXPECT_EQ(rr.size(), 2); + + rr = s.item().resolve_time_raw(timebase::k_flicks_24fps * 0, media::MediaType::MT_AUDIO); + EXPECT_FALSE(rr.empty()); + EXPECT_EQ(rr.size(), 1); + + auto r = s.item().resolve_time( + timebase::k_flicks_24fps * 0, media::MediaType::MT_IMAGE, utility::UuidSet({}), true); + EXPECT_FALSE(r); + + r = s.item().resolve_time( + timebase::k_flicks_24fps * 0, + media::MediaType::MT_IMAGE, + utility::UuidSet({c1.uuid()}), + true); + EXPECT_TRUE(r); + EXPECT_EQ(std::get<0>(*r).uuid(), c1.uuid()); + + r = s.item().resolve_time( + timebase::k_flicks_24fps * 11, + media::MediaType::MT_IMAGE, + utility::UuidSet({c1.uuid()}), + true); + EXPECT_FALSE(r); + + r = s.item().resolve_time( + timebase::k_flicks_24fps * 0, + media::MediaType::MT_IMAGE, + utility::UuidSet({tv1.uuid()}), + true); + EXPECT_TRUE(r); + EXPECT_EQ(std::get<0>(*r).uuid(), c1.uuid()); + + r = s.item().resolve_time( + timebase::k_flicks_24fps * 11, + media::MediaType::MT_IMAGE, + utility::UuidSet({tv1.uuid()}), + true); + EXPECT_FALSE(r); + + r = s.item().resolve_time( + timebase::k_flicks_24fps * 0, + media::MediaType::MT_IMAGE, + utility::UuidSet({tv2.uuid()}), + true); + EXPECT_TRUE(r); + EXPECT_EQ(std::get<0>(*r).uuid(), c3.uuid()); + + r = s.item().resolve_time( + timebase::k_flicks_24fps * 0, + media::MediaType::MT_IMAGE, + utility::UuidSet({tv2.uuid()}), + true); + EXPECT_TRUE(r); + EXPECT_EQ(std::get<0>(*r).uuid(), c3.uuid()); + + r = s.item().resolve_time( + timebase::k_flicks_24fps * 0, + media::MediaType::MT_IMAGE, + utility::UuidSet({c3.uuid()}), + true); + EXPECT_TRUE(r); + EXPECT_EQ(std::get<0>(*r).uuid(), c3.uuid()); + + r = s.item().resolve_time( + timebase::k_flicks_24fps * 0, + media::MediaType::MT_IMAGE, + utility::UuidSet({c4.uuid()}), + true); + EXPECT_FALSE(r); + r = s.item().resolve_time( + timebase::k_flicks_24fps * 1, + media::MediaType::MT_IMAGE, + utility::UuidSet({c3.uuid()}), + true); + EXPECT_TRUE(r); + EXPECT_EQ(std::get<0>(*r).uuid(), c3.uuid()); + + r = s.item().resolve_time( + timebase::k_flicks_24fps * 36, + media::MediaType::MT_IMAGE, + utility::UuidSet({c4.uuid()}), + true); + EXPECT_TRUE(r); + EXPECT_EQ(std::get<0>(*r).uuid(), c4.uuid()); +} diff --git a/src/timeline/test/timeline_actor_test.cpp b/src/timeline/test/timeline_actor_test.cpp index 7c763d7c5..75d0b0e59 100644 --- a/src/timeline/test/timeline_actor_test.cpp +++ b/src/timeline/test/timeline_actor_test.cpp @@ -60,8 +60,9 @@ TEST(TimelineActorHistoryTest, Test) { // add track to stack uuid.generate_in_place(); - auto track1 = f.self->spawn("Track-001", media::MediaType::MT_IMAGE, uuid); - auto hist1 = request_receive( + auto track1 = f.self->spawn( + "Track-001", utility::FrameRate(), media::MediaType::MT_IMAGE, uuid); + auto hist1 = request_receive( *(f.self), stack1, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, track1)})); // add clip to track.. @@ -79,7 +80,7 @@ TEST(TimelineActorHistoryTest, Test) { // stack uuid.generate_in_place(); - auto stack2 = f.self->spawn("Nested Stack-002", uuid); + auto stack2 = f.self->spawn("Nested Stack-002", utility::FrameRate(), uuid); result = request_receive( *(f.self), track1, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, stack2)})); result = request_receive( @@ -112,14 +113,14 @@ TEST(TimelineActorHistoryTest, Test) { // now populate nested stack uuid.generate_in_place(); - auto track2 = - f.self->spawn("Nested Track-002", media::MediaType::MT_IMAGE, uuid); + auto track2 = f.self->spawn( + "Nested Track-002", utility::FrameRate(), media::MediaType::MT_IMAGE, uuid); result = request_receive( *(f.self), stack2, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, track2)})); uuid.generate_in_place(); - auto track3 = - f.self->spawn("Nested Track-003", media::MediaType::MT_IMAGE, uuid); + auto track3 = f.self->spawn( + "Nested Track-003", utility::FrameRate(), media::MediaType::MT_IMAGE, uuid); result = request_receive( *(f.self), stack2, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, track3)})); result = request_receive( @@ -312,7 +313,8 @@ TEST(TimelineActorChildTest, Test) { // add track to stack uuid.generate_in_place(); - auto track1 = f.self->spawn("Track-001", media::MediaType::MT_IMAGE, uuid); + auto track1 = f.self->spawn( + "Track-001", utility::FrameRate(), media::MediaType::MT_IMAGE, uuid); auto result = request_receive( *(f.self), stack1, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, track1)})); @@ -331,7 +333,7 @@ TEST(TimelineActorChildTest, Test) { // stack uuid.generate_in_place(); - auto stack2 = f.self->spawn("Nested Stack-002", uuid); + auto stack2 = f.self->spawn("Nested Stack-002", utility::FrameRate(), uuid); result = request_receive( *(f.self), track1, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, stack2)})); result = request_receive( @@ -364,14 +366,14 @@ TEST(TimelineActorChildTest, Test) { // now populate nested stack uuid.generate_in_place(); - auto track2 = - f.self->spawn("Nested Track-002", media::MediaType::MT_IMAGE, uuid); + auto track2 = f.self->spawn( + "Nested Track-002", utility::FrameRate(), media::MediaType::MT_IMAGE, uuid); result = request_receive( *(f.self), stack2, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, track2)})); uuid.generate_in_place(); - auto track3 = - f.self->spawn("Nested Track-003", media::MediaType::MT_IMAGE, uuid); + auto track3 = f.self->spawn( + "Nested Track-003", utility::FrameRate(), media::MediaType::MT_IMAGE, uuid); result = request_receive( *(f.self), stack2, insert_item_atom_v, -1, UuidActorVector({UuidActor(uuid, track3)})); result = request_receive( diff --git a/src/timeline/test/track_actor_test.cpp b/src/timeline/test/track_actor_test.cpp index 5861deb7d..1f422a6b4 100644 --- a/src/timeline/test/track_actor_test.cpp +++ b/src/timeline/test/track_actor_test.cpp @@ -30,9 +30,9 @@ TEST(TrackActorAddTest, Test) { auto t = f.self->spawn("Top Track"); { - auto uuid = utility::Uuid::generate(); - auto invalid = - f.self->spawn("Invalid Track", media::MediaType::MT_IMAGE, uuid); + auto uuid = utility::Uuid::generate(); + auto invalid = f.self->spawn( + "Invalid Track", utility::FrameRate(), media::MediaType::MT_IMAGE, uuid); EXPECT_THROW( request_receive( *(f.self), @@ -45,8 +45,9 @@ TEST(TrackActorAddTest, Test) { } { - auto uuid = utility::Uuid::generate(); - auto invalid = f.self->spawn("Invalid Timeline", uuid); + auto uuid = utility::Uuid::generate(); + auto invalid = + f.self->spawn("Invalid Timeline", utility::FrameRate(), uuid); EXPECT_THROW( request_receive( *(f.self), @@ -60,7 +61,7 @@ TEST(TrackActorAddTest, Test) { { auto uuid = utility::Uuid::generate(); - auto valid = f.self->spawn("Valid Stack", uuid); + auto valid = f.self->spawn("Valid Stack", utility::FrameRate(), uuid); EXPECT_NO_THROW(request_receive( *(f.self), t, insert_item_atom_v, 0, UuidActorVector({UuidActor(uuid, valid)}))); } @@ -82,14 +83,14 @@ TEST(TrackActorAddTest, Test) { auto item = request_receive(*(f.self), t, item_atom_v); EXPECT_EQ(item.size(), 3); - EXPECT_NO_THROW(request_receive(*(f.self), t, erase_item_atom_v, 0)); + EXPECT_NO_THROW(request_receive(*(f.self), t, erase_item_atom_v, 0, false)); item = request_receive(*(f.self), t, item_atom_v); EXPECT_EQ(item.size(), 2); auto jitem = std::pair>(); EXPECT_NO_THROW( (jitem = request_receive>>( - *(f.self), t, remove_item_atom_v, 0))); + *(f.self), t, remove_item_atom_v, 0, false))); EXPECT_EQ(jitem.second[0].item_type(), ItemType::IT_GAP); f.self->send_exit(jitem.second[0].actor(), caf::exit_reason::user_shutdown); diff --git a/src/timeline/test/track_test.cpp b/src/timeline/test/track_test.cpp index 3d2e429e1..d4d602cf6 100644 --- a/src/timeline/test/track_test.cpp +++ b/src/timeline/test/track_test.cpp @@ -3,7 +3,9 @@ #include "xstudio/media/media_actor.hpp" #include "xstudio/timeline/track.hpp" +#include "xstudio/timeline/stack.hpp" #include "xstudio/timeline/gap.hpp" +#include "xstudio/timeline/clip.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/uuid.hpp" @@ -77,6 +79,68 @@ std::pair doMoveTest(Track &t, int start, int count, int b EXPECT_EQ((*(t.item().item_at_index(3)))->name(), "Gap 4"); +TEST(TrackShapeTest, Test) { + Track t("Track", MediaType::MT_IMAGE); + + auto gap1 = Gap("Gap 1", utility::FrameRateDuration(10, timebase::k_flicks_24fps)); + auto gap2 = Gap("Gap 2", utility::FrameRateDuration(10, timebase::k_flicks_24fps)); + + t.children().push_back(gap1.item()); + t.children().push_back(gap2.item()); + + t.refresh_item(); + + auto box = t.item().box(); + EXPECT_EQ(box.first.first, 0); + EXPECT_EQ(box.first.second, 0); + + EXPECT_EQ(box.second.first, 20); + EXPECT_EQ(box.second.second, 1); + + auto s = Stack("Stack"); + + auto sgap1 = Gap("Stack Gap 1", utility::FrameRateDuration(10, timebase::k_flicks_24fps)); + auto sgap2 = Gap("Stack Gap 2", utility::FrameRateDuration(10, timebase::k_flicks_24fps)); + + s.children().push_back(sgap1.item()); + s.children().push_back(sgap2.item()); + s.refresh_item(); + + t.children().push_back(s.item()); + t.refresh_item(); + + box = t.item().box(); + EXPECT_EQ(box.first.first, 0); + EXPECT_EQ(box.first.second, 0); + EXPECT_EQ(box.second.first, 30); + EXPECT_EQ(box.second.second, 2); + + auto cbox = t.item().box(gap1.uuid()); + EXPECT_EQ(cbox->first.first, 0); + EXPECT_EQ(cbox->first.second, 0); + EXPECT_EQ(cbox->second.first, 10); + EXPECT_EQ(cbox->second.second, 1); + + cbox = t.item().box(gap2.uuid()); + EXPECT_EQ(cbox->first.first, 10); + EXPECT_EQ(cbox->first.second, 0); + EXPECT_EQ(cbox->second.first, 20); + EXPECT_EQ(cbox->second.second, 1); + + cbox = t.item().box(sgap1.uuid()); + EXPECT_EQ(cbox->first.first, 20); + EXPECT_EQ(cbox->first.second, 0); + EXPECT_EQ(cbox->second.first, 30); + EXPECT_EQ(cbox->second.second, 1); + + cbox = t.item().box(sgap2.uuid()); + EXPECT_EQ(cbox->first.first, 20); + EXPECT_EQ(cbox->first.second, 1); + EXPECT_EQ(cbox->second.first, 30); + EXPECT_EQ(cbox->second.second, 2); +} + + TEST(TrackMoveTest, Test) { Track t("Track", MediaType::MT_IMAGE); @@ -140,3 +204,92 @@ TEST(TrackMoveTest, Test) { TESTMOVE() } } + +TEST(TrackResolveTest, Test) { + Track t("Track", MediaType::MT_IMAGE); + + auto id1 = FrameRange( + utility::FrameRateDuration(0, timebase::k_flicks_24fps), + utility::FrameRateDuration(10, timebase::k_flicks_24fps)); + auto i1 = Clip("Clip1"); + i1.item().set_available_range(id1); + + auto id2 = FrameRange( + utility::FrameRateDuration(0, timebase::k_flicks_24fps), + utility::FrameRateDuration(15, timebase::k_flicks_24fps)); + auto i2 = Clip("Clip2"); + i2.item().set_available_range(id2); + + auto id3 = utility::FrameRateDuration(20, timebase::k_flicks_24fps); + auto i3 = Gap("Gap1", id3); + + t.item().insert(t.item().end(), i1.item()); + t.item().insert(t.item().end(), i2.item()); + t.item().insert(t.item().end(), i3.item()); + + // we've added gaps so we need to refresh our state. + t.item().refresh(1); + + // check get clip + auto r = t.item().resolve_time(timebase::k_flicks_24fps * 0, media::MediaType::MT_IMAGE); + EXPECT_TRUE(r); + + // check get clip + r = t.item().resolve_time( + timebase::k_flicks_24fps * 0, + media::MediaType::MT_IMAGE, + utility::UuidSet({t.uuid(), i1.uuid()})); + EXPECT_TRUE(r); + + // check get nothing as GAPS don't count + r = t.item().resolve_time(timebase::k_flicks_24fps * 26, media::MediaType::MT_IMAGE); + EXPECT_FALSE(r); + + // neither in focus + r = t.item().resolve_time( + timebase::k_flicks_24fps * 0, media::MediaType::MT_IMAGE, utility::UuidSet({}), true); + EXPECT_FALSE(r); + + // track is focused + r = t.item().resolve_time( + timebase::k_flicks_24fps * 0, + media::MediaType::MT_IMAGE, + utility::UuidSet({t.uuid()}), + true); + EXPECT_TRUE(r); + + // clip focused + r = t.item().resolve_time( + timebase::k_flicks_24fps * 0, + media::MediaType::MT_IMAGE, + utility::UuidSet({i1.uuid()}), + true); + EXPECT_TRUE(r); + + // both focused + r = t.item().resolve_time( + timebase::k_flicks_24fps * 0, + media::MediaType::MT_IMAGE, + utility::UuidSet({t.uuid(), i1.uuid()}), + true); + EXPECT_TRUE(r); + + // not clip focused + r = t.item().resolve_time( + timebase::k_flicks_24fps * 15, + media::MediaType::MT_IMAGE, + utility::UuidSet({i1.uuid()}), + true); + EXPECT_FALSE(r); + + // not clip focused + r = t.item().resolve_time( + timebase::k_flicks_24fps * 15, + media::MediaType::MT_IMAGE, + utility::UuidSet({i2.uuid()}), + true); + + EXPECT_TRUE(r); + EXPECT_EQ(std::get<0>(*r).name(), i2.name()); + EXPECT_EQ(std::get<0>(*r).uuid(), i2.uuid()); +} diff --git a/src/ui/base/src/CMakeLists.txt b/src/ui/base/src/CMakeLists.txt index b2e3ef692..8bb65a596 100644 --- a/src/ui/base/src/CMakeLists.txt +++ b/src/ui/base/src/CMakeLists.txt @@ -1,25 +1,21 @@ +if (USE_VCPKG) + find_package(freetype CONFIG REQUIRED) +else() + find_package(Freetype REQUIRED) + include_directories("${FREETYPE_INCLUDE_DIRS}") +endif() +find_package(Imath) +find_package(OpenEXR) + SET(LINK_DEPS xstudio::utility Imath::Imath OpenEXR::OpenEXR + freetype ) if(UNIX) list(APPEND LINK_DEPS pthread) endif() -if(WIN32) -find_package(freetype CONFIG REQUIRED) -else() -find_package(Freetype) -include_directories("${FREETYPE_INCLUDE_DIRS}") -endif() -find_package(Imath) -find_package(OpenEXR) - -create_component_with_alias(ui_base xstudio::ui::base 0.1.0 "${LINK_DEPS}") - -target_link_libraries(${PROJECT_NAME} - PRIVATE - freetype -) +create_component_with_alias(ui_base xstudio::ui::base ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") \ No newline at end of file diff --git a/src/ui/base/src/font.cpp b/src/ui/base/src/font.cpp index 11fc56e7d..300e6347f 100644 --- a/src/ui/base/src/font.cpp +++ b/src/ui/base/src/font.cpp @@ -44,8 +44,8 @@ std::map Fonts::available_fonts() { // TODO: grep for ttf files in the /fonts/ folder, maybe freetype can give // use the font 'name' from the .ttf file? return { - {"Vera Mono", xstudio::utility::xstudio_root("/fonts/VeraMono.ttf")}, - {"Overpass Regular", xstudio::utility::xstudio_root("/fonts/Overpass-Regular.ttf")}}; + {"Vera Mono", xstudio::utility::xstudio_resources_dir("fonts/VeraMono.ttf")}, + {"Overpass Regular", xstudio::utility::xstudio_resources_dir("fonts/Overpass-Regular.ttf")}}; } AlphaBitmapFont::AlphaBitmapFont(const std::string &font_path, const int glyph_pixel_size) @@ -178,7 +178,7 @@ void AlphaBitmapFont::render_text2( const float viewport_du_dx) const { std::vector vertices; - precompute_text_rendering_vertex_layout( + std::ignore = precompute_text_rendering_vertex_layout( vertices, text, position, wrap_width, text_size, just, line_spacing); render_text( @@ -220,7 +220,7 @@ void AlphaBitmapFont::compute_wrap_indeces( x += character.hori_advance_ * scale; // bitshift by 6 to get value in pixels (2^6 = 64) - if (right_edge > box_width && last_word_begin != text.begin() && + if (box_width && right_edge > box_width && last_word_begin != text.begin() && !(result.size() && result.back() == last_word_begin)) { // need to wrap ... go back to last space c = last_word_begin; @@ -405,15 +405,15 @@ Imath::Box2f AlphaBitmapFont::precompute_text_rendering_vertex_layout( const float wrap_width, const float text_size, const Justification &just, - const float line_spacing) const { + const float line_spacing, + const bool align_around_position, + const VerticalJustification v_just) const { // N.B. the 'text_size' defines the font size in pixels if the viewport is width // fitted to a 1080p display. The 2.0 comes from the fact that xstudio's coordinate // system spans from -1.0 to +1.0 where the left and right edges of the image land. const float scale = text_size * 2.0f / (1920.0f * glyph_pixel_size()); - float x = position.x; - float y = position.y; std::vector wrap_points; compute_wrap_indeces(wrap_points, text, wrap_width, scale); @@ -421,24 +421,31 @@ Imath::Box2f AlphaBitmapFont::precompute_text_rendering_vertex_layout( std::string::const_iterator c; auto wrap_point = wrap_points.begin(); - result.resize(4 * 4 * text.size()); + result.resize(6 * 4 * text.size()); float *_vv = result.data(); Imath::Box2f bounding_box; + float x = position.x; + float y = position.y; + bounding_box.min.x = position.x - 0.2f * line_spacing * scale * glyph_pixel_size(); bounding_box.min.y = position.y - line_spacing * scale * glyph_pixel_size(); - bounding_box.max.x = bounding_box.min.x + wrap_width; int i = 0; + std::vector line_widths; + std::vector line_num_chars; for (c = text.begin(); c != text.end(); c++) { const CharacterMetrics &character = get_character(*c); if (wrap_point != wrap_points.end() && *wrap_point == c) { y += line_spacing * scale * glyph_pixel_size(); + line_widths.push_back(x - position.x); + line_num_chars.push_back(i); x = position.x; wrap_point++; + i = 0; } // skip non-printable @@ -457,23 +464,125 @@ Imath::Box2f AlphaBitmapFont::precompute_text_rendering_vertex_layout( float w = character.width_ * scale; float h = character.height_ * scale; // update VBO for each character - float vertices[4][4] = { + float vertices[6][4] = { // NOLINT {xpos, ypos + h, atlas_offset_x_, atlas_offset_y_ + hr}, {xpos, ypos, atlas_offset_x_, atlas_offset_y_}, {xpos + w, ypos, atlas_offset_x_ + wr, atlas_offset_y_}, - {xpos + w, ypos + h, atlas_offset_x_ + wr, atlas_offset_y_ + hr}}; + {xpos + w, ypos, atlas_offset_x_ + wr, atlas_offset_y_}, + {xpos + w, ypos + h, atlas_offset_x_ + wr, atlas_offset_y_ + hr}, + {xpos, ypos + h, atlas_offset_x_, atlas_offset_y_ + hr}}; memcpy(_vv, vertices, sizeof(vertices)); - _vv += 16; + _vv += 24; x += character.hori_advance_ * scale; // bitshift by 6 to get value in pixels (2^6 = 64) + i++; + + bounding_box.max.x = std::max(bounding_box.max.x, x); + } + + if (!wrap_width) { + bounding_box.max.x += 0.2f * line_spacing * scale * glyph_pixel_size(); + } + + line_widths.push_back(x - position.x); + line_num_chars.push_back(i); + + // deal with horizontal justification + if (just == JustifyRight) { + _vv = result.data(); + for (int j = 0; j < line_num_chars.size(); ++j) { + const float shift = + (align_around_position ? 0 : bounding_box.max.x) - line_widths[j]; + for (int i = 0; i < line_num_chars[j]; ++i) { + (*_vv) = *_vv + shift; + _vv += 4; + (*_vv) = *_vv + shift; + _vv += 4; + (*_vv) = *_vv + shift; + _vv += 4; + (*_vv) = *_vv + shift; + _vv += 4; + (*_vv) = *_vv + shift; + _vv += 4; + (*_vv) = *_vv + shift; + _vv += 4; + } + } + + if (!wrap_width) { + const float w = bounding_box.max.x - bounding_box.min.x; + bounding_box.max.x = position.x + 0.2f * line_spacing * scale * glyph_pixel_size(); + bounding_box.min.x = bounding_box.max.x - w; + } + + + } else if (just == JustifyCentre) { + + _vv = result.data(); + for (int j = 0; j < line_num_chars.size(); ++j) { + const float shift = + ((align_around_position ? 0 : bounding_box.max.x) - line_widths[j]) / 2.0f; + for (int i = 0; i < line_num_chars[j]; ++i) { + (*_vv) = *_vv + shift; + _vv += 4; + (*_vv) = *_vv + shift; + _vv += 4; + (*_vv) = *_vv + shift; + _vv += 4; + (*_vv) = *_vv + shift; + _vv += 4; + (*_vv) = *_vv + shift; + _vv += 4; + (*_vv) = *_vv + shift; + _vv += 4; + } + } + + if (!wrap_width) { + const float w = bounding_box.max.x - bounding_box.min.x; + bounding_box.max.x = position.x + w * 0.5f; + bounding_box.min.x = position.x - w * 0.5f; + } } if (wrap_points.size() && wrap_points.back() == text.end()) { // this means there's a carriage return right at the end of 'text' y += line_spacing * scale * glyph_pixel_size(); } - bounding_box.max.y = y + 0.25f * line_spacing * scale * glyph_pixel_size(); + + // deal with vertical justification ... + const float y_shift = v_just == JustifyTop ? line_spacing * scale * glyph_pixel_size() + : v_just == JustifyBottom ? position.y - bounding_box.max.y + : v_just == JustifyVCentre + ? position.y - (bounding_box.max.y + bounding_box.min.y) * 0.5f + : 0.0f; + + if (y_shift) { + + // move all verts in Y if necessary + bounding_box.min.y += y_shift; + bounding_box.max.y += y_shift; + _vv = result.data(); + _vv++; + for (int j = 0; j < line_num_chars.size(); ++j) { + for (int i = 0; i < line_num_chars[j]; ++i) { + (*_vv) = *_vv + y_shift; + _vv += 4; + (*_vv) = *_vv + y_shift; + _vv += 4; + (*_vv) = *_vv + y_shift; + _vv += 4; + (*_vv) = *_vv + y_shift; + _vv += 4; + (*_vv) = *_vv + y_shift; + _vv += 4; + (*_vv) = *_vv + y_shift; + _vv += 4; + } + } + } + return bounding_box; } diff --git a/src/ui/base/src/keyboard.cpp b/src/ui/base/src/keyboard.cpp index 545adf670..fe9a1bec6 100644 --- a/src/ui/base/src/keyboard.cpp +++ b/src/ui/base/src/keyboard.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include "xstudio/ui/keyboard.hpp" #include "xstudio/atoms.hpp" +#include "xstudio/utility/string_helpers.hpp" /* forward declaration of this function from tessellation_helpers.cpp */ using namespace xstudio::ui; @@ -11,20 +12,22 @@ Hotkey::Hotkey( const std::string name, const std::string component, const std::string description, - const std::string context, + const std::string window_name, const bool auto_repeat, - caf::actor_addr watcher) + caf::actor_addr watcher, + const utility::Uuid uuid) : key_(k), modifiers_(mod), - uuid_(utility::Uuid::generate_from_name((name + component).c_str())), + uuid_( + uuid.is_null() ? utility::Uuid::generate_from_name((name + component).c_str()) + : uuid), name_(name), component_(component), description_(description), - context_(context), auto_repeat_(auto_repeat) { if (watcher) - watchers_.emplace_back(watcher, context); + watchers_.emplace_back(watcher); } bool Hotkey::update(const Hotkey &o) { @@ -35,9 +38,8 @@ bool Hotkey::update(const Hotkey &o) { for (const auto &p : o.watchers_) { bool match = false; for (auto &q : watchers_) { - if (q.first == p.first) { - q.second = p.second; - match = true; + if (q == p) { + match = true; break; } } @@ -49,7 +51,15 @@ bool Hotkey::update(const Hotkey &o) { } void Hotkey::update_state( - const std::set ¤t_keys, const std::string &context, const bool auto_repeat) { + const std::set ¤t_keys, + const std::string &context, + const std::string &window, + const bool auto_repeat, + caf::actor keypress_monitor) { + + // context tells us where the hotkey interaction happened (e.g. 'main_viewport') + // If this hotkey has a context specifier that doesn't match then we don't + // update the hotkey state. int mod = 0; bool kp = false; @@ -65,28 +75,94 @@ void Hotkey::update_state( } if (mod == modifiers_ && kp == true) { + if (!pressed_) { + pressed_ = true; - notifty_watchers(context); + notify(context, window, keypress_monitor); + } else if (auto_repeat && auto_repeat_) { - notifty_watchers(context); + notify(context, window, keypress_monitor); } } else if (pressed_) { pressed_ = false; - notifty_watchers(context); + notify(context, window, keypress_monitor); + } +} + +void Hotkey::notify( + const std::string &context, const std::string &window, caf::actor keypress_monitor) { + + + auto p = exclusive_watchers_.begin(); + while (p != exclusive_watchers_.end()) { + auto exclusive = caf::actor_cast(*p); + if (exclusive) { + // we've found a valid 'exclusive' hotkey watcher + anon_mail(keypress_monitor::hotkey_event_atom_v, uuid_, pressed_, context, window) + .send(exclusive); + return; + } else { + // the exclusive watcher must have exited without telling us! + p = exclusive_watchers_.erase(p); + } + } + + // no exclusive watchers, broadcast hotkey event to everyone! + notify_watchers(context, window); + anon_mail(keypress_monitor::hotkey_event_atom_v, uuid_, pressed_, context, window) + .send(keypress_monitor); +} + +void Hotkey::exclusive_watcher(caf::actor_addr w, bool watch) { + + auto p = exclusive_watchers_.begin(); + while (p != exclusive_watchers_.end()) { + if (*p == w) { + p = exclusive_watchers_.erase(p); + } else { + p++; + } + } + + if (watch) { + exclusive_watchers_.insert(exclusive_watchers_.begin(), w); + } +} + +void Hotkey::watcher_died(caf::actor_addr &watcher) { + auto p = watchers_.begin(); + while (p != watchers_.end()) { + if (*p == watcher) { + p = watchers_.erase(p); + } else { + p++; + } + } +} + +void Hotkey::add_watcher(caf::actor_addr w) { + + auto p = watchers_.begin(); + while (p != watchers_.end()) { + if (*p == w) { + return; + } + p++; } + watchers_.push_back(w); } -void Hotkey::notifty_watchers(const std::string &context) { + +void Hotkey::notify_watchers(const std::string &context, const std::string &window) { auto p = watchers_.begin(); while (p != watchers_.end()) { - auto a = caf::actor_cast((*p).first); + auto a = caf::actor_cast(*p); if (!a) { p = watchers_.erase(p); // the 'watcher' must have closed down - let's remove it - } else if (context == (*p).second || (*p).second == "any" || (*p).second.empty()) { - anon_send(a, keypress_monitor::hotkey_event_atom_v, uuid_, pressed_, context); - p++; } else { + anon_mail(keypress_monitor::hotkey_event_atom_v, uuid_, pressed_, context, window) + .send(a); p++; } } @@ -97,10 +173,6 @@ const std::string Hotkey::key() const { return std::string(k.data()); } - -void Hotkey::add_watcher(caf::actor_addr watcher) { watchers_.emplace_back(watcher, context_); } - - std::string Hotkey::hotkey_sequence() const { std::string r; @@ -131,3 +203,36 @@ std::string Hotkey::hotkey_sequence() const { } return r; } + +void Hotkey::sequence_to_key_and_modifier( + const std::string &sequence, int &keycode, int &modifier) { + + std::vector seq = utility::split(sequence, '+'); + modifier = 0; + keycode = -1; + for (const auto &p : seq) { + const std::string D = utility::replace_all(p, " ", ""); + const std::string d = utility::to_lower(D); + if (d == "shift") { + modifier |= ShiftModifier; + } else if (d == "meta") { + modifier |= MetaModifier; + } else if (d == "alt") { + modifier |= AltModifier; + } else if (d == "ctrl") { + modifier |= ControlModifier; + } else { + for (const auto &q : ui::Hotkey::key_names) { + if (q.second == D) { + keycode = q.first; + break; + } + } + } + } + if (keycode == -1) { + throw std::runtime_error( + fmt::format("Unable to identify key name in hotkey sequence \"{}\"", sequence) + .c_str()); + } +} \ No newline at end of file diff --git a/src/ui/canvas/src/CMakeLists.txt b/src/ui/canvas/src/CMakeLists.txt index 240b4d906..dab233a55 100644 --- a/src/ui/canvas/src/CMakeLists.txt +++ b/src/ui/canvas/src/CMakeLists.txt @@ -1,27 +1,23 @@ +if (USE_VCPKG) + find_package(freetype CONFIG REQUIRED) +else() + find_package(Freetype REQUIRED) + include_directories("${FREETYPE_INCLUDE_DIRS}") +endif() + +find_package(Imath) +find_package(OpenEXR) + SET(LINK_DEPS xstudio::utility Imath::Imath OpenEXR::OpenEXR ui_base + freetype ) if(UNIX) list(APPEND LINK_DEPS pthread) endif() -if(WIN32) - find_package(freetype CONFIG REQUIRED) -else() - find_package(Freetype) - include_directories("${FREETYPE_INCLUDE_DIRS}") -endif() - -find_package(Imath) -find_package(OpenEXR) - -create_component_with_alias(ui_canvas xstudio::ui::canvas 0.1.0 "${LINK_DEPS}") - -target_link_libraries(${PROJECT_NAME} - PRIVATE - freetype -) \ No newline at end of file +create_component_with_alias(ui_canvas xstudio::ui::canvas ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") \ No newline at end of file diff --git a/src/ui/canvas/src/canvas.cpp b/src/ui/canvas/src/canvas.cpp index 391e37ea5..0196d9898 100644 --- a/src/ui/canvas/src/canvas.cpp +++ b/src/ui/canvas/src/canvas.cpp @@ -28,6 +28,7 @@ void Canvas::clear(const bool clear_history) { } items_.clear(); current_item_.reset(); + next_shape_id_ = 0; changed(); } @@ -116,6 +117,199 @@ bool Canvas::fade_all_strokes(float opacity) { return remaining_strokes > 0; } +uint32_t Canvas::start_quad( + const utility::ColourTriplet &colour, const std::vector &corners) { + + std::unique_lock l(mutex_); + end_draw_no_lock(); + + Quad q; + q._id = next_shape_id_++; + q.bl = corners[0]; + q.tl = corners[1]; + q.tr = corners[2]; + q.br = corners[3]; + q.colour = colour; + + current_item_ = q; + changed(); + + return q._id; +} + +void Canvas::update_quad( + uint32_t id, + const utility::ColourTriplet &colour, + const std::vector &corners, + float softness, + float opacity, + bool invert) { + + std::unique_lock l(mutex_); + + for (auto &item : items_) { + if (std::holds_alternative(item)) { + auto &quad = std::get(item); + + if (quad._id == id) { + + quad.bl = corners[0]; + quad.tl = corners[1]; + quad.tr = corners[2]; + quad.br = corners[3]; + + quad.colour = colour; + quad.softness = softness; + quad.opacity = opacity; + quad.invert = invert; + + changed(); + } + } + } +} + +uint32_t Canvas::start_polygon( + const utility::ColourTriplet &colour, const std::vector &points) { + + std::unique_lock l(mutex_); + end_draw_no_lock(); + + Polygon q; + q._id = next_shape_id_++; + q.points = points; + q.colour = colour; + + current_item_ = q; + changed(); + + return q._id; +} + +void Canvas::update_polygon( + uint32_t id, + const utility::ColourTriplet &colour, + const std::vector &points, + float softness, + float opacity, + bool invert) { + + std::unique_lock l(mutex_); + + for (auto &item : items_) { + if (std::holds_alternative(item)) { + auto &polygon = std::get(item); + + if (polygon._id == id) { + + polygon.points = points; + + polygon.colour = colour; + polygon.softness = softness; + polygon.opacity = opacity; + polygon.invert = invert; + + changed(); + } + } + } +} + +uint32_t Canvas::start_ellipse( + const utility::ColourTriplet &colour, + const Imath::V2f ¢er, + const Imath::V2f &radius, + float angle) { + + std::unique_lock l(mutex_); + end_draw_no_lock(); + + Ellipse e; + e._id = next_shape_id_++; + e.center = center; + e.radius = radius; + e.angle = angle; + e.colour = colour; + + current_item_ = e; + changed(); + + return e._id; +} + +void Canvas::update_ellipse( + uint32_t id, + const utility::ColourTriplet &colour, + const Imath::V2f ¢er, + const Imath::V2f &radius, + float angle, + float softness, + float opacity, + bool invert) { + + std::unique_lock l(mutex_); + + for (auto &item : items_) { + if (std::holds_alternative(item)) { + auto &ellipse = std::get(item); + + if (ellipse._id == id) { + + ellipse.center = center; + ellipse.radius = radius; + ellipse.angle = angle; + + ellipse.colour = colour; + ellipse.softness = softness; + ellipse.opacity = opacity; + ellipse.invert = invert; + + changed(); + } + } + } +} + +bool Canvas::remove_shape(uint32_t id) { + + std::unique_lock l(mutex_); + + int remove_idx = -1; + + for (int i = 0; i < items_.size(); ++i) { + if (std::holds_alternative(items_[i])) { + auto &quad = std::get(items_[i]); + + if (quad._id == id) { + remove_idx = i; + break; + } + } else if (std::holds_alternative(items_[i])) { + auto &polygon = std::get(items_[i]); + + if (polygon._id == id) { + remove_idx = i; + break; + } + } else if (std::holds_alternative(items_[i])) { + auto &ellipse = std::get(items_[i]); + + if (ellipse._id == id) { + remove_idx = i; + break; + } + } + } + + if (remove_idx >= 0) { + items_.erase(items_.begin() + remove_idx); + changed(); + return true; + } + + return false; +} + void Canvas::start_square( const utility::ColourTriplet &colour, float thickness, float opacity) { @@ -623,8 +817,13 @@ void Canvas::end_draw_no_lock() { } } -void Canvas::changed() { last_change_time_ = utility::clock::now(); } +void Canvas::changed() { + last_change_time_ = utility::clock::now(); + std::ostringstream oss; + oss << last_change_time_.time_since_epoch().count() << (void*)this; + hash_ = std::hash{}(oss.str()); +} void xstudio::ui::canvas::from_json(const nlohmann::json &j, Canvas &c) { @@ -639,6 +838,31 @@ void xstudio::ui::canvas::from_json(const nlohmann::json &j, Canvas &c) { c.items_.push_back(item.template get()); } } + + if (j.contains("quads") && j["quads"].is_array()) { + for (const auto &item : j["quads"]) { + Quad quad = item.template get(); + quad._id = c.next_shape_id_++; + c.items_.push_back(quad); + } + } + + if (j.contains("polygons") && j["polygons"].is_array()) { + for (const auto &item : j["polygons"]) { + Polygon polygon = item.template get(); + polygon._id = c.next_shape_id_++; + c.items_.push_back(polygon); + } + } + + if (j.contains("ellipses") && j["ellipses"].is_array()) { + for (const auto &item : j["ellipses"]) { + Ellipse ellipse = item.template get(); + ellipse._id = c.next_shape_id_++; + c.items_.push_back(ellipse); + } + } + c.changed(); } @@ -646,6 +870,9 @@ void xstudio::ui::canvas::to_json(nlohmann::json &j, const Canvas &c) { j["pen_strokes"] = nlohmann::json::array(); j["captions"] = nlohmann::json::array(); + j["quads"] = nlohmann::json::array(); + j["polygons"] = nlohmann::json::array(); + j["ellipses"] = nlohmann::json::array(); for (const auto &item : c) { std::visit( @@ -655,6 +882,12 @@ void xstudio::ui::canvas::to_json(nlohmann::json &j, const Canvas &c) { j["pen_strokes"].push_back(nlohmann::json(arg)); else if constexpr (std::is_same_v) j["captions"].push_back(nlohmann::json(arg)); + else if constexpr (std::is_same_v) + j["quads"].push_back(nlohmann::json(arg)); + else if constexpr (std::is_same_v) + j["polygons"].push_back(nlohmann::json(arg)); + else if constexpr (std::is_same_v) + j["ellipses"].push_back(nlohmann::json(arg)); else static_assert(always_false_v, "Missing serialiser for canvas item!"); }, diff --git a/src/ui/canvas/src/shapes.cpp b/src/ui/canvas/src/shapes.cpp new file mode 100644 index 000000000..586271fb0 --- /dev/null +++ b/src/ui/canvas/src/shapes.cpp @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/ui/canvas/shapes.hpp" + +using namespace xstudio::ui::canvas; +using namespace xstudio; + + +bool Quad::operator==(const Quad &o) const { + return ( + bl == o.bl && tl == o.tl && tr == o.tr && br == o.br && colour == o.colour && + softness == o.softness && opacity == o.opacity && invert == o.invert); +} + +std::string Quad::hash() const { + + std::string hash; + hash += std::to_string(bl.x); + hash += std::to_string(bl.y); + hash += std::to_string(tl.x); + hash += std::to_string(tl.y); + hash += std::to_string(tr.x); + hash += std::to_string(tr.y); + hash += std::to_string(br.x); + hash += std::to_string(br.y); + hash += utility::to_string(colour); + hash += std::to_string(softness); + hash += std::to_string(opacity); + hash += std::to_string(invert); + return hash; +} + +void xstudio::ui::canvas::from_json(const nlohmann::json &j, Quad &q) { + + j.at("bl").get_to(q.bl); + j.at("tl").get_to(q.tl); + j.at("tr").get_to(q.tr); + j.at("br").get_to(q.br); + j.at("colour").get_to(q.colour); + j.at("softness").get_to(q.softness); + j.at("opacity").get_to(q.opacity); + j.at("invert").get_to(q.invert); +} + +void xstudio::ui::canvas::to_json(nlohmann::json &j, const Quad &q) { + + j["bl"] = q.bl; + j["tl"] = q.tl; + j["tr"] = q.tr; + j["br"] = q.br; + j["colour"] = q.colour; + j["softness"] = q.softness; + j["opacity"] = q.opacity; + j["invert"] = q.invert; +} + +std::string Polygon::hash() const { + + std::string hash; + for (int i = 0; i < points.size(); ++i) { + hash += std::to_string(points[i].x); + hash += std::to_string(points[i].y); + } + hash += utility::to_string(colour); + hash += std::to_string(softness); + hash += std::to_string(opacity); + hash += std::to_string(invert); + return hash; +} + +bool Polygon::operator==(const Polygon &o) const { + return ( + points == o.points && colour == o.colour && softness == o.softness && + opacity == o.opacity && invert == o.invert); +} + +void xstudio::ui::canvas::from_json(const nlohmann::json &j, Polygon &q) { + + j.at("points").get_to(q.points); + j.at("colour").get_to(q.colour); + j.at("softness").get_to(q.softness); + j.at("opacity").get_to(q.opacity); + j.at("invert").get_to(q.invert); +} + +void xstudio::ui::canvas::to_json(nlohmann::json &j, const Polygon &q) { + + j["points"] = q.points; + j["colour"] = q.colour; + j["softness"] = q.softness; + j["opacity"] = q.opacity; + j["invert"] = q.invert; +} + +std::string Ellipse::hash() const { + + std::string hash; + hash += std::to_string(center.x); + hash += std::to_string(center.y); + hash += std::to_string(radius.x); + hash += std::to_string(radius.y); + hash += std::to_string(angle); + hash += utility::to_string(colour); + hash += std::to_string(softness); + hash += std::to_string(opacity); + hash += std::to_string(invert); + return hash; +} + +bool Ellipse::operator==(const Ellipse &o) const { + return ( + center == o.center && radius == o.radius && angle == o.angle && colour == o.colour && + softness == o.softness && opacity == o.opacity && invert == o.invert); +} + +void xstudio::ui::canvas::from_json(const nlohmann::json &j, Ellipse &e) { + + j.at("center").get_to(e.center); + j.at("radius").get_to(e.radius); + j.at("angle").get_to(e.angle); + j.at("colour").get_to(e.colour); + j.at("softness").get_to(e.softness); + j.at("opacity").get_to(e.opacity); + j.at("invert").get_to(e.invert); +} + +void xstudio::ui::canvas::to_json(nlohmann::json &j, const Ellipse &e) { + + j["center"] = e.center; + j["radius"] = e.radius; + j["angle"] = e.angle; + j["colour"] = e.colour; + j["softness"] = e.softness; + j["opacity"] = e.opacity; + j["invert"] = e.invert; +} \ No newline at end of file diff --git a/src/ui/canvas/src/stroke.cpp b/src/ui/canvas/src/stroke.cpp index f8b37b202..3e0d54e96 100644 --- a/src/ui/canvas/src/stroke.cpp +++ b/src/ui/canvas/src/stroke.cpp @@ -56,6 +56,20 @@ bool Stroke::operator==(const Stroke &o) const { colour == o.colour && type == o.type && points == o.points); } +std::string Stroke::hash() const { + + std::string hash; + // Not adding every points coordinate to hash on purpose + // as the list might get very long. + hash += std::to_string(points.size()); + hash += std::to_string(type); + hash += utility::to_string(colour); + hash += std::to_string(thickness); + hash += std::to_string(softness); + hash += std::to_string(opacity); + return hash; +} + void Stroke::make_square(const Imath::V2f &corner1, const Imath::V2f &corner2) { points = std::vector( diff --git a/src/ui/model_data/src/CMakeLists.txt b/src/ui/model_data/src/CMakeLists.txt index 0a05284cf..87d4d17c2 100644 --- a/src/ui/model_data/src/CMakeLists.txt +++ b/src/ui/model_data/src/CMakeLists.txt @@ -1,8 +1,9 @@ SET(LINK_DEPS - ${CAF_LIBRARY_core} + CAF::core xstudio::json_store xstudio::global_store xstudio::utility + xstudio::ui::base ) -create_component_with_alias(ui_model_data xstudio::ui::model_data 0.1.0 "${LINK_DEPS}") \ No newline at end of file +create_component_with_alias(ui_model_data xstudio::ui::model_data ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") \ No newline at end of file diff --git a/src/ui/model_data/src/model_data_actor.cpp b/src/ui/model_data/src/model_data_actor.cpp index f92f20960..bc5c9cd56 100644 --- a/src/ui/model_data/src/model_data_actor.cpp +++ b/src/ui/model_data/src/model_data_actor.cpp @@ -1,14 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 #include #include +#include + #include "xstudio/atoms.hpp" +#include "xstudio/global_store/global_store.hpp" #include "xstudio/json_store/json_store_actor.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/helpers.hpp" -#include "xstudio/module/global_module_events_actor.hpp" +#include "xstudio/utility/tree.hpp" #include "xstudio/module/attribute.hpp" #include "xstudio/broadcast/broadcast_actor.hpp" #include "xstudio/ui/model_data/model_data_actor.hpp" +#include "xstudio/ui/keyboard.hpp" #include @@ -32,25 +36,35 @@ std::string path_from_node(utility::JsonTree *node) { } inline void dump_tree2(const JsonTree &node, const int depth = 0) { - spdlog::warn("{:>{}} {}", " ", depth * 4, node.data().dump(2)); + std::cerr << fmt::format("{:>{}} {}", " ", depth * 4, node.data().dump()) << "\n"; for (const auto &i : node.base()) dump_tree2(i, depth + 1); } utility::JsonTree *find_node_matching_string_field( - utility::JsonTree *data, const std::string &field_name, const std::string &field_value) { + utility::JsonTree *data, + const std::string &field_name, + const std::string &field_value, + int depth = -1, + int real_depth = 0) { if (data->data().value(field_name, std::string()) == field_value) { return data; } - for (auto c = data->begin(); c != data->end(); c++) { - try { - utility::JsonTree *r = - find_node_matching_string_field(&(*c), field_name, field_value); + if (depth) { + for (auto c = data->begin(); c != data->end(); c++) { + utility::JsonTree *r = find_node_matching_string_field( + &(*c), field_name, field_value, depth - 1, real_depth + 1); if (r) return r; - } catch (...) { } } + if (!real_depth) { + /*std::stringstream ss; + ss << "Failed to find field \"" << field_name << "\" with value matching \"" + << field_value << "\""; + std::cerr << ss.str() << "\n"; + throw std::runtime_error(ss.str().c_str());*/ + } return nullptr; } @@ -83,6 +97,30 @@ GlobalUIModelData::GlobalUIModelData(caf::actor_config &cfg) : caf::event_based_ system().registry().put(global_ui_model_data_registry, this); + { + // here we join the hotkeys manager events group to get update on + // chnanges to hotkeys. If menu items in the menu models have links + // to a hotkey then we need to update the info if the hotkey is + // changed + auto keyboard_manager = system().registry().template get(keyboard_events); + request( + keyboard_manager, + infinite, + utility::get_event_group_atom_v, + keypress_monitor::hotkey_event_atom_v) + .then( + [=](caf::actor hotkeys_config_events_group) mutable { + anon_send( + hotkeys_config_events_group, + broadcast::join_broadcast_atom_v, + caf::actor_cast(this)); + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } + + set_down_handler([=](down_msg &msg) { // has a client exited? for (auto p : models_) { @@ -94,6 +132,7 @@ GlobalUIModelData::GlobalUIModelData(caf::actor_config &cfg) : caf::event_based_ c++; } } + std::vector> dead_menu_nodes; auto w = p.second->menu_watchers_.begin(); while (w != p.second->menu_watchers_.end()) { auto watcher = w->second.begin(); @@ -104,12 +143,25 @@ GlobalUIModelData::GlobalUIModelData(caf::actor_config &cfg) : caf::event_based_ watcher++; } } + if (w->second.empty()) { + dead_menu_nodes.emplace_back(p.first, w->first); + } w++; } + for (const auto &d : dead_menu_nodes) { + remove_node(d.first, d.second); + } } }); behavior_.assign( + [=](register_model_data_atom, + const std::string &model_name, + const bool deregister, + caf::actor client) { + if (deregister) + stop_watching_model(model_name, client); + }, [=](register_model_data_atom, const std::string &model_name, const utility::JsonStore &model_data, @@ -121,6 +173,18 @@ GlobalUIModelData::GlobalUIModelData(caf::actor_config &cfg) : caf::event_based_ return caf::make_error(xstudio_error::error, e.what()); } }, + [=](register_model_data_atom, + const std::string &model_name, + const utility::JsonStore &model_data, + caf::actor client, + bool force_resend_data_to_client) -> result { + try { + register_model(model_name, model_data, client, force_resend_data_to_client); + return model_data_as_json(model_name); + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } + }, [=](register_model_data_atom, const std::string &model_name, const utility::JsonStore &attribute_data, @@ -135,7 +199,7 @@ GlobalUIModelData::GlobalUIModelData(caf::actor_config &cfg) : caf::event_based_ return caf::make_error(xstudio_error::error, e.what()); } }, - [=](register_model_data_atom, + [=](deregister_model_data_atom, const std::string &model_name, const utility::Uuid &attribute_uuid, caf::actor client) { @@ -146,17 +210,49 @@ GlobalUIModelData::GlobalUIModelData(caf::actor_config &cfg) : caf::event_based_ // return caf::make_error(xstudio_error::error, e.what()); } }, + [=](register_model_data_atom, + const std::string &model_name, + const std::string &preferences_path, + const utility::JsonStore &model_data, + caf::actor client) -> result { + try { + register_model(model_name, model_data, client, false, preferences_path); + return model_data_as_json(model_name); + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } + }, [=](register_model_data_atom, const std::string &model_name, const std::string &preferences_path, caf::actor client) -> result { try { - register_model(model_name, utility::JsonStore(), client, preferences_path); + register_model( + model_name, utility::JsonStore(), client, false, preferences_path); return model_data_as_json(model_name); } catch (std::exception &e) { return caf::make_error(xstudio_error::error, e.what()); } }, + [=](reset_model_atom, + const std::string &model_name, + const std::string &preferences_path) { + try { + reset_model(model_name, preferences_path); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + }, + [=](reset_model_atom, + const std::string &model_name, + const utility::JsonStore &data, + const bool broadcast_back_to_sender) { + reset_model( + model_name, + data, + broadcast_back_to_sender ? caf::actor() + : actor_cast(current_sender())); + }, [=](model_data_atom, const std::string &model_name) -> result { try { return model_data_as_json(model_name); @@ -232,9 +328,13 @@ GlobalUIModelData::GlobalUIModelData(caf::actor_config &cfg) : caf::event_based_ } }, - [=](menu_node_activated_atom, const std::string &model_name, const std::string &path) { - node_activated(model_name, path); + [=](menu_node_activated_atom, + const std::string &model_name, + const std::string &path, + const std::string &user_data) { + node_activated(model_name, path, user_data, false); }, + [=](insert_or_update_menu_node_atom, const std::string &model_name, const std::string &menu_path, @@ -242,6 +342,23 @@ GlobalUIModelData::GlobalUIModelData(caf::actor_config &cfg) : caf::event_based_ caf::actor watcher) { insert_into_menu_model(model_name, menu_path, menu_data, watcher); }, + [=](insert_or_update_menu_node_atom, + const std::string &model_name, + const std::string &menu_path, + const float position_in_menu) { + // this message can be used to change the order of some submenu + // within a menu model to set it's position amongst submenus at + // the same level. + set_menu_node_position(model_name, menu_path, position_in_menu); + }, + [=](set_row_ordering_role_atom, + const std::string &model_name, + const std::string &role_name) { + // not implemented yet .. idea is you can sort the model based off + // a particular role. + check_model_is_registered(model_name, true); + models_[model_name]->sort_key_ = role_name; + }, [=](remove_node_atom, const std::string model_name, const utility::Uuid &model_item_id) { remove_node(model_name, model_item_id); }, @@ -252,18 +369,32 @@ GlobalUIModelData::GlobalUIModelData(caf::actor_config &cfg) : caf::event_based_ for (const auto &model_name : models_to_be_fully_broadcasted_) { const auto model_data = model_data_as_json(model_name); - for (auto &client : models_[model_name]->clients_) { - send( - client, - utility::event_atom_v, - model_data_atom_v, - model_name, - model_data); + if (client) + anon_send( + client, + utility::event_atom_v, + model_data_atom_v, + model_name, + model_data); } } models_to_be_fully_broadcasted_.clear(); }, + [=](keypress_monitor::hotkey_event_atom, const std::vector & /*hotkeys*/) { + // ignored event from hotkey manager + }, + [=](keypress_monitor::hotkey_event_atom, Hotkey &hotkey) { + update_hotkeys_model_data(hotkey); + }, + [=](keypress_monitor::hotkey_event_atom, + const utility::Uuid kotkey_uuid, + const bool pressed, + const std::string &context, + const std::string &window) { + if (pressed) + hotkey_pressed(kotkey_uuid, context, window); + }, [=](const caf::error &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }, @@ -310,17 +441,30 @@ void GlobalUIModelData::set_data( changed = true; } - if (changed && !role.empty()) { - for (auto &client : models_[model_name]->clients_) - send( - client, - utility::event_atom_v, - set_node_data_atom_v, - model_name, - path, - data, - role, - uuid_role_data); + if (changed) { //} && !role.empty()) { + + if (!role.empty()) { + for (auto &client : models_[model_name]->clients_) { + if (client) + mail( + utility::event_atom_v, + set_node_data_atom_v, + model_name, + path, + data, + role, + uuid_role_data) + .send(client); + } + } else { + for (auto &client : models_[model_name]->clients_) { + if (client) { + mail( + utility::event_atom_v, set_node_data_atom_v, model_name, path, data) + .send(client); + } + } + } push_to_prefs(model_name); @@ -330,15 +474,16 @@ void GlobalUIModelData::set_data( if (p != models_[model_name]->menu_watchers_.end()) { auto &watchers = p->second; for (auto watcher : watchers) { - send( - watcher, - utility::event_atom_v, - set_node_data_atom_v, - model_name, - path, - role, - data, - uuid_role_data); + if (watcher) + mail( + utility::event_atom_v, + set_node_data_atom_v, + model_name, + path, + role, + data, + uuid_role_data.is_null() ? uuid : uuid_role_data) + .send(watcher); } } } @@ -351,7 +496,7 @@ void GlobalUIModelData::set_data( } } catch ([[maybe_unused]] std::exception &e) { - // spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, e.what()); + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } @@ -370,9 +515,11 @@ void GlobalUIModelData::set_data( utility::JsonTree *node = find_node_matching_string_field( &(models_[model_name]->data_), attr_uuid_role_name, to_string(attribute_uuid)); + if (!node) { - throw std::runtime_error("Failed to find expected field"); + throw std::runtime_error(fmt::format("Couln't find attr UUID in data.").c_str()); } + auto &j = node->data(); bool changed = false; @@ -389,21 +536,22 @@ void GlobalUIModelData::set_data( j = data; } + if (changed) { std::string path = path_from_node(node); for (auto &client : models_[model_name]->clients_) { if (client != setter) - send( - client, + mail( utility::event_atom_v, set_node_data_atom_v, model_name, path, data, role, - uuid_role_data); + uuid_role_data) + .send(client); } push_to_prefs(model_name); @@ -417,15 +565,15 @@ void GlobalUIModelData::set_data( // we don't notify the thing that is setting this data // as it will update it's local data if (watcher != setter) - send( - watcher, + mail( utility::event_atom_v, set_node_data_atom_v, model_name, path, role, data, - uuid_role_data); + uuid_role_data) + .send(watcher); } } } @@ -461,21 +609,38 @@ void GlobalUIModelData::insert_attribute_data_into_model( } } else { - utility::JsonStore blank_model(nlohmann::json::parse(R"({ "children": [] })")); + utility::JsonStore blank_model(nlohmann::json::parse(R"({})")); models_[model_name] = std::make_shared(model_name, blank_model, client); monitor(client); } utility::JsonTree *parent_node = &(models_[model_name]->data_); try { - auto found_node = find_node_matching_string_field( + auto pnode = find_node_matching_string_field( parent_node, attr_uuid_role_name, to_string(attribute_uuid)); - if (found_node) { - parent_node = found_node; - } else { - throw std::runtime_error("Failed to find expected field"); + + if (!pnode) { + if (!sort_role.empty() && attribute_data.contains(sort_role)) { + const auto &sort_v = attribute_data[sort_role]; + auto insert_pt = parent_node->begin(); + while (insert_pt != parent_node->end()) { + if (insert_pt->data().contains(sort_role)) { + if (insert_pt->data()[sort_role] >= sort_v) { + break; + } + } + insert_pt++; + } + parent_node->insert(insert_pt, attribute_data); + } else { + parent_node->insert(parent_node->end(), attribute_data); + } + broadcast_whole_model_data(model_name); + return; } + parent_node = pnode; + const auto &d = parent_node->data(); bool full_push = false; @@ -541,16 +706,39 @@ void GlobalUIModelData::remove_attribute_data_from_model( } } +void GlobalUIModelData::stop_watching_model(const std::string &model_name, caf::actor client) { + auto p = models_.find(model_name); + if (p != models_.end()) { + + // model with this name already exists. Simply add client and send the + // full model state to the client + auto c = p->second->clients_.begin(); + while (c != p->second->clients_.end()) { + if (*c == client) { + c = p->second->clients_.erase(c); + } else { + c++; + } + } + } +} void GlobalUIModelData::register_model( const std::string &model_name, const utility::JsonStore &model_data, caf::actor client, + const bool force_resend, const std::string &preference_path) { auto p = models_.find(model_name); if (p != models_.end()) { + if (p->second->data_.total_size() == 0 && p->second->data_.data().is_null()) { + // model is already registered but it is empty. Use incoming model_data + // to set-up the model data. + p->second->data_ = utility::json_to_tree(model_data, "children"); + } + // model with this name already exists. Simply add client and send the // full model state to the client bool already_a_client = false; @@ -559,10 +747,17 @@ void GlobalUIModelData::register_model( already_a_client = true; } } + if (!already_a_client) { p->second->clients_.push_back(client); monitor(client); - send( + } + + if (force_resend) { + + caf::scoped_actor sys(system()); + utility::request_receive( + *sys, client, utility::event_atom_v, model_data_atom_v, @@ -577,13 +772,49 @@ void GlobalUIModelData::register_model( JsonStore j; prefs.get_group(j); auto data_from_prefs = global_store::preference_value(j, preference_path); + models_[model_name] = std::make_shared(model_name, data_from_prefs, client, preference_path); - send(client, utility::event_atom_v, model_data_atom_v, model_name, data_from_prefs); + anon_send( + client, utility::event_atom_v, model_data_atom_v, model_name, data_from_prefs); + monitor(client); + + } else if (!preference_path.empty()) { + + // don't load from preferennces, but make the model data a saved preference + models_[model_name] = + std::make_shared(model_name, model_data, client, preference_path); monitor(client); } else { - models_[model_name] = std::make_shared(model_name, model_data, client); + + auto new_model_data = std::make_shared(model_name, model_data, client); + + // If we are adding a new model - check if there is a model with a wildcard + // If there is, we duplicate that wildcard model data into the new + // model that we're adding as a starting point. + // + // This specifically means that if there is a C++ plugin that has created + // menu items in a model named 'MyModel*' then any models created + // in the UI layer called 'MyModel' will include the menu + // items created in the backend. + for (auto &p : models_) { + + if (p.first.find("*") == p.first.size() - 1) { + + std::string match_name(p.first, 0, (p.first.size() - 1)); + if (model_name.find(match_name) == 0) { + new_model_data->data_ = p.second->data_; + new_model_data->clients_.insert( + new_model_data->clients_.begin(), + p.second->clients_.begin(), + p.second->clients_.end()); + new_model_data->menu_watchers_ = p.second->menu_watchers_; + } + } + } + + models_[model_name] = new_model_data; monitor(client); } } @@ -606,8 +837,23 @@ void GlobalUIModelData::check_model_is_registered( ss << "No such model \"" << model_name << "\" registered with GlobalUIModelData"; throw std::runtime_error(ss.str().c_str()); } else { - models_[model_name] = + + // is there a model here with a wildcard in its name that matches + // the model we need to create? + + auto new_model_data = std::make_shared(model_name, nlohmann::json::parse("[]")); + + for (auto &p : models_) { + if (p.first.find("*") == p.first.size() - 1) { + std::string match_name(p.first, 0, (p.first.size() - 1)); + if (model_name.find(match_name) == 0) { + new_model_data->data_ = p.second->data_; + } + } + } + + models_[model_name] = new_model_data; } } } @@ -622,7 +868,14 @@ void GlobalUIModelData::insert_rows( try { - check_model_is_registered(model_name); + bool blank = false; + if (models_.find(model_name) == models_.end()) { + // it's possible something tries to insert data to a model that + // hasn't been set-up yet, so do the set-up now. + utility::JsonStore blank_model(nlohmann::json::parse(R"({})")); + models_[model_name] = std::make_shared(model_name, blank_model); + blank = true; + } auto &model_data = models_[model_name]->data_; @@ -631,22 +884,44 @@ void GlobalUIModelData::insert_rows( if (j and row >= 0 and row <= static_cast(j->size())) { while (count--) { - j->insert(std::next(j->begin(), row), data); + j->insert(std::next(j->begin(), row), utility::json_to_tree(data, "children")); } - } else { + + } else if (j) { std::stringstream ss; ss << "Trying to insert row @ " << row << " where only " << j->size() << " rows exist already"; throw std::runtime_error(ss.str().c_str()); + } else { + std::stringstream ss; + ss << "Trying to insert row @ " << row << " to node with path \"" << path + << "\" - no node at this path!"; + throw std::runtime_error(ss.str().c_str()); } - const auto model_data_json = model_data_as_json(model_name); + if (!models_[model_name]->sort_key_.empty()) { + do_ordering(&model_data, models_[model_name]->sort_key_); + } + + auto model_data_json = model_data_as_json(model_name); + + if (data.contains("hotkey_uuid")) { + utility::Uuid hotkey_uuid(data["hotkey_uuid"]); + if (!hotkey_uuid.is_null()) { + + models_[model_name]->hotkeys_.insert(hotkey_uuid); + } + } + + // caf::scoped_actor sys(system()); for (auto &client : models_[model_name]->clients_) { + // if we know 'requester', then the requester does not want to // get the change event as it has already updated its local model - if (client != requester) { - send( + if (client && client != requester) { + anon_send( + //*sys, client, utility::event_atom_v, model_data_atom_v, @@ -655,8 +930,11 @@ void GlobalUIModelData::insert_rows( } } + push_to_prefs(model_name); + } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + spdlog::warn( + "{} {} {} {} {}", __PRETTY_FUNCTION__, e.what(), model_name, path, data.dump()); } } @@ -680,10 +958,19 @@ void GlobalUIModelData::remove_rows( } const auto model_data_json = model_data_as_json(model_name); + + caf::scoped_actor sys(system()); for (auto &client : models_[model_name]->clients_) { - if (client == requester) - continue; - send(client, utility::event_atom_v, model_data_atom_v, model_name, model_data_json); + // if we know 'requester', then the requester does not want to + // get the change event as it has already updated its local model + if (client != requester) { + anon_send( + client, + utility::event_atom_v, + model_data_atom_v, + model_name, + model_data_json); + } } push_to_prefs(model_name); @@ -713,6 +1000,7 @@ void GlobalUIModelData::push_to_prefs(const std::string &model_name, const bool try { check_model_is_registered(model_name); + if (models_[model_name]->preference_path_.empty()) return; @@ -727,7 +1015,7 @@ void GlobalUIModelData::push_to_prefs(const std::string &model_name, const bool models_[model_name]->pending_prefs_update_ = true; delayed_anon_send( caf::actor_cast(this), - std::chrono::seconds(20), + std::chrono::seconds(2), json_store::update_atom_v, model_name); } else if (actually_push) { @@ -743,7 +1031,11 @@ void GlobalUIModelData::push_to_prefs(const std::string &model_name, const bool } } -void GlobalUIModelData::node_activated(const std::string &model_name, const std::string &path) { +void GlobalUIModelData::node_activated( + const std::string &model_name, + const std::string &path, + const std::string &user_data, + const bool from_hotkey) { try { @@ -754,13 +1046,21 @@ void GlobalUIModelData::node_activated(const std::string &model_name, const std: if (!path.empty()) { nlohmann::json::json_pointer ptr(path); auto &j = pointer_to_tree(model_data, "children", ptr)->data(); + if (j.contains("uuid")) { utility::Uuid uuid(j["uuid"].get()); auto p = models_[model_name]->menu_watchers_.find(uuid); if (p != models_[model_name]->menu_watchers_.end()) { auto &watchers = p->second; for (auto watcher : watchers) { - send(watcher, utility::event_atom_v, menu_node_activated_atom_v, path); + mail( + utility::event_atom_v, + menu_node_activated_atom_v, + path, + utility::JsonStore(j), + user_data, + from_hotkey) + .send(watcher); } } } @@ -771,7 +1071,6 @@ void GlobalUIModelData::node_activated(const std::string &model_name, const std: } } - void GlobalUIModelData::insert_into_menu_model( const std::string &model_name, const std::string &menu_path, @@ -781,85 +1080,281 @@ void GlobalUIModelData::insert_into_menu_model( check_model_is_registered(model_name, true); - utility::JsonTree *menu_model_data = &(models_[model_name]->data_); + // store_wilcarded_menu_item(model_name, menu_path, menu_data, watcher); - // If menu path is, say "Publish|Annotations|Latest", we need to find - // the top level node where "name"=="Publish". Then we search that node's - // children for one where "name"=="Annotaitons". Then we search that - // node's children for one where "name"=="Latest". If we do not find - // the matching child, we break as this is where we need to start - // creating children items to build the sub-menu heirachy - std::vector parent_menus = utility::split(menu_path, '|'); + auto menu_models = get_menu_models(model_name); - while (parent_menus.size()) { - try { + for (auto mmn : menu_models) { + + utility::JsonTree *menu_model_data = &(mmn->data_); + const std::string &model_name = mmn->name_; + + // If menu path is, say "Publish|Annotations|Latest", we need to find + // the top level node where "name"=="Publish". Then we search that node's + // children for one where "name"=="Annotaitons". Then we search that + // node's children for one where "name"=="Latest". If we do not find + // the matching child, we break as this is where we need to start + // creating children items to build the sub-menu heirachy + std::vector parent_menus = utility::split(menu_path, '|'); + + while (parent_menus.size()) { + auto node = find_node_matching_string_field( + menu_model_data, "name", parent_menus.front(), 1); + + if (!node) + break; + + menu_model_data = node; + + parent_menus.erase(parent_menus.begin()); + } + + while (parent_menus.size()) { + + nlohmann::json data; + data["name"] = parent_menus.front(); + data["menu_item_type"] = "menu"; + menu_model_data = add_node(menu_model_data, data); + parent_menus.erase(parent_menus.begin()); + } + + bool already_defined = false; + + for (auto p = menu_model_data->begin(); p != menu_model_data->end(); p++) { + if ((*p).data().is_object() && + (*p).data().value("uuid", std::string()) == menu_data["uuid"]) { + already_defined = true; + menu_model_data = &(*p); + break; + } + } + + if (!already_defined) { + menu_model_data = &(*(menu_model_data->insert( + std::next(menu_model_data->begin(), menu_model_data->size()), menu_data))); + } else { + // update - copy back in key/values that were there before but + // not defined in 'menu_data' + const auto old_data = menu_model_data->data(); + menu_model_data->data() = menu_data; + if (old_data.is_object() && menu_data.is_object()) { + for (auto it = old_data.begin(); it != old_data.end(); it++) { + if (!menu_data.contains(it.key())) { + menu_model_data->data()[it.key()] = it.value(); + } + } + } + } + + if (menu_model_data->data().contains("hotkey_uuid")) { + + utility::Uuid hotkey_uuid(menu_model_data->data()["hotkey_uuid"]); + if (!hotkey_uuid.is_null()) { + + mmn->hotkeys_.insert(hotkey_uuid); + auto keyboard_manager = + system().registry().template get(keyboard_events); + caf::scoped_actor sys(system()); + try { + auto hotkey = utility::request_receive( + *sys, + keyboard_manager, + ui::keypress_monitor::hotkey_atom_v, + hotkey_uuid); + menu_model_data->data()["hotkey_sequence"] = hotkey.hotkey_sequence(); + + if (!menu_model_data->data().contains("name")) { + // we've been asked to add a menu item based off a hotkey + // that's alread been defined + menu_model_data->data()["name"] = hotkey.hotkey_name(); + } + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + } + } + + if (menu_model_data->parent()) + do_ordering(menu_model_data->parent()); + + utility::Uuid uuid(menu_data["uuid"].get()); + auto &watchers = models_[model_name]->menu_watchers_[uuid]; + if (std::find(watchers.begin(), watchers.end(), watcher) == watchers.end()) { + watchers.push_back(watcher); + } + monitor(watcher); + broadcast_whole_model_data(model_name); + } + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + +/*void GlobalUIModelData::store_wilcarded_menu_item( + const std::string &model_name, + const std::string &menu_path, + const utility::JsonStore &menu_data, + caf::actor watcher) +{ + if (model_name.find("*") != std::string::npos) { + + // someone wants to create a menu item that is added to all instances + // of a menu model matching the wildcard. We need to keep a record + // of these in case a matching menu model is created *after* the + // wildcarded menu model item was instances + + + } +}*/ + +void GlobalUIModelData::set_menu_node_position( + const std::string &model_name, const std::string &menu_path, const float position_in_menu) { + utility::JsonTree *menu_model_data = nullptr; + try { + + check_model_is_registered(model_name, true); + + auto menu_models = get_menu_models(model_name); + for (auto mmn : menu_models) { + + menu_model_data = &(mmn->data_); + + // see note in insert_into_menu_model for an explanation of this + std::vector parent_menus = utility::split(menu_path, '|'); + while (parent_menus.size()) { + // this call will throw an exception if there isn't a menu + // with a name matching the first element in parent_menus + // which would mean that 'menu_path' does not match an existing + // menu item in the menu model menu_model_data = find_node_matching_string_field( menu_model_data, "name", parent_menus.front()); if (!menu_model_data) { - throw std::runtime_error("Failed to find expected field"); + throw std::runtime_error(fmt::format("No such menu {}", menu_path).c_str()); } parent_menus.erase(parent_menus.begin()); - } catch ([[maybe_unused]] std::exception &e) { - // exception is thrown if we fail to find a match - break; } + + menu_model_data->data()["menu_item_position"] = position_in_menu; + + if (menu_model_data->parent()) { + do_ordering(menu_model_data->parent()); + } + + broadcast_whole_model_data(model_name); } - while (parent_menus.size()) { + } catch (std::exception &e) { + + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} - nlohmann::json data; - data["name"] = parent_menus.front(); - data["menu_item_type"] = "menu"; - menu_model_data = add_node(menu_model_data, data); - parent_menus.erase(parent_menus.begin()); +GlobalUIModelData::ModelDataVec +GlobalUIModelData::get_menu_models(const std::string model_name) { + + // One awkward problem with how we create menus items in QML by declaring + // XsMenuItem{} is that if the parent object is instanced multiple times + // we have multiple declarations of the menu item trying to insert themselves + // into the same backend menu model data. To get around this, we can + // create a unique menu model for each instance of that parent object by + // making a unique menu model name - so in other words the menu model data + // itself has multiple instances managed by this class (GlobalUIModelData). + // + // This way given instances of an actual menu in the UI will be connected + // to unique instances of the associated XsMenuItem. However, this presents + // another problem: if we have a singleton backend C++ plugin that wants to + // expose menu items in the UI, how does it connect to these multiple menu + // models? + // The solution is that the backend object uses a wildcard '*' at the end + // of the menu model name so that the menu item it is creating gets inserted + // to all matching menu models. + // + + // So in the frontend we might have this: + + /* + + Item { + id: the_parent + + // make a unique menu model name for every instance of this Item + property string menuModelName: "MyPluginMenu" + the_parent + + // declare menu items: + XsMenuItem { + menu_model_name: menuModelName + } + ... + XsMenuModel { + id: the_model + menu_model_name: menuModelName } + ... + XsPopupMenu { + menu_mode: the_model + } + } - bool already_defined = false; - for (auto p = menu_model_data->begin(); p != menu_model_data->end(); p++) { - if ((*p).data().is_object() && - (*p).data().value("uuid", std::string()) == menu_data["uuid"]) { - already_defined = true; - menu_model_data = &(*p); - break; + // and it the backend plugin, to insert menu items into all instances of + // this menu: + + void MyPlugin::setup_menus() { + + // here we can add a simple menu item - we get a callback with the uuid + // in menu_item_activated when the user clicks on it + my_menu_item_ = insert_menu_item( + "MyPluginMenu*", // N.B. note the wildcard! + "Backend Multichoice", + "Backend Example|Submenu 1|Submenu 2", + 0.0f); + } + + */ + + + ModelDataVec result; + if (model_name.find("*") == (model_name.size() - 1)) { + + // we have a model with a wildcard in the name, so find all matching models + std::string match_name(model_name, 0, (model_name.size() - 1)); + for (auto p : models_) { + if (p.first.find(match_name) == 0 || p.first == model_name) { + result.push_back(p.second); } } - if (!already_defined) { - menu_model_data->insert( - std::next(menu_model_data->begin(), menu_model_data->size()), menu_data); + } else { + auto p = models_.find(model_name); + if (p != models_.end()) { + result.push_back(p->second); } else { - // update - menu_model_data->data() = menu_data; - } - - utility::Uuid uuid(menu_data["uuid"].get()); - auto &watchers = models_[model_name]->menu_watchers_[uuid]; - if (std::find(watchers.begin(), watchers.end(), watcher) == watchers.end()) { - watchers.push_back(watcher); + std::stringstream ss; + ss << "No such model \"" << model_name << "\" registered with GlobalUIModelData"; + throw std::runtime_error(ss.str().c_str()); } - monitor(watcher); - broadcast_whole_model_data(model_name); - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } -} + return result; +} void GlobalUIModelData::remove_node( const std::string &model_name, const utility::Uuid &model_item_id) { try { check_model_is_registered(model_name, true); + auto menu_models = get_menu_models(model_name); + for (auto mmn : menu_models) { - auto *menu_model_data = &(models_[model_name]->data_); + utility::JsonTree *menu_model_data = &(mmn->data_); - std::string uuid_string = to_string(model_item_id); - std::string field("uuid"); - if (find_and_delete(menu_model_data, field, uuid_string)) { - broadcast_whole_model_data(model_name); + std::string uuid_string = to_string(model_item_id); + std::string field("uuid"); + if (find_and_delete(menu_model_data, field, uuid_string)) { + broadcast_whole_model_data(mmn->name_); + } } - } catch (std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } @@ -881,3 +1376,125 @@ void GlobalUIModelData::broadcast_whole_model_data(const std::string &model_name models_to_be_fully_broadcasted_.insert(model_name); } } + +void GlobalUIModelData::do_ordering(utility::JsonTree *node, const std::string &ordering_key) { + + auto comp = [&ordering_key]( + const utility::JsonTree &branchA, + const utility::JsonTree &branchB) -> bool { + return branchA.data().value(ordering_key, 0.0f) < + branchB.data().value(ordering_key, 0.0f); + }; + + // order the children of the node by their "menu_item_position" (if there + // is such data) + node->do_sort(comp); +} + +void GlobalUIModelData::update_hotkeys_model_data(const Hotkey &hotkey) { + for (auto &p : models_) { + if (p.second->hotkeys_.find(hotkey.uuid()) != p.second->hotkeys_.end()) { + + try { + + auto menu_model_data = find_node_matching_string_field( + &(p.second->data_), "hotkey_uuid", to_string(hotkey.uuid())); + if (!menu_model_data) { + throw std::runtime_error( + fmt::format("No such hotkey {}", p.second->name_).c_str()); + } + + set_data( + p.second->name_, + path_from_node(menu_model_data), + utility::JsonStore(hotkey.hotkey_sequence()), + "hotkey_sequence"); + + } catch (std::exception &e) { + spdlog::debug("{} {} {}", __PRETTY_FUNCTION__, e.what(), hotkey.hotkey_name()); + } + } + } +} + +void GlobalUIModelData::hotkey_pressed( + const utility::Uuid &hotkey_uuid, const std::string &context, const std::string &window) { + // a hotkey has been pressed and we've beein informed by the keypress_monitor + // class + + // Are any of our models linked to this hotkey? + for (auto &p : models_) { + + if (p.second->hotkeys_.find(hotkey_uuid) != p.second->hotkeys_.end()) { + + try { + + // find the menu item that is linked to the hotkey + auto menu_model_data = find_node_matching_string_field( + &(p.second->data_), "hotkey_uuid", to_string(hotkey_uuid)); + if (!menu_model_data) { + throw std::runtime_error( + fmt::format("No such hotkey {}", to_string(hotkey_uuid)).c_str()); + } + + if (p.second->name_ == "popout windows" && menu_model_data) { + // special case .. the "popout windows" model is the button tray + // in the top left of the viewport panels that shows/hides some + // registered ui elements in pop-out windows... we can use + // a hotkey to toggle them. That's what's happening now. + // We will toggle the "window_is_visible" role data + if (menu_model_data->data().contains("window_is_visible") && + menu_model_data->data()["window_is_visible"].is_boolean()) { + auto v = nlohmann::json( + !menu_model_data->data()["window_is_visible"].get()); + set_data( + p.second->name_, + path_from_node(menu_model_data), + v, + "window_is_visible"); + return; + } + } + + // run our 'activated' method which will trigger an 'activated' + // signal on any XsMenuItem that are hooked to this node + node_activated(p.second->name_, path_from_node(menu_model_data), context, true); + + } catch (std::exception &e) { + spdlog::warn( + "{} {} for model {}", __PRETTY_FUNCTION__, e.what(), p.second->name_); + } + } + } +} + +void GlobalUIModelData::reset_model( + const std::string &model_name, const std::string &preferences_path) { + + check_model_is_registered(model_name, true); + + // load data from preferennces + auto prefs_helper = global_store::GlobalStoreHelper(system()); + auto data_from_prefs = prefs_helper.default_value(preferences_path); + models_[model_name]->data_ = utility::json_to_tree(data_from_prefs, "children"); + broadcast_whole_model_data(model_name); +} + +void GlobalUIModelData::reset_model( + const std::string &model_name, const utility::JsonStore &data, caf::actor excluded_client) { + + try { + check_model_is_registered(model_name, true); + models_[model_name]->data_ = utility::json_to_tree(data, "children"); + const auto model_data = model_data_as_json(model_name); + for (auto &client : models_[model_name]->clients_) { + if (client == excluded_client) + continue; + anon_send(client, utility::event_atom_v, model_data_atom_v, model_name, model_data); + } + push_to_prefs(model_name, false); + + } catch (std::exception &e) { + spdlog::warn("{} {} for model {}", __PRETTY_FUNCTION__, e.what(), model_name); + } +} \ No newline at end of file diff --git a/src/ui/opengl/src/CMakeLists.txt b/src/ui/opengl/src/CMakeLists.txt index 3d3eaa1cb..0d373d9b5 100644 --- a/src/ui/opengl/src/CMakeLists.txt +++ b/src/ui/opengl/src/CMakeLists.txt @@ -1,12 +1,14 @@ SET(LINK_DEPS - ${CAF_LIBRARY_core} + CAF::core OpenGL::GL OpenGL::GLU GLEW::GLEW xstudio::ui::base xstudio::ui::canvas + xstudio::ui::viewport xstudio::utility xstudio::media_reader + xstudio::colour_pipeline OpenEXR::OpenEXR Imath::Imath ) @@ -19,16 +21,19 @@ find_package(OpenGL REQUIRED) find_package(GLEW REQUIRED) find_package(OpenEXR) find_package(Imath) -if(WIN32) -find_package(freetype CONFIG REQUIRED) -else() -find_package(Freetype) -include_directories("${FREETYPE_INCLUDE_DIRS}") -endif() -create_component_with_alias(opengl_viewport xstudio::ui::opengl::viewport 0.1.0 "${LINK_DEPS}") +create_component_with_alias(opengl_viewport xstudio::ui::opengl::viewport ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}") +if (USE_VCPKG) + find_package(freetype CONFIG REQUIRED) +else() + find_package(Freetype REQUIRED) + target_include_directories(opengl_viewport PUBLIC "${FREETYPE_INCLUDE_DIRS}") +endif() +if (APPLE) +target_compile_definitions(opengl_viewport PRIVATE GL_SILENCE_DEPRECATION=1) +endif() target_link_libraries(${PROJECT_NAME} PRIVATE diff --git a/src/ui/opengl/src/gl_debug_utils.cpp b/src/ui/opengl/src/gl_debug_utils.cpp index 8435e456c..c4a952147 100644 --- a/src/ui/opengl/src/gl_debug_utils.cpp +++ b/src/ui/opengl/src/gl_debug_utils.cpp @@ -33,4 +33,118 @@ void grab_framebuffer_to_disk() { } } +#ifdef OPENGL_DEBUG_CB + +void debug_message_callback( + GLenum source, + GLenum type, + GLuint id, + GLenum severity, + GLsizei length, + const GLchar *msg, + const void *data) { + + std::string _source; + std::string _type; + std::string _severity; + + switch (source) { + case GL_DEBUG_SOURCE_API: + _source = "API"; + break; + + case GL_DEBUG_SOURCE_WINDOW_SYSTEM: + _source = "WINDOW SYSTEM"; + break; + + case GL_DEBUG_SOURCE_SHADER_COMPILER: + _source = "SHADER COMPILER"; + break; + + case GL_DEBUG_SOURCE_THIRD_PARTY: + _source = "THIRD PARTY"; + break; + + case GL_DEBUG_SOURCE_APPLICATION: + _source = "APPLICATION"; + break; + + case GL_DEBUG_SOURCE_OTHER: + _source = "OTHER"; + break; + + default: + _source = "UNKNOWN"; + break; + } + + switch (type) { + case GL_DEBUG_TYPE_ERROR: + _type = "ERROR"; + break; + + case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: + _type = "DEPRECATED BEHAVIOR"; + break; + + case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: + _type = "UNDEFINED BEHAVIOR"; + break; + + case GL_DEBUG_TYPE_PORTABILITY: + _type = "PORTABILITY"; + break; + + case GL_DEBUG_TYPE_PERFORMANCE: + _type = "PERFORMANCE"; + break; + + case GL_DEBUG_TYPE_OTHER: + _type = "OTHER"; + break; + + case GL_DEBUG_TYPE_MARKER: + _type = "MARKER"; + break; + + default: + _type = "UNKNOWN"; + break; + } + + switch (severity) { + case GL_DEBUG_SEVERITY_HIGH: + _severity = "HIGH"; + break; + + case GL_DEBUG_SEVERITY_MEDIUM: + _severity = "MEDIUM"; + break; + + case GL_DEBUG_SEVERITY_LOW: + _severity = "LOW"; + break; + + case GL_DEBUG_SEVERITY_NOTIFICATION: + _severity = "NOTIFICATION"; + break; + + default: + _severity = "UNKNOWN"; + break; + } + + if (severity == GL_DEBUG_SEVERITY_HIGH) { + std::cerr << id << ": " << _type << " of " << _severity << " severity, raised from " + << _source << ": " << msg << "\n"; + } + + // Uncomment to allow simple gdb usage, if GL_DEBUG_OUTPUT_SYNCHRONOUS + // is enabled, the stacktrace in gdb will directly point to the + // problematic opengl call. + // std::raise(SIGINT); +} + +#endif + } // namespace xstudio::ui::opengl \ No newline at end of file diff --git a/src/ui/opengl/src/gl_debug_utils.hpp b/src/ui/opengl/src/gl_debug_utils.hpp deleted file mode 100644 index d5e07f56c..000000000 --- a/src/ui/opengl/src/gl_debug_utils.hpp +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -namespace xstudio { -namespace ui { - namespace opengl { - - // reads the entire GL viewport into a buffer and writes out - // to EXR in /user_data/.tmp/xstudio_viewport.%04d.exr - // incrementing frame number on every draw from 1 - void grab_framebuffer_to_disk(); - - } // namespace opengl -} // namespace ui -} // namespace xstudio \ No newline at end of file diff --git a/src/ui/opengl/src/opengl_canvas_renderer.cpp b/src/ui/opengl/src/opengl_canvas_renderer.cpp index 1a528c8c1..134607f77 100644 --- a/src/ui/opengl/src/opengl_canvas_renderer.cpp +++ b/src/ui/opengl/src/opengl_canvas_renderer.cpp @@ -13,6 +13,7 @@ OpenGLCanvasRenderer::OpenGLCanvasRenderer() { stroke_renderer_.reset(new OpenGLStrokeRenderer()); caption_renderer_.reset(new OpenGLCaptionRenderer()); + shape_renderer_.reset(new OpenGLShapeRenderer()); } void OpenGLCanvasRenderer::render_canvas( @@ -21,22 +22,43 @@ void OpenGLCanvasRenderer::render_canvas( const Imath::M44f &transform_window_to_viewport_space, const Imath::M44f &transform_viewport_to_image_space, const float viewport_du_dpixel, - const bool have_alpha_buffer) { + const bool have_alpha_buffer, + const float image_aspectratio) { if (canvas.empty()) return; - stroke_renderer_->render_strokes( - all_canvas_items(canvas), - transform_window_to_viewport_space, - transform_viewport_to_image_space, - viewport_du_dpixel, - have_alpha_buffer); - - caption_renderer_->render_captions( - all_canvas_items(canvas), - handle_state, - transform_window_to_viewport_space, - transform_viewport_to_image_space, - viewport_du_dpixel); + const auto &strokes = all_canvas_items(canvas); + if (!strokes.empty()) { + stroke_renderer_->render_strokes( + strokes, + transform_window_to_viewport_space, + transform_viewport_to_image_space, + viewport_du_dpixel, + have_alpha_buffer); + } + + const auto &captions = all_canvas_items(canvas); + if (!captions.empty()) { + caption_renderer_->render_captions( + captions, + handle_state, + transform_window_to_viewport_space, + transform_viewport_to_image_space, + viewport_du_dpixel); + } + + const auto &quads = all_canvas_items(canvas); + const auto &polygons = all_canvas_items(canvas); + const auto &ellipses = all_canvas_items(canvas); + if (!quads.empty() || !polygons.empty() || !ellipses.empty()) { + shape_renderer_->render_shapes( + quads, + polygons, + ellipses, + transform_window_to_viewport_space, + transform_viewport_to_image_space, + viewport_du_dpixel, + image_aspectratio); + } } diff --git a/src/ui/opengl/src/opengl_caption_renderer.cpp b/src/ui/opengl/src/opengl_caption_renderer.cpp index 3e3cf5c72..454022649 100644 --- a/src/ui/opengl/src/opengl_caption_renderer.cpp +++ b/src/ui/opengl/src/opengl_caption_renderer.cpp @@ -8,7 +8,7 @@ using namespace xstudio::ui::opengl; namespace { const char *flat_color_vertex_shader = R"( - #version 430 core + #version 410 core uniform mat4 to_coord_system; uniform mat4 to_canvas; layout (location = 0) in vec2 aPos; diff --git a/src/ui/opengl/src/opengl_colour_lut_texture.cpp b/src/ui/opengl/src/opengl_colour_lut_texture.cpp new file mode 100644 index 000000000..a0c0c8954 --- /dev/null +++ b/src/ui/opengl/src/opengl_colour_lut_texture.cpp @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include + +#include "xstudio/ui/opengl/opengl_colour_lut_texture.hpp" +#include "xstudio/utility/chrono.hpp" + +using namespace xstudio::ui::opengl; + +GLint GLColourLutTexture::interpolation() { + switch (descriptor_.interpolation_) { + case colour_pipeline::LUTDescriptor::NEAREST: + return GL_NEAREST; + case colour_pipeline::LUTDescriptor::LINEAR: + return GL_LINEAR; + default: + return GL_LINEAR; + } +} + +GLint GLColourLutTexture::internal_format() { + if (descriptor_.channels_ == colour_pipeline::LUTDescriptor::RGB) { + switch (descriptor_.data_type_) { + case colour_pipeline::LUTDescriptor::UINT8: + return GL_RGB8UI; + case colour_pipeline::LUTDescriptor::UINT16: + return GL_RGB16UI; + case colour_pipeline::LUTDescriptor::FLOAT16: + return GL_RGB16F; + case colour_pipeline::LUTDescriptor::FLOAT32: + return GL_RGB32F; + default: + return GL_RGB32F; + } + } else { + switch (descriptor_.data_type_) { + case colour_pipeline::LUTDescriptor::UINT8: + return GL_R8UI; + case colour_pipeline::LUTDescriptor::UINT16: + return GL_R16UI; + case colour_pipeline::LUTDescriptor::FLOAT16: + return GL_R16F; + case colour_pipeline::LUTDescriptor::FLOAT32: + return GL_R32F; + default: + return GL_R32F; + } + } +} + +GLint GLColourLutTexture::format() { + if (descriptor_.channels_ == colour_pipeline::LUTDescriptor::RGB) { + + switch (descriptor_.data_type_) { + case colour_pipeline::LUTDescriptor::UINT8: + return GL_RGB_INTEGER; + case colour_pipeline::LUTDescriptor::UINT16: + return GL_RGB_INTEGER; + case colour_pipeline::LUTDescriptor::FLOAT16: + return GL_RGB; + case colour_pipeline::LUTDescriptor::FLOAT32: + return GL_RGB; + default: + return GL_RGB; + } + } else { + switch (descriptor_.data_type_) { + case colour_pipeline::LUTDescriptor::UINT8: + return GL_RED_INTEGER; + case colour_pipeline::LUTDescriptor::UINT16: + return GL_RED_INTEGER; + case colour_pipeline::LUTDescriptor::FLOAT16: + return GL_RED; + case colour_pipeline::LUTDescriptor::FLOAT32: + return GL_RED; + default: + return GL_RED; + } + } +} + +GLint GLColourLutTexture::data_type() { + switch (descriptor_.data_type_) { + case colour_pipeline::LUTDescriptor::UINT8: + return GL_UNSIGNED_BYTE; + case colour_pipeline::LUTDescriptor::UINT16: + return GL_UNSIGNED_SHORT; + case colour_pipeline::LUTDescriptor::FLOAT16: + return GL_HALF_FLOAT; + case colour_pipeline::LUTDescriptor::FLOAT32: + return GL_FLOAT; + default: + return GL_FLOAT; + } +} + + +GLColourLutTexture::GLColourLutTexture( + const colour_pipeline::LUTDescriptor desc, const std::string texture_name) + : descriptor_(std::move(desc)), texture_name_(std::move(texture_name)) { + // Create the texture for RGB float display and the Y component of YUV display + glGenTextures(1, &tex_id_); + + if (desc.dimension_ == colour_pipeline::LUTDescriptor::ONE_D) { + glBindTexture(target(), tex_id_); + glTexParameteri(target(), GL_TEXTURE_MIN_FILTER, interpolation()); + glTexParameteri(target(), GL_TEXTURE_MAG_FILTER, interpolation()); + glTexParameteri(target(), GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage1D( + target(), 0, internal_format(), desc.xsize_, 0, format(), data_type(), nullptr); + + } else if (desc.dimension_ & colour_pipeline::LUTDescriptor::TWO_D) { + glBindTexture(target(), tex_id_); + glTexParameteri(target(), GL_TEXTURE_MIN_FILTER, interpolation()); + glTexParameteri(target(), GL_TEXTURE_MAG_FILTER, interpolation()); + glTexParameteri(target(), GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(target(), GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D( + target(), + 0, + internal_format(), + desc.xsize_, + desc.ysize_, + 0, + format(), + data_type(), + nullptr); + + } else if (desc.dimension_ == colour_pipeline::LUTDescriptor::THREE_D) { + glBindTexture(target(), tex_id_); + glTexParameteri(target(), GL_TEXTURE_MIN_FILTER, interpolation()); + glTexParameteri(target(), GL_TEXTURE_MAG_FILTER, interpolation()); + glTexParameteri(target(), GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(target(), GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(target(), GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage3D( + target(), + 0, + internal_format(), + desc.xsize_, + desc.ysize_, + desc.zsize_, + 0, + format(), + data_type(), + nullptr); + } + + glGenBuffers(1, &pbo_); +} + +GLColourLutTexture::~GLColourLutTexture() { + glDeleteTextures(1, &tex_id_); + glDeleteBuffers(1, &pbo_); +} + +GLenum GLColourLutTexture::target() const { + if (descriptor_.dimension_ == colour_pipeline::LUTDescriptor::ONE_D) + return GL_TEXTURE_1D; + else if (descriptor_.dimension_ == colour_pipeline::LUTDescriptor::TWO_D) + return GL_TEXTURE_2D; + else if (descriptor_.dimension_ == colour_pipeline::LUTDescriptor::THREE_D) + return GL_TEXTURE_3D; + else if (descriptor_.dimension_ == colour_pipeline::LUTDescriptor::RECT_TWO_D) + return GL_TEXTURE_RECTANGLE; + + return GL_TEXTURE_RECTANGLE; +} + + +void GLColourLutTexture::upload_texture_data(const colour_pipeline::ColourLUTPtr &lut) { + + if (lut->cache_id() == lut_cache_id_) + return; + + lut_cache_id_ = lut->cache_id(); + + // Create the texture for RGGLColourLutTextureB float display and the Y component of YUV + // display + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_); + glBufferData(GL_PIXEL_UNPACK_BUFFER, lut->data_size(), nullptr, GL_STREAM_DRAW); + + void *mappedBuffer = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY); + memcpy(mappedBuffer, lut->data(), lut->data_size()); + + glBindTexture(target(), tex_id_); + glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); + + if (descriptor_.dimension_ == colour_pipeline::LUTDescriptor::ONE_D) { + + + glTexSubImage1D(target(), 0, 0, descriptor_.xsize_, format(), data_type(), nullptr); + + + } else if (descriptor_.dimension_ & colour_pipeline::LUTDescriptor::TWO_D) { + + glTexSubImage2D( + target(), + 0, + 0, + 0, + descriptor_.xsize_, + descriptor_.ysize_, + format(), + data_type(), + nullptr); + + } else if (descriptor_.dimension_ == colour_pipeline::LUTDescriptor::THREE_D) { + + glTexSubImage3D( + target(), + 0, + 0, + 0, + 0, + descriptor_.xsize_, + descriptor_.ysize_, + descriptor_.zsize_, + format(), + data_type(), + nullptr); + } + // glActiveTexture((GLenum)(GL_TEXTURE0)); + glBindTexture(target(), 0); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); +} + +void GLColourLutTexture::bind(int tex_index) { + glActiveTexture(tex_index + GL_TEXTURE0); + glBindTexture(target(), tex_id_); +} \ No newline at end of file diff --git a/src/ui/opengl/src/opengl_multi_buffered_texture.cpp b/src/ui/opengl/src/opengl_multi_buffered_texture.cpp new file mode 100644 index 000000000..b5fe52fb5 --- /dev/null +++ b/src/ui/opengl/src/opengl_multi_buffered_texture.cpp @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include + +#include "xstudio/ui/opengl/opengl_multi_buffered_texture.hpp" +#include "xstudio/utility/chrono.hpp" + +using namespace xstudio::ui::opengl; + +namespace { + +class DebugTimer { + public: + DebugTimer(std::string m) : message_(std::move(m)) { t1_ = xstudio::utility::clock::now(); } + + ~DebugTimer() { + std::cerr << message_ << " completed in " + << std::chrono::duration_cast( + xstudio::utility::clock::now() - t1_) + .count() + << " microseconds\n"; + } + + private: + xstudio::utility::clock::time_point t1_; + const std::string message_; +}; + +} // namespace + +namespace xstudio::ui::opengl { + +class TextureTransferWorker { + public: + TextureTransferWorker(GLDoubleBufferedTexture *owner) { + for (int i = 0; i < 8; ++i) { + threads_.emplace_back(std::thread(&TextureTransferWorker::run, this)); + } + owner_ = owner; + } + + ~TextureTransferWorker() { + + for (auto &t : threads_) { + // when any thread picks up an empty job it exits its loop + add_job(GLDoubleBufferedTexture::GLBlindTexturePtr()); + } + + for (auto &t : threads_) { + t.join(); + } + } + + std::vector threads_; + + GLDoubleBufferedTexture::GLBlindTexturePtr get_job() { + std::unique_lock lk(m); + if (queue.empty()) { + cv.wait(lk, [=] { return !queue.empty(); }); + } + auto rt = queue.front(); + queue.pop_front(); + return rt; + } + + void clear_jobs() { + std::lock_guard lk(m); + while (queue.size()) { + auto rt = queue.front(); + rt->cancel_upload(); + queue.pop_front(); + } + } + + void add_job(GLDoubleBufferedTexture::GLBlindTexturePtr ptr) { + + { + std::lock_guard lk(m); + queue.push_back(ptr); + } + cv.notify_one(); + } + + void run() { + while (1) { + + // this blocks until there is something in queue for us + GLDoubleBufferedTexture::GLBlindTexturePtr tex = get_job(); + if (!tex) + break; // exit + tex->do_pixel_upload(); + } + } + + std::mutex m; + std::condition_variable cv; + std::deque queue; + GLDoubleBufferedTexture *owner_; +}; +} // namespace xstudio::ui::opengl + +GLDoubleBufferedTexture::GLDoubleBufferedTexture() { + + worker_ = new TextureTransferWorker(this); +} + +void GLDoubleBufferedTexture::bind( + const media_reader::ImageBufPtr &image, int &tex_index, Imath::V2i &dims) { + + auto p = textures_.find_pending(image); + if (p != textures_.end()) { + // the image is already uploaded to one of our unused textures + (*p)->bind(tex_index, dims); + return; + } +} + +void GLDoubleBufferedTexture::queue_image_set_for_upload( + const media_reader::ImageBufDisplaySetPtr &image_set) { + + worker_->clear_jobs(); + + // image_set includes images that are due to go on-screen NOW in the + // current draw, plus images that are not going on screen now but will + // be soon. + // We can do a-sync uploads of images to texture memory for performance + // optimisation so that images due on screen soon are being uploaded while + // we draw the images that are going on-screen now etc. + + // First, we loop over the 'onscreen_images' and put them in our ordered + // queue of images that need to be in texture memory + image_queue_.clear(); + + const std::vector &draw_order = image_set->layout_data()->image_draw_order_hint_; + + auto available_textures = textures_; + + for (const auto &i : draw_order) { + auto im = image_set->onscreen_image(i); + queue_for_upload(available_textures, im); + } + + // now queue images the we'll need in the *next* redraw + for (const auto &i : draw_order) { + if (image_set->future_images(i).size()) { + auto im = image_set->future_images(i)[0]; + queue_for_upload(available_textures, im); + } + } + + // now queue images the we'll need in the *next* *next* redraw + for (const auto &i : draw_order) { + if (image_set->future_images(i).size() > 1) { + auto im = image_set->future_images(i)[1]; + queue_for_upload(available_textures, im); + } + } +} + +void GLDoubleBufferedTexture::queue_for_upload( + GLDoubleBufferedTexture::TexSet &available_textures, + const media_reader::ImageBufPtr &image) { + if (!image) + return; + auto q = available_textures.find_pending(image); + if (q != available_textures.end()) { + available_textures.erase(q); + } else if (available_textures.size()) { + q = available_textures.begin(); + (*q)->prepare_for_upload(image); + worker_->add_job(*q); + available_textures.erase(q); + } else { + image_queue_.push_back(image); + } +} + +void GLDoubleBufferedTexture::release(const media_reader::ImageBufPtr &image) { + + // move the texture containing 'image' into the used_textures_ map so + // it can be used for another image + + if (image_queue_.size()) { + auto im = image_queue_.front(); + auto q = textures_.find(image); + if (q != textures_.end()) { + (*q)->prepare_for_upload(im); + worker_->add_job(*q); + image_queue_.pop_front(); + } else { + // std::cerr << "Didn't release\n"; + } + } +} diff --git a/src/ui/opengl/src/opengl_offscreen_renderer.cpp b/src/ui/opengl/src/opengl_offscreen_renderer.cpp index 96a6d9afb..d78a9dcf7 100644 --- a/src/ui/opengl/src/opengl_offscreen_renderer.cpp +++ b/src/ui/opengl/src/opengl_offscreen_renderer.cpp @@ -16,6 +16,8 @@ void OpenGLOffscreenRenderer::resize(const Imath::V2f &dims) { return; } + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &restore_fbo_id_); + cleanup(); fbo_dims_ = dims; @@ -33,6 +35,7 @@ void OpenGLOffscreenRenderer::resize(const Imath::V2f &dims) { glGenRenderbuffers(1, &rbo_id_); glBindRenderbuffer(GL_RENDERBUFFER, rbo_id_); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, w, h); + glBindRenderbuffer(GL_RENDERBUFFER, 0); glGenFramebuffers(1, &fbo_id_); glBindFramebuffer(GL_FRAMEBUFFER, fbo_id_); @@ -41,7 +44,7 @@ void OpenGLOffscreenRenderer::resize(const Imath::V2f &dims) { glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo_id_); - glBindFramebuffer(GL_FRAMEBUFFER, 0); + glBindFramebuffer(GL_FRAMEBUFFER, restore_fbo_id_); } void OpenGLOffscreenRenderer::begin() { @@ -49,15 +52,15 @@ void OpenGLOffscreenRenderer::begin() { unsigned int w = fbo_dims_.x; unsigned int h = fbo_dims_.y; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &restore_fbo_id_); glGetIntegerv(GL_VIEWPORT, vp_state_.data()); - glViewport(0, 0, w, h); - glBindFramebuffer(GL_FRAMEBUFFER, fbo_id_); + glViewport(0, 0, w, h); } void OpenGLOffscreenRenderer::end() { - glBindFramebuffer(GL_FRAMEBUFFER, 0); - + // restore bound FB + glBindFramebuffer(GL_FRAMEBUFFER, restore_fbo_id_); // Restore viewport state glViewport(vp_state_[0], vp_state_[1], vp_state_[2], vp_state_[3]); } diff --git a/src/ui/opengl/src/opengl_rgba8bit_image_texture.cpp b/src/ui/opengl/src/opengl_rgba8bit_image_texture.cpp new file mode 100644 index 000000000..7e62c1646 --- /dev/null +++ b/src/ui/opengl/src/opengl_rgba8bit_image_texture.cpp @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include + +#include "xstudio/ui/opengl/opengl_rgba8bit_image_texture.hpp" +#include "xstudio/utility/chrono.hpp" + +using namespace xstudio::ui::opengl; + +GLBlindRGBA8bitTex::~GLBlindRGBA8bitTex() { + // ensure no copying is in flight + if (upload_thread_.joinable()) + upload_thread_.join(); + + glDeleteTextures(1, &tex_id_); + glDeleteBuffers(1, &pixel_buf_object_id_); +} + +bool GLBlindRGBA8bitTex::resize(const size_t required_size_bytes) { + + if (!required_size_bytes) + return false; + + // N.B. Seeing redraw issues if the textures owned by GLDoubleBufferedTexture + // are not all the same size. This can happen if, when not playing back, the + // user looks at one frame of a large image (so one texture gets resized to + // suit that large image). Then they start playing back another image of a + // smaller size, the other textures might get initialised to the smaller + // size of the playback images. When this happens, we see scrambled pixels. + // I haven't got to the bottom of the problem so for now we force all + // textures to have the same size, the maximum of any texture that has been + // requested. Note that this method is called on every texture upload. + + static size_t max_tex_size = 0; + max_tex_size = std::max(max_tex_size, required_size_bytes); + + if (tex_id_) { + + if (max_tex_size <= tex_size_bytes()) { + + return false; + } + +#ifdef __OPENGL_4_1__ + if (mapped_address_) { + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixel_buf_object_id_); + glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); + mapped_address_ = nullptr; + } +#endif + + glDeleteTextures(1, &tex_id_); + glDeleteBuffers(1, &pixel_buf_object_id_); + } + + // Create the texture for RGB float display and the Y component of YUV display + glGenTextures(1, &tex_id_); + glBindTexture(GL_TEXTURE_RECTANGLE, tex_id_); + glEnable(GL_TEXTURE_RECTANGLE); + glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glPixelStorei(GL_UNPACK_ALIGNMENT, 8); + + // ensure the texture width and height are both a power of 2. + bytes_per_pixel_ = 4; + tex_width_ = (int)std::pow( + 2.0f, + std::ceil( + std::log(std::sqrt(float(max_tex_size / bytes_per_pixel_))) / std::log(2.0f))); + tex_height_ = (int)std::pow( + 2.0f, + std::ceil( + std::log(float(max_tex_size / (tex_width_ * bytes_per_pixel_))) / std::log(2.0f))); + + // tex_width_ = 8192; + // tex_height_ = 4096; + + glTexImage2D( + GL_TEXTURE_RECTANGLE, + 0, + GL_RGBA8UI, + tex_width_, + tex_height_, + 0, + GL_RGBA_INTEGER, + GL_UNSIGNED_BYTE, + nullptr); + + glGenBuffers(1, &pixel_buf_object_id_); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixel_buf_object_id_); +#ifdef __OPENGL_4_1__ + glBufferData( + GL_PIXEL_UNPACK_BUFFER, + tex_width_ * tex_height_ * bytes_per_pixel_, + nullptr, + GL_DYNAMIC_DRAW); +#else + glNamedBufferData( + pixel_buf_object_id_, + tex_width_ * tex_height_ * bytes_per_pixel_, + nullptr, + GL_DYNAMIC_DRAW); +#endif + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + return true; +} + +uint8_t *GLBlindRGBA8bitTex::map_buffer_for_upload(const size_t buffer_size) { + + glEnable(GL_TEXTURE_RECTANGLE); + if (!resize(buffer_size) && mapped_address_) { + // already mapped and unused + return mapped_address_; + } +#ifdef __OPENGL_4_1__ + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixel_buf_object_id_); + glBufferData( + GL_PIXEL_UNPACK_BUFFER, + tex_width_ * tex_height_ * bytes_per_pixel_, + nullptr, + GL_DYNAMIC_DRAW); + mapped_address_ = (uint8_t *)glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY); + // glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + return mapped_address_; +#else + glNamedBufferData( + pixel_buf_object_id_, + tex_width_ * tex_height_ * bytes_per_pixel_, + nullptr, + GL_DYNAMIC_DRAW); + return (uint8_t *)glMapNamedBuffer(pixel_buf_object_id_, GL_WRITE_ONLY); +#endif +} + +void GLBlindRGBA8bitTex::__bind(int tex_index, Imath::V2i &dims) { + + dims.x = tex_width_; + dims.y = tex_height_; + + + int row_length; + glGetIntegerv(GL_UNPACK_ROW_LENGTH, &row_length); + + if (source_frame_ && source_frame_->size()) { + + // now the texture data is transferred (on the GPU). + // Assumption is that this is fast. +#ifdef __OPENGL_4_1__ + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixel_buf_object_id_); + bool r = glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); + mapped_address_ = nullptr; +#else + glUnmapNamedBuffer(pixel_buf_object_id_); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixel_buf_object_id_); +#endif + glBindTexture(GL_TEXTURE_RECTANGLE, tex_id_); + glPixelStorei(GL_UNPACK_ROW_LENGTH, tex_width_); + glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, source_frame_->size() / (tex_width_ * 4)); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); + glPixelStorei(GL_UNPACK_ALIGNMENT, 8); + + glTexSubImage2D( + GL_TEXTURE_RECTANGLE, + 0, + 0, + 0, + tex_width_, + source_frame_->size() / (tex_width_ * 4), + GL_RGBA_INTEGER, + GL_UNSIGNED_BYTE, + nullptr); + + glBindTexture(GL_TEXTURE_RECTANGLE, 0); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + + // we can reset our source frame as we have transferred it to + // texture memory + source_frame_.reset(); + } + + if (tex_id_) { + glActiveTexture(tex_index + GL_TEXTURE0); + glBindTexture(GL_TEXTURE_RECTANGLE, tex_id_); + } + glPixelStorei(GL_UNPACK_ROW_LENGTH, row_length); +} \ No newline at end of file diff --git a/src/ui/opengl/src/opengl_shape_renderer.cpp b/src/ui/opengl/src/opengl_shape_renderer.cpp new file mode 100644 index 000000000..b6e2856e5 --- /dev/null +++ b/src/ui/opengl/src/opengl_shape_renderer.cpp @@ -0,0 +1,993 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/ui/opengl/opengl_shape_renderer.hpp" +#include "xstudio/ui/canvas/shapes.hpp" + +#include + +using namespace xstudio::ui::canvas; +using namespace xstudio::ui::opengl; + +#ifdef __OPENGL_4_1__ + +// To support GL 4.1 (for MacOS) we can't use SSBOs. Therefore the shape data +// is packed into a 1D texture. Keeping the SSBO implementation as it might +// have performance/clarity advantages + +#define SHAPE_DATA_TEX_SIZE 4096 + +namespace { + +const char *vertex_shader = R"( + #version 410 core + + layout (location = 0) in vec2 vtx; + out vec2 coords; + + uniform float scale_ar; + + void main() { + gl_Position = vec4(vtx, 0.0, 1.0); + coords = vec2(gl_Position.x, -gl_Position.y * scale_ar); + } +)"; + +const char *frag_shader = R"( + #version 410 + + // We plot all the shapes in the fragment shader in one hit, using a + // sign-distance-function approach so for each fragment we iterate over + // all shapes and compute the distance to the shape boundary. + // In order to use this approach we pack all the data about each shape ( + // including the polygon points for polygons) into texture data and unpack + // here in the shader routine + uniform sampler1D shapesData; + + uniform int shape_count; + + in vec2 coords; + out vec4 color; + + int getShapeDataIntVal(int idx) { + return int(texelFetch(shapesData, idx, 0).r); + } + + float getShapeDataFloatVal(int idx) { + return texelFetch(shapesData, idx, 0).r; + } + + vec4 getShapeDataVec4(int idx) { + return vec4( + texelFetch(shapesData, idx, 0).r, + texelFetch(shapesData, idx+1, 0).r, + texelFetch(shapesData, idx+2, 0).r, + texelFetch(shapesData, idx+3, 0).r + ); + } + + vec2 getShapeDataVec2(int idx) { + return vec2( + texelFetch(shapesData, idx, 0).r, + texelFetch(shapesData, idx+1, 0).r + ); + } + + + vec2 opRotate(vec2 coord, float degrees) + { + float rad = radians(degrees); + + mat2 rotation_mat = mat2( + cos(rad),-sin(rad), + sin(rad), cos(rad) + ); + + return coord * rotation_mat; + } + + ///////////////////////////////////////// + // License for sdQuad() sdPolygon() + ///////////////////////////////////////// + + // The MIT License + // Copyright © 2021 Inigo Quilez + // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + // https://www.shadertoy.com/view/7dSGWK + float sdQuad(vec2 p, vec2 p0, vec2 p1, vec2 p2, vec2 p3) + { + vec2 e0 = p1 - p0; vec2 v0 = p - p0; + vec2 e1 = p2 - p1; vec2 v1 = p - p1; + vec2 e2 = p3 - p2; vec2 v2 = p - p2; + vec2 e3 = p0 - p3; vec2 v3 = p - p3; + + vec2 pq0 = v0 - e0*clamp( dot(v0,e0)/dot(e0,e0), 0.0, 1.0 ); + vec2 pq1 = v1 - e1*clamp( dot(v1,e1)/dot(e1,e1), 0.0, 1.0 ); + vec2 pq2 = v2 - e2*clamp( dot(v2,e2)/dot(e2,e2), 0.0, 1.0 ); + vec2 pq3 = v3 - e3*clamp( dot(v3,e3)/dot(e3,e3), 0.0, 1.0 ); + + vec2 ds = min( min( vec2( dot( pq0, pq0 ), v0.x*e0.y-v0.y*e0.x ), + vec2( dot( pq1, pq1 ), v1.x*e1.y-v1.y*e1.x )), + min( vec2( dot( pq2, pq2 ), v2.x*e2.y-v2.y*e2.x ), + vec2( dot( pq3, pq3 ), v3.x*e3.y-v3.y*e3.x ) )); + + float d = sqrt(ds.x); + + return (ds.y>0.0) ? -d : d; + } + + // https://iquilezles.org/articles/distfunctions2d/ + float sdPolygon(vec2 p, int idx, int point_count) + { + vec2 v0 = getShapeDataVec2(idx); + float d = dot(p-v0,p-v0); + float s = 1.0; + for( int i=0, j=point_count-1; i=vi.y,p.ye.y*w.x); + if( all(c) || all(not(c)) ) s*=-1.0; + } + return s*sqrt(d); + } + + // https://blog.chatfield.io/simple-method-for-distance-to-ellipse/ trig-less version + // https://github.com/0xfaded/ellipse_demo/issues/1 + // https://www.shadertoy.com/view/tt3yz7 + float sdEllipse4(vec2 p, vec2 e) + { + vec2 pAbs = abs(p); + vec2 ei = 1.0 / e; + vec2 e2 = e*e; + vec2 ve = ei * vec2(e2.x - e2.y, e2.y - e2.x); + + // cos/sin(math.pi / 4) + vec2 t = vec2(0.70710678118654752, 0.70710678118654752); + + for (int i = 0; i < 3; i++) { + vec2 v = ve*t*t*t; + vec2 u = normalize(pAbs - v) * length(t * e - v); + vec2 w = ei * (v + u); + t = normalize(clamp(w, 0.0, 1.0)); + } + + vec2 nearestAbs = t * e; + float dist = length(pAbs - nearestAbs); + return dot(pAbs, pAbs) < dot(nearestAbs, nearestAbs) ? -dist : dist; + } + + vec4 evaluate_polygon(int idx, vec4 in_colour) { + + vec4 color = getShapeDataVec4(idx); + idx += 4; + float softness = getShapeDataFloatVal(idx); + idx += 1; + float opacity = getShapeDataFloatVal(idx); + idx += 1; + float invert = getShapeDataFloatVal(idx); + idx += 1; + int pcount = getShapeDataIntVal(idx); + idx += 1; + float d = sdPolygon(coords, idx, pcount) * invert; + d = d + softness * (invert - 1) * -0.5; + return max(in_colour, mix(color*opacity, vec4(0.0f), smoothstep(0.0f, softness, d))); + } + + vec4 evaluate_ellipse(int idx, vec4 in_colour) { + + vec4 color = getShapeDataVec4(idx); + idx += 4; + float softness = getShapeDataFloatVal(idx); + idx += 1; + float opacity = getShapeDataFloatVal(idx); + idx += 1; + float invert = getShapeDataFloatVal(idx); + idx += 1; + float angle = getShapeDataFloatVal(idx); + idx += 1; + vec2 center = getShapeDataVec2(idx); + idx += 2; + vec2 radius = getShapeDataVec2(idx); + + float d = sdEllipse4(opRotate(coords - center, angle), radius) * invert; + d = d + softness * (invert - 1) * -0.5; + return max(in_colour, mix(color*opacity, vec4(0.0f), smoothstep(0.0f, softness, d))); + + } + + vec4 evaluate_quad(int idx, vec4 in_colour) { + + vec4 color = getShapeDataVec4(idx); + idx += 4; + float softness = getShapeDataFloatVal(idx); + idx += 1; + float opacity = getShapeDataFloatVal(idx); + idx += 1; + float invert = getShapeDataFloatVal(idx); + idx += 1; + vec2 bl = getShapeDataVec2(idx); + idx += 2; + vec2 tl = getShapeDataVec2(idx); + idx += 2; + vec2 tr = getShapeDataVec2(idx); + idx += 2; + vec2 br = getShapeDataVec2(idx); + idx += 2; + + float d = sdQuad(coords, bl, tl, tr, br) * invert; + + // When shape is inverted, add softness to the current distance + // so that softness expands the original shape outward. + // (q.invert - 1) * -0.5 is 1 if inverted, 0 otherwise + d = d + softness * (invert - 1) * -0.5; + return max(in_colour, mix(color*opacity, vec4(0.0f), smoothstep(0.0f, softness, d))); + + } + + void main() + { + vec4 accum_color = vec4(0.0f); + + int idx = 0; + for (int i = 0; i < shape_count; ++i) { + int shape_type = getShapeDataIntVal(idx); + idx++; + int shape_size = getShapeDataIntVal(idx); + idx++; + if (shape_type == 0) { + //accum_color = evaluate_quad(idx, accum_color); + } else if (shape_type == 1) { + accum_color = evaluate_ellipse(idx, accum_color); + } else if (shape_type == 2) { + accum_color = evaluate_polygon(idx, accum_color); + } + idx += shape_size; + + } + color = accum_color; + + } +)"; + +} // namespace + +OpenGLShapeRenderer::~OpenGLShapeRenderer() { cleanup_gl(); } + +void OpenGLShapeRenderer::init_gl() { + + if (!shader_) { + + shader_ = std::make_unique(vertex_shader, frag_shader); + + std::array bg_verts = { + Imath::V2f(-1.0f, -1.0f), + Imath::V2f(1.0f, -1.0f), + Imath::V2f(1.0f, 1.0f), + Imath::V2f(1.0f, 1.0f), + Imath::V2f(-1.0f, 1.0f), + Imath::V2f(-1.0f, -1.0f)}; + + glGenVertexArrays(1, &vao_); + glGenBuffers(1, &vbo_); + glBindVertexArray(vao_); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData(GL_ARRAY_BUFFER, sizeof(bg_verts), bg_verts.data(), GL_DYNAMIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + // Create a 1D 32 bit float texture to stuff lists of shape descriptions + // into for use by our fragment shader + glGenTextures(1, &tex_id_); + glBindTexture(GL_TEXTURE_1D, tex_id_); + glEnable(GL_TEXTURE_1D); + glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glPixelStorei(GL_UNPACK_ALIGNMENT, 8); + + glTexImage1D( + GL_TEXTURE_1D, 0, GL_R32F, SHAPE_DATA_TEX_SIZE, 0, GL_RED, GL_FLOAT, nullptr); + + glBindTexture(GL_TEXTURE_1D, 0); + } +} + +void OpenGLShapeRenderer::cleanup_gl() { + if (shader_) { + glDeleteTextures(1, &tex_id_); + shader_.reset(); + glDeleteBuffers(1, &vbo_); + glDeleteVertexArrays(1, &vao_); + } +} + +void OpenGLShapeRenderer::upload_shape_data_to_tex(const std::vector &data_buf) { + + int row_length; + glGetIntegerv(GL_UNPACK_ROW_LENGTH, &row_length); + + glActiveTexture(GL_TEXTURE10); + glBindTexture(GL_TEXTURE_1D, tex_id_); + + glPixelStorei(GL_UNPACK_ROW_LENGTH, SHAPE_DATA_TEX_SIZE); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); + glPixelStorei(GL_UNPACK_ALIGNMENT, 8); + + glTexSubImage1D( + GL_TEXTURE_1D, 0, 0, SHAPE_DATA_TEX_SIZE, GL_RED, GL_FLOAT, data_buf.data()); + + // need to restore row length - stuff deep in QML expects this to be set + // to a particular value + glPixelStorei(GL_UNPACK_ROW_LENGTH, row_length); + + glActiveTexture(GL_TEXTURE10); + glBindTexture(GL_TEXTURE_1D, tex_id_); +} + +void OpenGLShapeRenderer::render_shapes( + const std::vector &quads, + const std::vector &polygons, + const std::vector &ellipses, + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + float viewport_du_dx, + float image_aspectratio) { + + if (!shader_) + init_gl(); + + utility::JsonStore shader_params; + + shader_params["scale_ar"] = 1.0f / image_aspectratio; + shader_params["shape_count"] = quads.size() + ellipses.size() + polygons.size(); + shader_params["shapesData"] = 10; + + // Transform every shape coordinates back into a square aspect ratio to ease + // shader computations and produce soft blur that behaves isotropically. + const float inv_canvas_ar = 1.0f / transform_window_to_viewport_space[1][1]; + + // here we pack the data about the shapes into a buffer - it gets unpacked + // in the gl fragment shader. We use 32 bit words, generally putting two + // 16 bit floats into each, but when we need to store an int (e.g. the + // number of polygon points) we can use all 32 bits + std::vector shapes_data; + shapes_data.resize(SHAPE_DATA_TEX_SIZE); + float *data_buf = shapes_data.data(); + int ct = 0; + + // note - we can pack up to SHAPE_DATA_TEX_SIZE floats for the shape data. It is + // extremely unlikely we will ever exceed this. If we do the shapes that + // would overrun the limit will just not get drawn. + + for (const auto &quad : quads) { + + ct += 17; + if (ct > SHAPE_DATA_TEX_SIZE) + break; + + (*data_buf++) = 0; // shape type + (*data_buf++) = 15; // shape data size + + (*data_buf++) = quad.colour.r; + (*data_buf++) = quad.colour.g; + (*data_buf++) = quad.colour.b; + (*data_buf++) = 1.0f; + (*data_buf++) = quad.softness / 500.0f; + (*data_buf++) = quad.opacity / 100.0f; + (*data_buf++) = quad.invert ? -1.f : 1.f; + (*data_buf++) = quad.tl.x; + (*data_buf++) = quad.tl.y; + (*data_buf++) = quad.tr.x; + (*data_buf++) = quad.tr.y; + (*data_buf++) = quad.br.x; + (*data_buf++) = quad.br.y; + (*data_buf++) = quad.bl.x; + (*data_buf++) = quad.bl.y; + } + + for (const auto &ellipse : ellipses) { + + ct += 14; + if (ct > SHAPE_DATA_TEX_SIZE) + break; + + (*data_buf++) = 1.0; // shape type + (*data_buf++) = 12.0; // shape data size + (*data_buf++) = ellipse.colour.r; + (*data_buf++) = ellipse.colour.g; + (*data_buf++) = ellipse.colour.b; + (*data_buf++) = 1.0f; + (*data_buf++) = ellipse.softness / 500.0f; + (*data_buf++) = ellipse.opacity / 100.0f; + (*data_buf++) = ellipse.invert ? -1.f : 1.f; + (*data_buf++) = ellipse.angle; + (*data_buf++) = ellipse.center.x; + (*data_buf++) = ellipse.center.y; + (*data_buf++) = ellipse.radius.x; + (*data_buf++) = ellipse.radius.y; + } + + for (const auto &polygon : polygons) { + + ct += 10 + polygon.points.size() * 2; + if (ct > SHAPE_DATA_TEX_SIZE) + break; + + (*data_buf++) = 2.0; // shape type + (*data_buf++) = 8.0 + polygon.points.size() * 2; // shape data size + + (*data_buf++) = polygon.colour.r; + (*data_buf++) = polygon.colour.g; + (*data_buf++) = polygon.colour.b; + (*data_buf++) = 1.0f; + (*data_buf++) = polygon.softness / 500.0f; + (*data_buf++) = polygon.opacity / 100.0f; + (*data_buf++) = polygon.invert ? -1.f : 1.f; + (*data_buf++) = (float)polygon.points.size(); + + memcpy(data_buf, polygon.points.data(), sizeof(float) * 2 * polygon.points.size()); + + data_buf += 2 * polygon.points.size(); + } + + upload_shape_data_to_tex(shapes_data); + + shader_->use(); + shader_->set_shader_parameters(shader_params); + + + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBlendEquation(GL_FUNC_ADD); + + glBindVertexArray(vao_); + glEnableVertexAttribArray(0); + glDrawArrays(GL_TRIANGLE_FAN, 0, 6); + + // glEnable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + glBindVertexArray(0); + glDisableVertexAttribArray(vao_); + glBindTexture(GL_TEXTURE_1D, 0); + + shader_->stop_using(); +} + +#else // #ifdef __OPENGL_4_1__ + + +namespace { + +const char *vertex_shader = R"( + #version 430 core + + in vec4 vertices; + out vec2 coords; + + uniform float scale_ar; + + const vec2 positions[4] = { + vec2(-1, -1), + vec2( 1, -1), + vec2(-1, 1), + vec2( 1, 1), + }; + + void main() { + gl_Position = vec4(positions[gl_VertexID].xy, 0.0, 1.0); + coords = vec2(gl_Position.x, -gl_Position.y * scale_ar); + } +)"; + +const char *frag_shader = R"( + #version 430 + #extension GL_ARB_shader_storage_buffer_object : require + + // We use SSBO to avoid thinking about size limits, + // in theory UBO would be much faster and might be used if + // we determine we won't need more than the allowed size. + // For our currrent usage here, we do not compute masked + // shape in real time, only when the canvas gets updated + // by the user, so no performance review was done in that + // sense. + + // Note that std430 is used here for easier match to C++ + // memory layout, note the following points: + // 1. std430 doesn't pack vec3 so we use vec4 instead + // to avoid dealing with padding in the C++ side + // (eg. for color fields). + // 2. vec4 are aligned on 16 bytes, so we place them + // at the begining of the structs to avoid having + // to deal with alignment on the C++ side. + // See the OpenGL specs for more details on std430 + + struct Quad { + vec4 color; + vec2 tl; + vec2 tr; + vec2 br; + vec2 bl; + float opacity; + float softness; + float invert; + }; + layout (std430, binding = 1) buffer QuadsBuffer { + Quad items[]; + } quads; + + struct Ellipse { + vec4 color; + vec2 center; + vec2 radius; + float angle; + float opacity; + float softness; + float invert; + }; + layout (std430, binding = 2) buffer EllipsesBuffer { + Ellipse items[]; + } ellipses; + + struct Polygon { + vec4 color; + uint offset; + uint count; + float opacity; + float softness; + float invert; + }; + layout (std430, binding = 3) buffer PolygonsBuffer { + Polygon items[]; + } polygons; + + layout (std430, binding = 4) buffer PointsBuffer { + vec2 items[]; + } points; + + // Note that we could in theory use the .length() function + // on the variable sized arrays, however it seem to have + // issues working reliably when multiple SSBO are present. + // So we use explicit size forr the time being. + uniform int quads_count; + uniform int ellipses_count; + uniform int polygons_count; + + in vec2 coords; + out vec4 color; + + ///////////////////////////////////////// + // License for sdQuad() sdPolygon() + ///////////////////////////////////////// + + // The MIT License + // Copyright © 2021 Inigo Quilez + // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + // https://www.shadertoy.com/view/7dSGWK + float sdQuad(vec2 p, vec2 p0, vec2 p1, vec2 p2, vec2 p3) + { + vec2 e0 = p1 - p0; vec2 v0 = p - p0; + vec2 e1 = p2 - p1; vec2 v1 = p - p1; + vec2 e2 = p3 - p2; vec2 v2 = p - p2; + vec2 e3 = p0 - p3; vec2 v3 = p - p3; + + vec2 pq0 = v0 - e0*clamp( dot(v0,e0)/dot(e0,e0), 0.0, 1.0 ); + vec2 pq1 = v1 - e1*clamp( dot(v1,e1)/dot(e1,e1), 0.0, 1.0 ); + vec2 pq2 = v2 - e2*clamp( dot(v2,e2)/dot(e2,e2), 0.0, 1.0 ); + vec2 pq3 = v3 - e3*clamp( dot(v3,e3)/dot(e3,e3), 0.0, 1.0 ); + + vec2 ds = min( min( vec2( dot( pq0, pq0 ), v0.x*e0.y-v0.y*e0.x ), + vec2( dot( pq1, pq1 ), v1.x*e1.y-v1.y*e1.x )), + min( vec2( dot( pq2, pq2 ), v2.x*e2.y-v2.y*e2.x ), + vec2( dot( pq3, pq3 ), v3.x*e3.y-v3.y*e3.x ) )); + + float d = sqrt(ds.x); + + return (ds.y>0.0) ? -d : d; + } + + // https://iquilezles.org/articles/distfunctions2d/ + float sdPolygon(vec2 p, uint offset, uint count) + { + vec2 v0 = points.items[offset]; + float d = dot(p-v0,p-v0); + float s = 1.0; + for( uint i=0, j=count-1; i=vi.y,p.ye.y*w.x); + if( all(c) || all(not(c)) ) s*=-1.0; + } + return s*sqrt(d); + } + + // https://blog.chatfield.io/simple-method-for-distance-to-ellipse/ trig-less version + // https://github.com/0xfaded/ellipse_demo/issues/1 + // https://www.shadertoy.com/view/tt3yz7 + float sdEllipse4(vec2 p, vec2 e) + { + vec2 pAbs = abs(p); + vec2 ei = 1.0 / e; + vec2 e2 = e*e; + vec2 ve = ei * vec2(e2.x - e2.y, e2.y - e2.x); + + // cos/sin(math.pi / 4) + vec2 t = vec2(0.70710678118654752, 0.70710678118654752); + + for (int i = 0; i < 3; i++) { + vec2 v = ve*t*t*t; + vec2 u = normalize(pAbs - v) * length(t * e - v); + vec2 w = ei * (v + u); + t = normalize(clamp(w, 0.0, 1.0)); + } + + vec2 nearestAbs = t * e; + float dist = length(pAbs - nearestAbs); + return dot(pAbs, pAbs) < dot(nearestAbs, nearestAbs) ? -dist : dist; + } + + float opRound(float shape, float r) + { + return shape - r; + } + + vec2 opRotate(vec2 coord, float degrees) + { + float rad = radians(degrees); + + mat2 rotation_mat = mat2( + cos(rad),-sin(rad), + sin(rad), cos(rad) + ); + + return coord * rotation_mat; + } + + void main() + { + vec4 accum_color = vec4(0.0f); + + for (int i = 0; i < quads_count; ++i) { + Quad q = quads.items[i]; + // Shape boundary is at distance 0, inside is < 0, outside is > 0 + // To invert the shape, we can simply multiply by -1. + float d = sdQuad(coords, q.bl, q.tl, q.tr, q.br) * q.invert; + // When shape is inverted, add softness to the current distance + // so that softness expands the original shape outward. + // (q.invert - 1) * -0.5 is 1 if inverted, 0 otherwise + d = d + q.softness * (q.invert - 1) * -0.5; + vec4 color = mix(vec4(0.0f), q.color, q.opacity); + accum_color = max(accum_color, mix(color, vec4(0.0f), smoothstep(0.0f, q.softness, d))); + } + + for (int i = 0; i < ellipses_count; ++i) { + Ellipse e = ellipses.items[i]; + float d = sdEllipse4(opRotate(coords - e.center, e.angle), e.radius) * e.invert; + d = d + e.softness * (e.invert - 1) * -0.5; + vec4 color = mix(vec4(0.0f), e.color, e.opacity); + accum_color = max(accum_color, mix(color, vec4(0.0f), smoothstep(0.0f, e.softness, d))); + } + + for (int i = 0; i < polygons_count; ++i) { + Polygon p = polygons.items[i]; + float d = sdPolygon(coords, p.offset, p.count) * p.invert; + d = d + p.softness * (p.invert - 1) * -0.5; + vec4 color = mix(vec4(0.0f), p.color, p.opacity); + accum_color = max(accum_color, mix(color, vec4(0.0f), smoothstep(0.0f, p.softness, d))); + } + + color = accum_color; + } +)"; + +// These struct must match the GLSL buffer layout, see shader for more details. +// We avoid the need to manually specify each member alignment (with alignas()) +// by using the adjustments mentioned in the shader. +// However, we need to align the whole structures to vec4 (16 bytes), quote from +// the spec (https://registry.khronos.org/OpenGL/specs/gl/glspec45.core.pdf#page=160): +// +// If the member is a structure, the base alignment of the structure is N, where +// N is the largest base alignment value of any of its members, and rounded +// up to the base alignment of a vec4. + +struct alignas(16) GLQuad { + float colour[4]; + float tl[2]; + float tr[2]; + float br[2]; + float bl[2]; + float opacity; + float softness; + float invert; +}; + +struct alignas(16) GLEllipse { + float colour[4]; + float center[2]; + float radius[2]; + float angle; + float opacity; + float softness; + float invert; +}; + +struct alignas(16) GLPolygon { + float colour[4]; + uint32_t offset; + uint32_t count; + float opacity; + float softness; + float invert; +}; + +struct GLPoint { + float pt[2]; +}; + + +// Use to debug memory layout issues +void dump_mem_layout(GLuint prog_obj) { + // C++ + + std::cout << "sizeof GLQuad = " << sizeof(GLQuad) << "\n"; + std::cout << "offset of GLQuad.colour = " << offsetof(GLQuad, colour) << '\n' + << "offset of GLQuad.tl = " << offsetof(GLQuad, tl) << '\n' + << "offset of GLQuad.tr = " << offsetof(GLQuad, tr) << '\n' + << "offset of GLQuad.br = " << offsetof(GLQuad, br) << '\n' + << "offset of GLQuad.bl = " << offsetof(GLQuad, bl) << '\n' + << "offset of GLQuad.opacity = " << offsetof(GLQuad, opacity) << '\n' + << "offset of GLQuad.softness = " << offsetof(GLQuad, softness) << '\n' + << "offset of GLQuad.invert = " << offsetof(GLQuad, invert) << '\n'; + + std::cout << "sizeof GLEllipse = " << sizeof(GLEllipse) << "\n"; + std::cout << "offset of GLEllipse.colour = " << offsetof(GLEllipse, colour) << '\n' + << "offset of GLEllipse.center = " << offsetof(GLEllipse, center) << '\n' + << "offset of GLEllipse.radius = " << offsetof(GLEllipse, radius) << '\n' + << "offset of GLEllipse.angle = " << offsetof(GLEllipse, angle) << '\n' + << "offset of GLEllipse.opacity = " << offsetof(GLEllipse, opacity) << '\n' + << "offset of GLEllipse.softness = " << offsetof(GLEllipse, softness) << '\n' + << "offset of GLEllipse.invert = " << offsetof(GLEllipse, invert) << '\n'; + + std::cout << "sizeof GLPolygon = " << sizeof(GLPolygon) << "\n"; + std::cout << "offset of GLPolygon.colour = " << offsetof(GLPolygon, colour) << '\n' + << "offset of GLPolygon.offset = " << offsetof(GLPolygon, offset) << '\n' + << "offset of GLPolygon.count = " << offsetof(GLPolygon, count) << '\n' + << "offset of GLPolygon.opacity = " << offsetof(GLPolygon, opacity) << '\n' + << "offset of GLPolygon.softness = " << offsetof(GLPolygon, softness) << '\n' + << "offset of GLPolygon.invert = " << offsetof(GLPolygon, invert) << '\n'; + + // GLSL + // https://stackoverflow.com/a/56513136 + + GLint no_of, ssbo_max_len, var_max_len; + glGetProgramInterfaceiv(prog_obj, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES, &no_of); + glGetProgramInterfaceiv( + prog_obj, GL_SHADER_STORAGE_BLOCK, GL_MAX_NAME_LENGTH, &ssbo_max_len); + glGetProgramInterfaceiv(prog_obj, GL_BUFFER_VARIABLE, GL_MAX_NAME_LENGTH, &var_max_len); + + std::vector name(var_max_len); + for (int i_resource = 0; i_resource < no_of; i_resource++) { + + // get name of the shader storage block + GLsizei strLength; + glGetProgramResourceName( + prog_obj, + GL_SHADER_STORAGE_BLOCK, + i_resource, + ssbo_max_len, + &strLength, + name.data()); + + // get resource index of the shader storage block + GLint resInx = + glGetProgramResourceIndex(prog_obj, GL_SHADER_STORAGE_BLOCK, name.data()); + + // get number of the buffer variables in the shader storage block + GLenum prop = GL_NUM_ACTIVE_VARIABLES; + GLint num_var; + glGetProgramResourceiv( + prog_obj, GL_SHADER_STORAGE_BLOCK, resInx, 1, &prop, 1, nullptr, &num_var); + + // get resource indices of the buffer variables + std::vector vars(num_var); + prop = GL_ACTIVE_VARIABLES; + glGetProgramResourceiv( + prog_obj, + GL_SHADER_STORAGE_BLOCK, + resInx, + 1, + &prop, + (GLsizei)vars.size(), + nullptr, + vars.data()); + + std::vector offsets(num_var); + std::vector var_names(num_var); + for (GLint i = 0; i < num_var; i++) { + + // get offset of buffer variable relative to SSBO + GLenum prop = GL_OFFSET; + glGetProgramResourceiv( + prog_obj, + GL_BUFFER_VARIABLE, + vars[i], + 1, + &prop, + (GLsizei)offsets.size(), + nullptr, + &offsets[i]); + + // get name of buffer variable + std::vector var_name(var_max_len); + GLsizei strLength; + glGetProgramResourceName( + prog_obj, + GL_BUFFER_VARIABLE, + vars[i], + var_max_len, + &strLength, + var_name.data()); + var_names[i] = var_name.data(); + } + + for (int i = 0; i < offsets.size(); ++i) { + std::cout << var_names[i] << " offset is " << offsets[i] << "\n"; + } + } +} + +} // anonymous namespace + + +OpenGLShapeRenderer::~OpenGLShapeRenderer() { cleanup_gl(); } + +void OpenGLShapeRenderer::init_gl() { + + if (!shader_) { + shader_ = std::make_unique(vertex_shader, frag_shader); + } + + if (ssbo_id_ == std::array{0, 0, 0, 0}) { + glCreateBuffers(4, ssbo_id_.data()); + } +} + +void OpenGLShapeRenderer::cleanup_gl() { + + if (ssbo_id_ != std::array{0, 0, 0, 0}) { + glDeleteBuffers(4, ssbo_id_.data()); + ssbo_id_ = {0, 0, 0, 0}; + } +} + +void OpenGLShapeRenderer::upload_ssbo( + const std::vector &quads, + const std::vector &ellipses, + const std::vector &polygons, + const std::vector &points) { + + std::array size = { + static_cast(quads.size() * sizeof(GLQuad)), + static_cast(ellipses.size() * sizeof(GLEllipse)), + static_cast(polygons.size() * sizeof(GLPolygon)), + static_cast(points.size() * sizeof(GLPoint))}; + + std::array data = { + quads.data(), ellipses.data(), polygons.data(), points.data()}; + + for (int i = 0; i < 4; ++i) { + glNamedBufferData(ssbo_id_[i], size[i], nullptr, GL_DYNAMIC_DRAW); + if (size[i] > 0) { + void *buf = glMapNamedBuffer(ssbo_id_[i], GL_WRITE_ONLY); + memcpy(buf, data[i], size[i]); + glUnmapNamedBuffer(ssbo_id_[i]); + } + } +} + +void OpenGLShapeRenderer::render_shapes( + const std::vector &quads, + const std::vector &polygons, + const std::vector &ellipses, + const Imath::M44f &transform_window_to_viewport_space, + const Imath::M44f &transform_viewport_to_image_space, + float viewport_du_dx, + float image_aspectratio) { + + if (!shader_) + init_gl(); + + utility::JsonStore shader_params; + + shader_params["scale_ar"] = 1.0f / image_aspectratio; + shader_params["quads_count"] = quads.size(); + shader_params["ellipses_count"] = ellipses.size(); + shader_params["polygons_count"] = polygons.size(); + + std::vector gl_quads; + for (int i = 0; i < quads.size(); ++i) { + gl_quads.push_back({ + {quads[i].colour.r, quads[i].colour.g, quads[i].colour.b, 1.0f}, + {quads[i].tl.x, quads[i].tl.y}, + {quads[i].tr.x, quads[i].tr.y}, + {quads[i].br.x, quads[i].br.y}, + {quads[i].bl.x, quads[i].bl.y}, + quads[i].opacity / 100.0f, + quads[i].softness / 500.0f, + quads[i].invert ? -1.f : 1.f, + }); + } + + std::vector gl_ellipses; + for (int i = 0; i < ellipses.size(); ++i) { + gl_ellipses.push_back({ + {ellipses[i].colour.r, ellipses[i].colour.g, ellipses[i].colour.b, 1.0f}, + {ellipses[i].center.x, ellipses[i].center.y}, + {ellipses[i].radius.x, ellipses[i].radius.y}, + ellipses[i].angle, + ellipses[i].opacity / 100.0f, + ellipses[i].softness / 500.0f, + ellipses[i].invert ? -1.f : 1.f, + }); + } + + std::vector gl_polygons; + std::vector gl_points; + for (int i = 0; i < polygons.size(); ++i) { + gl_polygons.push_back({ + {polygons[i].colour.r, polygons[i].colour.g, polygons[i].colour.b, 1.0f}, + (unsigned int)gl_points.size(), + (unsigned int)polygons[i].points.size(), + polygons[i].opacity / 100.0f, + polygons[i].softness / 500.0f, + polygons[i].invert ? -1.f : 1.f, + }); + for (int j = 0; j < polygons[i].points.size(); ++j) { + gl_points.push_back({{polygons[i].points[j].x, polygons[i].points[j].y}}); + } + } + + upload_ssbo(gl_quads, gl_ellipses, gl_polygons, gl_points); + + for (int i = 0; i < 4; ++i) { + glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo_id_[i]); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, i + 1, ssbo_id_[i]); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + } + + + shader_->use(); + shader_->set_shader_parameters(shader_params); + + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBlendEquation(GL_FUNC_ADD); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + // glEnable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + + shader_->stop_using(); +} + + +#endif \ No newline at end of file diff --git a/src/ui/opengl/src/opengl_ssbo_image_texture.cpp b/src/ui/opengl/src/opengl_ssbo_image_texture.cpp new file mode 100644 index 000000000..fe3d8c90a --- /dev/null +++ b/src/ui/opengl/src/opengl_ssbo_image_texture.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +// OpenGL SSBO not supported on deprecated MacOS drivers! +#ifndef __OPENGL_4_1__ + +#include +#include +#include +#include + +#include "xstudio/ui/opengl/opengl_ssbo_image_texture.hpp" +#include "xstudio/utility/chrono.hpp" + +using namespace xstudio::ui::opengl; + +GLSsboTex::~GLSsboTex() { + + // ensure no copying is in flight + if (upload_thread_.joinable()) + upload_thread_.join(); + + wait_on_upload_pixels(); + if (source_frame_ && source_frame_->size()) { + glUnmapNamedBuffer(ssbo_id_); + } + glDeleteBuffers(1, &ssbo_id_); +} + + +void GLSsboTex::__bind(int /*tex_index*/, Imath::V2i & /*dims*/) { + + if (source_frame_ && source_frame_->size()) { + glUnmapNamedBuffer(ssbo_id_); + source_frame_.reset(); + } + + if (ssbo_id_) { + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo_id_); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + } +} + +uint8_t *GLSsboTex::map_buffer_for_upload(const size_t buffer_size) { + + if (!ssbo_id_) { + glGenBuffers(1, &ssbo_id_); + } + compute_size(buffer_size); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo_id_); + glNamedBufferData(ssbo_id_, tex_size_bytes(), nullptr, GL_DYNAMIC_DRAW); + auto rt = (uint8_t *)glMapNamedBuffer(ssbo_id_, GL_WRITE_ONLY); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + return rt; +} + + +void GLSsboTex::compute_size(const size_t required_size_bytes) { + + if (!required_size_bytes) + return; + + if (ssbo_id_) { + if (required_size_bytes <= tex_size_bytes()) { + return; + } + } + + tex_data_size_ = + pow(2, ceil(std::log((1 + (required_size_bytes / 4096)) * 4096) / std::log(2.0))); +} +#endif \ No newline at end of file diff --git a/src/ui/opengl/src/opengl_stroke_renderer.cpp b/src/ui/opengl/src/opengl_stroke_renderer.cpp index 42c2cbcb9..b81117ad4 100644 --- a/src/ui/opengl/src/opengl_stroke_renderer.cpp +++ b/src/ui/opengl/src/opengl_stroke_renderer.cpp @@ -5,75 +5,53 @@ using namespace xstudio::ui::canvas; using namespace xstudio::ui::opengl; - namespace { const char *vertex_shader = R"( - #version 430 core - #extension GL_ARB_shader_storage_buffer_object : require + #version 410 core uniform float z_adjust; uniform float thickness; uniform float soft_dim; uniform mat4 to_coord_system; uniform mat4 to_canvas; + layout (location = 0) in vec2 lstart; + layout (location = 1) in vec2 lend; flat out vec2 line_start; flat out vec2 line_end; out vec2 frag_pos; out float soft_edge; uniform bool do_soft_edge; - uniform int point_count; uniform int offset_into_points; - layout (std430, binding = 1) buffer ssboObject { - vec2 vtxs[]; - } ssboData; - void main() { - // We draw a thick line by plotting a quad that encloses the line that - // joins two pen stroke vertices - we use a distance-to-line calculation - // for the fragments within the quad and employ a smoothstep to draw - // an anti-aliased 'sausage' shape that joins the two stroke vertices - // with a circular join between each connected pair of vertices - - int v_idx = gl_VertexID/4; - int i = gl_VertexID%4; + // 6 verts are needed for each line segment - we are drawing a rectangle + // that bounds the line and is 'thickness' wide and thickness longer + // than the line segment + + int i = gl_VertexID%6; vec2 vtx; float quad_thickness = thickness + (do_soft_edge ? soft_dim : 0.00001f); float zz = z_adjust - (do_soft_edge ? 0.0005 : 0.0); - line_start = ssboData.vtxs[offset_into_points+v_idx].xy; // current vertex in stroke - line_end = ssboData.vtxs[offset_into_points+1+v_idx].xy; // next vertex in stroke - - if (line_start == line_end) { - // draw a quad centred on the line point - if (i == 0) { - vtx = line_start+vec2(-quad_thickness, -quad_thickness); - } else if (i == 1) { - vtx = line_start+vec2(-quad_thickness, quad_thickness); - } else if (i == 2) { - vtx = line_end+vec2(quad_thickness, quad_thickness); - } else { - vtx = line_end+vec2(quad_thickness, -quad_thickness); - } - } else { - // draw a quad around the line segment - vec2 v = normalize(line_end-line_start); // vector between the two vertices - vec2 tr = normalize(vec2(v.y,-v.x))*quad_thickness; // tangent - - // now we 'emit' one of four vertices to make a quad. We do it by adding - // or subtracting the tangent to the line segment , depending of the - // vertex index in the quad - - if (i == 0) { - vtx = line_start-tr-v*quad_thickness; - } else if (i == 1) { - vtx = line_start+tr-v*quad_thickness; - } else if (i == 2) { - vtx = line_end+tr; - } else { - vtx = line_end-tr; - } + line_start = lstart; // start point of the line + line_end = lend; // end point of the line + + vec2 v = (line_end == line_start ? vec2(1.0, 0.0) : normalize(line_end-line_start))*quad_thickness; // vector between the two vertices + vec2 tr = vec2(v.y,-v.x); // tangent + + if (i == 0) { + vtx = line_start-v+tr; + } else if (i == 1) { + vtx = line_end+v+tr; + } else if (i == 2) { + vtx = line_end+v-tr; + } else if (i == 3) { + vtx = line_end+v-tr; + } else if (i == 4) { + vtx = line_start-v-tr; + } else if (i == 5) { + vtx = line_start-v+tr; } soft_edge = (do_soft_edge ? soft_dim : 0.00001f); @@ -149,45 +127,17 @@ void OpenGLStrokeRenderer::init_gl() { if (!shader_) { shader_ = std::make_unique(vertex_shader, frag_shader); - } - - if (!ssbo_id_) { - glCreateBuffers(1, &ssbo_id_); + glGenBuffers(1, &vbo_); + glGenVertexArrays(1, &vao_); } } void OpenGLStrokeRenderer::cleanup_gl() { - if (ssbo_id_) { - glDeleteBuffers(1, &ssbo_id_); - ssbo_id_ = 0; - } -} - -void OpenGLStrokeRenderer::resize_ssbo(std::size_t size) { - const auto next_power_of_2 = - static_cast(std::pow(2.0f, std::ceil(std::log2(size)))); - - if (ssbo_size_ < next_power_of_2) { - ssbo_size_ = next_power_of_2; - glNamedBufferData(ssbo_id_, ssbo_size_, nullptr, GL_DYNAMIC_DRAW); - } -} - -void OpenGLStrokeRenderer::upload_ssbo(const std::vector &points) { - - const std::size_t size = points.size() * sizeof(Imath::V2f); - resize_ssbo(size); - - const char *data = reinterpret_cast(points.data()); - const size_t hash = std::hash{}(std::string_view(data, size)); - - if (ssbo_data_hash_ != hash) { - ssbo_data_hash_ = hash; - - void *buf = glMapNamedBuffer(ssbo_id_, GL_WRITE_ONLY); - memcpy(buf, points.data(), size); - glUnmapNamedBuffer(ssbo_id_); + if (shader_) { + glDeleteBuffers(1, &vbo_); + glDeleteVertexArrays(1, &vao_); + shader_.reset(); } } @@ -198,23 +148,47 @@ void OpenGLStrokeRenderer::render_strokes( float viewport_du_dx, bool have_alpha_buffer) { - const bool do_erase_strokes_first = !have_alpha_buffer; - if (!shader_) init_gl(); - std::vector strokes_vertices; + std::vector line_start_end_per_vertex; for (const auto &stroke : strokes) { - auto vertices = stroke.vertices(); - strokes_vertices.insert(strokes_vertices.end(), vertices.begin(), vertices.end()); + const Imath::V2f *v0 = stroke.points.data(); + const Imath::V2f *v1 = v0; + v1++; + const int n_segments = (stroke.points.size() - 1); + + for (int i = 0; i < n_segments; ++i) { + line_start_end_per_vertex.push_back(*v0); + line_start_end_per_vertex.push_back(*v1); + line_start_end_per_vertex.push_back(*v0); + line_start_end_per_vertex.push_back(*v1); + line_start_end_per_vertex.push_back(*v0); + line_start_end_per_vertex.push_back(*v1); + line_start_end_per_vertex.push_back(*v0); + line_start_end_per_vertex.push_back(*v1); + line_start_end_per_vertex.push_back(*v0); + line_start_end_per_vertex.push_back(*v1); + line_start_end_per_vertex.push_back(*v0); + line_start_end_per_vertex.push_back(*v1); + v0++; + v1++; + } } - upload_ssbo(strokes_vertices); - - // Buffer binding point 1, see vertex shader - glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo_id_); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, ssbo_id_); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + glBindVertexArray(vao_); + // 2. copy our vertices array in a buffer for OpenGL to use + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData( + GL_ARRAY_BUFFER, + line_start_end_per_vertex.size() * sizeof(Imath::V2f), + line_start_end_per_vertex.data(), + GL_STREAM_DRAW); + float *ptr = 0; + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)ptr); + ptr += 2; + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)ptr); + glBindBuffer(GL_ARRAY_BUFFER, 0); // strokes are made up of partially overlapping triangles - as we // draw with opacity we use depth test to stop overlapping triangles @@ -242,33 +216,41 @@ void OpenGLStrokeRenderer::render_strokes( GLint offset = 0; float depth = 0.0f; - if (do_erase_strokes_first) { + glEnableVertexAttribArray(0); + glEnableVertexAttribArray(1); + + // Here we draw the erase strokes. Essentially we just draw into the + // depth buffer, nothing goes into the frag buffer. + // Later when we draw visible strokes the depth test will cause the + // 'erase' to work. + glDepthFunc(GL_GREATER); + for (const auto &stroke : strokes) { depth += 0.001; - glDepthFunc(GL_GREATER); - for (const auto &stroke : strokes) { - if (stroke.type != StrokeType_Erase) { - offset += (stroke.points.size() + 1); - continue; - } - shader_params2["z_adjust"] = depth; - shader_params2["brush_colour"] = stroke.colour; - shader_params2["brush_opacity"] = 0.0f; - shader_params2["thickness"] = stroke.thickness; - shader_params2["do_soft_edge"] = false; - shader_params2["point_count"] = stroke.points.size() + 1; - shader_params2["offset_into_points"] = offset; - shader_->set_shader_parameters(shader_params2); - glDrawArrays(GL_QUADS, 0, stroke.points.size() * 4); - offset += (stroke.points.size() + 1); + if (stroke.type != StrokeType_Erase) { + offset += (stroke.points.size() - 1) * 6; + ; + continue; } + shader_params2["z_adjust"] = depth; + shader_params2["brush_colour"] = stroke.colour; + shader_params2["brush_opacity"] = 0.0f; + shader_params2["thickness"] = stroke.thickness; + shader_params2["do_soft_edge"] = false; + shader_->set_shader_parameters(shader_params2); + + glDrawArrays(GL_TRIANGLES, offset, (stroke.points.size() - 1) * 6); + offset += (stroke.points.size() - 1) * 6; } + offset = 0; depth = 0.0f; for (const auto &stroke : strokes) { depth += 0.001; - if (do_erase_strokes_first && stroke.type == StrokeType_Erase) { - offset += (stroke.points.size() + 1); + if (stroke.type == StrokeType_Erase) { + // now we skip erase strokes + offset += (stroke.points.size() - 1) * 6; + ; continue; } @@ -306,13 +288,11 @@ void OpenGLStrokeRenderer::render_strokes( } // set up the shader uniforms - strok thickness, colour etc - shader_params2["z_adjust"] = depth; - shader_params2["brush_colour"] = stroke.colour; - shader_params2["brush_opacity"] = stroke.opacity; - shader_params2["thickness"] = stroke.thickness; - shader_params2["do_soft_edge"] = false; - shader_params2["point_count"] = stroke.points.size() + 1; - shader_params2["offset_into_points"] = offset; + shader_params2["z_adjust"] = depth; + shader_params2["brush_colour"] = stroke.colour; + shader_params2["brush_opacity"] = stroke.opacity; + shader_params2["thickness"] = stroke.thickness; + shader_params2["do_soft_edge"] = false; shader_->set_shader_parameters(shader_params2); // For each adjacent PAIR of points in a stroke, we draw a quad of @@ -323,7 +303,8 @@ void OpenGLStrokeRenderer::render_strokes( // to the stroke and also rounded 'elbows' at angled joins. // The vertex shader computes the 4 vertices for each quad directly from // the stroke points and thickness - glDrawArrays(GL_QUADS, 0, stroke.points.size() * 4); + + glDrawArrays(GL_TRIANGLES, offset, (stroke.points.size() - 1) * 6); /* ---- Second pass, draw soft edged stroke underneath ---- */ @@ -340,13 +321,19 @@ void OpenGLStrokeRenderer::render_strokes( shader_params3["do_soft_edge"] = true; shader_params3["soft_dim"] = viewport_du_dx * 4.0f + stroke.softness * stroke.thickness; shader_->set_shader_parameters(shader_params3); - glDrawArrays(GL_QUADS, 0, stroke.points.size() * 4); + glDrawArrays(GL_TRIANGLES, offset, (stroke.points.size() - 1) * 6); - offset += (stroke.points.size() + 1); + offset += (stroke.points.size() - 1) * 6; } glBlendEquation(GL_FUNC_ADD); glBindVertexArray(0); + glDisableVertexAttribArray(0); + glDisableVertexAttribArray(1); + shader_->stop_using(); -} \ No newline at end of file + + glDisable(GL_BLEND); + glDisable(GL_DEPTH_TEST); +} diff --git a/src/ui/opengl/src/opengl_text_rendering.cpp b/src/ui/opengl/src/opengl_text_rendering.cpp index 9cf1aa08b..444d0076c 100644 --- a/src/ui/opengl/src/opengl_text_rendering.cpp +++ b/src/ui/opengl/src/opengl_text_rendering.cpp @@ -3,8 +3,13 @@ #include #include +#ifdef __apple__ +#include +#else #include #include +#endif + #include #include #include @@ -97,7 +102,7 @@ OpenGLTextRendererBitmap::OpenGLTextRendererBitmap( glGenBuffers(1, &vbo_); glBindVertexArray(vao_); glBindBuffer(GL_ARRAY_BUFFER, vbo_); - glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 8192 * 4, nullptr, GL_DYNAMIC_DRAW); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 8192 * 8, nullptr, GL_DYNAMIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr); glBindBuffer(GL_ARRAY_BUFFER, 0); @@ -142,7 +147,7 @@ void OpenGLTextRendererBitmap::render_text( glBindBuffer(GL_ARRAY_BUFFER, 0); // render quad - glDrawArrays(GL_QUADS, 0, precomputed_vertex_buffer.size() / 4); + glDrawArrays(GL_TRIANGLES, 0, precomputed_vertex_buffer.size() / 4); // now advance cursors for next glyph (note that advance is number of 1/64 pixels) glBindVertexArray(0); @@ -195,6 +200,13 @@ OpenGLTextRendererSDF::OpenGLTextRendererSDF( const std::string ttf_font_path, const int glyph_image_size_in_pixels) : SDFBitmapFont(ttf_font_path, glyph_image_size_in_pixels) { + int skip_rows, skip_pixels, row_length, alignment, hh; + glGetIntegerv(GL_UNPACK_SKIP_ROWS, &skip_rows); + glGetIntegerv(GL_UNPACK_SKIP_PIXELS, &skip_pixels); + glGetIntegerv(GL_UNPACK_ROW_LENGTH, &row_length); + glGetIntegerv(GL_UNPACK_ALIGNMENT, &alignment); + glGetIntegerv(GL_UNPACK_IMAGE_HEIGHT, &hh); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // disable byte-alignment restriction glPixelStorei(GL_UNPACK_ROW_LENGTH, atlas_width()); glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, atlas_height()); @@ -234,11 +246,20 @@ OpenGLTextRendererSDF::OpenGLTextRendererSDF( glGenBuffers(1, &vbo_); glBindVertexArray(vao_); glBindBuffer(GL_ARRAY_BUFFER, vbo_); - glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 8192 * 4, nullptr, GL_DYNAMIC_DRAW); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 8192, nullptr, GL_DYNAMIC_DRAW); + buf_size_ = 8192; glEnableVertexAttribArray(0); glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); + + glBindTexture(GL_TEXTURE_RECTANGLE, 0); + + glPixelStorei(GL_UNPACK_SKIP_ROWS, skip_rows); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, skip_pixels); + glPixelStorei(GL_UNPACK_ROW_LENGTH, row_length); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); + glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, hh); } @@ -282,15 +303,25 @@ void OpenGLTextRendererSDF::render_text( // update content of vbo_ memory glBindBuffer(GL_ARRAY_BUFFER, vbo_); - glBufferSubData( - GL_ARRAY_BUFFER, - 0, - sizeof(float) * precomputed_vertex_buffer.size(), - precomputed_vertex_buffer.data()); + if (precomputed_vertex_buffer.size() > buf_size_) { + glBufferData( + GL_ARRAY_BUFFER, + sizeof(float) * precomputed_vertex_buffer.size(), + precomputed_vertex_buffer.data(), + GL_DYNAMIC_DRAW); + auto b = const_cast(&buf_size_); + *b = precomputed_vertex_buffer.size(); + } else { + glBufferSubData( + GL_ARRAY_BUFFER, + 0, + sizeof(float) * precomputed_vertex_buffer.size(), + precomputed_vertex_buffer.data()); + } glBindBuffer(GL_ARRAY_BUFFER, 0); // render quad - glDrawArrays(GL_QUADS, 0, precomputed_vertex_buffer.size() / 4); + glDrawArrays(GL_TRIANGLES, 0, precomputed_vertex_buffer.size() / 4); glBindVertexArray(0); glBindTexture(GL_TEXTURE_RECTANGLE, 0); diff --git a/src/ui/opengl/src/opengl_texthandle_renderer.cpp b/src/ui/opengl/src/opengl_texthandle_renderer.cpp index 7a6035f70..c81d433ae 100644 --- a/src/ui/opengl/src/opengl_texthandle_renderer.cpp +++ b/src/ui/opengl/src/opengl_texthandle_renderer.cpp @@ -9,7 +9,7 @@ using namespace xstudio::ui::opengl; namespace { const char *vertex_shader = R"( - #version 430 core + #version 410 core uniform mat4 to_coord_system; uniform mat4 to_canvas; uniform vec2 box_position; @@ -245,7 +245,7 @@ void OpenGLTextHandleRenderer::render_handles( shader_params2["box_position"] = positions[i]; shader_params2["box_type"] = 2; shader_->set_shader_parameters(shader_params2); - glDrawArrays(GL_QUADS, 0, 4); + glDrawArrays(GL_TRIANGLE_FAN, 0, 6); } static const auto aa_jitter = std::vector( diff --git a/src/ui/opengl/src/opengl_texture_base.cpp b/src/ui/opengl/src/opengl_texture_base.cpp new file mode 100644 index 000000000..025f697ad --- /dev/null +++ b/src/ui/opengl/src/opengl_texture_base.cpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include + +#include "xstudio/ui/opengl/opengl_texture_base.hpp" +#include "xstudio/utility/chrono.hpp" + +using namespace xstudio::ui::opengl; + + +void GLBlindTex::release() { when_last_used_ = utility::clock::now(); } + +GLBlindTex::GLBlindTex() { + + static int f = 0; + media_key_ = media::MediaKey("EmptyTex{}{}{}", caf::uri(), f++, "_0"); +} + +GLBlindTex::~GLBlindTex() {} + +void GLBlindTex::do_pixel_upload() { + + auto t0 = utility::clock::now(); + + std::unique_lock lk(mutex_); + in_progress_ = true; + lk.unlock(); + if (pending_source_frame_) { + + uint8_t *xstudio_pixel_buffer = (uint8_t *)pending_source_frame_->buffer(); + size_t copy_size = std::min(tex_size_bytes(), pending_source_frame_->size()); + + // just a memcpy! + if (gpu_mapped_mem_ && xstudio_pixel_buffer && copy_size) { + memcpy(gpu_mapped_mem_, xstudio_pixel_buffer, copy_size); + } + } + lk.lock(); + pending_upload_ = false; + in_progress_ = false; + media_key_ = pending_media_key_; + source_frame_ = pending_source_frame_; + lk.unlock(); + cv_.notify_one(); + + // std::cerr << "UPLOADED " << to_string(media_key_) << "\n"; + + /*std::cerr << "Tex " << to_string(media_key_) << " uploaded in " << + std::chrono::duration_cast(utility::clock::now()-t0).count() + << + "\n";*/ +} + +void GLBlindTex::wait_on_upload_pixels() { + std::unique_lock lk(mutex_); + while (pending_upload_) { + cv_.wait(lk); + } +} + +void GLBlindTex::cancel_upload() { + std::unique_lock lk(mutex_); + while (in_progress_) { + cv_.wait(lk); + } + pending_upload_ = false; + pending_media_key_ = media::MediaKey(); + pending_source_frame_.reset(); +} + + +void GLBlindTex::prepare_for_upload(const media_reader::ImageBufPtr &frame) { + + if (!frame) + return; + + // make sure we're not still uploading pixels from a previous request + wait_on_upload_pixels(); + + pending_media_key_ = frame->media_key(); + pending_source_frame_ = frame; + + if (pending_source_frame_ && pending_source_frame_->size()) { + + gpu_mapped_mem_ = map_buffer_for_upload(pending_source_frame_->size()); + if (!gpu_mapped_mem_) { + spdlog::warn("Failed to map buffer for frame {} ", to_string(pending_media_key_)); + media_key_ = media::MediaKey(); + pending_source_frame_.reset(); + return; + } + + { + std::lock_guard lk(mutex_); + pending_upload_ = true; + } + } +} \ No newline at end of file diff --git a/src/ui/opengl/src/opengl_viewport_renderer.cpp b/src/ui/opengl/src/opengl_viewport_renderer.cpp index 26d8f072a..b48f28ac8 100644 --- a/src/ui/opengl/src/opengl_viewport_renderer.cpp +++ b/src/ui/opengl/src/opengl_viewport_renderer.cpp @@ -2,15 +2,16 @@ #include "xstudio/media_reader/media_reader.hpp" #include "xstudio/ui/opengl/no_image_shader_program.hpp" #include "xstudio/ui/opengl/shader_program_base.hpp" -#include "xstudio/ui/opengl/texture.hpp" +#include "xstudio/ui/opengl/opengl_colour_lut_texture.hpp" +#include "xstudio/ui/opengl/opengl_rgba8bit_image_texture.hpp" +#include "xstudio/ui/opengl/opengl_ssbo_image_texture.hpp" +#include "xstudio/ui/opengl/opengl_multi_buffered_texture.hpp" #include "xstudio/ui/opengl/opengl_viewport_renderer.hpp" +#include "xstudio/ui/opengl/opengl_text_rendering.hpp" +#include "xstudio/ui/opengl/gl_debug_utils.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/uuid.hpp" -#ifdef DEBUG_GRAB_FRAMEBUFFER -#include "xstudio/ui/opengl/gl_debug_utils.h" -#endif - using namespace xstudio; using namespace xstudio::ui::opengl; using namespace xstudio::ui::viewport; @@ -81,65 +82,95 @@ void ColourPipeLutCollection::bind_luts(GLShaderProgramPtr shader, int &tex_idx) } } +std::map + OpenGLViewportRenderer::s_resources_store_; + OpenGLViewportRenderer::OpenGLViewportRenderer( - const int viewer_index, const bool gl_context_shared) - : viewport::ViewportRenderer(), - gl_context_shared_(gl_context_shared), - viewport_index_(viewer_index) {} + const std::string &window_id, const utility::JsonStore &prefs) + : viewport::ViewportRenderer(), window_id_(window_id) { + // Instances of OpenGLViewportRenderer that are in the same xstudio + // window need to share texture resources as they display exactly the + // same image. + + use_ssbo_ = + prefs.get("/ui/viewport/texture_mode").value("value", std::string("")) == "SSBO"; + + if (s_resources_store_.contains(window_id)) { + resources_ = s_resources_store_[window_id]; + } else { + s_resources_store_[window_id].reset(new SharedResources(prefs)); + resources_ = s_resources_store_[window_id]; + } +} + +OpenGLViewportRenderer::~OpenGLViewportRenderer() { + + resources_.reset(); + + // check if we were the last instance of OpenGLViewportRenderer to be + // using the entry in s_resources_store_ .. if so, remove it from + // s_resources_store_ so it gets destroyed and texture resources etc are + // released + auto p = s_resources_store_.find(window_id_); + if (p != s_resources_store_.end() && p->second.use_count() == 1) { + s_resources_store_.erase(p); + } +} + void OpenGLViewportRenderer::upload_image_and_colour_data( - std::vector &next_images) { + const media_reader::ImageBufPtr &image) { - colour_pipeline::ColourPipelineDataPtr colour_pipe_data = onscreen_frame_.colour_pipe_data_; + colour_pipeline::ColourPipelineDataPtr colour_pipe_data = image.colour_pipe_data_; - if (!textures_.size()) + if (!textures().size()) return; - textures_[0]->set_use_ssbo(use_ssbo_); + if (image) { - if (onscreen_frame_) { - if (onscreen_frame_->error_state() == BufferErrorState::HAS_ERROR) { + if (image->error_state() == BufferErrorState::HAS_ERROR) { // the frame contains errors, no need to continue from that point + active_shader_program_ = no_image_shader_program(); return; } // check if the frame we need to draw has already been // uploaded to texture memory and set the 'draw_texture_index_' // accordingly - textures_[0]->upload_next(next_images); + // textures()[0]->upload_image(image); } - if (colour_pipe_data && colour_pipe_data->cache_id_ != latest_colour_pipe_data_cacheid_) { - colour_pipe_textures_.clear(); + if (colour_pipe_data && colour_pipe_data->cache_id() != latest_colour_pipe_data_cacheid_) { + colour_pipe_textures().clear(); for (const auto &op : colour_pipe_data->operations()) { - colour_pipe_textures_.upload_luts(op->luts_); - colour_pipe_textures_.register_texture(op->textures_); + colour_pipe_textures().upload_luts(op->luts_); + colour_pipe_textures().register_texture(op->textures_); } - latest_colour_pipe_data_cacheid_ = colour_pipe_data->cache_id_; + latest_colour_pipe_data_cacheid_ = colour_pipe_data->cache_id(); } - if (onscreen_frame_ && colour_pipe_data && - activate_shader(onscreen_frame_->shader(), colour_pipe_data->operations())) { + if (image && colour_pipe_data && + activate_shader(image->shader(), colour_pipe_data->operations())) { - active_shader_program_->set_shader_parameters(onscreen_frame_); - active_shader_program_->set_shader_parameters(onscreen_frame_.colour_pipe_uniforms_); + active_shader_program_->set_shader_parameters(image); + active_shader_program_->set_shader_parameters(image.colour_pipe_uniforms_); } else { - active_shader_program_ = no_image_shader_program_; + active_shader_program_ = no_image_shader_program(); } - bind_textures(); + bind_textures(image); } -void OpenGLViewportRenderer::bind_textures() { +void OpenGLViewportRenderer::bind_textures(const media_reader::ImageBufPtr &image) { - if (!active_shader_program_ || active_shader_program_ == no_image_shader_program_) + if (!active_shader_program_ || active_shader_program_ == no_image_shader_program()) return; int tex_idx = 1; Imath::V2i tex_dims; - textures_[0]->bind(tex_idx, tex_dims); + textures()[0]->bind(image, tex_idx, tex_dims); utility::JsonStore txshder_param; txshder_param["the_tex"] = tex_idx; txshder_param["tex_dims"] = tex_dims; @@ -147,285 +178,304 @@ void OpenGLViewportRenderer::bind_textures() { tex_idx++; active_shader_program_->set_shader_parameters(txshder_param); - colour_pipe_textures_.bind_luts(active_shader_program_, tex_idx); + colour_pipe_textures().bind_luts(active_shader_program_, tex_idx); + + // return active texture to default + glActiveTexture(GL_TEXTURE0); } void OpenGLViewportRenderer::release_textures() { - if (active_shader_program_ == no_image_shader_program_) + if (active_shader_program_ == no_image_shader_program()) return; - textures_[0]->release(); + // textures()[0]->release(); } // static std::mutex m; -void OpenGLViewportRenderer::clear_viewport_area(const Imath::M44f &to_scene_matrix) { - - // The GL viewport does not necessarily match the area of the xstudio - // viewport ... for example, the xstudio viewport is typically embedded in - // a QML interface with various other widgets arranged around it. At render - // time, we take account of this using the 'to_scene_matrix' which maps from - // the current GL_VIEWPORT to the area of the xstudio viewport within it. - // In this case, we are using this to run glClear but only on the area of - // the viewport. This might save a tiny bit of time when rendering the whole - // interface, but also means we don't stomp on something that has been drawn - // into the GL canvas that is supposed to be there! - std::array vp; - glGetIntegerv(GL_VIEWPORT, vp.data()); +void OpenGLViewportRenderer::clear_viewport_area( + const Imath::M44f &window_to_viewport_matrix, const Imath::V2i &window_size) { // Our ref coord system maps -1.0, -1.0 to the bottom left of the viewport and // 1.0, 1.0 to the top right Imath::V4f botomleft(-1.0, -1.0, 0.0f, 1.0f); Imath::V4f topright(1.0, 1.0, 0.0f, 1.0f); - topright = topright * to_scene_matrix; - botomleft = botomleft * to_scene_matrix; + topright = topright * window_to_viewport_matrix; + botomleft = botomleft * window_to_viewport_matrix; // Now convert to window pixels for glScissor window botomleft *= 1.0f / botomleft.w; topright *= 1.0f / topright.w; - float bottom = 0.5f * (botomleft.y + 1.0f) * float(vp[3]); - float top = 0.5f * (topright.y + 1.0f) * float(vp[3]); + float bottom = 0.5f * (botomleft.y + 1.0f) * float(window_size.y); + float top = 0.5f * (topright.y + 1.0f) * float(window_size.y); + + float left = 0.5f * (botomleft.x + 1.0f) * float(window_size.x); + float right = 0.5f * (topright.x + 1.0f) * float(window_size.x); - float left = 0.5f * (botomleft.x + 1.0f) * float(vp[2]); - float right = 0.5f * (topright.x + 1.0f) * float(vp[2]); + viewport_coords_in_window_[0] = (int)round(left); + viewport_coords_in_window_[1] = (int)round(bottom); + viewport_coords_in_window_[2] = (int)round(right - left); + viewport_coords_in_window_[3] = (int)round(top - bottom); + + glViewport(0, 0, window_size.x, window_size.y); glEnable(GL_SCISSOR_TEST); glScissor( - (int)round(left), - (int)round(bottom), - (int)round(right - left), - (int)round(top - bottom)); - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + viewport_coords_in_window()[0], + viewport_coords_in_window()[1], + viewport_coords_in_window()[2], + viewport_coords_in_window()[3]); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glDisable(GL_SCISSOR_TEST); } -utility::JsonStore OpenGLViewportRenderer::default_prefs() { - JsonStore r(R"("texture_mode": { - "path": "/ui/viewport/texture_mode", - "default_value": "Image Texture", - "description": "Viewport Low Level Texture mode - SSBO can give better performance on some systems but not all", - "value": "Image Texture", - "value_range": ["Image Texture", "SSBO"] - "datatype": "string", - "context": ["APPLICATION"] - })"); - return r; -} - -void OpenGLViewportRenderer::set_prefs(const utility::JsonStore &prefs) { - const std::string tex_mode = prefs.value("texture_mode", "SSBO"); - if (tex_mode == "SSBO") - use_ssbo_ = true; - else - use_ssbo_ = false; -} - void OpenGLViewportRenderer::render( - const std::vector &_next_images, - const Imath::M44f &to_scene_matrix, - const Imath::M44f &projection_matrix, - const Imath::M44f &fit_mode_matrix) { - - // we want our images to be modifiable so we can append colour op sidecar - // data in the pre_viewport_draw_gpu_hook calls - std::vector next_images = _next_images; + const media_reader::ImageBufDisplaySetPtr &images, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const Imath::V2i &window_size, + const std::map &overlay_renderers) { - // const std::lock_guard mutex_locker(m); init(); - const auto transform_viewport_to_image_space = - projection_matrix * fit_mode_matrix.inverse(); + auto t0 = utility::clock::now(); // this value tells us how much we are zoomed into the image in the viewport (in // the x dimension). If the image is width-fitted exactly to the viewport, then this // value will be 1.0 (what it means is the coordinates -1.0 to 1.0 are mapped to // the width of the viewport) - const float image_zoom_in_viewport = transform_viewport_to_image_space[0][0]; + const float image_zoom_in_viewport = viewport_to_image_space[0][0]; // this value gives us how much of the parent window is covered by the viewport. // So if the xstudio window is 1000px in width, and the viewport is 500px wide // (with the rest of the UI taking up the remainder) then this value will be 0.5 - const float viewport_x_size_in_window = to_scene_matrix[0][0] / to_scene_matrix[3][3]; - - // the gl viewport corresponds to the parent window size. - std::array gl_viewport; - glGetIntegerv(GL_VIEWPORT, gl_viewport.data()); - const auto viewport_width = (float)gl_viewport[2]; + const float viewport_x_size_in_window = + window_to_viewport_matrix[0][0] / window_to_viewport_matrix[3][3]; // this value tells us how much a screen pixel width in the viewport is in the units // of viewport coordinate space const float viewport_du_dx = - image_zoom_in_viewport / (viewport_width * viewport_x_size_in_window); + image_zoom_in_viewport / (window_size.x * viewport_x_size_in_window); /* we do our own clear of the viewport */ - clear_viewport_area(to_scene_matrix); + clear_viewport_area(window_to_viewport_matrix, window_size); glUseProgram(0); - if (!next_images.size()) { - if (onscreen_frame_) - onscreen_frame_.reset(); - active_shader_program_ = no_image_shader_program_; - } else { - onscreen_frame_ = next_images.front(); - } - /* Here we allow plugins to run arbitrary GPU draw & computation routines. - This will allow pixel data to be rendered to textures (offscreen), for example, - which can then be sampled at actual draw time.*/ - if (onscreen_frame_) { - for (auto hook : pre_render_gpu_hooks_) { - hook.second->pre_viewport_draw_gpu_hook( - to_scene_matrix, - transform_viewport_to_image_space, + if (images && images->layout_data()) { + + glDisable(GL_DEPTH_TEST); + + // we must avoid painting outside the geometry of the viewport window, or it will be + // visible underneath QML elements with opactity etc. or other viewports in the + // same window + glScissor( + viewport_coords_in_window()[0], + viewport_coords_in_window()[1], + viewport_coords_in_window()[2], + viewport_coords_in_window()[3]); + + textures()[0]->queue_image_set_for_upload(images); + + for (const auto &idx : images->layout_data()->image_draw_order_hint_) { + __draw_image( + images, + idx, + window_to_viewport_matrix, + viewport_to_image_space, + viewport_du_dx); + } + + glEnable(GL_SCISSOR_TEST); + if (images->layout_data()->draw_hero_overlays_only_) { + __draw_per_image_overlays( + images, + images->hero_sub_playhead_index(), + window_to_viewport_matrix, + viewport_to_image_space, viewport_du_dx, - onscreen_frame_); + overlay_renderers); + } else { + for (const auto &idx : images->layout_data()->image_draw_order_hint_) { + __draw_per_image_overlays( + images, + idx, + window_to_viewport_matrix, + viewport_to_image_space, + viewport_du_dx, + overlay_renderers); + + } } + glDisable(GL_SCISSOR_TEST); - // if we've received a new image and/or colour pipeline data (LUTs etc) since the last - // draw, upload the data - upload_image_and_colour_data(next_images); } - /* Call the render functions of overlay plugins - for the BeforeImage pass, we only call - this if we have an alpha buffer that allows us to 'under' the image with the overlay - drawings. */ - if (onscreen_frame_ && has_alpha_) { - for (auto orf : viewport_overlay_renderers_) { - if (orf.second->preferred_render_pass() == - plugin::ViewportOverlayRenderer::BeforeImage) { - orf.second->render_opengl( - to_scene_matrix, - transform_viewport_to_image_space, - abs(viewport_du_dx), - onscreen_frame_, - has_alpha_); - } + // Some plugins want to draw on the whole viewport canvas (not over a particular + // image) + for (auto orf : overlay_renderers) { + if (orf.second->preferred_render_pass() == + plugin::ViewportOverlayRenderer::BeforeImage) { + orf.second->render_viewport_overlay( + window_to_viewport_matrix, viewport_to_image_space, abs(viewport_du_dx), false); } } - glDisable(GL_DEPTH_TEST); +#ifdef DEBUG_GRAB_FRAMEBUFFER + grab_framebuffer_to_disk(); +#endif - glEnable(GL_BLEND); - glBlendFunc(has_alpha_ ? GL_ONE_MINUS_DST_ALPHA : GL_ONE, GL_ONE); - glBlendEquation(GL_FUNC_ADD); - if (active_shader_program_) { + // The use of this is questionable - I'm not sure how it plays with the QML + // rendering engine. The idea is that we are guaranteeing the image has + // been rendered at this stage so we are sure it's on screen for the next + // refresh. In testing, it seems to help smooth playback under PCOIP when + // doing demanding rendering - e.g. high frame rate, high res sources with + // bilinear filtering. + // glFinish(); + /*std::cerr << "DRAW completed uploaded in " << + std::chrono::duration_cast(utility::clock::now()-t0).count() + << + "\n";*/ +} - active_shader_program_->use(); +void OpenGLViewportRenderer::__draw_image( + const media_reader::ImageBufDisplaySetPtr &images, + const int index, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx) { - bool use_bilinear_filtering = false; + if (!images || images->empty()) { + return; + } + media_reader::ImageBufPtr image_to_be_drawn = images->onscreen_image(index); + if (!image_to_be_drawn) + return; - if (onscreen_frame_) { - // here we can work out the ratio of image pixels to screen pixels - const float image_pix_to_screen_pix = - onscreen_frame_->image_size_in_pixels().x * viewport_du_dx; - if (render_hints_ == AlwaysBilinear) - use_bilinear_filtering = - image_pix_to_screen_pix < 0.99999f || image_pix_to_screen_pix > 1.00001f; - else if (render_hints_ == BilinearWhenZoomedOut) - use_bilinear_filtering = - image_pix_to_screen_pix > 1.00001f; // filter_mode_ == BilinearWhenZoomedOut - } + Imath::M44f to_image_matrix = + (image_to_be_drawn.layout_transform() * viewport_to_image_space.inverse()).inverse(); - // coordinate system set-up - utility::JsonStore shader_params = shader_uniforms_; - shader_params["to_coord_system"] = transform_viewport_to_image_space; - shader_params["to_canvas"] = to_scene_matrix; - shader_params["use_bilinear_filtering"] = use_bilinear_filtering; - shader_params["pixel_aspect"] = - onscreen_frame_ ? onscreen_frame_->pixel_aspect() : 1.0f; - active_shader_program_->set_shader_parameters(shader_params); - - // The quad that we draw simply fills the viewport area. Note the projection and model - // matrices are identity at draw time. - // In this case coord -1.0,-1.0 maps to bottom left and 1.0,1.0 - // maps to top right of the gl_viewport. - // - // However, when rendering in qml as a QQuickItem the gl_viewport is currently set to - // the shape of the QQuickWindow that contains the whole application UI - to transform - // to the coordinates of the Viewport QQuickItem, we multiply by the "to_canvas" matrix, - // which is done in the main shader. - static std::array vertices = { - -1.0f, - 1.0f, - 0.0f, - 1.0f, - 1.0f, - 1.0f, - 0.0f, - 1.0f, - 1.0f, - -1.0f, - 0.0f, - 1.0f, - -1.0f, - -1.0f, - 0.0f, - 1.0f}; - - glBindVertexArray(vao_); - // 2. copy our vertices array in a buffer for OpenGL to use - glBindBuffer(GL_ARRAY_BUFFER, vbo_); - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices.data(), GL_STATIC_DRAW); - // 3. then set our vertex module pointers - glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr); - glEnableVertexAttribArray(0); - glBindBuffer(GL_ARRAY_BUFFER, 0); + /* Here we allow plugins to run arbitrary GPU draw & computation routines. + This will allow pixel data to be rendered to textures (offscreen), for example, + which can then be sampled at actual draw time.*/ + for (auto hook : pre_render_gpu_hooks_) { + hook.second->pre_viewport_draw_gpu_hook( + window_to_viewport_matrix, to_image_matrix, viewport_du_dx, image_to_be_drawn); } - glBindVertexArray(vao_); - glDrawArrays(GL_QUADS, 0, 4); - glUseProgram(0); + // if we've received a new image and/or colour pipeline data (LUTs etc) since the last + // draw, upload the data + upload_image_and_colour_data(image_to_be_drawn); + + glEnable(GL_SCISSOR_TEST); + draw_image( + image_to_be_drawn, + images->layout_data(), + index, + window_to_viewport_matrix, + viewport_to_image_space, + viewport_du_dx); + glDisable(GL_SCISSOR_TEST); + + textures()[0]->release(image_to_be_drawn); +} + +void OpenGLViewportRenderer::__draw_per_image_overlays( + const media_reader::ImageBufDisplaySetPtr &images, + const int index, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx, + const std::map &overlay_renderers) { - /* N.B. To draw into the image coordinate system (where image with identity transform has - spans of -1.0 to 1.0 and is centred on 0,0): Imath::M44f r = - to_coord_sys.inverse()*to_canvas; glMatrixMode(GL_MODELVIEW_MATRIX); glMultMatrixf(r[0]); - */ - /* N.B. this glfinish is required to keep the popout viewport in sync with the main viewport - because the two may viewports share textures, shaders and other resources (they can be the - same GL context)* - without this segfault in graphics driver is possible */ - if (gl_context_shared_) - glFinish(); + if (!images || images->empty()) { + return; + } + + media_reader::ImageBufPtr target_image = images->onscreen_image(index); - release_textures(); + Imath::M44f to_image_matrix = + (target_image.layout_transform() * viewport_to_image_space.inverse()).inverse(); /* Call the render functions of overlay plugins - note that if the overlay prefers to draw before the image but we have no alpha channel, we still call its render function here */ - if (onscreen_frame_) { - for (auto orf : viewport_overlay_renderers_) { - if (orf.second->preferred_render_pass() == - plugin::ViewportOverlayRenderer::AfterImage || - !has_alpha_) { - orf.second->render_opengl( - to_scene_matrix, - transform_viewport_to_image_space, - abs(viewport_du_dx), - onscreen_frame_, - has_alpha_); - } + if (target_image) { + + + for (auto orf : overlay_renderers) { + orf.second->render_image_overlay( + window_to_viewport_matrix, + to_image_matrix, + abs(viewport_du_dx), + target_image, + false); } - } -#ifdef DEBUG_GRAB_FRAMEBUFFER - grab_framebuffer_to_disk(); -#endif + // display err message attached to image if there is one + if (!target_image.error_details().empty()) { + + std::vector vtxs; + resources_->text_renderer_->precompute_text_rendering_vertex_layout( + vtxs, + target_image.error_details(), + Imath::V2f(0.0f, 0.0f), + 1.0f, + 24.0f, + JustifyCentre, + 1.0f); + + resources_->text_renderer_->render_text( + vtxs, + window_to_viewport_matrix, + to_image_matrix, + utility::ColourTriplet(1.0f, 1.0f, 1.0f), + viewport_du_dx, + 15.0f, + 1.0f); + } + } +} - // The use of this is questionable - I'm not sure how it plays with the QML - // rendering engine. The idea is that we are guaranteeing the image has - // been rendered at this stage so we are sure it's on screen for the next - // refresh. In testing, it seems to help smooth playback under PCOIP when - // doing demanding rendering - e.g. high frame rate, high res sources with - // bilinear filtering. - // glFinish(); +void OpenGLViewportRenderer::draw_image( + const media_reader::ImageBufPtr &image_to_be_drawn, + const media_reader::ImageSetLayoutDataPtr &layout_data, + const int index, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx) { + + + active_shader_program_->use(); + + // set-up core shader parameters (e.g. image transform matrix etc) + utility::JsonStore shader_params = core_shader_params( + image_to_be_drawn, + window_to_viewport_matrix, + viewport_to_image_space, + viewport_du_dx, + layout_data->custom_layout_data_, + index); + + active_shader_program_->set_shader_parameters(shader_params); + + glDisable(GL_BLEND); + // the actual draw .. a quad that spans -1.0, 1.0 in x & y. + glBindVertexArray(vao()); + glEnableVertexAttribArray(0); + glDrawArrays(GL_TRIANGLES, 0, 6); + glDisableVertexAttribArray(0); + glBindVertexArray(0); + glUseProgram(0); } bool OpenGLViewportRenderer::activate_shader( @@ -446,11 +496,11 @@ bool OpenGLViewportRenderer::activate_shader( std::string shader_id = to_string(image_buffer_unpack_shader->shader_id()); for (const auto &op : colour_operations) { - shader_id += op->cache_id_; + shader_id += op->cache_id(); } // do we already have this shader compiled? - if (programs_.find(shader_id) == programs_.end()) { + if (shader_programs().find(shader_id) == shader_programs().end()) { // try to compile the shader for this combo of image buffer unpack // and colour pipeline components @@ -468,7 +518,7 @@ bool OpenGLViewportRenderer::activate_shader( shader_components.push_back(pr->shader_code()); } - programs_[shader_id].reset(new GLShaderProgram( + shader_programs()[shader_id].reset(new GLShaderProgram( default_vertex_shader, image_buffer_unpack_shader->shader_code(), shader_components, @@ -476,47 +526,116 @@ bool OpenGLViewportRenderer::activate_shader( } catch (std::exception &e) { spdlog::error("{}", e.what()); - programs_[shader_id].reset(); + shader_programs()[shader_id].reset(); } } - if (programs_[shader_id]) { - active_shader_program_ = programs_[shader_id]; + if (shader_programs()[shader_id]) { + active_shader_program_ = shader_programs()[shader_id]; } else { - active_shader_program_ = no_image_shader_program_; + active_shader_program_ = no_image_shader_program(); } - return active_shader_program_ != no_image_shader_program_; + return active_shader_program_ != no_image_shader_program(); } -void OpenGLViewportRenderer::pre_init() { +void OpenGLViewportRenderer::pre_init() { resources_->init(); } - glewInit(); +OpenGLViewportRenderer::SharedResources::SharedResources(const utility::JsonStore &prefs) { + + use_ssbo = prefs.get("/ui/viewport/texture_mode").value("value", std::string("")) == "SSBO"; + num_textures = prefs.get("/ui/viewport/num_textures").value("value", 4); +} + +OpenGLViewportRenderer::SharedResources::~SharedResources() { + if (vbo_) + glDeleteBuffers(1, &vbo_); + if (vao_) + glDeleteVertexArrays(1, &vao_); +} + + +void OpenGLViewportRenderer::SharedResources::init() { - // we need to know if we have alpha in our draw buffer, which might require - // different strategies for drawing overlays - int alpha_bits; - glGetIntegerv(GL_ALPHA_BITS, &alpha_bits); - has_alpha_ = alpha_bits != 0; + if (textures_.size()) + return; - // N.B. - if sharing of GL contexts is set-up for multiple GL viewport - // then we only create one set of textures and use them in both viewports - // thus meaning we only upload image data once. - static std::vector shared_textures; +#ifndef __apple__ + glewInit(); +#endif - if (shared_textures.size()) { - textures_ = shared_textures; +// #define OPENGL_DEBUG_CB +#ifdef OPENGL_DEBUG_CB + glEnable(GL_DEBUG_OUTPUT); + glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); + glDebugMessageCallback(debug_message_callback, nullptr); + // For simplicity we filter messages inside the callback + // Alternative would be using glDebugMessageControl +#endif + +#ifdef __OPENGL_4_1__ + textures_.emplace_back(GLDoubleBufferedTexture::create(num_textures)); +#else + if (use_ssbo) { + textures_.emplace_back(GLDoubleBufferedTexture::create(num_textures)); } else { - textures_.emplace_back(new GLDoubleBufferedTexture()); - if (gl_context_shared_) { - shared_textures = textures_; - } + textures_.emplace_back( + GLDoubleBufferedTexture::create(num_textures)); } - +#endif + // Set up the geometry used at draw time ... it couldn't be more simple, + // it's just two triangles to make a rectangle glGenBuffers(1, &vbo_); glGenVertexArrays(1, &vao_); + static std::array vertices = { + // 1st triangle + -1.0f, + 1.0f, + 0.0f, + 1.0f, // top left + 1.0f, + 1.0f, + 0.0f, + 1.0f, // top right + 1.0f, + -1.0f, + 0.0f, + 1.0f, // bottom right + // 2nd triangle + 1.0f, + -1.0f, + 0.0f, + 1.0f, // bottom right + -1.0f, + 1.0f, + 0.0f, + 1.0f, // top left + -1.0f, + -1.0f, + 0.0f, + 1.0f // bottom left + }; + + glBindVertexArray(vao_); + // 2. copy our vertices array in a buffer for OpenGL to use + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices.data(), GL_STATIC_DRAW); + // 3. then set our vertex module pointers + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr); + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + // glDeleteBuffers(1, &vbo); + text_renderer_ = std::make_unique( + utility::xstudio_resources_dir("fonts/Overpass-Regular.ttf"), 96); + // add shader for no image render - no_image_shader_program_ = - GLShaderProgramPtr(static_cast(new NoImageShaderProgram())); -} \ No newline at end of file + try { + no_image_shader_program_ = + GLShaderProgramPtr(static_cast(new NoImageShaderProgram())); + } catch (std::exception &e) { + spdlog::critical("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} diff --git a/src/ui/opengl/src/shader_program_base.cpp b/src/ui/opengl/src/shader_program_base.cpp index 34e10c8ce..b4eac2bd3 100644 --- a/src/ui/opengl/src/shader_program_base.cpp +++ b/src/ui/opengl/src/shader_program_base.cpp @@ -2,7 +2,7 @@ #include #include -#include "xstudio/ui/opengl/texture.hpp" +#include "xstudio/ui/opengl/opengl_texture_base.hpp" #include "xstudio/ui/opengl/shader_program_base.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/string_helpers.hpp" @@ -66,26 +66,39 @@ const char *vertex_shader_base = R"( layout (location = 0) in vec4 aPos; out vec2 texPosition; uniform ivec2 image_dims; +uniform mat4 image_transform_matrix; uniform mat4 to_coord_system; uniform mat4 to_canvas; -uniform float pixel_aspect; +uniform float image_aspect; +uniform ivec2 image_bounds_min; +uniform ivec2 image_bounds_max; vec2 calc_pixel_coordinate(vec2 viewport_coordinate); void main() { - vec4 rpos = aPos*to_coord_system; - gl_Position = aPos*to_canvas; - texPosition = vec2( - (rpos.x + 1.0f) * float(image_dims.x), - (rpos.y * pixel_aspect * float(image_dims.x)) + float(image_dims.y) - ) * 0.5f; + // awkward scale/translate to accommodate overscan where image_bounds (i.e. + // exr data window) is different to image_dims (i.e. display window size) + // This could/should go in image_transform_matrix! + float bdbx = float(image_bounds_max.x-image_bounds_min.x); + float bdby = float(image_bounds_max.y-image_bounds_min.y); + float alpha = float(image_bounds_min.x + image_bounds_max.x)/float(image_dims.x) - 1.0f; + float beta = bdbx/float(image_dims.x); + float alpha_y = float(image_bounds_min.y + image_bounds_max.y)/float(image_dims.y) - 1.0f; + float beta_y = bdby/float(image_dims.y); + vec4 rpos = aPos; + + rpos.x = alpha + beta*rpos.x; + rpos.y = alpha_y + beta_y*rpos.y; + rpos.y = rpos.y/image_aspect; + + gl_Position = rpos*image_transform_matrix*to_coord_system*to_canvas; + texPosition = vec2(image_bounds_min.x + bdbx*(aPos.x + 1.0f)*0.5f, image_bounds_min.y + bdby*(aPos.y + 1.0f)*0.5f); } )"; const char *frag_shader_base_tex = R"( -#version 430 core -#extension GL_ARB_shader_storage_buffer_object : require +#version 410 core in vec2 texPosition; out vec4 FragColor; //uniform usampler2DRect the_tex; @@ -97,7 +110,28 @@ uniform bool use_bilinear_filtering; uniform usampler2DRect the_tex; uniform ivec2 tex_dims; -uniform bool pack_rgb_10_bit; +uniform bool use_alpha; + +vec2 unpackHalf2x16(uint vv) { + + uint a = vv & uint(0xFFFF); + uint a_frac = a & uint(0x03FF); + uint a_exp = (a >> 10) & uint(0x1F); + uint signbit = a >> 15; + float pwr = a_exp == 0 ? -14.0 : -15.0 + float(a_exp); + float b = pow(2.0, pwr)*((a_exp == 0 ? 0.0 : 1.0) + float(a_frac)/1024.0); + b = signbit == 1 ? -b : b; + + a = vv >> 16; + a_frac = a & uint(0x3FF); + a_exp = (a >> 10) & uint(0x1F); + signbit = a >> 15; + pwr = a_exp == 0 ? -14.0 : -15.0 + float(a_exp); + float c = pow(2.0, pwr)*((a_exp == 0 ? 0.0 : 1.0) + float(a_frac)/1024.0); + c = signbit == 1 ? -c : c; + + return vec2(b, c); +} ivec2 step_sample(ivec2 tex_coord) { @@ -278,33 +312,9 @@ vec4 get_bicubic_filter(vec2 pos) mix(sample1, sample0, sx), sy); } -vec4 pack_RGB_10_10_10_2(vec4 rgb) -{ - // this sets up the rgba value so that if the fragment - // bit depth is 8 bit RGBA, the 4 bytes contain the - // RGB as packed 10 bit colours. We use this for SDI - // output, for example. - - // scale to 10 bits - uint offset = 64; - float scale = 876.0f; - uint r = offset + uint(max(0.0,min(rgb.r*scale,scale))); - uint g = offset + uint(max(0.0,min(rgb.g*scale,scale))); - uint b = offset + uint(max(0.0,min(rgb.b*scale,scale))); - - // pack - uint RR = (r << 20) + (g << 10) + b; - - // unpack! - return vec4(float((RR >> 24)&255)/255.0, - float((RR >> 16)&255)/255.0, - float((RR >> 8)&255)/255.0, - float(RR&255)/255.0); - -} - void main(void) { + if (texPosition.x < image_bounds_min.x || texPosition.x > image_bounds_max.x) FragColor = vec4(0.0,0.0,0.0,1.0); else if (texPosition.y < image_bounds_min.y || texPosition.y > image_bounds_max.y) FragColor = vec4(0.0,0.0,0.0,1.0); else { @@ -320,14 +330,11 @@ void main(void) } //INJECT_COLOUR_OPS_CALL - if (pack_rgb_10_bit) { - rgb_frag_value = pack_RGB_10_10_10_2(rgb_frag_value); - } else { + if (!use_alpha) { rgb_frag_value.a = 1.0; } FragColor = rgb_frag_value; - } } )"; @@ -341,7 +348,7 @@ out vec4 FragColor; uniform ivec2 image_dims; uniform ivec2 image_bounds_min; uniform ivec2 image_bounds_max; -uniform bool pack_rgb_10_bit; +uniform bool use_alpha; uniform bool use_bilinear_filtering; @@ -502,34 +509,10 @@ vec4 get_bicubic_filter(vec2 pos) mix(sample1, sample0, sx), sy); } -vec4 pack_RGB_10_10_10_2(vec4 rgb) -{ - // this sets up the rgba value so that if the fragment - // bit depth is 8 bit RGBA, the 4 bytes contain the - // RGB as packed 10 bit colours. We use this for SDI - // output, for example. - - // scale to 10 bits - uint offset = 64; - float scale = 876.0f; - uint r = offset + uint(max(0.0,min(rgb.r*scale,scale))); - uint g = offset + uint(max(0.0,min(rgb.g*scale,scale))); - uint b = offset + uint(max(0.0,min(rgb.b*scale,scale))); - - // pack - uint RR = (r << 20) + (g << 10) + b; - - // unpack! - return vec4(float((RR >> 24)&255)/255.0, - float((RR >> 16)&255)/255.0, - float((RR >> 8)&255)/255.0, - float(RR&255)/255.0); -} - void main(void) { - if (texPosition.x < image_bounds_min.x || texPosition.x > image_bounds_max.x) FragColor = vec4(0.0,0.0,0.0,1.0); - else if (texPosition.y < image_bounds_min.y || texPosition.y > image_bounds_max.y) FragColor = vec4(0.0,0.0,0.0,1.0); + if (texPosition.x < image_bounds_min.x || texPosition.x > image_bounds_max.x) FragColor = vec4(1.0,0.0,0.0,1.0); + else if (texPosition.y < image_bounds_min.y || texPosition.y > image_bounds_max.y) FragColor = vec4(0.0,.0,1.0,1.0); else { // For now, disabling bilinear filtering as it is too expensive and slowing refresh badly @@ -544,12 +527,6 @@ void main(void) //INJECT_COLOUR_OPS_CALL - if (pack_rgb_10_bit) { - rgb_frag_value = pack_RGB_10_10_10_2(rgb_frag_value); - } else { - rgb_frag_value.a = 1.0; - } - FragColor = rgb_frag_value; } } @@ -740,7 +717,7 @@ void GLShaderProgram::inject_colour_op_shader(const std::string &colour_op_shade colour_operation_index_++; } -void GLShaderProgram::compile() { +void GLShaderProgram::compile(const bool force_combine_frag_shaders) { // Get a program object. program_ = glCreateProgram(); @@ -755,14 +732,52 @@ void GLShaderProgram::compile() { shaders_.push_back(compile_vertex_shader(shader_code)); }); + // Note: Our approach of compiling the fragment shader components + // separately and linking into the final program is resulting in link + // errors in some cases. This is likely a bug in the OpenGL drivers + // and Krohnos docs pages actually warns us NOT to use this approach, + // even though it is 'perfectly legal'. + // I'm thinking that our original approach has performance benefits + // as shader programs are cached to disk by nvidia drivers, so we might + // see quicker generation of the final program if we've seen some of + // the fragment shaders before. So we try the old approach (where + // force_combine_frag_shaders=false) and then retry if we hit a link + // error with force_combine_frag_shaders=true + // This does mean that in the case where link fails first time, we are + // wasting some time. + + if (force_combine_frag_shaders) { + + std::string combined_shaders; + std::for_each( + fragment_shaders_.begin(), + fragment_shaders_.end(), + [&combined_shaders](const std::string &shader_code) { + combined_shaders = combined_shaders + "\n" + shader_code; + }); + + // here we strip version directives that each shader may (or may + // not) include. Instead we force version 410 which is the highest + // version that still allows MacOS compatibility. If this causes + // a compile failure the log will warn the developer anyway and we + // hope shaders will remain compatible with V410. + static std::regex version_regex(R"(\#version.+\n)"); + combined_shaders = std::regex_replace(combined_shaders, version_regex, " "); + // add back in v410 directive (see note above) + combined_shaders = "#version 410\n" + combined_shaders; + fragment_shaders_.clear(); + fragment_shaders_.emplace_back(std::move(combined_shaders)); + } + // compile the fragment shader objects std::for_each( fragment_shaders_.begin(), fragment_shaders_.end(), - [=](const std::string &shader_code) { + [this](const std::string &shader_code) { shaders_.push_back(compile_frag_shader(shader_code)); }); + } catch (...) { // a shader hasn't compiled ... delete anthing that did compile @@ -790,6 +805,12 @@ void GLShaderProgram::compile() { std::vector infoLog(maxLength); glGetProgramInfoLog(program_, maxLength, &maxLength, &infoLog[0]); + // Detach shaders after failed link .... (not clear if this is correct, + // but no errors have been observed) + std::for_each(shaders_.begin(), shaders_.end(), [&](GLuint shdr) { + glDetachShader(program_, shdr); + }); + // We don't need the program anymore. glDeleteProgram(program_); @@ -799,9 +820,23 @@ void GLShaderProgram::compile() { shaders_.clear(); + if (!force_combine_frag_shaders) { + // here we re-try compilation but combine all our fragment_shaders_ + // into a single shader, which may overcome the link error. See + // note above + compile(true); + return; + } + // Use the infoLog as you see fit. std::stringstream e; e << "Shader link error:\n\n" << infoLog.data(); + + std::for_each( + fragment_shaders_.begin(), + fragment_shaders_.end(), + [=](const std::string &shader_code) { std::cerr << shader_code << "\n\n"; }); + throw std::runtime_error(e.str().c_str()); } diff --git a/src/ui/opengl/src/texture.cpp b/src/ui/opengl/src/texture.cpp deleted file mode 100644 index 1ffb77ffd..000000000 --- a/src/ui/opengl/src/texture.cpp +++ /dev/null @@ -1,699 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include -#include -#include -#include - -#include "xstudio/ui/opengl/texture.hpp" -#include "xstudio/utility/chrono.hpp" - -using namespace xstudio::ui::opengl; - -namespace { - -class DebugTimer { - public: - DebugTimer(std::string m) : message_(std::move(m)) { t1_ = xstudio::utility::clock::now(); } - - ~DebugTimer() { - std::cerr << message_ << " completed in " - << std::chrono::duration_cast( - xstudio::utility::clock::now() - t1_) - .count() - << " microseconds\n"; - } - - private: - xstudio::utility::clock::time_point t1_; - const std::string message_; -}; - -} // namespace - -void GLBlindTex::release() { - // if linux - // mutex_.unlock(); - // endif - when_last_used_ = utility::clock::now(); -} - -GLBlindTex::~GLBlindTex() {} - -GLDoubleBufferedTexture::GLDoubleBufferedTexture() { - - if (using_ssbo_) { - textures_.emplace_back(new GLSsboTex()); - } else { - textures_.emplace_back(new GLBlindRGBA8bitTex()); - } - current_ = textures_.front(); -} - -void GLDoubleBufferedTexture::set_use_ssbo(const bool using_ssbo) { - - if (using_ssbo != using_ssbo_) { - textures_.clear(); - using_ssbo_ = using_ssbo; - if (using_ssbo_) { - textures_.emplace_back(new GLSsboTex()); - } else { - textures_.emplace_back(new GLBlindRGBA8bitTex()); - } - current_ = textures_.front(); - } -} - -void GLDoubleBufferedTexture::bind(int &tex_index, Imath::V2i &dims) { - - for (auto &t : textures_) { - if (t->media_key() == active_media_key_) { - // t->wait_on_upload(); - t->bind(tex_index, dims); - current_ = t; - break; - } - } -} - -void GLDoubleBufferedTexture::upload_next( - std::vector images_due_onscreen_soon) { - if (images_due_onscreen_soon.size()) { - active_media_key_ = images_due_onscreen_soon.front()->media_key(); - } - - // ensure that we have enough textures - we create 2 more than we need to - // give us some slack so we don't immediately need to re-use a texture that - // was just used for drawing - while (textures_.size() < (images_due_onscreen_soon.size() + 2)) { - if (using_ssbo_) { - textures_.emplace_back(new GLSsboTex()); - } else { - textures_.emplace_back(new GLBlindRGBA8bitTex()); - } - } - - auto available_textures = textures_; - - // knock images out of images_due_onscreen_soon that are already uploaded - auto p = images_due_onscreen_soon.begin(); - while (p != images_due_onscreen_soon.end()) { - - const auto key = (*p)->media_key(); - auto cmp = [&key](const GLBlindTexturePtr &tex) { return tex->media_key() == key; }; - auto tx = std::find_if(available_textures.begin(), available_textures.end(), cmp); - - if (tx != available_textures.end()) { - available_textures.erase(tx); - p = images_due_onscreen_soon.erase(p); - } else { - p++; - } - } - - // we have some textures that are 'free' because they do not have an image - // uploaded that is part of the 'images_due_onscreen_soon' set. We want to make sure that we - // use the texture that was actually used to draw the viewport as far back - // in time as possible. This is because OpenGL/graphics drivers do some - // clever async stuff meaning that although we think we have finished using - // a texture in our own draw routines, the GPU might still be using it to - // finalise draw instructions and thus our call to GLMapBuffer when we try - // to re-use the texture with another image could be blocked. - std::sort( - available_textures.begin(), - available_textures.end(), - [](const GLBlindTexturePtr &a, const GLBlindTexturePtr &b) -> bool { - return a->when_last_used() < b->when_last_used(); - }); - - - for (auto &tx : available_textures) { - if (!images_due_onscreen_soon.size()) - break; - tx->map_buffer_for_upload(images_due_onscreen_soon.front()); - images_due_onscreen_soon.erase(images_due_onscreen_soon.begin()); - tx->start_pixel_upload(); - } -} - -void GLDoubleBufferedTexture::release() { current_->release(); } - -GLBlindRGBA8bitTex::~GLBlindRGBA8bitTex() { - // if linux? TODO: Merged this in but might be problematic for Windows, do check. - // ensure no copying is in flight - if (upload_thread_.joinable()) - upload_thread_.join(); - - glDeleteTextures(1, &tex_id_); - glDeleteBuffers(1, &pixel_buf_object_id_); -} - -void GLBlindRGBA8bitTex::resize(const size_t required_size_bytes) { - - if (!required_size_bytes) - return; - - // N.B. Seeing redraw issues if the textures owned by GLDoubleBufferedTexture - // are not all the same size. This can happen if, when not playing back, the - // user looks at one frame of a large image (so one texture gets resized to - // suit that large image). Then they start playing back another image of a - // smaller size, the other textures might get initialised to the smaller - // size of the playback images. When this happens, we see scrambled pixels. - // I haven't got to the bottom of the problem so for now we force all - // textures to have the same size, the maximum of any texture that has been - // requested. Note that this method is called on every texture upload. - - static size_t max_tex_size = 0; - max_tex_size = std::max(max_tex_size, required_size_bytes); - - if (tex_id_) { - - if (max_tex_size <= tex_size_bytes()) { - - return; - } - - glDeleteTextures(1, &tex_id_); - glDeleteBuffers(1, &pixel_buf_object_id_); - } - - // Create the texture for RGB float display and the Y component of YUV display - glGenTextures(1, &tex_id_); - glBindTexture(GL_TEXTURE_RECTANGLE, tex_id_); - glEnable(GL_TEXTURE_RECTANGLE); - glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glPixelStorei(GL_UNPACK_ALIGNMENT, 8); - - // ensure the texture width and height are both a power of 2. - bytes_per_pixel_ = 4; - tex_width_ = (int)std::pow( - 2.0f, - std::ceil( - std::log(std::sqrt(float(max_tex_size / bytes_per_pixel_))) / std::log(2.0f))); - tex_height_ = (int)std::pow( - 2.0f, - std::ceil( - std::log(float(max_tex_size / (tex_width_ * bytes_per_pixel_))) / std::log(2.0f))); - - // tex_width_ = 8192; - // tex_height_ = 4096; - - glTexImage2D( - GL_TEXTURE_RECTANGLE, - 0, - GL_RGBA8UI, - tex_width_, - tex_height_, - 0, - GL_RGBA_INTEGER, - GL_UNSIGNED_BYTE, - nullptr); - - glGenBuffers(1, &pixel_buf_object_id_); - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixel_buf_object_id_); - glNamedBufferData( - pixel_buf_object_id_, - tex_width_ * tex_height_ * bytes_per_pixel_, - nullptr, - GL_DYNAMIC_DRAW); - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); -} - -void GLBlindRGBA8bitTex::start_pixel_upload() { - - // if (new_source_frame_) { - // if (upload_thread_.joinable()) - // upload_thread_.join(); - // std::unique_lock lck(mutex_); - // mutex_.lock(); - // upload_thread_ = std::thread(&GLBlindRGBA8bitTex::pixel_upload, this); - GLBlindRGBA8bitTex::pixel_upload(); - //} -} - -void GLBlindRGBA8bitTex::pixel_upload() { - - // std::unique_lock lck(mutex_); - if (!new_source_frame_->size()) { - // if linux - // mutex_.unlock(); - // endif - return; - } - - const int n_threads = 8; // TODO: proper thread count here - std::vector memcpy_threads; - size_t sz = std::min(tex_size_bytes(), new_source_frame_->size()); - size_t step = ((sz / n_threads) / 4096) * 4096; - auto *dst = (uint8_t *)new_source_frame_->buffer(); - - uint8_t *ioPtrY = buffer_io_ptr_; - - for (int i = 0; i < n_threads; ++i) { - memcpy_threads.emplace_back(memcpy, ioPtrY, dst, std::min(sz, step)); - ioPtrY += step; - dst += step; - sz -= step; - } - - // ensure any threads still running to copy data to this texture are done - for (auto &t : memcpy_threads) { - if (t.joinable()) - t.join(); - } - - // cv.notify_one(); // notify the waiting thread -} - - -void GLBlindRGBA8bitTex::map_buffer_for_upload(media_reader::ImageBufPtr &frame) { - - if (!frame) - return; - // acquire a write lock, - // mutex_.lock(); - // std::lock_guard lock(mutex_); - - new_source_frame_ = frame; - media_key_ = frame->media_key(); - - glEnable(GL_TEXTURE_RECTANGLE); - - if (new_source_frame_->size()) { - resize(new_source_frame_->size()); - - glNamedBufferData( - pixel_buf_object_id_, - tex_width_ * tex_height_ * bytes_per_pixel_, - nullptr, - GL_DYNAMIC_DRAW); - - buffer_io_ptr_ = (uint8_t *)glMapNamedBuffer(pixel_buf_object_id_, GL_WRITE_ONLY); - } - // The mutex will be automatically unlocked here when lock goes out of scope. - // No need to manually call mutex_.unlock(). - // mutex_.unlock(); - - // N.B. threads are probably still running here! -} - - -void GLBlindRGBA8bitTex::bind(int tex_index, Imath::V2i &dims) { - - // mutex_.lock(); - // std::unique_lock lck(mutex_); - - dims.x = tex_width_; - dims.y = tex_height_; - - if (new_source_frame_) { - - if (new_source_frame_->size()) { - // if (upload_thread_.joinable()) { - // upload_thread_.join(); - // } - - // now the texture data is transferred (on the GPU). - // Assumption is that this is fast. - - glUnmapNamedBuffer(pixel_buf_object_id_); - - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixel_buf_object_id_); - glBindTexture(GL_TEXTURE_RECTANGLE, tex_id_); - glPixelStorei(GL_UNPACK_ROW_LENGTH, tex_width_); - glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, new_source_frame_->size() / (tex_width_ * 4)); - glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); - glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); - glPixelStorei(GL_UNPACK_ALIGNMENT, 8); - - glTexSubImage2D( - GL_TEXTURE_RECTANGLE, - 0, - 0, - 0, - tex_width_, - new_source_frame_->size() / (tex_width_ * 4), - GL_RGBA_INTEGER, - GL_UNSIGNED_BYTE, - nullptr); - - glBindTexture(GL_TEXTURE_RECTANGLE, 0); - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); - } - - current_source_frame_ = new_source_frame_; - new_source_frame_.reset(); - } - - if (current_source_frame_->size()) { - glActiveTexture(tex_index + GL_TEXTURE0); - glBindTexture(GL_TEXTURE_RECTANGLE, tex_id_); - } -} - -GLint GLColourLutTexture::interpolation() { - switch (descriptor_.interpolation_) { - case colour_pipeline::LUTDescriptor::NEAREST: - return GL_NEAREST; - case colour_pipeline::LUTDescriptor::LINEAR: - return GL_LINEAR; - default: - return GL_LINEAR; - } -} - -GLint GLColourLutTexture::internal_format() { - if (descriptor_.channels_ == colour_pipeline::LUTDescriptor::RGB) { - switch (descriptor_.data_type_) { - case colour_pipeline::LUTDescriptor::UINT8: - return GL_RGB8UI; - case colour_pipeline::LUTDescriptor::UINT16: - return GL_RGB16UI; - case colour_pipeline::LUTDescriptor::FLOAT16: - return GL_RGB16F; - case colour_pipeline::LUTDescriptor::FLOAT32: - return GL_RGB32F; - default: - return GL_RGB32F; - } - } else { - switch (descriptor_.data_type_) { - case colour_pipeline::LUTDescriptor::UINT8: - return GL_R8UI; - case colour_pipeline::LUTDescriptor::UINT16: - return GL_R16UI; - case colour_pipeline::LUTDescriptor::FLOAT16: - return GL_R16F; - case colour_pipeline::LUTDescriptor::FLOAT32: - return GL_R32F; - default: - return GL_R32F; - } - } -} - -GLint GLColourLutTexture::format() { - if (descriptor_.channels_ == colour_pipeline::LUTDescriptor::RGB) { - - switch (descriptor_.data_type_) { - case colour_pipeline::LUTDescriptor::UINT8: - return GL_RGB_INTEGER; - case colour_pipeline::LUTDescriptor::UINT16: - return GL_RGB_INTEGER; - case colour_pipeline::LUTDescriptor::FLOAT16: - return GL_RGB; - case colour_pipeline::LUTDescriptor::FLOAT32: - return GL_RGB; - default: - return GL_RGB; - } - } else { - switch (descriptor_.data_type_) { - case colour_pipeline::LUTDescriptor::UINT8: - return GL_RED_INTEGER; - case colour_pipeline::LUTDescriptor::UINT16: - return GL_RED_INTEGER; - case colour_pipeline::LUTDescriptor::FLOAT16: - return GL_RED; - case colour_pipeline::LUTDescriptor::FLOAT32: - return GL_RED; - default: - return GL_RED; - } - } -} - -GLint GLColourLutTexture::data_type() { - switch (descriptor_.data_type_) { - case colour_pipeline::LUTDescriptor::UINT8: - return GL_UNSIGNED_BYTE; - case colour_pipeline::LUTDescriptor::UINT16: - return GL_UNSIGNED_SHORT; - case colour_pipeline::LUTDescriptor::FLOAT16: - return GL_HALF_FLOAT; - case colour_pipeline::LUTDescriptor::FLOAT32: - return GL_FLOAT; - default: - return GL_FLOAT; - } -} - - -GLColourLutTexture::GLColourLutTexture( - const colour_pipeline::LUTDescriptor desc, const std::string texture_name) - : descriptor_(std::move(desc)), texture_name_(std::move(texture_name)) { - // Create the texture for RGB float display and the Y component of YUV display - glGenTextures(1, &tex_id_); - - if (desc.dimension_ == colour_pipeline::LUTDescriptor::ONE_D) { - glBindTexture(target(), tex_id_); - glTexParameteri(target(), GL_TEXTURE_MIN_FILTER, interpolation()); - glTexParameteri(target(), GL_TEXTURE_MAG_FILTER, interpolation()); - glTexParameteri(target(), GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - glTexImage1D( - target(), 0, internal_format(), desc.xsize_, 0, format(), data_type(), nullptr); - - } else if (desc.dimension_ & colour_pipeline::LUTDescriptor::TWO_D) { - glBindTexture(target(), tex_id_); - glTexParameteri(target(), GL_TEXTURE_MIN_FILTER, interpolation()); - glTexParameteri(target(), GL_TEXTURE_MAG_FILTER, interpolation()); - glTexParameteri(target(), GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(target(), GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - glTexImage2D( - target(), - 0, - internal_format(), - desc.xsize_, - desc.ysize_, - 0, - format(), - data_type(), - nullptr); - - } else if (desc.dimension_ == colour_pipeline::LUTDescriptor::THREE_D) { - glBindTexture(target(), tex_id_); - glTexParameteri(target(), GL_TEXTURE_MIN_FILTER, interpolation()); - glTexParameteri(target(), GL_TEXTURE_MAG_FILTER, interpolation()); - glTexParameteri(target(), GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(target(), GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(target(), GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - glTexImage3D( - target(), - 0, - internal_format(), - desc.xsize_, - desc.ysize_, - desc.zsize_, - 0, - format(), - data_type(), - nullptr); - } - - glGenBuffers(1, &pbo_); -} - -GLColourLutTexture::~GLColourLutTexture() { - glDeleteTextures(1, &tex_id_); - glDeleteBuffers(1, &pbo_); -} - -GLenum GLColourLutTexture::target() const { - if (descriptor_.dimension_ == colour_pipeline::LUTDescriptor::ONE_D) - return GL_TEXTURE_1D; - else if (descriptor_.dimension_ == colour_pipeline::LUTDescriptor::TWO_D) - return GL_TEXTURE_2D; - else if (descriptor_.dimension_ == colour_pipeline::LUTDescriptor::THREE_D) - return GL_TEXTURE_3D; - else if (descriptor_.dimension_ == colour_pipeline::LUTDescriptor::RECT_TWO_D) - return GL_TEXTURE_RECTANGLE; - - return GL_TEXTURE_RECTANGLE; -} - - -void GLColourLutTexture::upload_texture_data(const colour_pipeline::ColourLUTPtr &lut) { - - if (lut->cache_id() == lut_cache_id_) - return; - - lut_cache_id_ = lut->cache_id(); - - // Create the texture for RGGLColourLutTextureB float display and the Y component of YUV - // display - glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); - glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); - glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_); - glBufferData(GL_PIXEL_UNPACK_BUFFER, lut->data_size(), nullptr, GL_STREAM_DRAW); - - void *mappedBuffer = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY); - memcpy(mappedBuffer, lut->data(), lut->data_size()); - - glBindTexture(target(), tex_id_); - glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); - - if (descriptor_.dimension_ == colour_pipeline::LUTDescriptor::ONE_D) { - - - glTexSubImage1D(target(), 0, 0, descriptor_.xsize_, format(), data_type(), nullptr); - - - } else if (descriptor_.dimension_ & colour_pipeline::LUTDescriptor::TWO_D) { - - glTexSubImage2D( - target(), - 0, - 0, - 0, - descriptor_.xsize_, - descriptor_.ysize_, - format(), - data_type(), - nullptr); - - } else if (descriptor_.dimension_ == colour_pipeline::LUTDescriptor::THREE_D) { - - glTexSubImage3D( - target(), - 0, - 0, - 0, - 0, - descriptor_.xsize_, - descriptor_.ysize_, - descriptor_.zsize_, - format(), - data_type(), - nullptr); - } - // glActiveTexture((GLenum)(GL_TEXTURE0)); - glBindTexture(target(), 0); - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); -} - -void GLColourLutTexture::bind(int tex_index) { - glActiveTexture(tex_index + GL_TEXTURE0); - glBindTexture(target(), tex_id_); -} - -GLSsboTex::GLSsboTex() { glGenBuffers(1, &ssbo_id_); } - -GLSsboTex::~GLSsboTex() { - if (upload_thread_.joinable()) - upload_thread_.join(); - glDeleteBuffers(1, &ssbo_id_); -} - - -void GLSsboTex::wait_on_upload() { - - mutex_.lock(); - if (new_source_frame_) { - - if (new_source_frame_->size()) { - if (upload_thread_.joinable()) { - upload_thread_.join(); - } - - glUnmapNamedBuffer(ssbo_id_); - } - - current_source_frame_ = new_source_frame_; - new_source_frame_.reset(); - } - - if (current_source_frame_->size()) { - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo_id_); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - } - - mutex_.unlock(); -} - -void GLSsboTex::map_buffer_for_upload(media_reader::ImageBufPtr &frame) { - - auto t0 = utility::clock::now(); - - if (!frame) - return; - - mutex_.lock(); - - new_source_frame_ = frame; - media_key_ = frame->media_key(); - - if (new_source_frame_->size()) { - compute_size(new_source_frame_->size()); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo_id_); - glNamedBufferData(ssbo_id_, tex_size_bytes(), nullptr, GL_DYNAMIC_DRAW); - buffer_io_ptr_ = (uint8_t *)glMapNamedBuffer(ssbo_id_, GL_WRITE_ONLY); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - } - - mutex_.unlock(); -} - - -void GLSsboTex::compute_size(const size_t required_size_bytes) { - - if (!required_size_bytes) - return; - - if (ssbo_id_) { - if (required_size_bytes <= tex_size_bytes()) { - return; - } - } - - tex_data_size_ = pow(2, ceil(log((1 + (required_size_bytes / 4096)) * 4096) / log(2.0))); -} - -void GLSsboTex::start_pixel_upload() { - - if (new_source_frame_) { - if (upload_thread_.joinable()) - upload_thread_.join(); - mutex_.lock(); - upload_thread_ = std::thread(&GLSsboTex::pixel_upload, this); - } -} - -void GLSsboTex::pixel_upload() { - - if (!new_source_frame_->size()) { - mutex_.unlock(); - return; - } - - const int n_threads = 8; // TODO: proper thread count here - std::vector memcpy_threads; - size_t sz = std::min(tex_size_bytes(), new_source_frame_->size()); - size_t step = ((sz / n_threads) / 4096) * 4096; - auto *dst = (uint8_t *)new_source_frame_->buffer(); - - uint8_t *ioPtrY = buffer_io_ptr_; - - for (int i = 0; i < n_threads; ++i) { - memcpy_threads.emplace_back(memcpy, ioPtrY, dst, std::min(sz, step)); - ioPtrY += step; - dst += step; - sz -= step; - } - - // ensure any threads still running to copy data to this texture are done - for (auto &t : memcpy_threads) { - if (t.joinable()) - t.join(); - } - - mutex_.unlock(); -} diff --git a/src/ui/opengl/test/font_rendering_test.cpp b/src/ui/opengl/test/font_rendering_test.cpp index a359a681c..943070d57 100644 --- a/src/ui/opengl/test/font_rendering_test.cpp +++ b/src/ui/opengl/test/font_rendering_test.cpp @@ -10,5 +10,5 @@ using namespace xstudio; TEST(LoadFontTest, Test) { - OpenGLTextRendererBitmap(utility::xstudio_root("/fonts/Overpass-Regular.ttf"), 48); + OpenGLTextRendererBitmap(utility::xstudio_resources_dir("fonts/Overpass-Regular.ttf"), 48); } \ No newline at end of file diff --git a/src/ui/qml/CMakeLists.txt b/src/ui/qml/CMakeLists.txt index 0554a4823..024827ed8 100644 --- a/src/ui/qml/CMakeLists.txt +++ b/src/ui/qml/CMakeLists.txt @@ -5,7 +5,13 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -find_package(Qt5 COMPONENTS Core Quick Gui Widgets OpenGL Test Concurrent REQUIRED) +find_package(Qt6 COMPONENTS Core Quick Gui OpenGL OpenGLWidgets Test Widgets Concurrent REQUIRED) + +if(WIN32) +else() + find_package(Qt6 COMPONENTS DBus REQUIRED) +endif() + configure_file(.clang-tidy .clang-tidy) if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU") @@ -13,21 +19,18 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU" set(DCMAKE_EXE_LINKER_FLAGS "${DCMAKE_EXE_LINKER_FLAGS} -fpic") endif() # QT5_ADD_RESOURCES(PROTOTYPE_RCS) -# if (Qt5_POSITION_INDEPENDENT_CODE) +# if (Qt6_POSITION_INDEPENDENT_CODE) # SET(CMAKE_POSITION_INDEPENDENT_CODE ON) # endif() add_src_and_test(bookmark) +add_src_and_test(conform) add_src_and_test(embedded_python) -add_src_and_test(event) add_src_and_test(global_store) add_src_and_test(helper) add_src_and_test(json_store) add_src_and_test(log) -# add_src_and_test(media) -add_src_and_test(module) add_src_and_test(playhead) add_src_and_test(session) add_src_and_test(studio) -add_src_and_test(tag) add_src_and_test(viewport) diff --git a/src/ui/qml/bookmark/src/CMakeLists.txt b/src/ui/qml/bookmark/src/CMakeLists.txt index 711d36305..f994e94c8 100644 --- a/src/ui/qml/bookmark/src/CMakeLists.txt +++ b/src/ui/qml/bookmark/src/CMakeLists.txt @@ -1,6 +1,6 @@ SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core + CAF::core + Qt6::Core xstudio::ui::qml::helper xstudio::utility ) @@ -9,4 +9,4 @@ SET(EXTRAMOC "${ROOT_DIR}/include/xstudio/ui/qml/bookmark_model_ui.hpp" ) -create_qml_component(bookmark 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") +create_qml_component(bookmark ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/bookmark/src/bookmark_model_ui.cpp b/src/ui/qml/bookmark/src/bookmark_model_ui.cpp index ff3d3367b..73d4f3ede 100644 --- a/src/ui/qml/bookmark/src/bookmark_model_ui.cpp +++ b/src/ui/qml/bookmark/src/bookmark_model_ui.cpp @@ -1,15 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 -// CAF_PUSH_WARNINGS -// CAF_POP_WARNINGS // #include "xstudio/atoms.hpp" #include "xstudio/ui/qml/bookmark_model_ui.hpp" #include "xstudio/ui/qml/json_tree_model_ui.hpp" #include "xstudio/bookmark/bookmark.hpp" +#include "xstudio/thumbnail/thumbnail.hpp" // #include "xstudio/utility/helpers.hpp" // #include "xstudio/utility/json_store.hpp" // #include "xstudio/utility/logging.hpp" // #include "xstudio/global_store/global_store.hpp" +CAF_PUSH_WARNINGS +#include +CAF_POP_WARNINGS using namespace caf; using namespace xstudio; @@ -72,10 +74,76 @@ QVariant BookmarkCategoryModel::data(const QModelIndex &index, int role) const { } BookmarkFilterModel::BookmarkFilterModel(QObject *parent) : QSortFilterProxyModel(parent) { + + connect( + this, &QSortFilterProxyModel::rowsInserted, this, &BookmarkFilterModel::lengthChanged); + connect( + this, &QSortFilterProxyModel::modelReset, this, &BookmarkFilterModel::lengthChanged); + connect( + this, &QSortFilterProxyModel::rowsRemoved, this, &BookmarkFilterModel::lengthChanged); + + setDynamicSortFilter(true); sort(0, Qt::DescendingOrder); } +QModelIndex +BookmarkFilterModel::nextBookmark(const int mediaFrame, const QUuid &ownerUuid) const { + // find last match + auto result = QModelIndex(); + auto startedMatching = false; + + for (auto i = 0; i < rowCount(); i++) { + auto ind = index(i, 0); + if (ind.isValid()) { + auto owner = ind.data(BookmarkModel::Roles::ownerRole).toUuid(); + auto startFrame = ind.data(BookmarkModel::Roles::startFrameRole).toInt(); + auto endFrame = ind.data(BookmarkModel::Roles::endFrameRole).toInt(); + + if (ownerUuid == owner and mediaFrame >= startFrame and mediaFrame <= endFrame) { + startedMatching = true; + } else if (not startedMatching and ownerUuid == owner and mediaFrame < startFrame) { + result = ind; + break; + } else if (startedMatching) { + result = ind; + break; + } + } + } + + return result; +} + +QModelIndex +BookmarkFilterModel::previousBookmark(const int mediaFrame, const QUuid &ownerUuid) const { + auto result = QModelIndex(); + auto startedMatching = false; + + for (auto i = rowCount() - 1; i >= 0; i--) { + auto ind = index(i, 0); + + if (ind.isValid()) { + auto owner = ind.data(BookmarkModel::Roles::ownerRole).toUuid(); + auto startFrame = ind.data(BookmarkModel::Roles::startFrameRole).toInt(); + auto endFrame = ind.data(BookmarkModel::Roles::endFrameRole).toInt(); + + if (ownerUuid == owner and endFrame < mediaFrame) { + result = ind; + break; + } else if (ownerUuid == owner) { + startedMatching = true; + } else if (startedMatching) { + result = ind; + break; + } + } + } + + return result; +} + + bool BookmarkFilterModel::filterAcceptsRow( int source_row, const QModelIndex &source_parent) const { bool result = true; @@ -83,7 +151,7 @@ bool BookmarkFilterModel::filterAcceptsRow( QModelIndex index = sourceModel()->index(source_row, 0, source_parent); auto visible = sourceModel()->data(index, BookmarkModel::Roles::visibleRole).toBool(); - if (not visible) + if (not visible and not showHidden_) return false; auto owner = sourceModel()->data(index, BookmarkModel::Roles::ownerRole).toString(); @@ -92,10 +160,27 @@ bool BookmarkFilterModel::filterAcceptsRow( "--:--:--:--") return false; + if (!showUserType_.isEmpty()) { + if (showUserType_ != index.data(BookmarkModel::Roles::userTypeRole).toString()) + return false; + } else { + if (!index.data(BookmarkModel::Roles::userTypeRole).toString().isEmpty()) + return false; + } + + if (included_categories_.size()) { + if (!included_categories_.contains( + index.data(BookmarkModel::Roles::categoryRole).toString())) + return false; + } else if (excluded_categories_.size()) { + if (excluded_categories_.contains( + index.data(BookmarkModel::Roles::categoryRole).toString())) + return false; + } + switch (depth_) { - case 3: - break; case 2: + break; case 1: result = media_order_.contains(owner); break; @@ -113,19 +198,28 @@ bool BookmarkFilterModel::lessThan( const QModelIndex &source_left, const QModelIndex &source_right) const { bool result = false; - auto lor = source_left.data(BookmarkModel::Roles::ownerRole); - auto ror = source_right.data(BookmarkModel::Roles::ownerRole); + if (sortbyCreated_) { + + auto lcr = source_left.data(BookmarkModel::Roles::createdEpochRole).toLongLong(); + auto rcr = source_right.data(BookmarkModel::Roles::createdEpochRole).toLongLong(); + result = lcr > rcr; - if (lor == ror) { - result = source_left.data(BookmarkModel::Roles::startTimecodeRole).toString() > - source_right.data(BookmarkModel::Roles::startTimecodeRole).toString(); } else { - auto lors = lor.toString(); - auto rors = ror.toString(); - result = - ((media_order_.contains(lors) ? media_order_[lors].toInt() : -1) > - (media_order_.contains(rors) ? media_order_[rors].toInt() : -1)); + auto lor = source_left.data(BookmarkModel::Roles::ownerRole); + auto ror = source_right.data(BookmarkModel::Roles::ownerRole); + + if (lor == ror) { + result = source_left.data(BookmarkModel::Roles::startTimecodeRole).toString() > + source_right.data(BookmarkModel::Roles::startTimecodeRole).toString(); + } else { + auto lors = lor.toString(); + auto rors = ror.toString(); + + result = + ((media_order_.contains(lors) ? media_order_[lors].toInt() : -1) > + (media_order_.contains(rors) ? media_order_[rors].toInt() : -1)); + } } return result; @@ -156,34 +250,78 @@ void BookmarkFilterModel::setCurrentMedia(const QVariant &value) { } } +void BookmarkFilterModel::setShowHidden(const bool value) { + if (value != showHidden_) { + showHidden_ = value; + emit showHiddenChanged(); + invalidateFilter(); + } +} + +void BookmarkFilterModel::setShowUserType(const QString &value) { + if (value != showUserType_) { + showUserType_ = value; + emit showUserTypeChanged(); + invalidateFilter(); + } +} + +void BookmarkFilterModel::setExcludedCategories(const QStringList value) { + if (value != excluded_categories_) { + excluded_categories_ = value; + emit excludedCategoriesChanged(); + invalidateFilter(); + } +} + +void BookmarkFilterModel::setIncludedCategories(const QStringList value) { + if (value != included_categories_) { + included_categories_ = value; + emit includedCategoriesChanged(); + invalidateFilter(); + } +} + +void BookmarkFilterModel::setsortbyCreated(const bool value) { + if (value != sortbyCreated_) { + sortbyCreated_ = value; + emit sortbyCreatedChanged(); + invalidateFilter(); + } +} + BookmarkModel::BookmarkModel(QObject *parent) : super(parent) { init(CafSystemObject::get_actor_system()); setRoleNames(std::vector( - {"enabledRole", - "focusRole", - "frameRole", - "frameFromTimecodeRole", - "startTimecodeRole", - "endTimecodeRole", - "durationTimecodeRole", - "startFrameRole", - "endFrameRole", - "hasNoteRole", - "subjectRole", - "noteRole", - "authorRole", + {"authorRole", "categoryRole", "colourRole", + "createdEpochRole", "createdRole", + "durationFrameRole", + "durationRole", + "durationTimecodeRole", + "enabledRole", + "endFrameRole", + "endTimecodeRole", + "focusRole", + "frameFromTimecodeRole", + "frameRole", "hasAnnotationRole", - "thumbnailRole", - "ownerRole", - "uuidRole", + "hasNoteRole", + "metadataChangedRole", + "noteRole", "objectRole", + "ownerRole", + "startFrameRole", "startRole", - "durationRole", - "durationFrameRole", + "startTimecodeRole", + "subjectRole", + "thumbnailRole", + "userDataRole", + "userTypeRole", + "uuidRole", "visibleRole"})); } @@ -191,6 +329,10 @@ BookmarkModel::BookmarkModel(QObject *parent) : super(parent) { QVector BookmarkModel::getRoleChanges( const bookmark::BookmarkDetail &ood, const bookmark::BookmarkDetail &nbd) const { QVector roles; + if (ood.annotation_hash_ && nbd.annotation_hash_ && + *(ood.annotation_hash_) != *(nbd.annotation_hash_)) { + roles.push_back(thumbnailRole); + } return roles; } @@ -217,25 +359,47 @@ BookmarkModel::getJSONFuture(const QModelIndex &index, const QString &path) cons }); } +// special handing for media!! +QFuture +BookmarkModel::getJSONObjectFuture(const QModelIndex &index, const QString &path) const { + return QtConcurrent::run([=]() { + if (bookmark_actor_) { + std::string path_string = StdFromQString(path); + try { + scoped_actor sys{system()}; + auto addr = UuidFromQUuid(index.data(uuidRole).toUuid()); + auto result = request_receive( + *sys, bookmark_actor_, json_store::get_json_atom_v, addr, path_string); + + return mapFromValue(result); + + } catch ([[maybe_unused]] const std::exception &err) { + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + return QVariant(); // QStringFromStd(err.what()); + } + } + return QVariant(); + }); +} + void BookmarkModel::init(caf::actor_system &_system) { super::init(_system); - self()->set_default_handler(caf::drop); - set_message_handler([=](caf::actor_companion * /*self*/) -> caf::message_handler { return { [=](utility::event_atom, utility::last_changed_atom, const time_point &) {}, [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg &) {}, [=](utility::event_atom, utility::change_atom) {}, + [=](utility::event_atom, utility::name_atom, const std::string &) {}, + [=](utility::event_atom, bookmark::bookmark_change_atom, const utility::Uuid &) {}, [=](utility::event_atom, bookmark::bookmark_change_atom, const utility::UuidActor &ua) { - // spdlog::warn("bookmark::bookmark_change_atom {}", to_string(ua.uuid()) ); - auto ind = search_recursive(QUuidFromUuid(ua.uuid()), "uuidRole"); + auto ind = searchRecursive(QUuidFromUuid(ua.uuid()), "uuidRole"); if (ind.isValid()) { + try { auto detail = getDetail(ua.actor()); @@ -245,11 +409,20 @@ void BookmarkModel::init(caf::actor_system &_system) { auto node = indexToTree(ind); node->data().update(jsn); + // Needs optimising!! Currently this leaves 'change' + // empty, meaning all roles are changing. This means + // thumbnail role is always updated so thumbnail is + // re-rendered for every character the user enters into + // a note, for example auto change = getRoleChanges(bookmarks_.at(ua.uuid()), detail); - // if(not change.isEmpty()) { + + if (change.contains(thumbnailRole)) { + out_of_date_thumbnails_.insert(detail.uuid_); + } + bookmarks_[ua.uuid()] = detail; emit dataChanged(ind, ind, change); - // } + } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -259,23 +432,33 @@ void BookmarkModel::init(caf::actor_system &_system) { bookmark::remove_bookmark_atom, const utility::Uuid &uuid) { // find in model, remove row and delete from helper map. - auto ind = search_recursive(QUuidFromUuid(uuid), "uuidRole"); + auto ind = searchRecursive(QUuidFromUuid(uuid), "uuidRole"); if (ind.isValid()) { removeRows(ind.row(), 1, ind.parent()); bookmarks_.erase(uuid); emit lengthChanged(); } + // may need to unsubscribe!! }, [=](utility::event_atom, bookmark::add_bookmark_atom, const utility::UuidActor &ua) { // spdlog::warn("bookmark::add_bookmark_atom {}", to_string(ua.uuid()) ); // spdlog::warn("bookmark::add_bookmark_atom {}", data_.dump(2) ); - auto ind = search_recursive(QUuidFromUuid(ua.uuid()), "uuidRole"); + auto ind = searchRecursive(QUuidFromUuid(ua.uuid()), "uuidRole"); + if (not ind.isValid()) { + // join events from bookmark. + scoped_actor sys{system()}; + // request detail. try { + auto adetail = + request_receive(*sys, ua.actor(), detail_atom_v); + request_receive( + *sys, adetail.group_, broadcast::join_broadcast_atom_v, as_actor()); + auto detail = getDetail(ua.actor()); if (JSONTreeModel::insertRows(rowCount(), 1)) { @@ -295,14 +478,64 @@ void BookmarkModel::init(caf::actor_system &_system) { // populateBookmarks(); }, - [=](utility::event_atom, utility::name_atom, const std::string &) {}, + [=](media_reader::get_thumbnail_atom, + const bookmark::BookmarkDetail &detail, + const thumbnail::ThumbnailBufferPtr &thumbnail) { + // this is the response we made for a thumbnail, sent to us + // from the global BookmarksActor + thumbnail_cache_[detail.uuid_] = + QImage(thumbnail->width(), thumbnail->height(), QImage::Format_RGB888); + auto p = out_of_date_thumbnails_.find(detail.uuid_); + if (p != out_of_date_thumbnails_.end()) + out_of_date_thumbnails_.erase(p); + + uint8_t *bits = thumbnail_cache_[detail.uuid_].bits(); + memcpy( + bits, + (uchar *)&(thumbnail->data()[0]), + 3 * thumbnail->width() * thumbnail->height()); + auto ind = searchRecursive(QUuidFromUuid(detail.uuid_), "uuidRole"); + if (ind.isValid()) { + dataChanged(ind, ind, QVector({thumbnailRole})); + } + }, + [=](json_store::update_atom, const JsonStore & /*change*/, const std::string &, - const JsonStore &) {}, - - [=](json_store::update_atom, const JsonStore &) {}, - }; + const JsonStore &) { + // copied from session model.. + try { + auto src = caf::actor_cast(self()->current_sender()); + + for (const auto &i : bookmarks_) { + if (i.second.actor_addr_ == src) { + auto ind = searchRecursive(QUuidFromUuid(i.first), "uuidRole"); + if (ind.isValid()) + emit dataChanged(ind, ind, QVector({metadataChangedRole})); + break; + } + } + } catch (...) { + } + }, + [=](json_store::update_atom, const JsonStore &) { + // // emit metadata changed if we get an event from a bookmark + try { + auto src = caf::actor_cast(self()->current_sender()); + + for (const auto &i : bookmarks_) { + if (i.second.actor_addr_ == src) { + auto ind = searchRecursive(QUuidFromUuid(i.first), "uuidRole"); + if (ind.isValid()) + emit dataChanged(ind, ind, QVector({metadataChangedRole})); + break; + } + } + } catch (...) { + } + }, + [=](caf::message) {}}; }); } @@ -348,6 +581,7 @@ void BookmarkModel::setBookmarkActorAddr(const QString &addr) { // we now need to pull all the bookmarks and reset the model. // we might want to spin this off.. setModelData(jsonFromBookmarks()); + emit lengthChanged(); } } @@ -357,6 +591,9 @@ BookmarkModel::createJsonFromDetail(const bookmark::BookmarkDetail &detail) cons auto result = R"({"uuid": null, "thumbnailURL": "qrc:///feather_icons/film.svg"})"_json; result["uuid"] = detail.uuid_; + if (detail.annotation_hash_) { + result["annotation_hash"] = *(detail.annotation_hash_); + } // spdlog::warn("{} {} {}", bool(detail.owner_) , to_string((*(detail.owner_)).actor()), // bool(detail.logical_start_frame_) ); @@ -381,6 +618,12 @@ nlohmann::json BookmarkModel::jsonFromBookmarks() { // we now need to build json data from these... for (const auto &i : details) { + + auto adetail = request_receive( + *sys, caf::actor_cast(i.actor_addr_), detail_atom_v); + request_receive( + *sys, adetail.group_, broadcast::join_broadcast_atom_v, as_actor()); + bookmarks_[i.uuid_] = i; result.emplace_back(createJsonFromDetail(i)); } @@ -407,7 +650,7 @@ bool BookmarkModel::removeRows(int row, int count, const QModelIndex &parent) { } for (const auto &i : uuids) { - anon_send(bookmark_actor_, bookmark::remove_bookmark_atom_v, i); + anon_mail(bookmark::remove_bookmark_atom_v, i).send(bookmark_actor_); bookmarks_.erase(i); } } @@ -444,9 +687,21 @@ QVariant BookmarkModel::data(const QModelIndex &index, int role) const { break; case durationFrameRole: - if (detail.media_reference_) { + if (detail.media_reference_ && detail.duration_) { result = QVariant::fromValue( - *(detail.duration_) / detail.media_reference_->rate()); + detail.duration_->count() / detail.media_reference_->rate().count()); + } + break; + + case userTypeRole: + if (detail.user_type_) { + result = QVariant::fromValue(QStringFromStd(*(detail.user_type_))); + } + break; + + case userDataRole: + if (detail.user_data_) { + result = QVariantMapFromJson(*(detail.user_data_)); } break; @@ -556,12 +811,35 @@ QVariant BookmarkModel::data(const QModelIndex &index, int role) const { QDateTime::currentDateTime(), QLocale::ShortFormat)); } break; + case createdEpochRole: + if (detail.created_) { + result = QVariant::fromValue( + std::chrono::duration_cast( + (*(detail.created_)).time_since_epoch()) + .count()); + } else { + result = QVariant::fromValue( + std::chrono::duration_cast( + utility::sysclock::now().time_since_epoch()) + .count()); + } + break; case hasAnnotationRole: result = QVariant::fromValue(*(detail.has_annotation_)); break; - case thumbnailRole: - result = QVariant::fromValue(QStringFromStd(j.at("thumbnailURL"))); - break; + case thumbnailRole: { + bool get_thumbnail = true; + if (thumbnail_cache_.count(detail.uuid_)) { + result = thumbnail_cache_.at(detail.uuid_); + if (!out_of_date_thumbnails_.count(detail.uuid_)) { + get_thumbnail = false; + } + } + if (get_thumbnail) { + anon_mail(media_reader::get_thumbnail_atom_v, detail, 256, as_actor()) + .send(bookmark_actor_); + } + } break; case ownerRole: result = QVariant::fromValue(QUuidFromUuid((*(detail.owner_)).uuid())); break; @@ -607,9 +885,14 @@ bool BookmarkModel::setData(const QModelIndex &index, const QVariant &value, int sendDetail(bm); result = true; } + } else { } break; + case metadataChangedRole: + result = true; + break; + case endFrameRole: if (detail.media_reference_) { auto new_end = value.toInt() * (*(detail.media_reference_)).rate(); @@ -687,6 +970,15 @@ bool BookmarkModel::setData(const QModelIndex &index, const QVariant &value, int } } break; + case createdEpochRole: { + auto created = utility::sys_time_point(milliseconds(value.toLongLong())); + if (not detail.created_ or (*detail.created_) != created) { + detail.created_ = bm.created_ = created; + sendDetail(bm); + result = true; + } + } break; + case ownerRole: { auto str = UuidFromQUuid(value.toUuid()); if (not detail.owner_ or (*detail.owner_).uuid() != str) { @@ -697,7 +989,7 @@ bool BookmarkModel::setData(const QModelIndex &index, const QVariant &value, int } break; case startRole: { - timebase::flicks val = timebase::to_flicks(value.toDouble()); + timebase::flicks val(value.toLongLong()); if (not detail.start_ or (*detail.start_) != val) { detail.start_ = bm.start_ = val; sendDetail(bm); @@ -730,6 +1022,36 @@ bool BookmarkModel::setData(const QModelIndex &index, const QVariant &value, int } } } break; + + case userTypeRole: { + auto str = StdFromQString(value.toString()); + if (not detail.user_type_ or (*detail.user_type_) != str) { + detail.user_type_ = bm.user_type_ = str; + sendDetail(bm); + result = true; + } + } break; + + case userDataRole: { + // Copied from shotgun_model_ui.cpp setData() + nlohmann::json jval; + if (std::string(value.typeName()) == "QJSValue") { + jval = nlohmann::json::parse( + QJsonDocument::fromVariant(value.value().toVariant()) + .toJson(QJsonDocument::Compact) + .constData()); + } else { + jval = nlohmann::json::parse(QJsonDocument::fromVariant(value) + .toJson(QJsonDocument::Compact) + .constData()); + } + + if (not detail.user_data_ or (*detail.user_data_) != jval) { + detail.user_data_ = bm.user_data_ = jval; + sendDetail(bm); + result = true; + } + } break; } } } catch (const std::exception &err) { @@ -745,7 +1067,7 @@ bool BookmarkModel::setData(const QModelIndex &index, const QVariant &value, int void BookmarkModel::sendDetail(const bookmark::BookmarkDetail &detail) const { if (bookmark_actor_) - anon_send(bookmark_actor_, bookmark::bookmark_detail_atom_v, detail.uuid_, detail); + anon_mail(bookmark::bookmark_detail_atom_v, detail.uuid_, detail).send(bookmark_actor_); } Q_INVOKABLE bool BookmarkModel::insertRows(int row, int count, const QModelIndex &parent) { @@ -762,6 +1084,11 @@ Q_INVOKABLE bool BookmarkModel::insertRows(int row, int count, const QModelIndex *sys, bookmark_actor_, bookmark::add_bookmark_atom_v); auto detail = getDetail(ua.actor()); + auto adetail = + request_receive(*sys, ua.actor(), detail_atom_v); + request_receive( + *sys, adetail.group_, broadcast::join_broadcast_atom_v, as_actor()); + bookmarks_[ua.uuid()] = detail; auto ind = index(row + i, 0); JSONTreeModel::setData( diff --git a/src/ui/qml/bookmark/src/include/bookmark_qml_export.h b/src/ui/qml/bookmark/src/include/bookmark_qml_export.h deleted file mode 100644 index cf2dd4ade..000000000 --- a/src/ui/qml/bookmark/src/include/bookmark_qml_export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef BOOKMARK_QML_EXPORT_H -#define BOOKMARK_QML_EXPORT_H - -#ifdef BOOKMARK_QML_STATIC_DEFINE -# define BOOKMARK_QML_EXPORT -# define BOOKMARK_QML_NO_EXPORT -#else -# ifndef BOOKMARK_QML_EXPORT -# ifdef bookmark_qml_EXPORTS - /* We are building this library */ -# define BOOKMARK_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define BOOKMARK_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef BOOKMARK_QML_NO_EXPORT -# define BOOKMARK_QML_NO_EXPORT -# endif -#endif - -#ifndef BOOKMARK_QML_DEPRECATED -# define BOOKMARK_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef BOOKMARK_QML_DEPRECATED_EXPORT -# define BOOKMARK_QML_DEPRECATED_EXPORT BOOKMARK_QML_EXPORT BOOKMARK_QML_DEPRECATED -#endif - -#ifndef BOOKMARK_QML_DEPRECATED_NO_EXPORT -# define BOOKMARK_QML_DEPRECATED_NO_EXPORT BOOKMARK_QML_NO_EXPORT BOOKMARK_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef BOOKMARK_QML_NO_DEPRECATED -# define BOOKMARK_QML_NO_DEPRECATED -# endif -#endif - -#endif /* BOOKMARK_QML_EXPORT_H */ diff --git a/src/ui/qml/conform/src/CMakeLists.txt b/src/ui/qml/conform/src/CMakeLists.txt new file mode 100644 index 000000000..115e3748a --- /dev/null +++ b/src/ui/qml/conform/src/CMakeLists.txt @@ -0,0 +1,14 @@ +SET(LINK_DEPS + CAF::core + Qt6::Core + xstudio::ui::qml::helper + xstudio::ui::qml::session + xstudio::conform + xstudio::utility +) + +SET(EXTRAMOC + "${ROOT_DIR}/include/xstudio/ui/qml/conform_ui.hpp" +) + +create_qml_component(conform ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/conform/src/conform_ui.cpp b/src/ui/qml/conform/src/conform_ui.cpp new file mode 100644 index 000000000..de8c46da1 --- /dev/null +++ b/src/ui/qml/conform/src/conform_ui.cpp @@ -0,0 +1,1122 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "xstudio/conform/conformer.hpp" +#include "xstudio/ui/qml/conform_ui.hpp" +#include "xstudio/ui/qml/json_tree_model_ui.hpp" +#include "xstudio/ui/qml/session_model_ui.hpp" +#include "xstudio/timeline/track_actor.hpp" + +CAF_PUSH_WARNINGS +CAF_POP_WARNINGS + +using namespace caf; +using namespace xstudio; +using namespace xstudio::utility; +using namespace xstudio::ui::qml; + +ConformEngineUI::ConformEngineUI(QObject *parent) : super(parent) { + init(CafSystemObject::get_actor_system()); + + setRoleNames(std::vector({"nameRole"})); +} + + +void ConformEngineUI::init(caf::actor_system &_system) { + super::init(_system); + + // join conform engine events. + try { + scoped_actor sys{system()}; + utility::print_on_create(as_actor(), "ConformEngineUI"); + auto conform_manager = system().registry().template get(conform_registry); + + try { + auto uuids = + request_receive(*sys, conform_manager, json_store::sync_atom_v); + conform_uuid_ = uuids[0]; + + // get system presets + auto data = request_receive( + *sys, conform_manager, json_store::sync_atom_v, conform_uuid_); + setModelData(data); + + // join events. + if (conform_events_) { + try { + request_receive( + *sys, conform_events_, broadcast::leave_broadcast_atom_v, as_actor()); + } catch (const std::exception &) { + } + conform_events_ = caf::actor(); + } + try { + conform_events_ = + request_receive(*sys, conform_manager, get_event_group_atom_v); + request_receive( + *sys, conform_events_, broadcast::join_broadcast_atom_v, as_actor()); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + // setModelData(tree); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + set_message_handler([=](caf::actor_companion * /*self*/) -> caf::message_handler { + return { + [=](utility::event_atom, + conform::conform_tasks_atom, + const std::vector &) {}, + + [=](utility::event_atom, + json_store::sync_atom, + const Uuid &uuid, + const JsonStore &event) { + try { + if (uuid == conform_uuid_) + receiveEvent(event); + + // spdlog::warn("{}", modelData().dump(2)); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + }, + [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + [=](caf::message) {}}; + }); +} + + +QVariant ConformEngineUI::data(const QModelIndex &index, int role) const { + auto result = QVariant(); + + try { + const auto &j = indexToData(index); + + switch (role) { + case Roles::nameRole: + case Qt::DisplayRole: + try { + if (j.count("name")) + result = QString::fromStdString(j.at("name")); + } catch (...) { + } + break; + + default: + result = JSONTreeModel::data(index, role); + break; + } + } catch (const std::exception &err) { + spdlog::warn("{} {} {} {}", __PRETTY_FUNCTION__, err.what(), role, index.row()); + } + + return result; +} + +QFuture ConformEngineUI::conformPrepareSequenceFuture( + const QModelIndex &sequenceIndex, const bool onlyCreateConfrom) const { + auto future = QFuture(); + + try { + if (not sequenceIndex.isValid()) + throw std::runtime_error("Invalid Sequence"); + if (sequenceIndex.data(SessionModel::Roles::typeRole).toString() != QString("Timeline")) + throw std::runtime_error("Invalid Sequence type"); + + auto sequence_actor = actorFromString( + system(), + StdFromQString(sequenceIndex.data(SessionModel::Roles::actorRole).toString())); + auto sequence_uuid = + UuidFromQUuid(sequenceIndex.data(SessionModel::Roles::actorUuidRole).toUuid()); + + if (not sequence_actor) + throw std::runtime_error("Invalid sequence actor"); + + future = QtConcurrent::run([=]() { + auto result = false; + + // populate data into conform request.. ? + // or should conformer do the heavy lifting.. + auto conform_manager = + system().registry().template get(conform_registry); + scoped_actor sys{system()}; + + // always inserts next to old items ? + // how would this work with timelines ? + try { + result = request_receive( + *sys, + conform_manager, + conform::conform_atom_v, + UuidActor(sequence_uuid, sequence_actor), + onlyCreateConfrom); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; + }); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + future = QtConcurrent::run([=]() { + auto result = false; + return result; + }); + } + + return future; +} + +QFuture> ConformEngineUI::conformItemsFuture( + const QString &qtask, + const QModelIndex &container, + const QModelIndex &item, + const bool fanOut, + const bool removeSource) const { + const auto task = StdFromQString(qtask); + + // get container detail + QModelIndexList items; + auto nofity_item = false; + + // maybe media or clip, or track + auto item_type = StdFromQString(item.data(SessionModel::Roles::typeRole).toString()); + auto smodel = + qobject_cast(const_cast(container.model())); + + if (item_type == "Media") { + items.push_back(item); + } else if ( + item_type == "Clip" and not item.data(SessionModel::Roles::lockedRole).toBool() and + not item.parent().data(SessionModel::Roles::lockedRole).toBool()) { + items.push_back(item); + } else if ( + (item_type == "Video Track" or item_type == "Audio Track") and + not item.data(SessionModel::Roles::lockedRole).toBool()) { + nofity_item = true; + + item_type = "Clip"; + // iterate of track clips, adding those that are not locked. + for (int i = 0; i < item.model()->rowCount(item); i++) { + auto ind = item.model()->index(i, 0, item); + + if (ind.isValid() and + ind.data(SessionModel::Roles::typeRole).toString() == "Clip" and + not ind.data(SessionModel::Roles::lockedRole).toBool()) { + items.push_back(ind); + } + } + } + + auto jmodel = qobject_cast(smodel); + + auto cactor = actorFromString( + system(), StdFromQString(container.data(SessionModel::Roles::actorRole).toString())); + auto cuuid = UuidFromQUuid(container.data(SessionModel::Roles::actorUuidRole).toUuid()); + + auto playlist_index = smodel->getPlaylistIndex(container); + + auto pactor = actorFromString( + system(), + StdFromQString(playlist_index.data(SessionModel::Roles::actorRole).toString())); + auto puuid = + UuidFromQUuid(playlist_index.data(SessionModel::Roles::actorUuidRole).toUuid()); + + auto item_uav = UuidActorVector(); + auto before_uv = UuidVector(); + + for (const auto &i : items) { + auto media_index = QModelIndex(); + + auto iactor = actorFromString( + system(), StdFromQString(i.data(SessionModel::Roles::actorRole).toString())); + + auto iuuid = Uuid(); + if (item_type == "Media") { + media_index = i; + iuuid = UuidFromQUuid(i.data(SessionModel::Roles::actorUuidRole).toUuid()); + } else if (item_type == "Clip") { + // get media actor from clip.. + // there MAY NOT BE ONE.. + // get meda uuid.. + iuuid = UuidFromQUuid(i.data(JSONTreeModel::Roles::idRole).toUuid()); + auto muuid = UuidFromQUuid(i.data(SessionModel::Roles::clipMediaUuidRole).toUuid()); + // find media actor + if (not muuid.is_null()) { + // find in playlist + auto mlist = container.model()->index(0, 0, container); + if (mlist.isValid()) { + media_index = jmodel->searchRecursive( + item.data(SessionModel::Roles::clipMediaUuidRole), + SessionModel::Roles::actorUuidRole, + mlist, + 0, + 0); + } + } + } + // get uuid of next media.. + auto buuid = Uuid(); + if (media_index.isValid()) { + auto next_index = + media_index.model()->sibling(media_index.row() + 1, 0, media_index); + if (next_index.isValid()) + buuid = + UuidFromQUuid(next_index.data(SessionModel::Roles::actorUuidRole).toUuid()); + } + + if (not iuuid.is_null()) { + item_uav.emplace_back(UuidActor(iuuid, iactor)); + before_uv.push_back(buuid); + } + } + + // spdlog::warn( + // "ConformEngineUI::conformItemsFuture task {} p {} {} c {} {} {} {} {} {}", + // task, + // to_string(puuid), + // to_string(pactor), + // to_string(cuuid), + // to_string(cactor), + // item_type, + // to_string(iuuid), + // to_string(iactor), + // remove); + + if (item_uav.size() > 1 and fanOut) { + return QtConcurrent::run([=]() { + auto result = QList(); + + auto smodel = qobject_cast( + const_cast(container.model())); + + auto pending = std::vector>>(); + + QUuid notify_uuid; + + if (nofity_item) + notify_uuid = smodel->progressRangeNotification( + item, "Conforming Track {}", 0, item_uav.size()); + + for (size_t i = 0; i < item_uav.size(); i++) { + pending.emplace_back(conformItemsFuture( + task, + utility::UuidActorVector({item_uav.at(i)}), + UuidActor(puuid, pactor), + UuidActor(cuuid, cactor), + item_type, + utility::UuidVector({before_uv.at(i)}), + removeSource)); + } + + if (nofity_item) { + std::vector is_done(item_uav.size()); + auto done = 0; + while (done != item_uav.size()) { + for (size_t i = 0; i < is_done.size(); i++) { + if (not is_done[i] and pending[i].isResultReadyAt(0)) { + is_done[i] = true; + done++; + smodel->updateProgressNotification(item, notify_uuid, done); + } + } + // sleep.. + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + } + } + + for (auto &i : pending) { + auto r = i.result(); + result.append(r); + } + + if (nofity_item) + smodel->removeNotification(item, notify_uuid); + + return result; + }); + } + + QUuid notify_uuid; + + if (nofity_item) + notify_uuid = smodel->processingNotification(item, "Conforming Track"); + + return conformItemsFuture( + task, + item_uav, + UuidActor(puuid, pactor), + UuidActor(cuuid, cactor), + item_type, + before_uv, + removeSource, + QPersistentModelIndex(item), + notify_uuid); +} + + +QFuture> ConformEngineUI::conformToSequenceFuture( + const QModelIndex &_playlistIndex, + const QModelIndexList &_mediaIndexes, + const QModelIndex &sequenceIndex, + const QModelIndex &conformTrackIndex, + const bool replace, + const QString &newTrackName) const { + + auto playlistIndex = _playlistIndex; + auto mediaIndexes = _mediaIndexes; + + auto future = QFuture>(); + + try { + if (mediaIndexes.empty()) + throw std::runtime_error("Empty media list"); + + if (not playlistIndex.isValid()) + throw std::runtime_error("Invalid Playlist"); + if (playlistIndex.data(SessionModel::Roles::typeRole).toString() != QString("Playlist")) + throw std::runtime_error("Invalid Playlist type"); + + + if (not sequenceIndex.isValid()) + throw std::runtime_error("Invalid Sequence"); + if (sequenceIndex.data(SessionModel::Roles::typeRole).toString() != QString("Timeline")) + throw std::runtime_error("Invalid Sequence type"); + + + auto conformTrackUuidActor = UuidActor(); + + if (conformTrackIndex.isValid()) { + auto ctuuid = + UuidFromQUuid(conformTrackIndex.data(JSONTreeModel::Roles::idRole).toUuid()); + auto ctactor = actorFromString( + system(), + StdFromQString( + conformTrackIndex.data(SessionModel::Roles::actorRole).toString())); + + if (ctactor and not ctuuid.is_null()) + conformTrackUuidActor = UuidActor(ctuuid, ctactor); + } + + // safe ?? + auto sessionModel = qobject_cast( + const_cast(playlistIndex.model())); + auto sequencePlaylistIndex = sessionModel->getPlaylistIndex(sequenceIndex); + + if (sequencePlaylistIndex != playlistIndex) { + playlistIndex = sequencePlaylistIndex; + mediaIndexes = sessionModel->copyRows( + mediaIndexes, + sessionModel->rowCount(sessionModel->index(0, 0, playlistIndex)), + playlistIndex); + } + + auto playlist_actor = actorFromString( + system(), + StdFromQString(playlistIndex.data(SessionModel::Roles::actorRole).toString())); + auto playlist_uuid = + UuidFromQUuid(playlistIndex.data(SessionModel::Roles::actorUuidRole).toUuid()); + + auto media_list = utility::UuidActorVector(); + for (const auto &i : mediaIndexes) { + auto media_actor = actorFromString( + system(), StdFromQString(i.data(SessionModel::Roles::actorRole).toString())); + auto media_uuid = + UuidFromQUuid(i.data(SessionModel::Roles::actorUuidRole).toUuid()); + if (media_actor) + media_list.emplace_back(UuidActor(media_uuid, media_actor)); + } + + if (not playlist_actor) + throw std::runtime_error("Invalid playlist actor"); + + auto sequence_actor = actorFromString( + system(), + StdFromQString(sequenceIndex.data(SessionModel::Roles::actorRole).toString())); + auto sequence_uuid = + UuidFromQUuid(sequenceIndex.data(SessionModel::Roles::actorUuidRole).toUuid()); + + if (not sequence_actor) + throw std::runtime_error("Invalid sequence actor"); + + future = QtConcurrent::run([=]() { + auto result = QList(); + + // populate data into conform request.. ? + // or should conformer do the heavy lifting.. + auto conform_manager = + system().registry().template get(conform_registry); + scoped_actor sys{system()}; + + // always inserts next to old items ? + // how would this work with timelines ? + try { + auto operations = JsonStore(conform::ConformOperationsJSON); + operations["create_media"] = false; + operations["remove_media"] = false; + operations["insert_media"] = true; + operations["replace_clip"] = replace; + operations["new_track_name"] = StdFromQString(newTrackName); + operations["remove_failed_clip"] = true; + + auto reply = request_receive( + *sys, + conform_manager, + conform::conform_atom_v, + operations, + UuidActor(playlist_uuid, playlist_actor), + UuidActor(sequence_uuid, sequence_actor), + conformTrackUuidActor, + media_list); + + for (const auto &i : reply.items_) { + if (i) { + for (const auto &j : *i) + result.push_back(QUuidFromUuid(std::get<0>(j))); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; + }); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + future = QtConcurrent::run([=]() { + auto result = QList(); + return result; + }); + } + + return future; +} + + +QFuture> ConformEngineUI::conformItemsFuture( + const std::string &task, + const utility::UuidActorVector &items, + const utility::UuidActor &playlist, + const utility::UuidActor &container, + const std::string &item_type, + const utility::UuidVector &before, + const bool removeSource, + const QPersistentModelIndex ¬ifyIndex, + const QUuid ¬ifyUuid) const { + + return QtConcurrent::run([=]() { + auto result = QList(); + + if (items.empty()) + return result; + + // populate data into conform request.. ? + // or should conformer do the heavy lifting.. + auto conform_manager = system().registry().template get(conform_registry); + scoped_actor sys{system()}; + + // always inserts next to old items ? + // how would this work with timelines ? + try { + auto operations = JsonStore(conform::ConformOperationsJSON); + operations["create_media"] = true; + operations["insert_media"] = true; + operations["replace_clip"] = true; + operations["remove_media"] = removeSource; + operations["remove_failed_clip"] = removeSource; + + auto reply = request_receive( + *sys, + conform_manager, + conform::conform_atom_v, + task, + operations, + playlist, + container, + item_type, + items, + before); + + for (const auto &i : reply.items_) { + if (i) { + for (const auto &j : *i) { + result.push_back(QUuidFromUuid(std::get<0>(j))); + } + } + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + if (not notifyUuid.isNull() and notifyIndex.isValid()) { + auto smodel = qobject_cast( + const_cast(notifyIndex.model())); + smodel->removeNotification(notifyIndex, notifyUuid); + } + + return result; + }); +} + +QFuture> ConformEngineUI::conformToNewSequenceFuture( + const QModelIndexList &mediaIndexes, + const QString &qtask, + const int siblings, + const QModelIndex &playlistIndex) const { + + auto media = UuidActorVector(); + + auto src_playlist_index = QPersistentModelIndex(); + SessionModel *smodel = nullptr; + + // get media uuidactors + for (const auto &i : mediaIndexes) { + if (i.data(SessionModel::Roles::typeRole) != "Media") + continue; + + if (smodel == nullptr) { + smodel = qobject_cast(const_cast(i.model())); + + src_playlist_index = QPersistentModelIndex(smodel->getPlaylistIndex(i)); + } + + auto media_actor = actorFromString( + system(), StdFromQString(i.data(SessionModel::Roles::actorRole).toString())); + auto media_uuid = UuidFromQUuid(i.data(SessionModel::Roles::actorUuidRole).toUuid()); + + if (media_actor) { + media.emplace_back(UuidActor(media_uuid, media_actor)); + } + } + + // get target playlist + auto target_playlist = UuidActor(); + if (playlistIndex.isValid()) { + auto playlist_actor = actorFromString( + system(), + StdFromQString(playlistIndex.data(SessionModel::Roles::actorRole).toString())); + auto playlist_uuid = + UuidFromQUuid(playlistIndex.data(SessionModel::Roles::actorUuidRole).toUuid()); + + target_playlist = UuidActor(playlist_uuid, playlist_actor); + } + + auto nquuid = QUuid(); + if (smodel and not media.empty()) + nquuid = smodel->progressRangeNotification( + src_playlist_index, "Conforming Media", 0, media.size()); + + auto task = StdFromQString(qtask); + + return QtConcurrent::run([=]() { + auto result = QList(); + + if (not media.empty()) { + auto conform_manager = + system().registry().template get(conform_registry); + scoped_actor sys{system()}; + try { + + auto reply = request_receive>>>( + *sys, conform_manager, conform::conform_atom_v, media); + + auto seq_to_media = std::map>(); + auto seq_to_name = std::map(); + auto seq_to_meta = std::map(); + + auto count = 0; + auto media_done = 0; + + for (const auto &i : reply) { + if (i) { + const auto &[name, uri, meta] = *i; + if (not seq_to_media.count(uri)) + seq_to_media[uri] = std::vector(); + + seq_to_media[uri].push_back(media[count]); + seq_to_name[uri] = name; + seq_to_meta[uri] = meta; + } + // else + // spdlog::warn("NO RESULT"); + count++; + } + + // for(const auto &i: seq_to_name) { + // spdlog::warn("{} {}", to_string(i.first), i.second); + // } + + // for(const auto &i: seq_to_media) { + // spdlog::warn("{} {}", to_string(i.first), i.second.size()); + // } + + if (not seq_to_media.empty()) { + // find session actor + auto session = request_receive( + *sys, + system().registry().template get(studio_registry), + session::session_atom_v); + + + for (const auto &i : seq_to_media) { + auto playlist = target_playlist; + auto playlist_media = i.second; + + // we've got a path for a timeline and a list of media actors. + // spdlog::warn("{} {}", to_string(i.first), i.second.size()); + + // create new playlist + if (not playlist.actor()) { + playlist = request_receive>( + *sys, + session, + session::add_playlist_atom_v, + seq_to_name.at(i.first), + Uuid(), + false) + .second; + + // clone media in to new playlist + request_receive( + *sys, + session, + playlist::copy_media_atom_v, + playlist.uuid(), + vector_to_uuid_vector(playlist_media), + false, + Uuid(), + false); + // as playlist will only contain this media just get it all.. + playlist_media = request_receive( + *sys, playlist.actor(), playlist::get_media_atom_v); + } + + // import timeline into playlist + auto timeline = request_receive( + *sys, + playlist.actor(), + session::import_atom_v, + i.first, + Uuid(), + true); + + // push version meta into timeline + request_receive( + *sys, + timeline.actor(), + json_store::set_json_atom_v, + seq_to_meta.at(i.first)); + + // generate conform track. + request_receive( + *sys, conform_manager, conform::conform_atom_v, timeline, false); + + // if task is supplied add conformed track. + // we really need a hint to the media locations within this timeline.. + // that way we can remove clips we don't want in the conform track. + // to support the sibling option. + + if (not task.empty()) { + // duplicate nominated conform track + // always track 0 ? + try { + auto timeline_item = request_receive( + *sys, timeline.actor(), timeline::item_atom_v); + auto conform_track_uuid = timeline_item.prop().value( + "conform_track_uuid", utility::Uuid()); + + // iterate over tracks to find it. + for (int j = 0; + j < static_cast(timeline_item.front().size()); + j++) { + if ((*timeline_item.front().item_at_index(j))->uuid() == + conform_track_uuid) { + + auto orig_ctrack_item = + (*timeline_item.front().item_at_index(j)); + // manipulate ITEM, then create track ? + auto ctrack_item = orig_ctrack_item->clone(true); + + // set track name to match task + std::ignore = ctrack_item.set_name(task); + + // unlock track + std::ignore = ctrack_item.set_locked(false); + + // clear children flags. (as conform track will be + // green) + for (auto &citem : ctrack_item) { + if (citem.item_type() == timeline::IT_CLIP) + std::ignore = citem.set_flag(""); + } + + if (siblings != -1) { + // we need to prune the clips.. + // for this we need to know which media == which + // clip + auto operations = + JsonStore(conform::ConformOperationsJSON); + operations["match_only"] = true; + auto reply = request_receive( + *sys, + conform_manager, + conform::conform_atom_v, + operations, + playlist, + timeline, + UuidActor(), + playlist_media); + + + auto clip_uuids = std::set(); + for (const auto &c : reply.items_) { + // collect clip uuids.. + if (c) { + for (const auto &cc : (*c)) + clip_uuids.insert( + std::get<0>(cc).uuid()); + } + } + + // build set of clip indexes + auto reset_citems = std::set(); + for (auto &citem : ctrack_item) { + if (citem.item_type() == timeline::IT_CLIP) + reset_citems.insert(citem.uuid()); + } + + + for (int index = 0; + index < + static_cast(orig_ctrack_item->size()); + index++) { + const auto ocuuid = + (*(orig_ctrack_item->item_at_index(index))) + ->uuid(); + if (clip_uuids.count(ocuuid)) { + // add surrounding clips.. + for (auto add_index = index - siblings; + add_index <= index + siblings; + add_index++) { + if (add_index >= 0 and + add_index < static_cast( + orig_ctrack_item + ->size()) and + (*(ctrack_item.item_at_index( + add_index))) + ->item_type() == + timeline::IT_CLIP) { + reset_citems.erase( + (*(ctrack_item.item_at_index( + add_index))) + ->uuid()); + } + } + } + } + + auto reset_prop = + JsonStore(R"({"media_uuid": null})"_json); + reset_prop["media_uuid"] = Uuid(); + + for (auto &citem : ctrack_item) { + if (reset_citems.count(citem.uuid())) + std::ignore = citem.set_prop(reset_prop); + } + // clean track.. + ctrack_item.merge_gaps(true); + } + // spawn track from item.. + auto ctrack_actor = sys->spawn( + ctrack_item, ctrack_item); + auto dup = ctrack_item.uuid_actor(); + auto citems = UuidActorVector(); + + for (const auto &ditem : ctrack_item) { + if (ditem.item_type() == timeline::IT_CLIP) { + citems.emplace_back(ditem.uuid_actor()); + } + } + + // insert track to be conformed above + auto inserted = request_receive( + *sys, + timeline_item.front().actor(), + timeline::insert_item_atom_v, + j, + UuidActorVector({dup})); + + // conform clips in new track + conformItemsFuture( + task, + citems, + playlist, + timeline, + "Clip", + UuidVector(), + true) + .result(); + break; + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + + // conform media to timeline.. + auto operations = JsonStore(conform::ConformOperationsJSON); + operations["create_media"] = false; + operations["remove_media"] = false; + operations["insert_media"] = true; + operations["replace_clip"] = false; + operations["new_track_name"] = "Conformed Media"; + operations["remove_failed_clip"] = true; + + auto reply = request_receive( + *sys, + conform_manager, + conform::conform_atom_v, + operations, + playlist, + timeline, + UuidActor(), + playlist_media); + if (not nquuid.isNull()) { + media_done += playlist_media.size(); + smodel->updateProgressNotification( + src_playlist_index, nquuid, media_done); + } + } + } else { + if (not nquuid.isNull()) + smodel->warnNotification( + src_playlist_index, "No sequence found for media", 10, nquuid); + + throw std::runtime_error("No sequence found for media"); + } + + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + if (not nquuid.isNull()) + smodel->warnNotification(src_playlist_index, err.what(), 10, nquuid); + + throw; + } + } + + // return uuid of each timeline containing copied media ? + + if (not nquuid.isNull()) + smodel->infoNotification( + src_playlist_index, "Media Conformed to Sequences", 5, nquuid); + + return result; + }); +} + +QFuture> ConformEngineUI::conformTracksToSequenceFuture( + const QModelIndexList &trackIndexes, const QModelIndex &sequenceIndex) const { + + try { + if (not sequenceIndex.isValid()) + throw std::runtime_error("Invalid destination timeline"); + + auto smodel = qobject_cast( + const_cast(sequenceIndex.model())); + + auto stype = + StdFromQString(sequenceIndex.data(SessionModel::Roles::typeRole).toString()); + if (stype != "Timeline") + throw std::runtime_error("Invalid destination timeline " + stype); + + auto source_playlist = UuidActor(); + auto source_timeline = UuidActor(); + auto tracks = UuidActorVector(); + + // need to sort these.. + auto sortedTrackIndexes = + std::vector(trackIndexes.begin(), trackIndexes.end()); + std::sort( + sortedTrackIndexes.begin(), + sortedTrackIndexes.end(), + [](QModelIndex &a, QModelIndex &b) { return a.row() > b.row(); }); + + for (const auto &i : sortedTrackIndexes) { + if (i.isValid()) { + auto type = StdFromQString(i.data(SessionModel::Roles::typeRole).toString()); + if (type == "Video Track" or type == "Audio Track") { + auto source_track = UuidActor( + UuidFromQUuid(i.data(SessionModel::Roles::actorUuidRole).toUuid()), + actorFromString( + system(), + StdFromQString(i.data(SessionModel::Roles::actorRole).toString()))); + + if (not source_timeline.actor()) { + auto source_timeline_index = smodel->getTimelineIndex(i); + if (source_timeline_index.isValid()) { + auto source_playlist_index = + smodel->getPlaylistIndex(source_timeline_index); + source_timeline = UuidActor( + UuidFromQUuid(source_timeline_index + .data(SessionModel::Roles::actorUuidRole) + .toUuid()), + actorFromString( + system(), + StdFromQString(source_timeline_index + .data(SessionModel::Roles::actorRole) + .toString()))); + + source_playlist = UuidActor( + UuidFromQUuid(source_playlist_index + .data(SessionModel::Roles::actorUuidRole) + .toUuid()), + actorFromString( + system(), + StdFromQString(source_playlist_index + .data(SessionModel::Roles::actorRole) + .toString()))); + } + } + + tracks.emplace_back(source_track); + } else { + spdlog::warn("{} Invalid track type {}", __PRETTY_FUNCTION__, type); + } + } + } + + auto playlistIndex = smodel->getPlaylistIndex(sequenceIndex); + auto target_playlist = UuidActor( + UuidFromQUuid(playlistIndex.data(SessionModel::Roles::actorUuidRole).toUuid()), + actorFromString( + system(), + StdFromQString(playlistIndex.data(SessionModel::Roles::actorRole).toString()))); + + auto target_timeline = UuidActor( + UuidFromQUuid(sequenceIndex.data(SessionModel::Roles::actorUuidRole).toUuid()), + actorFromString( + system(), + StdFromQString(sequenceIndex.data(SessionModel::Roles::actorRole).toString()))); + + if (not target_timeline.actor()) + throw std::runtime_error("Invalid destination actor"); + + if (not source_timeline.actor()) + throw std::runtime_error("Invalid source actor"); + + if (not source_playlist.actor()) + throw std::runtime_error("Invalid source playlist"); + + if (tracks.empty()) + throw std::runtime_error("No source tracks"); + + return QtConcurrent::run([=]() { + auto result = QList(); + auto conform_manager = + system().registry().template get(conform_registry); + scoped_actor sys{system()}; + + try { + auto reply = request_receive( + *sys, + conform_manager, + conform::conform_atom_v, + source_playlist, + source_timeline, + tracks, + target_playlist, + target_timeline, + UuidActor()); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; + }); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return QtConcurrent::run([=]() { + auto result = QList(); + return result; + }); +} + +QFuture> ConformEngineUI::conformFindRelatedFuture( + const QString &key, const QModelIndex &clipIndex, const QModelIndex &sequenceIndex) const { + + try { + if (not clipIndex.isValid() or + clipIndex.data(SessionModel::Roles::typeRole).toString() != "Clip") + throw std::runtime_error("Invalid clip"); + if (not sequenceIndex.isValid() or + sequenceIndex.data(SessionModel::Roles::typeRole).toString() != "Timeline") + throw std::runtime_error("Invalid sequence"); + + return QtConcurrent::run([=]() { + auto result = QList(); + auto conform_manager = + system().registry().template get(conform_registry); + scoped_actor sys{system()}; + + auto clip_ua = UuidActor( + UuidFromQUuid(clipIndex.data(JSONTreeModel::Roles::idRole).toUuid()), + actorFromString( + system(), + StdFromQString(clipIndex.data(SessionModel::Roles::actorRole).toString()))); + + auto sequence_ua = UuidActor( + UuidFromQUuid(sequenceIndex.data(SessionModel::Roles::actorUuidRole).toUuid()), + actorFromString( + system(), + StdFromQString( + sequenceIndex.data(SessionModel::Roles::actorRole).toString()))); + + try { + auto reply = request_receive( + *sys, + conform_manager, + conform::conform_atom_v, + StdFromQString(key), + clip_ua, + sequence_ua); + + auto sessionModel = qobject_cast( + const_cast(sequenceIndex.model())); + + for (const auto &i : reply) + result.push_back(QUuidFromUuid(i.uuid())); + + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; + }); + + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return QtConcurrent::run([=]() { + auto result = QList(); + return result; + }); +} diff --git a/retired/contact_sheet/test/CMakeLists.txt b/src/ui/qml/conform/test/CMakeLists.txt similarity index 100% rename from retired/contact_sheet/test/CMakeLists.txt rename to src/ui/qml/conform/test/CMakeLists.txt diff --git a/src/ui/qml/embedded_python/src/CMakeLists.txt b/src/ui/qml/embedded_python/src/CMakeLists.txt index 3fd46c74c..6d1c74956 100644 --- a/src/ui/qml/embedded_python/src/CMakeLists.txt +++ b/src/ui/qml/embedded_python/src/CMakeLists.txt @@ -1,6 +1,6 @@ SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core + CAF::core + Qt6::Core xstudio::ui::qml::helper xstudio::utility xstudio::global_store @@ -9,4 +9,4 @@ SET(LINK_DEPS SET(EXTRAMOC ) -create_qml_component(embedded_python 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") +create_qml_component(embedded_python ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/embedded_python/src/embedded_python_ui.cpp b/src/ui/qml/embedded_python/src/embedded_python_ui.cpp index 54a8a54e2..90e5c5cf3 100644 --- a/src/ui/qml/embedded_python/src/embedded_python_ui.cpp +++ b/src/ui/qml/embedded_python/src/embedded_python_ui.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include "xstudio/atoms.hpp" #include "xstudio/ui/qml/embedded_python_ui.hpp" @@ -16,82 +17,65 @@ using namespace xstudio::utility; using namespace xstudio::ui::qml; -SnippetUI::SnippetUI(const utility::JsonStore &json, QObject *parent) : QObject(parent) { - auto js = json; - name_ = QStringFromStd(js.value("name", "Untitled")); - menu_name_ = QStringFromStd(js.value("menu", "Untitled")); - script_ = QStringFromStd(js.value("script", "print(\"hello\")")); - description_ = QStringFromStd(js.value("name", "No description.")); +EmbeddedPythonUI::EmbeddedPythonUI(QObject *parent) : super(parent) { + init(CafSystemObject::get_actor_system()); + + setRoleNames(std::vector({ + "nameRole", + "menuPathRole", + "scriptPathRole", + "snippetTypeRole", + "typeRole", + })); } -EmbeddedPythonUI::EmbeddedPythonUI(QObject *parent) - : QMLActor(parent), backend_(), backend_events_(), event_uuid_(utility::Uuid::generate()) { - init(CafSystemObject::get_actor_system()); + +bool SnippetFilterModel::filterAcceptsRow( + int source_row, const QModelIndex &source_parent) const { + auto result = true; + auto source_index = sourceModel()->index(source_row, 0, source_parent); + + if (not snippet_type_.isEmpty() and + source_index.data(EmbeddedPythonUI::Roles::snippetTypeRole).toString() != snippet_type_) + result = false; + + return result; } void EmbeddedPythonUI::set_backend(caf::actor backend) { spdlog::debug("EmbeddedPythonUI set_backend"); scoped_actor sys{system()}; - if (backend_events_) { - try { - request_receive( - *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch ([[maybe_unused]] const std::exception &e) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - backend_events_ = caf::actor(); - } - - backend_ = backend; - - try { - backend_events_ = request_receive(*sys, backend_, get_event_group_atom_v); - request_receive( - *sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - // capture snippets.. try { - // we're doing this a bit oddly... - // as prefs isn't the place for this.. - // should have a dir for it ? - - auto prefs = global_store::GlobalStoreHelper(system()); - JsonStore j; - prefs.get_group(j); - - auto paths = global_store::preference_value( - j, "/ui/qml/reskin_windows_and_panels_model"); - // process app/user.. - - JsonStore snippets; - - if (check_create_path(xstudio_root("/snippets"))) { - snippets.merge(merge_json_from_path(xstudio_root("/snippets"))); - } - - for (const auto &i : paths) { + backend_ = backend; + // join events. + if (backend_events_) { try { - snippets.merge(merge_json_from_path(i)); - } catch (...) { + request_receive( + *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); + } catch ([[maybe_unused]] const std::exception &) { } + backend_events_ = caf::actor(); } - - if (check_create_path(snippets_path())) { - snippets.merge(merge_json_from_path(snippets_path())); + try { + backend_events_ = + request_receive(*sys, backend_, get_event_group_atom_v); + request_receive( + *sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } - // for each path scan for snippet files, build into snippet dict. - for (const auto &i : snippets.items()) { - addSnippet(new SnippetUI(i.value(), this)); - } - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } + auto uuids = request_receive(*sys, backend_, json_store::sync_atom_v); + snippet_uuid_ = uuids[0]; + // get system presets + auto data = + request_receive(*sys, backend_, json_store::sync_atom_v, snippet_uuid_); + setModelData(data); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } emit backendChanged(); } @@ -103,6 +87,7 @@ QUuid EmbeddedPythonUI::createSession() { auto uuid = request_receive( *sys, backend_, embedded_python::python_create_session_atom_v, true); event_uuid_ = uuid; + emit sessionIdChanged(); return QUuidFromUuid(uuid); } catch (const std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); @@ -115,17 +100,13 @@ QUuid EmbeddedPythonUI::createSession() { bool EmbeddedPythonUI::sendInput(const QString &str) { if (backend_) { - scoped_actor sys{system()}; // spdlog::warn("pyEval {0}", str.toStdString()); try { waiting_ = true; emit waitingChanged(); std::string input_string = StdFromQString(str); - sys->anon_send( - backend_, - embedded_python::python_session_input_atom_v, - event_uuid_, - input_string); + anon_mail(embedded_python::python_session_input_atom_v, event_uuid_, input_string) + .send(backend_); return true; } catch (const std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); @@ -150,23 +131,52 @@ bool EmbeddedPythonUI::sendInterrupt() { return false; } -void EmbeddedPythonUI::pyExec(const QString &str) { +void EmbeddedPythonUI::pyExec(const QString &str) const { + if (backend_) { + // spdlog::warn("pyExec {0}", str.toStdString()); + anon_mail(embedded_python::python_exec_atom_v, str.toStdString(), event_uuid_) + .send(backend_); + } else { + spdlog::warn("No python backend"); + } +} + +void EmbeddedPythonUI::reloadSnippets() const { + if (backend_) { + // spdlog::warn("pyExec {0}", str.toStdString()); + anon_mail(media::rescan_atom_v).send(backend_); + } else { + spdlog::warn("No python backend"); + } +} + +bool EmbeddedPythonUI::saveSnippet(const QUrl &path, const QString &content) const { + auto result = false; if (backend_) { scoped_actor sys{system()}; // spdlog::warn("pyExec {0}", str.toStdString()); - sys->anon_send( - backend_, embedded_python::python_exec_atom_v, str.toStdString(), event_uuid_); + try { + result = request_receive( + *sys, + backend_, + global_store::save_atom_v, + UriFromQUrl(path), + StdFromQString(content)); + } catch (const std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } } else { spdlog::warn("No python backend"); } + + return result; } void EmbeddedPythonUI::pyEvalFile(const QUrl &path) { if (backend_) { - scoped_actor sys{system()}; auto uri = UriFromQUrl(path); // spdlog::warn("pyExec {0}", to_string(uri)); - sys->anon_send(backend_, embedded_python::python_eval_file_atom_v, uri, event_uuid_); + anon_mail(embedded_python::python_eval_file_atom_v, uri, event_uuid_).send(backend_); } else { spdlog::warn("No python backend"); } @@ -190,7 +200,7 @@ QVariant EmbeddedPythonUI::pyEval(const QString &str) { void EmbeddedPythonUI::init(actor_system &system_) { - QMLActor::init(system_); + super::init(system_); spdlog::debug("EmbeddedPythonUI init"); @@ -203,18 +213,22 @@ void EmbeddedPythonUI::init(actor_system &system_) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } - // self()->set_down_handler([=](down_msg& msg) { - // if(msg.source == store) - // unsubscribe(); - // }); - set_message_handler([=](actor_companion * /*self_*/) -> message_handler { return { [=](utility::event_atom, utility::last_changed_atom, const time_point &) {}, [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) { - // if(msg.source == store_events) - // unsubscribe(); + [=](utility::event_atom, + json_store::sync_atom, + const Uuid &uuid, + const JsonStore &event) { + try { + if (uuid == snippet_uuid_) + receiveEvent(event); + + // spdlog::warn("{}", modelData().dump(2)); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } }, [=](utility::event_atom, @@ -224,7 +238,6 @@ void EmbeddedPythonUI::init(actor_system &system_) { if (uuid == event_uuid_) { auto out = std::get<0>(output); auto err = std::get<1>(output); - std::cerr << out << err; if (not out.empty()) { emit stdoutEvent(QStringFromStd(out)); } @@ -242,24 +255,52 @@ void EmbeddedPythonUI::init(actor_system &system_) { }); } +QVariant EmbeddedPythonUI::data(const QModelIndex &index, int role) const { + auto result = QVariant(); + + try { + const auto &j = indexToData(index); + + switch (role) { + case Roles::nameRole: + case Qt::DisplayRole: + if (j.count("name")) + result = QString::fromStdString(j.at("name")); + break; + + case Roles::typeRole: + if (j.count("type")) + result = QString::fromStdString(j.at("type")); + break; + + case Roles::snippetTypeRole: + if (j.count("snippet_type")) + result = QString::fromStdString(j.at("snippet_type")); + break; + + case Roles::menuPathRole: + if (j.count("menu_path")) + result = QString::fromStdString(j.at("menu_path")); + break; -void EmbeddedPythonUI::addSnippet(SnippetUI *snippet) { - // check for existing menu. - auto menu_name = snippet->menuModelName(); - SnippetMenuUI *menu = nullptr; + case Roles::scriptPathRole: + try { + if (j.count("script_path")) { + auto uri = caf::make_uri(j.at("script_path").get()); + if (uri) + result = QVariant::fromValue(QUrlFromUri(*uri)); + } + } catch (...) { + } + break; - for (auto &i : snippet_menus_) { - if (dynamic_cast(i)->name() == menu_name) { - menu = dynamic_cast(i); + default: + result = JSONTreeModel::data(index, role); break; } + } catch (const std::exception &err) { + spdlog::warn("{} {} {} {}", __PRETTY_FUNCTION__, err.what(), role, index.row()); } - if (menu == nullptr) { - // add menu.. - menu = new SnippetMenuUI(menu_name, this); - snippet_menus_.push_back(menu); - emit snippetMenusChanged(); - } - menu->addSnippet(snippet); + return result; } diff --git a/src/ui/qml/embedded_python/src/export.h b/src/ui/qml/embedded_python/src/export.h deleted file mode 100644 index 900fd2778..000000000 --- a/src/ui/qml/embedded_python/src/export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef EMBEDDED_PYTHON_QML_EXPORT_H -#define EMBEDDED_PYTHON_QML_EXPORT_H - -#ifdef EMBEDDED_PYTHON_QML_STATIC_DEFINE -# define EMBEDDED_PYTHON_QML_EXPORT -# define EMBEDDED_PYTHON_QML_NO_EXPORT -#else -# ifndef EMBEDDED_PYTHON_QML_EXPORT -# ifdef embedded_python_qml_EXPORTS - /* We are building this library */ -# define EMBEDDED_PYTHON_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define EMBEDDED_PYTHON_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef EMBEDDED_PYTHON_QML_NO_EXPORT -# define EMBEDDED_PYTHON_QML_NO_EXPORT -# endif -#endif - -#ifndef EMBEDDED_PYTHON_QML_DEPRECATED -# define EMBEDDED_PYTHON_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef EMBEDDED_PYTHON_QML_DEPRECATED_EXPORT -# define EMBEDDED_PYTHON_QML_DEPRECATED_EXPORT EMBEDDED_PYTHON_QML_EXPORT EMBEDDED_PYTHON_QML_DEPRECATED -#endif - -#ifndef EMBEDDED_PYTHON_QML_DEPRECATED_NO_EXPORT -# define EMBEDDED_PYTHON_QML_DEPRECATED_NO_EXPORT EMBEDDED_PYTHON_QML_NO_EXPORT EMBEDDED_PYTHON_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef EMBEDDED_PYTHON_QML_NO_DEPRECATED -# define EMBEDDED_PYTHON_QML_NO_DEPRECATED -# endif -#endif - -#endif /* EMBEDDED_PYTHON_QML_EXPORT_H */ diff --git a/src/ui/qml/embedded_python/src/include/embedded_python_qml_export.h b/src/ui/qml/embedded_python/src/include/embedded_python_qml_export.h deleted file mode 100644 index 900fd2778..000000000 --- a/src/ui/qml/embedded_python/src/include/embedded_python_qml_export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef EMBEDDED_PYTHON_QML_EXPORT_H -#define EMBEDDED_PYTHON_QML_EXPORT_H - -#ifdef EMBEDDED_PYTHON_QML_STATIC_DEFINE -# define EMBEDDED_PYTHON_QML_EXPORT -# define EMBEDDED_PYTHON_QML_NO_EXPORT -#else -# ifndef EMBEDDED_PYTHON_QML_EXPORT -# ifdef embedded_python_qml_EXPORTS - /* We are building this library */ -# define EMBEDDED_PYTHON_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define EMBEDDED_PYTHON_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef EMBEDDED_PYTHON_QML_NO_EXPORT -# define EMBEDDED_PYTHON_QML_NO_EXPORT -# endif -#endif - -#ifndef EMBEDDED_PYTHON_QML_DEPRECATED -# define EMBEDDED_PYTHON_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef EMBEDDED_PYTHON_QML_DEPRECATED_EXPORT -# define EMBEDDED_PYTHON_QML_DEPRECATED_EXPORT EMBEDDED_PYTHON_QML_EXPORT EMBEDDED_PYTHON_QML_DEPRECATED -#endif - -#ifndef EMBEDDED_PYTHON_QML_DEPRECATED_NO_EXPORT -# define EMBEDDED_PYTHON_QML_DEPRECATED_NO_EXPORT EMBEDDED_PYTHON_QML_NO_EXPORT EMBEDDED_PYTHON_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef EMBEDDED_PYTHON_QML_NO_DEPRECATED -# define EMBEDDED_PYTHON_QML_NO_DEPRECATED -# endif -#endif - -#endif /* EMBEDDED_PYTHON_QML_EXPORT_H */ diff --git a/src/ui/qml/event/src/CMakeLists.txt b/src/ui/qml/event/src/CMakeLists.txt deleted file mode 100644 index 49d605062..000000000 --- a/src/ui/qml/event/src/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core - xstudio::ui::qml::helper -) - -SET(EXTRAMOC -) - -create_qml_component(event 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/event/src/event_ui.cpp b/src/ui/qml/event/src/event_ui.cpp deleted file mode 100644 index 1d5e77649..000000000 --- a/src/ui/qml/event/src/event_ui.cpp +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/ui/qml/event_ui.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" - -#include -#include - -using namespace caf; -using namespace xstudio; -using namespace xstudio::event; -using namespace xstudio::ui::qml; - -EventUI::EventUI(const event::Event &event, QObject *parent) : QObject(parent) { - update(event); -} - -void EventUI::update(const event::Event &e) { - auto tmp = event_; - event_ = e; - - if (tmp.progress() != event_.progress()) - emit progressChanged(); - if (tmp.progress_minimum() != event_.progress_minimum()) - emit progressMinimumChanged(); - if (tmp.progress_maximum() != event_.progress_maximum()) - emit progressMaximumChanged(); - if (tmp.progress_percentage() != event_.progress_percentage()) - emit progressPercentageChanged(); - if (tmp.progress_text() != event_.progress_text()) - emit textChanged(); - if (tmp.progress_text_range() != event_.progress_text_range()) - emit textRangeChanged(); - if (tmp.progress_text_percentage() != event_.progress_text_percentage()) - emit textPercentageChanged(); - if (tmp.event() != event_.event()) - emit eventChanged(); - if (tmp.complete() != event_.complete()) - emit completeChanged(); -} - - -EventAttrs::EventAttrs(QObject *parent) : QQmlPropertyMap(this, parent) { - new EventManagerUI(this); -} - -void EventAttrs::addEvent(const event::Event &e) { - if (e.targets().empty()) { - auto uuid = QUuid().toString(); - if (not contains(uuid) or value(uuid).isNull()) - insert(uuid, QVariant::fromValue(new EventUI(e, this))); - else { - qobject_cast(qvariant_cast(value(uuid)))->update(e); - } - } else { - for (const auto &i : e.targets()) { - auto uuid = QUuidFromUuid(i).toString(); - - if (not contains(uuid) or value(uuid).isNull()) { - auto o = new EventUI(e, this); - insert(uuid, QVariant::fromValue(o)); - } else { - qobject_cast(qvariant_cast(value(uuid)))->update(e); - } - } - } -} - -EventManagerUI::EventManagerUI(EventAttrs *attrs_map) - : QMLActor(static_cast(attrs_map)), attrs_map_(attrs_map) { - - init(CafSystemObject::get_actor_system()); -} - -void EventManagerUI::init(caf::actor_system &system) { - - QMLActor::init(system); - - spdlog::debug("EventManagerUI init"); - - self()->set_default_handler(caf::drop); - - self()->join(system.groups().get_local(global_event_group)); - - set_message_handler([=](caf::actor_companion * /*self*/) -> caf::message_handler { - return { - [=](utility::event_atom, const Event &e) { attrs_map_->addEvent(e); }, - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg &g) { - caf::aout(self()) - << "EventManagerUI down: " << to_string(g.source) << std::endl; - }}; - }); -} diff --git a/src/ui/qml/event/src/export.h b/src/ui/qml/event/src/export.h deleted file mode 100644 index 3432c93fc..000000000 --- a/src/ui/qml/event/src/export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef EVENT_QML_EXPORT_H -#define EVENT_QML_EXPORT_H - -#ifdef EVENT_QML_STATIC_DEFINE -# define EVENT_QML_EXPORT -# define EVENT_QML_NO_EXPORT -#else -# ifndef EVENT_QML_EXPORT -# ifdef event_qml_EXPORTS - /* We are building this library */ -# define EVENT_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define EVENT_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef EVENT_QML_NO_EXPORT -# define EVENT_QML_NO_EXPORT -# endif -#endif - -#ifndef EVENT_QML_DEPRECATED -# define EVENT_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef EVENT_QML_DEPRECATED_EXPORT -# define EVENT_QML_DEPRECATED_EXPORT EVENT_QML_EXPORT EVENT_QML_DEPRECATED -#endif - -#ifndef EVENT_QML_DEPRECATED_NO_EXPORT -# define EVENT_QML_DEPRECATED_NO_EXPORT EVENT_QML_NO_EXPORT EVENT_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef EVENT_QML_NO_DEPRECATED -# define EVENT_QML_NO_DEPRECATED -# endif -#endif - -#endif /* EVENT_QML_EXPORT_H */ diff --git a/src/ui/qml/event/src/include/event_qml_export.h b/src/ui/qml/event/src/include/event_qml_export.h deleted file mode 100644 index 3432c93fc..000000000 --- a/src/ui/qml/event/src/include/event_qml_export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef EVENT_QML_EXPORT_H -#define EVENT_QML_EXPORT_H - -#ifdef EVENT_QML_STATIC_DEFINE -# define EVENT_QML_EXPORT -# define EVENT_QML_NO_EXPORT -#else -# ifndef EVENT_QML_EXPORT -# ifdef event_qml_EXPORTS - /* We are building this library */ -# define EVENT_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define EVENT_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef EVENT_QML_NO_EXPORT -# define EVENT_QML_NO_EXPORT -# endif -#endif - -#ifndef EVENT_QML_DEPRECATED -# define EVENT_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef EVENT_QML_DEPRECATED_EXPORT -# define EVENT_QML_DEPRECATED_EXPORT EVENT_QML_EXPORT EVENT_QML_DEPRECATED -#endif - -#ifndef EVENT_QML_DEPRECATED_NO_EXPORT -# define EVENT_QML_DEPRECATED_NO_EXPORT EVENT_QML_NO_EXPORT EVENT_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef EVENT_QML_NO_DEPRECATED -# define EVENT_QML_NO_DEPRECATED -# endif -#endif - -#endif /* EVENT_QML_EXPORT_H */ diff --git a/src/ui/qml/global_store/src/CMakeLists.txt b/src/ui/qml/global_store/src/CMakeLists.txt index 2ac09733d..125d1faed 100644 --- a/src/ui/qml/global_store/src/CMakeLists.txt +++ b/src/ui/qml/global_store/src/CMakeLists.txt @@ -1,6 +1,6 @@ SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core + CAF::core + Qt6::Core xstudio::global_store xstudio::ui::qml::helper ) @@ -9,4 +9,4 @@ SET(EXTRAMOC "${ROOT_DIR}/include/xstudio/ui/qml/global_store_model_ui.hpp" ) -create_qml_component(global_store 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") +create_qml_component(global_store ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/global_store/src/export.h b/src/ui/qml/global_store/src/export.h deleted file mode 100644 index a13120dfd..000000000 --- a/src/ui/qml/global_store/src/export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef GLOBAL_STORE_QML_EXPORT_H -#define GLOBAL_STORE_QML_EXPORT_H - -#ifdef GLOBAL_STORE_QML_STATIC_DEFINE -# define GLOBAL_STORE_QML_EXPORT -# define GLOBAL_STORE_QML_NO_EXPORT -#else -# ifndef GLOBAL_STORE_QML_EXPORT -# ifdef global_store_qml_EXPORTS - /* We are building this library */ -# define GLOBAL_STORE_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define GLOBAL_STORE_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef GLOBAL_STORE_QML_NO_EXPORT -# define GLOBAL_STORE_QML_NO_EXPORT -# endif -#endif - -#ifndef GLOBAL_STORE_QML_DEPRECATED -# define GLOBAL_STORE_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef GLOBAL_STORE_QML_DEPRECATED_EXPORT -# define GLOBAL_STORE_QML_DEPRECATED_EXPORT GLOBAL_STORE_QML_EXPORT GLOBAL_STORE_QML_DEPRECATED -#endif - -#ifndef GLOBAL_STORE_QML_DEPRECATED_NO_EXPORT -# define GLOBAL_STORE_QML_DEPRECATED_NO_EXPORT GLOBAL_STORE_QML_NO_EXPORT GLOBAL_STORE_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef GLOBAL_STORE_QML_NO_DEPRECATED -# define GLOBAL_STORE_QML_NO_DEPRECATED -# endif -#endif - -#endif /* GLOBAL_STORE_QML_EXPORT_H */ diff --git a/src/ui/qml/global_store/src/global_store_model_ui.cpp b/src/ui/qml/global_store/src/global_store_model_ui.cpp index a92d186eb..696c665a3 100644 --- a/src/ui/qml/global_store/src/global_store_model_ui.cpp +++ b/src/ui/qml/global_store/src/global_store_model_ui.cpp @@ -10,6 +10,16 @@ using namespace caf; using namespace xstudio; using namespace xstudio::ui::qml; +namespace { +QString capitaliseFirst(QString in) { + QStringList parts = in.split("_"); + for (int i = 0; i < parts.size(); ++i) { + parts[i] = parts[i].replace(0, 1, parts[i][0].toUpper()); + } + return parts.join(" "); +} +} // namespace + GlobalStoreModel::GlobalStoreModel(QObject *parent) : super(parent) { init(CafSystemObject::get_actor_system()); @@ -29,8 +39,6 @@ GlobalStoreModel::GlobalStoreModel(QObject *parent) : super(parent) { void GlobalStoreModel::init(caf::actor_system &_system) { super::init(_system); - self()->set_default_handler(caf::drop); - // join global store events. try { scoped_actor sys{system()}; @@ -75,8 +83,6 @@ void GlobalStoreModel::init(caf::actor_system &_system) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } - // self()->join(system.groups().get_local(global_event_group)); - set_message_handler([=](caf::actor_companion * /*self*/) -> caf::message_handler { return { [=](json_store::update_atom, @@ -107,18 +113,15 @@ void GlobalStoreModel::init(caf::actor_system &_system) { [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg &g) { - caf::aout(self()) - << "GlobalStoreModel down: " << to_string(g.source) << std::endl; - }}; + [=](caf::message) {}}; }); } void GlobalStoreModel::setAutosave(const bool enabled) { scoped_actor sys{system()}; autosave_ = enabled; - sys->send(gsh_->get_actor(), xstudio::global_store::autosave_atom_v, enabled); - gsh_->set_value(enabled, "/ui/qml/autosave"); + sys->mail(xstudio::global_store::autosave_atom_v, enabled).send(gsh_->get_actor()); + gsh_->set_value(enabled, "/core/global_store/autosave_enable"); emit autosaveChanged(); } @@ -140,13 +143,13 @@ bool GlobalStoreModel::updateProperty( const std::string &path, const utility::JsonStore &change) { auto result = false; auto index = - search_recursive(QVariant::fromValue(QStringFromStd(path)), QString("pathRole")); + searchRecursive(QVariant::fromValue(QStringFromStd(path)), QString("pathRole")); try { // single term update // should handle more that value... if (not index.isValid() and ends_with(path, "/value")) { - index = search_recursive( + index = searchRecursive( QVariant::fromValue(QStringFromStd(path.substr(0, path.find_last_of('/')))), QString("pathRole")); if (index.isValid()) { @@ -203,6 +206,7 @@ bool GlobalStoreModel::updateProperty( nlohmann::json GlobalStoreModel::storeToTree(const nlohmann::json &src) { auto result = R"([])"_json; + for (const auto &[k, v] : src.items()) { if (v.count("datatype")) { // spdlog::warn("{}", v.dump(2)); @@ -218,7 +222,6 @@ nlohmann::json GlobalStoreModel::storeToTree(const nlohmann::json &src) { return result; } - QVariant GlobalStoreModel::data(const QModelIndex &index, int role) const { auto result = QVariant(); @@ -324,6 +327,351 @@ bool GlobalStoreModel::setData(const QModelIndex &index, const QVariant &value, return result; } +PublicPreferencesModel::PublicPreferencesModel(QObject *parent) : super(parent) { + init(CafSystemObject::get_actor_system()); + + setRoleNames(std::vector( + {"nameRole", + "pathRole", + "datatypeRole", + "contextRole", + "valueRole", + "descriptionRole", + "defaultValueRole", + "overriddenValueRole", + "overriddenPathRole", + "displayNameRole", + "categoryRole", + "optionsRole"})); +} + + +void PublicPreferencesModel::init(caf::actor_system &_system) { + super::init(_system); + + // join global store events. + try { + scoped_actor sys{system()}; + utility::print_on_create(as_actor(), "PublicPreferencesModel"); + gsh_ = std::make_shared(system()); + + utility::JsonStore tmp; + auto gsh_group = gsh_->get_group(tmp); + + utility::request_receive( + *sys, gsh_group, broadcast::join_broadcast_atom_v, as_actor()); + + buildModel(tmp); + + // connect up auto save. + { + try { + auto grp = utility::request_receive( + *sys, gsh_->get_actor(), utility::get_event_group_atom_v); + utility::request_receive( + *sys, grp, broadcast::join_broadcast_atom_v, as_actor()); + } catch (const std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + try { + autosave_ = utility::request_receive( + *sys, gsh_->get_actor(), xstudio::global_store::autosave_atom_v); + emit autosaveChanged(); + } catch (const std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + } + + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + set_message_handler([=](caf::actor_companion * /*self*/) -> caf::message_handler { + return { + [=](json_store::update_atom, + const utility::JsonStore &change, + const std::string &path, + const utility::JsonStore &) { updateProperty(path, change); }, + + [=](json_store::update_atom, const utility::JsonStore &json) { buildModel(json); }, + + [=](utility::event_atom, global_store::autosave_atom, bool enabled) { + if (autosave_ != enabled) { + autosave_ = enabled; + emit autosaveChanged(); + } + }, + + [=](utility::event_atom, utility::last_changed_atom, const utility::time_point &) { + }, + [=](utility::event_atom, + json_store::update_atom, + const utility::JsonStore &, + const std::string &) {}, + [=](utility::event_atom, utility::change_atom) {}, + + + [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + // self()->set_default_handler(caf::drop); + [=](caf::message) {}}; + }); +} + +void PublicPreferencesModel::setAutosave(const bool enabled) { + scoped_actor sys{system()}; + autosave_ = enabled; + sys->mail(xstudio::global_store::autosave_atom_v, enabled).send(gsh_->get_actor()); + gsh_->set_value(enabled, "/core/global_store/autosave_enable"); + emit autosaveChanged(); +} + + +// "context": [ +// "QML_UI" +// ], +// "datatype": "string", +// "default_value": "Playlists", +// "description": "Default context.", +// "overridden_path": "/u/al/.config/DNEG/xstudio/preferences/qml_ui.json", +// "overridden_value": "Versions", +// "path": "/plugin/data_source/shotgun/context", +// "value": "Reference" +// } /plugin/data_source/shotgun/context + + +bool PublicPreferencesModel::updateProperty( + const std::string &path, const utility::JsonStore &change) { + auto result = false; + auto index = + searchRecursive(QVariant::fromValue(QStringFromStd(path)), QString("pathRole")); + + try { + // single term update + // should handle more that value... + if (not index.isValid() and ends_with(path, "/value")) { + index = searchRecursive( + QVariant::fromValue(QStringFromStd(path.substr(0, path.find_last_of('/')))), + QString("pathRole")); + if (index.isValid()) { + nlohmann::json &j = indexToData(index); + if (j["value"] != change) { + // spdlog::warn("json differ {} {}", j.dump(2), change.dump(2)); + QVector roles({Roles::valueRole}); + j["value"] = change; + result = true; + emit dataChanged(index, index, roles); + } + } + } else if (index.isValid()) { + nlohmann::json &j = indexToData(index); + // compare to current preset. + if (change != j) { + QVector roles({}); + result = true; + // spdlog::warn("json differ {} {}", j.dump(2), change.dump(2)); + + static const auto fields = std::map( + {{"default_value", Roles::defaultValueRole}, + {"value", Roles::valueRole}, + {"overridden_path", Roles::overriddenPathRole}, + {"overridden_value", Roles::overriddenValueRole}, + {"description", Roles::descriptionRole}, + {"context", Roles::contextRole}, + {"path", Roles::pathRole}, + {"datatype", Roles::datatypeRole}, + {"options", Roles::optionsRole}, + {"display_name", Roles::displayNameRole}, + {"category", Roles::categoryRole}}); + + for (const auto &i : fields) { + if (j.count(i.first) and j.at(i.first) != change.at(i.first)) { + j[i.first] = change.at(i.first); + roles.push_back(i.second); + } + } + + emit dataChanged(index, index, roles); + } + } else { + spdlog::warn( + "{} Unmatched update {} {}", __PRETTY_FUNCTION__, path, change.dump(2)); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + + return result; +} + +void PublicPreferencesModel::buildModel(const utility::JsonStore &entireGlobalStore) { + + // the idea here is we flatten the global store into a tree of depth 2. + // The first level lets us group preferences (that we want the user to + // access) accorind to their 'category'. Under this we just duplicate each + // preference in that category as a child node. In QML our repeaters/list + // view does the rest to make widgets for the prefs. + + auto tree = R"({"children": []})"_json; + storeToTree(entireGlobalStore, tree); + setModelData(tree); +} + +// convert to internal representation. +void PublicPreferencesModel::storeToTree(const nlohmann::json &src, nlohmann::json &tree) { + + auto find_or_make_child = [](nlohmann::json &tree, + const std::string &category) -> nlohmann::json & { + nlohmann::json &children = tree["children"]; + for (auto &v : children) { + + if (v["category"].get() == category) { + return v; + } + } + + nlohmann::json new_child = R"({})"_json; + new_child["category"] = category; + children.push_back(new_child); + return children.back(); + }; + + auto result = R"([])"_json; + + for (const auto &[k, v] : src.items()) { + if (v.count("category") && v["category"].is_string()) { + + const std::string category = v["category"].get(); + nlohmann::json &category_group = find_or_make_child(tree, category); + category_group["children"].push_back(v); + + } else if (v.is_object()) { + storeToTree(v, tree); + } + } +} + +QVariant PublicPreferencesModel::data(const QModelIndex &index, int role) const { + auto result = QVariant(); + + try { + const auto &j = indexToData(index); + switch (role) { + case JSONTreeModel::Roles::JSONRole: + result = QVariantMapFromJson(j); + break; + + case JSONTreeModel::Roles::JSONTextRole: + result = QString::fromStdString(j.dump(2)); + break; + + case Roles::datatypeRole: + if (j.count("datatype")) + result = QString::fromStdString(j.at("datatype").get()); + else + result = QString::fromStdString("group"); + break; + + case Roles::pathRole: + if (j.contains("path")) { + result = QString::fromStdString(j.at("path").get()); + } + break; + + case Roles::contextRole: { + auto tmp = QStringList(); + for (const auto &i : j.at("context")) + tmp.append(QStringFromStd(i.get())); + result = tmp; + } break; + + case Roles::descriptionRole: + result = QString::fromStdString(j.at("description").get()); + break; + + case Roles::overriddenPathRole: + result = QString::fromStdString(j.at("overridden_path").get()); + break; + + case Roles::valueRole: + result = mapFromValue(j.at("value")); + break; + + case Roles::defaultValueRole: + result = mapFromValue(j.at("default_value")); + break; + + case Roles::overriddenValueRole: + result = mapFromValue(j.at("overridden_value")); + break; + + case Roles::categoryRole: + result = mapFromValue(j.at("category")); + break; + + case Roles::optionsRole: + result = mapFromValue(j.at("options")); + break; + case Roles::nameRole: + case Qt::DisplayRole: + case Roles::displayNameRole: + if (j.contains("display_name")) { + result = mapFromValue(j.at("display_name")); + } else { + QString p = mapFromValue(j.at("path")).toString(); + result = capitaliseFirst(p.mid(p.lastIndexOf("/") + 1)); + } + break; + + default: + break; + } + } catch (const std::exception &err) { + + spdlog::warn( + "{} {} {} {} {}", + __PRETTY_FUNCTION__, + err.what(), + role, + index.row(), + index.internalId()); + } + + return result; +} + +bool PublicPreferencesModel::setData( + const QModelIndex &index, const QVariant &value, int role) { + bool result = false; + + QVector roles({role}); + + try { + if (role == Roles::valueRole and index.isValid()) { + nlohmann::json &j = indexToData(index); + + // test for change ? + // push to global data model + // spdlog::warn("variant {}", value.typeName()); + j["value"] = mapFromValue(value); + // spdlog::warn("json {} ", j["value"].dump(2)); + + if (gsh_) + gsh_->set_value(j["value"], j["path"], false); + result = true; + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + if (result) { + emit dataChanged(index, index, roles); + } + + return result; +} // "context": [ // "QML_UI" diff --git a/src/ui/qml/global_store/src/include/global_store_qml_export.h b/src/ui/qml/global_store/src/include/global_store_qml_export.h deleted file mode 100644 index a13120dfd..000000000 --- a/src/ui/qml/global_store/src/include/global_store_qml_export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef GLOBAL_STORE_QML_EXPORT_H -#define GLOBAL_STORE_QML_EXPORT_H - -#ifdef GLOBAL_STORE_QML_STATIC_DEFINE -# define GLOBAL_STORE_QML_EXPORT -# define GLOBAL_STORE_QML_NO_EXPORT -#else -# ifndef GLOBAL_STORE_QML_EXPORT -# ifdef global_store_qml_EXPORTS - /* We are building this library */ -# define GLOBAL_STORE_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define GLOBAL_STORE_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef GLOBAL_STORE_QML_NO_EXPORT -# define GLOBAL_STORE_QML_NO_EXPORT -# endif -#endif - -#ifndef GLOBAL_STORE_QML_DEPRECATED -# define GLOBAL_STORE_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef GLOBAL_STORE_QML_DEPRECATED_EXPORT -# define GLOBAL_STORE_QML_DEPRECATED_EXPORT GLOBAL_STORE_QML_EXPORT GLOBAL_STORE_QML_DEPRECATED -#endif - -#ifndef GLOBAL_STORE_QML_DEPRECATED_NO_EXPORT -# define GLOBAL_STORE_QML_DEPRECATED_NO_EXPORT GLOBAL_STORE_QML_NO_EXPORT GLOBAL_STORE_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef GLOBAL_STORE_QML_NO_DEPRECATED -# define GLOBAL_STORE_QML_NO_DEPRECATED -# endif -#endif - -#endif /* GLOBAL_STORE_QML_EXPORT_H */ diff --git a/src/ui/qml/helper/src/CMakeLists.txt b/src/ui/qml/helper/src/CMakeLists.txt index d0a4e6e07..3cbf4fa31 100644 --- a/src/ui/qml/helper/src/CMakeLists.txt +++ b/src/ui/qml/helper/src/CMakeLists.txt @@ -1,18 +1,40 @@ -SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core - Qt5::Qml - Qt5::Gui - xstudio::utility -) +if(WIN32) + SET(LINK_DEPS + CAF::core + Qt6::Core + Qt6::Qml + Qt6::Gui + Qt6::OpenGL + Qt6::Quick + Qt6::Widgets + xstudio::global_store + xstudio::media + xstudio::utility + ) +else() + SET(LINK_DEPS + CAF::core + Qt6::Core + Qt6::Qml + Qt6::Gui + Qt6::OpenGL + Qt6::Quick + Qt6::DBus + Qt6::Widgets + xstudio::global_store + xstudio::media + xstudio::utility + ) +endif() SET(EXTRAMOC "${ROOT_DIR}/include/xstudio/ui/qml/thumbnail_provider_ui.hpp" "${ROOT_DIR}/include/xstudio/ui/qml/shotgun_provider_ui.hpp" + "${ROOT_DIR}/include/xstudio/ui/qml/QTreeModelToTableModel.hpp" "${ROOT_DIR}/include/xstudio/ui/qml/json_tree_model_ui.hpp" "${ROOT_DIR}/include/xstudio/ui/qml/model_data_ui.hpp" "${ROOT_DIR}/include/xstudio/ui/qml/module_data_ui.hpp" - "${ROOT_DIR}/include/xstudio/ui/qml/snapshot_model_ui.hpp" + "${ROOT_DIR}/include/xstudio/ui/qml/actor_object.hpp" ) -create_qml_component(helper 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") +create_qml_component(helper ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/helper/src/QTreeModelToTableModel.cpp b/src/ui/qml/helper/src/QTreeModelToTableModel.cpp new file mode 100644 index 000000000..a7f0dbf9c --- /dev/null +++ b/src/ui/qml/helper/src/QTreeModelToTableModel.cpp @@ -0,0 +1,1179 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR +// GPL-3.0-only + +#include +// #include +// #include +#include +#include + +#include "xstudio/ui/qml/QTreeModelToTableModel.hpp" + +// QT_BEGIN_NAMESPACE + +// //#define QQMLTREEMODELADAPTOR_DEBUG +// #if defined(QQMLTREEMODELADAPTOR_DEBUG) && !defined(QT_TESTLIB_LIB) +// # define ASSERT_CONSISTENCY() Q_ASSERT_X(testConsistency(true /* dumpOnFail */), +// Q_FUNC_INFO, "Consistency test failed") #else +#define ASSERT_CONSISTENCY qt_noop +// #endif + +QTreeModelToTableModel::QTreeModelToTableModel(QObject *parent) : QAbstractItemModel(parent) {} + +QAbstractItemModel *QTreeModelToTableModel::model() const { return m_model; } + +void QTreeModelToTableModel::setModel(QAbstractItemModel *arg) { + struct Cx { + const char *signal; + const char *slot; + }; + const Cx connections[] = { + {SIGNAL(destroyed(QObject *)), SLOT(modelHasBeenDestroyed())}, + {SIGNAL(modelReset()), SLOT(modelHasBeenReset())}, + {SIGNAL(dataChanged(QModelIndex, QModelIndex, QVector)), + SLOT(modelDataChanged(QModelIndex, QModelIndex, QVector))}, + + {SIGNAL(layoutAboutToBeChanged( + QList, QAbstractItemModel::LayoutChangeHint)), + SLOT(modelLayoutAboutToBeChanged( + QList, QAbstractItemModel::LayoutChangeHint))}, + {SIGNAL( + layoutChanged(QList, QAbstractItemModel::LayoutChangeHint)), + SLOT(modelLayoutChanged( + QList, QAbstractItemModel::LayoutChangeHint))}, + + {SIGNAL(rowsAboutToBeInserted(QModelIndex, int, int)), + SLOT(modelRowsAboutToBeInserted(QModelIndex, int, int))}, + {SIGNAL(rowsInserted(QModelIndex, int, int)), + SLOT(modelRowsInserted(QModelIndex, int, int))}, + {SIGNAL(rowsAboutToBeRemoved(QModelIndex, int, int)), + SLOT(modelRowsAboutToBeRemoved(QModelIndex, int, int))}, + {SIGNAL(rowsRemoved(QModelIndex, int, int)), + SLOT(modelRowsRemoved(QModelIndex, int, int))}, + {SIGNAL(rowsAboutToBeMoved(QModelIndex, int, int, QModelIndex, int)), + SLOT(modelRowsAboutToBeMoved(QModelIndex, int, int, QModelIndex, int))}, + {SIGNAL(rowsMoved(QModelIndex, int, int, QModelIndex, int)), + SLOT(modelRowsMoved(QModelIndex, int, int, QModelIndex, int))}, + {nullptr, nullptr}}; + + if (m_model != arg) { + if (m_model) { + for (const Cx *c = &connections[0]; c->signal; c++) + disconnect(m_model, c->signal, this, c->slot); + } + + clearModelData(); + m_model = arg; + + if (m_rootIndex.isValid() && m_rootIndex.model() != m_model) + m_rootIndex = QModelIndex(); + + if (m_model) { + for (const Cx *c = &connections[0]; c->signal; c++) + connect(m_model, c->signal, this, c->slot); + + showModelTopLevelItems(); + } + + emit modelChanged(arg); + } +} + +void QTreeModelToTableModel::clearModelData() { + beginResetModel(); + m_items.clear(); + m_expandedItems.clear(); + endResetModel(); + emit lengthChanged(); +} + +QModelIndex QTreeModelToTableModel::rootIndex() const { return m_rootIndex; } + +void QTreeModelToTableModel::setRootIndex(const QModelIndex &idx) { + + // sometimes, idx is not valid and neither is m_rootIndex but length > 0. + // This can happen if m_rootIndex has gone invalid because the node it + // points to has been deleted from the source model. + // In this case, we don't want to return here but continue to rebuild the + // model. + if (m_rootIndex == idx && m_rootIndex.isValid() || !length()) + return; + + if (m_model) + clearModelData(); + m_rootIndex = idx; + if (m_model) + showModelTopLevelItems(); + emit rootIndexChanged(); +} + +void QTreeModelToTableModel::resetRootIndex() { setRootIndex(QModelIndex()); } + +QModelIndex +QTreeModelToTableModel::index(int row, int column, const QModelIndex &parent) const { + return hasIndex(row, column, parent) ? createIndex(row, column) : QModelIndex(); +} + +QModelIndex QTreeModelToTableModel::parent(const QModelIndex &child) const { + Q_UNUSED(child) + return QModelIndex(); +} + +QHash QTreeModelToTableModel::roleNames() const { + if (!m_model) + return QHash(); + return m_model->roleNames(); +} + +int QTreeModelToTableModel::rowCount(const QModelIndex &) const { + if (!m_model) + return 0; + return m_items.size(); +} + +int QTreeModelToTableModel::columnCount(const QModelIndex &parent) const { + if (!m_model) + return 0; + return m_model->columnCount(parent); +} + +QVariant QTreeModelToTableModel::data(const QModelIndex &index, int role) const { + if (!m_model) + return QVariant(); + + return m_model->data(mapToModel(index), role); +} + +bool QTreeModelToTableModel::setData( + const QModelIndex &index, const QVariant &value, int role) { + if (!m_model) + return false; + + return m_model->setData(mapToModel(index), value, role); +} + +QVariant +QTreeModelToTableModel::headerData(int section, Qt::Orientation orientation, int role) const { + return m_model->headerData(section, orientation, role); +} + +Qt::ItemFlags QTreeModelToTableModel::flags(const QModelIndex &index) const { + return m_model->flags(mapToModel(index)); +} + +int QTreeModelToTableModel::depthAtRow(int row) const { + if (row < 0 || row >= m_items.size()) + return 0; + return m_items.at(row).depth; +} + +int QTreeModelToTableModel::itemIndex(const QModelIndex &index) const { + // This is basically a plagiarism of QTreeViewPrivate::viewIndex() + if (!index.isValid() || index == m_rootIndex || m_items.isEmpty()) + return -1; + + const int totalCount = m_items.size(); + + // We start nearest to the lastViewedItem + int localCount = qMin(m_lastItemIndex - 1, totalCount - m_lastItemIndex); + + for (int i = 0; i < localCount; ++i) { + const TreeItem &item1 = m_items.at(m_lastItemIndex + i); + if (item1.index == index) { + m_lastItemIndex = m_lastItemIndex + i; + return m_lastItemIndex; + } + const TreeItem &item2 = m_items.at(m_lastItemIndex - i - 1); + if (item2.index == index) { + m_lastItemIndex = m_lastItemIndex - i - 1; + return m_lastItemIndex; + } + } + + for (int j = qMax(0, m_lastItemIndex + localCount); j < totalCount; ++j) { + const TreeItem &item = m_items.at(j); + if (item.index == index) { + m_lastItemIndex = j; + return j; + } + } + + for (int j = qMin(totalCount, m_lastItemIndex - localCount) - 1; j >= 0; --j) { + const TreeItem &item = m_items.at(j); + if (item.index == index) { + m_lastItemIndex = j; + return j; + } + } + + // nothing found + return -1; +} + +bool QTreeModelToTableModel::isVisible(const QModelIndex &index) { + return itemIndex(index) != -1; +} + +bool QTreeModelToTableModel::childrenVisible(const QModelIndex &index) { + return (index == m_rootIndex && !m_items.isEmpty()) || + (m_expandedItems.contains(index) && isVisible(index)); +} + +QModelIndex QTreeModelToTableModel::mapToModel(const QModelIndex &index) const { + if (!index.isValid()) + return QModelIndex(); + + const int row = index.row(); + if (row < 0 || row > m_items.size() - 1) + return QModelIndex(); + + const QModelIndex sourceIndex = m_items.at(row).index; + return m_model->index(sourceIndex.row(), index.column(), sourceIndex.parent()); +} + +QModelIndex QTreeModelToTableModel::mapFromModel(const QModelIndex &index) const { + if (!index.isValid()) + return QModelIndex(); + + int row = -1; + for (int i = 0; i < m_items.size(); ++i) { + const QModelIndex proxyIndex = m_items[i].index; + if (proxyIndex.row() == index.row() && proxyIndex.parent() == index.parent()) { + row = i; + break; + } + } + + if (row == -1) + return QModelIndex(); + + return this->index(row, index.column()); +} + +QModelIndex QTreeModelToTableModel::mapToModel(int row) const { + if (row < 0 || row >= m_items.size()) + return QModelIndex(); + return m_items.at(row).index; +} + +QItemSelection QTreeModelToTableModel::selectionForRowRange( + const QModelIndex &fromIndex, const QModelIndex &toIndex) const { + int from = itemIndex(fromIndex); + int to = itemIndex(toIndex); + if (from == -1) { + if (to == -1) + return QItemSelection(); + return QItemSelection(toIndex, toIndex); + } + + to = qMax(to, 0); + if (from > to) + qSwap(from, to); + + typedef QPair MIPair; + typedef QHash MI2MIPairHash; + MI2MIPairHash ranges; + QModelIndex firstIndex = m_items.at(from).index; + QModelIndex lastIndex = firstIndex; + QModelIndex previousParent = firstIndex.parent(); + bool selectLastRow = false; + for (int i = from + 1; i <= to || (selectLastRow = true); i++) { + // We run an extra iteration to make sure the last row is + // added to the selection. (And also to avoid duplicating + // the insertion code.) + QModelIndex index; + QModelIndex parent; + if (!selectLastRow) { + index = m_items.at(i).index; + parent = index.parent(); + } + if (selectLastRow || previousParent != parent) { + const MI2MIPairHash::iterator &it = ranges.find(previousParent); + if (it == ranges.end()) + ranges.insert(previousParent, MIPair(firstIndex, lastIndex)); + else + it->second = lastIndex; + + if (selectLastRow) + break; + + firstIndex = index; + previousParent = parent; + } + lastIndex = index; + } + + QItemSelection sel; + sel.reserve(ranges.size()); + for (const MIPair &pair : std::as_const(ranges)) + sel.append(QItemSelectionRange(pair.first, pair.second)); + + return sel; +} + +void QTreeModelToTableModel::showModelTopLevelItems(bool doInsertRows) { + if (!m_model) + return; + + if (m_model->hasChildren(m_rootIndex) && m_model->canFetchMore(m_rootIndex)) + m_model->fetchMore(m_rootIndex); + const long topLevelRowCount = m_model->rowCount(m_rootIndex); + if (topLevelRowCount == 0) + return; + + showModelChildItems(TreeItem(m_rootIndex), 0, topLevelRowCount - 1, doInsertRows); +} + +void QTreeModelToTableModel::showModelChildItems( + const TreeItem &parentItem, + int start, + int end, + bool doInsertRows, + bool doExpandPendingRows) { + const QModelIndex &parentIndex = parentItem.index; + int rowIdx = + parentIndex.isValid() && parentIndex != m_rootIndex ? itemIndex(parentIndex) + 1 : 0; + Q_ASSERT(rowIdx == 0 || parentItem.expanded); + if (parentIndex.isValid() && parentIndex != m_rootIndex && + (rowIdx == 0 || !parentItem.expanded)) + return; + + if (m_model->rowCount(parentIndex) == 0) { + if (m_model->hasChildren(parentIndex) && m_model->canFetchMore(parentIndex)) + m_model->fetchMore(parentIndex); + return; + } + + int insertCount = end - start + 1; + int startIdx; + if (start == 0) { + startIdx = rowIdx; + } else { + // Prefer to insert before next sibling instead of after last child of previous, as + // the latter is potentially buggy, see QTBUG-66062 + const QModelIndex &nextSiblingIdx = m_model->index(end + 1, 0, parentIndex); + if (nextSiblingIdx.isValid()) { + startIdx = itemIndex(nextSiblingIdx); + } else { + const QModelIndex &prevSiblingIdx = m_model->index(start - 1, 0, parentIndex); + startIdx = lastChildIndex(prevSiblingIdx) + 1; + } + } + + int rowDepth = rowIdx == 0 ? 0 : parentItem.depth + 1; + if (doInsertRows) + beginInsertRows(QModelIndex(), startIdx, startIdx + insertCount - 1); + m_items.reserve(m_items.size() + insertCount); + + for (int i = 0; i < insertCount; i++) { + const QModelIndex &cmi = m_model->index(start + i, 0, parentIndex); + const bool expanded = m_expandedItems.contains(cmi); + const TreeItem treeItem(cmi, rowDepth, expanded); + m_items.insert(startIdx + i, treeItem); + + if (expanded) + m_itemsToExpand.append(treeItem); + } + + if (doInsertRows) { + endInsertRows(); + emit lengthChanged(); + } + + if (doExpandPendingRows) + expandPendingRows(doInsertRows); +} + + +void QTreeModelToTableModel::expand(const QModelIndex &idx) { + ASSERT_CONSISTENCY(); + if (!m_model) + return; + + Q_ASSERT(!idx.isValid() || idx.model() == m_model); + + if (!idx.isValid() || !m_model->hasChildren(idx)) + return; + if (m_expandedItems.contains(idx)) + return; + + int row = itemIndex(idx); + if (row != -1) { + expandRow(row); + } else + m_expandedItems.insert(idx); + ASSERT_CONSISTENCY(); + + emit expanded(idx); +} + +void QTreeModelToTableModel::collapse(const QModelIndex &idx) { + ASSERT_CONSISTENCY(); + if (!m_model) + return; + + Q_ASSERT(!idx.isValid() || idx.model() == m_model); + + if (!idx.isValid() || !m_model->hasChildren(idx)) + return; + if (!m_expandedItems.contains(idx)) + return; + + int row = itemIndex(idx); + if (row != -1) + collapseRow(row); + else + m_expandedItems.remove(idx); + ASSERT_CONSISTENCY(); + + emit collapsed(idx); +} + +bool QTreeModelToTableModel::isExpanded(const QModelIndex &index) const { + ASSERT_CONSISTENCY(); + if (!m_model) + return false; + + Q_ASSERT(!index.isValid() || index.model() == m_model); + return !index.isValid() || m_expandedItems.contains(index); +} + +bool QTreeModelToTableModel::isExpanded(int row) const { + if (row < 0 || row >= m_items.size()) + return false; + return m_items.at(row).expanded; +} + +bool QTreeModelToTableModel::hasChildren(int row) const { + if (row < 0 || row >= m_items.size()) + return false; + return m_model->hasChildren(m_items[row].index); +} + +bool QTreeModelToTableModel::hasSiblings(int row) const { + const QModelIndex &index = mapToModel(row); + return index.row() != m_model->rowCount(index.parent()) - 1; +} + +void QTreeModelToTableModel::expandAll(int depth) { + + // we can trigger expandAll on a change in row count (see XsShotBrowser.qml) + // ... but doing the expand changes the row count. We want to avoid a + // recursive call here using this flag. + if (m_expanding_all) + return; + + m_expanding_all = true; + int rc = rowCount() + 1; + while (rc != rowCount()) { + rc = rowCount(); + for (int i = 0; i < rc; ++i) { + if (!isExpanded(i) && depthAtRow(i) < depth) { + expandRow(i); + } + } + } + m_expanding_all = false; +} + +void QTreeModelToTableModel::expandRow(int n) { + + if (!m_model || isExpanded(n)) + return; + + TreeItem &item = m_items[n]; + + // Ted - commenting the below test out, because hasChildren can return false + // since the session model might not yet be filled out for the given index - + // assumption is if we explicitly called 'expandRow' we want it expanded + // regarless of whether it has children yet or now. + + /*if ((item.index.flags() & Qt::ItemNeverHasChildren) || !m_model->hasChildren(item.index)) + return;*/ + + item.expanded = true; + m_expandedItems.insert(item.index); + QVector changedRole(1, ExpandedRole); + emit dataChanged(index(n, m_column), index(n, m_column), changedRole); + + m_itemsToExpand.append(item); + expandPendingRows(); +} + +void QTreeModelToTableModel::expandRecursively(int row, int depth) { + Q_ASSERT(depth == -1 || depth > 0); + const int startDepth = depthAtRow(row); + + auto expandHelp = + [this, depth, startDepth](const auto expandHelp, const QModelIndex &index) -> void { + const int rowToExpand = itemIndex(index); + if (!m_expandedItems.contains(index)) { + expandRow(rowToExpand); + } + + if (depth != -1 && depthAtRow(rowToExpand) == startDepth + depth - 1) + return; + + const int childCount = m_model->rowCount(index); + for (int childRow = 0; childRow < childCount; ++childRow) { + const QModelIndex childIndex = m_model->index(childRow, 0, index); + if (m_model->hasChildren(childIndex)) + expandHelp(expandHelp, childIndex); + } + }; + + const QModelIndex index = m_items[row].index; + if (index.isValid()) + expandHelp(expandHelp, index); +} + +void QTreeModelToTableModel::expandPendingRows(bool doInsertRows) { + while (!m_itemsToExpand.isEmpty()) { + const TreeItem item = m_itemsToExpand.takeFirst(); + Q_ASSERT(item.expanded); + const QModelIndex &index = item.index; + int childrenCount = m_model->rowCount(index); + if (childrenCount == 0) { + if (m_model->hasChildren(index) && m_model->canFetchMore(index)) + m_model->fetchMore(index); + continue; + } + + // TODO Pre-compute the total number of items made visible + // so that we only call a single beginInsertRows()/endInsertRows() + // pair per expansion (same as we do for collapsing). + showModelChildItems(item, 0, childrenCount - 1, doInsertRows, false); + } +} + +void QTreeModelToTableModel::collapseRecursively(int row) { + auto collapseHelp = [this](const auto collapseHelp, const QModelIndex &index) -> void { + if (m_expandedItems.contains(index)) { + const int rowToCollapse = itemIndex(index); + if (rowToCollapse != -1) + collapseRow(rowToCollapse); + else + m_expandedItems.remove(index); + } + + const int childCount = m_model->rowCount(index); + for (int childRow = 0; childRow < childCount; ++childRow) { + const QModelIndex childIndex = m_model->index(childRow, 0, index); + if (m_model->hasChildren(childIndex)) + collapseHelp(collapseHelp, childIndex); + } + }; + + const QModelIndex index = m_items[row].index; + if (index.isValid()) + collapseHelp(collapseHelp, index); +} + +#include "xstudio/utility/logging.hpp" + +void QTreeModelToTableModel::collapseRow(int n) { + if (!m_model || !isExpanded(n)) + return; + + SignalFreezer aggregator(this); + + TreeItem &item = m_items[n]; + item.expanded = false; + m_expandedItems.remove(item.index); + QVector changedRole(1, ExpandedRole); + queueDataChanged(index(n, m_column), index(n, m_column), changedRole); + int childrenCount = m_model->rowCount(item.index); + if ((item.index.flags() & Qt::ItemNeverHasChildren) || !m_model->hasChildren(item.index) || + childrenCount == 0) + return; + + const QModelIndex &emi = m_model->index(childrenCount - 1, 0, item.index); + int lastIndex = lastChildIndex(emi); + removeVisibleRows(n + 1, lastIndex); +} + +int QTreeModelToTableModel::lastChildIndex(const QModelIndex &index) const { + // The purpose of this function is to return the row of the last decendant of a node N. + // But note: index should point to the last child of N, and not N itself! + // This means that if index is not expanded, the last child will simply be index itself. + // Otherwise, since the tree underneath index can be of any depth, it will instead find + // the first sibling of N, get its table row, and simply return the row above. + + // index is last direct child of item being collapsed + + if (!m_expandedItems.contains(index)) + return itemIndex(index); + + // last child is expanded, we need to find last child of it.. + // this is recursive.. + + // item being collapsed + QModelIndex parent = index; + QModelIndex nextSiblingIndex; + + int firstIndex; + + while (parent.isValid() and m_expandedItems.contains(parent)) { + // last child of parent + nextSiblingIndex = m_model->index(m_model->rowCount(parent) - 1, 0, parent); + firstIndex = itemIndex(nextSiblingIndex); + parent = nextSiblingIndex; + } + + return firstIndex; +} + +void QTreeModelToTableModel::removeVisibleRows( + int startIndex, int endIndex, bool doRemoveRows) { + if (startIndex < 0 || endIndex < 0 || startIndex > endIndex) + return; + + if (doRemoveRows) + beginRemoveRows(QModelIndex(), startIndex, endIndex); + m_items.erase(m_items.begin() + startIndex, m_items.begin() + endIndex + 1); + if (doRemoveRows) { + endRemoveRows(); + emit lengthChanged(); + + + /* We need to update the model index for all the items below the removed ones */ + int lastIndex = m_items.size() - 1; + if (startIndex <= lastIndex) { + const QModelIndex &topLeft = index(startIndex, 0, QModelIndex()); + const QModelIndex &bottomRight = index(lastIndex, 0, QModelIndex()); + const QVector changedRole(1, ModelIndexRole); + queueDataChanged(topLeft, bottomRight, changedRole); + } + } +} + +void QTreeModelToTableModel::modelHasBeenDestroyed() { + // The model has been deleted. This should behave as if no model was set + clearModelData(); + emit modelChanged(nullptr); +} + +void QTreeModelToTableModel::modelHasBeenReset() { + clearModelData(); + emit lengthChanged(); + + showModelTopLevelItems(); + ASSERT_CONSISTENCY(); +} + +void QTreeModelToTableModel::modelDataChanged( + const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { + Q_ASSERT(topLeft.parent() == bottomRight.parent()); + const QModelIndex &parent = topLeft.parent(); + if (parent.isValid() && !childrenVisible(parent)) { + ASSERT_CONSISTENCY(); + return; + } + + int topIndex = itemIndex(topLeft.siblingAtColumn(0)); + if (topIndex == -1) // 'parent' is not visible anymore, though it's been expanded previously + return; + for (int i = topLeft.row(); i <= bottomRight.row(); i++) { + // Group items with same parent to minize the number of 'dataChanged()' emits + int bottomIndex = topIndex; + while (bottomIndex < m_items.size()) { + const QModelIndex &idx = m_items.at(bottomIndex).index; + if (idx.parent() != parent) { + --bottomIndex; + break; + } + if (idx.row() == bottomRight.row()) + break; + ++bottomIndex; + } + emit dataChanged( + index(topIndex, topLeft.column()), index(bottomIndex, bottomRight.column()), roles); + + i += bottomIndex - topIndex; + if (i == bottomRight.row()) + break; + topIndex = bottomIndex + 1; + while (topIndex < m_items.size() && m_items.at(topIndex).index.parent() != parent) + topIndex++; + } + ASSERT_CONSISTENCY(); +} + +void QTreeModelToTableModel::modelLayoutAboutToBeChanged( + const QList &parents, QAbstractItemModel::LayoutChangeHint hint) { + Q_UNUSED(hint) + + // Since the m_items is a list of TreeItems that contains QPersistentModelIndexes, we + // cannot wait until we get a modelLayoutChanged() before we remove the affected rows + // from that list. After the layout has changed, the list (or, the persistent indexes + // that it contains) is no longer in sync with the model (after all, that is what we're + // supposed to correct in modelLayoutChanged()). + // This means that vital functions, like itemIndex(index), cannot be trusted at that point. + // Therefore we need to do the update in two steps; First remove all the affected rows + // from here (while we're still in sync with the model), and then add back the + // affected rows, and notify about it, from modelLayoutChanged(). + m_modelLayoutChanged = false; + + if (parents.isEmpty() || !parents[0].isValid()) { + // Update entire model + emit layoutAboutToBeChanged(); + m_modelLayoutChanged = true; + m_items.clear(); + return; + } + + for (const QPersistentModelIndex &pmi : parents) { + for (const auto &i : m_items) { + if (i.index == pmi) { + // Update entire model + emit layoutAboutToBeChanged(); + m_modelLayoutChanged = true; + m_items.clear(); + return; + } + } + } + + for (const QPersistentModelIndex &pmi : parents) { + if (!m_expandedItems.contains(pmi)) + continue; + const int row = itemIndex(pmi); + if (row == -1) + continue; + const int rowCount = m_model->rowCount(pmi); + if (rowCount == 0) + continue; + + if (!m_modelLayoutChanged) { + emit layoutAboutToBeChanged(); + m_modelLayoutChanged = true; + } + + const QModelIndex &lmi = m_model->index(rowCount - 1, 0, pmi); + const int lastRow = lastChildIndex(lmi); + removeVisibleRows(row + 1, lastRow, false /*doRemoveRows*/); + } + + ASSERT_CONSISTENCY(); +} + +void QTreeModelToTableModel::modelLayoutChanged( + const QList &parents, QAbstractItemModel::LayoutChangeHint hint) { + Q_UNUSED(hint) + + if (!m_modelLayoutChanged) { + // No relevant changes done from modelLayoutAboutToBeChanged() + return; + } + + if (m_items.isEmpty()) { + // Entire model has changed. Add back all rows. + showModelTopLevelItems(false /*doInsertRows*/); + const QModelIndex &mi = m_model->index(0, 0); + const int columnCount = m_model->columnCount(mi); + emit dataChanged(index(0, 0), index(m_items.size() - 1, columnCount - 1)); + emit layoutChanged(); + return; + } + + for (const QPersistentModelIndex &pmi : parents) { + if (!m_expandedItems.contains(pmi)) + continue; + const int row = itemIndex(pmi); + if (row == -1) + continue; + const int rowCount = m_model->rowCount(pmi); + if (rowCount == 0) + continue; + + const QModelIndex &lmi = m_model->index(rowCount - 1, 0, pmi); + const int columnCount = m_model->columnCount(lmi); + showModelChildItems(m_items.at(row), 0, rowCount - 1, false /*doInsertRows*/); + const int lastRow = lastChildIndex(lmi); + emit dataChanged(index(row + 1, 0), index(lastRow, columnCount - 1)); + } + + emit layoutChanged(); + + ASSERT_CONSISTENCY(); +} + +void QTreeModelToTableModel::modelRowsAboutToBeInserted( + const QModelIndex &parent, int start, int end) { + Q_UNUSED(parent) + Q_UNUSED(start) + Q_UNUSED(end) + ASSERT_CONSISTENCY(); +} + +void QTreeModelToTableModel::modelRowsInserted(const QModelIndex &parent, int start, int end) { + TreeItem item; + int parentRow = itemIndex(parent); + if (parentRow >= 0) { + const QModelIndex &parentIndex = index(parentRow, m_column); + QVector changedRole(1, HasChildrenRole); + queueDataChanged(parentIndex, parentIndex, changedRole); + item = m_items.at(parentRow); + if (!item.expanded) { + ASSERT_CONSISTENCY(); + return; + } + } else if (parent == m_rootIndex) { + item = TreeItem(parent); + } else { + ASSERT_CONSISTENCY(); + return; + } + showModelChildItems(item, start, end); + ASSERT_CONSISTENCY(); +} + +void QTreeModelToTableModel::modelRowsAboutToBeRemoved( + const QModelIndex &parent, int start, int end) { + ASSERT_CONSISTENCY(); + enableSignalAggregation(); + if (parent == m_rootIndex || childrenVisible(parent)) { + const QModelIndex &smi = m_model->index(start, 0, parent); + int startIndex = itemIndex(smi); + const QModelIndex &emi = m_model->index(end, 0, parent); + int endIndex = -1; + if (isExpanded(emi)) { + int rowCount = m_model->rowCount(emi); + if (rowCount > 0) { + const QModelIndex &idx = m_model->index(rowCount - 1, 0, emi); + endIndex = lastChildIndex(idx); + } + } + if (endIndex == -1) + endIndex = itemIndex(emi); + + removeVisibleRows(startIndex, endIndex); + } + + for (int r = start; r <= end; r++) { + const QModelIndex &cmi = m_model->index(r, 0, parent); + m_expandedItems.remove(cmi); + } +} + +void QTreeModelToTableModel::modelRowsRemoved(const QModelIndex &parent, int start, int end) { + Q_UNUSED(start) + Q_UNUSED(end) + int parentRow = itemIndex(parent); + if (parentRow >= 0) { + const QModelIndex &parentIndex = index(parentRow, m_column); + QVector changedRole(1, HasChildrenRole); + queueDataChanged(parentIndex, parentIndex, changedRole); + } + disableSignalAggregation(); + ASSERT_CONSISTENCY(); +} + +void QTreeModelToTableModel::modelRowsAboutToBeMoved( + const QModelIndex &sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex &destinationParent, + int destinationRow) { + ASSERT_CONSISTENCY(); + enableSignalAggregation(); + m_visibleRowsMoved = false; + if (!childrenVisible(sourceParent)) + return; // Do nothing now. See modelRowsMoved() below. + + if (!childrenVisible(destinationParent)) { + modelRowsAboutToBeRemoved(sourceParent, sourceStart, sourceEnd); + /* If the destination parent has no children, we'll need to + * report a change on the HasChildrenRole */ + if (isVisible(destinationParent) && m_model->rowCount(destinationParent) == 0) { + const QModelIndex &topLeft = index(itemIndex(destinationParent), 0, QModelIndex()); + const QModelIndex &bottomRight = topLeft; + const QVector changedRole(1, HasChildrenRole); + queueDataChanged(topLeft, bottomRight, changedRole); + } + } else { + int depthDifference = -1; + if (destinationParent.isValid()) { + int destParentIndex = itemIndex(destinationParent); + if (destParentIndex >= 0) + depthDifference = m_items.at(destParentIndex).depth; + else + depthDifference = 0; + } + if (sourceParent.isValid()) { + int sourceParentIndex = itemIndex(sourceParent); + if (sourceParentIndex >= 0) + depthDifference -= m_items.at(sourceParentIndex).depth; + else + depthDifference = 0; + } else { + depthDifference++; + } + + int startIndex = itemIndex(m_model->index(sourceStart, 0, sourceParent)); + const QModelIndex &emi = m_model->index(sourceEnd, 0, sourceParent); + int endIndex = -1; + if (isExpanded(emi)) { + int rowCount = m_model->rowCount(emi); + if (rowCount > 0) + endIndex = lastChildIndex(m_model->index(rowCount - 1, 0, emi)); + } + if (endIndex == -1) + endIndex = itemIndex(emi); + + int destIndex = -1; + if (destinationRow == m_model->rowCount(destinationParent)) { + const QModelIndex &emi = m_model->index(destinationRow - 1, 0, destinationParent); + destIndex = lastChildIndex(emi) + 1; + } else { + destIndex = itemIndex(m_model->index(destinationRow, 0, destinationParent)); + } + + int totalMovedCount = endIndex - startIndex + 1; + + /* This beginMoveRows() is matched by a endMoveRows() in the + * modelRowsMoved() method below. */ + m_visibleRowsMoved = + startIndex != destIndex && + beginMoveRows(QModelIndex(), startIndex, endIndex, QModelIndex(), destIndex); + + const QList &buffer = m_items.mid(startIndex, totalMovedCount); + int bufferCopyOffset; + if (destIndex > endIndex) { + for (int i = endIndex + 1; i < destIndex; i++) { + m_items.swapItemsAt( + i, i - totalMovedCount); // Fast move from 1st to 2nd position + } + bufferCopyOffset = destIndex - totalMovedCount; + } else { + // NOTE: we will not enter this loop if startIndex == destIndex + for (int i = startIndex - 1; i >= destIndex; i--) { + m_items.swapItemsAt( + i, i + totalMovedCount); // Fast move from 1st to 2nd position + } + bufferCopyOffset = destIndex; + } + for (int i = 0; i < buffer.size(); i++) { + TreeItem item = buffer.at(i); + item.depth += depthDifference; + m_items.replace(bufferCopyOffset + i, item); + } + + /* If both source and destination items are visible, the indexes of + * all the items in between will change. If they share the same + * parent, then this is all; however, if they belong to different + * parents, their bottom siblings will also get displaced, so their + * index also needs to be updated. + * Given that the bottom siblings of the top moved elements are + * already included in the update (since they lie between the + * source and the dest elements), we only need to worry about the + * siblings of the bottom moved element. + */ + const int top = qMin(startIndex, bufferCopyOffset); + int bottom = qMax(endIndex, bufferCopyOffset + totalMovedCount - 1); + if (sourceParent != destinationParent) { + const QModelIndex &bottomParent = + bottom == endIndex ? sourceParent : destinationParent; + + const int rowCount = m_model->rowCount(bottomParent); + if (rowCount > 0) + bottom = + qMax(bottom, lastChildIndex(m_model->index(rowCount - 1, 0, bottomParent))); + } + const QModelIndex &topLeft = index(top, 0, QModelIndex()); + const QModelIndex &bottomRight = index(bottom, 0, QModelIndex()); + const QVector changedRole(1, ModelIndexRole); + queueDataChanged(topLeft, bottomRight, changedRole); + + if (depthDifference != 0) { + const QModelIndex &topLeft = index(bufferCopyOffset, 0, QModelIndex()); + const QModelIndex &bottomRight = + index(bufferCopyOffset + totalMovedCount - 1, 0, QModelIndex()); + const QVector changedRole(1, DepthRole); + queueDataChanged(topLeft, bottomRight, changedRole); + } + } +} + +void QTreeModelToTableModel::modelRowsMoved( + const QModelIndex &sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex &destinationParent, + int destinationRow) { + if (!childrenVisible(sourceParent)) { + modelRowsInserted( + destinationParent, destinationRow, destinationRow + sourceEnd - sourceStart); + } else if (!childrenVisible(destinationParent)) { + modelRowsRemoved(sourceParent, sourceStart, sourceEnd); + } + + if (m_visibleRowsMoved) + endMoveRows(); + + if (isVisible(sourceParent) && m_model->rowCount(sourceParent) == 0) { + int parentRow = itemIndex(sourceParent); + collapseRow(parentRow); + const QModelIndex &topLeft = index(parentRow, 0, QModelIndex()); + const QModelIndex &bottomRight = topLeft; + const QVector changedRole{ExpandedRole, HasChildrenRole}; + queueDataChanged(topLeft, bottomRight, changedRole); + } + + disableSignalAggregation(); + + ASSERT_CONSISTENCY(); +} + +void QTreeModelToTableModel::dump() const { + if (!m_model) + return; + int count = m_items.size(); + if (count == 0) + return; + int countWidth = floor(log10(double(count))) + 1; + // qInfo() << "Dumping" << this; + for (int i = 0; i < count; i++) { + const TreeItem &item = m_items.at(i); + bool hasChildren = m_model->hasChildren(item.index); + int children = m_model->rowCount(item.index); + // qInfo().noquote().nospace() + // << QStringLiteral("%1 ").arg(i, countWidth) << QString(4 * item.depth, + // QChar::fromLatin1('.')) + // << QLatin1String(!hasChildren ? ".. " : item.expanded ? " v " : " > ") + // << item.index << children; + } +} + +bool QTreeModelToTableModel::testConsistency(bool dumpOnFail) const { + if (!m_model) { + if (!m_items.isEmpty()) { + // qWarning() << "Model inconsistency: No model but stored visible items"; + return false; + } + if (!m_expandedItems.isEmpty()) { + // qWarning() << "Model inconsistency: No model but stored expanded items"; + return false; + } + return true; + } + QModelIndex parent = m_rootIndex; + QStack ancestors; + QModelIndex idx = m_model->index(0, 0, parent); + for (int i = 0; i < m_items.size(); i++) { + bool isConsistent = true; + const TreeItem &item = m_items.at(i); + if (item.index != idx) { + // qWarning() << "QModelIndex inconsistency" << i << item.index; + // qWarning() << " expected" << idx; + isConsistent = false; + } + if (item.index.parent() != parent) { + // qWarning() << "Parent inconsistency" << i << item.index; + // qWarning() << " stored index parent" << item.index.parent() << "model parent" + // << parent; + isConsistent = false; + } + if (item.depth != ancestors.size()) { + // qWarning() << "Depth inconsistency" << i << item.index; + // qWarning() << " item depth" << item.depth << "ancestors stack" << + // ancestors.size(); + isConsistent = false; + } + if (item.expanded && !m_expandedItems.contains(item.index)) { + // qWarning() << "Expanded inconsistency" << i << item.index; + // qWarning() << " set" << m_expandedItems.contains(item.index) << "item" << + // item.expanded; + isConsistent = false; + } + if (!isConsistent) { + if (dumpOnFail) + dump(); + return false; + } + QModelIndex firstChildIndex; + if (item.expanded) + firstChildIndex = m_model->index(0, 0, idx); + if (firstChildIndex.isValid()) { + ancestors.push(parent); + parent = idx; + idx = m_model->index(0, 0, parent); + } else { + while (idx.row() == m_model->rowCount(parent) - 1) { + if (ancestors.isEmpty()) + break; + idx = parent; + parent = ancestors.pop(); + } + idx = m_model->index(idx.row() + 1, 0, parent); + } + } + + return true; +} + +void QTreeModelToTableModel::enableSignalAggregation() { m_signalAggregatorStack++; } + +void QTreeModelToTableModel::disableSignalAggregation() { + m_signalAggregatorStack--; + Q_ASSERT(m_signalAggregatorStack >= 0); + if (m_signalAggregatorStack == 0) { + emitQueuedSignals(); + } +} + +void QTreeModelToTableModel::queueDataChanged( + const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { + if (isAggregatingSignals()) { + m_queuedDataChanged.append(DataChangedParams{topLeft, bottomRight, roles}); + } else { + emit dataChanged(topLeft, bottomRight, roles); + } +} + +void QTreeModelToTableModel::emitQueuedSignals() { + QVector combinedUpdates; + /* First, iterate through the queued updates and merge the overlapping ones + * to reduce the number of updates. + * We don't merge adjacent updates, because they are typically filed with a + * different role (a parent row is next to its children). + */ + for (const DataChangedParams &dataChange : std::as_const(m_queuedDataChanged)) { + int startRow = dataChange.topLeft.row(); + int endRow = dataChange.bottomRight.row(); + bool merged = false; + for (DataChangedParams &combined : combinedUpdates) { + int combinedStartRow = combined.topLeft.row(); + int combinedEndRow = combined.bottomRight.row(); + if ((startRow <= combinedStartRow && endRow >= combinedStartRow) || + (startRow <= combinedEndRow && endRow >= combinedEndRow)) { + if (startRow < combinedStartRow) { + combined.topLeft = dataChange.topLeft; + } + if (endRow > combinedEndRow) { + combined.bottomRight = dataChange.bottomRight; + } + for (int role : dataChange.roles) { + if (!combined.roles.contains(role)) + combined.roles.append(role); + } + merged = true; + break; + } + } + if (!merged) { + combinedUpdates.append(dataChange); + } + } + + /* Finally, emit the dataChanged signals */ + for (const DataChangedParams &dataChange : combinedUpdates) { + emit dataChanged(dataChange.topLeft, dataChange.bottomRight, dataChange.roles); + } + m_queuedDataChanged.clear(); +} + +// QT_END_NAMESPACE + +// #include "moc_QTreeModelToTableModel_p_p.cpp" diff --git a/src/ui/qml/helper/src/helper_ui.cpp b/src/ui/qml/helper/src/helper_ui.cpp index d5c1ac272..5ebe1c364 100644 --- a/src/ui/qml/helper/src/helper_ui.cpp +++ b/src/ui/qml/helper/src/helper_ui.cpp @@ -1,8 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 +#include + #include "xstudio/ui/qml/helper_ui.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/caf_helpers.hpp" #include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/frame_range.hpp" #include "xstudio/colour_pipeline/colour_pipeline.hpp" #include "xstudio/media/media.hpp" @@ -14,10 +17,9 @@ using namespace xstudio::ui::qml; #include #include #include - -QMLActor::QMLActor(QObject *parent) : super(parent) {} - -QMLActor::~QMLActor() {} +#include +#include +#include CafSystemObject::CafSystemObject(QObject *parent, caf::actor_system &sys) : QObject(parent), system_ref_(sys) { @@ -90,10 +92,11 @@ QString xstudio::ui::qml::getThumbnailURL( auto mhash = utility::request_receive>( *sys, actor, media::checksum_atom_v); - auto display_transform_hash = utility::request_receive( + auto display_transform_hash = utility::request_receive( *sys, colour_pipe, colour_pipeline::display_colour_transform_hash_atom_v, mp); hash = std::hash{}(static_cast( - display_transform_hash + mhash.first + std::to_string(mhash.second))); + std::to_string(display_transform_hash) + mhash.first + + std::to_string(mhash.second))); } catch ([[maybe_unused]] const std::exception &err) { // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -117,6 +120,10 @@ QString xstudio::ui::qml::getThumbnailURL( } nlohmann::json xstudio::ui::qml::qvariant_to_json(const QVariant &var) { + + if (not var.isValid()) + return nlohmann::json(); + switch (QMetaType::Type(var.type())) { case QMetaType::Bool: return nlohmann::json(var.toBool()); @@ -142,6 +149,12 @@ nlohmann::json xstudio::ui::qml::qvariant_to_json(const QVariant &var) { case QMetaType::QString: return nlohmann::json(var.toString().toStdString()); break; + case QMetaType::QUuid: + return nlohmann::json(UuidFromQUuid(var.toUuid())); + break; + case QMetaType::QUrl: + return nlohmann::json(StdFromQString(var.toUrl().toString())); + break; case QMetaType::QColor: { auto c = var.value(); return nlohmann::json(utility::ColourTriplet( @@ -152,7 +165,7 @@ nlohmann::json xstudio::ui::qml::qvariant_to_json(const QVariant &var) { break; case QMetaType::QVariantMap: { const QVariantMap m = var.toMap(); - nlohmann::json rt; + auto rt = R"({})"_json; for (auto p = m.begin(); p != m.end(); ++p) { rt[p.key().toStdString()] = xstudio::ui::qml::qvariant_to_json(p.value()); } @@ -160,7 +173,7 @@ nlohmann::json xstudio::ui::qml::qvariant_to_json(const QVariant &var) { } break; case QMetaType::QVariantList: { const QVariantList m = var.toList(); - nlohmann::json rt; + auto rt = R"([])"_json; for (const auto &p : m) { rt.push_back(xstudio::ui::qml::qvariant_to_json(p)); } @@ -168,6 +181,7 @@ nlohmann::json xstudio::ui::qml::qvariant_to_json(const QVariant &var) { } break; default: { // No QMetaType for QJSValue + // watchout there is a bug in toVariant when the QJSValue is an object.. if (var.canConvert()) { const auto m = var.value(); return xstudio::ui::qml::qvariant_to_json(m.toVariant()); @@ -190,12 +204,6 @@ QVariant xstudio::ui::qml::json_to_qvariant(const nlohmann::json &json) { return QVariant(json.get().c_str()); } else if (json.is_boolean()) { return QVariant(json.get()); - } else if (json.is_object()) { - QMap m; - for (auto p = json.begin(); p != json.end(); ++p) { - m[p.key().c_str()] = json_to_qvariant(p.value()); - } - return QVariant(m); } else if (json.is_array()) { if (json.size() == 5 && json[0].is_string() && json[0].get() == "colour") { @@ -207,12 +215,30 @@ QVariant xstudio::ui::qml::json_to_qvariant(const nlohmann::json &json) { static_cast(round(t.g * 255.0f)), static_cast(round(t.b * 255.0f))); return QVariant(c); + + } else if ( + json.size() == 6 && json[0].is_string() && json[0].get() == "vec4") { + + // it should be a color! + const auto t = json.get(); + QVector4D rt; + rt[0] = t[0]; + rt[1] = t[1]; + rt[2] = t[2]; + rt[3] = t[3]; + return QVariant(rt); } QList rt; for (auto p = json.begin(); p != json.end(); ++p) { rt.append(json_to_qvariant(p.value())); } return rt; + } else if (json.is_object()) { + QMap m; + for (auto p = json.begin(); p != json.end(); ++p) { + m[p.key().c_str()] = json_to_qvariant(p.value()); + } + return QVariant(m); } else if (json.is_null()) { return QVariant(); } @@ -220,6 +246,77 @@ QVariant xstudio::ui::qml::json_to_qvariant(const nlohmann::json &json) { } +KeyEventsItem::KeyEventsItem(QQuickItem *parent) : QQuickItem(parent) { + + keypress_monitor_ = CafSystemObject::get_actor_system().registry().template get( + xstudio::keyboard_events); + setAcceptHoverEvents(true); +} + +bool KeyEventsItem::event(QEvent *event) { + + // Key events are forwarded to keypress_monitor_ - we pass our 'context' + // property so we have an idea WHERE (i.e. what UI element) the hotkey was + // pressed + + if (window_name_.empty()) + window_name_ = StdFromQString(item_window_name(parent())); + + /* This is where keyboard events are captured and sent to the backend!! */ + if (event->type() == QEvent::KeyPress) { + + auto key_event = dynamic_cast(event); + if (key_event) { + anon_mail( + ui::keypress_monitor::key_down_atom_v, + key_event->key(), + context_, + window_name_, + key_event->isAutoRepeat()) + .send(keypress_monitor_); + } + } else if (event->type() == QEvent::KeyRelease) { + + auto key_event = dynamic_cast(event); + if (key_event && !key_event->isAutoRepeat()) { + anon_mail( + ui::keypress_monitor::key_up_atom_v, key_event->key(), context_, window_name_) + .send(keypress_monitor_); + } + } else if ( + event->type() == QEvent::Leave || event->type() == QEvent::HoverLeave || + event->type() == QEvent::DragLeave || event->type() == QEvent::GraphicsSceneDragLeave || + event->type() == QEvent::GraphicsSceneHoverLeave) { + anon_mail(ui::keypress_monitor::all_keys_up_atom_v, context_).send(keypress_monitor_); + } else if (event->type() == QEvent::HoverEnter) { + forceActiveFocus(Qt::MouseFocusReason); + } + return QQuickItem::event(event); +} + +void KeyEventsItem::keyPressEvent(QKeyEvent *event) { + + if (window_name_.empty()) + window_name_ = StdFromQString(item_window_name(parent())); + + anon_mail( + ui::keypress_monitor::text_entry_atom_v, + StdFromQString(event->text()), + context_, + window_name_) + .send(keypress_monitor_); +} +void KeyEventsItem::keyReleaseEvent(QKeyEvent *event) { + + if (window_name_.empty()) + window_name_ = StdFromQString(item_window_name(parent())); + + if (!event->isAutoRepeat()) { + anon_mail(ui::keypress_monitor::key_up_atom_v, event->key(), context_, window_name_) + .send(keypress_monitor_); + } +} + ClipboardProxy::ClipboardProxy(QObject *parent) : QObject(parent) { QClipboard *clipboard = QGuiApplication::clipboard(); connect(clipboard, &QClipboard::dataChanged, this, &ClipboardProxy::dataChanged); @@ -280,6 +377,111 @@ QDateTime Helpers::getFileMTime(const QUrl &url) const { std::chrono::duration_cast(mtim.time_since_epoch()).count()); } +QModelIndexList Helpers::getParentIndexesFromRange(const QItemSelection &l) const { + return getParentIndexes(l.indexes()); +} + +QModelIndexList Helpers::getParentIndexes(const QModelIndexList &l) const { + auto result = QModelIndexList(); + + for (auto i : l) { + while (i.isValid()) { + i = i.parent(); + result.push_front(i); + } + } + + return result; +} + +#ifdef __linux__ +#include +#endif + +void Helpers::inhibitScreenSaver(const bool inhibit) const { + // quint32 screensaver_cookie_{0}; + + spdlog::debug("inhibitScreenSaver {}", inhibit); + +#ifdef __linux__ + const int MAX_SERVICES = 1; + + QDBusConnection bus = QDBusConnection::sessionBus(); + if (bus.isConnected()) { + QString services[MAX_SERVICES] = { + "org.freedesktop.ScreenSaver", + // "org.gnome.SessionManager" + }; + QString paths[MAX_SERVICES] = { + "/org/freedesktop/ScreenSaver", + // "/org/gnome/SessionManager" + }; + + static quint32 cookies[2] = {0, 0}; + + for (int i = 0; i < MAX_SERVICES; i++) { + QDBusInterface screenSaverInterface(services[i], paths[i], services[i], bus); + + if (!screenSaverInterface.isValid()) + continue; + + QDBusReply reply; + + if (inhibit) + reply = screenSaverInterface.call("Inhibit", "xStudio", "Playing"); + else if (cookies[i]) { + reply = screenSaverInterface.call("UnInhibit", cookies[i]); + cookies[i] = 0; + } + + if (inhibit) { + if (reply.isValid()) + cookies[i] = reply.value(); + else { + QDBusError error = reply.error(); + spdlog::warn( + "{} {} {}", + __PRETTY_FUNCTION__, + StdFromQString(error.message()), + StdFromQString(error.name())); + } + } + } + } + +#endif +} + +QVariant Helpers::python_callback( + QString method_name, QUuid python_plugin_uuid, const QVariant args) const { + + spdlog::warn("{} Not Implemented Yet!", __PRETTY_FUNCTION__); + /*QString method_name, QUuid python_plugin_uuid, const QVariant args) const { + try { + + utility::JsonStore packed_args(xstudio::ui::qml::qvariant_to_json(args)); + auto python_interp_actor = + CafSystemObject::get_actor_system().registry().template get( + embedded_python_registry); + + scoped_actor sys{CafSystemObject::get_actor_system()}; + auto return_val = utility::request_receive( + *sys, + python_interp_actor, + embedded_python::python_exec_atom_v, + UuidFromQUuid(python_plugin_uuid), + StdFromQString(method_name), + packed_args); + + return json_to_qvariant(return_val); + + } catch (std::exception &e) { + spdlog::critical("{} {}", __PRETTY_FUNCTION__, e.what()); + }*/ + + return QVariant(); +} + bool Helpers::startDetachedProcess( const QString &program, const QStringList &arguments, @@ -299,3 +501,257 @@ QString Helpers::readFile(const QUrl &url) const { return ""; } + +QObject *Helpers::contextPanel(QObject *obj) const { + + // traverse up the qml context hierarchy until we hit an object named + // XsPanelParent - then we've gone one level beyond the actual panel + // item that we want + if (qmlContext(obj)) { + QQmlContext *c = qmlContext(obj); + QObject *pobj = obj; + while (c) { + QObject *cobj = c->contextObject(); + if (cobj && cobj->objectName() == "XsPanelParent") { + return pobj; + } + pobj = cobj; + c = c->parentContext(); + } + } + return nullptr; +} + +void Helpers::setMenuPathPosition( + const QString &menu_path, const QString &menu_name, const float position) const { + + if (!menu_path.isEmpty() && !menu_name.isEmpty()) { + + auto central_models_data_actor = + CafSystemObject::get_actor_system().registry().template get( + global_ui_model_data_registry); + + anon_mail( + ui::model_data::insert_or_update_menu_node_atom_v, + StdFromQString(menu_name), + StdFromQString(menu_path), + position) + .send(central_models_data_actor); + } +} + +ImagePainter::ImagePainter(QQuickItem *parent) : QQuickPaintedItem(parent) {} + +void ImagePainter::paint(QPainter *painter) { + + if (image_.isNull()) + return; + + const float image_aspect = float(image_.width()) / float(image_.height()); + const float canvas_aspect = float(width()) / float(height()); + + if (!fill_) { + if (image_aspect > canvas_aspect) { + float bottom = + float(height()) * 0.5f * (image_aspect - canvas_aspect) / image_aspect; + painter->drawImage( + QRectF(0.0f, bottom, width(), height() - (bottom * 2.0f)), image_); + } else { + float left = float(width()) * 0.5f * (canvas_aspect - image_aspect) / canvas_aspect; + painter->drawImage(QRectF(left, 0.0f, width() - (left * 2.0f), height()), image_); + } + } else { + painter->drawImage(QRectF(0.0f, -5.0f, width(), height() + 5.0f), image_); + } +} + +MarkerModel::MarkerModel(QObject *parent) : JSONTreeModel(parent) { + auto role_names = std::vector( + {{"commentRole"}, + {"durationRole"}, + {"flagRole"}, + {"layerRole"}, + {"nameRole"}, + {"rateRole"}, + {"startRole"}}); + + setRoleNames(role_names); + + connect(this, &MarkerModel::rowsInserted, this, &MarkerModel::markerDataChanged); + connect(this, &MarkerModel::rowsRemoved, this, &MarkerModel::markerDataChanged); +} + +bool MarkerModel::setMarkerData(const QVariant &qdata) { + auto result = true; + try { + auto data = mapFromValue(qdata); + if (modelData().is_null() or data != modelData().at("children")) { + setModelData(data); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + result = false; + } + return result; +} + +QVariant MarkerModel::markerData() const { + // auto data = R"([])"_json; + // try { + // data = modelData().at("children"); + // } catch(...) {} + + return QVariantFromJson(modelData()); +} + +QVariant MarkerModel::data(const QModelIndex &index, int role) const { + auto result = QVariant(); + try { + const auto &j = indexToData(index); + switch (role) { + case JSONTreeModel::Roles::idRole: + result = QVariant::fromValue(QUuidFromUuid(j.value("uuid", utility::Uuid()))); + break; + case Roles::flagRole: + result = QString::fromStdString(j.value("flag", std::string(""))); + break; + case Roles::nameRole: + result = QString::fromStdString(j.value("name", std::string(""))); + break; + case Roles::commentRole: + result = mapFromValue(j.value("prop", R"({})"_json)); + break; + case Roles::startRole: + result = QVariant::fromValue(j.at("range").value("start", 0l)); + break; + case Roles::durationRole: + result = QVariant::fromValue(j.at("range").value("duration", 0l)); + break; + case Roles::rateRole: + result = QVariant::fromValue(j.at("range").value("rate", 0l)); + break; + case Roles::layerRole: { + auto layer = 0; + + for (auto i = 0; i < index.row(); i++) { + auto previ = MarkerModel::index(i, 0, index.parent()); + if (previ.data(startRole) == index.data(startRole)) + layer++; + } + result = layer; + } break; + default: + result = JSONTreeModel::data(index, role); + break; + } + } catch (const std::exception &err) { + // spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; +} + +bool MarkerModel::setData(const QModelIndex &index, const QVariant &value, int role) { + QVector roles({role}); + auto result = false; + + try { + auto &j = indexToData(index); + switch (role) { + + case Roles::nameRole: { + auto data = mapFromValue(value); + if (j["name"] != data) { + j["name"] = data; + result = true; + emit dataChanged(index, index, roles); + emit markerDataChanged(); + } + } break; + + case Roles::commentRole: { + auto data = mapFromValue(value); + if (j["prop"] != data) { + j["prop"] = data; + result = true; + emit dataChanged(index, index, roles); + emit markerDataChanged(); + } + } break; + + case Roles::flagRole: { + auto data = mapFromValue(value); + if (j["flag"] != data) { + j["flag"] = data; + result = true; + emit dataChanged(index, index, roles); + emit markerDataChanged(); + } + } break; + + default: + result = JSONTreeModel::setData(index, value, role); + break; + } + } catch (const std::exception &err) { + spdlog::warn( + "{} {} {} {}", + __PRETTY_FUNCTION__, + err.what(), + role, + StdFromQString(value.toString())); + } + + return result; +} + +QModelIndex MarkerModel::addMarker( + const int frame, + const double rate, + const QString &name, + const QString &flag, + const QVariant &metadata) { + auto result = QModelIndex(); + auto marker = + R"({"name": null, "prop": null,"range": null,"flag": null, "uuid":null})"_json; + + marker["uuid"] = utility::Uuid::generate(); + marker["name"] = StdFromQString(name); + marker["flag"] = StdFromQString(flag); + marker["prop"] = mapFromValue(metadata); + marker["range"] = utility::FrameRange( + utility::FrameRate(frame * timebase::to_flicks(1.0 / rate)), + utility::FrameRate(), + utility::FrameRate(1.0 / rate)); + + auto row = rowCount(); + if (insertRows(row, 1, QModelIndex(), marker)) { + result = index(row, 0, QModelIndex()); + } + + return result; +} + +void PropertyFollower::setTarget(QObject *target) { + + if (target_ != target) { + target_ = target; + emit targetChanged(); + if (target_) { + the_property_ = QQmlProperty(target_, property_name_); + the_property_.connectNotifySignal(this, SIGNAL(propertyValueChanged())); + } + } +} + +void PropertyFollower::setPropertyName(const QString propertyName) { + + if (property_name_ != propertyName) { + property_name_ = propertyName; + emit propertyNameChanged(); + if (target_) { + the_property_ = QQmlProperty(target_, property_name_); + the_property_.connectNotifySignal(this, SIGNAL(propertyValueChanged())); + } + } +} diff --git a/src/ui/qml/helper/src/include/helper_qml_export.h b/src/ui/qml/helper/src/include/helper_qml_export.h deleted file mode 100644 index 8f856285f..000000000 --- a/src/ui/qml/helper/src/include/helper_qml_export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef HELPER_QML_EXPORT_H -#define HELPER_QML_EXPORT_H - -#ifdef HELPER_QML_STATIC_DEFINE -# define HELPER_QML_EXPORT -# define HELPER_QML_NO_EXPORT -#else -# ifndef HELPER_QML_EXPORT -# ifdef helper_qml_EXPORTS - /* We are building this library */ -# define HELPER_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define HELPER_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef HELPER_QML_NO_EXPORT -# define HELPER_QML_NO_EXPORT -# endif -#endif - -#ifndef HELPER_QML_DEPRECATED -# define HELPER_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef HELPER_QML_DEPRECATED_EXPORT -# define HELPER_QML_DEPRECATED_EXPORT HELPER_QML_EXPORT HELPER_QML_DEPRECATED -#endif - -#ifndef HELPER_QML_DEPRECATED_NO_EXPORT -# define HELPER_QML_DEPRECATED_NO_EXPORT HELPER_QML_NO_EXPORT HELPER_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef HELPER_QML_NO_DEPRECATED -# define HELPER_QML_NO_DEPRECATED -# endif -#endif - -#endif /* HELPER_QML_EXPORT_H */ diff --git a/src/ui/qml/helper/src/json_tree_model_ui.cpp b/src/ui/qml/helper/src/json_tree_model_ui.cpp index 3197701a0..d51dbe7f2 100644 --- a/src/ui/qml/helper/src/json_tree_model_ui.cpp +++ b/src/ui/qml/helper/src/json_tree_model_ui.cpp @@ -4,6 +4,7 @@ #include "xstudio/ui/qml/json_tree_model_ui.hpp" #include "xstudio/utility/string_helpers.hpp" +#include "xstudio/utility/json_store_sync.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/ui/qml/helper_ui.hpp" @@ -66,7 +67,17 @@ JsonTree *JSONTreeModel::indexToTree(const QModelIndex &index) const { // validate pointer exists in our tree.. auto t = (utility::JsonTree *)index.internalPointer(); + // this might slow things down.. // if(not data_.contains(t)) { + // throw std::runtime_error( + // fmt::format( + // "indexToTree : Invalid pointer row {} col {} ptr {}", + // index.row(), + // index.column(), + // fmt::ptr(t)) + // ); + // } + // // dump_tree(data_); // // spdlog::warn("{}", tree_to_json(data_, "children").dump(2)); // throw std::runtime_error(fmt::format("indexToTree : Invalid pointer row {} col {} ptr @@ -97,9 +108,23 @@ QModelIndex JSONTreeModel::getPathIndex(const nlohmann::json::json_pointer &path return result; } -void JSONTreeModel::setModelData(const nlohmann::json &data) { +void JSONTreeModel::setModelDataBase(const nlohmann::json &data, const bool local) { beginResetModel(); + if (local and event_send_callback_) { + auto event = SyncEvent; + + event["redo"] = xstudio::utility::ResetEvent; + event["redo"]["id"] = model_id_; + event["redo"]["data"] = data; + + event["undo"] = xstudio::utility::ResetEvent; + event["undo"]["id"] = model_id_; + event["undo"]["data"] = tree_to_json(data_); + + event_send_callback_(event); + } + if (role_names_.empty()) setRoleNames(data); @@ -110,6 +135,8 @@ void JSONTreeModel::setModelData(const nlohmann::json &data) { emit jsonChanged(); } +void JSONTreeModel::setModelData(const nlohmann::json &data) { setModelDataBase(data, true); } + QVariant JSONTreeModel::get(const QModelIndex &item, const QString &role) const { return data(item, roleId(role)); } @@ -129,7 +156,7 @@ QModelIndex JSONTreeModel::search( auto result = QModelIndex(); START_SLOW_WATCHER() - QModelIndexList indexes = search_list(value, role, parent, start, 1); + QModelIndexList indexes = searchList(value, role, parent, start, 1); if (not indexes.empty()) result = indexes[0]; @@ -139,45 +166,45 @@ QModelIndex JSONTreeModel::search( return result; } -QModelIndexList JSONTreeModel::search_list( +QModelIndexList JSONTreeModel::searchList( const QVariant &value, const QString &role, const QModelIndex &parent, const int start, const int hits) { - return search_list(value, roleId(role), parent, start, hits); + return searchList(value, roleId(role), parent, start, hits); } -QModelIndex JSONTreeModel::search_recursive( +QModelIndex JSONTreeModel::searchRecursive( const QVariant &value, const QString &role, const QModelIndex &parent, const int start, const int depth) { - return search_recursive(value, roleId(role), parent, start, depth); + return searchRecursive(value, roleId(role), parent, start, depth); } -QModelIndexList JSONTreeModel::search_recursive_list( +QModelIndexList JSONTreeModel::searchRecursiveList( const QVariant &value, const QString &role, const QModelIndex &parent, const int start, const int hits, const int depth) { - return search_recursive_list(value, roleId(role), parent, start, hits, depth); + return searchRecursiveList(value, roleId(role), parent, start, hits, depth); } -QModelIndexList JSONTreeModel::search_list( +QModelIndexList JSONTreeModel::searchList( const QVariant &value, const int role, const QModelIndex &parent, const int start, const int hits) { - return search_recursive_list_base(value, role, parent, start, hits, 0); + return searchRecursiveListBase(value, role, parent, start, hits, 0); } -QModelIndex JSONTreeModel::search_recursive( +QModelIndex JSONTreeModel::searchRecursive( const QVariant &value, const int role, const QModelIndex &parent, @@ -188,7 +215,7 @@ QModelIndex JSONTreeModel::search_recursive( START_SLOW_WATCHER() - QModelIndexList indexes = search_recursive_list(value, role, parent, start, 1, depth); + QModelIndexList indexes = searchRecursiveList(value, role, parent, start, 1, depth); if (not indexes.empty()) result = indexes[0]; @@ -197,7 +224,7 @@ QModelIndex JSONTreeModel::search_recursive( return result; } -QModelIndexList JSONTreeModel::search_recursive_list_base( +QModelIndexList JSONTreeModel::searchRecursiveListBase( const QVariant &value, const int role, const QModelIndex &parent, @@ -205,6 +232,8 @@ QModelIndexList JSONTreeModel::search_recursive_list_base( const int hits, const int max_depth) { + auto tp = utility::clock::now(); + START_SLOW_WATCHER() QModelIndexList result = QAbstractItemModel::match( @@ -218,7 +247,7 @@ QModelIndexList JSONTreeModel::search_recursive_list_base( for (int i = start; i < rowCount(parent); i++) { auto chd = index(i, 0, parent); if (hasChildren(chd)) { - auto more_result = search_recursive_list( + auto more_result = searchRecursiveList( value, role, chd, 0, hits == -1 ? -1 : hits - result.size(), max_depth - 1); result.append(more_result); if (hits != -1 and result.size() >= hits) @@ -229,17 +258,32 @@ QModelIndexList JSONTreeModel::search_recursive_list_base( CHECK_SLOW_WATCHER() + if (std::chrono::duration_cast(utility::clock::now() - tp) + .count() > 100) { + spdlog::warn( + "searchRecursiveListBase: Slow search parent {} value {} role {} start {} hits {} " + "max_depth {} duration milliseconds {}\n", + mapFromValue(parent.data(Roles::JSONRole)).dump(2), + mapFromValue(value).dump(2), + StdFromQString(roleName(role)), + start, + hits, + max_depth, + std::chrono::duration_cast(utility::clock::now() - tp) + .count()); + } + return result; } -QModelIndexList JSONTreeModel::search_recursive_list( +QModelIndexList JSONTreeModel::searchRecursiveList( const QVariant &value, const int role, const QModelIndex &parent, const int start, const int hits, const int depth) { - return search_recursive_list_base(value, role, parent, start, hits, depth); + return searchRecursiveListBase(value, role, parent, start, hits, depth); } int JSONTreeModel::countExpandedChildren( @@ -267,6 +311,17 @@ int JSONTreeModel::countExpandedChildren( return count; } +void JSONTreeModel::fetchMoreWait(const QModelIndex &parent) { + if (canFetchMore(parent)) { + fetchMore(parent); + + while (canFetchMore(parent)) { + QCoreApplication::processEvents( + QEventLoop::WaitForMoreEvents | QEventLoop::ExcludeUserInputEvents, 50); + } + } +} + bool JSONTreeModel::canFetchMore(const QModelIndex &parent) const { auto result = false; @@ -286,12 +341,19 @@ bool JSONTreeModel::canFetchMore(const QModelIndex &parent) const { bool JSONTreeModel::hasChildren(const QModelIndex &parent) const { auto result = false; + // spdlog::warn("hasChildren"); + try { if (not parent.isValid() and not data_.empty()) { result = true; - } else { - auto node = indexToTree(parent); - result = not node->empty(); + } else if (parent.isValid()) { + const auto &jsn = indexToData(parent); + if (jsn.count(children_) and jsn.at(children_).is_null()) + result = true; + else { + auto node = indexToTree(parent); + result = not node->empty(); + } } } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -400,12 +462,21 @@ QVariant JSONTreeModel::data(const QModelIndex &index, int role) const { case Qt::DisplayRole: field = display_role_; break; + case Roles::idRole: + result = QVariant::fromValue(QUuidFromUuid(j.value("id", Uuid()))); + break; case Roles::JSONRole: result = QVariantMapFromJson(j); break; case Roles::JSONTextRole: result = QString::fromStdString(j.dump(2)); break; + case Roles::childCountRole: + result = rowCount(index); + break; + case Roles::JSONPathRole: + result = QString::fromStdString(getIndexPath(index).to_string()); + break; default: { auto id = role - Roles::LASTROLE; if (id >= 0 and id < static_cast(role_names_.size())) { @@ -419,7 +490,8 @@ QVariant JSONTreeModel::data(const QModelIndex &index, int role) const { result = mapFromValue(j.at(field)); } catch (const std::exception &err) { - spdlog::warn( + + spdlog::debug( "{} {} role: {}", __PRETTY_FUNCTION__, err.what(), @@ -460,25 +532,47 @@ bool JSONTreeModel::setData(const QModelIndex &index, const QVariant &value, int auto old_node = indexToTree(index); // remove old children - if (old_node->size()) { - emit beginRemoveRows(index, 0, old_node->size() - 1); - old_node->clear(); - emit endRemoveRows(); - } + if (not old_node->empty()) + removeRows(0, old_node->size(), index); // replace data.. - old_node->data() = new_node.data(); - // copy children - // this doesn't work.. - // need to invalidate/add surplus rows. - if (new_node.size()) { - emit beginInsertRows(index, 0, new_node.size() - 1); + // global replace of data ? + + baseSetDataAll(index, value); - old_node->splice(old_node->end(), new_node.base()); + // old_node->data() = new_node.data(); - emit endInsertRows(); + if (jval.contains(children_) and not jval.at(children_).empty()) + insertRowsData( + 0, jval.at(children_).size(), index, mapFromValue(jval.at(children_))); + + result = true; + roles.clear(); + } break; + case Roles::childrenRole: { + // more involved.. + nlohmann::json jval; + + if (std::string(value.typeName()) == "QJSValue") { + jval = nlohmann::json::parse( + QJsonDocument::fromVariant(value.value().toVariant()) + .toJson(QJsonDocument::Compact) + .constData()); + } else { + jval = nlohmann::json::parse(QJsonDocument::fromVariant(value) + .toJson(QJsonDocument::Compact) + .constData()); } + auto old_node = indexToTree(index); + + // remove old children + if (not old_node->empty()) + removeRows(0, old_node->size(), index); + + if (not jval.at(children_).empty()) + insertRowsData( + 0, jval.at(children_).size(), index, mapFromValue(jval.at(children_))); result = true; roles.clear(); @@ -491,9 +585,15 @@ bool JSONTreeModel::setData(const QModelIndex &index, const QVariant &value, int if (id >= 0 and id < static_cast(role_names_.size())) { field = role_names_.at(id); } - // if (j.count(field)) { - j[field] = mapFromValue(value); - result = true; + + if (j.count(field) && j[field].is_number_integer() && + value.canConvert(QMetaType::Int)) { + // preserving int types from being changed to doubles + j[field] = value.toInt(); + } else { + j[field] = mapFromValue(value); + } + result = true; //} } break; @@ -509,7 +609,206 @@ bool JSONTreeModel::setData(const QModelIndex &index, const QVariant &value, int return result; } -bool JSONTreeModel::removeRows(int row, int count, const QModelIndex &parent) { +bool JSONTreeModel::receiveEvent(const utility::JsonStore &event_pair) { + // is it an event for me ? + auto result = false; + + try { + const auto &event = event_pair.at("redo"); + + auto type = event.value("type", ""); + auto id = event.value("id", Uuid()); + + + // ignore reflected events + if (id != model_id_) { + if (type == "insert_rows") { + auto path = nlohmann::json::json_pointer(event.value("parent", "")); + auto index = getPathIndex(path); + if (index.isValid()) { + + result = baseInsertRows( + event.value("row", 0), + event.value("count", 0), + index, + event.value("data", nlohmann::json::array()), + false); + } + } else if (type == "insert") { + auto path = nlohmann::json::json_pointer(event.value("parent", "")); + auto index = getPathIndex(path); + if (index.isValid()) { + nlohmann::json &j = indexToData(index); + j[event.value("key", "")] = event.value("data", nlohmann::json()); + result = true; + emit dataChanged(index, index, QVector()); + } + } else if (type == "remove_rows") { + auto path = nlohmann::json::json_pointer(event.value("parent", "")); + auto index = getPathIndex(path); + result = baseRemoveRows( + event.value("row", 0), event.value("count", 0), index, false); + } else if (type == "remove") { + // remove a key from object. + auto path = nlohmann::json::json_pointer(event.value("parent", "")); + auto index = getPathIndex(path); + if (index.isValid()) { + nlohmann::json &j = indexToData(index); + j.erase(event.value("key", "")); + result = true; + emit dataChanged(index, index, QVector()); + } + } else if (type == "set") { + // find index.. + auto path = nlohmann::json::json_pointer(event.value("parent", "")); + path /= children_; + path /= std::to_string(event.value("row", 0)); + auto index = getPathIndex(path); + if (index.isValid()) { + auto data = event.value("data", nlohmann::json::object()); + + // we can't know the role.. + for (const auto &i : data.items()) + result |= baseSetData( + index, mapFromValue(i.value()), i.key(), QVector(), false); + } + } else if (type == "move") { + auto src_index = + getPathIndex(nlohmann::json::json_pointer(event.value("src_parent", ""))); + auto dst_index = + getPathIndex(nlohmann::json::json_pointer(event.value("dst_parent", ""))); + if (src_index.isValid() and dst_index.isValid()) { + result = baseMoveRows( + src_index, + event.value("src_row", 0), + event.value("count", 0), + dst_index, + event.value("dst_row", 0), + false); + } + } else if (type == "reset") { + setModelDataBase(event.value("data", nlohmann::json::object()), false); + result = true; + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; +} + +bool JSONTreeModel::baseSetDataAll( + const QModelIndex &index, const QVariant &value, const bool local) { + + auto result = false; + + try { + nlohmann::json &j = indexToData(index); + const auto v = mapFromValue(value); + + if (j != v) { + if (local and event_send_callback_) { + auto event = xstudio::utility::SyncEvent; + event["redo"] = xstudio::utility::SetEvent; + event["redo"]["id"] = model_id_; + event["redo"]["parent"] = + StdFromQString(index.parent().data(JSONPathRole).toString()); + event["redo"]["row"] = index.row(); + event["redo"]["data"] = v; + + event["undo"] = xstudio::utility::SetEvent; + event["undo"]["id"] = model_id_; + event["undo"]["parent"] = + StdFromQString(index.parent().data(JSONPathRole).toString()); + event["undo"]["row"] = index.row(); + event["undo"]["data"] = j; + + event_send_callback_(event); + } + + j = v; + result = true; + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + result = false; + } + + if (result) + emit dataChanged(index, index, QVector()); + + return result; +} + +bool JSONTreeModel::baseSetData( + const QModelIndex &index, + const QVariant &value, + const std::string &key, + QVector roles, + const bool local) { + + auto result = false; + + try { + nlohmann::json &j = indexToData(index); + const auto v = mapFromValue(value); + + if (j.count(key) and j.at(key) != v) { + if (local and event_send_callback_) { + auto event = xstudio::utility::SyncEvent; + event["redo"] = xstudio::utility::SetEvent; + event["redo"]["id"] = model_id_; + event["redo"]["parent"] = + StdFromQString(index.parent().data(JSONPathRole).toString()); + event["redo"]["row"] = index.row(); + event["redo"]["data"] = R"({})"_json; + event["redo"]["data"][key] = v; + + event["undo"] = xstudio::utility::SetEvent; + event["undo"]["id"] = model_id_; + event["undo"]["parent"] = + StdFromQString(index.parent().data(JSONPathRole).toString()); + event["undo"]["row"] = index.row(); + event["undo"]["data"] = R"({})"_json; + event["undo"]["data"][key] = j.at(key); + + event_send_callback_(event); + } + + j[key] = v; + result = true; + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + result = false; + } + + if (result) + emit dataChanged(index, index, roles); + + return result; +} + +bool JSONTreeModel::removeNodes(const int row, const int count, JsonTree *node) { + auto result = false; + if (node and (row + (count - 1)) < static_cast(node->size())) { + result = true; + + auto start = node->begin(); + auto end = start; + std::advance(start, row); + std::advance(end, row + count); + node->erase(start, end); + } + return result; +} + + +bool JSONTreeModel::baseRemoveRows( + int row, int count, const QModelIndex &parent, const bool local) { auto result = false; try { @@ -521,17 +820,36 @@ bool JSONTreeModel::removeRows(int row, int count, const QModelIndex &parent) { node = indexToTree(parent); if (node and (row + (count - 1)) < static_cast(node->size())) { - result = true; - auto start = node->begin(); - auto end = start; - std::advance(start, row); - std::advance(end, row + count); - - beginRemoveRows(parent, row, row + (count - 1)); + result = true; - node->erase(start, end); + // send event upstream + if (local and event_send_callback_) { + auto event = xstudio::utility::SyncEvent; + event["redo"] = xstudio::utility::RemoveRowsEvent; + event["redo"]["id"] = model_id_; + event["redo"]["parent"] = StdFromQString(parent.data(JSONPathRole).toString()); + event["redo"]["row"] = row; + event["redo"]["count"] = count; + + event["undo"] = xstudio::utility::InsertRowsEvent; + event["undo"]["id"] = model_id_; + event["undo"]["parent"] = event["redo"]["parent"]; + event["undo"]["row"] = row; + event["undo"]["count"] = count; + + auto undo_data = R"([])"_json; + for (auto i = 0; i < count; i++) + undo_data.push_back(tree_to_json(*(node->child(i + row)))); + event["undo"]["data"] = undo_data; + + event_send_callback_(event); + } + beginRemoveRows(parent, row, row + (count - 1)); + removeNodes(row, count, node); endRemoveRows(); + + emit dataChanged(parent, parent, {Roles::childCountRole}); } } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -548,19 +866,140 @@ bool JSONTreeModel::removeRows(int row, int count, const QModelIndex &parent) { return result; } + +bool JSONTreeModel::removeRows(int row, int count, const QModelIndex &parent) { + return baseRemoveRows(row, count, parent); +} bool JSONTreeModel::moveRows( const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild) { - auto result = false; + return baseMoveRows(sourceParent, sourceRow, count, destinationParent, destinationChild); +} - // spdlog::warn("moveRows"); +bool JSONTreeModel::moveNodes( + utility::JsonTree *src, int first_row, int last_row, utility::JsonTree *dst, int dst_row) { - // for(const auto &i: persistentIndexList()) { - // spdlog::warn("before {} {}", i.row(), getIndexPath(i.internalId()).to_string()); - // } + // clone data + std::list tmp; + + auto begin = src->begin(); + auto end = begin; + std::advance(begin, first_row); + std::advance(end, last_row + 1); + + tmp.splice(tmp.end(), src->base(), begin, end); + + auto dst_begin = dst->begin(); + std::advance(dst_begin, dst_row); + // insert into dst. + dst->splice(dst_begin, tmp); + + return true; +} + +bool JSONTreeModel::reorderRows( + const QModelIndex &parent, const std::vector &new_row_indeces) { + + // if we have rows in the model A,B,C,D,E and we are re-ordering to + // E,A,D,C,B then new_row_indeces should be [4,0,3,2,1] + + if (!parent.isValid()) { + spdlog::error("{} invalid index.", __PRETTY_FUNCTION__); + return false; + } + + auto tree = indexToTree(parent); + + if (new_row_indeces.size() != rowCount(parent)) { + spdlog::error( + "{}. {} indexes in re-ordered layout, expecting {}.", + __PRETTY_FUNCTION__, + new_row_indeces.size(), + rowCount(parent)); + return false; + } + + // See Qt docs on layout changes QtAbstractItemModel + emit layoutAboutToBeChanged( + QList{parent}, QAbstractItemModel::VerticalSortHint); + + // We need to make a list of the indeces that are going to be changing + // due to our re-ordering. + QModelIndexList from; + for (int dest_row = 0; dest_row < new_row_indeces.size(); ++dest_row) { + int src_row = new_row_indeces[dest_row]; + if (src_row == dest_row) + continue; + from.append(QPersistentModelIndex(index(src_row, 0, parent))); + } + + // we need a mutable copy of new_row_indeces + std::vector indeces_mapping = new_row_indeces; + + for (int dest_row = 0; dest_row < indeces_mapping.size(); ++dest_row) { + + int src_row = indeces_mapping[dest_row]; + + if (src_row == dest_row) + continue; + + + // move our internal data, one row at a time + moveNodes(tree, src_row, src_row, tree, dest_row); + + // each row move is the equivalent of removing a row from somewhere + // and inserting it somewhere else (dest_row) ... + + // because we have moved a row and inserted a row, we have changed + // the position of all the other rows that lie between the src and + // dest - this means we have to update the src rows in 'indeces_mapping' + // so it works for the current re-ordered state of the data + for (int j = dest_row + 1; j < indeces_mapping.size(); ++j) { + // looping over the remaining destination indeces ... + + if (indeces_mapping[j] >= dest_row) { + // the row we just moved has been inserted below the src + // row at destination j, so the src row needs to be incremented + indeces_mapping[j]++; + } + if (indeces_mapping[j] > src_row) { + // The row that we moved has been taken away from a position + // that is less than the src at row at j so decrement + indeces_mapping[j]--; + } + } + } + + // now we make a list of the updated model indexes for those rows that + // got moved + QModelIndexList to; + for (int dest_row = 0; dest_row < new_row_indeces.size(); ++dest_row) { + int src_row = new_row_indeces[dest_row]; + if (src_row == dest_row) + continue; + to.append(QPersistentModelIndex(index(dest_row, 0, parent))); + } + + // see Qt docs ... + changePersistentIndexList(from, to); + + emit layoutChanged( + QList{parent}, QAbstractItemModel::VerticalSortHint); + + return true; +} + +bool JSONTreeModel::baseMoveRows( + const QModelIndex &sourceParent, + int sourceRow, + int count, + const QModelIndex &destinationParent, + int destinationChild, + const bool local) { + auto result = false; if (destinationChild < 0 or count < 1 or sourceRow < 0) return false; @@ -569,6 +1008,7 @@ bool JSONTreeModel::moveRows( JsonTree *dst_children = nullptr; try { + // get src array if (sourceParent.isValid()) { src_children = indexToTree(sourceParent); @@ -595,37 +1035,60 @@ bool JSONTreeModel::moveRows( // spdlog::warn("{}-{} -> {} {}", moveFirst, moveLast, dest, sourceParent == // destinationParent); - // clone data - std::list tmp; - - auto begin = src_children->begin(); - auto end = begin; - std::advance(begin, moveFirst); - std::advance(end, moveLast + 1); + if (moveFirst < dest && sourceParent == destinationParent) + dest = std::max(0, dest - count); + // clone data + moveNodes(src_children, moveFirst, moveLast, dst_children, dest); + endMoveRows(); - tmp.splice(tmp.end(), src_children->base(), begin, end); + emit dataChanged(sourceParent, sourceParent, {Roles::childCountRole}); - // // remove from source. - // src_children->erase(begin, end); + if (sourceParent != destinationParent) { + emit dataChanged(destinationParent, destinationParent, {Roles::childCountRole}); + if (sourceParent == QModelIndex() or destinationParent == QModelIndex()) { + emit lengthChanged(); + } + } - if (moveFirst < dest) - dest = std::max(0, dest - count); + // send event upstream + if (local and event_send_callback_) { + auto event = SyncEvent; + auto src_path = StdFromQString(sourceParent.data(JSONPathRole).toString()); + auto dst_path = StdFromQString(destinationParent.data(JSONPathRole).toString()); + + // we handle moves differently... + // so we need to massage the data.. + event["redo"] = xstudio::utility::MoveEvent; + event["redo"]["id"] = model_id_; + event["redo"]["src_parent"] = src_path; + event["redo"]["src_row"] = sourceRow; + event["redo"]["count"] = count; + event["redo"]["dst_parent"] = dst_path; + + if (src_path == dst_path) { + if (destinationChild < sourceRow) + event["redo"]["dst_row"] = destinationChild; + else { + event["redo"]["dst_row"] = destinationChild - count; + } - auto dst_begin = dst_children->begin(); - std::advance(dst_begin, dest); - // insert into dst. - dst_children->splice(dst_begin, tmp); + // qt does wierd things here.. + } else { + // qt adds after index.. + event["redo"]["dst_row"] = destinationChild; + } - // spdlog::warn("{}", dst_children->dump(2)); + event["undo"] = xstudio::utility::MoveEvent; + event["undo"]["id"] = model_id_; + event["undo"]["src_parent"] = dst_path; + event["undo"]["src_row"] = event["redo"]["dst_row"]; + event["undo"]["count"] = count; + event["undo"]["dst_parent"] = src_path; + event["undo"]["dst_row"] = sourceRow; - endMoveRows(); - // rowsMovePersistent(sourceParent, - // moveFirst, - // moveLast, - // destinationParent, - // dest - // ); + event_send_callback_(event); + } } else { spdlog::warn( "{} invalid move: f {} l {} d {}", @@ -635,7 +1098,6 @@ bool JSONTreeModel::moveRows( dest); } - } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); result = false; @@ -644,15 +1106,39 @@ bool JSONTreeModel::moveRows( if (result) emit jsonChanged(); - // for(const auto &i: persistentIndexList()) { - // spdlog::warn("after {} {}", i.row(), getIndexPath(i.internalId()).to_string()); - // } + return result; +} + +bool JSONTreeModel::insertNodes( + const int row, const int count, utility::JsonTree *node, const nlohmann::json &data) { + auto result = false; + + if (node and row >= 0) { + result = true; + auto begin = node->begin(); + std::advance(begin, row); + + if (data.is_array()) { + for (auto i = 0; i < count; i++) { + node->insert(begin, json_to_tree(data.at(i), children_)); + } + } else { + for (auto i = 0; i < count; i++) { + node->insert(begin, json_to_tree(data, children_)); + } + } + } return result; } -bool JSONTreeModel::insertRows( - int row, int count, const QModelIndex &parent, const nlohmann::json &data) { + +bool JSONTreeModel::baseInsertRows( + int row, + int count, + const QModelIndex &parent, + const nlohmann::json &data, + const bool local) { auto result = false; // spdlog::warn("JSONTreeModel::insertRows {}", data.dump(2)); @@ -666,28 +1152,43 @@ bool JSONTreeModel::insertRows( node = indexToTree(parent); if (node and row >= 0) { - if (row < static_cast(node->size())) { - result = true; - auto begin = node->begin(); - std::advance(begin, row); - beginInsertRows(parent, row, row + count - 1); - for (auto i = 0; i < count; i++) { - node->insert(begin, data); - } - endInsertRows(); - } else { - result = true; - row = static_cast(node->size()); - auto begin = node->begin(); - std::advance(begin, row); - beginInsertRows(parent, row, row + count - 1); - - for (auto i = 0; i < count; i++) { - node->insert(begin, data); - } + if (row >= static_cast(node->size())) + row = static_cast(node->size()); + + beginInsertRows(parent, row, row + count - 1); + result = insertNodes(row, count, node, data); + endInsertRows(); - endInsertRows(); + emit dataChanged(parent, parent, {Roles::childCountRole}); + + // send event + if (local and event_send_callback_) { + auto parent_path = StdFromQString(parent.data(JSONPathRole).toString()); + + auto tweak_data = R"([])"_json; + if (data.is_array()) + tweak_data = data; + else if (not data.empty()) + tweak_data[0] = data; + + auto event = SyncEvent; + + event["redo"] = xstudio::utility::InsertRowsEvent; + event["redo"]["id"] = model_id_; + event["redo"]["parent"] = parent_path; + event["redo"]["row"] = row; + event["redo"]["count"] = count; + event["redo"]["data"] = tweak_data; + + event["undo"] = xstudio::utility::RemoveRowsEvent; + event["undo"]["id"] = model_id_; + event["undo"]["parent"] = parent_path; + event["undo"]["row"] = row; + event["undo"]["count"] = count; + + event_send_callback_(event); } + } else { spdlog::warn("ERM"); } @@ -706,9 +1207,26 @@ bool JSONTreeModel::insertRows( return result; } +bool JSONTreeModel::insertRows( + int row, int count, const QModelIndex &parent, const nlohmann::json &data) { + return baseInsertRows(row, count, parent, data); +} + +bool JSONTreeModel::insertRowsData( + int row, int count, const QModelIndex &parent, const QVariant &data) { + return insertRows(row, count, parent, mapFromValue(data)); +} + bool JSONTreeModel::insertRows(int row, int count, const QModelIndex &parent) { - return insertRows(row, count, parent, R"({})"_json); + return baseInsertRows(row, count, parent, R"({})"_json); +} + + +void JSONTreeModel::bindEventFunc(JSONTreeSendEventFunc fs) { + event_send_callback_ = [fs](auto &&PH1) { return fs(std::forward(PH1)); }; + + model_id_ = Uuid::generate(); } @@ -781,22 +1299,26 @@ QVariant JSONTreeFilterModel::get(const QModelIndex &item, const QString &role) bool JSONTreeFilterModel::filterAcceptsRow( int source_row, const QModelIndex &source_parent) const { // check level - if (not roleFilterMap_.empty() and sourceModel()) { QModelIndex index = sourceModel()->index(source_row, 0, source_parent); for (const auto &[k, v] : roleFilterMap_) { if (not v.isNull()) { try { auto qv = sourceModel()->data(index, k); - if (v.userType() == QMetaType::Bool) - if (v.toBool() != qv.toBool()) - return false; - else if (v != qv) - return false; + if (v.type() == qv.type() && v == qv) + return invert_ ? false : true; + else if (v.canConvert(QMetaType::QVariantList)) { + const auto vl = v.toList(); + for (const auto &vli : vl) { + if (qv.type() == vli.type() && qv == vli) + return invert_ ? false : true; + } + } } catch (...) { } } } + return invert_ ? true : false; } return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } @@ -819,4 +1341,11 @@ void JSONTreeFilterModel::setRoleFilter(const QVariant &filter, const QString &r } } -// #include "json_tree_model_ui.moc" + +// QModelIndex JSONTreeListModel::mapFromSource(const QModelIndex &sourceIndex) const { +// return index(); +// } + +// QModelIndex JSONTreeListModel::mapToSource(const QModelIndex &proxyIndex) const { +// return proxyIndex; +// } diff --git a/src/ui/qml/helper/src/model_data_ui.cpp b/src/ui/qml/helper/src/model_data_ui.cpp index 0f024fe36..b5afccd85 100644 --- a/src/ui/qml/helper/src/model_data_ui.cpp +++ b/src/ui/qml/helper/src/model_data_ui.cpp @@ -1,11 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 #include #include +#include +#include "xstudio/global_store/global_store.hpp" #include "xstudio/ui/qml/model_data_ui.hpp" #include "xstudio/utility/string_helpers.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/ui/qml/helper_ui.hpp" +#include "xstudio/ui/qml/session_model_ui.hpp" +#include "xstudio/ui/qml/QTreeModelToTableModel.hpp" #include "xstudio/global_store/global_store.hpp" @@ -16,11 +20,18 @@ using namespace std::chrono_literals; UIModelData::UIModelData( - QObject *parent, const std::string &model_name, const std::string &data_preference_path) + QObject *parent, + const std::string &model_name, + const std::string &data_preference_path, + const std::vector &role_names) : super(parent), model_name_(model_name), data_preference_path_(data_preference_path) { init(CafSystemObject::get_actor_system()); + if (!role_names.empty()) { + setRoleNames(role_names); + } + try { central_models_data_actor_ = @@ -33,7 +44,7 @@ UIModelData::UIModelData( central_models_data_actor_, ui::model_data::register_model_data_atom_v, model_name, - data_preference_path, + data_preference_path_, as_actor()); setModelData(data); @@ -43,6 +54,10 @@ UIModelData::UIModelData( } } + +std::mutex UIModelData::mutex_; +QMap UIModelData::context_object_lookup_map_; + UIModelData::UIModelData(QObject *parent) : super(parent) { init(CafSystemObject::get_actor_system()); @@ -56,14 +71,45 @@ void UIModelData::setModelDataName(QString name) { std::string m_name = StdFromQString(name); if (model_name_ != m_name) { + if (model_name_ != "") { + // stops us from watching the old model + anon_mail(ui::model_data::register_model_data_atom_v, model_name_, true, as_actor()) + .send(central_models_data_actor_); + } + model_name_ = m_name; - anon_send( - central_models_data_actor_, - ui::model_data::register_model_data_atom_v, - model_name_, - utility::JsonStore(nlohmann::json::parse("{}")), - as_actor()); + caf::scoped_actor sys{self()->home_system()}; + + // we send empty data to 'register' but if the model already exists + // we'll be sent back what's already in the model + if (data_preference_path_.empty()) { + auto data = request_receive( + *sys, + central_models_data_actor_, + ui::model_data::register_model_data_atom_v, + model_name_, + utility::JsonStore(nlohmann::json::parse("{}")), + as_actor()); + + // now we update with the returned model data + setModelData(data); + + } else { + + auto data = request_receive( + *sys, + central_models_data_actor_, + ui::model_data::register_model_data_atom_v, + model_name_, + data_preference_path_, + utility::JsonStore(modelData()), + as_actor()); + + // now we update with the returned model data + setModelData(data); + } + emit lengthChanged(); // process app/user.. emit modelDataNameChanged(); @@ -73,8 +119,6 @@ void UIModelData::setModelDataName(QString name) { void UIModelData::init(caf::actor_system &system) { super::init(system); - self()->set_default_handler(caf::drop); - try { utility::print_on_create(as_actor(), "UIModelData"); } catch (const std::exception &err) { @@ -90,13 +134,13 @@ void UIModelData::init(caf::actor_system &system) { const std::string path, const utility::JsonStore &data) { try { - - QModelIndex idx = getPathIndex(nlohmann::json::json_pointer(path)); - nlohmann::json &j = indexToData(idx); - j = data; - emit dataChanged(idx, idx, QVector()); + const std::string filtered_path = apply_filter(path); + if (filtered_path == "") + return; + QModelIndex idx = getPathIndex(nlohmann::json::json_pointer(filtered_path)); + JSONTreeModel::setData(idx, QVariantMapFromJson(data), Roles::JSONRole); } catch (std::exception &e) { - spdlog::warn( + spdlog::debug( "{} {} : {} {} {}", __PRETTY_FUNCTION__, e.what(), @@ -114,19 +158,20 @@ void UIModelData::init(caf::actor_system &system) { const utility::Uuid &uuid) { try { - QModelIndex idx = getPathIndex(nlohmann::json::json_pointer(path)); + const std::string filtered_path = apply_filter(path); + if (filtered_path == "") + return; + QModelIndex idx = getPathIndex(nlohmann::json::json_pointer(filtered_path)); nlohmann::json &j = indexToData(idx); if (!j.contains(role) || j[role] != data) { j[role] = data; for (size_t i = 0; i < role_names_.size(); ++i) { if (role_names_[i] == role) { - emit dataChanged( idx, idx, QVector({static_cast( Roles::LASTROLE + static_cast(i))})); - break; } } @@ -154,18 +199,31 @@ void UIModelData::init(caf::actor_system &system) { // process app/user.. setModelData(data); } else { - // suppressing this warning, because you can get it when it's not - // a problem if this node gets a set_node_data message before the - // given node has been added. The backend model is all fine, but - // we can get a bit out of sync here and it's no big deal. - spdlog::debug("{} {} {}", __PRETTY_FUNCTION__, e.what(), path); + spdlog::debug( + "{} {} {} {} {} {} {}", + __PRETTY_FUNCTION__, + e.what(), + path, + model_name_, + model_name, + data.dump(), + role); } } }, [=](utility::event_atom, xstudio::ui::model_data::model_data_atom, const std::string model_name, - const utility::JsonStore &data) { setModelData(data); }}; + const utility::JsonStore &data) -> bool { + if (debug_) { + debug_ = false; + } + if (model_name == model_name_) { + setModelData(data); + } + return true; + }, + [=](caf::message) {}}; }); } @@ -193,12 +251,12 @@ bool UIModelData::setData(const QModelIndex &index, const QVariant &value, int r .constData()); } - anon_send( - central_models_data_actor_, + anon_mail( xstudio::ui::model_data::set_node_data_atom_v, model_name_, - path, - j); + reverse_apply_filter(path), + j) + .send(central_models_data_actor_); } else { @@ -207,18 +265,20 @@ bool UIModelData::setData(const QModelIndex &index, const QVariant &value, int r auto id = role - Roles::LASTROLE; if (id >= 0 and id < static_cast(role_names_.size())) { - auto field = role_names_.at(id); - anon_send( - central_models_data_actor_, + auto field = role_names_.at(id); + nlohmann::json &j = indexToData(index); + anon_mail( xstudio::ui::model_data::set_node_data_atom_v, model_name_, - path, - utility::JsonStore(mapFromValue(value)), - field); + reverse_apply_filter(path), + utility::JsonStore(j[field]), + field) + .send(central_models_data_actor_); } } } catch (const std::exception &err) { + auto id = role - Roles::LASTROLE; spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -232,14 +292,13 @@ bool UIModelData::removeRows(int row, int count, const QModelIndex &parent) { try { auto path = getIndexPath(parent).to_string(); - - anon_send( - central_models_data_actor_, + anon_mail( xstudio::ui::model_data::remove_rows_atom_v, model_name_, - path, + reverse_apply_filter(path), row, - count); + count) + .send(central_models_data_actor_); result = true; @@ -256,16 +315,18 @@ bool UIModelData::removeRowsSync(int row, int count, const QModelIndex &parent) try { + auto path = getIndexPath(parent).to_string(); - anon_send( - central_models_data_actor_, - xstudio::ui::model_data::remove_rows_atom_v, - model_name_, - path, - row, - count, - false); + auto a = caf::actor_cast(self()); + a->mail( + xstudio::ui::model_data::remove_rows_atom_v, + model_name_, + reverse_apply_filter(path), + row, + count, + false) + .send(central_models_data_actor_); result = JSONTreeModel::removeRows(row, count, parent); @@ -284,47 +345,27 @@ bool UIModelData::moveRows( const QModelIndex &destinationParent, int destinationChild) { - // TODO!! - - /*if (destinationChild < 0 or count < 1 or sourceRow < 0) - return false; + auto result = false; try { - nlohmann::json *src_children = nullptr; - - // get src array - if (sourceParent.isValid()) { - nlohmann::json &j = indexToData(sourceParent); - if (j.contains(children_)) - src_children = &(j.at(children_)); - } else if (not sourceParent.isValid()) { - src_children = &data_; - } - // check valid move.. - auto moveLast = sourceRow + count - 1; + result = JSONTreeModel::moveRows( + sourceParent, sourceRow, count, destinationParent, destinationChild); - if (moveLast >= static_cast(src_children->size())) - return false; - - auto sourceParentPath = getIndexPath(sourceParent.internalId()).to_string(); - auto destinationParentPath = getIndexPath(destinationParent.internalId()).to_string(); - - anon_send( - central_models_data_actor_, - xstudio::ui::model_data::model_data_atom_v, - utility::Uuid("8f399658-d2ba-4e62-8887-64fc4c6b7a17"), - sourceParentPath, - sourceRow, - count, - destinationParentPath, - destinationChild); + // we send the whole data set to the central models data store as + // move rows hasn't been properly implemented on that backend + auto a = caf::actor_cast(self()); + a->mail( + xstudio::ui::model_data::reset_model_atom_v, + model_name_, + utility::JsonStore(modelData()), + false) + .send(central_models_data_actor_); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - return false; - }*/ - + result = false; + } return false; } @@ -339,14 +380,14 @@ bool UIModelData::insertRows(int row, int count, const QModelIndex &parent) { auto bod = R"({})"; - anon_send( - central_models_data_actor_, + anon_mail( xstudio::ui::model_data::insert_rows_atom_v, model_name_, - path, + reverse_apply_filter(path), row, count, - utility::JsonStore(nlohmann::json::parse(bod))); + utility::JsonStore(nlohmann::json::parse(bod))) + .send(central_models_data_actor_); result = true; @@ -370,15 +411,16 @@ bool UIModelData::insertRowsSync(int row, int count, const QModelIndex &parent) // update the backend model, but don't broadcast the change back to // this instance of UIModelData, because we are going to update our // own local copy of the model data - anon_send( - central_models_data_actor_, - xstudio::ui::model_data::insert_rows_atom_v, - model_name_, - path, - row, - count, - utility::JsonStore(nlohmann::json::parse(bod)), - false); + auto a = caf::actor_cast(self()); + a->mail( + xstudio::ui::model_data::insert_rows_atom_v, + model_name_, + reverse_apply_filter(path), + row, + count, + utility::JsonStore(nlohmann::json::parse(bod)), + false) + .send(central_models_data_actor_); // here we update our own result = JSONTreeModel::insertRows(row, count, parent); @@ -389,24 +431,93 @@ bool UIModelData::insertRowsSync(int row, int count, const QModelIndex &parent) return result; } -void UIModelData::nodeActivated(const QModelIndex &idx) { +void UIModelData::nodeActivated( + const QModelIndex &idx, const QString &data, QObject *context_panel) { try { auto path = getIndexPath(idx).to_string(); + if (context_panel) { + add_context_object_lookup(context_panel); + } // Tell the backend item that a node in the model has been 'activated' - anon_send( - central_models_data_actor_, + anon_mail( xstudio::ui::model_data::menu_node_activated_atom_v, model_name_, - path); + reverse_apply_filter(path), + StdFromQString(Helpers::objPtrTostring(context_panel))) + .send(central_models_data_actor_); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } } +std::string UIModelData::apply_filter(const std::string &path) const { + + // this 'filter' is a way of reducing a model that is some set of + // rows (with only a depth of 1 to a single row). This filtered_child_idx_ + // gives us the index of the row that we're interested in. + // + // This is useful when we have a model that has lots of rows (like a set + // of attributes) but we're just interested in a single attribute (so we + // can create a UI widget to control just that attribute) instead of + // repeating over all of the attributes in the set. + // + // look for 'singleAttributeName' in the QML UI code to see where this + // is used. + + if (filtered_child_idx_ != -1) { + // when we get here it's because the backend is sending us an update + // about some row in the model. Our own clone of the model has been + // reduced to one row, however. First check that the row being modified + // is the one that we have filtered down to. + if (fmt::format("/children/{}", filtered_child_idx_) != path) { + return std::string(""); + } + // now we make path valid for our own filtered model data, which is + // simple as we just have one row + return std::string("/children/0"); + } + return path; +} + +std::string UIModelData::reverse_apply_filter(const std::string &path) const { + + // see notes above. + if (filtered_child_idx_ != -1) { + // we use the reverse filter to change the json path to the row in our + // local (filtered) data to the json path for the whole model data that + // exists in the backend GlobalUIModelData class + return fmt::format("/children/{}", filtered_child_idx_); + ; + } + return path; +} + +void UIModelData::restoreDefaults() { + + anon_mail(xstudio::ui::model_data::reset_model_atom_v, model_name_, data_preference_path_) + .send(central_models_data_actor_); +} + +void UIModelData::add_context_object_lookup(QObject *obj) { + std::unique_lock l(mutex_); + context_object_lookup_map_[Helpers::objPtrTostring(obj)] = obj; +} + +QObject *UIModelData::context_object_lookup(const QString &obj_str) { + + std::unique_lock l(mutex_); + auto p = context_object_lookup_map_.find(obj_str); + if (p != context_object_lookup_map_.end()) { + return p.value(); + } + return nullptr; +} + + MenusModelData::MenusModelData(QObject *parent) : UIModelData(parent) { setRoleNames(std::vector{ "uuid", @@ -415,24 +526,53 @@ MenusModelData::MenusModelData(QObject *parent) : UIModelData(parent) { "name", "is_checked", "choices", + "choices_ids", "current_choice", - "hotkey"}); + "combo_box_options_enabled", + "hotkey_uuid", + "menu_icon", + "custom_menu_qml", + "user_data", + "hotkey_sequence", + "menu_item_enabled", + "menu_item_context", + "watch_visibility"}); } ViewsModelData::ViewsModelData(QObject *parent) : UIModelData(parent) { - setRoleNames(std::vector{"view_name", "view_qml_source"}); + setRoleNames(std::vector{ + "view_name", "position", "view_qml_source", "singleton_qml_source"}); setModelDataName("view widgets"); + + // make the rows in the model order by the 'button_position' role + anon_mail(ui::model_data::set_row_ordering_role_atom_v, "view widgets", "position") + .send(central_models_data_actor_); } -void ViewsModelData::register_view(QString qml_path, QString view_name) { +void ViewsModelData::register_view(QString qml_path, QString view_name, const float position) { - auto rc = rowCount(index(-1, -1)); // QModelIndex()); + /*auto rc = rowCount(index(-1, -1)); // QModelIndex()); insertRowsSync(rc, 1, index(-1, -1)); QModelIndex view_reg_index = index(rc, 0, index(-1, -1)); std::ignore = set(view_reg_index, QVariant(view_name), QString("view_name")); std::ignore = set(view_reg_index, QVariant(qml_path), QString("view_qml_source")); + std::ignore = set(view_reg_index, QVariant(position), QString("position"));*/ + + utility::JsonStore data; + data["view_name"] = StdFromQString(view_name); + data["view_qml_source"] = StdFromQString(qml_path); + data["position"] = position; + anon_mail( + ui::model_data::insert_rows_atom_v, + "view widgets", // the model called 'view widgets' is what's used to build the + // panels menu + "", // (path) add to root + 0, // row + 1, // count + data) + .send(central_models_data_actor_); } QVariant ViewsModelData::view_qml_source(QString view_name) { @@ -444,13 +584,96 @@ QVariant ViewsModelData::view_qml_source(QString view_name) { return QVariant(); } -ReskinPanelsModel::ReskinPanelsModel(QObject *parent) +PopoutWindowsData::PopoutWindowsData(QObject *parent) : UIModelData(parent) { + + setRoleNames(std::vector{ + "view_name", + "view_qml_source", + "icon_path", + "button_position", + "window_is_visible", + "user_data", + "hotkey_uuid"}); + + setModelDataName("popout windows"); + + // make the rows in the model order by the 'button_position' role + anon_mail(ui::model_data::set_row_ordering_role_atom_v, "popout windows", "button_position") + .send(central_models_data_actor_); +} + +void PopoutWindowsData::register_popout_window( + QString name, + QString qml_path, + QString icon_path, + float button_position, + const QUuid hotkey) { + + utility::JsonStore data; + data["view_name"] = StdFromQString(name); + data["icon_path"] = StdFromQString(icon_path); + data["view_qml_source"] = StdFromQString(qml_path); + data["button_position"] = button_position; + data["window_is_visible"] = false; + if (!hotkey.isNull()) { + data["hotkey_uuid"] = UuidFromQUuid(hotkey); + } + + try { + + anon_mail( + ui::model_data::insert_rows_atom_v, + "popout windows", + "", // (path) add to root + 0, // row + 1, // count + data) + .send(central_models_data_actor_); + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + + +SingletonsModelData::SingletonsModelData(QObject *parent) : UIModelData(parent) { + + setRoleNames(std::vector{"source"}); + setModelDataName("singleton items"); +} + +void SingletonsModelData::register_singleton_qml(const QString &qml_code) { + + int rc = rowCount(); + insertRowsSync(rc, 1); + QModelIndex idx = index(rc, 0); + std::ignore = set(idx, qml_code, "source"); +} + +PanelsModel::PanelsModel(QObject *parent) : UIModelData( parent, - std::string("reskin panels model"), - std::string("/ui/qml/reskin_windows_and_panels_model")) {} - -void ReskinPanelsModel::close_panel(QModelIndex panel_index) { + std::string("panels model"), + std::string("/ui/qml/reskin_windows_and_panels_model"), + std::vector{ + "window_name", + "width", + "height", + "current_layout", + "position_x", + "position_y", + "children", + "layout_name", + "enabled", + "split_horizontal", + "child_dividers", + "current_tab", + "tab_view", + "tabs_hidden", + "user_data"}) {} + + +void PanelsModel::close_panel(QModelIndex panel_index) { // Logic for closing a 'panel' is not trivial. Panels are hosted in // 'splitters' which chop up the window area into resizable sections @@ -465,8 +688,6 @@ void ReskinPanelsModel::close_panel(QModelIndex panel_index) { if (siblings > 2) { - removeRows(panel_index.row(), 1, panel_index.parent()); - // get the divider positions from the parent QVariant dividers = get(panel_index.parent(), "child_dividers"); if (dividers.userType() == QMetaType::QVariantList) { @@ -474,20 +695,22 @@ void ReskinPanelsModel::close_panel(QModelIndex panel_index) { divs.removeAt(panel_index.row() ? panel_index.row() - 1 : 0); std::ignore = set(panel_index.parent(), divs, "child_dividers"); } + removeRowsSync(panel_index.row(), 1, panel_index.parent()); } else if (siblings == 2) { - QModelIndex parentNode = panel_index.parent(); + const QModelIndex parentIdx = panel_index.parent(); + const int parentRow = parentIdx.row(); + // delete the panel + removeRowsSync(panel_index.row(), 1, parentIdx); - // get the json data about the other panel that's not being deleted - nlohmann::json other_panel_data = indexToFullData(index( - !panel_index - .row(), // we have two rows ... we want the OTHER row to the one being removed - 0, - parentNode)); + // move the sibling panel (which is now at row 0) up one level to + // replace the parent panel + moveRows(parentIdx, 0, 1, parentIdx.parent(), parentIdx.row()); - // now we wipe out the parent splitter with the 'other' panel - setData(parentNode, QVariantMapFromJson(other_panel_data), Roles::JSONRole); + // now remove the empty parent (whose row has gone up by one due to + // the moveRows above) + removeRowsSync(parentRow + 1, 1, parentIdx.parent()); } else { @@ -496,9 +719,37 @@ void ReskinPanelsModel::close_panel(QModelIndex panel_index) { } } -void ReskinPanelsModel::split_panel(QModelIndex panel_index, bool horizontal_split) { +int PanelsModel::add_layout(QString layout_name, QModelIndex root, QString tabType) { + + // make a node at the root which is the new layout. Note we always insert + // at the end so it shows leftmost in the list of layouts in the UI + int rc = rowCount(root); + insertRowsSync(rc, 1, root); + QModelIndex layoutIndex = index(rc, 0, root); + + // now make a node one layer deeper which is the single panel filling the + // new layout + insertRowsSync(0, 1, layoutIndex); + QModelIndex panelIndex = index(0, 0, layoutIndex); + + // now make a node one layer deeper which is the tabs group inside the panel + insertRowsSync(0, 1, panelIndex); + QModelIndex tab_index = index(0, 0, panelIndex); + + // insert a default tab + std::ignore = set(tab_index, tabType, "tab_view"); + std::ignore = set(panelIndex, 0, "current_tab"); + + // Set the layout name + std::ignore = set(layoutIndex, layout_name, "layout_name"); + std::ignore = set(layoutIndex, true, "enabled"); + + return rc; +} + +void PanelsModel::split_panel(QModelIndex panel_index, bool horizontal_split) { - QModelIndex parentNode = panel_index.parent(); + QPersistentModelIndex parentNode(panel_index.parent()); const int insertion_row = panel_index.row(); QVariant h = get(parentNode, "split_horizontal"); @@ -516,59 +767,102 @@ void ReskinPanelsModel::split_panel(QModelIndex panel_index, bool horizontal_spl } std::ignore = set(parentNode, divider_positions, "child_dividers"); - // do the insertion - nlohmann::json j; - j["current_tab"] = 0; - j["children"] = nlohmann::json::parse(R"([{"tab_view" : "Playlists"}])"); + // do the insertion of the new panel + insertRowsSync(insertion_row + 1, 1, parentNode); - insertRowsSync(insertion_row, 1, parentNode); - setData(index(insertion_row, 0, parentNode), QVariantMapFromJson(j), Roles::JSONRole); + // now make a node one layer deeper which is the tabs group for the + // new panel + insertRowsSync(0, 1, index(insertion_row + 1, 0, parentNode)); + QModelIndex new_node_index = index(0, 0, index(insertion_row + 1, 0, parentNode)); + + // insert a defaul tab + std::ignore = set(new_node_index, "Playlists", "tab_view"); + std::ignore = set(new_node_index, 0, "current_tab"); } else { - nlohmann::json current_panel_data = indexToFullData(panel_index); + // insert a new node at the same level as the panel being split + insertRowsSync(insertion_row + 1, 1, parentNode); + + // move the existing panel down one level, so it is a child node of + // the node position that it started at + moveRows(parentNode, insertion_row, 1, index(insertion_row + 1, 0, parentNode), 0); + + QModelIndex new_tabs_group_index = index(insertion_row, 0, parentNode); - // parent splitter type does not match the type of split we want - // so we need to replace the current panel with a new slitter - // of the desired type + std::ignore = + set(index(insertion_row, 0, parentNode), horizontal_split, "split_horizontal"); + std::ignore = + set(index(insertion_row, 0, parentNode), + QVariantList({QVariant(0.51111f)}), + "child_dividers"); - // this is the data of the new splitter - new divider position is - // at 0.5 - nlohmann::json j; - j["child_dividers"] = nlohmann::json::parse(R"([0.5])"); - j["split_horizontal"] = horizontal_split; + // now add another child which is our tabs group + insertRowsSync(1, 1, new_tabs_group_index); - // this is the new panel - nlohmann::json new_child; - new_child["current_tab"] = 0; - new_child["children"] = nlohmann::json::parse(R"([{"tab_view" : "Playlists"}])"); + // and now add another child on level deeper which is our actual tab + QModelIndex new_sub_tabs_group_index = index(1, 0, new_tabs_group_index); + insertRowsSync(0, 1, new_sub_tabs_group_index); - // add the existing panel and new panel to the new splitter - j["children"] = nlohmann::json::array(); - j["children"].push_back(current_panel_data); - j["children"].push_back(new_child); + // set the 'view' that our tab contains + std::ignore = set(index(0, 0, new_sub_tabs_group_index), "Playlists", "tab_view"); - // wipe out the existing panel with the new splitter and its children - setData(panel_index, QVariantMapFromJson(j), Roles::JSONRole); + // wet the index of the current tag + std::ignore = set(new_sub_tabs_group_index, 0, "current_tab"); } } -void ReskinPanelsModel::duplicate_layout(QModelIndex layout_index) { +QModelIndex PanelsModel::duplicate_layout(QModelIndex layout_index) { nlohmann::json layout_data = indexToFullData(layout_index); int rc = rowCount(layout_index.parent()); insertRowsSync(rc, 1, layout_index.parent()); QModelIndex idx = index(rc, 0, layout_index.parent()); setData(idx, QVariantMapFromJson(layout_data), Roles::JSONRole); + return idx; } +void PanelsModel::storeFloatingWindowData(QString window_name, QVariant data) { + + QModelIndex idx = searchRecursive(window_name, QString("window_name")); + if (!idx.isValid()) { + int rc = rowCount(); + insertRowsSync(rc, 1); + idx = index(rc, 0); + set(idx, window_name, "window_name"); + } + set(idx, data, "user_data"); +} + +QVariant PanelsModel::retrieveFloatingWindowData(QString window_name) { + QVariant result; + QModelIndex idx = searchRecursive(window_name, QString("window_name")); + if (idx.isValid()) { + result = get(idx, "user_data"); + } + return result; +} MediaListColumnsModel::MediaListColumnsModel(QObject *parent) : UIModelData( parent, - std::string("media list columns model"), - std::string("/ui/qml/media_list_columns_config")) {} + "media metata exposure model", + std::string("/ui/qml/media_list_columns_config")) { + setRoleNames(std::vector{ + "title", + "metadata_path", + "data_type", + "size", + "object", + "resizable", + "sortable", + "position", + "uuid", + "info_key", + "regex_match", + "regex_format"}); +} MenuModelItem::MenuModelItem(QObject *parent) : super(parent) { init(CafSystemObject::get_actor_system()); @@ -576,24 +870,16 @@ MenuModelItem::MenuModelItem(QObject *parent) : super(parent) { MenuModelItem::~MenuModelItem() { - auto central_models_data_actor = self()->home_system().registry().template get( - global_ui_model_data_registry); - - if (central_models_data_actor) { - std::string menu_name_string = StdFromQString(menu_name_); - anon_send( - central_models_data_actor, - ui::model_data::remove_node_atom_v, - menu_name_string, - model_entry_id_); - } + // TODO: Big optimisations required. Printing debug out here shows that + // many of these are created and destroyed when xstudio starts up. Something + // is not working the way we want! + // std::cerr << "OUT " << to_string(as_actor()) << " " << StdFromQString(menu_name_) << + // "\n"; } void MenuModelItem::init(caf::actor_system &system) { super::init(system); - self()->set_default_handler(caf::drop); - try { utility::print_on_create(as_actor(), "MenuModelItem"); } catch (const std::exception &err) { @@ -602,9 +888,21 @@ void MenuModelItem::init(caf::actor_system &system) { set_message_handler([=](caf::actor_companion * /*self*/) -> caf::message_handler { return { + [=](utility::event_atom, xstudio::ui::model_data::menu_node_activated_atom, - const std::string path) { emit activated(); }, + const std::string path, + const utility::JsonStore &menu_item_data, + const std::string &user_data, // for hotkeys this is the 'context' + const bool fromHotkey) { + if (fromHotkey) { + emit hotkeyActivated(); + } else { + QObject *context = + UIModelData::context_object_lookup(QStringFromStd(user_data)); + emit activated(context); + } + }, [=](utility::event_atom, xstudio::ui::model_data::set_node_data_atom, const std::string model_name, @@ -619,7 +917,8 @@ void MenuModelItem::init(caf::actor_system &system) { setCurrentChoice(QStringFromStd(data.get())); } dont_update_model_ = false; - }}; + }, + [=](caf::message) {}}; }); } @@ -649,7 +948,9 @@ void MenuModelItem::setMenuName(const QString &menu_name) { void MenuModelItem::insertIntoMenuModel() { - if (!dont_update_model_ && !menu_name_.isEmpty() && menu_path_ != "Undefined") { + if (!dont_update_model_ && !menu_name_.isEmpty() && menu_path_ != "Undefined" && + (!text_.isEmpty() || menu_item_type_ == "divider" || menu_item_type_ == "radiogroup")) { + try { auto central_models_data_actor = @@ -657,17 +958,13 @@ void MenuModelItem::insertIntoMenuModel() { global_ui_model_data_registry); utility::JsonStore menu_item_data; - std::string name_string = StdFromQString(text_); - menu_item_data["name"] = name_string; - if (!hotkey_.isEmpty()) { - std::string hotkey_string = StdFromQString(hotkey_); - menu_item_data["hotkey"] = hotkey_string; - } - if (!current_choice_.isEmpty()) { - std::string current_choice_string = StdFromQString(current_choice_); - menu_item_data["current_choice"] = current_choice_string; - } + std::string name_string = StdFromQString(text_); + menu_item_data["name"] = name_string; + menu_item_data["hotkey_uuid"] = UuidFromQUuid(hotkey_uuid_); + + if (!current_choice_.isEmpty()) + menu_item_data["current_choice"] = StdFromQString(current_choice_); if (!choices_.empty()) { auto choices = nlohmann::json::parse("[]"); for (const auto &c : choices_) { @@ -675,25 +972,156 @@ void MenuModelItem::insertIntoMenuModel() { } menu_item_data["choices"] = choices; } - if (menu_item_type_ == "toggle") { + if (menu_item_type_ == "toggle" || menu_item_type_ == "toggle_settings") { menu_item_data["is_checked"] = is_checked_; } + + // this needs some explanation! Our flexible way of creating menu items + // causes some headaches. We can put an XsMenuModelItem anywhere in + // qml code to insert an item into some menu. But what happens when + // we have multiple instances of the *same* panel (which instances + // the same menu multiple times?). For example, the qml code for the + // media list might require us to declare some menu items for the + // right-click popup menu ... there is only one menu model instance + // living in GlobalUIModelData that describes this menu. But we have + // multiple instances of the menu and multiple instances of MenuModelItem + // that populates the menu. To get around this we can sniff out the + // panel that the MenuModelItem is created under. We can also sniff + // out the panel that the XsMenuItem (the actual graphical menu item + // that we see in the GUI) is created under. We can cross reference + // these fellas to call back to the correct MenuModelItem when the + // XsMenuItem is triggered by the user etc. + // + // If singleton_ is true, this checking is turned off. We assume + // that there is a singleton MenuModelItem that wants to insert itself + // into all instances of the given menu and always get a callback + // when its item is clicked on in one of those instances. + // + // The singleton case is useful when you have, say, a datasource + // plugin that wants to insert items into the media list menu. For + // example you might want a feature where it loads the latest version + // of some media that's already in the media list... in this case + // you want an identical menu item to appear in the pop-up menu of + // each media list. When the user clicks on it you want a single + // callback to be run but you want a handle giving access to the + // context (i.e. the mediaList) in which the menu event orignated + const std::string context = StdFromQString(Helpers::objPtrTostring(panel_context_)); + + // this string identifies a menu item in the 'tree' of the menu + // model. If multiple MenuModelItems exist for the same point in + // the tree (due to multiple panel instances resulting in multiple + // instances of MenuModelItem) they will share + std::string full_path = StdFromQString(menu_name_) + StdFromQString(menu_path_) + + StdFromQString(text_) + context; + + if (menu_item_type_ == "divider") { + // since dividers often don't have a 'text_' string we use their + // menu item position to make their ID + full_path += fmt::format("{}", menu_item_position_); + } + const auto item_id = utility::Uuid::generate_from_name(full_path.c_str()); + + if (!model_entry_id_.is_null() && item_id != model_entry_id_) { + // NEEDS OPTIMISATION!!!! THIS GETS CALLED LIKE MAD AS MENU_ITEMS BUILD + std::string menu_name_string = StdFromQString(menu_name_); + anon_mail(ui::model_data::remove_node_atom_v, menu_name_string, model_entry_id_) + .send(central_models_data_actor); + } + model_entry_id_ = item_id; + menu_item_data["menu_item_position"] = menu_item_position_; - std::string menu_item_type_string = StdFromQString(menu_item_type_); - menu_item_data["menu_item_type"] = menu_item_type_string; + menu_item_data["menu_item_type"] = StdFromQString(menu_item_type_); menu_item_data["uuid"] = model_entry_id_; + menu_item_data["menu_icon"] = StdFromQString(menu_custom_icon_); + menu_item_data["custom_menu_qml"] = StdFromQString(custom_menu_qml_); + menu_item_data["menu_item_enabled"] = enabled_; + menu_item_data["menu_item_context"] = context; + + if (!user_data_.isNull()) { + menu_item_data["user_data"] = qvariant_to_json(user_data_); + } - anon_send( - central_models_data_actor, + std::string menu_name_string = StdFromQString(menu_name_); + std::string menu_path_string = StdFromQString(menu_path_); + anon_mail( ui::model_data::insert_or_update_menu_node_atom_v, - StdFromQString(menu_name_), - StdFromQString(menu_path_), + menu_name_string, + menu_path_string, menu_item_data, - as_actor()); + as_actor()) + .send(central_models_data_actor); } catch (std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } } + +void MenuModelItem::setMenuPathPosition(const QString &menu_path, const QVariant position) { + if (!menu_path.isEmpty() && !menu_name_.isEmpty()) { + + auto central_models_data_actor = + self()->home_system().registry().template get( + global_ui_model_data_registry); + + bool ok; + const float pos = position.toFloat(&ok); + + if (ok) { + anon_mail( + ui::model_data::insert_or_update_menu_node_atom_v, + StdFromQString(menu_name_), + StdFromQString(menu_path), + pos) + .send(central_models_data_actor); + } else { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, "Third parameter should be a number."); + } + } +} + +QObject *MenuModelItem::contextPanel() { + QQmlContext *c = qmlContext(this); + QObject *pobj = this; + while (c) { + QObject *cobj = c->contextObject(); + if (cobj && cobj->objectName() == "XsPanelParent") { + return pobj; + } + pobj = cobj; + c = c->parentContext(); + } + return nullptr; +} + +PanelMenuModelFilter::PanelMenuModelFilter(QObject *parent) : QSortFilterProxyModel(parent) {} + +void PanelMenuModelFilter::setSourceModel(QAbstractItemModel *sourceModel) { + + if (auto m = dynamic_cast(sourceModel)) { + menu_item_context_role_id_ = m->roleId(QString("menu_item_context")); + source_model_ = m; + } else { + menu_item_context_role_id_ = 0; + source_model_ = nullptr; + } + QSortFilterProxyModel::setSourceModel(sourceModel); +} + +bool PanelMenuModelFilter::filterAcceptsRow( + int source_row, const QModelIndex &source_parent) const { + + if (!sourceModel()) + return false; + + if (menu_item_context_role_id_) { + + QModelIndex index = sourceModel()->index(source_row, 0, source_parent); + auto menu_item_context = + sourceModel()->data(index, menu_item_context_role_id_).toString(); + return menu_item_context.isEmpty() || menu_item_context == panel_address_; + } + + return true; +} \ No newline at end of file diff --git a/src/ui/qml/helper/src/model_helper_ui.cpp b/src/ui/qml/helper/src/model_helper_ui.cpp index 4c1736cae..d81703b34 100644 --- a/src/ui/qml/helper/src/model_helper_ui.cpp +++ b/src/ui/qml/helper/src/model_helper_ui.cpp @@ -1,4 +1,3 @@ -// SPDX-License-Identifier: Apache-2.0 #include "xstudio/ui/qml/helper_ui.hpp" #include "xstudio/utility/helpers.hpp" @@ -6,6 +5,7 @@ using namespace xstudio::ui::qml; #include #include +#include void ModelRowCount::setCount(const int count) { if (count != count_) { @@ -79,16 +79,24 @@ void ModelRowCount::setIndex(const QModelIndex &index) { void ModelProperty::setIndex(const QModelIndex &index) { if (index.isValid()) { - if (index_.isValid()) + if (index_.isValid()) { disconnect( index_.model(), &QAbstractItemModel::dataChanged, this, &ModelProperty::dataChanged); + disconnect( + index_.model(), + &QAbstractItemModel::rowsRemoved, + this, + &ModelProperty::removed); + } connect( index.model(), &QAbstractItemModel::dataChanged, this, &ModelProperty::dataChanged); + connect(index.model(), &QAbstractItemModel::rowsRemoved, this, &ModelProperty::removed); + index_ = QPersistentModelIndex(index); emit indexChanged(); setRole(role_); @@ -97,8 +105,14 @@ void ModelProperty::setIndex(const QModelIndex &index) { } else { index_ = QPersistentModelIndex(index); emit indexChanged(); - if (updateValue()) - emit valueChanged(); + value_ = QVariant(); + emit valueChanged(); + } +} + +void ModelProperty::removed(const QModelIndex &parent, int first, int last) { + if (not index_.isValid()) { + setIndex(parent.model()->index(-1, -1, parent)); } } @@ -161,12 +175,40 @@ void ModelProperty::setValue(const QVariant &value) { } } +void PreferencePropertyMap::setMyValue(const QVariant &value) { + if (values_->value("valueRole") != value) { + values_->setProperty("valueRole", value); + emit myValueChanged(); + } +} + +void PreferencePropertyMap::valueChanged(const QString &key, const QVariant &value) { + ModelPropertyMap::valueChanged(key, value); + emitChange(key); +} + +void PreferencePropertyMap::emitChange(const QString &key) { + if (key == "valueRole") + emit myValueChanged(); + else if (key == "datatypeRole") + emit dataTypeChanged(); + else if (key == "contextRole") + emit contextChanged(); + else if (key == "nameRole") + emit nameChanged(); + else if (key == "defaultValueRole") + emit defaultValueChanged(); + else if (key == "jsonTextRole") + emit jsonStringChanged(); +} + ModelPropertyMap::ModelPropertyMap(QObject *parent) : QObject(parent) { values_ = new QQmlPropertyMap(this); connect(values_, &QQmlPropertyMap::valueChanged, this, &ModelPropertyMap::valueChangedSlot); } void ModelPropertyMap::setIndex(const QModelIndex &index) { + if (index != index_) { auto model_change = true; @@ -202,13 +244,16 @@ void ModelPropertyMap::setIndex(const QModelIndex &index) { emit indexChanged(); - if (model_change) + if (model_change) { emit valuesChanged(); + emit contentChanged(); + } } else { index_ = QPersistentModelIndex(index); updateValues(); emit indexChanged(); emit valuesChanged(); + emit contentChanged(); } } else if (not index.isValid()) { // force update as will auto become invalid @@ -216,6 +261,7 @@ void ModelPropertyMap::setIndex(const QModelIndex &index) { updateValues(); emit indexChanged(); emit valuesChanged(); + emit contentChanged(); } } @@ -235,7 +281,8 @@ void ModelPropertyMap::dump() { } } -void ModelPropertyMap::updateValues(const QVector &roles) { +bool ModelPropertyMap::updateValues(const QVector &roles) { + auto changed = false; if (index_.isValid()) { auto hash = index_.model()->roleNames(); @@ -248,6 +295,7 @@ void ModelPropertyMap::updateValues(const QVector &roles) { if (model_value != (*values_)[propery_name]) { values_->setProperty(StdFromQString(propery_name).c_str(), model_value); + changed = true; } } ++i; @@ -257,13 +305,18 @@ void ModelPropertyMap::updateValues(const QVector &roles) { for (const auto &i : values_->keys()) { values_->setProperty(StdFromQString(i).c_str(), QVariant()); } + changed = true; } + + return changed; } void ModelPropertyMap::dataChanged( const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { - if (index_.isValid() and QItemSelectionRange(topLeft, bottomRight).contains(index_)) - updateValues(roles); + if (index_.isValid() and QItemSelectionRange(topLeft, bottomRight).contains(index_)) { + if (updateValues(roles)) + emit contentChanged(); + } } void ModelPropertyMap::valueChangedSlot(const QString &key, const QVariant &value) { @@ -321,7 +374,7 @@ void ModelNestedPropertyMap::valueChanged(const QString &key, const QVariant &va } // change from frontend. -void ModelNestedPropertyMap::updateValues(const QVector &roles) { +bool ModelNestedPropertyMap::updateValues(const QVector &roles) { // populate QMLPropertyMap from value. if (index_.isValid()) { auto jvalue = mapFromValue(index_.data(getRoleId(data_role_))); @@ -358,6 +411,7 @@ void ModelNestedPropertyMap::updateValues(const QVector &roles) { } } } + return true; } @@ -367,17 +421,46 @@ QVariant xstudio::ui::qml::mapFromValue(const nlohmann::json &value) { if (value.is_boolean()) result = QVariant::fromValue(value.get()); else if (value.is_number_integer()) - result = QVariant::fromValue(value.get()); + result = QVariant::fromValue(value.get()); else if (value.is_number_unsigned()) - result = QVariant::fromValue(value.get()); + result = QVariant::fromValue(value.get()); else if (value.is_number_float()) result = QVariant::fromValue(value.get()); else if (value.is_string()) result = QVariant::fromValue(QStringFromStd(value.get())); - else if (value.is_array()) - result = QVariantListFromJson(utility::JsonStore(value)); - else if (value.is_object()) - result = QVariantMapFromJson(value); + else if (value.is_array()) { + if (value.size() == 5 && value[0].is_string() && + value[0].get() == "colour") { + + // it should be a color! + const auto t = value.get(); + QColor c( + static_cast(round(t.r * 255.0f)), + static_cast(round(t.g * 255.0f)), + static_cast(round(t.b * 255.0f))); + result = QVariant(c); + } else if ( + value.size() == 6 && value[0].is_string() && + value[0].get() == "vec4") { + + // it should be a color! + const auto t = value.get(); + QVector4D rt; + rt[0] = t[0]; + rt[1] = t[1]; + rt[2] = t[2]; + rt[3] = t[3]; + return QVariant(rt); + } else { + result = QVariantListFromJson(utility::JsonStore(value)); + } + } else if (value.is_object()) { + QVariantMap r; + for (auto p = value.begin(); p != value.end(); ++p) { + r[QStringFromStd(p.key())] = mapFromValue(p.value()); + } + result = r; + } return result; } @@ -386,7 +469,9 @@ nlohmann::json xstudio::ui::qml::mapFromValue(const QVariant &value) { nlohmann::json result; if (value.userType() == qMetaTypeId()) { - QVariant v = qvariant_cast(value).toVariant(); + auto qjsvalue = qvariant_cast(value); + + QVariant v = qjsvalue.toVariant(); switch (static_cast(v.type())) { case QMetaType::QVariantMap: @@ -399,6 +484,17 @@ nlohmann::json xstudio::ui::qml::mapFromValue(const QVariant &value) { QJsonDocument(v.toJsonArray()).toJson(QJsonDocument::Compact).constData()); break; + case QMetaType::QColor: { + auto c = v.value(); + result = nlohmann::json(utility::ColourTriplet( + float(c.red()) / 255.0f, float(c.green()) / 255.0f, float(c.blue()) / 255.0f)); + } break; + + case QMetaType::QVector4D: { + auto c = v.value(); + result = nlohmann::json(Imath::V4f(c[0], c[1], c[2], c[3])); + } break; + default: spdlog::warn("1 Unsupported datatype {} {}", v.type(), v.typeName()); break; @@ -417,6 +513,9 @@ nlohmann::json xstudio::ui::qml::mapFromValue(const QVariant &value) { case QMetaType::Double: result = value.toDouble(); break; + case QMetaType::Float: + result = value.toDouble(); + break; case QMetaType::Int: result = value.toInt(); break; @@ -432,6 +531,9 @@ nlohmann::json xstudio::ui::qml::mapFromValue(const QVariant &value) { case QMetaType::LongLong: result = value.toLongLong(); break; + case QMetaType::ULongLong: + result = value.toULongLong(); + break; case QMetaType::QString: result = StdFromQString(value.toString()); break; @@ -441,11 +543,27 @@ nlohmann::json xstudio::ui::qml::mapFromValue(const QVariant &value) { QJsonDocument(value.toJsonObject()).toJson(QJsonDocument::Compact).constData()); break; + case QMetaType::QStringList: + result = nlohmann::json::parse( + QJsonDocument(value.toJsonArray()).toJson(QJsonDocument::Compact).constData()); + break; + case QMetaType::QVariantList: result = nlohmann::json::parse( QJsonDocument(value.toJsonArray()).toJson(QJsonDocument::Compact).constData()); break; + case QMetaType::QColor: { + auto c = value.value(); + result = nlohmann::json(utility::ColourTriplet( + float(c.red()) / 255.0f, float(c.green()) / 255.0f, float(c.blue()) / 255.0f)); + } break; + + case QMetaType::QVector4D: { + auto c = value.value(); + result = nlohmann::json(Imath::V4f(c[0], c[1], c[2], c[3])); + } break; + case QMetaType::QJsonDocument: { QVariant v = value; if (value.userType() == qMetaTypeId()) diff --git a/src/ui/qml/helper/src/module_data_ui.cpp b/src/ui/qml/helper/src/module_data_ui.cpp index e5d9de144..5d846da10 100644 --- a/src/ui/qml/helper/src/module_data_ui.cpp +++ b/src/ui/qml/helper/src/module_data_ui.cpp @@ -17,3 +17,76 @@ ModulesModelData::ModulesModelData(QObject *parent) : UIModelData(parent) { setRoleNames(utility::map_value_to_vec(module::Attribute::role_names)); } + +void ModulesModelData::setSingleAttributeName(const QString single_attr_name) { + + if (single_attr_name_ != single_attr_name && !model_name_.empty()) { + single_attr_name_ = single_attr_name; + emit singleAttributeNameChanged(); + + try { + + caf::scoped_actor sys{self()->home_system()}; + auto data = utility::request_receive( + *sys, + central_models_data_actor_, + ui::model_data::register_model_data_atom_v, + model_name_, + utility::JsonStore(nlohmann::json::parse("{}")), + as_actor()); + + // process app/user.. + setModelData(data); + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + } +} + +void ModulesModelData::setModelData(const nlohmann::json &data) { + + if (!single_attr_name_.isEmpty()) { + + // if we have single_attr_name_ set, then we + nlohmann::json filtered_data = nlohmann::json::parse(R"({ "children": [] })"); + const std::string filter_attr_name = StdFromQString(single_attr_name_); + if (data.contains("children")) { + nlohmann::json children = data["children"]; + if (children.is_array()) { + int filtered_child_idx = 0; + filtered_child_idx_ = -1; + for (auto &child : children) { + if (child.contains("title") && child["title"].is_string() && + child["title"].get() == filter_attr_name) { + filtered_data["children"].push_back(child); + filtered_child_idx_ = filtered_child_idx; + break; + } + filtered_child_idx++; + } + } + } + if (filtered_child_idx_ != -1) { + UIModelData::setModelData(filtered_data); + } else { + spdlog::debug( + "{} Attribute named \"{}\" not found in model data \"{}\"", + __PRETTY_FUNCTION__, + filter_attr_name, + model_name_); + UIModelData::setModelData(data); + } + + } else { + UIModelData::setModelData(data); + } +} + +QVariant ModulesModelData::attributeRoleData(const QString attr_name, const QString role_name) { + QModelIndex idx = searchRecursive(attr_name, "title"); + if (idx.isValid()) { + return get(idx, role_name); + } + return QVariant(); +} diff --git a/src/ui/qml/helper/src/snapshot_model_ui.cpp b/src/ui/qml/helper/src/snapshot_model_ui.cpp deleted file mode 100644 index 99e29d752..000000000 --- a/src/ui/qml/helper/src/snapshot_model_ui.cpp +++ /dev/null @@ -1,213 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -// #include "xstudio/ui/qml/job_control_ui.hpp" -#include "xstudio/ui/qml/snapshot_model_ui.hpp" -// #include "xstudio/ui/qml/caf_response_ui.hpp" - -CAF_PUSH_WARNINGS -#include -#include -#include -// #include -CAF_POP_WARNINGS - -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::ui::qml; - -SnapshotModel::SnapshotModel(QObject *parent) : JSONTreeModel(parent) { - - setRoleNames(std::vector({ - {"childrenRole"}, - {"mtimeRole"}, - {"nameRole"}, - {"pathRole"}, - {"typeRole"}, - })); - - try { - items_.bind_ignore_entry_func(ignore_not_session); - setModelData(items_.dump()); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } -} - -Q_INVOKABLE void SnapshotModel::rescan(const QModelIndex &index, const int depth) { - auto changed = false; - FileSystemItem *item = nullptr; - - if (index.isValid()) { - auto path = UriFromQUrl(index.data(pathRole).toUrl()); - item = items_.find_by_path(path); - } else { - item = &items_; - } - - if (item) { - changed = item->scan(depth); - - if (changed) { - auto jsn = item->dump(); - sortByName(jsn); - if (index.isValid()) - setData(index, QVariantMapFromJson(jsn), JSONRole); - else - setModelData(jsn); - } - } -} - - -void SnapshotModel::setPaths(const QVariant &value) { - try { - if (not value.isNull()) { - paths_ = value; - emit pathsChanged(); - - auto jsn = mapFromValue(paths_); - if (jsn.is_array()) { - items_.clear(); - - for (const auto &i : jsn) { - if (i.count("name") and i.count("path")) { - auto uri = caf::make_uri(i.at("path")); - if (uri) - items_.insert(items_.end(), FileSystemItem(i.at("name"), *uri)); - } - } - rescan(QModelIndex(), 1); - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } -} - -nlohmann::json SnapshotModel::sortByNameType(const nlohmann::json &jsn) const { - auto result = jsn; - - if (result.is_array()) { - std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) -> bool { - try { - if (a.at("type") == b.at("type")) - return a.at("name") < b.at("name"); - return a.at("type") < b.at("type"); - } catch (const std::exception &err) { - spdlog::warn("{}", err.what()); - } - return false; - }); - } - - return result; -} - -void SnapshotModel::sortByName(nlohmann::json &jsn) { - // this needs - if (jsn.is_object() and jsn.count("children") and jsn.at("children").is_array()) { - jsn["children"] = sortByNameType(jsn["children"]); - - for (auto &item : jsn["children"]) { - sortByName(item); - } - } -} - - -QVariant SnapshotModel::data(const QModelIndex &index, int role) const { - auto result = QVariant(); - - try { - if (index.isValid()) { - const auto &j = indexToData(index); - - switch (role) { - case Roles::typeRole: - if (j.count("type_name")) - result = QString::fromStdString(j.at("type_name")); - break; - - case Roles::pathRole: - if (j.count("path")) { - auto uri = caf::make_uri(j.at("path")); - if (uri) - result = QVariant::fromValue(QUrlFromUri(*uri)); - } - break; - - - case Roles::mtimeRole: - if (j.count("last_write") and not j.at("last_write").is_null()) { - result = QVariant::fromValue(QDateTime::fromMSecsSinceEpoch( - std::chrono::duration_cast( - j.at("last_write").get().time_since_epoch()) - .count())); - } - break; - - case Qt::DisplayRole: - case Roles::nameRole: - if (j.count("name")) { - result = QString::fromStdString(j.at("name")); - } - break; - case Roles::childrenRole: - if (j.count("children")) { - result = QVariantMapFromJson(j.at("children")); - } - break; - default: - result = JSONTreeModel::data(index, role); - break; - } - } - } catch (const std::exception &err) { - spdlog::warn( - "{} {} {} {} {}", - __PRETTY_FUNCTION__, - err.what(), - role, - StdFromQString(roleName(role)), - index.row()); - } - - return result; -} - -bool SnapshotModel::createFolder(const QModelIndex &index, const QString &name) { - auto result = false; - - // get path.. - if (index.isValid()) { - auto uri = caf::make_uri(StdFromQString(index.data(pathRole).toString())); - if (uri) { - auto path = uri_to_posix_path(*uri); - auto new_path = fs::path(path) / StdFromQString(name); - try { - fs::create_directory(new_path); - rescan(index); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - } - - return result; -} - -QUrl SnapshotModel::buildSavePath(const QModelIndex &index, const QString &name) const { - // get path.. - auto result = QUrl(); - - if (index.isValid()) { - auto uri = caf::make_uri(StdFromQString(index.data(pathRole).toString())); - if (uri) { - auto path = uri_to_posix_path(*uri); - auto new_path = fs::path(path) / std::string(StdFromQString(name) + ".xsz"); - result = QUrlFromUri(posix_path_to_uri(new_path.string())); - } - } - - return result; -} diff --git a/src/ui/qml/json_store/src/CMakeLists.txt b/src/ui/qml/json_store/src/CMakeLists.txt index 26836ddc3..0e413d737 100644 --- a/src/ui/qml/json_store/src/CMakeLists.txt +++ b/src/ui/qml/json_store/src/CMakeLists.txt @@ -1,6 +1,6 @@ SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core + CAF::core + Qt6::Core xstudio::json_store xstudio::ui::qml::helper ) @@ -8,4 +8,4 @@ SET(LINK_DEPS SET(EXTRAMOC ) -create_qml_component(json_store 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") +create_qml_component(json_store ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/json_store/src/export.h b/src/ui/qml/json_store/src/export.h deleted file mode 100644 index 1f5b8916a..000000000 --- a/src/ui/qml/json_store/src/export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef JSON_STORE_QML_EXPORT_H -#define JSON_STORE_QML_EXPORT_H - -#ifdef JSON_STORE_QML_STATIC_DEFINE -# define JSON_STORE_QML_EXPORT -# define JSON_STORE_QML_NO_EXPORT -#else -# ifndef JSON_STORE_QML_EXPORT -# ifdef json_store_qml_EXPORTS - /* We are building this library */ -# define JSON_STORE_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define JSON_STORE_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef JSON_STORE_QML_NO_EXPORT -# define JSON_STORE_QML_NO_EXPORT -# endif -#endif - -#ifndef JSON_STORE_QML_DEPRECATED -# define JSON_STORE_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef JSON_STORE_QML_DEPRECATED_EXPORT -# define JSON_STORE_QML_DEPRECATED_EXPORT JSON_STORE_QML_EXPORT JSON_STORE_QML_DEPRECATED -#endif - -#ifndef JSON_STORE_QML_DEPRECATED_NO_EXPORT -# define JSON_STORE_QML_DEPRECATED_NO_EXPORT JSON_STORE_QML_NO_EXPORT JSON_STORE_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef JSON_STORE_QML_NO_DEPRECATED -# define JSON_STORE_QML_NO_DEPRECATED -# endif -#endif - -#endif /* JSON_STORE_QML_EXPORT_H */ diff --git a/src/ui/qml/json_store/src/include/json_store_qml_export.h b/src/ui/qml/json_store/src/include/json_store_qml_export.h deleted file mode 100644 index 1f5b8916a..000000000 --- a/src/ui/qml/json_store/src/include/json_store_qml_export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef JSON_STORE_QML_EXPORT_H -#define JSON_STORE_QML_EXPORT_H - -#ifdef JSON_STORE_QML_STATIC_DEFINE -# define JSON_STORE_QML_EXPORT -# define JSON_STORE_QML_NO_EXPORT -#else -# ifndef JSON_STORE_QML_EXPORT -# ifdef json_store_qml_EXPORTS - /* We are building this library */ -# define JSON_STORE_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define JSON_STORE_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef JSON_STORE_QML_NO_EXPORT -# define JSON_STORE_QML_NO_EXPORT -# endif -#endif - -#ifndef JSON_STORE_QML_DEPRECATED -# define JSON_STORE_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef JSON_STORE_QML_DEPRECATED_EXPORT -# define JSON_STORE_QML_DEPRECATED_EXPORT JSON_STORE_QML_EXPORT JSON_STORE_QML_DEPRECATED -#endif - -#ifndef JSON_STORE_QML_DEPRECATED_NO_EXPORT -# define JSON_STORE_QML_DEPRECATED_NO_EXPORT JSON_STORE_QML_NO_EXPORT JSON_STORE_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef JSON_STORE_QML_NO_DEPRECATED -# define JSON_STORE_QML_NO_DEPRECATED -# endif -#endif - -#endif /* JSON_STORE_QML_EXPORT_H */ diff --git a/src/ui/qml/json_store/src/json_store_ui.cpp b/src/ui/qml/json_store/src/json_store_ui.cpp index 0ec46bb34..57acc0783 100644 --- a/src/ui/qml/json_store/src/json_store_ui.cpp +++ b/src/ui/qml/json_store/src/json_store_ui.cpp @@ -19,11 +19,6 @@ JsonStoreUI::JsonStoreUI(QObject *parent) : QMLActor(parent) {} void JsonStoreUI::init(actor_system &system_) { QMLActor::init(system_); - self()->set_down_handler([=](down_msg &msg) { - if (msg.source == store) - unsubscribe(); - }); - set_message_handler([=](actor_companion * /*self_*/) -> message_handler { return { [=](json_store::update_atom, @@ -33,7 +28,6 @@ void JsonStoreUI::init(actor_system &system_) { set_json_string(QStringFromStd(full.dump(4)), true); }, [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg &) {}, [=](json_store::update_atom, const utility::JsonStore &json) { set_json_string(QStringFromStd(json.dump(4)), true); @@ -56,16 +50,21 @@ void JsonStoreUI::set_json_string(const QString &str, const bool from_actor) { } void JsonStoreUI::subscribe(caf::actor actor) { + monitor_.dispose(); unsubscribe(); - self()->monitor(actor); + + monitor_ = self()->monitor( + actor, [this, addr = actor.address()](const error &) { unsubscribe(); }); + store = actor; // get group, and watch.. scoped_actor sys{system()}; - sys->request(actor, infinite, utility::get_group_atom_v) + sys->mail(utility::get_group_atom_v) + .request(actor, infinite) .receive( - [&](const std::pair &data) { - const auto [grp, json] = data; - store_events = grp; + [&](const std::tuple &data) { + const auto [js, grp, json] = data; + store_events = grp; scoped_actor sys{system()}; request_receive( @@ -74,12 +73,11 @@ void JsonStoreUI::subscribe(caf::actor actor) { set_json_string(QStringFromStd(json.dump(4)), true); }, [&](const caf::error &e) { - aout(self()) << __PRETTY_FUNCTION__ << " " << e << std::endl; + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(e)); }); } void JsonStoreUI::unsubscribe() { - self()->demonitor(store); store = caf::actor(); scoped_actor sys{system()}; @@ -98,7 +96,7 @@ void JsonStoreUI::send_update(QString text) { if (store != caf::actor()) { try { utility::JsonStore json_data(nlohmann::json::parse(text.toUtf8().constData())); - anon_send(store, json_store::set_json_atom_v, json_data); + anon_mail(json_store::set_json_atom_v, json_data).send(store); } catch (...) { } } diff --git a/src/ui/qml/json_store/test/CMakeLists.txt b/src/ui/qml/json_store/test/CMakeLists.txt index b7a440cf4..c5b0ff1e9 100644 --- a/src/ui/qml/json_store/test/CMakeLists.txt +++ b/src/ui/qml/json_store/test/CMakeLists.txt @@ -1,13 +1,13 @@ -include(CTest) +#include(CTest) -add_executable(json_store_qml_test json_store_ui_test.cpp qml.qrc) -default_options_gtest(json_store_qml_test) -target_link_libraries(json_store_qml_test - PUBLIC - xstudio::ui::qml::json_store - Qt5::Gui - Qt5::Quick - ${GTEST_LDFLAGS} -) +# add_executable(json_store_qml_test json_store_ui_test.cpp) +# default_options_gtest(json_store_qml_test) +#target_link_libraries(json_store_qml_test +# PUBLIC +# xstudio::ui::qml::json_store +# Qt6::Gui +# Qt6::Quick +# ${GTEST_LDFLAGS} +#) # add_test(json_store_qml_tests json_store_qml_test) diff --git a/src/ui/qml/json_store/test/main.qml b/src/ui/qml/json_store/test/main.qml index 8d374fc6b..82b4e65c6 100644 --- a/src/ui/qml/json_store/test/main.qml +++ b/src/ui/qml/json_store/test/main.qml @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.3 +import QtQuick + +import QtQuick.Layouts + import xstudio.qml.json_store 1.0 Window { diff --git a/src/ui/qml/log/src/CMakeLists.txt b/src/ui/qml/log/src/CMakeLists.txt index 6c1b25f0b..f13d48c55 100644 --- a/src/ui/qml/log/src/CMakeLists.txt +++ b/src/ui/qml/log/src/CMakeLists.txt @@ -1,6 +1,6 @@ SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core + CAF::core + Qt6::Core xstudio::ui::qml::helper xstudio::utility ) @@ -8,4 +8,4 @@ SET(LINK_DEPS SET(EXTRAMOC ) -create_qml_component(log 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") +create_qml_component(log ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/log/src/export.h b/src/ui/qml/log/src/export.h deleted file mode 100644 index 750db9793..000000000 --- a/src/ui/qml/log/src/export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef LOG_QML_EXPORT_H -#define LOG_QML_EXPORT_H - -#ifdef LOG_QML_STATIC_DEFINE -# define LOG_QML_EXPORT -# define LOG_QML_NO_EXPORT -#else -# ifndef LOG_QML_EXPORT -# ifdef log_qml_EXPORTS - /* We are building this library */ -# define LOG_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define LOG_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef LOG_QML_NO_EXPORT -# define LOG_QML_NO_EXPORT -# endif -#endif - -#ifndef LOG_QML_DEPRECATED -# define LOG_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef LOG_QML_DEPRECATED_EXPORT -# define LOG_QML_DEPRECATED_EXPORT LOG_QML_EXPORT LOG_QML_DEPRECATED -#endif - -#ifndef LOG_QML_DEPRECATED_NO_EXPORT -# define LOG_QML_DEPRECATED_NO_EXPORT LOG_QML_NO_EXPORT LOG_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef LOG_QML_NO_DEPRECATED -# define LOG_QML_NO_DEPRECATED -# endif -#endif - -#endif /* LOG_QML_EXPORT_H */ diff --git a/src/ui/qml/log/src/include/log_qml_export.h b/src/ui/qml/log/src/include/log_qml_export.h deleted file mode 100644 index 750db9793..000000000 --- a/src/ui/qml/log/src/include/log_qml_export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef LOG_QML_EXPORT_H -#define LOG_QML_EXPORT_H - -#ifdef LOG_QML_STATIC_DEFINE -# define LOG_QML_EXPORT -# define LOG_QML_NO_EXPORT -#else -# ifndef LOG_QML_EXPORT -# ifdef log_qml_EXPORTS - /* We are building this library */ -# define LOG_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define LOG_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef LOG_QML_NO_EXPORT -# define LOG_QML_NO_EXPORT -# endif -#endif - -#ifndef LOG_QML_DEPRECATED -# define LOG_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef LOG_QML_DEPRECATED_EXPORT -# define LOG_QML_DEPRECATED_EXPORT LOG_QML_EXPORT LOG_QML_DEPRECATED -#endif - -#ifndef LOG_QML_DEPRECATED_NO_EXPORT -# define LOG_QML_DEPRECATED_NO_EXPORT LOG_QML_NO_EXPORT LOG_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef LOG_QML_NO_DEPRECATED -# define LOG_QML_NO_DEPRECATED -# endif -#endif - -#endif /* LOG_QML_EXPORT_H */ diff --git a/src/ui/qml/log/src/log_ui.cpp b/src/ui/qml/log/src/log_ui.cpp index 616cb5c83..a9f4c9fdf 100644 --- a/src/ui/qml/log/src/log_ui.cpp +++ b/src/ui/qml/log/src/log_ui.cpp @@ -112,19 +112,19 @@ QVariant LogModel::data(const QModelIndex &index, int role) const { if (index.row() < data_.size()) { switch (role) { case TimeRole: - result = QVariant::fromValue(data_[index.row()].timeStamp); + result = data_[index.row()].timeStamp; break; case LevelRole: - result = QVariant::fromValue(data_[index.row()].level); + result = data_[index.row()].level; break; case LevelStringRole: - result = QVariant::fromValue(logLevelToQString(data_[index.row()].level)); + result = logLevelToQString(data_[index.row()].level); break; case StringRole: - result = QVariant::fromValue(data_[index.row()].text); + result = data_[index.row()].text; break; } } diff --git a/src/ui/qml/module/src/CMakeLists.txt b/src/ui/qml/module/src/CMakeLists.txt deleted file mode 100644 index d6bd19ddc..000000000 --- a/src/ui/qml/module/src/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ - -SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core - xstudio::module - xstudio::ui::qml::helper -) - -SET(EXTRAMOC - "${ROOT_DIR}/include/xstudio/ui/qml/module_menu_ui.hpp" -) - -create_qml_component(module 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/module/src/export.h b/src/ui/qml/module/src/export.h deleted file mode 100644 index 25f4f6c2c..000000000 --- a/src/ui/qml/module/src/export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef MODULE_QML_EXPORT_H -#define MODULE_QML_EXPORT_H - -#ifdef MODULE_QML_STATIC_DEFINE -# define MODULE_QML_EXPORT -# define MODULE_QML_NO_EXPORT -#else -# ifndef MODULE_QML_EXPORT -# ifdef module_qml_EXPORTS - /* We are building this library */ -# define MODULE_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define MODULE_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef MODULE_QML_NO_EXPORT -# define MODULE_QML_NO_EXPORT -# endif -#endif - -#ifndef MODULE_QML_DEPRECATED -# define MODULE_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef MODULE_QML_DEPRECATED_EXPORT -# define MODULE_QML_DEPRECATED_EXPORT MODULE_QML_EXPORT MODULE_QML_DEPRECATED -#endif - -#ifndef MODULE_QML_DEPRECATED_NO_EXPORT -# define MODULE_QML_DEPRECATED_NO_EXPORT MODULE_QML_NO_EXPORT MODULE_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef MODULE_QML_NO_DEPRECATED -# define MODULE_QML_NO_DEPRECATED -# endif -#endif - -#endif /* MODULE_QML_EXPORT_H */ diff --git a/src/ui/qml/module/src/include/module_qml_export.h b/src/ui/qml/module/src/include/module_qml_export.h deleted file mode 100644 index 25f4f6c2c..000000000 --- a/src/ui/qml/module/src/include/module_qml_export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef MODULE_QML_EXPORT_H -#define MODULE_QML_EXPORT_H - -#ifdef MODULE_QML_STATIC_DEFINE -# define MODULE_QML_EXPORT -# define MODULE_QML_NO_EXPORT -#else -# ifndef MODULE_QML_EXPORT -# ifdef module_qml_EXPORTS - /* We are building this library */ -# define MODULE_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define MODULE_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef MODULE_QML_NO_EXPORT -# define MODULE_QML_NO_EXPORT -# endif -#endif - -#ifndef MODULE_QML_DEPRECATED -# define MODULE_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef MODULE_QML_DEPRECATED_EXPORT -# define MODULE_QML_DEPRECATED_EXPORT MODULE_QML_EXPORT MODULE_QML_DEPRECATED -#endif - -#ifndef MODULE_QML_DEPRECATED_NO_EXPORT -# define MODULE_QML_DEPRECATED_NO_EXPORT MODULE_QML_NO_EXPORT MODULE_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef MODULE_QML_NO_DEPRECATED -# define MODULE_QML_NO_DEPRECATED -# endif -#endif - -#endif /* MODULE_QML_EXPORT_H */ diff --git a/src/ui/qml/module/src/module_menu_ui.cpp b/src/ui/qml/module/src/module_menu_ui.cpp deleted file mode 100644 index ea885eebc..000000000 --- a/src/ui/qml/module/src/module_menu_ui.cpp +++ /dev/null @@ -1,558 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/ui/qml/module_menu_ui.hpp" -#include "xstudio/ui/qml/module_ui.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/logging.hpp" - -#include -#include -#include - -using namespace caf; -using namespace xstudio; -using namespace xstudio::module; -using namespace xstudio::ui::qml; - -namespace { - -QUuid uuid_from_attr_data(QMap &attr_data) { - - if (attr_data.contains(ModuleMenusModel::XsMenuRoles::Uuid)) { - return attr_data[ModuleMenusModel::XsMenuRoles::Uuid].toUuid(); - } - return QUuid(); -} - -} // namespace - -ModuleMenusModel::ModuleMenusModel(QObject *parent) : QAbstractListModel(parent) { - // QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); - shim_ = new ModuleAttrsToQMLShim(this); -} - -ModuleMenusModel::~ModuleMenusModel() { - shim_->disconnect(); - shim_->clear(); -} - -QHash ModuleMenusModel::roleNames() const { - QHash roles; - for (const auto &p : ModuleMenusModel::role_names) { - roles[p.first] = p.second.c_str(); - } - return roles; -} - -int ModuleMenusModel::rowCount(const QModelIndex &) const { - return static_cast(attributes_data_.size()); -} - -QVariant ModuleMenusModel::data(const QModelIndex &index, int role) const { - - QVariant rt; - try { - if ((int)attributes_data_.size() > index.row() && - attributes_data_[index.row()].contains(role)) { - rt = attributes_data_[index.row()][role]; - } else { - } - } catch ([[maybe_unused]] std::exception &e) { - } - return rt; -} - -bool ModuleMenusModel::already_have_attr_in_this_menu(const QUuid &uuid) { - - bool rt = false; - for (const auto &p : attributes_data_) { - if (p.contains(XsMenuRoles::Uuid)) { - if (p[XsMenuRoles::Uuid].toUuid() == uuid) { - rt = true; - } - } - } - return rt; -} - -QString pathAtDepth(const QString &path, const int depth) { - int d = depth; - int pos = 0; - while (path.indexOf("|", pos) != -1) { - pos = path.indexOf("|", pos + 1); - if (!d) { - return path.mid(0, path.indexOf("|", pos)); - } - d--; - } - return path; -} - -bool ModuleMenusModel::is_attr_in_this_menu(const ConstAttributePtr &attr) { - - if (menu_path_.isEmpty()) - return false; - auto bf = submenu_names_; - try { - bool changed = false; - - if (attr->has_role_data(Attribute::MenuPaths)) { - const QUuid attr_uuid(QUuidFromUuid(attr->uuid())); - - auto menu_paths = - attr->get_role_data>(Attribute::MenuPaths); - - for (const auto &menu_path : menu_paths) { - const int nesting_depth = menu_path_.count("|"); - - QString path = QStringFromStd(menu_path); - QString path_to_my_depth = pathAtDepth(path, nesting_depth); - - if (menu_path_ == path) { - return true; - } else if (path.indexOf(menu_path_) == 0) { - - const QString child_menu_path = pathAtDepth(path, nesting_depth + 1); - - if (not submenu_names_.count(child_menu_path)) { - submenu_names_.append(child_menu_path); - attrs_per_submenus_[child_menu_path].append(attr_uuid); - changed = true; - } else { - if (not attrs_per_submenus_[child_menu_path].count(attr_uuid)) { - attrs_per_submenus_[child_menu_path].append(attr_uuid); - } - } - } - } - } - - if (changed) { - emit submenu_namesChanged(); - emit num_submenusChanged(); - } - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return false; -} - -void ModuleMenusModel::add_attributes_from_backend(const module::AttributeSet &attrs) { - - const bool e = empty(); - try { - if (not attrs.empty()) { - - module::AttributeSet new_menu_attrs; - for (const auto &attr : attrs) { - - if (not is_attr_in_this_menu(attr)) - continue; - - if (not already_have_attr_in_this_menu(QUuidFromUuid(attr->uuid()))) { - new_menu_attrs.push_back(attr); - } else { - update_full_attribute_from_backend(attr); - } - } - - - for (const auto &attr : new_menu_attrs) { - - if (attr->has_role_data(Attribute::Type)) { - const auto attr_type = attr->get_role_data(Attribute::Type); - if (attr_type == Attribute::type_name(Attribute::StringChoiceAttribute)) { - add_multi_choice_menu_item(attr); - } else if (attr_type == Attribute::type_name(Attribute::BooleanAttribute)) { - add_checkable_menu_item(attr); - } else if (attr_type == Attribute::type_name(Attribute::ActionAttribute)) { - add_menu_action_item(attr); - } - } - } - } - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - if (empty() != e) - emit emptyChanged(); -} - -void ModuleMenusModel::add_multi_choice_menu_item(const ConstAttributePtr &attr) { - - const bool e = empty(); - - try { - - auto string_choices = - attr->get_role_data>(Attribute::StringChoices); - const QUuid attr_uuid = QUuidFromUuid(attr->uuid()); - - std::vector enabled(string_choices.size(), true); - if (attr->has_role_data(Attribute::StringChoicesEnabled)) { - try { - auto string_choices_enabled_data = - attr->get_role_data>(Attribute::StringChoicesEnabled); - if (string_choices_enabled_data.size() == enabled.size()) { - enabled = string_choices_enabled_data; - } - } catch (...) { - // if the enabled vector is empty it won't cast to vect - } - } - - QString value = QStringFromStd(attr->get_role_data(Attribute::Value)); - QStringList choices; - for (auto choice : string_choices) { - choices.append(QStringFromStd(choice)); - } - - bool dummy_entry = false; - if (!choices.size()) { - choices.append("-"); - enabled.push_back(false); - dummy_entry = true; - } - - beginInsertRows( - QModelIndex(), - attributes_data_.size(), - static_cast(attributes_data_.size() + choices.size()) - 1); - - int idx = 0; - for (auto choice : choices) { - QMap roles_data; - roles_data[XsMenuRoles::MenuText] = QVariant(choice); - roles_data[XsMenuRoles::IsCheckable] = QVariant(dummy_entry ? false : true); - roles_data[XsMenuRoles::IsMultiChoice] = QVariant(dummy_entry ? false : true); - roles_data[XsMenuRoles::Value] = QVariant(value == choice); - roles_data[XsMenuRoles::Uuid] = attr_uuid; - roles_data[XsMenuRoles::AttrType] = "MultiChoice"; - roles_data[XsMenuRoles::Enabled] = QVariant(enabled[idx]); - attributes_data_.push_back(roles_data); - idx++; - } - - endInsertRows(); - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - if (empty() != e) - emit emptyChanged(); -} - -void ModuleMenusModel::update_multi_choice_menu_item( - const utility::Uuid &attr_uuid, const utility::JsonStore &string_choice_data) { - - const QUuid quuid = QUuidFromUuid(attr_uuid); - const bool e = empty(); - - if (not already_have_attr_in_this_menu(quuid)) - return; - - if (string_choice_data.is_array() && string_choice_data.size() && - string_choice_data.begin().value().is_string()) { - - const size_t num_new_string_choices = string_choice_data.size(); - - int idx = 0; - auto p = attributes_data_.begin(); - - // first, clear out existing choices - while (p != attributes_data_.end()) { - if ((*p)[XsMenuRoles::Uuid].toUuid() == quuid) { - beginRemoveRows(QModelIndex(), idx, idx); - p = attributes_data_.erase(p); - endRemoveRows(); - } else { - p++; - idx++; - } - } - - QStringList choices; - for (size_t i = 0; i < num_new_string_choices; ++i) { - choices.append(QStringFromStd(string_choice_data[i].get())); - } - - beginInsertRows( - QModelIndex(), - attributes_data_.size(), - static_cast(attributes_data_.size() + choices.size()) - 1); - - int i = 0; - for (auto choice : choices) { - QMap roles_data; - roles_data[XsMenuRoles::MenuText] = QVariant(choice); - roles_data[XsMenuRoles::IsCheckable] = QVariant(true); - roles_data[XsMenuRoles::IsMultiChoice] = QVariant(true); - // Reset the current value to the first item, can we do something more clever? - roles_data[XsMenuRoles::Value] = i++ == 0 ? QVariant(true) : QVariant(false); - roles_data[XsMenuRoles::Uuid] = quuid; - roles_data[XsMenuRoles::AttrType] = "MultiChoice"; - roles_data[XsMenuRoles::Enabled] = QVariant(true); - attributes_data_.push_back(roles_data); - } - - endInsertRows(); - } - - if (empty() != e) - emit emptyChanged(); -} - -void ModuleMenusModel::add_checkable_menu_item(const ConstAttributePtr &attr) { - - const bool e = empty(); - try { - - QString title = QStringFromStd(attr->get_role_data(Attribute::Title)); - const QUuid attr_uuid = QUuidFromUuid(attr->uuid()); - const bool value = attr->get_role_data(Attribute::Value); - - beginInsertRows(QModelIndex(), attributes_data_.size(), attributes_data_.size()); - - QMap roles_data; - roles_data[XsMenuRoles::MenuText] = QVariant(title); - roles_data[XsMenuRoles::IsCheckable] = QVariant(true); - roles_data[XsMenuRoles::Value] = QVariant(value); - roles_data[XsMenuRoles::Uuid] = attr_uuid; - roles_data[XsMenuRoles::AttrType] = "Toggle"; - attributes_data_.push_back(roles_data); - - endInsertRows(); - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - if (empty() != e) - emit emptyChanged(); -} - -void ModuleMenusModel::add_menu_action_item(const ConstAttributePtr &attr) { - - const bool e = empty(); - try { - - QString title = QStringFromStd(attr->get_role_data(Attribute::Title)); - const QUuid attr_uuid = QUuidFromUuid(attr->uuid()); - - beginInsertRows(QModelIndex(), attributes_data_.size(), attributes_data_.size()); - - QMap roles_data; - roles_data[XsMenuRoles::MenuText] = QVariant(title); - roles_data[XsMenuRoles::IsCheckable] = QVariant(false); - roles_data[XsMenuRoles::Uuid] = attr_uuid; - roles_data[XsMenuRoles::Value] = QVariant(false); - roles_data[XsMenuRoles::AttrType] = "Action"; - if (attr->has_role_data(Attribute::HotkeyUuid)) { - roles_data[XsMenuRoles::HotkeyUuid] = - QUuid(attr->get_role_data(Attribute::HotkeyUuid).c_str()); - } - attributes_data_.push_back(roles_data); - - endInsertRows(); - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - if (empty() != e) - emit emptyChanged(); -} - -void ModuleMenusModel::remove_attribute(const utility::Uuid &attr_uuid) { - - auto bf = submenu_names_; - const bool e = empty(); - const QUuid quuid = QUuidFromUuid(attr_uuid); - int idx = 0; - auto attr = attributes_data_.begin(); - while (attr != attributes_data_.end()) { - if ((*attr).contains(XsMenuRoles::Uuid) && - (*attr)[XsMenuRoles::Uuid].toUuid() == quuid) { - beginRemoveRows(QModelIndex(), idx, idx); - attr = attributes_data_.erase(attr); - endRemoveRows(); - } else { - attr++; - idx++; - } - } - - auto p = attrs_per_submenus_.begin(); - bool changed = false; - while (p != attrs_per_submenus_.end()) { - - if (p.value().count(quuid)) { - p.value().removeAll(quuid); - if (p.value().empty()) { - // a submenu has been completely cleared out - now we remove it by changing - // submenu_names - submenu_names_.removeAll(p.key()); - attrs_per_submenus_.remove(p.key()); - p = attrs_per_submenus_.begin(); - changed = true; - } else { - p++; - } - - } else { - p++; - } - } - if (changed) { - emit submenu_namesChanged(); - } - if (empty() != e) - emit emptyChanged(); -} - -bool ModuleMenusModel::setData(const QModelIndex &index, const QVariant &value, int role) { - - if (role == (XsMenuRoles::Value)) { - if (attributes_data_[index.row()][XsMenuRoles::AttrType].toString() == "MultiChoice" && - value.toBool()) { - emit setAttributeFromFrontEnd( - attributes_data_[index.row()][XsMenuRoles::Uuid].toUuid(), - Attribute::Value, - attributes_data_[index.row()][XsMenuRoles::MenuText]); - } else if ( - attributes_data_[index.row()][XsMenuRoles::AttrType].toString() == "Toggle") { - emit setAttributeFromFrontEnd( - attributes_data_[index.row()][XsMenuRoles::Uuid].toUuid(), - Attribute::Value, - value); - } else if ( - attributes_data_[index.row()][XsMenuRoles::AttrType].toString() == "Action") { - emit setAttributeFromFrontEnd( - attributes_data_[index.row()][XsMenuRoles::Uuid].toUuid(), - Attribute::Value, - QVariant()); - } - } - return true; -} - -void ModuleMenusModel::update_full_attribute_from_backend( - const module::ConstAttributePtr &attr) { - - const bool e = empty(); - - try { - const QUuid attr_uuid(QUuidFromUuid(attr->uuid())); - int row = 0; - for (auto &attribute : attributes_data_) { - - QUuid uuid = uuid_from_attr_data(attribute); - if (uuid == attr_uuid) { - for (const int role : attribute.keys()) { - - if (attr->has_role_data(role)) { - QVariant new_role_data = - json_to_qvariant(attr->role_data_as_json(role)); - if (new_role_data != attribute[role]) { - attribute[role] = new_role_data; - const QModelIndex index = createIndex(row, 0); - emit dataChanged(index, index, {role}); - } - } - } - } - row++; - } - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - if (empty() != e) - emit emptyChanged(); -} - -void ModuleMenusModel::update_attribute_from_backend( - const utility::Uuid &attr_uuid, const int role, const utility::JsonStore &role_value) { - - try { - if (role == Attribute::Value) { - const QUuid quuid = QUuidFromUuid(attr_uuid); - int row = 0; - for (auto &p : attributes_data_) { - - if (p.contains(XsMenuRoles::Uuid) && p[XsMenuRoles::Uuid].toUuid() == quuid) { - if (p[XsMenuRoles::AttrType].toString() == "MultiChoice") { - if (p[XsMenuRoles::MenuText] == json_to_qvariant(role_value) && - !p[XsMenuRoles::Value].toBool()) { - p[XsMenuRoles::Value] = QVariant(true); - const QModelIndex index = createIndex(row, 0); - emit dataChanged(index, index, {XsMenuRoles::Value}); - } else if ( - p[XsMenuRoles::MenuText] != json_to_qvariant(role_value) && - p[XsMenuRoles::Value].toBool()) { - p[XsMenuRoles::Value] = QVariant(false); - const QModelIndex index = createIndex(row, 0); - emit dataChanged(index, index, {XsMenuRoles::Value}); - } - } else if (p[XsMenuRoles::AttrType].toString() == "Toggle") { - p[XsMenuRoles::Value] = json_to_qvariant(role_value); - const QModelIndex index = createIndex(row, 0); - emit dataChanged(index, index, {XsMenuRoles::Value}); - } - } - row++; - } - } else if (role == Attribute::StringChoices) { - - update_multi_choice_menu_item(attr_uuid, role_value); - - } else if (role == Attribute::StringChoicesEnabled) { - - QList enabled; - if (role_value.is_array() && role_value.size() && - role_value.begin().value().is_boolean()) { - - for (size_t i = 0; i < role_value.size(); ++i) { - enabled.append(role_value[i].get()); - } - } - - const QUuid quuid = QUuidFromUuid(attr_uuid); - int row = 0; - int idx = 0; - for (auto &p : attributes_data_) { - - if (p.contains(XsMenuRoles::Uuid) && p[XsMenuRoles::Uuid].toUuid() == quuid) { - if (p[XsMenuRoles::AttrType].toString() == "MultiChoice") { - - if (idx < enabled.size()) { - p[XsMenuRoles::Enabled] = QVariant(enabled[idx]); - const QModelIndex index = createIndex(row, 0); - emit dataChanged(index, index, {XsMenuRoles::Enabled}); - } - idx++; - } - } - row++; - } - } - - } catch ([[maybe_unused]] std::exception &e) { - } -} - -void ModuleMenusModel::setRootMenuName(QString root_menu_name) { - - if (menu_path_ != root_menu_name) { - menu_path_ = root_menu_name; - emit rootMenuNameChanged(menu_path_); - - if (root_menu_name.contains("|")) { - title_ = root_menu_name.mid(root_menu_name.lastIndexOf("|") + 1); - emit titleChanged(); - } - } -} \ No newline at end of file diff --git a/src/ui/qml/module/src/module_ui.cpp b/src/ui/qml/module/src/module_ui.cpp deleted file mode 100644 index 16590b638..000000000 --- a/src/ui/qml/module/src/module_ui.cpp +++ /dev/null @@ -1,651 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/ui/qml/module_ui.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/json_store.hpp" -#include "xstudio/utility/logging.hpp" - -#include -#include - -using namespace caf; -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::module; -using namespace xstudio::ui::qml; - -namespace { - -bool attr_is_in_group( - const module::ConstAttributePtr &attr, const QStringList &group_names_in) { - - bool match = false; - try { - auto group_names = - attr->get_role_data>(Attribute::Role::Groups); - for (auto g : group_names) { - if (group_names_in.contains(QStringFromStd(g))) { - match = true; - break; - } - } - } catch ([[maybe_unused]] std::exception &e) { - } - - return match; -} - -QUuid uuid_from_attr_data(QMap &attr_data) { - - if (attr_data.contains(Attribute::UuidRole)) { - return attr_data[Attribute::UuidRole].toUuid(); - } - return QUuid(); -} - -} // namespace - - -ModuleAttrsDirect::ModuleAttrsDirect(QObject *parent) - : QQmlPropertyMap(this, parent), role_name_("value") { - new ModuleAttrsToQMLShim(this); - emit roleNameChanged(); -} - -ModuleAttrsDirect::~ModuleAttrsDirect() {} - -void ModuleAttrsDirect::add_attributes_from_backend( - const module::AttributeSet &attrs, bool check_group) { - try { - auto role_id = Attribute::role_index(role_name_.toStdString()); - for (const auto &attr : attrs) { - try { - if (check_group && - (!attr_is_in_group(attr, attrs_group_name_) || - (role_id != Attribute::Role::Value and !attr->has_role_data(role_id)))) - continue; - - const auto uuid = QUuidFromUuid(attr->uuid()); - QString qstr_name( - QStringFromStd(attr->get_role_data(Attribute::Role::Title)) - .toLower()); - qstr_name = qstr_name.replace(" ", "_"); - - // force the attr name to be lower case alphanumeric and underscore only - qstr_name = qstr_name.replace(QRegExp("[^_a-z0-9]"), ""); - QVariant valueqv; - try { - valueqv = json_to_qvariant(attr->role_data_as_json(role_id)); - } catch (...) { - } - - attr_uuids_by_name_[qstr_name] = uuid; - attr_names_by_uuid_[uuid] = qstr_name; - attr_values_by_uuid_[uuid] = valueqv; - - insert(qstr_name, valueqv); - emit attrAdded(qstr_name); - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - } - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } -} - -void ModuleAttrsDirect::setRoleName(QString role_name) { - - if (role_name != role_name_) { - role_name_ = role_name; - - // re-use this signal to get the backend to re-broadcast attributes - // back to this class so we can update our properties to reflect the - // data role - emit attributesGroupNamesChanged(attrs_group_name_); - } -} - - -void ModuleAttrsDirect::update_attribute_from_backend( - const utility::Uuid &attr_uuid, const int role, const utility::JsonStore &role_value) { - int role_id = Attribute::Role::Value; - try { - role_id = Attribute::role_index(role_name_.toStdString()); - // spdlog::warn("{} {} {} {} {}", __PRETTY_FUNCTION__, role_value.dump(2), role_id, - // role, to_string(attr_uuid)); - - if (role == role_id) { - const QUuid uuidv = QUuidFromUuid(attr_uuid); - if (attr_names_by_uuid_.contains(uuidv)) { - try { - const QVariant valueqv = json_to_qvariant(role_value); - insert(attr_names_by_uuid_[uuidv], valueqv); - attr_values_by_uuid_[uuidv] = valueqv; - emit valueChanged(attr_names_by_uuid_[uuidv], valueqv); - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - }; - } else { - /*spdlog::warn("{} Backend requested update of unregistered attribute. {}", - __PRETTY_FUNCTION__, to_string(attr_uuid));*/ - } - } - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } -} - -void ModuleAttrsDirect::remove_attribute(const utility::Uuid &) { - // QQmlPropertyMap does not provide a way to remove properties after they - // have been created -} - -QVariant ModuleAttrsDirect::updateValue(const QString &key, const QVariant &input) { - - if (!attr_uuids_by_name_.contains(key)) { - spdlog::warn( - "{} Attempt to update value of unregistered attribute \"{}\"", - __PRETTY_FUNCTION__, - StdFromQString(key)); - return QVariant(); - } - - const QUuid &uuid = attr_uuids_by_name_[key]; - - // the attribute is being set from QML ... we send a signal to pass this to the backend, - // and wait for the change to come back via update_attribute_from_backend - emit setAttributeFromFrontEnd(uuid, Attribute::Role::Value, input); - - // now we OVERRIDE the change from QML, because the change can't happen until it has - // bubbled back up from the backend. The intention is to resolve sync issues between front - // and backend that could otherwise set-up infinite loop where the backend change creates - // a change signal in QML which is passed back to the backend - if a floating point round - // error, say, means data in the backend is slightly different to the front end we would - // get stuck in an infinite loop! - return attr_values_by_uuid_[uuid]; -} - -void ModuleAttrsDirect::setattributesGroupNames(QStringList group_name) { - - if (attrs_group_name_ != group_name) { - attrs_group_name_ = group_name; - emit attributesGroupNamesChanged(attrs_group_name_); - } -} - -OrderedModuleAttrsModel::OrderedModuleAttrsModel(QObject *parent) - : QSortFilterProxyModel(parent) { - setSourceModel(new ModuleAttrsModel(this)); - setDynamicSortFilter(true); - setSortRole(module::Attribute::ToolbarPosition + Qt::UserRole + 1); - sort(0, Qt::AscendingOrder); -} - -QStringList OrderedModuleAttrsModel::attributesGroupNames() const { - auto mam = dynamic_cast(sourceModel()); - if (mam) - return mam->attributesGroupNames(); - return QStringList(); -} - -void OrderedModuleAttrsModel::setattributesGroupNames(const QStringList &group_name) { - auto mam = dynamic_cast(sourceModel()); - if (mam) { - mam->setattributesGroupNames(group_name); - emit attributesGroupNamesChanged(group_name); - } -} - -ModuleAttrsModel::ModuleAttrsModel(QObject *parent) : QAbstractListModel(parent) { - new ModuleAttrsToQMLShim(this); -} - -ModuleAttrsModel::~ModuleAttrsModel() {} - -QHash ModuleAttrsModel::roleNames() const { - QHash roles; - for (const auto &p : Attribute::role_names) { - roles[p.first + Qt::UserRole + 1] = p.second.c_str(); - } - return roles; -} - -int ModuleAttrsModel::rowCount(const QModelIndex &) const { - return static_cast(attributes_data_.size()); -} - -QVariant ModuleAttrsModel::data(const QModelIndex &index, int role) const { - - QVariant rt; - - role -= Qt::UserRole + 1; - - try { - if ((int)attributes_data_.size() > index.row() && - attributes_data_[index.row()].contains(role)) { - rt = attributes_data_[index.row()][role]; - } - } catch (const std::exception &) { - } - - return rt; -} - -bool ModuleAttrsModel::have_attr(const QUuid &uuid) { - - bool rt = false; - for (const auto &p : attributes_data_) { - if (p.contains(Attribute::UuidRole)) { - if (p[Attribute::UuidRole].toUuid() == uuid) { - rt = true; - } - } - } - return rt; -} - -void ModuleAttrsModel::add_attributes_from_backend( - const module::AttributeSet &attrs, const bool check_group) { - if (not attrs.empty()) { - - module::AttributeSet new_attrs; - for (const auto &attr : attrs) { - - if (not have_attr(QUuidFromUuid(attr->uuid()))) { - if (check_group && !attr_is_in_group(attr, attrs_group_name_)) - continue; - new_attrs.push_back(attr); - } else { - update_full_attribute_from_backend(attr); - } - } - - if (!new_attrs.empty()) { - beginInsertRows( - QModelIndex(), - attributes_data_.size(), - static_cast(attributes_data_.size() + new_attrs.size()) - 1); - - for (const auto &attr : new_attrs) { - - QMap attr_qt_data; - const nlohmann::json json = attr->as_json(); - - for (auto p = json.begin(); p != json.end(); ++p) { - const int role = Attribute::role_index(p.key()); - if (role == Attribute::UuidRole) { - attr_qt_data[role] = QUuidFromUuid(p.value().get()); - } else { - attr_qt_data[role] = json_to_qvariant(p.value()); - } - } - attributes_data_.push_back(attr_qt_data); - } - - endInsertRows(); - emit rowCountChanged(); - } - } -} - -void ModuleAttrsModel::remove_attribute(const utility::Uuid &attr_uuid) { - const QUuid quuid = QUuidFromUuid(attr_uuid); - int idx = 0; - for (auto attr = attributes_data_.begin(); attr != attributes_data_.end(); attr++) { - if ((*attr).contains(Attribute::UuidRole) && - (*attr)[Attribute::UuidRole].toUuid() == quuid) { - beginRemoveRows(QModelIndex(), idx, idx); - attributes_data_.erase(attr); - endRemoveRows(); - emit rowCountChanged(); - break; - } - idx++; - } -} - -bool ModuleAttrsModel::setData(const QModelIndex &index, const QVariant &value, int role) { - - role -= Qt::UserRole + 1; - emit setAttributeFromFrontEnd( - attributes_data_[index.row()][Attribute::UuidRole].toUuid(), role, value); - - return true; -} - -void ModuleAttrsModel::update_full_attribute_from_backend( - const module::ConstAttributePtr &full_attr) { - - try { - const QUuid attr_uuid(QUuidFromUuid(full_attr->uuid())); - int row = 0; - for (auto &attribute : attributes_data_) { - - QUuid uuid = uuid_from_attr_data(attribute); - if (uuid == attr_uuid) { - for (const int role : attribute.keys()) { - - if (full_attr->has_role_data(role)) { - QVariant new_role_data = - json_to_qvariant(full_attr->role_data_as_json(role)); - if (new_role_data != attribute[role]) { - attribute[role] = new_role_data; - const QModelIndex index = createIndex(row, 0); - emit dataChanged(index, index, {role + Qt::UserRole + 1}); - } - } - } - } - row++; - } - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } -} - -void ModuleAttrsModel::update_attribute_from_backend( - const utility::Uuid &attr_uuid, const int role, const utility::JsonStore &role_value) { - - try { - - const QUuid quuid = QUuidFromUuid(attr_uuid); - - // special case ... if an attribute has been added to a 'group' (so is dynamically - // added to the Model), or if an attribute is removed from a 'group' (so is dynamiucally - // removed from the Model) then we need to trigger a full update - if (role == Attribute::Groups) { - - bool attr_is_in_this_model = false; - for (auto &p : attributes_data_) { - - if (p.contains(Attribute::UuidRole) && - p[Attribute::UuidRole].toUuid() == quuid) { - attr_is_in_this_model = true; - break; - } - } - - bool attr_should_be_in_this_model = false; - if (role_value.is_array()) { - for (const auto &o : role_value) { - QString attr_group_name = QStringFromStd(o.get()); - if (attrs_group_name_.contains(attr_group_name)) { - attr_should_be_in_this_model = true; - break; - } - } - } - - if (attr_is_in_this_model && !attr_should_be_in_this_model) { - remove_attribute(attr_uuid); - } else if (!attr_is_in_this_model && attr_should_be_in_this_model) { - emit doFullupdateFromBackend(attrs_group_name_); - } - } - - int row = 0; - for (auto &p : attributes_data_) { - - if (p.contains(Attribute::UuidRole) && p[Attribute::UuidRole].toUuid() == quuid) { - if (role_value.is_null()) - p[role] = QVariant(); - else - p[role] = json_to_qvariant(role_value); - const QModelIndex index = createIndex(row, 0); - emit dataChanged(index, index, {role + Qt::UserRole + 1}); - } - row++; - } - } catch ([[maybe_unused]] std::exception &e) { - } -} - -void ModuleAttrsModel::setattributesGroupNames(QStringList group_name) { - - if (attrs_group_name_ != group_name) { - attrs_group_name_ = group_name; - emit attributesGroupNamesChanged(attrs_group_name_); - } -} - -ModuleAttrsToQMLShim::~ModuleAttrsToQMLShim() { - - // wipe the message handler, because it seems to be possible that messages - // come in before our actor companion has exited but AFTER this object - // (ModuleAttrsToQMLShim) has been destroyed - if we don't wipe the message - // handler it gets a bit 'crashy' as it tries to execute a function in - // the handler declared in 'ModuleAttrsToQMLShim::init' when the object is deleted. - set_message_handler([=](caf::actor_companion * /*self*/) -> caf::message_handler { - return caf::message_handler(); - }); - - if (attrs_events_actor_group_) { - caf::scoped_actor sys(CafSystemObject::get_actor_system()); - try { - request_receive( - *sys, attrs_events_actor_group_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch ([[maybe_unused]] const std::exception &e) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - } - disconnect(); -} - - -ModuleAttrsToQMLShim::ModuleAttrsToQMLShim(ModuleAttrsModel *model) - : QMLActor(static_cast(model)), model_(model), uuid_(utility::Uuid::generate()) { - - init(CafSystemObject::get_actor_system()); - - QObject::connect( - model_, - &ModuleAttrsModel::setAttributeFromFrontEnd, - this, - &ModuleAttrsToQMLShim::setAttributeFromFrontEnd); - - QObject::connect( - model_, - &ModuleAttrsModel::attributesGroupNamesChanged, - this, - &ModuleAttrsToQMLShim::setattributesGroupNames); - - QObject::connect( - model_, - &ModuleAttrsModel::doFullupdateFromBackend, - this, - &ModuleAttrsToQMLShim::doFullupdateFromBackend); -} - -ModuleAttrsToQMLShim::ModuleAttrsToQMLShim(ModuleAttrsDirect *qml_attrs_map) - : QMLActor(static_cast(qml_attrs_map)), - qml_attrs_map_(qml_attrs_map), - uuid_(utility::Uuid::generate()) { - - init(CafSystemObject::get_actor_system()); - - QObject::connect( - qml_attrs_map, - &ModuleAttrsDirect::setAttributeFromFrontEnd, - this, - &ModuleAttrsToQMLShim::setAttributeFromFrontEnd); - - QObject::connect( - qml_attrs_map, - &ModuleAttrsDirect::attributesGroupNamesChanged, - this, - &ModuleAttrsToQMLShim::setattributesGroupNames); -} - -ModuleAttrsToQMLShim::ModuleAttrsToQMLShim(ModuleMenusModel *model) - : QMLActor(static_cast(model)), - menu_model_(model), - uuid_(utility::Uuid::generate()) { - - init(CafSystemObject::get_actor_system()); - - QObject::connect( - model, - &ModuleMenusModel::setAttributeFromFrontEnd, - this, - &ModuleAttrsToQMLShim::setAttributeFromFrontEnd); - - QObject::connect( - model, - &ModuleMenusModel::rootMenuNameChanged, - this, - &ModuleAttrsToQMLShim::setRootMenuName); -} - -void ModuleAttrsToQMLShim::init(caf::actor_system &system) { - - QMLActor::init(system); - - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(as_actor())); - - - // this doesn't help with Qt actors.. - self()->set_default_handler(caf::drop); - - attrs_events_actor_ = system.registry().get(module_events_registry); - - try { - caf::scoped_actor sys(system); - attrs_events_actor_group_ = utility::request_receive( - *sys, attrs_events_actor_, utility::get_group_atom_v); - - anon_send(attrs_events_actor_group_, broadcast::join_broadcast_atom_v, as_actor()); - // request_receive(*sys, group, broadcast::join_broadcast_atom_v, as_actor()); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - set_message_handler([=](caf::actor_companion * /*self*/) -> caf::message_handler { - return { - [=](utility::serialise_atom) -> utility::JsonStore { return utility::JsonStore(); }, - [=](broadcast::broadcast_down_atom, const caf::actor_addr &addr) { - if (addr == caf::actor_cast(attrs_events_actor_group_)) { - attrs_events_actor_group_ = caf::actor(); - } - }, - [=](const group_down_msg &g) {}, - [=](full_attributes_description_atom, - const AttributeSet &attrs, - const utility::Uuid &requester_uuid) { - if (requester_uuid == uuid_) { - if (model_) - model_->add_attributes_from_backend(attrs); - if (qml_attrs_map_) - qml_attrs_map_->add_attributes_from_backend(attrs); - if (menu_model_) - menu_model_->add_attributes_from_backend(attrs); - } - }, - [=](full_attributes_description_atom, const AttributeSet &attrs) { - if (model_) - model_->add_attributes_from_backend(attrs, true); - if (qml_attrs_map_) - qml_attrs_map_->add_attributes_from_backend(attrs, true); - if (menu_model_) - menu_model_->add_attributes_from_backend(attrs); - }, - - [=](change_attribute_event_atom, - const utility::Uuid & /*module_uuid*/, - const utility::Uuid &attr_uuid, - const int role, - const utility::JsonStore &role_value) { - if (model_) - model_->update_attribute_from_backend(attr_uuid, role, role_value); - if (qml_attrs_map_) - qml_attrs_map_->update_attribute_from_backend(attr_uuid, role, role_value); - if (menu_model_) - menu_model_->update_attribute_from_backend(attr_uuid, role, role_value); - }, - [=](remove_attrs_from_ui_atom, const std::vector &attr_uuids) { - if (model_) { - for (const auto &attr_uuid : attr_uuids) { - model_->remove_attribute(attr_uuid); - } - } - if (menu_model_) { - for (const auto &attr_uuid : attr_uuids) { - menu_model_->remove_attribute(attr_uuid); - } - } - }, - [=](attribute_deleted_event_atom, const utility::Uuid & /*attr_uuid*/) { - // model_->remove_attribute(attr_uuid); - }}; - }); -} - -void ModuleAttrsToQMLShim::setAttributeFromFrontEnd( - const QUuid property_uuid, const int role, const QVariant role_value) { - try { - auto js = utility::JsonStore(); - if (not role_value.isNull()) - js = utility::JsonStore(qvariant_to_json(role_value)); - - anon_send( - attrs_events_actor_, - change_attribute_request_atom_v, - UuidFromQUuid(property_uuid), - role, - js); - - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } -} - -void ModuleAttrsToQMLShim::setattributesGroupNames(QStringList group_name) { - if (attrs_group_name_ != group_name) { - - // The attributes group name (e.g. 'main_toolbar' or 'playhead') has changed - // (due to something happening in QML code). We therefore request a full list - // of attributes belonging to that group from the backend. - attrs_group_name_ = group_name; - std::vector strs; - for (const auto &n : group_name) { - strs.emplace_back(StdFromQString(n)); - } - anon_send(attrs_events_actor_, request_full_attributes_description_atom_v, strs, uuid_); - } -} - -void ModuleAttrsToQMLShim::doFullupdateFromBackend(QStringList group_name) { - - attrs_group_name_ = group_name; - std::vector strs; - for (const auto &n : group_name) { - strs.emplace_back(StdFromQString(n)); - } - anon_send(attrs_events_actor_, request_full_attributes_description_atom_v, strs, uuid_); -} - -void ModuleAttrsToQMLShim::setRootMenuName(QString root_menu_name) { - - if (attrs_group_name_.empty() || - (attrs_group_name_.size() && attrs_group_name_[0] != root_menu_name)) { - - // The attributes group name (e.g. 'main_toolbar' or 'playhead') has changed - // (due to something happening in QML code). We therefore request a full list - // of attributes belonging to that group from the backend. - if (attrs_group_name_.empty()) { - attrs_group_name_.append(root_menu_name); - } else { - attrs_group_name_[0] = root_menu_name; - } - - anon_send( - attrs_events_actor_, - request_menu_attributes_description_atom_v, - StdFromQString(root_menu_name), - uuid_); - } -} diff --git a/src/ui/qml/module/test/CMakeLists.txt b/src/ui/qml/module/test/CMakeLists.txt deleted file mode 100644 index ca5580c50..000000000 --- a/src/ui/qml/module/test/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -include(CTest) diff --git a/src/ui/qml/playhead/src/CMakeLists.txt b/src/ui/qml/playhead/src/CMakeLists.txt deleted file mode 100644 index 208173f1f..000000000 --- a/src/ui/qml/playhead/src/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core - xstudio::audio_output - xstudio::ui::qml::helper - xstudio::utility -) - -# SET(EXTRAMOC -# "${ROOT_DIR}/include/xstudio/ui/qml/playlist_selection_ui.hpp" -# ) - -create_qml_component(playhead 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/playhead/src/export.h b/src/ui/qml/playhead/src/export.h deleted file mode 100644 index ed4cfde8c..000000000 --- a/src/ui/qml/playhead/src/export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef PLAYHEAD_QML_EXPORT_H -#define PLAYHEAD_QML_EXPORT_H - -#ifdef PLAYHEAD_QML_STATIC_DEFINE -# define PLAYHEAD_QML_EXPORT -# define PLAYHEAD_QML_NO_EXPORT -#else -# ifndef PLAYHEAD_QML_EXPORT -# ifdef playhead_qml_EXPORTS - /* We are building this library */ -# define PLAYHEAD_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define PLAYHEAD_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef PLAYHEAD_QML_NO_EXPORT -# define PLAYHEAD_QML_NO_EXPORT -# endif -#endif - -#ifndef PLAYHEAD_QML_DEPRECATED -# define PLAYHEAD_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef PLAYHEAD_QML_DEPRECATED_EXPORT -# define PLAYHEAD_QML_DEPRECATED_EXPORT PLAYHEAD_QML_EXPORT PLAYHEAD_QML_DEPRECATED -#endif - -#ifndef PLAYHEAD_QML_DEPRECATED_NO_EXPORT -# define PLAYHEAD_QML_DEPRECATED_NO_EXPORT PLAYHEAD_QML_NO_EXPORT PLAYHEAD_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef PLAYHEAD_QML_NO_DEPRECATED -# define PLAYHEAD_QML_NO_DEPRECATED -# endif -#endif - -#endif /* PLAYHEAD_QML_EXPORT_H */ diff --git a/src/ui/qml/playhead/src/include/playhead_qml_export.h b/src/ui/qml/playhead/src/include/playhead_qml_export.h deleted file mode 100644 index ed4cfde8c..000000000 --- a/src/ui/qml/playhead/src/include/playhead_qml_export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef PLAYHEAD_QML_EXPORT_H -#define PLAYHEAD_QML_EXPORT_H - -#ifdef PLAYHEAD_QML_STATIC_DEFINE -# define PLAYHEAD_QML_EXPORT -# define PLAYHEAD_QML_NO_EXPORT -#else -# ifndef PLAYHEAD_QML_EXPORT -# ifdef playhead_qml_EXPORTS - /* We are building this library */ -# define PLAYHEAD_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define PLAYHEAD_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef PLAYHEAD_QML_NO_EXPORT -# define PLAYHEAD_QML_NO_EXPORT -# endif -#endif - -#ifndef PLAYHEAD_QML_DEPRECATED -# define PLAYHEAD_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef PLAYHEAD_QML_DEPRECATED_EXPORT -# define PLAYHEAD_QML_DEPRECATED_EXPORT PLAYHEAD_QML_EXPORT PLAYHEAD_QML_DEPRECATED -#endif - -#ifndef PLAYHEAD_QML_DEPRECATED_NO_EXPORT -# define PLAYHEAD_QML_DEPRECATED_NO_EXPORT PLAYHEAD_QML_NO_EXPORT PLAYHEAD_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef PLAYHEAD_QML_NO_DEPRECATED -# define PLAYHEAD_QML_NO_DEPRECATED -# endif -#endif - -#endif /* PLAYHEAD_QML_EXPORT_H */ diff --git a/src/ui/qml/playhead/src/playhead_ui.cpp b/src/ui/qml/playhead/src/playhead_ui.cpp deleted file mode 100644 index 6b3233f15..000000000 --- a/src/ui/qml/playhead/src/playhead_ui.cpp +++ /dev/null @@ -1,634 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/playhead/playhead.hpp" -#include "xstudio/playhead/playhead_actor.hpp" -#include "xstudio/playhead/playhead_selection_actor.hpp" -#include "xstudio/ui/qml/playhead_ui.hpp" -#include "xstudio/utility/edit_list.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" -#include "xstudio/utility/media_reference.hpp" -#include "xstudio/utility/timecode.hpp" - -using namespace caf; -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::ui::qml; - -PlayheadUI::PlayheadUI(QObject *parent) - : QMLActor(parent), - backend_(), - backend_events_(), - - timecode_start_("00:00:00:00"), - timecode_("00:00:00:00"), - timecode_frames_(1), - timecode_end_("00:00:00:00"), - timecode_duration_("00:00:00:00") {} - -// helper ? - -void PlayheadUI::set_backend(caf::actor backend) { - - if (backend_ == backend) - return; - - scoped_actor sys{system()}; - - bool had_backend = bool(backend_); - - if (backend_) { - self()->demonitor(backend_); - anon_send(backend_, playhead::play_atom_v, false); // make sure we stop playback - anon_send(backend_, module::disconnect_from_ui_atom_v); - } - - backend_ = backend; - // get backend state.. - if (backend_events_) { - - try { - request_receive( - *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch (const std::exception &) { - } - self()->demonitor(backend_events_); - backend_events_ = caf::actor(); - } - - - if (!backend_) { - looping_ = playhead::LoopMode::LM_LOOP; - play_rate_mode_ = TimeSourceMode::FIXED; - playing_ = false; - forward_ = true; - fr_rate_ = FrameRate(timebase::k_flicks_24fps); - fr_playhead_rate_ = FrameRate(timebase::k_flicks_24fps); - velocity_multiplier_ = 1.0f; - loop_start_ = 1; - loop_end_ = 1; - frames_ = 0; - use_loop_range_ = false; - key_playhead_index_ = 0; - source_offset_frames_ = 0; - } else { - self()->monitor(backend_); - try { - - auto detail = request_receive(*sys, backend_, detail_atom_v); - name_ = QString::fromStdString(detail.name_); - uuid_ = detail.uuid_; - - backend_events_ = detail.group_; - request_receive( - *sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); - self()->monitor(backend_events_); - - fr_rate_ = request_receive(*sys, backend_, utility::rate_atom_v); - rate_ = fr_rate_.to_seconds(); - fr_playhead_rate_ = - request_receive(*sys, backend_, playhead::playhead_rate_atom_v); - playhead_rate_ = fr_playhead_rate_.to_seconds(); - velocity_multiplier_ = - request_receive(*sys, backend_, playhead::velocity_multiplier_atom_v); - loop_start_ = - request_receive(*sys, backend_, playhead::simple_loop_start_atom_v); - loop_end_ = request_receive(*sys, backend_, playhead::simple_loop_end_atom_v); - frames_ = request_receive(*sys, backend_, playhead::duration_frames_atom_v); - use_loop_range_ = - request_receive(*sys, backend_, playhead::use_loop_range_atom_v); - key_playhead_index_ = - request_receive(*sys, backend_, playhead::key_playhead_index_atom_v); - source_offset_frames_ = - request_receive(*sys, backend_, media::source_offset_frames_atom_v); - looping_ = - (int)request_receive(*sys, backend_, playhead::loop_atom_v); - play_rate_mode_ = request_receive( - *sys, backend_, playhead::play_rate_mode_atom_v); - playing_ = request_receive(*sys, backend_, playhead::play_atom_v); - forward_ = request_receive(*sys, backend_, playhead::play_forward_atom_v); - - } catch (const std::exception &e) { - source_offset_frames_ = 0; - spdlog::warn("B {} {}", __PRETTY_FUNCTION__, e.what()); - } - } - - loop_mode_options_ = QList({ - QMap( - {{QString("text"), QString("Play Once")}, - {QString("value"), (int)playhead::LoopMode::LM_PLAY_ONCE}}), - QMap( - {{QString("text"), QString("Loop")}, - {QString("value"), (int)playhead::LoopMode::LM_LOOP}}), - QMap( - {{QString("text"), QString("Loop")}, - {QString("value"), (int)playhead::LoopMode::LM_PING_PONG}}), - }); - - emit framesChanged(); - emit secondsChanged(); - emit frameChanged(); - emit secondChanged(); - emit uuidChanged(); - emit backendChanged(); - emit velocityMultiplierChanged(); - emit playingChanged(); - emit loopModeChanged(); - emit nativeChanged(); - emit playRateModeChanged(); - emit rateChanged(); - emit playheadRateChanged(); - emit forwardChanged(); - - emit timecodeStartChanged(); - emit timecodeChanged(); - emit timecodeEndChanged(); - - emit keyPlayheadIndexChanged(); - emit sourceOffsetFramesChanged(); - - media_changed(); - rebuild_cache(); - rebuild_bookmarks(); - emit cachedFramesChanged(); - emit bookmarkedFramesChanged(); - - spdlog::debug("PlayheadUI set_backend {}", to_string(uuid_)); - - if (backend_) { - anon_send(backend_, module::connect_to_ui_atom_v); - } - - if (bool(backend_) != had_backend) { - emit isNullChanged(); - } -} - -void PlayheadUI::initSystem(QObject *system_qobject) { - init(dynamic_cast(system_qobject)->system()); -} - -void PlayheadUI::init(actor_system &system_) { - QMLActor::init(system_); - - spdlog::debug("PlayheadUI init"); - - // self()->set_down_handler([=](down_msg& msg) { - // if(msg.source == store) - // unsubscribe(); - // }); - - // media_uuid_ = QUuid(); - // emit mediaUuidChanged(media_uuid_); - - self()->set_down_handler([=](down_msg &msg) { - if (msg.source == backend_) { - backend_ = caf::actor(); - set_backend(caf::actor()); - } else if (msg.source == backend_events_) { - backend_events_ = caf::actor(); - } - }); - - set_message_handler([=](actor_companion * /*self_*/) -> message_handler { - return { - - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg & /*msg*/) { - // if(msg.source == store_events) - // unsubscribe(); - }, - - [=](utility::event_atom, media::source_offset_frames_atom, const int index) { - source_offset_frames_ = index; - emit(sourceOffsetFramesChanged()); - }, - - [=](utility::event_atom, - media_cache::cached_frames_atom, - const std::vector> &cached_regions) { - cache_detail_.clear(); - for (const auto &r : cached_regions) { - cache_detail_.push_back(QPoint(r.first, r.second)); - } - emit cachedFramesChanged(); - }, - - [=](utility::event_atom, - bookmark::get_bookmarks_atom, - const std::vector> - &bookmarked_regions) { - bookmark_detail_ = bookmarked_regions; - - bookmark_detail_ui_.clear(); - for (const auto &r : bookmark_detail_) { - bookmark_detail_ui_.push_back(new TimesliderMarker( - std::get<2>(r), std::get<3>(r), QStringFromStd(std::get<1>(r)), this)); - } - emit bookmarkedFramesChanged(); - }, - - [=](utility::event_atom, playhead::duration_frames_atom, const size_t frames) { - // something changed in the playhead... - // use this for media changes, which impact timeline - if (frames_ != frames) { - frames_ = frames; - emit framesChanged(); - // reaquire cache.. - rebuild_cache(); - rebuild_bookmarks(); - // emit cachedFramesChanged(); - // emit cachedFramesChanged(); - } - }, - - [=](utility::event_atom, playhead::key_playhead_index_atom, const int index) { - key_playhead_index_ = index; - emit(keyPlayheadIndexChanged()); - emit(compareLayerNameChanged()); - rebuild_cache(); - rebuild_bookmarks(); - // emit cachedFramesChanged(); - }, - - [=](utility::event_atom, playhead::loop_atom, const playhead::LoopMode looping) { - if (looping_ != (int)looping) { - looping_ = (playhead::LoopMode)looping; - emit loopModeChanged(); - } - }, - - [=](utility::event_atom, - playhead::media_source_atom, - const UuidActor &media_actor) { - auto uuid = QUuidFromUuid(media_actor.uuid()); - if (media_uuid_ != uuid) { - media_uuid_ = uuid; - emit mediaUuidChanged(media_uuid_); - } - }, - - [=](utility::event_atom, playhead::play_atom, const bool playing) { - if (playing_ != playing) { - playing_ = playing; - emit playingChanged(); - } - }, - - [=](utility::event_atom, playhead::play_forward_atom, const bool forward) { - if (forward_ != forward) { - forward_ = forward; - emit forwardChanged(); - } - }, - - [=](utility::event_atom, - playhead::play_rate_mode_atom, - const utility::TimeSourceMode tsm) { - if (play_rate_mode_ != tsm) { - play_rate_mode_ = tsm; - emit nativeChanged(); - emit playRateModeChanged(); - } - }, - - [=](utility::event_atom, playhead::playhead_rate_atom, const FrameRate &rate) { - if (fr_playhead_rate_.count() != rate.count()) { - fr_playhead_rate_ = rate; - playhead_rate_ = fr_playhead_rate_.to_seconds(); - emit playheadRateChanged(); - } - }, - - // pretty sure these are redundant.. - // [=](utility::event_atom, playhead::position_atom, const double second) { - // // something changed in the playhead... - // // use this for media changes, which impact timeline - // if (second_ != second) { - // second_ = second; - // emit secondChanged(); - // } - // }, - - // [=](utility::event_atom, playhead::position_atom, const int frame) { - // // something changed in the playhead... - // // use this for media changes, which impact timeline - // if (frame_ != frame) { - // frame_ = frame; - // emit frameChanged(); - // } - // }, - - [=](utility::event_atom, - playhead::position_atom, - const int frame, - const int media_frame, - const int media_logical_frame, - const utility::FrameRate &media_rate, - const timebase::flicks position, - const utility::Timecode &timecode) { - // spdlog::warn("frame {} media_frame {} media_logical_frame {}", frame, - // media_frame, media_logical_frame); - - // spdlog::debug("position_atom changed {}", frame); - auto new_ms = timebase::to_seconds(media_frame * media_rate); - if (new_ms != media_second_) { - media_second_ = new_ms; - emit mediaSecondChanged(); - } - - if (media_frame != media_frame_) { - media_frame_ = media_frame; - // spdlog::debug("frame changed, emit {}", frame); - emit mediaFrameChanged(); - } - new_ms = timebase::to_seconds(media_logical_frame * media_rate); - if (new_ms != media_logical_second_) { - media_logical_second_ = new_ms; - emit mediaLogicalSecondChanged(); - } - - if (media_logical_frame != media_logical_frame_) { - media_logical_frame_ = media_logical_frame; - // spdlog::debug("frame changed, emit {}", frame); - emit mediaLogicalFrameChanged(); - } - if (frame != frame_) { - frame_ = frame; - // spdlog::debug("frame changed, emit {}", frame); - emit frameChanged(); - } - if (timebase::to_seconds(position) != second_) { - second_ = timebase::to_seconds(position); - // spdlog::debug("second changed {}", second_); - emit secondChanged(); - } - - const QString tc_str = QStringFromStd(to_string(timecode)); - if (tc_str != timecode_) { - timecode_ = tc_str; - - timecode_frames_ = timecode.total_frames(); - - emit timecodeFramesChanged(); - emit timecodeChanged(); - } - }, - - [=](utility::event_atom, playhead::simple_loop_end_atom, const int loop_end) { - loop_end_ = loop_end; - emit(loopEndChanged()); - }, - - [=](utility::event_atom, playhead::simple_loop_start_atom, const int loop_start) { - loop_start_ = loop_start; - emit(loopStartChanged()); - }, - - [=](utility::event_atom, playhead::use_loop_range_atom, const bool use_loop_range) { - use_loop_range_ = use_loop_range; - emit(useLoopRangeChanged()); - }, - - [=](utility::event_atom, - playhead::velocity_multiplier_atom, - const float velocity_multiplier) { - if (velocity_multiplier_ != velocity_multiplier) { - velocity_multiplier_ = velocity_multiplier; - emit velocityMultiplierChanged(); - } - }, - - [=](utility::event_atom, utility::change_atom) {}, - - [=](utility::event_atom, utility::last_changed_atom, const time_point &) {}, - - [=](utility::event_atom, utility::name_atom, const std::string &name) { - name_ = QString::fromStdString(name); - emit nameChanged(); - }, - - [=](utility::event_atom, utility::rate_atom, const FrameRate &rate) { - if (fr_rate_.count() != rate.count()) { - fr_rate_ = rate; - rate_ = fr_rate_.to_seconds(); - emit rateChanged(); - } - }}; - }); -} - -void PlayheadUI::media_changed() { - - if (!backend_) - return; - - try { - scoped_actor sys{system()}; - - auto media_actor = request_receive_wait( - *sys, backend_, std::chrono::milliseconds(250), playhead::media_atom_v); - - auto uuid = request_receive_wait( - *sys, media_actor, std::chrono::milliseconds(250), utility::uuid_atom_v); - - auto tmp = QUuidFromUuid(uuid); - if (media_uuid_ != tmp) { - media_uuid_ = tmp; - emit mediaUuidChanged(media_uuid_); - } - } catch ([[maybe_unused]] const std::exception &e) { - if (media_uuid_ != QUuid()) { - media_uuid_ = QUuid(); - emit mediaUuidChanged(media_uuid_); - } - } -} - -void PlayheadUI::setPlaying(const bool playing) { - anon_send(backend_, playhead::play_atom_v, playing); -} - -void PlayheadUI::setFrame(const int frame) { - if (frame != frame_) { - anon_send(backend_, playhead::scrub_frame_atom_v, std::max(0, frame)); - } -} - -void PlayheadUI::setLoopStart(const int loop_start) { - - anon_send(backend_, playhead::simple_loop_start_atom_v, loop_start); -} - -void PlayheadUI::setLoopEnd(const int loop_end) { - - anon_send(backend_, playhead::simple_loop_end_atom_v, loop_end); -} - -void PlayheadUI::setUseLoopRange(const bool use_loop_range) { - - anon_send(backend_, playhead::use_loop_range_atom_v, use_loop_range); -} - -bool PlayheadUI::jumpToNextSource() { - - try { - scoped_actor sys{system()}; - return request_receive(*sys, backend_, playhead::skip_through_sources_atom_v, 1); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return false; -} - -bool PlayheadUI::jumpToPreviousSource() { - try { - scoped_actor sys{system()}; - return request_receive(*sys, backend_, playhead::skip_through_sources_atom_v, -1); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - return false; -} - -void PlayheadUI::setLoopMode(const int looping) { - anon_send(backend_, playhead::loop_atom_v, playhead::LoopMode(looping)); -} - -void PlayheadUI::setNative(const bool native) { - anon_send( - backend_, - playhead::play_rate_mode_atom_v, - (native ? TimeSourceMode::REMAPPED : TimeSourceMode::FIXED)); -} - -void PlayheadUI::setPlayRateMode(const int tsm) { - anon_send( - backend_, playhead::play_rate_mode_atom_v, static_cast(tsm)); -} - -void PlayheadUI::setForward(const bool forward) { - anon_send(backend_, playhead::play_forward_atom_v, forward); -} - -void PlayheadUI::step(int step_frames) { - - anon_send(backend_, playhead::step_atom_v, step_frames); -} - -void PlayheadUI::setPlayheadRate(const double rate) { - anon_send(backend_, playhead::playhead_rate_atom_v, FrameRate(rate)); -} - -void PlayheadUI::setVelocityMultiplier(const float velocity_multiplier) { - anon_send(backend_, playhead::velocity_multiplier_atom_v, velocity_multiplier); -} - -void PlayheadUI::skip(const bool forwards) { - anon_send(backend_, playhead::step_atom_v, forwards ? 1 : -1); -} - -void PlayheadUI::setKeyPlayheadIndex(int i) { - anon_send(backend_, playhead::key_playhead_index_atom_v, i); -} - -void PlayheadUI::setSourceOffsetFrames(int i) { - anon_send(backend_, media::source_offset_frames_atom_v, i); -} - - -QString PlayheadUI::compareLayerName() { - char c[2]; - c[0] = 'A'; - c[1] = 0; - c[0] += (char)key_playhead_index_; - return QString(c); -} - -void PlayheadUI::jumpToSource(const QUuid media_uuid) { - anon_send(backend_, playhead::jump_atom_v, UuidFromQUuid(media_uuid)); -} - -void PlayheadUI::resetReadaheadRequests() { - anon_send(backend_, playhead::jump_atom_v, frame_); -} - -void PlayheadUI::setFitMode(const QString mode) { - if (backend_) { - anon_send( - backend_, - module::change_attribute_request_atom_v, - std::string("Fit Mode"), - (int)module::Attribute::Value, - utility::JsonStore(qvariant_to_json(QVariant(mode)))); - } -} - -void PlayheadUI::connectToUI() { anon_send(backend_, module::connect_to_ui_atom_v); } - -void PlayheadUI::disconnectFromUI() { anon_send(backend_, module::disconnect_from_ui_atom_v); } - -void PlayheadUI::rebuild_cache() { anon_send(backend_, media_cache::cached_frames_atom_v); } - -void PlayheadUI::rebuild_bookmarks() { anon_send(backend_, bookmark::get_bookmarks_atom_v); } - -int PlayheadUI::nextBookmark(const int search_from_frame) const { - auto new_frame = search_from_frame; - auto distance = std::numeric_limits::max(); - for (const auto &i : bookmark_detail_) { - const auto &[u, c, s, e] = i; - if (s > search_from_frame) { - auto dist = s - search_from_frame; - if (dist < distance) { - distance = dist; - new_frame = s; - } - } - } - return std::max(new_frame, 0); -} - -int PlayheadUI::previousBookmark(int search_from_frame) const { - if (search_from_frame == -1) - search_from_frame = frames_ - 1; - - auto new_frame = search_from_frame; - auto distance = std::numeric_limits::max(); - for (const auto &i : bookmark_detail_) { - const auto &[u, c, s, e] = i; - if (s < search_from_frame) { - auto dist = search_from_frame - s; - if (dist < distance) { - distance = dist; - new_frame = s; - } - } - } - return new_frame; -} - -QVariantList PlayheadUI::getNearestBookmark(const int search_from_frame) const { - QVariantList pos; - auto distance = std::numeric_limits::max(); - - for (const auto &i : bookmark_detail_) { - const auto &[u, c, s, e] = i; - - if (s < search_from_frame) { - auto dist = search_from_frame - s; - if (dist < distance) { - distance = dist; - pos.clear(); - pos.push_back(s); - pos.push_back(e); - pos.push_back(QVariant::fromValue(QUuidFromUuid(u))); - } - } - } - - return pos; -} diff --git a/src/ui/qml/playhead/test/CMakeLists.txt b/src/ui/qml/playhead/test/CMakeLists.txt deleted file mode 100644 index ca5580c50..000000000 --- a/src/ui/qml/playhead/test/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -include(CTest) diff --git a/src/ui/qml/session/src/CMakeLists.txt b/src/ui/qml/session/src/CMakeLists.txt index e525aa794..1efa86926 100644 --- a/src/ui/qml/session/src/CMakeLists.txt +++ b/src/ui/qml/session/src/CMakeLists.txt @@ -1,9 +1,8 @@ SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core - Qt5::Test + CAF::core + Qt6::Core + Qt6::Test xstudio::ui::qml::helper - xstudio::ui::qml::tag xstudio::timeline xstudio::utility xstudio::session @@ -12,7 +11,6 @@ SET(LINK_DEPS SET(EXTRAMOC "${ROOT_DIR}/include/xstudio/ui/qml/session_model_ui.hpp" "${ROOT_DIR}/include/xstudio/ui/qml/caf_response_ui.hpp" - #"${ROOT_DIR}/include/xstudio/ui/qml/tag_ui.hpp" ) -create_qml_component(session 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") +create_qml_component(session ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/session/src/caf_response_ui.cpp b/src/ui/qml/session/src/caf_response_ui.cpp index 521acb7ec..ef1adf9e1 100644 --- a/src/ui/qml/session/src/caf_response_ui.cpp +++ b/src/ui/qml/session/src/caf_response_ui.cpp @@ -30,19 +30,7 @@ class CafRequest : public ControllableJob> { role_(role), role_name_(std::move(role_name)) {} - CafRequest( - const nlohmann::json json, - const int role, - const std::string role_name, - const std::map &metadata_paths) - : ControllableJob(), - json_(std::move(json)), - role_(role), - role_name_(std::move(role_name)), - metadata_paths_(metadata_paths) {} - QMap run(JobControl &cjc) override { - QMap result; try { @@ -57,551 +45,686 @@ class CafRequest : public ControllableJob> { switch (role_) { case SessionModel::Roles::thumbnailURLRole: - if (type == "MediaSource") { - // get current source/stream.. - - QString thumburl("qrc:///feather_icons/film.svg"); - try { - // get mediasource stream detail. - auto mr = request_receive( - *sys, - actorFromString(system_, json_.at("actor")), - media::media_reference_atom_v); - if (mr.frame_count() != 0) { - auto middle_frame = (mr.frame_count() - 1) / 2; - thumburl = getThumbnailURL( - system_, - actorFromString(system_, json_.at("actor")), - middle_frame, - true); - result[SessionModel::Roles::thumbnailURLRole] = - QStringFromStd(json(StdFromQString(thumburl)).dump()); - } else { - // ignore leave null.. - // and leave for a retry..? - result[SessionModel::Roles::thumbnailURLRole] = - QStringFromStd(json("RETRY").dump()); - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } + requestThumbnailURLRole(type, sys, system_, result); break; case SessionModel::Roles::rateFPSRole: - if (type == "MediaSource") { - auto rr = request_receive( - *sys, - actorFromString(system_, json_.at("actor")), - media::media_reference_atom_v); + requestRateFPSRole(type, sys, system_, result); + break; - result[SessionModel::Roles::pathRole] = - QStringFromStd(json(to_string(rr.uri())).dump()); - result[SessionModel::Roles::rateFPSRole] = - QStringFromStd(json(rr.rate()).dump()); - } else if (type == "Session") { - auto value = request_receive( - *sys, - actorFromString(system_, json_.at("actor")), - session::media_rate_atom_v); - result[SessionModel::Roles::rateFPSRole] = - QStringFromStd(json(value).dump()); - } + case SessionModel::Roles::bookmarkUuidsRole: + requestBookmarkUuidsRole(type, sys, system_, result); + + break; + case SessionModel::Roles::notificationRole: + requestNotificationRole(type, sys, system_, result); break; case SessionModel::Roles::bitDepthRole: case SessionModel::Roles::resolutionRole: case SessionModel::Roles::formatRole: case SessionModel::Roles::pixelAspectRole: + // this seems wasteful, as we'll have to send the request for each item + // why not populate all four at once ? if (type == "MediaSource") { + std::string path; + switch (role_) { + case SessionModel::Roles::bitDepthRole: + path = "regex:/metadata/media/.*standard_fields/bit_depth"; + break; + case SessionModel::Roles::formatRole: + path = "regex:/metadata/media/.*standard_fields/format"; + break; + case SessionModel::Roles::resolutionRole: + path = "regex:/metadata/media/.*standard_fields/resolution"; + break; + case SessionModel::Roles::pixelAspectRole: + path = "regex:/metadata/media/.*standard_fields/pixel_aspect"; + break; + default: + break; + } + auto data = request_receive( *sys, actorFromString(system_, json_.at("actor")), json_store::get_json_atom_v, - "/metadata/media"); - - nlohmann::json standard_fields; - - if (data.is_array() and not data.empty() and - data.at(0).contains("standard_fields")) { - standard_fields = data.at(0).at("standard_fields"); - } else if ( - data.is_object() and not data.contains("standard_fields") and - not data.empty()) { - standard_fields = data.front().at("standard_fields"); - ; - } else if (data.contains("standard_fields")) { - standard_fields = data.at("standard_fields"); - } + path); - if (standard_fields.contains("bit_depth")) - result[SessionModel::Roles::bitDepthRole] = - QStringFromStd(standard_fields.at("bit_depth").dump()); - if (standard_fields.contains("format")) - result[SessionModel::Roles::formatRole] = - QStringFromStd(standard_fields.at("format").dump()); - if (standard_fields.contains("resolution")) - result[SessionModel::Roles::resolutionRole] = - QStringFromStd(standard_fields.at("resolution").dump()); - if (standard_fields.contains("pixel_aspect")) - result[SessionModel::Roles::pixelAspectRole] = - QStringFromStd(standard_fields.at("pixel_aspect").dump()); + result[role_] = QStringFromStd(data.dump()); } break; - case SessionModel::Roles::pathRole: - if (type == "Session") { - auto pt = request_receive>( - *sys, - actorFromString(system_, json_.at("actor")), - session::path_atom_v); - - result[SessionModel::Roles::pathRole] = - QStringFromStd(json(to_string(pt.first)).dump()); - result[SessionModel::Roles::mtimeRole] = - QStringFromStd(json(pt.second).dump()); - } else if (type == "MediaSource") { - auto rr = request_receive( - *sys, - actorFromString(system_, json_.at("actor")), - media::media_reference_atom_v); - result[SessionModel::Roles::pathRole] = - QStringFromStd(json(to_string(rr.uri())).dump()); - result[SessionModel::Roles::rateFPSRole] = - QStringFromStd(json(rr.rate()).dump()); - } + case SessionModel::timecodeAsFramesRole: + case SessionModel::Roles::pathShakeRole: + case SessionModel::Roles::pathRole: + requestMediaReference(type, sys, system_, result); break; + case SessionModel::Roles::nameRole: case SessionModel::Roles::actorUuidRole: case SessionModel::Roles::groupActorRole: case SessionModel::Roles::typeRole: - if (type == "Session" or type == "Playlist" or type == "Subset" or - type == "Timeline" or type == "Media" or type == "PlayheadSelection" or - type == "Playhead") { - - auto actor = caf::actor(); - - if (json_.count("actor") and not json_.at("actor").is_null()) { - actor = actorFromString(system_, json_.at("actor")); - } else if ( - not json_.at("actor_owner").is_null() and type == "PlayheadSelection") { - // get selection actor from owner - actor = request_receive( - *sys, - actorFromString(system_, json_.at("actor_owner")), - playlist::selection_actor_atom_v); - - result[SessionModel::Roles::actorRole] = - QStringFromStd(json(actorToString(system_, actor)).dump()); - } else if (not json_.at("actor_owner").is_null() and type == "Playhead") { - // get selection actor from owner - - auto playhead = request_receive( - *sys, - actorFromString(system_, json_.at("actor_owner")), - playlist::get_playhead_atom_v); - - result[SessionModel::Roles::actorRole] = QStringFromStd( - json(actorToString(system_, playhead.actor())).dump()); - - result[SessionModel::Roles::actorUuidRole] = - QStringFromStd(json(to_string(playhead.uuid())).dump()); - } + requestActorInfo(type, sys, system_, result); + break; - if (actor) { - // get detail. - auto detail = request_receive( - *sys, actor, utility::detail_atom_v); - - result[SessionModel::Roles::groupActorRole] = - QStringFromStd(json(actorToString(system_, detail.group_)).dump()); - result[SessionModel::Roles::nameRole] = - QStringFromStd(json(detail.name_).dump()); - result[SessionModel::Roles::actorUuidRole] = - QStringFromStd(json(detail.uuid_).dump()); - result[SessionModel::Roles::typeRole] = - QStringFromStd(json(detail.type_).dump()); - } - } + case SessionModel::expandedRole: + requestExpandedRole(type, sys, system_, result); + break; + + case SessionModel::mediaDisplayInfoRole: + requestMediaDisplayInfoRole(type, sys, system_, result); break; case SessionModel::Roles::mediaStatusRole: - if (type == "Media" or type == "MediaSource") { - auto target = actorFromString(system_, json_.at("actor")); - - if (target) { - try { - auto answer = request_receive( - *sys, target, media::media_status_atom_v); - - result[SessionModel::Roles::mediaStatusRole] = - QStringFromStd(json(answer).dump()); - } catch (...) { - // silence if no sources.. - } - } - } + requestMediaStatusRole(type, sys, system_, result); break; case SessionModel::Roles::imageActorUuidRole: - if (type == "Media") { - auto target = actorFromString(system_, json_.at("actor")); - if (target) { - auto answer = request_receive( - *sys, target, media::current_media_source_atom_v, media::MT_IMAGE); - - result[SessionModel::Roles::imageActorUuidRole] = - QStringFromStd(json(answer.uuid()).dump()); - } - } else if (type == "MediaSource") { - auto answer = request_receive( - *sys, - actorFromString(system_, json_.at("actor")), - media::current_media_stream_atom_v, - media::MT_IMAGE); - - result[SessionModel::Roles::imageActorUuidRole] = - QStringFromStd(json(answer.uuid()).dump()); - } + requestImageActorUuidRole(type, sys, system_, result); break; case SessionModel::Roles::flagColourRole: case SessionModel::Roles::flagTextRole: - if (type == "Media") { - auto target = actorFromString(system_, json_.at("actor")); - if (target) { - auto [flag, text] = - request_receive>( - *sys, target, playlist::reflag_container_atom_v); - - result[SessionModel::Roles::flagColourRole] = - QStringFromStd(json(flag).dump()); - result[SessionModel::Roles::flagTextRole] = - QStringFromStd(json(text).dump()); - } - } + requestFlagColourRole(type, sys, system_, result); break; - case SessionModel::Roles::selectionRole: { - if (type == "PlayheadSelection") { - auto target = actorFromString(system_, json_.at("actor")); - if (target) { - auto selection = request_receive>( - *sys, target, playhead::get_selection_atom_v); - - auto j = nlohmann::json::array(); - for (const auto &uuid : selection) { - j.push_back(uuid); - } + case SessionModel::Roles::selectionRole: + requestSelectionRole(type, sys, system_, result); + break; - result[SessionModel::Roles::selectionRole] = QStringFromStd(j.dump()); - } - } - } break; case SessionModel::Roles::audioActorUuidRole: - if (type == "Media") { - try { - auto answer = request_receive( - *sys, - actorFromString(system_, json_.at("actor")), - media::current_media_source_atom_v, - media::MT_AUDIO); - - result[SessionModel::Roles::audioActorUuidRole] = - QStringFromStd(json(answer.uuid()).dump()); - } catch (...) { - // no audio source.. - result[SessionModel::Roles::audioActorUuidRole] = - QStringFromStd(json(Uuid()).dump()); - } - } else if (type == "MediaSources") { - try { - auto answer = request_receive( - *sys, - actorFromString(system_, json_.at("actor")), - media::current_media_stream_atom_v, - media::MT_AUDIO); - - result[SessionModel::Roles::audioActorUuidRole] = - QStringFromStd(json(answer.uuid()).dump()); - } catch (...) { - // no audio source.. - result[SessionModel::Roles::audioActorUuidRole] = - QStringFromStd(json(Uuid()).dump()); - } - } + requestAudioActorUuidRole(type, sys, system_, result); break; - case SessionModel::Roles::childrenRole: - // spdlog::error("SessionModel::Roles::childrenRole {}", type); - if (type == "Session") { - auto session = actorFromString(system_, json_.at("actor")); - auto containers = request_receive( - *sys, session, playlist::get_container_atom_v); - auto actors = request_receive>( - *sys, session, session::get_playlists_atom_v); - - // session tree contains... - - result[SessionModel::Roles::childrenRole] = - QStringFromStd(SessionModel::sessionTreeToJson( - containers, system_, uuidactor_vect_to_map(actors)) - .at("children") - .dump()); - - } else if (type == "Media List") { - // spdlog::error( - // "get Media List children {} {}", - // json_.dump(2), - // to_string(actorFromString(system_, json_.at("actor_owner"))) - // ); - - auto detail = request_receive>( - *sys, - actorFromString(system_, json_.at("actor_owner")), - playlist::get_media_atom_v, - true); + case JSONTreeModel::Roles::childrenRole: + requestChildrenRole(type, sys, system_, result); - auto jsn = R"([])"_json; - for (const auto &i : detail) - jsn.emplace_back(SessionModel::containerDetailToJson(i, system_)); + default: + break; + } - result[SessionModel::Roles::childrenRole] = QStringFromStd(jsn.dump()); - } else if (type == "Clip") { - auto target = actorFromString(system_, json_.at("actor")); + } catch (const std::exception &err) { - auto mua = request_receive( - *sys, target, playlist::get_media_atom_v); - auto detail = request_receive( - *sys, mua.actor(), utility::detail_atom_v); + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } - auto jsn = R"([])"_json; - jsn.emplace_back(SessionModel::containerDetailToJson(detail, system_)); + return result; + } - result[SessionModel::Roles::childrenRole] = QStringFromStd(jsn.dump()); - } else if (type == "Container List") { - // // only happens from playlist. - auto containers = request_receive( - *sys, - actorFromString(system_, json_.at("actor_owner")), - playlist::get_container_atom_v); - auto actors = request_receive>( - *sys, - actorFromString(system_, json_.at("actor_owner")), - playlist::get_container_atom_v, + private: + void requestThumbnailURLRole( + const std::string &type, + scoped_actor &sys, + caf::actor_system &system_, + QMap &result) { + if (type == "MediaSource") { + // get current source/stream.. + + QString thumburl("qrc:///feather_icons/film.svg"); + try { + // get mediasource stream detail. + auto mr = request_receive( + *sys, + actorFromString(system_, json_.at("actor")), + media::media_reference_atom_v); + if (mr.frame_count() != 0) { + auto middle_frame = (mr.frame_count() - 1) / 2; + thumburl = getThumbnailURL( + system_, + actorFromString(system_, json_.at("actor")), + middle_frame, true); + result[SessionModel::Roles::thumbnailURLRole] = + QStringFromStd(json(StdFromQString(thumburl)).dump()); - result[SessionModel::Roles::childrenRole] = - QStringFromStd(SessionModel::playlistTreeToJson( - containers, system_, uuidactor_vect_to_map(actors)) - .at("children") - .dump()); - - } else if (type == "Media") { - auto target = actorFromString(system_, json_.at("actor")); - if (target) { - auto detail = request_receive>( - *sys, target, utility::detail_atom_v, true); - auto jsn = R"([])"_json; - for (const auto &i : detail) - jsn.emplace_back(SessionModel::containerDetailToJson(i, system_)); - - result[SessionModel::Roles::childrenRole] = QStringFromStd(jsn.dump()); - } - } else if (type == "TimelineItem") { - auto owner = actorFromString(system_, json_.at("actor_owner")); - auto item = - request_receive(*sys, owner, timeline::item_atom_v); - - // we want our own instance of the item.. - result[JSONTreeModel::Roles::JSONTextRole] = - QStringFromStd(item.serialise().dump()); - - // spdlog::warn("{}", jsn.dump(2)); - // auto jsn = SessionModel::timelineItemToJson(item, system_); - // result[SessionModel::Roles::rateRole] = - // QStringFromStd(jsn.at("rate").dump()); - // result[SessionModel::Roles::trimmedRangeRole] = - // QStringFromStd(jsn.at("trimmed_range").dump()); - // result[SessionModel::Roles::activeRangeRole] = - // QStringFromStd(jsn.at("active_range").dump()); - // result[SessionModel::Roles::availableRangeRole] = - // QStringFromStd(jsn.at("available_range").dump()); - // result[SessionModel::Roles::enabledRole] = - // QStringFromStd(jsn.at("enabled").dump()); - // result[SessionModel::Roles::transparentRole] = - // QStringFromStd(jsn.at("transparent").dump()); - // result[SessionModel::Roles::uuidRole] = - // QStringFromStd(jsn.at("uuid").dump()); - // result[SessionModel::Roles::actorRole] = - // QStringFromStd(jsn.at("actor").dump()); - // // result[SessionModel::Roles::typeRole] = - // QStringFromStd(jsn.at("type").dump()); - - // result[SessionModel::Roles::childrenRole] = - // QStringFromStd(jsn.at("children").dump()); we can also update other - // fields.. - - } else if (type == "MediaSource") { - auto idetail = request_receive>( - *sys, - actorFromString(system_, json_.at("actor")), - utility::detail_atom_v, - media::MT_IMAGE); - auto adetail = request_receive>( - *sys, - actorFromString(system_, json_.at("actor")), - utility::detail_atom_v, - media::MT_AUDIO); - - auto jsn = R"([{},{}])"_json; - - jsn[0] = SessionModel::createEntry( - R"({"type": "Image Stream", "children": [], "actor_owner": null})"_json); - jsn[0]["actor_owner"] = json_.at("actor"); - jsn[1] = SessionModel::createEntry( - R"({"type": "Audio Stream", "children": [], "actor_owner": null})"_json); - jsn[1]["actor_owner"] = json_.at("actor"); - - for (const auto &i : idetail) - jsn[0]["children"].emplace_back( - SessionModel::containerDetailToJson(i, system_)); - for (const auto &i : adetail) - jsn[1]["children"].emplace_back( - SessionModel::containerDetailToJson(i, system_)); - - result[SessionModel::Roles::childrenRole] = QStringFromStd(jsn.dump()); - } else if (type == "PlayheadSelection") { - auto actor = caf::actor(); - // spdlog::warn("request children PlayheadSelection {}", json_.dump(2)); - - if (not json_.at("actor").is_null()) { - actor = actorFromString(system_, json_.at("actor")); - } else if (not json_.at("actor_owner").is_null()) { - // spdlog::warn("PlayheadSelection {}", - // json_.at("actor_owner").get()); get selection actor from - // owner - actor = request_receive( - *sys, - actorFromString(system_, json_.at("actor_owner")), - playlist::selection_actor_atom_v); - // get detail. - auto detail = request_receive( - *sys, actor, utility::detail_atom_v); - - result[SessionModel::Roles::groupActorRole] = - QStringFromStd(json(actorToString(system_, detail.group_)).dump()); - result[SessionModel::Roles::nameRole] = - QStringFromStd(json(detail.name_).dump()); - result[SessionModel::Roles::actorRole] = - QStringFromStd(json(actorToString(system_, actor)).dump()); - result[SessionModel::Roles::actorUuidRole] = - QStringFromStd(json(detail.uuid_).dump()); - result[SessionModel::Roles::typeRole] = - QStringFromStd(json(detail.type_).dump()); - - // spdlog::warn("{}", detail.name_); - } + result[SessionModel::Roles::timecodeAsFramesRole] = + QStringFromStd(json(mr.timecode().total_frames()).dump()); + result[SessionModel::Roles::pathRole] = + QStringFromStd(json(to_string(mr.uri())).dump()); + result[SessionModel::Roles::pathShakeRole] = QStringFromStd( + json(to_string(mr.uri(MediaReference::FramePadFormat::FPF_SHAKE))) + .dump()); + result[SessionModel::Roles::rateFPSRole] = + QStringFromStd(json(mr.rate()).dump()); + } else { + // ignore leave null.. + // and leave for a retry..? + result[SessionModel::Roles::thumbnailURLRole] = + QStringFromStd(json("RETRY").dump()); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + } - if (actor) { - auto selection = request_receive( - *sys, actor, playhead::get_selection_atom_v); - auto jsn = R"([])"_json; - auto actor_str = actorToString(system_, actor); - for (const auto &i : selection) { - auto sel = SessionModel::createEntry( - R"({"type": "Uuid", "uuid": null, "actor_owner": null})"_json); - sel.erase("children"); - sel["uuid"] = i; - sel["actor_owner"] = actor_str; - jsn.emplace_back(sel); - } - // spdlog::warn("{}", jsn.dump(2)); - result[SessionModel::Roles::childrenRole] = QStringFromStd(jsn.dump()); - } + void requestRateFPSRole( + const std::string &type, + scoped_actor &sys, + caf::actor_system &system_, + QMap &result) { + if (type == "MediaSource") { + auto rr = request_receive( + *sys, + actorFromString(system_, json_.at("actor")), + media::media_reference_atom_v); + + result[SessionModel::Roles::timecodeAsFramesRole] = + QStringFromStd(json(rr.timecode().total_frames()).dump()); + result[SessionModel::Roles::pathRole] = + QStringFromStd(json(to_string(rr.uri())).dump()); + result[SessionModel::Roles::pathShakeRole] = QStringFromStd( + json(to_string(rr.uri(MediaReference::FramePadFormat::FPF_SHAKE))).dump()); + result[SessionModel::Roles::rateFPSRole] = QStringFromStd(json(rr.rate()).dump()); + } else if (type == "Session") { + auto value = request_receive( + *sys, actorFromString(system_, json_.at("actor")), session::media_rate_atom_v); + result[SessionModel::Roles::rateFPSRole] = QStringFromStd(json(value).dump()); + } + } - } else if (type == "Playhead") { - // Playhead has no children - } else { - spdlog::warn("CafRequest unhandled ChildrenRole type {}", type); + void requestBookmarkUuidsRole( + const std::string &type, + scoped_actor &sys, + caf::actor_system &system_, + QMap &result) { + if (type == "Media") { + + auto uids = request_receive( + *sys, + actorFromString(system_, json_.at("actor")), + bookmark::get_bookmarks_atom_v); + auto j = nlohmann::json::array(); + for (const auto &uuid : uids) { + j.push_back(uuid); + } + result[SessionModel::Roles::bookmarkUuidsRole] = QStringFromStd(j.dump()); + } + } + + void requestNotificationRole( + const std::string &type, + scoped_actor &sys, + caf::actor_system &system_, + QMap &result) { + + auto actor = actorFromString(system_, json_.at("actor")); + + if (actor) { + // if group is null join it.. + if (json_.at("group_actor").is_null()) { + auto detail = + request_receive(*sys, actor, utility::detail_atom_v); + + result[SessionModel::Roles::groupActorRole] = + QStringFromStd(json(actorToString(system_, detail.group_)).dump()); + } + + auto n = request_receive( + *sys, + actorFromString(system_, json_.at("actor")), + utility::notification_atom_v); + + result[SessionModel::Roles::notificationRole] = QStringFromStd(n.dump()); + } + } + + void requestMediaReference( + const std::string &type, + scoped_actor &sys, + caf::actor_system &system_, + QMap &result) { + if (type == "Session") { + + auto pt = request_receive>( + *sys, actorFromString(system_, json_.at("actor")), session::path_atom_v); + + result[SessionModel::Roles::pathRole] = + QStringFromStd(json(to_string(pt.first)).dump()); + result[SessionModel::Roles::mtimeRole] = QStringFromStd(json(pt.second).dump()); + + } else if (type == "MediaSource") { + auto rr = request_receive( + *sys, + actorFromString(system_, json_.at("actor")), + media::media_reference_atom_v); + + result[SessionModel::Roles::timecodeAsFramesRole] = + QStringFromStd(json(rr.timecode().total_frames()).dump()); + result[SessionModel::Roles::pathRole] = + QStringFromStd(json(to_string(rr.uri())).dump()); + result[SessionModel::Roles::pathShakeRole] = QStringFromStd( + json(to_string(rr.uri(MediaReference::FramePadFormat::FPF_SHAKE))).dump()); + result[SessionModel::Roles::rateFPSRole] = QStringFromStd(json(rr.rate()).dump()); + } + } + + void requestActorInfo( + const std::string &type, + scoped_actor &sys, + caf::actor_system &system_, + QMap &result) { + if (type == "Session" or type == "Playlist" or type == "ContactSheet" or + type == "Subset" or type == "Timeline" or type == "Media" or + type == "PlayheadSelection" or type == "Playhead") { + + auto actor = caf::actor(); + + if (json_.count("actor") and not json_.at("actor").is_null()) { + actor = actorFromString(system_, json_.at("actor")); + } else if (not json_.at("actor_owner").is_null() and type == "PlayheadSelection") { + // get selection actor from owner + actor = request_receive( + *sys, + actorFromString(system_, json_.at("actor_owner")), + playlist::selection_actor_atom_v); + + result[SessionModel::Roles::actorRole] = + QStringFromStd(json(actorToString(system_, actor)).dump()); + } else if (not json_.at("actor_owner").is_null() and type == "Playhead") { + // get selection actor from owner + + auto playhead = request_receive( + *sys, + actorFromString(system_, json_.at("actor_owner")), + playlist::get_playhead_atom_v); + + result[SessionModel::Roles::actorRole] = + QStringFromStd(json(actorToString(system_, playhead.actor())).dump()); + + result[SessionModel::Roles::actorUuidRole] = + QStringFromStd(json(to_string(playhead.uuid())).dump()); + } + + if (actor) { + // get detail. + auto detail = + request_receive(*sys, actor, utility::detail_atom_v); + + result[SessionModel::Roles::groupActorRole] = + QStringFromStd(json(actorToString(system_, detail.group_)).dump()); + result[SessionModel::Roles::nameRole] = + QStringFromStd(json(detail.name_).dump()); + result[SessionModel::Roles::actorUuidRole] = + QStringFromStd(json(detail.uuid_).dump()); + result[SessionModel::Roles::typeRole] = + QStringFromStd(json(detail.type_).dump()); + } + } + } + + void requestImageActorUuidRole( + const std::string &type, + scoped_actor &sys, + caf::actor_system &system_, + QMap &result) { + if (type == "Media") { + auto target = actorFromString(system_, json_.at("actor")); + if (target) { + auto answer = request_receive( + *sys, target, media::current_media_source_atom_v, media::MT_IMAGE); + + result[SessionModel::Roles::imageActorUuidRole] = + QStringFromStd(json(answer.uuid()).dump()); + } + } else if (type == "MediaSource") { + auto answer = request_receive( + *sys, + actorFromString(system_, json_.at("actor")), + media::current_media_stream_atom_v, + media::MT_IMAGE); + + result[SessionModel::Roles::imageActorUuidRole] = + QStringFromStd(json(answer.uuid()).dump()); + } + } + + + void requestFlagColourRole( + const std::string &type, + scoped_actor &sys, + caf::actor_system &system_, + QMap &result) { + if (type == "Media") { + auto target = actorFromString(system_, json_.at("actor")); + if (target) { + auto [flag, text] = request_receive>( + *sys, target, playlist::reflag_container_atom_v); + + result[SessionModel::Roles::flagColourRole] = QStringFromStd(json(flag).dump()); + result[SessionModel::Roles::flagTextRole] = QStringFromStd(json(text).dump()); + } + } + } + + void requestAudioActorUuidRole( + const std::string &type, + scoped_actor &sys, + caf::actor_system &system_, + QMap &result) { + if (type == "Media") { + try { + auto answer = request_receive( + *sys, + actorFromString(system_, json_.at("actor")), + media::current_media_source_atom_v, + media::MT_AUDIO); + + result[SessionModel::Roles::audioActorUuidRole] = + QStringFromStd(json(answer.uuid()).dump()); + } catch (...) { + // no audio source.. + result[SessionModel::Roles::audioActorUuidRole] = + QStringFromStd(json(Uuid()).dump()); + } + } else if (type == "MediaSource") { + try { + auto answer = request_receive( + *sys, + actorFromString(system_, json_.at("actor")), + media::current_media_stream_atom_v, + media::MT_AUDIO); + + result[SessionModel::Roles::audioActorUuidRole] = + QStringFromStd(json(answer.uuid()).dump()); + } catch (...) { + // no audio source.. + result[SessionModel::Roles::audioActorUuidRole] = + QStringFromStd(json(Uuid()).dump()); + } + } + } + + void requestSelectionRole( + const std::string &type, + scoped_actor &sys, + caf::actor_system &system_, + QMap &result) { + if (type == "PlayheadSelection" && !json_.at("actor").is_null()) { + auto target = actorFromString(system_, json_.at("actor")); + if (target) { + + auto selection = request_receive( + *sys, target, playhead::get_selection_atom_v); + + auto j = nlohmann::json::array(); + for (const auto &uuid : selection) { + j.push_back(uuid); } - break; - default: + result[SessionModel::Roles::selectionRole] = QStringFromStd(j.dump()); + } + } + } - if (not metadata_paths_.empty()) { - const int max_index = metadata_paths_.rbegin()->first; - auto r = nlohmann::json::array(); - if (type == "Media") { - - for (int idx = 0; idx <= max_index; idx++) { - if (metadata_paths_.find(idx) == metadata_paths_.end()) { - r.push_back(nullptr); - continue; - } - if (metadata_paths_.find(idx)->second.empty()) { - r.push_back(nullptr); - continue; - } - try { - // get media actor to try the current media source - // if it doesn't have this metadata item iteself - auto data = request_receive( - *sys, - actorFromString(system_, json_.at("actor")), - json_store::get_json_atom_v, - metadata_paths_.find(idx)->second, - true); - r.push_back(data); - } catch (...) { - r.push_back(nullptr); - } // suppress 'no metadata' warnings - } - - } else if (type == "MediaSource") { - - for (int idx = 0; idx <= max_index; idx++) { - if (metadata_paths_.find(idx) == metadata_paths_.end()) { - r.push_back(nullptr); - continue; - } - if (metadata_paths_.find(idx)->second.empty()) { - r.push_back(nullptr); - continue; - } - try { - auto data = request_receive( - *sys, - actorFromString(system_, json_.at("actor")), - json_store::get_json_atom_v, - metadata_paths_.find(idx)->second); - r.push_back(data); - } catch (...) { - r.push_back(nullptr); - } // suppress 'no metadata' warnings - } - } - result[role_] = QStringFromStd(r.dump()); + void requestMediaStatusRole( + const std::string &type, + scoped_actor &sys, + caf::actor_system &system_, + QMap &result) { + if (type == "Media" or type == "MediaSource") { + auto target = actorFromString(system_, json_.at("actor")); + + if (target) { + try { + auto answer = request_receive( + *sys, target, media::media_status_atom_v); + result[SessionModel::Roles::mediaStatusRole] = + QStringFromStd(json(answer).dump()); + } catch (...) { + // silence if no sources.. } - break; } + } + } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + + void requestExpandedRole( + const std::string &type, + scoped_actor &sys, + caf::actor_system &system_, + QMap &result) { + if (type == "Playlist") { + auto target = actorFromString(system_, json_.at("actor")); + if (target) { + try { + auto answer = + request_receive(*sys, target, playlist::expanded_atom_v); + result[SessionModel::Roles::expandedRole] = + QStringFromStd(json(answer).dump()); + } catch (std::exception &e) { + // silence if no sources.. + } + } } + } - return result; + void requestMediaDisplayInfoRole( + const std::string &type, + scoped_actor &sys, + caf::actor_system &system_, + QMap &result) { + if (type == "Media") { + auto target = actorFromString(system_, json_.at("actor")); + + if (target) { + try { + auto answer = request_receive( + *sys, target, media::media_display_info_atom_v); + result[SessionModel::Roles::mediaDisplayInfoRole] = + QStringFromStd(answer.dump()); + } catch (std::exception &e) { + // silence if no sources.. + } + } + } } + void requestChildrenRole( + const std::string &type, + scoped_actor &sys, + caf::actor_system &system_, + QMap &result) { + if (type == "Session") { + auto session = actorFromString(system_, json_.at("actor")); + auto containers = request_receive( + *sys, session, playlist::get_container_atom_v); + auto actors = request_receive>( + *sys, session, session::get_playlists_atom_v); + + // session tree contains... + + result[JSONTreeModel::Roles::childrenRole] = + QStringFromStd(SessionModel::sessionTreeToJson( + containers, system_, uuidactor_vect_to_map(actors)) + .at("children") + .dump()); + + } else if (type == "Media List") { + // spdlog::error( + // "get Media List children {} {}", + // json_.dump(2), + // to_string(actorFromString(system_, json_.at("actor_owner"))) + // ); + + auto detail = request_receive>( + *sys, + actorFromString(system_, json_.at("actor_owner")), + playlist::get_media_atom_v, + true); + + auto jsn = R"([])"_json; + for (const auto &i : detail) + jsn.emplace_back(SessionModel::containerDetailToJson(i, system_)); + + // also get media actors current sources. + + auto current_sources = request_receive>>>( + *sys, + actorFromString(system_, json_.at("actor_owner")), + media::current_media_source_atom_v); + + + auto source_map = + std::map>(); + for (const auto &i : current_sources) + source_map[i.first.uuid()] = + std::make_pair(i.second.first.uuid(), i.second.second.uuid()); + + for (auto &i : jsn) { + auto it = source_map.find(i.value("actor_uuid", Uuid())); + if (it != source_map.end()) { + i["image_actor_uuid"] = it->second.first; + i["audio_actor_uuid"] = it->second.second; + } + } + + result[JSONTreeModel::Roles::childrenRole] = QStringFromStd(jsn.dump()); + } else if (type == "Clip") { + auto target = actorFromString(system_, json_.at("actor")); + + auto mua = + request_receive(*sys, target, playlist::get_media_atom_v); + auto detail = + request_receive(*sys, mua.actor(), utility::detail_atom_v); + + auto jsn = R"([])"_json; + jsn.emplace_back(SessionModel::containerDetailToJson(detail, system_)); + + result[JSONTreeModel::Roles::childrenRole] = QStringFromStd(jsn.dump()); + } else if (type == "Container List") { + // // only happens from playlist. + auto containers = request_receive( + *sys, + actorFromString(system_, json_.at("actor_owner")), + playlist::get_container_atom_v); + auto actors = request_receive>( + *sys, + actorFromString(system_, json_.at("actor_owner")), + playlist::get_container_atom_v, + true); + + result[JSONTreeModel::Roles::childrenRole] = + QStringFromStd(SessionModel::playlistTreeToJson( + containers, system_, uuidactor_vect_to_map(actors)) + .at("children") + .dump()); + + } else if (type == "Media") { + auto target = actorFromString(system_, json_.at("actor")); + if (target) { + auto detail = request_receive>( + *sys, target, utility::detail_atom_v, true); + auto jsn = R"([])"_json; + for (const auto &i : detail) + jsn.emplace_back(SessionModel::containerDetailToJson(i, system_)); + + result[JSONTreeModel::Roles::childrenRole] = QStringFromStd(jsn.dump()); + } + } else if (type == "TimelineItem") { + auto owner = actorFromString(system_, json_.at("actor_owner")); + auto item = request_receive(*sys, owner, timeline::item_atom_v); + + // we want our own instance of the item.. + result[JSONTreeModel::Roles::JSONTextRole] = + QStringFromStd(item.serialise().dump()); + + // spdlog::warn("{}", item.serialise().dump(2)); + } else if (type == "MediaSource") { + + auto idetail = request_receive>( + *sys, + actorFromString(system_, json_.at("actor")), + utility::detail_atom_v, + media::MT_IMAGE); + auto adetail = request_receive>( + *sys, + actorFromString(system_, json_.at("actor")), + utility::detail_atom_v, + media::MT_AUDIO); + + auto jsn = R"([{},{}])"_json; + + jsn[0] = SessionModel::createEntry( + R"({"type": "Image Stream", "children": [], "actor_owner": null})"_json); + jsn[0]["actor_owner"] = json_.at("actor"); + jsn[1] = SessionModel::createEntry( + R"({"type": "Audio Stream", "children": [], "actor_owner": null})"_json); + jsn[1]["actor_owner"] = json_.at("actor"); + + for (const auto &i : idetail) + jsn[0]["children"].emplace_back( + SessionModel::containerDetailToJson(i, system_)); + for (const auto &i : adetail) + jsn[1]["children"].emplace_back( + SessionModel::containerDetailToJson(i, system_)); + result[JSONTreeModel::Roles::childrenRole] = QStringFromStd(jsn.dump()); + } else if (type == "PlayheadSelection") { + auto actor = caf::actor(); + // spdlog::warn("request children PlayheadSelection {}", json_.dump(2)); + + if (not json_.at("actor").is_null()) { + actor = actorFromString(system_, json_.at("actor")); + } else if (not json_.at("actor_owner").is_null()) { + // spdlog::warn("PlayheadSelection {}", + // json_.at("actor_owner").get()); get selection actor from + // owner + actor = request_receive( + *sys, + actorFromString(system_, json_.at("actor_owner")), + playlist::selection_actor_atom_v); + // get detail. + auto detail = + request_receive(*sys, actor, utility::detail_atom_v); + + result[SessionModel::Roles::groupActorRole] = + QStringFromStd(json(actorToString(system_, detail.group_)).dump()); + result[SessionModel::Roles::nameRole] = + QStringFromStd(json(detail.name_).dump()); + result[SessionModel::Roles::actorRole] = + QStringFromStd(json(actorToString(system_, actor)).dump()); + result[SessionModel::Roles::actorUuidRole] = + QStringFromStd(json(detail.uuid_).dump()); + result[SessionModel::Roles::typeRole] = + QStringFromStd(json(detail.type_).dump()); + } + + if (actor) { + auto selection = + request_receive(*sys, actor, playhead::get_selection_atom_v); + auto jsn = R"([])"_json; + auto actor_str = actorToString(system_, actor); + for (const auto &i : selection) { + auto sel = SessionModel::createEntry( + R"({"type": "Uuid", "uuid": null, "actor_owner": null})"_json); + sel.erase("children"); + sel["uuid"] = i; + sel["actor_owner"] = actor_str; + jsn.emplace_back(sel); + } + result[JSONTreeModel::Roles::childrenRole] = QStringFromStd(jsn.dump()); + } + } else if (type == "Playhead") { + // Playhead has no children + } else { + spdlog::warn("CafRequest unhandled ChildrenRole type {} {}", type, json_.dump(2)); + } + } + + private: const nlohmann::json json_; const int role_; const std::string role_name_; - const std::map metadata_paths_; }; +static int ct = 0; + CafResponse::CafResponse( const QVariant search_value, const int search_role, @@ -609,14 +732,12 @@ CafResponse::CafResponse( const nlohmann::json &data, const int role, const std::string &role_name, - const std::map &metadata_paths, QThreadPool *pool) : search_value_(std::move(search_value)), search_role_(search_role), search_hint_(std::move(search_hint)), role_(role) { - // create a future.. connect( &watcher_, @@ -624,36 +745,11 @@ CafResponse::CafResponse( this, &CafResponse::handleFinished); - try { - QFuture> future = - JobExecutor::run(new CafRequest(data, role, role_name, metadata_paths), pool); - - watcher_.setFuture(future); - } catch (...) { - deleteLater(); - } -} - -CafResponse::CafResponse( - const QVariant search_value, - const int search_role, - const QPersistentModelIndex search_hint, - const nlohmann::json &data, - const int role, - const std::string &role_name, - QThreadPool *pool) - : search_value_(std::move(search_value)), - search_role_(search_role), - search_hint_(std::move(search_hint)), - role_(role) { - - - // create a future.. connect( &watcher_, - &QFutureWatcher>::finished, + &QFutureWatcher>::started, this, - &CafResponse::handleFinished); + &CafResponse::handleStarted); try { QFuture> future = @@ -665,6 +761,8 @@ CafResponse::CafResponse( } } +CafResponse::~CafResponse() {} + void CafResponse::handleFinished() { emit finished(search_value_, search_role_, role_); @@ -680,3 +778,5 @@ void CafResponse::handleFinished() { deleteLater(); } } + +void CafResponse::handleStarted() { emit started(search_value_, search_role_, role_); } diff --git a/src/ui/qml/session/src/export.h b/src/ui/qml/session/src/export.h deleted file mode 100644 index cc4807985..000000000 --- a/src/ui/qml/session/src/export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef SESSION_QML_EXPORT_H -#define SESSION_QML_EXPORT_H - -#ifdef SESSION_QML_STATIC_DEFINE -# define SESSION_QML_EXPORT -# define SESSION_QML_NO_EXPORT -#else -# ifndef SESSION_QML_EXPORT -# ifdef session_qml_EXPORTS - /* We are building this library */ -# define SESSION_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define SESSION_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef SESSION_QML_NO_EXPORT -# define SESSION_QML_NO_EXPORT -# endif -#endif - -#ifndef SESSION_QML_DEPRECATED -# define SESSION_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef SESSION_QML_DEPRECATED_EXPORT -# define SESSION_QML_DEPRECATED_EXPORT SESSION_QML_EXPORT SESSION_QML_DEPRECATED -#endif - -#ifndef SESSION_QML_DEPRECATED_NO_EXPORT -# define SESSION_QML_DEPRECATED_NO_EXPORT SESSION_QML_NO_EXPORT SESSION_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef SESSION_QML_NO_DEPRECATED -# define SESSION_QML_NO_DEPRECATED -# endif -#endif - -#endif /* SESSION_QML_EXPORT_H */ diff --git a/src/ui/qml/session/src/include/session_qml_export.h b/src/ui/qml/session/src/include/session_qml_export.h deleted file mode 100644 index cc4807985..000000000 --- a/src/ui/qml/session/src/include/session_qml_export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef SESSION_QML_EXPORT_H -#define SESSION_QML_EXPORT_H - -#ifdef SESSION_QML_STATIC_DEFINE -# define SESSION_QML_EXPORT -# define SESSION_QML_NO_EXPORT -#else -# ifndef SESSION_QML_EXPORT -# ifdef session_qml_EXPORTS - /* We are building this library */ -# define SESSION_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define SESSION_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef SESSION_QML_NO_EXPORT -# define SESSION_QML_NO_EXPORT -# endif -#endif - -#ifndef SESSION_QML_DEPRECATED -# define SESSION_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef SESSION_QML_DEPRECATED_EXPORT -# define SESSION_QML_DEPRECATED_EXPORT SESSION_QML_EXPORT SESSION_QML_DEPRECATED -#endif - -#ifndef SESSION_QML_DEPRECATED_NO_EXPORT -# define SESSION_QML_DEPRECATED_NO_EXPORT SESSION_QML_NO_EXPORT SESSION_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef SESSION_QML_NO_DEPRECATED -# define SESSION_QML_NO_DEPRECATED -# endif -#endif - -#endif /* SESSION_QML_EXPORT_H */ diff --git a/src/ui/qml/session/src/session_model_core_ui.cpp b/src/ui/qml/session/src/session_model_core_ui.cpp index 224f8ca67..3a61665c5 100644 --- a/src/ui/qml/session/src/session_model_core_ui.cpp +++ b/src/ui/qml/session/src/session_model_core_ui.cpp @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include "xstudio/session/session_actor.hpp" -#include "xstudio/tag/tag.hpp" #include "xstudio/media/media.hpp" #include "xstudio/ui/qml/job_control_ui.hpp" #include "xstudio/ui/qml/session_model_ui.hpp" @@ -9,9 +8,6 @@ CAF_PUSH_WARNINGS #include -#include -#include -#include CAF_POP_WARNINGS using namespace caf; @@ -21,75 +17,72 @@ using namespace xstudio::ui::qml; SessionModel::SessionModel(QObject *parent) : super(parent) { - tag_manager_ = new TagManagerUI(this); init(CafSystemObject::get_actor_system()); - auto role_names = std::vector({ - {"activeDurationRole"}, - {"activeStartRole"}, - {"actorRole"}, - {"actorUuidRole"}, - {"audioActorUuidRole"}, - {"availableDurationRole"}, - {"availableStartRole"}, - {"bitDepthRole"}, - {"busyRole"}, - {"childrenRole"}, - {"clipMediaUuidRole"}, - {"containerUuidRole"}, - {"enabledRole"}, - {"errorRole"}, - {"flagColourRole"}, - {"flagTextRole"}, - {"formatRole"}, - {"groupActorRole"}, - {"idRole"}, - {"imageActorUuidRole"}, - {"mediaCountRole"}, - {"mediaStatusRole"}, - {"metadataSet0Role"}, - {"metadataSet10Role"}, - {"metadataSet1Role"}, - {"metadataSet2Role"}, - {"metadataSet3Role"}, - {"metadataSet4Role"}, - {"metadataSet5Role"}, - {"metadataSet6Role"}, - {"metadataSet7Role"}, - {"metadataSet8Role"}, - {"metadataSet9Role"}, - {"mtimeRole"}, - {"nameRole"}, - {"parentStartRole"}, - {"pathRole"}, - {"pixelAspectRole"}, - {"placeHolderRole"}, - {"rateFPSRole"}, - {"resolutionRole"}, - {"selectionRole"}, - {"thumbnailURLRole"}, - {"trackIndexRole"}, - {"trimmedDurationRole"}, - {"trimmedStartRole"}, - {"typeRole"}, - {"uuidRole"}, - }); + auto role_names = std::vector( + {{"activeDurationRole"}, + {"activeRangeValidRole"}, + {"activeStartRole"}, + {"actorRole"}, + {"actorUuidRole"}, + {"audioActorUuidRole"}, + {"availableDurationRole"}, + {"availableStartRole"}, + {"bitDepthRole"}, + {"bookmarkUuidsRole"}, + {"busyRole"}, + {"clipMediaUuidRole"}, + {"containerUuidRole"}, + {"enabledRole"}, + {"errorRole"}, + {"expandedRole"}, + {"flagColourRole"}, + {"flagTextRole"}, + {"formatRole"}, + {"groupActorRole"}, + {"imageActorUuidRole"}, + {"lockedRole"}, + {"markersRole"}, + {"mediaCountRole"}, + {"mediaDisplayInfoRole"}, + {"mediaStatusRole"}, + {"metadataChangedRole"}, + {"mtimeRole"}, + {"nameRole"}, + {"notificationRole"}, + {"pathRole"}, + {"pathShakeRole"}, + {"pixelAspectRole"}, + {"placeHolderRole"}, + {"propertyRole"}, + {"rateFPSRole"}, + {"resolutionRole"}, + {"selectionRole"}, + {"thumbnailImageRole"}, + {"thumbnailURLRole"}, + {"timecodeAsFramesRole"}, + {"trackIndexRole"}, + {"trimmedDurationRole"}, + {"trimmedStartRole"}, + {"typeRole"}, + {"uuidRole"}, + {"userDataRole"}}); setRoleNames(role_names); request_handler_ = new QThreadPool(this); + request_handler_->setMaxThreadCount(8); } void SessionModel::fetchMore(const QModelIndex &parent) { try { - if (parent.isValid() and canFetchMore(parent)) { + if (canFetchMore(parent)) { const auto &j = indexToData(parent); - requestData( QVariant::fromValue(QUuidFromUuid(j.at("id"))), idRole, parent, parent, - Roles::childrenRole); + JSONTreeModel::Roles::childrenRole); } } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -97,7 +90,7 @@ void SessionModel::fetchMore(const QModelIndex &parent) { } -QModelIndexList SessionModel::search_recursive_list_base( +QModelIndexList SessionModel::searchRecursiveListBase( const QVariant &value, const int role, const QModelIndex &parent, @@ -128,15 +121,15 @@ QModelIndexList SessionModel::search_recursive_list_base( } } } else { + // spdlog::warn("not cached idRole"); cached_result = false; - results = JSONTreeModel::search_recursive_list_base( - value, role, parent, start, hits, depth); + results = + JSONTreeModel::searchRecursiveListBase(value, role, parent, start, hits, depth); for (const auto &i : results) { add_id_uuid_lookup(uuid, i); } } - } - if (role == actorUuidRole or role == containerUuidRole) { + } else if (role == actorUuidRole or role == containerUuidRole) { auto uuid = UuidFromQUuid(value.toUuid()); if (uuid.is_null()) { return QModelIndexList(); @@ -157,9 +150,10 @@ QModelIndexList SessionModel::search_recursive_list_base( } } } else { + // spdlog::warn("not cached actorUuidRole / containerUuidRole"); cached_result = false; - results = JSONTreeModel::search_recursive_list_base( - value, role, parent, start, hits, depth); + results = + JSONTreeModel::searchRecursiveListBase(value, role, parent, start, hits, depth); for (const auto &i : results) { add_uuid_lookup(uuid, i); } @@ -184,8 +178,10 @@ QModelIndexList SessionModel::search_recursive_list_base( } else { // back populate.. cached_result = false; - results = JSONTreeModel::search_recursive_list_base( - value, role, parent, start, hits, depth); + // spdlog::warn("not cached actorRole {}", + // StdFromQString(parent.data(typeRole).toString())); + results = + JSONTreeModel::searchRecursiveListBase(value, role, parent, start, hits, depth); for (const auto &i : results) { add_string_lookup(str, i); } @@ -198,7 +194,7 @@ QModelIndexList SessionModel::search_recursive_list_base( if (results.empty()) { cached_result = false; results = - JSONTreeModel::search_recursive_list_base(value, role, parent, start, hits, depth); + JSONTreeModel::searchRecursiveListBase(value, role, parent, start, hits, depth); } else if (cached_result) { // spdlog::info("have cached result {} {}", parent.isValid(), depth); @@ -288,7 +284,7 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { result = QVariant::fromValue(j.at("error_count").get()); break; - case Roles::idRole: + case JSONTreeModel::Roles::idRole: if (j.count("id")) result = QVariant::fromValue(QUuidFromUuid(j.at("id"))); break; @@ -315,13 +311,76 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { index, role); } else { - auto uri = caf::make_uri(j.at("path")); + auto uri = caf::make_uri(j.at("path").get()); if (uri) result = QVariant::fromValue(QUrlFromUri(*uri)); } } break; + // case Roles::metadataChangedRole: { + // auto tmp = Uuid::generate(); + // result = QVariant::fromValue(QUuidFromUuid(tmp)); + // } + // break; + + case Roles::pathShakeRole: + if (j.count("path_shake")) { + if (j.at("path_shake").is_null()) { + requestData( + QVariant::fromValue(QUuidFromUuid(j.at("id"))), + idRole, + index, + index, + role); + } else { + auto uri = caf::make_uri(j.at("path_shake").get()); + if (uri) + result = QVariant::fromValue(QUrlFromUri(*uri)); + } + } + break; + + case Roles::timecodeAsFramesRole: + if (j.count("timecode_as_frames")) { + if (j.at("timecode_as_frames").is_null()) { + requestData( + QVariant::fromValue(QUuidFromUuid(j.at("id"))), + idRole, + index, + index, + role); + } else { + result = QVariant::fromValue(j.at("timecode_as_frames").get()); + } + } + break; + + case Roles::bookmarkUuidsRole: { + auto type = j.value("type", ""); + if (type == "Media" && j.count("bookmark_uuids")) { + if (j.at("bookmark_uuids").is_null()) { + requestData( + QVariant::fromValue(QUuidFromUuid(j.at("id"))), + idRole, + index, + index, + role); + } else { + const auto &uuids = j.at("bookmark_uuids"); + if (uuids.is_array()) { + QList rt; + for (auto p = uuids.begin(); p != uuids.end(); ++p) { + rt.append(QVariant(QUuidFromUuid((*p).get()))); + } + result = QVariant(rt); + } else { + result = json_to_qvariant(uuids); + } + } + } + } break; + case Roles::trackIndexRole: { auto type = j.value("type", ""); if (type == "Audio Track" or type == "Video Track") { @@ -359,7 +418,9 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { } break; case Roles::rateFPSRole: - if (j.count("rate")) { + if (j.count("placeholder")) { + result = QVariant::fromValue(0.0); + } else if (j.count("rate")) { if (j.at("rate").is_null()) { requestData( QVariant::fromValue(QUuidFromUuid(j.at("id"))), @@ -406,6 +467,39 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { } break; + case propertyRole: + if (j.count("prop")) { + result = QVariant::fromValue(mapFromValue(j.at("prop"))); + } + break; + + case notificationRole: + if (j.count("notification")) { + try { + auto type = j.value("type", ""); + if (j.at("notification").is_null()) { + if (type == "Audio Track" or type == "Video Track") + requestData( + QVariant::fromValue(QUuidFromUuid(j.at("id"))), + idRole, + index, + index, + role); + else + requestData( + QVariant::fromValue(QUuidFromUuid(j.at("actor_uuid"))), + actorUuidRole, + getPlaylistIndex(index), + index, + role); + } else { + result = QVariant::fromValue(mapFromValue(j.at("notification"))); + } + } catch (...) { + } + } + break; + case Roles::resolutionRole: if (j.count("resolution")) { if (j.at("resolution").is_null()) { @@ -475,6 +569,21 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { } break; + case Roles::mediaDisplayInfoRole: { + if (j.count("media_display_info")) { + + if (j.at("media_display_info").is_null()) + requestData( + QVariant::fromValue(QUuidFromUuid(j.at("actor_uuid"))), + actorUuidRole, + getPlaylistIndex(index), + index, + role); + else + result = json_to_qvariant(j.at("media_display_info")); + } + } break; + case Roles::audioActorUuidRole: if (j.count("audio_actor_uuid")) { if (j.at("audio_actor_uuid").is_null()) @@ -514,7 +623,18 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { role); else result = QString::fromStdString(j.at("actor")); + } else if (j.count("actor_owner")) { + if (j.at("actor_owner").is_null()) + requestData( + QVariant::fromValue(QUuidFromUuid(j.at("id"))), + idRole, + index, + index, + role); + else + result = QString::fromStdString(j.at("actor_owner")); } + break; case Roles::groupActorRole: @@ -550,6 +670,34 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { if (j.count("uuid")) result = QVariant::fromValue(QUuidFromUuid(j.at("uuid"))); break; + case Roles::expandedRole: + if (j.count("expanded")) { + if (j.at("expanded").is_null()) + requestData( + QVariant::fromValue(QUuidFromUuid(j.at("actor_uuid"))), + actorUuidRole, + index, + index, + role); + else { + result = json_to_qvariant(j.at("expanded")); + } + } + break; + case Roles::thumbnailImageRole: { + auto type = j.value("type", ""); + if (type == "Media" && j.count("actor") and not j.at("actor").is_null()) { + auto actor_str = j.at("actor"); + auto p = media_thumbnails_.find(QStringFromStd(actor_str)); + if (p != media_thumbnails_.end()) { + result = p.value(); + } else { + auto actor = actorFromString(system(), actor_str); + if (actor) + anon_mail(media_reader::get_thumbnail_atom_v, 0.5f).send(actor); + } + } + } break; case Roles::thumbnailURLRole: if (j.count("thumbnail_url")) { @@ -561,7 +709,7 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { index, role); else { - auto uri = caf::make_uri(j.at("thumbnail_url")); + auto uri = caf::make_uri(j.at("thumbnail_url").get()); if (uri) result = QVariant::fromValue(QUrlFromUri(*uri)); } @@ -582,13 +730,25 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { index, index, role); - } else - result = json_to_qvariant(j.at("playhead_selection")); + } else { + const auto &uuids = j.at("playhead_selection"); + if (uuids.is_array()) { + QList rt; + for (auto p = uuids.begin(); p != uuids.end(); ++p) { + rt.append(QVariant(QUuidFromUuid((*p).get()))); + } + result = QVariant(rt); + } else { + result = json_to_qvariant(uuids); + } + } } break; case Roles::flagColourRole: - if (j.count("flag")) { + if (j.count("placeholder")) { + result = QString(""); + } else if (j.count("flag")) { if (j.at("flag").is_null()) { requestData( QVariant::fromValue(QUuidFromUuid(j.at("id"))), @@ -596,8 +756,12 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { index, index, role); - } else + } else { result = QString::fromStdString(j.at("flag")); + } + } else if (j.count("type") and j.at("type") == "MediaSource") { + // helper for media Sources. + result = index.parent().data(role); } break; @@ -612,18 +776,69 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { role); } else result = QString::fromStdString(j.at("flag_text")); + } else if (j.count("type") and j.at("type") == "MediaSource") { + // helper for media Sources. + result = index.parent().data(role); } break; case Roles::enabledRole: - if (j.count("enabled")) { + if (j.count("placeholder")) { + result = QVariant::fromValue(true); + } else if (j.count("enabled")) { result = QVariant::fromValue(j.value("enabled", true)); } break; + case Roles::lockedRole: + if (j.count("placeholder")) { + result = QVariant::fromValue(false); + } else if (j.count("locked")) { + result = QVariant::fromValue(j.value("locked", false)); + } + break; + + case Roles::markersRole: + if (j.count("placeholder")) { + result = QVariant::fromValue(mapFromValue(R"([])"_json)); + } else if (j.count("markers")) { + result = + QVariant::fromValue(mapFromValue(j.value("markers", R"([])"_json))); + } + break; + + + case Roles::activeRangeValidRole: + if (j.count("placeholder")) { + result = QVariant::fromValue(true); + } else if ( + j.at("type") != "Gap" and j.count("active_range") and + j.at("active_range").is_object() and j.count("available_range") and + j.at("available_range").is_object()) { + try { + const auto active = j.value("active_range", FrameRange()); + const auto avail = j.value("available_range", FrameRange()); + result = QVariant::fromValue(avail.intersect(active) == active); + } catch (...) { + result = QVariant::fromValue(true); + } + } else { + result = QVariant::fromValue(true); + } + break; + + case Roles::userDataRole: + if (j.count("user_data")) + result = mapFromValue(j.at("user_data")); + else + result = mapFromValue(R"({})"_json); + break; + case Roles::trimmedStartRole: - if (j.count("active_range") and j.at("active_range").is_object()) { + if (j.count("placeholder")) { + result = QVariant::fromValue(0); + } else if (j.count("active_range") and j.at("active_range").is_object()) { auto fr = j.value("active_range", FrameRange()); result = QVariant::fromValue(fr.frame_start().frames()); } else if (j.count("available_range") and j.at("available_range").is_object()) { @@ -634,30 +849,35 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { } break; - case Roles::parentStartRole: - // requires access to parent item. - if (j.count("active_range")) { - auto p = index.parent(); - auto t = getTimelineIndex(index); - - if (p.isValid() and t.isValid()) { - auto tactor = actorFromIndex(t); - auto puuid = UuidFromQUuid(p.data(idRole).toUuid()); - - if (timeline_lookup_.count(tactor)) { - auto pitem = timeline::find_item( - timeline_lookup_.at(tactor).children(), puuid); - if (pitem) - result = - QVariant::fromValue((*pitem)->frame_at_index(index.row())); - } - } else - result = QVariant::fromValue(0); - } - break; + // case Roles::parentStartRole: + // // requires access to parent item. + // if (j.count("placeholder")) { + // result = QVariant::fromValue(0); + // } else if (j.count("active_range")) { + // spdlog::warn("PARENTSTARTROLE {}", index.row()); + // auto p = index.parent(); + // auto t = getTimelineIndex(index); + + // if (p.isValid() and t.isValid()) { + // auto tactor = actorFromIndex(t); + // auto puuid = UuidFromQUuid(p.data(idRole).toUuid()); + + // if (timeline_lookup_.count(tactor)) { + // auto pitem = timeline::find_item( + // timeline_lookup_.at(tactor).children(), puuid); + // if (pitem) + // result = + // QVariant::fromValue((*pitem)->frame_at_index(index.row())); + // } + // } else + // result = QVariant::fromValue(0); + // } + // break; case Roles::trimmedDurationRole: - if (j.count("active_range") and j.at("active_range").is_object()) { + if (j.count("placeholder")) { + result = QVariant::fromValue(0); + } else if (j.count("active_range") and j.at("active_range").is_object()) { auto fr = j.value("active_range", FrameRange()); result = QVariant::fromValue(fr.frame_duration().frames()); } else if (j.count("available_range") and j.at("available_range").is_object()) { @@ -692,7 +912,9 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { break; case Roles::availableDurationRole: - if (j.count("available_range")) { + if (j.count("placeholder")) { + result = QVariant::fromValue(0); + } else if (j.count("available_range")) { if (j.at("available_range").is_object()) { auto fr = j.value("available_range", FrameRange()); result = QVariant::fromValue(fr.frame_duration().frames()); @@ -703,7 +925,9 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { break; case Roles::availableStartRole: - if (j.count("available_range")) { + if (j.count("placeholder")) { + result = QVariant::fromValue(0); + } else if (j.count("available_range")) { if (j.at("available_range").is_object()) { auto fr = j.value("available_range", FrameRange()); result = QVariant::fromValue(fr.frame_start().frames()); @@ -715,7 +939,9 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { case Qt::DisplayRole: case Roles::nameRole: - if (j.count("name")) { + if (j.count("placeholder")) { + result = QString(""); + } else if (j.count("name")) { if (j.at("name").is_null()) requestData( QVariant::fromValue(QUuidFromUuid(j.at("id"))), @@ -727,7 +953,7 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { result = QString::fromStdString(j.at("name")); } break; - case Roles::childrenRole: + case JSONTreeModel::Roles::childrenRole: if (j.count("children")) { if (j.at("children").is_null()) { // stop more requests being sent.. @@ -743,29 +969,7 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const { } break; default: { - // are we looking for one of the flexible metadata set roles? - int did = role - Roles::metadataSet0Role; - if (did >= 0 && did <= 9) { - const std::string key = fmt::format("metadata_set{}", did); - if (j.count(key)) { - if (j.at(key).is_null() && !metadata_sets_.empty()) { - - requestData( - QVariant::fromValue(QUuidFromUuid(j.at("id"))), - idRole, - index, - index, - role, - metadata_sets_.find(did)->second); - - } else { - result = json_to_qvariant(j.at(key)); - } - } - - } else { - result = JSONTreeModel::data(index, role); - } + result = JSONTreeModel::data(index, role); } break; } } @@ -797,7 +1001,9 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int auto type = j.count("type") ? j.at("type").get() : std::string(); auto actor = j.count("actor") and not j.at("actor").is_null() ? actorFromString(system(), j.at("actor")) - : caf::actor(); + : (j.count("actor_owner") and not j.at("actor_owner").is_null() + ? actorFromString(system(), j.at("actor_owner")) + : caf::actor()); switch (role) { @@ -808,6 +1014,10 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int } break; + case metadataChangedRole: + result = true; + break; + case errorRole: if (j.count("error_count") and j["error_count"] != value) { j["error_count"] = value; @@ -822,6 +1032,41 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int } break; + + case Roles::clipMediaUuidRole: + if (type == "Clip") { + // find media actor.. + auto timeline = getTimelineIndex(index); + // find media in timeline.. + auto media_index = searchRecursive(qvalue, actorUuidRole, timeline); + + if (media_index.isValid()) { + nlohmann::json &mj = indexToData(media_index); + auto ua = UuidActor( + value, + mj.count("actor") and not mj.at("actor").is_null() + ? actorFromString(system(), mj.at("actor")) + : (mj.count("actor_owner") and + not mj.at("actor_owner").is_null() + ? actorFromString(system(), mj.at("actor_owner")) + : caf::actor())); + + if (ua.actor()) { + j["prop"]["media_uuid"] = value; + + if (actor) + anon_mail(timeline::link_media_atom_v, ua).send(actor); + result = true; + } + } + } + break; + + case Roles::userDataRole: + j["user_data"] = value; + result = true; + break; + case activeStartRole: if (j.count("active_range")) { auto fr = FrameRange(); @@ -844,8 +1089,36 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int j["active_range"] = fr; // probably pointless, as this will trigger from the backend update roles.push_back(trimmedStartRole); + roles.push_back(activeRangeValidRole); + if (actor) + anon_mail(timeline::active_range_atom_v, fr).send(actor); + } + } + break; + + case Roles::markersRole: + if (j.count("markers")) { + if (j.at("markers") != value.at("children")) { + j["markers"] = value.at("children"); + result = true; + if (actor) { + auto markers = std::vector(); + for (const auto &i : j.at("markers")) + markers.emplace_back(timeline::Marker(JsonStore(i))); + anon_mail(timeline::item_marker_atom_v, markers).send(actor); + } + } + } + break; + + case propertyRole: + if (j.count("prop")) { + if (j.at("prop") != value) { + j["prop"] = value; + result = true; + roles.push_back(clipMediaUuidRole); if (actor) - anon_send(actor, timeline::active_range_atom_v, fr); + anon_mail(timeline::item_prop_atom_v, JsonStore(value)).send(actor); } } break; @@ -872,8 +1145,9 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int j["active_range"] = fr; // probably pointless, as this will trigger from the backend update roles.push_back(trimmedDurationRole); + roles.push_back(activeRangeValidRole); if (actor) - anon_send(actor, timeline::active_range_atom_v, fr); + anon_mail(timeline::active_range_atom_v, fr).send(actor); } } break; @@ -890,8 +1164,10 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int j["available_range"] = fr; // probably pointless, as this will trigger from the backend update roles.push_back(trimmedStartRole); + roles.push_back(activeRangeValidRole); + if (actor) - anon_send(actor, timeline::available_range_atom_v, fr); + anon_mail(timeline::available_range_atom_v, fr).send(actor); } } break; @@ -908,8 +1184,9 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int j["available_range"] = fr; // probably pointless, as this will trigger from the backend update roles.push_back(trimmedDurationRole); + roles.push_back(activeRangeValidRole); if (actor) - anon_send(actor, timeline::available_range_atom_v, fr); + anon_mail(timeline::available_range_atom_v, fr).send(actor); } } break; @@ -920,7 +1197,18 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int result = true; if (type == "Clip" or type == "Gap" or type == "Audio Track" or type == "Video Track" or type == "Stack") { - anon_send(actor, plugin_manager::enable_atom_v, value.get()); + anon_mail(plugin_manager::enable_atom_v, value.get()).send(actor); + } + } + break; + + case lockedRole: + if (j.count("locked") and j["locked"] != value) { + j["locked"] = value; + result = true; + if (type == "Clip" or type == "Gap" or type == "Audio Track" or + type == "Video Track" or type == "Stack" or type == "Timeline") { + anon_mail(timeline::item_lock_atom_v, value.get()).send(actor); } } break; @@ -953,6 +1241,7 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int result = true; } break; + case actorRole: if (j.count("actor") and j.at("actor") != value) { j["actor"] = value; @@ -973,17 +1262,17 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int j["image_actor_uuid"] = value; result = true; if (type == "MediaSource") { - anon_send( - actor, + anon_mail( media::current_media_stream_atom_v, media::MT_IMAGE, - j["image_actor_uuid"].get()); + j["image_actor_uuid"].get()) + .send(actor); } else if (type == "Media") { - anon_send( - actor, + anon_mail( media::current_media_source_atom_v, j["image_actor_uuid"].get(), - media::MT_IMAGE); + media::MT_IMAGE) + .send(actor); } } break; @@ -1013,21 +1302,31 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int auto mr = request_receive( *sys, actor, media::media_reference_atom_v); mr.set_rate(fr); - anon_send(actor, media::media_reference_atom_v, mr); + anon_mail(media::media_reference_atom_v, mr).send(actor); } else if (type == "Session") { - anon_send(session_actor_, session::media_rate_atom_v, fr); + anon_mail(session::media_rate_atom_v, fr).send(session_actor_); } } } break; + case pixelAspectRole: { + auto image_source_actor = j.count("actor") and not j.at("actor").is_null() + ? actorFromString(system(), j.at("actor")) + : caf::actor(); + if (image_source_actor) { + anon_mail(media::pixel_aspect_atom_v, value.get()) + .send(image_source_actor); + } + } + case nameRole: if (j.count("name") and j["name"] != value) { - if ((type == "Session" or type == "Subset" or type == "Timeline" or - type == "Playlist") and + if ((type == "Session" or type == "ContactSheet" or type == "Subset" or + type == "Timeline" or type == "Playlist") and actor) { // spdlog::warn("Send update {} {}", j["name"], value); - anon_send(actor, utility::name_atom_v, value.get()); + anon_mail(utility::name_atom_v, value.get()).send(actor); j["name"] = value; result = true; emit playlistsChanged(); @@ -1048,11 +1347,11 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int if (pactor) { // spdlog::warn("Send update {} {}", j["name"], value); - anon_send( - pactor, + anon_mail( playlist::rename_container_atom_v, value.get(), - j.at("container_uuid").get()); + j.at("container_uuid").get()) + .send(pactor); j["name"] = value; result = true; } @@ -1060,7 +1359,8 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int } else if ( type == "Clip" or type == "Gap" or type == "Stack" or type == "Audio Track" or type == "Video Track") { - anon_send(actor, timeline::item_name_atom_v, value.get()); + anon_mail(timeline::item_name_atom_v, value.get()) + .send(actor); j["name"] = value; result = true; } @@ -1076,12 +1376,12 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int : caf::actor(); if (actor) { // spdlog::warn("Send update {} {}", j["flag"], value); - anon_send( - actor, + anon_mail( playlist::reflag_container_atom_v, std::make_tuple( std::optional(), - std::optional(value.get()))); + std::optional(value.get()))) + .send(actor); j["flag_text"] = value; result = true; } @@ -1099,18 +1399,18 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int : caf::actor(); if (actor) { // spdlog::warn("Send update {} {}", j["flag"], value); - anon_send( - actor, + anon_mail( playlist::reflag_container_atom_v, std::make_tuple( std::optional(value.get()), - std::optional())); + std::optional())) + .send(actor); j["flag"] = value; result = true; } } else if ( type == "ContainerDivider" or type == "Subset" or type == "Timeline" or - type == "Playlist") { + type == "Playlist" or type == "ContactSheet") { auto p = index.parent(); if (p.isValid()) { nlohmann::json &pj = indexToData(p); @@ -1126,11 +1426,11 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int if (pactor) { // spdlog::warn("Send update {} {}", j["flag"], value); - anon_send( - pactor, + anon_mail( playlist::reflag_container_atom_v, value.get(), - j.at("container_uuid").get()); + j.at("container_uuid").get()) + .send(pactor); j["flag"] = value; result = true; } @@ -1143,8 +1443,8 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int ? actorFromString(system(), j.at("actor")) : caf::actor(); if (actor) { - anon_send( - actor, timeline::item_flag_atom_v, value.get()); + anon_mail(timeline::item_flag_atom_v, value.get()) + .send(actor); j["flag"] = value; result = true; } @@ -1152,8 +1452,23 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int } break; + case expandedRole: + if (type == "Playlist" && j.count("expanded") and j["expanded"] != value) { + nlohmann::json &j = indexToData(index); + auto actor = j.count("actor") and not j.at("actor").is_null() + ? actorFromString(system(), j.at("actor")) + : caf::actor(); + if (actor) { + // spdlog::warn("Send update {} {}", j["flag"], value); + anon_mail(playlist::expanded_atom_v, value.get()).send(actor); + j["expanded"] = value; + result = true; + } + } + break; + default: - throw std::runtime_error("Unsupported"); + throw std::runtime_error("Unsupported Role"); break; } } @@ -1166,9 +1481,9 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int mapFromValue(qvalue).dump()); } - if (result) { + if (result) emit dataChanged(index, index, roles); - } + CHECK_SLOW_WATCHER_FAST() return result; @@ -1176,21 +1491,4 @@ bool SessionModel::setData(const QModelIndex &index, const QVariant &qvalue, int bool SessionModel::removeRows(int row, int count, const QModelIndex &parent) { return removeRows(row, count, false, parent); -} - -void SessionModel::updateMetadataSelection(const int slot, QStringList metadata_paths) { - - // This SLOT lets us decide which Media metadata fields are put into one - // of the metadataSet0Role, metadataSet1Role etc... - // A 'slot' of 2 would correspond to metadataSet2Role, for example - - // When this is set-up, the metadataSetXRole will be an array of metadata - // VALUES whose metadata KEYS (or PATHS) are defined by metadata_paths. - // Empry strings are allowed and the array element will be empty - - int idx = 0; - metadata_sets_[slot].clear(); - for (const auto &path : metadata_paths) { - metadata_sets_[slot][idx++] = StdFromQString(path); - } -} +} \ No newline at end of file diff --git a/src/ui/qml/session/src/session_model_handler_ui.cpp b/src/ui/qml/session/src/session_model_handler_ui.cpp index f2b6532e5..b3e0f1fd9 100644 --- a/src/ui/qml/session/src/session_model_handler_ui.cpp +++ b/src/ui/qml/session/src/session_model_handler_ui.cpp @@ -3,25 +3,17 @@ #include "xstudio/media/media.hpp" #include "xstudio/session/session_actor.hpp" -#include "xstudio/tag/tag.hpp" #include "xstudio/timeline/item.hpp" +#include "xstudio/thumbnail/thumbnail.hpp" #include "xstudio/ui/qml/caf_response_ui.hpp" #include "xstudio/ui/qml/job_control_ui.hpp" #include "xstudio/ui/qml/session_model_ui.hpp" -CAF_PUSH_WARNINGS -#include -#include -#include -#include -CAF_POP_WARNINGS - using namespace caf; using namespace xstudio; using namespace xstudio::utility; using namespace xstudio::ui::qml; - void SessionModel::updateMedia() { mediaStatusChangePending_ = false; emit mediaStatusChanged(mediaStatusIndex_); @@ -43,7 +35,15 @@ void SessionModel::triggerMediaStatusChange(const QModelIndex &index) { void SessionModel::init(caf::actor_system &_system) { super::init(_system); - self()->set_default_handler(caf::drop); + // self()->set_default_handler( + // [this](caf::scheduled_actor *, caf::message &msg) -> caf::skippable_result { + // // UNCOMMENT TO DEBUG UNEXPECT MESSAGES + + // spdlog::warn( + // "Got adfd from {} {}", to_string(self()->current_sender()), to_string(msg)); + + // return message{}; + // }); setModified(false, true); try { @@ -52,29 +52,9 @@ void SessionModel::init(caf::actor_system &_system) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } - conform_actor_ = system().registry().template get(conform_registry); - if (conform_actor_) { - scoped_actor sys{system()}; - try { - auto conform_events_ = request_receive( - *sys, conform_actor_, utility::get_event_group_atom_v); - - request_receive( - *sys, conform_events_, broadcast::join_broadcast_atom_v, as_actor()); - - updateConformTasks(request_receive>( - *sys, conform_actor_, conform::conform_tasks_atom_v)); - } catch (const std::exception &e) { - } - } - set_message_handler([=](caf::actor_companion * /*self*/) -> caf::message_handler { return { - [=](utility::event_atom, - conform::conform_tasks_atom, - const std::vector &tasks) { updateConformTasks(tasks); }, - [=](utility::event_atom, timeline::item_atom, const timeline::Item &) { // spdlog::info("utility::event_atom, timeline::item_atom, const timeline::Item // &"); @@ -91,7 +71,7 @@ void SessionModel::init(caf::actor_system &_system) { if (timeline_lookup_.count(src)) { // spdlog::warn("update timeline"); - if (timeline_lookup_[src].update(event)) { + if (not timeline_lookup_[src].update(event).empty()) { // refresh ? // timeline_lookup_[src].refresh(-1); // spdlog::warn("state changed"); @@ -105,10 +85,52 @@ void SessionModel::init(caf::actor_system &_system) { } }, + [=](json_store::update_atom, const utility::JsonStore &) { + try { + auto src = caf::actor_cast(self()->current_sender()); + auto src_str = actorToString(system(), src); + + auto indexes = searchRecursiveList( + QVariant::fromValue(QStringFromStd(src_str)), + actorRole, + QModelIndex(), + 0, + -1); + + for (const auto index : indexes) { + if (index.isValid()) + emit dataChanged(index, index, QVector({metadataChangedRole})); + // setData(index, QVariant(), metadataChangedRole); + } + } catch (...) { + } + }, + [=](json_store::update_atom, - const utility::JsonStore & /*change*/, - const std::string & /*path*/, - const utility::JsonStore &full) {}, + const utility::JsonStore &, + const std::string &path, + const utility::JsonStore &) { + try { + auto src = caf::actor_cast(self()->current_sender()); + auto src_str = actorToString(system(), src); + + auto indexes = searchRecursiveList( + QVariant::fromValue(QStringFromStd(src_str)), + actorRole, + QModelIndex(), + 0, + -1); + + + for (const auto index : indexes) { + if (index.isValid()) + emit dataChanged(index, index, QVector({metadataChangedRole})); + // setData(index, QVariant(), metadataChangedRole); + } + + } catch (...) { + } + }, [=](utility::event_atom, media_metadata::get_metadata_atom, @@ -127,7 +149,16 @@ void SessionModel::init(caf::actor_system &_system) { } }, - [=](json_store::update_atom, const utility::JsonStore &js) {}, + [=](utility::event_atom, utility::notification_atom, const JsonStore &digest) { + try { + auto src = caf::actor_cast(self()->current_sender()); + auto src_str = actorToString(system(), src); + + receivedData( + json(src_str), actorRole, QModelIndex(), notificationRole, digest); + } catch (...) { + } + }, [=](utility::event_atom, utility::name_atom, const std::string &name) { // find this actor... in our data.. @@ -139,6 +170,61 @@ void SessionModel::init(caf::actor_system &_system) { receivedData(json(src_str), actorRole, QModelIndex(), nameRole, json(name)); }, + [=](utility::event_atom, + media::media_display_info_atom, + const utility::JsonStore &info) { + // this comes direct from media actor, we don't use as we + // prefer to get it via the playlist to reduce the searching + // that we have to do + }, + + [=](utility::event_atom, + media::media_display_info_atom, + const utility::JsonStore &info, + caf::actor_addr media) { + // re-broadcast of media_display_info_atom event that came from + // a MediaActor. The playlist has done the re-braodcast + + auto src = caf::actor_cast(self()->current_sender()); + auto src_str = actorToString(system(), src); + // request update of children.. + // find container owner.. + auto playlist_index = + searchRecursive(QVariant::fromValue(QStringFromStd(src_str)), actorRole); + + if (playlist_index.isValid()) { + + auto m = caf::actor_cast(media); + auto media_str = actorToString(system(), m); + + auto indeces = searchRecursiveList( + QVariant::fromValue(QStringFromStd(media_str)), + actorRole, + playlist_index, + 0, + -1); + + for (const auto index : indeces) { + if (index.isValid()) { + // request update of containers. + try { + nlohmann::json &j = indexToData(index); + if (j.at("type") == "Media") { + if (j.count("media_display_info") and + j.at("media_display_info") != info) { + j["media_display_info"] = info; + emit dataChanged( + index, index, QVector({mediaDisplayInfoRole})); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + } + } + }, + [=](utility::event_atom, media::add_media_source_atom, const utility::UuidActorVector &uav) { @@ -150,7 +236,7 @@ void SessionModel::init(caf::actor_system &_system) { auto src = caf::actor_cast(self()->current_sender()); auto src_str = actorToString(system(), src); - auto indexes = search_recursive_list( + auto indexes = searchRecursiveList( QVariant::fromValue(QStringFromStd(src_str)), actorRole, QModelIndex(), @@ -159,13 +245,13 @@ void SessionModel::init(caf::actor_system &_system) { if (not indexes.empty() and indexes[0].isValid()) { const nlohmann::json &j = indexToData(indexes[0]); - requestData( QVariant::fromValue(QStringFromStd(src_str)), actorRole, getPlaylistIndex(indexes[0]), j, - childrenRole); + JSONTreeModel::Roles::childrenRole); + } else { spdlog::warn("FAIELD"); } @@ -183,23 +269,22 @@ void SessionModel::init(caf::actor_system &_system) { try { auto src = caf::actor_cast(self()->current_sender()); auto src_str = actorToString(system(), src); - auto index = search_recursive( + auto index = searchRecursive( QVariant::fromValue(QStringFromStd(src_str)), actorRole); if (index.isValid()) { const nlohmann::json &j = indexToData(index); - if (j.at("type") == "Subset" or j.at("type") == "Timeline" or - j.at("type") == "Playlist") { + if (j.at("type") == "ContactSheet" or j.at("type") == "Subset" or + j.at("type") == "Timeline" or j.at("type") == "Playlist") { // get media container index index = index.model()->index(0, 0, index); const nlohmann::json &jj = indexToData(index); - requestData( QVariant::fromValue(QUuidFromUuid(jj.at("id"))), idRole, index, index, - childrenRole); + JSONTreeModel::Roles::childrenRole); } } } catch (const std::exception &err) { @@ -207,18 +292,14 @@ void SessionModel::init(caf::actor_system &_system) { } }, - [=](utility::event_atom, playlist::add_media_atom, const UuidActor &ua) { + [=](utility::event_atom, playlist::add_media_atom, const UuidActorVector &) { auto src = caf::actor_cast(self()->current_sender()); if (src != session_actor_) { auto src_str = actorToString(system(), src); - // spdlog::info( - // "utility::event_atom, playlist::add_media_atom {} {}", - // to_string(ua.uuid()), - // src_str); - auto index = search_recursive( + auto index = searchRecursive( QVariant::fromValue(QStringFromStd(src_str)), actorRole); // trigger update of model.. @@ -226,23 +307,30 @@ void SessionModel::init(caf::actor_system &_system) { try { // request update of containers. const nlohmann::json &j = indexToData(index); - emit mediaAdded(index); index = SessionModel::index(0, 0, index); if (index.isValid()) { const nlohmann::json &jj = indexToData(index); + + // spdlog::info( + // "utility::event_atom, playlist::add_media_atom {} {} {}", + // to_string(jj.at("id").get()), + // src_str, + // jj.dump(2)); + requestData( QVariant::fromValue(QUuidFromUuid(jj.at("id"))), idRole, index, jj, - childrenRole); + JSONTreeModel::Roles::childrenRole); } } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } } + } else { // ignore this message from the session actor.. // not sure why it want's this.. @@ -258,7 +346,7 @@ void SessionModel::init(caf::actor_system &_system) { // request update of children.. // find container owner.. auto index = - search_recursive(QVariant::fromValue(QStringFromStd(src_str)), actorRole); + searchRecursive(QVariant::fromValue(QStringFromStd(src_str)), actorRole); if (index.isValid()) { // request update of containers. try { @@ -275,7 +363,7 @@ void SessionModel::init(caf::actor_system &_system) { idRole, index, index, - childrenRole); + JSONTreeModel::Roles::childrenRole); } } else { requestData( @@ -283,7 +371,7 @@ void SessionModel::init(caf::actor_system &_system) { idRole, index, index, - childrenRole); + JSONTreeModel::Roles::childrenRole); } } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -315,19 +403,17 @@ void SessionModel::init(caf::actor_system &_system) { }, [=](utility::event_atom, playlist::loading_media_atom, const bool value) { - // spdlog::info("utility::event_atom, playlist::loading_media_atom {}", value); - auto src = caf::actor_cast(self()->current_sender()); auto src_str = actorToString(system(), src); // request update of children.. // find container owner.. auto index = - search_recursive(QVariant::fromValue(QStringFromStd(src_str)), actorRole); + searchRecursive(QVariant::fromValue(QStringFromStd(src_str)), actorRole); + if (index.isValid()) { // request update of containers. try { nlohmann::json &j = indexToData(index); - if (j.at("type") == "Playlist") { if (j.count("busy") and j.at("busy") != value) { j["busy"] = value; @@ -340,10 +426,28 @@ void SessionModel::init(caf::actor_system &_system) { } }, - [=](utility::event_atom, session::current_playlist_atom, caf::actor playlist) { - // spdlog::info( - // "utility::event_atom, session::current_playlist_atom {}", - // to_string(playlist)); + [=](utility::event_atom, + session::active_media_container_atom, + utility::UuidActor &a) { + // this comes from the backend SessionActor + updateCurrentMediaContainerIndexFromBackend(); + }, + + [=](utility::event_atom, + session::viewport_active_media_container_atom, + utility::UuidActor &a) { + // this comes from the backend SessionActor + updateViewportCurrentMediaContainerIndexFromBackend(); + }, + + [=](utility::event_atom, + ui::viewport::viewport_playhead_atom, + utility::Uuid playhead_uuid) { + // this comes from the backend SessionActor + if (QUuidFromUuid(playhead_uuid) != on_screen_playhead_uuid_) { + on_screen_playhead_uuid_ = QUuidFromUuid(playhead_uuid); + emit onScreenPlayheadUuidChanged(); + } }, [=](utility::event_atom, @@ -352,8 +456,8 @@ void SessionModel::init(caf::actor_system &_system) { const Uuid &before, const bool) { // spdlog::info("utility::event_atom, playlist::move_container_atom"); - auto src_index = search_recursive( - QVariant::fromValue(QUuidFromUuid(src)), containerUuidRole); + auto src_index = + searchRecursive(QVariant::fromValue(QUuidFromUuid(src)), containerUuidRole); if (src_index.isValid()) { if (before.is_null()) { @@ -364,7 +468,7 @@ void SessionModel::init(caf::actor_system &_system) { src_index.parent(), rowCount(src_index.parent())); } else { - auto before_index = search_recursive( + auto before_index = searchRecursive( QVariant::fromValue(QUuidFromUuid(before)), containerUuidRole); // spdlog::warn("move before {} {}", src_index.row(), // before_index.row()); @@ -391,8 +495,8 @@ void SessionModel::init(caf::actor_system &_system) { const Uuid &src, const Uuid &before) { // spdlog::info("utility::event_atom, playlist::move_container_atom"); - auto src_index = search_recursive( - QVariant::fromValue(QUuidFromUuid(src)), containerUuidRole); + auto src_index = + searchRecursive(QVariant::fromValue(QUuidFromUuid(src)), containerUuidRole); if (src_index.isValid()) { if (before.is_null()) { @@ -403,7 +507,7 @@ void SessionModel::init(caf::actor_system &_system) { src_index.parent(), rowCount(src_index.parent())); } else { - auto before_index = search_recursive( + auto before_index = searchRecursive( QVariant::fromValue(QUuidFromUuid(before)), containerUuidRole); // spdlog::warn("move before {} {}", src_index.row(), // before_index.row()); @@ -454,6 +558,28 @@ void SessionModel::init(caf::actor_system &_system) { // to_string(ua.uuid())); }, + [=](utility::event_atom, + media_reader::get_thumbnail_atom, + const thumbnail::ThumbnailBufferPtr &buf) { + auto src = caf::actor_cast(self()->current_sender()); + auto src_str = actorToString(system(), src); + media_thumbnails_[QStringFromStd(src_str)] = QImage( + (uchar *)&(buf->data()[0]), + buf->width(), + buf->height(), + 3 * buf->width(), + QImage::Format_RGB888); + auto media_indexes = searchRecursiveList( + QVariant::fromValue(QStringFromStd(src_str)), + actorRole, + QModelIndex(), + 0, + -1); + for (const auto &idx : media_indexes) { + emit dataChanged(idx, idx, QVector({thumbnailImageRole})); + } + }, + [=](utility::event_atom, media::current_media_source_atom, const utility::UuidActor &ua, @@ -475,7 +601,7 @@ void SessionModel::init(caf::actor_system &_system) { QVariant::fromValue(QStringFromStd(actorToString(system(), ua.actor()))); auto media_actor_variant = QVariant::fromValue(QStringFromStd(src_str)); - auto media_indexes = search_recursive_list( + auto media_indexes = searchRecursiveList( QVariant::fromValue(QStringFromStd(src_str)), actorRole, QModelIndex(), @@ -536,7 +662,41 @@ void SessionModel::init(caf::actor_system &_system) { CHECK_SLOW_WATCHER() }, - [=](utility::event_atom, bookmark::bookmark_change_atom, const utility::Uuid &) {}, + [=](utility::event_atom, + bookmark::bookmark_change_atom, + const utility::Uuid &uuid) { + auto src = caf::actor_cast(self()->current_sender()); + auto src_str = actorToString(system(), src); + + auto qactor = QVariant::fromValue(QStringFromStd(src_str)); + + auto mindex = searchRecursive(qactor, actorRole); + if (mindex.isValid()) { + auto pindex = getPlaylistIndex(mindex); + if (pindex.isValid()) { + auto media_indexes = + searchRecursiveList(qactor, actorRole, pindex, 0, -1); + + for (auto &index : media_indexes) { + if (index.isValid() and + StdFromQString(index.data(typeRole).toString()) == "Media") { + const auto &j = indexToData(index); + requestData( + QVariant::fromValue(QUuidFromUuid(j.at("id"))), + idRole, + index, + index, + bookmarkUuidsRole); + } + } + } + } + }, + + [=](utility::event_atom, + bookmark::bookmark_change_atom, + const utility::Uuid &uuid, + const utility::UuidList &uuid_list) {}, [=](utility::event_atom, playlist::remove_container_atom, @@ -544,7 +704,7 @@ void SessionModel::init(caf::actor_system &_system) { for (const auto &uuid : uuids) { // spdlog::info("remove_containers_atom {} {}", to_string(uuid)); - auto index = search_recursive( + auto index = searchRecursive( QVariant::fromValue(QUuidFromUuid(uuid)), containerUuidRole); if (index.isValid()) { // use base class so we don't reissue the removal. @@ -562,7 +722,7 @@ void SessionModel::init(caf::actor_system &_system) { [=](utility::event_atom, playlist::remove_container_atom, const Uuid &uuid) { // find container entry and remove it.. // spdlog::info("remove_container_atom {} {}", to_string(uuid)); - auto index = search_recursive( + auto index = searchRecursive( QVariant::fromValue(QUuidFromUuid(uuid)), containerUuidRole); if (index.isValid()) { // use base class so we don't reissue the removal. @@ -574,7 +734,7 @@ void SessionModel::init(caf::actor_system &_system) { // bool) { // // find container entry and remove it.. // spdlog::info("remove_container_atom {} {}", to_string(uuid)); - // auto index = search_recursive( + // auto index = searchRecursive( // QVariant::fromValue(QUuidFromUuid(uuid)), containerUuidRole); // if (index.isValid()) { // // use base class so we don't reissue the removal. @@ -604,7 +764,8 @@ void SessionModel::init(caf::actor_system &_system) { // find container owner.. auto index = - search_recursive(QVariant::fromValue(QStringFromStd(src_str)), actorRole); + searchRecursive(QVariant::fromValue(QStringFromStd(src_str)), actorRole); + if (index.isValid()) { try { const nlohmann::json &j = indexToData(index); @@ -614,7 +775,7 @@ void SessionModel::init(caf::actor_system &_system) { idRole, index, index, - childrenRole); + JSONTreeModel::Roles::childrenRole); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -631,7 +792,7 @@ void SessionModel::init(caf::actor_system &_system) { auto src_str = actorToString(system(), src); if (src == session_actor_) { - auto index = search_recursive( + auto index = searchRecursive( QVariant::fromValue(QStringFromStd(src_str)), actorRole); if (index.isValid()) { try { @@ -653,7 +814,7 @@ void SessionModel::init(caf::actor_system &_system) { // "utility::event_atom, playlist::media_content_changed_atom {}", src_str); auto index = - search_recursive(QVariant::fromValue(QStringFromStd(src_str)), actorRole); + searchRecursive(QVariant::fromValue(QStringFromStd(src_str)), actorRole); // trigger update of model.. if (index.isValid()) { @@ -666,6 +827,7 @@ void SessionModel::init(caf::actor_system &_system) { index = SessionModel::index(0, 0, index); + if (index.isValid()) { const nlohmann::json &jj = indexToData(index); requestData( @@ -673,7 +835,7 @@ void SessionModel::init(caf::actor_system &_system) { idRole, index, index, - childrenRole); + JSONTreeModel::Roles::childrenRole); } } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -687,26 +849,67 @@ void SessionModel::init(caf::actor_system &_system) { const std::vector &actors) { // update media selection model. // PlayheadSelectionActor + try { - auto src = caf::actor_cast(self()->current_sender()); - auto src_str = actorToString(system(), src); + auto src = caf::actor_cast(self()->current_sender()); + auto src_str = actorToString(system(), src); + auto search_value = QVariant::fromValue(QStringFromStd(src_str)); + + auto playlists = searchList( + QVariant::fromValue(QString("Playlist")), + typeRole, + index(0, 0, QModelIndex()), + 0, + -1); + + auto psindex = QModelIndex(); + for (const auto &i : playlists) { + // spdlog::warn("search playlist {} {}", + // StdFromQString(i.data(typeRole).toString()), + // StdFromQString(i.data(nameRole).toString())); + psindex = search(search_value, actorRole, i, 0); + if (psindex.isValid()) { + break; + } - auto index = search_recursive( - QVariant::fromValue(QStringFromStd(src_str)), actorRole); + // search directly under playlist containers. + psindex = + searchRecursive(search_value, actorRole, index(2, 0, i), 0, 1); + if (psindex.isValid()) { + // spdlog::warn("FOUND in container {} {} {}", psindex.row(), + // StdFromQString(psindex.data(typeRole).toString()), + // StdFromQString(psindex.parent().data(typeRole).toString())); + break; + } + } + + if (not psindex.isValid()) { + psindex = searchRecursive( + QVariant::fromValue(QStringFromStd(src_str)), actorRole); + + if (psindex.isValid()) { + spdlog::warn( + "FOUND somewhere unexpected {} {} {}", + psindex.row(), + StdFromQString(psindex.data(typeRole).toString()), + StdFromQString(psindex.parent().data(typeRole).toString())); + } + } // request update of children. // trigger update of model.. - if (index.isValid()) { - const nlohmann::json &j = indexToData(index); + if (psindex.isValid()) { + const nlohmann::json &j = indexToData(psindex); if (j.at("type") == "PlayheadSelection") { requestData( QVariant::fromValue(QUuidFromUuid(j.at("id"))), idRole, - index, - index, - childrenRole); + psindex, + psindex, + selectionRole); } } + } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -720,7 +923,7 @@ void SessionModel::init(caf::actor_system &_system) { auto src = caf::actor_cast(self()->current_sender()); auto src_str = actorToString(system(), src); - auto indexes = search_recursive_list( + auto indexes = searchRecursiveList( QVariant::fromValue(QStringFromStd(src_str)), actorRole, QModelIndex(), @@ -731,13 +934,12 @@ void SessionModel::init(caf::actor_system &_system) { const nlohmann::json &j = indexToData(indexes[0]); // spdlog::warn("media::add_media_stream_atom REQUEST"); - requestData( QVariant::fromValue(QStringFromStd(src_str)), actorRole, getPlaylistIndex(indexes[0]), j, - childrenRole); + JSONTreeModel::Roles::childrenRole); } else { spdlog::warn("FAIELD"); } @@ -754,7 +956,7 @@ void SessionModel::init(caf::actor_system &_system) { auto src = caf::actor_cast(self()->current_sender()); auto src_str = actorToString(system(), src); - auto index = search_recursive( + auto index = searchRecursive( QVariant::fromValue(QStringFromStd(src_str)), actorRole); // spdlog::info("utility::event_atom, utility::move_media_atom {}", @@ -764,16 +966,16 @@ void SessionModel::init(caf::actor_system &_system) { if (index.isValid()) { const nlohmann::json &j = indexToData(index); // spdlog::warn("{}", j.dump(2)); - if (j.at("type") == "Subset" or j.at("type") == "Timeline") { + if (j.at("type") == "Subset" or j.at("type") == "Timeline" or + j.at("type") == "ContactSheet") { const auto tree = *(indexToTree(index)->child(0)); auto media_id = tree.data().at("id"); - requestData( QVariant::fromValue(QUuidFromUuid(media_id)), idRole, index, tree.data(), - childrenRole); + JSONTreeModel::Roles::childrenRole); } } } catch (const std::exception &err) { @@ -788,7 +990,7 @@ void SessionModel::init(caf::actor_system &_system) { // auto src_str = actorToString(system(), src); // auto index = - // search_recursive(QVariant::fromValue(QStringFromStd(src_str)), + // searchRecursive(QVariant::fromValue(QStringFromStd(src_str)), // actorRole); // spdlog::info("utility::event_atom, utility::change_atom {}", src_str); @@ -802,7 +1004,7 @@ void SessionModel::init(caf::actor_system &_system) { // QVariant::fromValue(QUuidFromUuid(media_id)), // idRole, // j.at("children").at(0), - // childrenRole); + // JSONTreeModel::Roles::childrenRole); // } // } // } catch (const std::exception &err) { @@ -833,9 +1035,16 @@ void SessionModel::init(caf::actor_system &_system) { // to_string(ua.uuid())); // request update of children.. // find container owner.. - auto index = search_recursive( + auto index = searchRecursive( QVariant::fromValue(QStringFromStd(src_str)), actorRole); + /*spdlog::info( + "create_subset_atom {} {} {}", + to_string(src), + src_str, + to_string(ua.uuid()));*/ + + if (index.isValid()) { const nlohmann::json &j = indexToData(index); // request update of containers. @@ -851,7 +1060,7 @@ void SessionModel::init(caf::actor_system &_system) { idRole, index, index, - childrenRole); + JSONTreeModel::Roles::childrenRole); } } } catch (const std::exception &err) { @@ -875,7 +1084,7 @@ void SessionModel::init(caf::actor_system &_system) { // to_string(ua.uuid())); // request update of children.. // find container owner.. - auto index = search_recursive( + auto index = searchRecursive( QVariant::fromValue(QStringFromStd(src_str)), actorRole); if (index.isValid()) { @@ -892,7 +1101,7 @@ void SessionModel::init(caf::actor_system &_system) { idRole, index, index, - childrenRole); + JSONTreeModel::Roles::childrenRole); } } } catch (const std::exception &err) { @@ -905,13 +1114,46 @@ void SessionModel::init(caf::actor_system &_system) { }, [=](utility::event_atom, playlist::create_contact_sheet_atom, - const utility::UuidActor &) {}, + const utility::UuidActor &ua) { + try { + auto src = caf::actor_cast(self()->current_sender()); + auto src_str = actorToString(system(), src); + auto index = searchRecursive( + QVariant::fromValue(QStringFromStd(src_str)), actorRole); - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const group_down_msg &g) { - caf::aout(self()) << "down: " << to_string(g.source) << std::endl; - } + /*spdlog::info( + "create_contact_sheet_atom {} {} {}", + to_string(src), + src_str, + to_string(ua.uuid()));*/ + // find container owner.. + + if (index.isValid()) { + const nlohmann::json &j = indexToData(index); + // request update of containers. + try { + if (j.at("type") == "Playlist") { + index = SessionModel::index(2, 0, index); + if (index.isValid()) { + const nlohmann::json &jj = indexToData(index); + requestData( + QVariant::fromValue(QUuidFromUuid(jj.at("id"))), + idRole, + index, + index, + JSONTreeModel::Roles::childrenRole); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + }, - }; + [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + [=](caf::message) { spdlog::warn("Unexpected message"); }}; }); } diff --git a/src/ui/qml/session/src/session_model_manip_ui.cpp b/src/ui/qml/session/src/session_model_manip_ui.cpp index 8791eb4cc..b51832461 100644 --- a/src/ui/qml/session/src/session_model_manip_ui.cpp +++ b/src/ui/qml/session/src/session_model_manip_ui.cpp @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include "xstudio/session/session_actor.hpp" -#include "xstudio/tag/tag.hpp" #include "xstudio/timeline/track_actor.hpp" #include "xstudio/timeline/stack_actor.hpp" #include "xstudio/timeline/gap_actor.hpp" @@ -11,13 +10,6 @@ #include "xstudio/ui/qml/session_model_ui.hpp" #include "xstudio/ui/qml/caf_response_ui.hpp" -CAF_PUSH_WARNINGS -#include -#include -#include -#include -CAF_POP_WARNINGS - using namespace caf; using namespace xstudio; using namespace xstudio::utility; @@ -37,23 +29,23 @@ SessionModel::removeRows(int row, int count, const bool deep, const QModelIndex if (index.isValid()) { nlohmann::json &j = indexToData(index); if (j.at("type") == "ContainerDivider" or j.at("type") == "Subset" or - j.at("type") == "Timeline") { + j.at("type") == "Timeline" or j.at("type") == "ContactSheet") { auto pactor = actorFromIndex(index.parent(), true); if (pactor) { if (j.at("type") == "ContainerDivider" and pactor == session_actor_) { - anon_send( - pactor, + anon_mail( playlist::remove_container_atom_v, - j.at("container_uuid").get()); + j.at("container_uuid").get()) + .send(pactor); } else { // spdlog::warn("Send remove {} {}", // j.at("type").get(), // j["container_uuid"].get()); - anon_send( - pactor, + anon_mail( playlist::remove_container_atom_v, j.at("container_uuid").get(), - deep); + deep) + .send(pactor); } can_delete = true; } @@ -61,10 +53,10 @@ SessionModel::removeRows(int row, int count, const bool deep, const QModelIndex auto pactor = actorFromIndex(index.parent(), true); if (pactor) { // spdlog::warn("Send remove {}", j["container_uuid"]); - anon_send( - pactor, + anon_mail( playlist::remove_container_atom_v, - j.at("container_uuid").get()); + j.at("container_uuid").get()) + .send(pactor); can_delete = true; emit playlistsChanged(); } @@ -74,10 +66,8 @@ SessionModel::removeRows(int row, int count, const bool deep, const QModelIndex media = true; if (pactor) { // spdlog::warn("Send remove {}", j["container_uuid"]); - anon_send( - pactor, - playlist::remove_media_atom_v, - j.at("actor_uuid").get()); + anon_mail(playlist::remove_media_atom_v, j.at("actor_uuid").get()) + .send(pactor); can_delete = true; } } else { @@ -93,7 +83,7 @@ SessionModel::removeRows(int row, int count, const bool deep, const QModelIndex result = JSONTreeModel::removeRows(row, count, parent); if (media) { - // spdlog::warn("mediaCountRole {}", rowCount(parent)); + // spdlog::warn("mediaCountRole 2 {}", rowCount(parent)); setData(parent.parent(), QVariant::fromValue(rowCount(parent)), mediaCountRole); } } @@ -177,17 +167,18 @@ bool SessionModel::duplicateRows(int row, int count, const QModelIndex &parent) nlohmann::json &j = indexToData(index); if (j.at("type") == "ContainerDivider" or j.at("type") == "Subset" or - j.at("type") == "Timeline" or j.at("type") == "Playlist") { + j.at("type") == "Timeline" or j.at("type") == "Playlist" or + j.at("type") == "ContactSheet") { auto pactor = actorFromIndex(index.parent(), true); if (pactor) { // spdlog::warn("Send Duplicate {}", j["container_uuid"]); - anon_send( - pactor, + anon_mail( playlist::duplicate_container_atom_v, j.at("container_uuid").get(), before, - false); + false) + .send(pactor); can_duplicate = true; emit playlistsChanged(); } @@ -196,14 +187,14 @@ bool SessionModel::duplicateRows(int row, int count, const QModelIndex &parent) auto uuid = actorUuidFromIndex(parent, true); if (not uuid.is_null()) { - anon_send( - session_actor_, + anon_mail( playlist::copy_media_atom_v, uuid, UuidVector({j.at("actor_uuid").get()}), true, before, - false); + false) + .send(session_actor_); } } else { @@ -230,86 +221,155 @@ QModelIndexList SessionModel::copyRows( // make sure indexes are valid.. count.. // if parent is playlist the media needs to be inserted in the media child. QModelIndexList result; + try { - UuidVector media_uuids; - Uuid target; + if (not parent.isValid()) + throw std::runtime_error("invalid parent index"); - auto media_parent = parent; - if (parent.isValid()) { - nlohmann::json &j = indexToData(parent); - // spdlog::warn("copyRows {}", j.dump(2)); + auto parent_type = StdFromQString(parent.data(typeRole).toString()); + + if (parent_type == "Video Track" or parent_type == "Audio Track") { + // clone indexes and insert.. + + std::set timeline_types( + {"Gap", "Clip", "Stack", "Video Track", "Audio Track"}); + + scoped_actor sys{system()}; + + nlohmann::json &pj = indexToData(parent); + + auto pactor = pj.count("actor") and not pj.at("actor").is_null() + ? actorFromString(system(), pj.at("actor")) + : caf::actor(); + + if (pactor) { + // timeline item duplication. + auto insertion_row = row; + for (const auto &i : indexes) { + if (i.isValid()) { + auto item_type = StdFromQString(i.data(typeRole).toString()); + nlohmann::json &j = indexToData(i); + auto actor = j.count("actor") and not j.at("actor").is_null() + ? actorFromString(system(), j.at("actor")) + : caf::actor(); + + if (actor) { + auto actor_uuid = request_receive( + *sys, actor, utility::duplicate_atom_v); + + // reset state.. + anon_mail(timeline::item_lock_atom_v, false) + .send(actor_uuid.actor()); + anon_mail(plugin_manager::enable_atom_v, true) + .send(actor_uuid.actor()); + // anon_mail(timeline::item_flag_atom_v, + // "").send(actor_uuid.actor()); + + auto insertion_json = + R"({"type": null, "id": null, "placeholder": true, "actor": null})"_json; + insertion_json["type"] = item_type; + insertion_json["id"] = actor_uuid.uuid(); + + insertion_json["actor"] = + actorToString(system(), actor_uuid.actor()); + + // spdlog::warn("{}", insertion_json.dump(2)); - media_parent = index(0, 0, parent); - target = j.at("actor_uuid"); + JSONTreeModel::insertRows(insertion_row, 1, parent, insertion_json); - if (j.at("type") == "Playlist") - emit playlistsChanged(); + anon_mail( + timeline::insert_item_atom_v, + insertion_row, + UuidActorVector({actor_uuid})) + .send(pactor); + + result.push_back(index(insertion_row, 0, parent)); + + insertion_row++; + } + } + } + } } else { - throw std::runtime_error("invalid parent index"); - } + UuidVector media_uuids; + Uuid target; - for (auto &i : indexes) { - if (i.isValid()) { - nlohmann::json &j = indexToData(i); - if (j.at("type") == "Media") - media_uuids.emplace_back(j.at("actor_uuid")); + auto media_parent = parent; + if (parent.isValid()) { + nlohmann::json &j = indexToData(parent); + media_parent = index(0, 0, parent); + target = j.at("actor_uuid"); + + if (j.at("type") == "Playlist") + emit playlistsChanged(); } - } - // insert dummy entries. - auto before = Uuid(); - auto insertrow = index(row, 0, media_parent); - if (insertrow.isValid()) { - nlohmann::json &irj = indexToData(insertrow); - before = irj.at("actor_uuid"); - } + for (auto &i : indexes) { + if (i.isValid()) { + nlohmann::json &j = indexToData(i); + if (j.at("type") == "Media") + media_uuids.emplace_back(j.at("actor_uuid")); + } + } + + // insert dummy entries. + auto before = Uuid(); + auto insertrow = index(row, 0, media_parent); + if (insertrow.isValid()) { + nlohmann::json &irj = indexToData(insertrow); + before = irj.at("actor_uuid"); + } - JSONTreeModel::insertRows( - row, - media_uuids.size(), - media_parent, - R"({"type": "Media", "placeholder": true})"_json); - for (size_t i = 0; i < media_uuids.size(); i++) { - result.push_back(index(row + i, 0, media_parent)); + JSONTreeModel::insertRows( + row, + media_uuids.size(), + media_parent, + R"({"type": "Media", "placeholder": true})"_json); + for (size_t i = 0; i < media_uuids.size(); i++) { + result.push_back(index(row + i, 0, media_parent)); + } + + // send action to backend. + anon_mail(playlist::copy_media_atom_v, target, media_uuids, false, before, false) + .send(session_actor_); } - // send action to backend. - anon_send( - session_actor_, - playlist::copy_media_atom_v, - target, - media_uuids, - false, - before, - false); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } + // wait for them to be valid.. + for (const auto &i : result) { + while (i.data(SessionModel::Roles::placeHolderRole).toBool() == true) { + QCoreApplication::processEvents( + QEventLoop::WaitForMoreEvents | QEventLoop::ExcludeUserInputEvents, 50); + } + } + return result; } QModelIndexList SessionModel::moveRows( - const QModelIndexList &indexes, const int row, const QModelIndex &parent) { + const QModelIndexList &indexes, + const int q_row, + const QModelIndex &parent, + const bool doCopy) { QModelIndexList result; try { UuidVector media_uuids; Uuid target; Uuid source; + auto row = q_row; auto media_parent = parent; nlohmann::json &j = indexToData(parent); + // spdlog::warn("moveRows {}", j.dump(2)); - if (j.at("type") == "Playlist") { - media_parent = index(0, 0, parent); - target = j.at("actor_uuid"); - // emit playlistsChanged(); - } else { - target = j.at("container_uuid"); - } + media_parent = index(0, 0, parent); + target = j.at("actor_uuid"); for (auto &i : indexes) { if (i.isValid()) { @@ -324,8 +384,10 @@ QModelIndexList SessionModel::moveRows( } } } + if (row == -1) { + row = rowCount(media_parent); + } - // insert dummy entries. auto before = Uuid(); auto insertrow = index(row, 0, media_parent); if (insertrow.isValid()) { @@ -333,29 +395,56 @@ QModelIndexList SessionModel::moveRows( before = irj.at("actor_uuid"); } - JSONTreeModel::insertRows( - row, - media_uuids.size(), - media_parent, - R"({"type": "Media", "placeholder": true})"_json); - for (size_t i = 0; i < media_uuids.size(); i++) { - result.push_back(index(row + i, 0, media_parent)); - } + if (not media_uuids.empty()) { + scoped_actor sys{system()}; + auto new_media = utility::UuidVector(); + // send action to backend. + if (doCopy) { + new_media = request_receive( + *sys, + session_actor_, + playlist::copy_media_atom_v, + target, + media_uuids, + false, + before, + false); - // move media - // spdlog::warn("moveRows {}", j.dump(2)); - // spdlog::warn("moveRows target {} source {}", to_string(target), to_string(source)); + } else { + new_media = request_receive( + *sys, + session_actor_, + playlist::move_media_atom_v, + target, + source, + media_uuids, + before, + false); + } - // send action to backend. - anon_send( - session_actor_, - playlist::move_media_atom_v, - target, - source, - media_uuids, - before, - false); + if (not new_media.empty()) { + // can't tell if it's already in there.. + // so we end up with duff entries. + // wait for the new media to appear so we can capture the indexes. + for (const auto &i : new_media) { + while (true) { + auto mindex = search( + QVariant::fromValue(QUuidFromUuid(i)), + actorUuidRole, + media_parent, + 0); + if (mindex.isValid()) { + result.push_back(mindex); + break; + } + QCoreApplication::processEvents( + QEventLoop::WaitForMoreEvents | QEventLoop::ExcludeUserInputEvents, + 50); + } + } + } + } } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -386,7 +475,7 @@ QModelIndexList SessionModel::insertRows( // spdlog::warn("{}", j.dump(2)); if (type == "ContainerDivider" or type == "Subset" or type == "Timeline" or - type == "Playlist") { + type == "Playlist" or type == "ContactSheet") { auto before = Uuid(); auto insertrow = index(row, 0, parent); auto actor = caf::actor(); @@ -422,28 +511,9 @@ QModelIndexList SessionModel::insertRows( R"({"type": "ContainerDivider", "placeholder": true, "container_uuid": null})"_json); for (auto i = 0; i < count; i++) { - if (not sync) { - anon_send( - actor, - playlist::create_divider_atom_v, - name, - before, - false); - } else { - auto new_item = request_receive( - *sys, - actor, - playlist::create_divider_atom_v, - name, - before, - false); - - setData( - index(row + i, 0, parent), - QUuidFromUuid(new_item), - containerUuidRole); - // container_uuid - } + // if (not sync) { + anon_mail(playlist::create_divider_atom_v, name, before, false) + .send(actor); result.push_back(index(row + i, 0, parent)); } } @@ -469,31 +539,36 @@ QModelIndexList SessionModel::insertRows( // count); for (auto i = 0; i < count; i++) { - if (not sync) { - anon_send( - actor, playlist::create_subset_atom_v, name, before, false); - } else { - auto new_item = request_receive( - *sys, - actor, - playlist::create_subset_atom_v, - name, - before, - false); - - setData( - index(row + i, 0, parent), - QUuidFromUuid(new_item.first), - containerUuidRole); - setData( - index(row + i, 0, parent), - QUuidFromUuid(new_item.second.uuid()), - actorUuidRole); - setData( - index(row + i, 0, parent), - actorToQString(system(), new_item.second.actor()), - actorRole); - } + anon_mail(playlist::create_subset_atom_v, name, before, false) + .send(actor); + result.push_back(index(row + i, 0, parent)); + } + } + } else if (type == "ContactSheet") { + actor = j.count("actor_owner") and not j.at("actor_owner").is_null() + ? actorFromString(system(), j.at("actor_owner")) + : caf::actor(); + + if (actor) { + if (before.is_null()) + row = rowCount(parent); + + JSONTreeModel::insertRows( + row, + count, + parent, + R"({ + "type": "ContactSheet", "placeholder": true, "container_uuid": null, "actor_uuid": null, "actor": null + })"_json); + + // spdlog::warn( + // "JSONTreeModel::insertRows Subset ({}, {}, parent);", row, + // count); + + for (auto i = 0; i < count; i++) { + anon_mail( + playlist::create_contact_sheet_atom_v, name, before, false) + .send(actor); result.push_back(index(row + i, 0, parent)); } } @@ -519,35 +594,9 @@ QModelIndexList SessionModel::insertRows( // count); for (auto i = 0; i < count; i++) { - if (not sync) { - anon_send( - actor, - playlist::create_timeline_atom_v, - name, - before, - false); - } else { - auto new_item = request_receive( - *sys, - actor, - playlist::create_timeline_atom_v, - name, - before, - false); - - setData( - index(row + i, 0, parent), - QUuidFromUuid(new_item.first), - containerUuidRole); - setData( - index(row + i, 0, parent), - QUuidFromUuid(new_item.second.uuid()), - actorUuidRole); - setData( - index(row + i, 0, parent), - actorToQString(system(), new_item.second.actor()), - actorRole); - } + anon_mail( + playlist::create_timeline_atom_v, name, before, false, true) + .send(actor); result.push_back(index(row + i, 0, parent)); } } @@ -571,32 +620,8 @@ QModelIndexList SessionModel::insertRows( // count); for (auto i = 0; i < count; i++) { - if (not sync) { - anon_send( - actor, session::add_playlist_atom_v, name, before, false); - } else { - auto new_item = request_receive( - *sys, - actor, - session::add_playlist_atom_v, - name, - before, - false); - - setData( - index(row + i, 0, parent), - QUuidFromUuid(new_item.first), - containerUuidRole); - setData( - index(row + i, 0, parent), - QUuidFromUuid(new_item.second.uuid()), - actorUuidRole); - setData( - index(row + i, 0, parent), - actorToQString(system(), new_item.second.actor()), - actorRole); - } - // spdlog::warn("ROW {}, {}", row + i, data_.dump(2)); + anon_mail(session::add_playlist_atom_v, name, before, false) + .send(actor); result.push_back(index(row + i, 0, parent)); } emit playlistsChanged(); @@ -613,6 +638,11 @@ QModelIndexList SessionModel::insertRows( R"({"type": null, "id": null, "placeholder": true, "actor": null})"_json; insertion_json["type"] = type; + // get timeline rate from parent. + auto rate = request_receive(*sys, parent_actor, rate_atom_v); + + // spdlog::warn("{}", rate.to_fps()); + JSONTreeModel::insertRows(row, count, parent, insertion_json); for (auto i = 0; i < count; i++) { @@ -621,21 +651,26 @@ QModelIndexList SessionModel::insertRows( if (type == "Video Track") { new_item = self()->spawn( - "New Video Track", media::MediaType::MT_IMAGE, new_uuid); + name.empty() ? "New Video Track" : name, + rate, + media::MediaType::MT_IMAGE, + new_uuid); } else if (type == "Audio Track") { new_item = self()->spawn( - "New Audio Track", media::MediaType::MT_AUDIO, new_uuid); + name.empty() ? "New Audio Track" : name, + rate, + media::MediaType::MT_AUDIO, + new_uuid); } else if (type == "Stack") { - new_item = - self()->spawn("New Stack", new_uuid); + new_item = self()->spawn( + name.empty() ? "New Stack" : name, rate, new_uuid); } else if (type == "Gap") { - auto duration = utility::FrameRateDuration( - 24, FrameRate(timebase::k_flicks_24fps)); - new_item = self()->spawn( - "New Gap", duration, new_uuid); + auto duration = utility::FrameRateDuration(24, rate); + new_item = self()->spawn( + name.empty() ? "Gap" : name, duration, new_uuid); } else if (type == "Clip") { new_item = self()->spawn( - UuidActor(), "New Clip", new_uuid); + UuidActor(), name.empty() ? "New Clip" : name, new_uuid); } // hopefully add to parent.. @@ -664,10 +699,18 @@ QModelIndexList SessionModel::insertRows( spdlog::warn("insertRows: unsupported type {}", type); } } + + if (sync) { + for (const auto &i : result) { + while (i.data(placeHolderRole).toBool() == true) { + QCoreApplication::processEvents( + QEventLoop::WaitForMoreEvents | QEventLoop::ExcludeUserInputEvents, 50); + } + } + } } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } - return result; } @@ -681,18 +724,15 @@ void SessionModel::mergeRows(const QModelIndexList &indexes, const QString &name } if (not playlists.empty()) - anon_send( - session_actor_, - session::merge_playlist_atom_v, - StdFromQString(name), - Uuid(), - playlists); + anon_mail(session::merge_playlist_atom_v, StdFromQString(name), Uuid(), playlists) + .send(session_actor_); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } } void SessionModel::updateErroredCount(const QModelIndex &media_index) { + return; try { if (media_index.isValid()) { auto media_list_index = media_index.parent(); diff --git a/src/ui/qml/session/src/session_model_methods_ui.cpp b/src/ui/qml/session/src/session_model_methods_ui.cpp index aeba9ff2e..2b9a4368c 100644 --- a/src/ui/qml/session/src/session_model_methods_ui.cpp +++ b/src/ui/qml/session/src/session_model_methods_ui.cpp @@ -1,20 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 +#include #include "xstudio/conform/conformer.hpp" #include "xstudio/media/media.hpp" #include "xstudio/session/session_actor.hpp" -#include "xstudio/tag/tag.hpp" #include "xstudio/timeline/clip_actor.hpp" #include "xstudio/timeline/timeline.hpp" #include "xstudio/ui/qml/caf_response_ui.hpp" #include "xstudio/ui/qml/job_control_ui.hpp" #include "xstudio/ui/qml/session_model_ui.hpp" +#include "xstudio/utility/notification_handler.hpp" CAF_PUSH_WARNINGS -#include -#include #include -#include CAF_POP_WARNINGS using namespace caf; @@ -22,6 +20,146 @@ using namespace xstudio; using namespace xstudio::utility; using namespace xstudio::ui::qml; +void SessionModel::removeNotification(const QModelIndex &index, const QUuid &uuid) { + if (index.isValid()) { + auto actor = actorFromQString(system(), index.data(actorRole).toString()); + if (actor) + anon_mail(utility::notification_atom_v, UuidFromQUuid(uuid)).send(actor); + } +} + +QString SessionModel::getNextName(const QString &nameTemplate) const { + QString result = nameTemplate; + + scoped_actor sys{system()}; + try { + result = QStringFromStd(request_receive( + *sys, session_actor_, name_atom_v, StdFromQString(nameTemplate), true)); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; +} + +void SessionModel::setSessionSelection(const QModelIndexList &indexes) const { + try { + UuidActorVector selection; + + for (auto &i : indexes) { + selection.emplace_back(UuidActor( + UuidFromQUuid(i.data(actorUuidRole).toUuid()), + actorFromQString(system(), i.data(actorRole).toString()))); + } + anon_mail(timeline::item_selection_atom_v, selection).send(session_actor_); + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} + +QUuid SessionModel::infoNotification( + const QModelIndex &index, + const QString &text, + const int seconds, + const QUuid &replaceUuid) { + auto result = QUuid(); + auto replace_uuid = UuidFromQUuid(replaceUuid); + if (index.isValid()) { + auto actor = actorFromQString(system(), index.data(actorRole).toString()); + if (actor) { + auto n = Notification::InfoNotification( + StdFromQString(text), std::chrono::seconds(seconds)); + if (not replace_uuid.is_null()) + n.uuid(replace_uuid); + result = QUuidFromUuid(n.uuid()); + anon_mail(utility::notification_atom_v, n).send(actor); + } + } + + return result; +} + +QUuid SessionModel::warnNotification( + const QModelIndex &index, + const QString &text, + const int seconds, + const QUuid &replaceUuid) { + auto result = QUuid(); + auto replace_uuid = UuidFromQUuid(replaceUuid); + + if (index.isValid()) { + auto actor = actorFromQString(system(), index.data(actorRole).toString()); + if (actor) { + auto n = Notification::WarnNotification( + StdFromQString(text), std::chrono::seconds(seconds)); + if (not replace_uuid.is_null()) + n.uuid(replace_uuid); + result = QUuidFromUuid(n.uuid()); + anon_mail(utility::notification_atom_v, n).send(actor); + } + } + + return result; +} + +QUuid SessionModel::processingNotification(const QModelIndex &index, const QString &text) { + auto result = QUuid(); + if (index.isValid()) { + auto actor = actorFromQString(system(), index.data(actorRole).toString()); + if (actor) { + auto n = Notification::ProcessingNotification(StdFromQString(text)); + result = QUuidFromUuid(n.uuid()); + anon_mail(utility::notification_atom_v, n).send(actor); + } + } + + return result; +} + +QUuid SessionModel::progressPercentageNotification( + const QModelIndex &index, const QString &text) { + auto result = QUuid(); + if (index.isValid()) { + auto actor = actorFromQString(system(), index.data(actorRole).toString()); + if (actor) { + auto n = Notification::ProgressPercentageNotification(StdFromQString(text)); + result = QUuidFromUuid(n.uuid()); + anon_mail(utility::notification_atom_v, n).send(actor); + } + } + + return result; +} + +QUuid SessionModel::progressRangeNotification( + const QModelIndex &index, const QString &text, const float min, const float max) { + auto result = QUuid(); + if (index.isValid()) { + auto actor = actorFromQString(system(), index.data(actorRole).toString()); + if (actor) { + auto n = + Notification::ProgressRangeNotification(StdFromQString(text), min, min, max); + result = QUuidFromUuid(n.uuid()); + anon_mail(utility::notification_atom_v, n).send(actor); + } + } + + return result; +} + + +void SessionModel::updateProgressNotification( + const QModelIndex &index, const QUuid &uuid, const float value) { + if (index.isValid()) { + auto actor = actorFromQString(system(), index.data(actorRole).toString()); + if (actor) { + anon_mail(utility::notification_atom_v, UuidFromQUuid(uuid), value).send(actor); + } + } +} + + QVariant SessionModel::playlists() const { // scan model for playlists.. auto data = R"([])"_json; @@ -38,18 +176,16 @@ QVariant SessionModel::playlists() const { return mapFromValue(data); } -QStringList SessionModel::conformTasks() const { return conform_tasks_; } - -void SessionModel::updateConformTasks(const std::vector &tasks) { - QStringList result; +void SessionModel::setSelectedMedia(const QModelIndexList &indexes) { + auto media = UuidActorVector(); - for (const auto &i : tasks) - result.push_back(QStringFromStd(i)); - - if (result != conform_tasks_) { - conform_tasks_ = result; - emit conformTasksChanged(); + for (const auto &i : indexes) { + auto muuid = UuidFromQUuid(i.data(actorUuidRole).toUuid()); + auto mactor = actorFromQString(system(), i.data(actorRole).toString()); + media.emplace_back(UuidActor(muuid, mactor)); } + + anon_mail(media::current_media_atom_v, media).send(session_actor_); } @@ -67,6 +203,92 @@ QModelIndex SessionModel::getPlaylistIndex(const QModelIndex &index) const { return result; } +QModelIndex SessionModel::getContainerIndex(const QModelIndex &index) const { + const static std::set container_names( + {"Timeline", "Subset", "ContactSheet", "Playlist"}); + try { + if (index.isValid()) { + auto type = StdFromQString(index.data(typeRole).toString()); + if (container_names.count(type)) + return index; + else + return getContainerIndex(index.parent()); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return QModelIndex(); +} + +Q_INVOKABLE void SessionModel::purgePlaylist(const QModelIndex &index) { + if (index.isValid() and index.data(typeRole).toString() == QString("Playlist")) { + auto actor = actorFromQString(system(), index.data(actorRole).toString()); + if (actor) + anon_mail(playlist::remove_orphans_atom_v, UuidVector()).send(actor); + } +} + +void SessionModel::updateCurrentMediaContainerIndexFromBackend() { + + scoped_actor sys{system()}; + try { + auto playlist = request_receive( + *sys, session_actor_, session::active_media_container_atom_v); + auto actor_string = QStringFromStd(actorToString(system(), playlist.actor())); + + // playlists are in 2nd row of root of the session model. We only need to go 2 levels + // deep to see all playlists and subsets/timelines which are children of playlists + auto r = + QPersistentModelIndex(searchRecursive(actor_string, "actorRole", index(1, 0), 2)); + + if (!r.isValid() && playlist) { + // we didn't find the actor in the model ... this could be that the model is + // still building so re-try in 250ms + QTimer::singleShot(250, this, SLOT(updateCurrentMediaContainerIndexFromBackend())); + } + + if (r != current_playlist_index_) { + current_playlist_index_ = r; + emit currentMediaContainerChanged(); + } + + } catch (const std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + +void SessionModel::updateViewportCurrentMediaContainerIndexFromBackend() { + scoped_actor sys{system()}; + try { + + auto playlist = request_receive( + *sys, session_actor_, session::viewport_active_media_container_atom_v); + auto actor_string = QStringFromStd(actorToString(system(), playlist.actor())); + + // playlists are in 2nd row of root of the session model. We only need to go 2 levels + // deep to see all playlists and subsets/timelines which are children of playlists + auto r = + QPersistentModelIndex(searchRecursive(actor_string, "actorRole", index(1, 0), 2)); + + if (!r.isValid() && playlist) { + // we didn't find the actor in the model ... this could be that the model is + // still building so re-try in 250ms + QTimer::singleShot( + 250, this, SLOT(updateViewportCurrentMediaContainerIndexFromBackend())); + } + + if (r != current_playhead_owner_index_) { + current_playhead_owner_index_ = r; + emit viewportCurrentMediaContainerIndexChanged(); + } + + } catch (const std::exception &e) { + spdlog::debug("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + + Q_INVOKABLE void SessionModel::decomposeMedia(const QModelIndexList &indexes) { for (const auto &i : indexes) { if (i.isValid() and i.data(typeRole).toString() == QString("Media")) { @@ -74,10 +296,9 @@ Q_INVOKABLE void SessionModel::decomposeMedia(const QModelIndexList &indexes) { if (plindex.isValid()) { auto actor = actorFromQString(system(), plindex.data(actorRole).toString()); if (actor) - anon_send( - actor, - media::decompose_atom_v, - UuidFromQUuid(i.data(actorUuidRole).toUuid())); + anon_mail( + media::decompose_atom_v, UuidFromQUuid(i.data(actorUuidRole).toUuid())) + .send(actor); } } } @@ -90,10 +311,9 @@ Q_INVOKABLE void SessionModel::rescanMedia(const QModelIndexList &indexes) { if (plindex.isValid()) { auto actor = actorFromQString(system(), plindex.data(actorRole).toString()); if (actor) - anon_send( - actor, - media::rescan_atom_v, - UuidFromQUuid(i.data(actorUuidRole).toUuid())); + anon_mail( + media::rescan_atom_v, UuidFromQUuid(i.data(actorUuidRole).toUuid())) + .send(actor); } } } @@ -109,28 +329,93 @@ Q_INVOKABLE void SessionModel::relinkMedia(const QModelIndexList &indexes, const for (const auto &i : indexes) { if (i.isValid()) { if (i.data(typeRole).toString() == QString("Media")) { - auto iind = search_recursive(i.data(imageActorUuidRole), actorUuidRole, i); - auto aind = search_recursive(i.data(audioActorUuidRole), actorUuidRole, i); + auto iind = searchRecursive(i.data(imageActorUuidRole), actorUuidRole, i); + auto aind = searchRecursive(i.data(audioActorUuidRole), actorUuidRole, i); if (iind.isValid()) { auto actor = actorFromQString(system(), iind.data(actorRole).toString()); if (actor) - anon_send(scanner, media::relink_atom_v, actor, uri); + anon_mail(media::relink_atom_v, actor, uri).send(scanner); } if (aind.isValid() and aind != iind) { auto actor = actorFromQString(system(), aind.data(actorRole).toString()); if (actor) - anon_send(scanner, media::relink_atom_v, actor, uri); + anon_mail(media::relink_atom_v, actor, uri).send(scanner); + } + } + } + } + } +} + +QStringList +SessionModel::getMediaSourceNames(const QModelIndex &media_index, const bool image_source) { + + QStringList rt; + try { + + scoped_actor sys{system()}; + + if (media_index.isValid()) { + nlohmann::json &j = indexToData(media_index); + if (j.at("actor").is_string()) { + auto actor = actorFromString(system(), j.at("actor")); + if (actor) { + auto media_source_names = + request_receive>>( + *sys, + actor, + media::get_media_source_names_atom_v, + image_source ? media::MT_IMAGE : media::MT_AUDIO); + for (const auto &n : media_source_names) { + rt.append(QStringFromStd(n.second)); } } } } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + /*if (index.isValid()) { + nlohmann::json &j = indexToData(index); + spdlog::warn("{}", j.dump(2)); + }*/ } + return rt; } +QStringList SessionModel::setMediaSource( + const QModelIndex &media_index, const QString &mediaSourceName, const bool image_source) { + + QStringList rt; + try { + + scoped_actor sys{system()}; + + if (media_index.isValid()) { + nlohmann::json &j = indexToData(media_index); + if (j.at("actor").is_string()) { + auto actor = actorFromString(system(), j.at("actor")); + if (actor) { + anon_mail( + playhead::media_source_atom_v, + StdFromQString(mediaSourceName), + image_source ? media::MT_IMAGE : media::MT_AUDIO) + .send(actor); + } + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + /*if (index.isValid()) { + nlohmann::json &j = indexToData(index); + spdlog::warn("{}", j.dump(2)); + }*/ + } + return rt; +} void SessionModel::setSessionActorAddr(const QString &addr) { try { @@ -148,6 +433,7 @@ void SessionModel::setSessionActorAddr(const QString &addr) { "name": null, "actor_uuid": null, "actor": null, + "notification": null, "group_actor": null, "container_uuid": null, "rate": null, @@ -159,33 +445,32 @@ void SessionModel::setSessionActorAddr(const QString &addr) { session_actor_ = actorFromQString(system(), addr); data["children"][0]["actor"] = StdFromQString(addr); - try { - auto actor = - request_receive(*sys, session_actor_, tag::get_tag_atom_v); - tag_manager_->set_backend(actor); - emit tagsChanged(); - - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - // clear lookup.. id_uuid_lookup_.clear(); uuid_lookup_.clear(); string_lookup_.clear(); setModelData(data); + add_lookup(*indexToTree(index(0, 0)), index(0, 0)); emit playlistsChanged(); - // if (backend_events_) { - // try { - // request_receive( - // *sys, backend_events_, broadcast::leave_broadcast_atom_v, - // as_actor()); - // } catch (const std::exception &e) { - // } - // backend_events_ = caf::actor(); - // } + // get the 'current' playlists (inspected and on-screen) + // from the session backend actor + updateCurrentMediaContainerIndexFromBackend(); + updateViewportCurrentMediaContainerIndexFromBackend(); + + try { + auto playhead_events_actor = + system().registry().template get(global_playhead_events_actor); + auto playhead = request_receive( + *sys, playhead_events_actor, ui::viewport::viewport_playhead_atom_v); + if (playhead) { + on_screen_playhead_uuid_ = QUuidFromUuid( + request_receive(*sys, playhead, utility::uuid_atom_v)); + emit onScreenPlayheadUuidChanged(); + } + } catch (...) { + } // join bookmark events if (session_actor_) { @@ -208,7 +493,7 @@ void SessionModel::setSessionActorAddr(const QString &addr) { idRole, QModelIndex(), data_.front().data(), - childrenRole); + JSONTreeModel::Roles::childrenRole); try { bookmark_actor_addr_ = actorToQString( @@ -236,6 +521,12 @@ QFuture SessionModel::saveFuture(const QUrl &path, const QModelIndexLis for (const auto &i : selection) { if (i.data(typeRole) == "Playlist") playlists.emplace_back(UuidFromQUuid(i.data(containerUuidRole).toUuid())); + else { + auto tmp = getPlaylistIndex(i); + if (tmp.isValid()) + playlists.emplace_back( + UuidFromQUuid(tmp.data(containerUuidRole).toUuid())); + } } if (playlists.empty()) { @@ -351,7 +642,7 @@ QFuture> SessionModel::handleMediaIdDropFuture( for (const auto &i : jdrop.at("xstudio/media-ids")) { // find media index - auto mind = search_recursive(QUuid::fromString(QStringFromStd(i)), idRole); + auto mind = searchRecursive(QUuid::fromString(QStringFromStd(i)), idRole); if (mind.isValid()) { if (media_owner_uuid.is_null()) { auto p = mind.parent().parent(); @@ -378,6 +669,9 @@ QFuture> SessionModel::handleMediaIdDropFuture( if (type == "Playlist") { target = actorFromString(system(), ij.at("actor")); target_uuid = ij.at("actor_uuid").get(); + } else if (type == "ContactSheet") { + target = actorFromIndex(index.parent(), true); + target_uuid = ij.at("actor_uuid").get(); } else if (type == "Subset") { target = actorFromIndex(index.parent(), true); target_uuid = ij.at("actor_uuid").get(); @@ -417,33 +711,33 @@ QFuture> SessionModel::handleMediaIdDropFuture( } if (local_mode) { - anon_send( - target, + anon_mail( playlist::move_media_atom_v, vector_to_uuid_vector(media), - before); + before) + .send(target); } else { if (proposedAction == Qt::MoveAction) { // spdlog::warn("proposedAction == Qt::MoveAction"); // move media to new playlist - anon_send( - session_actor_, + anon_mail( playlist::move_media_atom_v, target_uuid, media_owner_uuid, vector_to_uuid_vector(media), before, - false); + false) + .send(session_actor_); } else { // spdlog::warn("proposedAction == Qt::CopyAction"); - anon_send( - session_actor_, + anon_mail( playlist::copy_media_atom_v, target_uuid, vector_to_uuid_vector(media), false, before, - false); + false) + .send(session_actor_); } } @@ -466,21 +760,24 @@ QFuture> SessionModel::handleMediaIdDropFuture( auto new_uuid = utility::Uuid::generate(); auto new_item = self()->spawn(i, "", new_uuid); - anon_send( - track_actor, + anon_mail( timeline::insert_item_atom_v, row, - UuidActorVector({UuidActor(new_uuid, new_item)})); + UuidActorVector({UuidActor(new_uuid, new_item)})) + .send(track_actor); } } } } else { - // Moving or copying Media to new playlist - if (media_owner_name.empty()) - media_owner_name = "Untitled Playlist"; - else + if (media_owner_name.empty()) { + + // session will assign a new name + media_owner_name = ""; + + } else { media_owner_name += " - copy"; + } auto uua = request_receive( *sys, @@ -492,24 +789,24 @@ QFuture> SessionModel::handleMediaIdDropFuture( if (proposedAction == Qt::MoveAction) { // move media to new playlist - anon_send( - session_actor_, + anon_mail( playlist::move_media_atom_v, uua.second.uuid(), media_owner_uuid, vector_to_uuid_vector(media), Uuid(), - false); + false) + .send(session_actor_); } else { // copy media to new playlist. - anon_send( - session_actor_, + anon_mail( playlist::copy_media_atom_v, uua.second.uuid(), vector_to_uuid_vector(media), false, Uuid(), - false); + false) + .send(session_actor_); } } } @@ -548,7 +845,8 @@ QFuture> SessionModel::handleContainerIdDropFuture( for (const auto &i : jdrop.at("xstudio/container-ids")) { // spdlog::warn("MOVE CONTAINER {}", to_string(i)); // find container index - auto cind = search_recursive(QUuid::fromString(QStringFromStd(i)), idRole); + + auto cind = searchRecursive(QUuid::fromString(QStringFromStd(i)), idRole); if (cind.isValid()) { const auto &cij = indexToData(cind); // spdlog::info("{}", cij.dump(2)); @@ -656,9 +954,7 @@ QFuture> SessionModel::handleContainerIdDropFuture( QFuture> SessionModel::handleUriListDropFuture( - const int proposedAction_, const utility::JsonStore &drop, const QModelIndex &idx) { - const utility::JsonStore jdrop = drop; - const QModelIndex index = idx; + const int proposedAction_, const utility::JsonStore &jdrop, const QModelIndex &index) { return QtConcurrent::run([=]() { scoped_actor sys{system()}; @@ -679,19 +975,23 @@ QFuture> SessionModel::handleUriListDropFuture( const auto &type = ij.at("type").get(); auto sub_target = caf::actor(); - spdlog::warn("{}", type); + // spdlog::warn("{}", type); std::string actor; if (type == "Playlist") { actor = ij.at("actor"); target = actorFromString(system(), actor); - } else if (type == "Subset") { + } else if (type == "ContactSheet") { + actor = ij.at("actor"); target = actorFromIndex(index.parent(), true); + sub_target = actorFromString(system(), actor); + } else if (type == "Subset") { actor = ij.at("actor"); + target = actorFromIndex(index.parent(), true); sub_target = actorFromString(system(), actor); } else if (type == "Timeline") { - target = actorFromIndex(index.parent(), true); actor = ij.at("actor"); + target = actorFromIndex(index.parent(), true); sub_target = actorFromString(system(), actor); } else if ( type == "Video Track" or type == "Audio Track" or type == "Gap" or @@ -706,6 +1006,8 @@ QFuture> SessionModel::handleUriListDropFuture( } else if (type == "Media") { before = ij.at("actor_uuid"); target = actorFromIndex(index.parent().parent(), true); + } else if (type == "Media List") { + target = actorFromIndex(index.parent(), true); } else { spdlog::warn("UNHANDLED {}", ij.at("type").get()); } @@ -714,13 +1016,14 @@ QFuture> SessionModel::handleUriListDropFuture( for (const auto &path : jdrop.at("text/uri-list")) { auto path_string = path.get(); auto uri = caf::make_uri(url_clean(path_string)); + if (uri) { // uri maybe timeline... // hacky... if (is_timeline_supported(*uri)) { // spdlog::warn("LOAD TIMELINE {}", to_string(*uri)); new_media.push_back(request_receive( - *sys, target, session::import_atom_v, *uri, before)); + *sys, target, session::import_atom_v, *uri, before, true)); } else { auto new_media_tmp = request_receive( *sys, @@ -745,7 +1048,8 @@ QFuture> SessionModel::handleUriListDropFuture( if (sub_target) { for (const auto &i : new_media) - anon_send(sub_target, playlist::add_media_atom_v, i.uuid(), Uuid()); + anon_mail(playlist::add_media_atom_v, i.uuid(), Uuid()) + .send(sub_target); // post process timeline drops.. if (type == "Video Track" or type == "Audio Track" or type == "Gap" or @@ -766,11 +1070,11 @@ QFuture> SessionModel::handleUriListDropFuture( auto new_uuid = utility::Uuid::generate(); auto new_item = self()->spawn(i, "", new_uuid); - anon_send( - track_actor, + anon_mail( timeline::insert_item_atom_v, row, - UuidActorVector({UuidActor(new_uuid, new_item)})); + UuidActorVector({UuidActor(new_uuid, new_item)})) + .send(track_actor); } } } @@ -783,7 +1087,7 @@ QFuture> SessionModel::handleUriListDropFuture( if (uri) uris.emplace_back(*uri); } - anon_send(session_actor_, session::load_uris_atom_v, uris, false); + anon_mail(session::load_uris_atom_v, uris, false, true).send(session_actor_); } for (const auto &i : new_media) @@ -831,6 +1135,9 @@ QFuture> SessionModel::handleOtherDropFuture( } else if (type == "Subset") { target = actorFromIndex(index.parent(), true); sub_target = actorFromString(system(), ij.at("actor")); + } else if (type == "ContactSheet") { + target = actorFromIndex(index.parent(), true); + sub_target = actorFromString(system(), ij.at("actor")); } else if (type == "Timeline") { target = actorFromIndex(index.parent(), true); sub_target = actorFromString(system(), ij.at("actor")); @@ -847,6 +1154,8 @@ QFuture> SessionModel::handleOtherDropFuture( } else if (type == "Media") { before = ij.at("actor_uuid"); target = actorFromIndex(index.parent().parent(), true); + } else if (type == "Media List") { + target = actorFromIndex(index.parent(), true); } else { spdlog::warn("UNHANDLED {}", ij.at("type").get()); } @@ -875,7 +1184,7 @@ QFuture> SessionModel::handleOtherDropFuture( // lets assume they are media... (WARNING this may not be the // case...) create new playlist and add them... for (const auto &i : plugin_media_tmp) - anon_send(target, playlist::add_media_atom_v, i, before); + anon_mail(playlist::add_media_atom_v, i, before).send(target); new_media.insert( new_media.end(), plugin_media_tmp.begin(), plugin_media_tmp.end()); @@ -896,7 +1205,8 @@ QFuture> SessionModel::handleOtherDropFuture( if (sub_target) { for (const auto &i : new_media) - anon_send(sub_target, playlist::add_media_atom_v, i.uuid(), Uuid()); + anon_mail(playlist::add_media_atom_v, i.uuid(), Uuid()) + .send(sub_target); // post process timeline drops.. if (type == "Video Track" or type == "Audio Track" or type == "Gap" or @@ -916,11 +1226,11 @@ QFuture> SessionModel::handleOtherDropFuture( for (const auto &i : new_media) { auto new_uuid = utility::Uuid::generate(); auto new_item = self()->spawn(i, "", new_uuid); - anon_send( - track_actor, + anon_mail( timeline::insert_item_atom_v, row, - UuidActorVector({UuidActor(new_uuid, new_item)})); + UuidActorVector({UuidActor(new_uuid, new_item)})) + .send(track_actor); } } } @@ -940,45 +1250,24 @@ QFuture> SessionModel::handleOtherDropFuture( }); } -QUuid SessionModel::addTag( - const QUuid &quuid, - const QString &type, - const QString &data, - const QString &unique, - const bool persistent) { - QUuid result; - - try { - tag::Tag tag; - tag.set_link(UuidFromQUuid(quuid)); - tag.set_type(StdFromQString(type)); - tag.set_data(StdFromQString(data)); - if (not unique.isEmpty()) - tag.set_unique(StdFromQString(unique)); - tag.set_persistent(persistent); - - scoped_actor sys{system()}; - auto tag_actor = request_receive(*sys, session_actor_, tag::get_tag_atom_v); - result = QUuidFromUuid( - request_receive(*sys, tag_actor, tag::add_tag_atom_v, tag)); - - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - - return result; -} - QFuture SessionModel::importFuture(const QUrl &path, const QVariant &json) { + auto notify_processing = Notification::ProcessingNotification( + "Importing Session " + StdFromQString(path.path())); + auto notification_uuid = notify_processing.uuid(); + anon_mail(utility::notification_atom_v, notify_processing).send(session_actor_); + return QtConcurrent::run([=]() { bool result = false; JsonStore js; - scoped_actor sys{system()}; if (json.isNull()) { try { - js = utility::open_session(StdFromQString(path.path())); + js = utility::open_session(UriFromQUrl(path)); } catch (const std::exception &err) { + auto notify = Notification::WarnNotification( + std::string("Import Session Failed - ") + err.what()); + notify.uuid(notification_uuid); + anon_mail(utility::notification_atom_v, notify).send(session_actor_); spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); return false; } @@ -986,17 +1275,38 @@ QFuture SessionModel::importFuture(const QUrl &path, const QVariant &json) try { js = JsonStore(qvariant_to_json(json)); } catch (const std::exception &err) { + auto notify = Notification::WarnNotification( + std::string("Import Session Failed - ") + err.what()); + notify.uuid(notification_uuid); + anon_mail(utility::notification_atom_v, notify).send(session_actor_); spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); return false; } } try { + spdlog::stopwatch sw; + scoped_actor sys{system()}; + auto session = sys->spawn(js, UriFromQUrl(path)); - sys->anon_send(session_actor_, session::merge_session_atom_v, session); - result = true; + request_receive( + *sys, session_actor_, session::merge_session_atom_v, session); + + spdlog::info( + "Session {} merged in {:.3} seconds.", StdFromQString(path.path()), sw); + + result = true; + auto notify = Notification::InfoNotification( + "Import Session Succeeded", std::chrono::seconds(5)); + notify.uuid(notification_uuid); + anon_mail(utility::notification_atom_v, notify).send(session_actor_); + } catch (const std::exception &err) { + auto notify = Notification::WarnNotification( + std::string("Import Session Failed - ") + err.what()); + notify.uuid(notification_uuid); + anon_mail(utility::notification_atom_v, notify).send(session_actor_); spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -1082,7 +1392,7 @@ QFuture SessionModel::clearCacheFuture(const QModelIndexList &indexes) { if (j.at("type") == "Media") { auto actor = actorFromString(system(), j.at("actor")); if (actor) { - anon_send(actor, media::invalidate_cache_atom_v); + anon_mail(media::invalidate_cache_atom_v).send(actor); result = true; } } @@ -1098,15 +1408,15 @@ void SessionModel::gatherMediaFor(const QModelIndex &index, const QModelIndexLis if (index.isValid()) { nlohmann::json &j = indexToData(index); - if (j.at("type") == "Playlist" or j.at("type") == "Subset" or - j.at("type") == "Timeline") { + if (j.at("type") == "Playlist" or j.at("type") == "ContactSheet" or + j.at("type") == "Subset" or j.at("type") == "Timeline") { auto actor = actorFromString(system(), j.at("actor")); if (actor) { UuidList uv; for (const auto &i : selection) uv.emplace_back(UuidFromQUuid(i.data(actorUuidRole).toUuid())); - anon_send(actor, media_hook::gather_media_sources_atom_v, uv); + anon_mail(media_hook::gather_media_sources_atom_v, uv).send(actor); } } } @@ -1116,8 +1426,75 @@ void SessionModel::gatherMediaFor(const QModelIndex &index, const QModelIndexLis } // special handing for media!! +QFuture SessionModel::getJSONObjectFuture( + const QModelIndex &index, const QString &path, const bool includeSource) { + return QtConcurrent::run([=]() { + QVariant result; + try { + if (index.isValid()) { + nlohmann::json &j = indexToData(index); + if (not j.contains("placeholder")) { + auto actor = actorFromString(system(), j.at("actor")); + auto type = j.at("type").get(); + if (actor) { + scoped_actor sys{system()}; + + try { + std::string path_string = StdFromQString(path); + if (type == "Media") { + auto jsn = request_receive( + *sys, + actor, + json_store::get_json_atom_v, + Uuid(), + path_string); + + if (includeSource) { + auto imageuuid = + UuidFromQUuid(index.data(imageActorUuidRole).toUuid()); + auto audiouuid = + UuidFromQUuid(index.data(audioActorUuidRole).toUuid()); + auto ijsn = request_receive( + *sys, + actor, + json_store::get_json_atom_v, + imageuuid, + ""); + jsn["metadata"]["image_source_metadata"] = ijsn; + if (imageuuid == audiouuid) + jsn["metadata"]["audio_source_metadata"] = ijsn; + else { + auto ajsn = request_receive( + *sys, + actor, + json_store::get_json_atom_v, + audiouuid, + ""); + jsn["metadata"]["audio_source_metadata"] = ajsn; + } + } + + result = mapFromValue(jsn); + } else { + auto jsn = request_receive( + *sys, actor, json_store::get_json_atom_v, path_string); + + result = mapFromValue(jsn); + } + } catch (...) { + } + } + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + return result; + }); +} -QFuture SessionModel::getJSONFuture(const QModelIndex &index, const QString &path) { +QFuture SessionModel::getJSONFuture( + const QModelIndex &index, const QString &path, const bool includeSource) { return QtConcurrent::run([=]() { QString result; try { @@ -1134,6 +1511,27 @@ QFuture SessionModel::getJSONFuture(const QModelIndex &index, const QSt auto jsn = request_receive( *sys, actor, json_store::get_json_atom_v, Uuid(), path_string); + if (includeSource) { + auto imageuuid = + UuidFromQUuid(index.data(imageActorUuidRole).toUuid()); + auto audiouuid = + UuidFromQUuid(index.data(audioActorUuidRole).toUuid()); + auto ijsn = request_receive( + *sys, actor, json_store::get_json_atom_v, imageuuid, ""); + jsn["metadata"]["image_source_metadata"] = ijsn; + if (imageuuid == audiouuid) + jsn["metadata"]["audio_source_metadata"] = ijsn; + else { + auto ajsn = request_receive( + *sys, + actor, + json_store::get_json_atom_v, + audiouuid, + ""); + jsn["metadata"]["audio_source_metadata"] = ajsn; + } + } + result = QStringFromStd(jsn.dump()); } else { auto jsn = request_receive( @@ -1152,6 +1550,47 @@ QFuture SessionModel::getJSONFuture(const QModelIndex &index, const QSt }); } +QFuture SessionModel::setJSONObjectFuture( + const QModelIndex &index, const QVariant &json, const QString &path) { + return QtConcurrent::run([=]() { + bool result = false; + try { + if (index.isValid()) { + nlohmann::json &j = indexToData(index); + auto actor = actorFromString(system(), j.at("actor")); + auto type = j.at("type").get(); + if (actor) { + scoped_actor sys{system()}; + + try { + if (type == "Media") { + result = request_receive( + *sys, + actor, + json_store::set_json_atom_v, + Uuid(), + JsonStore(mapFromValue(json)), + StdFromQString(path)); + } else { + result = request_receive( + *sys, + actor, + json_store::set_json_atom_v, + JsonStore(mapFromValue(json)), + StdFromQString(path)); + } + } catch (...) { + } + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + return result; + }); +} + + QFuture SessionModel::setJSONFuture( const QModelIndex &index, const QString &json, const QString &path) { return QtConcurrent::run([=]() { @@ -1192,15 +1631,19 @@ QFuture SessionModel::setJSONFuture( }); } -void SessionModel::sortAlphabetically(const QModelIndex &index) { +void SessionModel::sortByMediaDisplayInfo( + const QModelIndex &index, const int sort_column_idx, const bool ascending) { try { if (index.isValid()) { nlohmann::json &j = indexToData(index); auto actor = actorFromString(system(), j.at("actor")); auto type = j.at("type").get(); - if (actor and (type == "Subset" or type == "Playlist" or type == "Timeline")) { - anon_send(actor, playlist::sort_alphabetically_atom_v); + if (actor and (type == "Subset" or type == "ContactSheet" or type == "Playlist" or + type == "Timeline")) { + anon_mail( + playlist::sort_by_media_display_info_atom_v, sort_column_idx, ascending) + .send(actor); } } @@ -1209,102 +1652,48 @@ void SessionModel::sortAlphabetically(const QModelIndex &index) { } } -void SessionModel::setCurrentPlaylist(const QModelIndex &index) { - try { - if (index.isValid()) { - nlohmann::json &j = indexToData(index); - auto actor = actorFromString(system(), j.at("actor")); - auto type = j.at("type").get(); - if (session_actor_ and actor and - (type == "Subset" or type == "Playlist" or type == "Timeline")) { - scoped_actor sys{system()}; - anon_send(session_actor_, session::current_playlist_atom_v, actor); - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } -} +void SessionModel::setCurrentMediaContainer(const QModelIndex &index) { -void SessionModel::setPlayheadTo(const QModelIndex &index) { - try { - if (index.isValid()) { - nlohmann::json &j = indexToData(index); - auto actor = actorFromString(system(), j.at("actor")); - auto type = j.at("type").get(); + if (index.isValid()) { - if (actor and (type == "Subset" or type == "Playlist" or type == "Timeline")) { - auto ph_events = - system().registry().template get(global_playhead_events_actor); - scoped_actor sys{system()}; - try { - auto playhead = request_receive( - *sys, actor, playlist::create_playhead_atom_v) - .actor(); - anon_send(ph_events, viewport::viewport_playhead_atom_v, playhead); - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + auto cuuid = UuidFromQUuid(index.data(actorUuidRole).toUuid()); + auto cactor = actorFromQString(system(), index.data(actorRole).toString()); + + scoped_actor sys{system()}; + request_receive( + *sys, + session_actor_, + session::active_media_container_atom_v, + UuidActor(cuuid, cactor)); + + updateCurrentMediaContainerIndexFromBackend(); } } -QFuture -SessionModel::conformInsertFuture(const QString &task, const QModelIndexList &indexes) { - auto playlist_ua = UuidActor(); - auto media_uas = UuidActorVector(); + +void SessionModel::setViewportCurrentMediaContainerIndex(const QModelIndex &index) { try { - if (not indexes.empty()) { - // populate playlist - auto playlist_index = getPlaylistIndex(indexes[0]); - - if (playlist_index.isValid()) { - // get uuid and actor - playlist_ua.uuid_ = UuidFromQUuid(playlist_index.data(actorUuidRole).toUuid()); - playlist_ua.actor_ = - actorFromQString(system(), playlist_index.data(actorRole).toString()); - } - for (const auto &i : indexes) { - if (i.data(typeRole) == QString("Media")) { - media_uas.emplace_back(UuidActor( - UuidFromQUuid(i.data(actorUuidRole).toUuid()), - actorFromQString(system(), i.data(actorRole).toString()))); - } - } - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - } + if (index != current_playhead_owner_index_) { - return QtConcurrent::run([=]() { - QModelIndexList result; - try { + auto cuuid = UuidFromQUuid(index.data(actorUuidRole).toUuid()); + auto cactor = actorFromQString(system(), index.data(actorRole).toString()); + + // This is the 'viewed' playlist + const bool viewed = true; scoped_actor sys{system()}; + request_receive( + *sys, + session_actor_, + session::viewport_active_media_container_atom_v, + UuidActor(cuuid, cactor)); - if (conform_actor_ and playlist_ua.actor() and not media_uas.empty()) { - auto response = request_receive( - *sys, - conform_actor_, - conform::conform_atom_v, - StdFromQString(task), - utility::JsonStore(), // conform detail - playlist_ua, - media_uas); - - // we've got come new stuff, we maybe to contruct them, - // or they may already exist of have been created for us. - } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + updateViewportCurrentMediaContainerIndexFromBackend(); } - - return result; - }); -} \ No newline at end of file + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } +} diff --git a/src/ui/qml/session/src/session_model_timeline_ui.cpp b/src/ui/qml/session/src/session_model_timeline_ui.cpp index 1a8448dba..7b38261bc 100644 --- a/src/ui/qml/session/src/session_model_timeline_ui.cpp +++ b/src/ui/qml/session/src/session_model_timeline_ui.cpp @@ -1,10 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 +#include #include "xstudio/session/session_actor.hpp" #include "xstudio/timeline/timeline.hpp" #include "xstudio/timeline/gap_actor.hpp" #include "xstudio/timeline/clip_actor.hpp" -#include "xstudio/tag/tag.hpp" #include "xstudio/media/media.hpp" #include "xstudio/ui/qml/job_control_ui.hpp" #include "xstudio/ui/qml/session_model_ui.hpp" @@ -22,6 +22,48 @@ using namespace xstudio; using namespace xstudio::utility; using namespace xstudio::ui::qml; +// begin timeline operations +QFuture SessionModel::importTimelineFuture( + const QModelIndex &playlist_index, const QUrl &path, const QUuid &before) { + return QtConcurrent::run([=]() { + auto result = QModelIndex(); + + try { + scoped_actor sys{system()}; + auto pactor = actorFromQString(system(), playlist_index.data(actorRole).toString()); + auto uri = UriFromQUrl(path); + if (is_timeline_supported(uri)) { + auto rc = rowCount(index(2, 0, playlist_index)); + auto au = request_receive( + *sys, pactor, session::import_atom_v, uri, UuidFromQUuid(before), true); + + // really don't like this, far too many ways to go wrong. + // should really create timeline first.. + while (true) { + // wait for item.. + while (rowCount(index(2, 0, playlist_index)) == rc) { + QCoreApplication::processEvents( + QEventLoop::WaitForMoreEvents | QEventLoop::ExcludeUserInputEvents, + 50); + } + + // get new index.. + result = searchRecursive( + QUuidFromUuid(au.uuid()), actorUuidRole, index(2, 0, playlist_index)); + if (result.isValid()) + break; + else + rc = rowCount(index(2, 0, playlist_index)); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + throw; + } + return QVariant::fromValue(result); + }); +} + void SessionModel::setTimelineFocus( const QModelIndex &timeline, const QModelIndexList &indexes) const { try { @@ -34,187 +76,990 @@ void SessionModel::setTimelineFocus( uuids.emplace_back(UuidFromQUuid(i.data(idRole).toUuid())); } - anon_send(actor, timeline::focus_atom_v, uuids); + anon_mail(timeline::focus_atom_v, uuids).send(actor); } } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } } - -QModelIndex SessionModel::getTimelineIndex(const QModelIndex &index) const { +void SessionModel::setTimelineSelection( + const QModelIndex &timeline, const QModelIndexList &indexes) const { try { - if (index.isValid()) { - auto type = StdFromQString(index.data(typeRole).toString()); - if (type == "Timeline") - return index; - else - return getTimelineIndex(index.parent()); + UuidActorVector selection; + + if (timeline.isValid()) { + auto tactor = actorFromQString(system(), timeline.data(actorRole).toString()); + + for (auto &i : indexes) { + auto uuid = UuidFromQUuid(i.data(idRole).toUuid()); + auto actor = actorFromQString(system(), i.data(actorRole).toString()); + + selection.emplace_back(UuidActor(uuid, actor)); + } + + anon_mail(timeline::item_selection_atom_v, selection).send(tactor); } } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } +} - return QModelIndex(); +QRect SessionModel::timelineRect(const QModelIndexList &indexes) const { + auto result = QRect(); + auto box_inited = false; + + if (not indexes.empty()) { + // get timeline object. + auto timelineindex = getTimelineIndex(indexes[0]); + if (timelineindex.isValid()) { + // get actor and then item.. + auto tactor = actorFromQString(system(), timelineindex.data(actorRole).toString()); + if (timeline_lookup_.count(tactor)) { + auto &item = timeline_lookup_.at(tactor); + + for (const auto &i : indexes) { + auto box = item.box(UuidFromQUuid(i.data(idRole).toUuid())); + if (box) { + if (box_inited) { + result.setCoords( + std::min(result.x(), box->first.first), + std::min(result.y(), box->first.second), + std::max(result.x() + result.width(), box->second.first), + std::max(result.y() + result.height(), box->second.second)); + } else { + result.setCoords( + box->first.first, + box->first.second, + box->second.first, + box->second.second); + box_inited = true; + } + } + } + } + } + } + return result; } -bool SessionModel::removeTimelineItems( - const QModelIndex &track_index, const int frame, const int duration) { - auto result = false; +QModelIndexList SessionModel::getIndexesByName( + const QModelIndex &idx, const QString &name, const QString &type) const { + auto results = QModelIndexList(); + + if (idx.isValid()) { + if ((type.isEmpty() or idx.data(typeRole).toString() == type) and + idx.data(nameRole).toString().contains(name, Qt::CaseInsensitive)) { + results.push_back(idx); + } + + if (idx.model()->hasChildren(idx)) { + for (auto i = 0; i < idx.model()->rowCount(idx); i++) + results.append(getIndexesByName(idx.model()->index(i, 0, idx), name, type)); + } + } + + return results; +} + +QModelIndexList SessionModel::getTimelineClipIndexes( + const QModelIndex &timelineIndex, const QModelIndex &mediaIndex) { + auto result = QModelIndexList(); + + auto media_uuid = QVariant(); + // get media uuid + if (mediaIndex.isValid() and mediaIndex.data(typeRole).toString() == QString("Media")) + media_uuid = mediaIndex.data(actorUuidRole); + + if (not media_uuid.isNull()) + result = searchRecursiveList(media_uuid, clipMediaUuidRole, timelineIndex, 0, -1); + + return result; +} + +QVariantList SessionModel::mediaFrameToTimelineFrames( + const QModelIndex &timelineIndex, + const QModelIndex &mediaIndex, + const int logicalMediaFrame, + const bool skipDisabled) { + + // given the index of a media item, and a logical frame into that media, + // we want to return the corresponding timeline logical frames - i.e. the + // timeline frames where 'logicalMediaFrame' of the given media will be + // on screen. + + auto result = QVariantList(); + + // get the timeline actor + auto timeline_actor = actorFromQString(system(), timelineIndex.data(actorRole).toString()); + + // get the uuid of the media actor + utility::Uuid media_uuid; + if (mediaIndex.isValid() and mediaIndex.data(typeRole).toString() == QString("Media")) { + media_uuid = UuidFromQUuid(mediaIndex.data(actorUuidRole).toUuid()); + } + + scoped_actor sys{system()}; try { - if (track_index.isValid()) { - auto type = StdFromQString(track_index.data(typeRole).toString()); - auto actor = actorFromQString(system(), track_index.data(actorRole).toString()); + auto ri = request_receive>( + *sys, + timeline_actor, + timeline::media_frame_to_timeline_frames_atom_v, + media_uuid, + logicalMediaFrame, + skipDisabled); + for (const auto &i : ri) { + result.push_back(i); + } + } catch (...) { + } + return result; +} - if (type == "Audio Track" or type == "Video Track") { - scoped_actor sys{system()}; - request_receive( - *sys, actor, timeline::erase_item_at_frame_atom_v, frame, duration); - result = true; +Q_INVOKABLE QVariantList SessionModel::snapTo( + const QModelIndex &ignore, + const int cursor, + const int clipStart, + const int clipDuration, + const int currentOffset, + const int window, + const QUuid &key) { + + auto result = QVariantList(); + + static QVariantList search_these; + static QUuid search_key; + + if (key.isNull() or key != search_key) { + search_these = boundaryFramesInTimeline(QModelIndexList({getTimelineIndex(ignore)})); + search_key = key; + } + + auto nearest = std::numeric_limits::max(); + + // snap start of clip to cursor + auto cursor_start = std::abs((clipStart + currentOffset) - cursor); + if (cursor_start < nearest and cursor_start < window) { + nearest = cursor_start; + result = QVariantList({cursor - clipStart, QModelIndex(), true}); + } + + // snap end of clip to cursor + auto cursor_end = std::abs((clipStart + clipDuration + currentOffset) - cursor); + if (cursor_end < nearest and cursor_end < window) { + nearest = cursor_end; + result = QVariantList({cursor - (clipStart + clipDuration), QModelIndex(), false}); + } + + for (const auto &i : search_these) { + auto qlist = i.toList(); + auto mi = qlist.at(0).toModelIndex(); + if (mi == ignore) + continue; + + auto head1 = std::abs(clipStart + currentOffset - qlist.at(1).toInt()); + auto tail1 = std::abs(clipStart + currentOffset - qlist.at(2).toInt()); + auto head2 = std::abs(clipStart + clipDuration + currentOffset - qlist.at(1).toInt()); + auto tail2 = std::abs(clipStart + clipDuration + currentOffset - qlist.at(2).toInt()); + + if (head1 < nearest and head1 < window) { + nearest = head1; + result = QVariantList({qlist.at(1).toInt() - clipStart, mi, true}); + } + + if (tail1 < nearest and tail1 < window) { + nearest = tail1; + result = QVariantList({qlist.at(2).toInt() - clipStart, mi, true}); + } + + if (head2 < nearest and head2 < window) { + nearest = head2; + result = + QVariantList({qlist.at(1).toInt() - (clipStart + clipDuration), mi, false}); + } + + if (tail2 < nearest and tail2 < window) { + nearest = tail2; + result = + QVariantList({qlist.at(2).toInt() - (clipStart + clipDuration), mi, false}); + } + } + + return result; +} + +QVariantList SessionModel::boundaryFramesInTimeline(const QModelIndexList &indexes) { + auto result = QVariantList(); + + if (not indexes.isEmpty() and indexes.at(0).isValid()) { + // should all be from same timeline.. + auto tindex = getTimelineIndex(indexes.at(0)); + if (tindex.isValid()) { + auto tactor = actorFromIndex(tindex); + if (timeline_lookup_.count(tactor)) { + auto titem = timeline_lookup_.at(tactor); + + for (const auto &i : indexes) { + if (i.isValid()) { + auto type = StdFromQString(i.data(typeRole).toString()); + + if (type == "Clip") { + auto puuid = UuidFromQUuid(i.parent().data(idRole).toUuid()); + auto pitem = timeline::find_item(titem.children(), puuid); + if (pitem) { + const auto start = (*pitem)->frame_at_index(i.row()); + result.push_back(QVariantList( + {i, start, start + i.data(trimmedDurationRole).toInt()})); + } + } else if ( + type == "Timeline" or type == "TimelineItem" or + type == "Audio Track" or type == "Video Track" or type == "Stack") { + for (auto row = 0; row < rowCount(i); row++) { + result.append(boundaryFramesInTimeline({index(row, 0, i)})); + } + } + } + } } } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } return result; } -bool SessionModel::removeTimelineItems(const QModelIndexList &indexes) { - auto result = false; +int SessionModel::startFrameInParent(const QModelIndex &timelineItemIndex) { + auto result = 0; + try { + if (timelineItemIndex.isValid()) { + const auto &j = indexToData(timelineItemIndex); + if (j.count("active_range")) { + auto p = timelineItemIndex.parent(); + auto t = getTimelineIndex(timelineItemIndex); + + if (p.isValid() and t.isValid()) { + auto tactor = actorFromIndex(t); + auto puuid = UuidFromQUuid(p.data(idRole).toUuid()); + + if (timeline_lookup_.count(tactor)) { + auto pitem = + timeline::find_item(timeline_lookup_.at(tactor).children(), puuid); + if (pitem) + result = (*pitem)->frame_at_index(timelineItemIndex.row()); + } + } + } + } + } catch (...) { + } - // ignore indexes that are not timeline items.. - // be careful of invalidation, deletion order matters ? + return result; +} - // simple operations.. deletion of tracks. - for (const auto &i : indexes) { - if (i.isValid()) { - auto type = StdFromQString(i.data(typeRole).toString()); - auto actor = actorFromQString(system(), i.data(actorRole).toString()); - auto parent_index = i.parent(); - if (parent_index.isValid()) { +int SessionModel::getTimelineFrameFromClip( + const QModelIndex &clipIndex, const int logicalMediaFrame) { + auto result = -1; - caf::scoped_actor sys(system()); - auto pactor = - actorFromQString(system(), parent_index.data(actorRole).toString()); - auto row = i.row(); - - if (type == "Clip") { - // replace with gap - // get parent, and index. - // find parent timeline. - auto range = request_receive( - *sys, actor, timeline::trimmed_range_atom_v); - - if (pactor) { - auto uuid = Uuid::generate(); - auto gap = self()->spawn( - "Gap", range.frame_duration(), uuid); - request_receive( - *sys, - pactor, - timeline::insert_item_atom_v, - row, - UuidActorVector({UuidActor(uuid, gap)})); - request_receive( - *sys, pactor, timeline::erase_item_atom_v, row + 1); - } - } else { - if (pactor) { - request_receive( - *sys, pactor, timeline::erase_item_atom_v, row); - } + if (clipIndex.isValid()) { + auto start = clipIndex.data(trimmedStartRole).toInt(); + auto pstart = startFrameInParent(clipIndex); + result = pstart + (logicalMediaFrame - start); + } + + return result; +} + +int SessionModel::getNextTimelineClipFrame(const QModelIndex &timelineIndex, const int frame) { + auto result = frame; + + auto tactor = actorFromQString(system(), timelineIndex.data(actorRole).toString()); + if (timeline_lookup_.count(tactor)) { + auto &item = timeline_lookup_.at(tactor); + auto frstart = FrameRate(item.rate().to_flicks() * frame); + auto frdur = FrameRate( + item.front().trimmed_duration().to_flicks() - + (frstart.to_flicks() - item.front().trimmed_start().to_flicks())); + + scoped_actor sys{system()}; + try { + auto ri = request_receive>>( + *sys, tactor, timeline::bake_atom_v, frstart, frdur); + + if (not ri.empty()) { + auto fuuid = Uuid(); + if (ri.front()) + fuuid = ri.front()->first.uuid(); + // scan.. + auto diff = FrameRate(); + for (const auto &i : ri) { + if ((fuuid.is_null() and i) or (not fuuid.is_null() and not i) or + (not fuuid.is_null() and i and fuuid != i->first.uuid())) { + + result += diff / item.rate().to_flicks(); + break; } + diff += item.rate(); } } + } catch (...) { } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } return result; } +int SessionModel::getPreviousTimelineClipFrame( + const QModelIndex &timelineIndex, const int frame) { + auto result = 0; -QFuture SessionModel::undoFuture(const QModelIndex &index) { - return QtConcurrent::run([=]() { - auto result = false; + auto tactor = actorFromQString(system(), timelineIndex.data(actorRole).toString()); + if (timeline_lookup_.count(tactor)) { + auto &item = timeline_lookup_.at(tactor); + auto frstart = item.front().trimmed_start(); + auto frdur = FrameRate( + FrameRate(item.rate().to_flicks() * frame).to_flicks() - frstart.to_flicks()); + + scoped_actor sys{system()}; try { - if (index.isValid()) { - nlohmann::json &j = indexToData(index); - auto actor = actorFromString(system(), j.at("actor")); - auto type = j.at("type").get(); - if (actor and type == "Timeline") { - scoped_actor sys{system()}; - result = request_receive( - *sys, - actor, - history::undo_atom_v, - utility::sys_time_duration(std::chrono::milliseconds(500))); + auto ri = request_receive>>( + *sys, tactor, timeline::bake_atom_v, frstart, frdur); + + // slightly different behavior, as we jump to start of current clip before + + if (not ri.empty()) { + + // scan.. + auto diff = item.rate() * ri.size(); + auto fuuid = Uuid(); + if (ri.back()) + fuuid = ri.back()->first.uuid(); + + for (auto it = ri.rbegin(); it != ri.rend(); ++it) { + if ((fuuid.is_null() and *it) or (not fuuid.is_null() and not *it) or + (not fuuid.is_null() and *it and fuuid != (*it)->first.uuid()) + + ) { + // handle at front of clip.. + if (frame == diff / item.rate().to_flicks()) { + auto prev = std::next(it, 1); + if (prev != ri.rend()) { + fuuid = Uuid(); + if (*prev) + fuuid = (*prev)->first.uuid(); + } + } else { + result = diff / item.rate().to_flicks(); + break; + } + } + diff -= item.rate(); } } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } catch (...) { } + } - return result; - }); + return result; } -QFuture SessionModel::redoFuture(const QModelIndex &index) { + +QModelIndex +SessionModel::getTimelineClipIndex(const QModelIndex &timelineIndex, const int frame) { + auto result = QModelIndex(); + auto tactor = actorFromQString(system(), timelineIndex.data(actorRole).toString()); + if (timeline_lookup_.count(tactor)) { + auto &item = timeline_lookup_.at(tactor); + + scoped_actor sys{system()}; + try { + auto ri = request_receive( + *sys, + tactor, + timeline::bake_atom_v, + FrameRate(item.front().rate().to_flicks() * frame)); + auto cuuid = std::get<0>(ri).uuid(); + result = searchRecursive( + QVariant::fromValue(QUuidFromUuid(cuuid)), idRole, timelineIndex); + } catch (...) { + } + } + return result; +} + +QModelIndexList SessionModel::getTimelineClipIndexesFromRect( + const QModelIndex &timelineIndex, + const int left, + const int top, + const int right, + const int bottom, + const double frameScale, + const double trackScale, + const timeline::ItemType type, + const bool skipLocked) { + + auto result = QModelIndexList(); + + auto tactor = actorFromQString(system(), timelineIndex.data(actorRole).toString()); + + if (timeline_lookup_.count(tactor)) { + const auto dleft = static_cast(left); + const auto dtop = static_cast(top); + const auto dright = static_cast(right); + const auto dbottom = static_cast(bottom); + + auto &item = timeline_lookup_.at(tactor); + + // use item to resolve rectangles for all clips ? + auto clips = item.find_all_items(timeline::IT_CLIP, type); + + for (const auto &i : clips) { + const auto box = *(item.box(i.get().uuid())); + + const auto cleft = static_cast(box.first.first) * frameScale; + const auto ctop = static_cast(box.first.second) * trackScale; + const auto cright = static_cast(box.second.first) * frameScale; + const auto cbottom = static_cast(box.second.second) * trackScale; + + if (skipLocked and i.get().locked()) + continue; + + if (cright <= dleft or cbottom <= dtop or cleft >= dright or ctop >= dbottom) + continue; + + result.push_back(searchRecursive( + QVariant::fromValue(QUuidFromUuid(i.get().uuid())), idRole, timelineIndex)); + } + } + + return result; +} + +QModelIndexList SessionModel::getTimelineVideoClipIndexesFromRect( + const QModelIndex &timelineIndex, + const int left, + const int top, + const int right, + const int bottom, + const double frameScale, + const double trackScale, + const bool skipLocked) { + return getTimelineClipIndexesFromRect( + timelineIndex, + left, + top, + right, + bottom, + frameScale, + trackScale, + timeline::IT_VIDEO_TRACK, + skipLocked); +} + +QModelIndexList SessionModel::getTimelineAudioClipIndexesFromRect( + const QModelIndex &timelineIndex, + const int left, + const int top, + const int right, + const int bottom, + const double frameScale, + const double trackScale, + const bool skipLocked) { + return getTimelineClipIndexesFromRect( + timelineIndex, + left, + top, + right, + bottom, + frameScale, + trackScale, + timeline::IT_AUDIO_TRACK, + skipLocked); +} + +QModelIndex SessionModel::getTimelineIndex(const QModelIndex &index) const { + try { + if (index.isValid()) { + auto type = StdFromQString(index.data(typeRole).toString()); + if (type == "Timeline") + return index; + else + return getTimelineIndex(index.parent()); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return QModelIndex(); +} + +QVariantList SessionModel::getTimelineExportTypes() const { + auto result = QVariantList(); + caf::scoped_actor sys(system()); + auto global = system().registry().template get(global_registry); + auto epa = request_receive(*sys, global, global::get_python_atom_v); + + auto supported = + request_receive>(*sys, epa, session::export_atom_v); + for (const auto &i : supported) + result.push_back(QVariant::fromValue(QStringFromStd(to_upper(i) + " (*." + i + ")"))); + + return result; +} + +QFuture SessionModel::exportOTIO( + const QModelIndex &timeline, const QUrl &path, const QString &type, const QString &schema) { + return QtConcurrent::run([=]() { auto result = false; try { - if (index.isValid()) { - nlohmann::json &j = indexToData(index); + if (timeline == getTimelineIndex(timeline)) { + nlohmann::json &j = indexToData(timeline); auto actor = actorFromString(system(), j.at("actor")); - auto type = j.at("type").get(); - if (actor and type == "Timeline") { + if (actor) { scoped_actor sys{system()}; result = request_receive( *sys, actor, - history::redo_atom_v, - utility::sys_time_duration(std::chrono::milliseconds(500))); + global_store::save_atom_v, + UriFromQUrl(path), + StdFromQString(type), + StdFromQString(schema)); } } } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + throw; // QFuture catches it.. } return result; }); } -// trigger actor creation -void SessionModel::item_event_callback(const utility::JsonStore &event, timeline::Item &item) { +QModelIndex SessionModel::getTimelineTrackIndex(const QModelIndex &index) const { try { - auto index = search_recursive( - QVariant::fromValue(QUuidFromUuid(event.at("uuid").get())), - idRole, - QModelIndex(), - 0, - -1); + if (index.isValid()) { + auto type = StdFromQString(index.data(typeRole).toString()); + if (type == "Audio Track" or type == "Video Track") + return index; + else + return getTimelineTrackIndex(index.parent()); + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } - switch (static_cast(event.at("action"))) { - case timeline::IT_INSERT: + return QModelIndex(); +} - // check for place holder entry.. - // spdlog::warn("timeline::IT_INSERT {}", event.dump(2)); - if (index.isValid()) { - auto tree = indexToTree(index); - if (tree) { - auto new_item = timeline::Item(event.at("item"), &system()); - auto new_node = timelineItemToJson(new_item, system(), true); - auto replaced = false; - // check children.. +bool SessionModel::removeTimelineItems( + const QModelIndex &track_index, const int frame, const int duration) { + auto result = false; + try { + if (track_index.isValid()) { + auto type = StdFromQString(track_index.data(typeRole).toString()); + auto actor = actorFromQString(system(), track_index.data(actorRole).toString()); + + if (type == "Audio Track" or type == "Video Track") { + scoped_actor sys{system()}; + request_receive( + *sys, actor, timeline::erase_item_at_frame_atom_v, frame, duration, false); + result = true; + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; +} + +bool SessionModel::replaceTimelineTrack(const QModelIndex &src, const QModelIndex &dst) { + auto result = false; + + if (src.isValid() and dst.isValid() and src != dst and + not dst.data(SessionModel::Roles::lockedRole).toBool()) { + auto src_type = StdFromQString(src.data(typeRole).toString()); + auto dst_type = StdFromQString(dst.data(typeRole).toString()); + if ((src_type == "Audio Track" or src_type == "Video Track") and + (dst_type == "Audio Track" or dst_type == "Video Track")) { + // args are valid.. + // purge dst content. + auto dactor = actorFromQString(system(), dst.data(actorRole).toString()); + if (dactor) { + anon_mail(timeline::erase_item_atom_v, 0, rowCount(dst), false).send(dactor); + // JSONTreeModel::removeRows(0, rowCount(dst), dst); + // wait for update ? + while (rowCount(dst)) { + QCoreApplication::processEvents( + QEventLoop::WaitForMoreEvents | QEventLoop::ExcludeUserInputEvents, 50); + } + // clone src clips into dst. + auto items = QModelIndexList(); + for (auto i = 0; i < rowCount(src); i++) + items.push_back(index(i, 0, src)); + auto new_clips = copyRows(items, 0, dst); + for (const auto &i : new_clips) + setData(i, "", flagColourRole); + result = true; + } + } + } + return result; +} + +QModelIndexList SessionModel::duplicateTimelineClipsTo( + const QModelIndexList &indexes, const QModelIndex &trackIndex) { + + auto result = QModelIndexList(); + + if (trackIndex.data(typeRole).toString() != QString("Video Track") and + trackIndex.data(typeRole).toString() != QString("Audio Track")) + return result; + + auto target_row = rowCount(trackIndex); + + for (const auto &i : indexes) { + if (not i.isValid()) + continue; + + // clone clip and insert into new track.. + auto new_clip = copyRows(QModelIndexList({i}), target_row, trackIndex)[0]; + + // wait for it ? Not sure we need to.. + target_row++; + result.push_back(new_clip); + } + return result; +} + + +// clones clips in to new track above +// should we try and support multple clips on multiple tracks ? +// should this really be done here or in the timeline actor...? +QModelIndexList SessionModel::duplicateTimelineClips( + const QModelIndexList &indexes, + const QString &qTrackName, + const QString &qTrackSuffix, + const bool append) { + // we only handle clips + // order clips by row and sort under their tracks. + auto result = QModelIndexList(); + auto track_clips = std::map>(); + auto trackName = StdFromQString(qTrackName); + auto trackSuffix = StdFromQString(qTrackSuffix); + + auto expanded_indexed = QModelIndexList(); + + for (const auto &i : indexes) { + auto type = StdFromQString(i.data(typeRole).toString()); + if (type == "Clip") + expanded_indexed.push_back(i); + else if (type == "Audio Track" or type == "Video Track") { + for (auto j = 0; j < rowCount(i); j++) { + auto ind = index(j, 0, i); + type = StdFromQString(ind.data(typeRole).toString()); + if (type == "Clip") + expanded_indexed.push_back(ind); + } + } + } + + + for (const auto &i : expanded_indexed) { + auto track_index = getTimelineTrackIndex(i); + if (track_index.isValid()) { + if (not track_clips.count(track_index)) + track_clips[track_index] = std::vector(); + track_clips[track_index].push_back(i); + } + } + + // We now need to sort tracks and clips so we don't mess the rows up + auto sorted_track_clips = std::vector>>(); + for (const auto &i : track_clips) { + sorted_track_clips.push_back(std::make_pair(i.first, i.second)); + // sort clips.. + std::sort( + sorted_track_clips.back().second.begin(), + sorted_track_clips.back().second.end(), + [](auto &a, auto &b) { return a.row() < b.row(); }); + } + + // sort by track row + // account for audio track behaviour ? Inserting below ? + std::sort(sorted_track_clips.begin(), sorted_track_clips.end(), [](auto &a, auto &b) { + return a.first.row() > b.first.row(); + }); + + // tracks and clips should now be sorted correctly. + auto count = 0; + for (const auto &i : sorted_track_clips) { + + auto track_type = StdFromQString(i.first.data(typeRole).toString()); + auto track_name = StdFromQString(i.first.data(nameRole).toString()); + + if (not trackName.empty()) + track_name = trackName; + + if (not trackSuffix.empty()) + track_name += " " + trackSuffix; + + QModelIndex new_track_index; + + auto new_row = track_type == "Video Track" + ? (append ? 0 : i.first.row()) + : (append ? rowCount(i.first.parent()) - count : i.first.row() + 1); + + count++; + + new_track_index = insertRowsSync( + new_row, + 1, + QStringFromStd(track_type), + QStringFromStd(track_name), + i.first.parent())[0]; + + // new track created, now populate with gaps and duplicated clips. + // we need to workout the gap required to insert before the clip... + auto current_clip_index = 0; + auto target_row = 0; + + for (const auto &j : i.second) { + // sum duration of items before this clip in list. + auto leading_track_frames = 0; + for (; current_clip_index < j.row(); current_clip_index++) { + leading_track_frames += + index(current_clip_index, 0, j.parent()).data(trimmedDurationRole).toInt(); + } + + if (leading_track_frames) { + auto gap_index = insertRowsSync( + target_row, + 1, + QStringFromStd("Gap"), + QStringFromStd("Gap"), + new_track_index)[0]; + + // we need to wait until our new gap becomes valid.. + // process events for a small time then check if we've got a populated gap.. + while (gap_index.data(placeHolderRole).toBool() == true) { + QCoreApplication::processEvents( + QEventLoop::WaitForMoreEvents | QEventLoop::ExcludeUserInputEvents, 50); + } + + setData( + gap_index, QVariant::fromValue(leading_track_frames), activeDurationRole); + // increment target track row. + target_row++; + } + + // clone clip and insert into new track.. + auto new_clip = copyRows(QModelIndexList({j}), target_row, new_track_index)[0]; + + // wait for it ? Not sure we need to.. + target_row++; + result.push_back(new_clip); + current_clip_index = j.row() + 1; + } + } + + return result; +} + + +bool SessionModel::removeTimelineItems(const QModelIndexList &indexes, const bool isRipple) { + auto result = false; + try { + + // ignore indexes that are not timeline items.. + // be careful of invalidation, deletion order matters ? + + // simple operations.. deletion of tracks. + // we're deleting items using rows.. + // The order matters, and we won't get model index updates to refresh the rows.. + // Order by descending row. + auto sorted_indexes = std::vector(indexes.begin(), indexes.end()); + std::sort( + sorted_indexes.begin(), sorted_indexes.end(), [](QModelIndex &a, QModelIndex &b) { + return a.parent().row() > b.parent().row() or + (a.parent().row() == b.parent().row() and a.row() > b.row()); + }); + + // for(const auto &i: sorted_indexes) + // spdlog::warn("{} {}", i.row(), i.parent().row()); + + for (const auto &i : sorted_indexes) { + if (i.isValid()) { + auto locked = i.data(lockedRole).toBool(); + if (locked) + continue; + auto name = StdFromQString(i.data(nameRole).toString()); + auto type = StdFromQString(i.data(typeRole).toString()); + auto actor = actorFromQString(system(), i.data(actorRole).toString()); + auto parent_index = i.parent(); + + // spdlog::warn("REMOVE {} {} {} {}", type, to_string(actor), i.row(), name); + + if (parent_index.isValid()) { + locked = parent_index.data(lockedRole).toBool(); + if (locked) + continue; + + caf::scoped_actor sys(system()); + auto pactor = + actorFromQString(system(), parent_index.data(actorRole).toString()); + auto uuid = UuidFromQUuid(i.data(JSONTreeModel::Roles::idRole).toUuid()); + + if (pactor) { + if (type == "Clip") { + // replace with gap + // get parent, and index. + // // find parent timeline. + // auto range = request_receive( + // *sys, actor, timeline::trimmed_range_atom_v); + + // auto uuid = Uuid::generate(); + // auto gap = self()->spawn( + // "Gap", range.frame_duration(), uuid); + // request_receive( + // *sys, + // pactor, + // timeline::insert_item_atom_v, + // row, + // UuidActorVector({UuidActor(uuid, gap)})); + request_receive( + *sys, pactor, timeline::erase_item_atom_v, uuid, not isRipple); + } else { + request_receive( + *sys, pactor, timeline::erase_item_atom_v, uuid, false); + } + } + } + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; +} + + +QFuture SessionModel::undoFuture(const QModelIndex &index) { + return QtConcurrent::run([=]() { + auto result = false; + try { + if (index.isValid()) { + nlohmann::json &j = indexToData(index); + auto actor = actorFromString(system(), j.at("actor")); + auto type = j.at("type").get(); + if (actor and type == "Timeline") { + scoped_actor sys{system()}; + result = request_receive( + *sys, + actor, + history::undo_atom_v, + utility::sys_time_duration(std::chrono::milliseconds(500))); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; + }); +} + +QFuture SessionModel::redoFuture(const QModelIndex &index) { + return QtConcurrent::run([=]() { + auto result = false; + try { + if (index.isValid()) { + nlohmann::json &j = indexToData(index); + auto actor = actorFromString(system(), j.at("actor")); + auto type = j.at("type").get(); + if (actor and type == "Timeline") { + scoped_actor sys{system()}; + result = request_receive( + *sys, + actor, + history::redo_atom_v, + utility::sys_time_duration(std::chrono::milliseconds(500))); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + + return result; + }); +} + +void SessionModel::add_processed_event(const utility::Uuid &uuid) { + processed_events_.insert(uuid); + processed_events_queue_.push(uuid); + + while (processed_events_queue_.size() > 1000) { + processed_events_.erase(processed_events_queue_.front()); + processed_events_queue_.pop(); + } +} + +bool SessionModel::wait_for_event( + const utility::Uuid &uuid, const std::chrono::milliseconds &timeout) { + auto result = processed_events_.count(uuid); + + if (not result) { + const auto then = clock::now() + timeout; + while (not result and clock::now() < then) { + QCoreApplication::processEvents( + QEventLoop::WaitForMoreEvents | QEventLoop::ExcludeUserInputEvents, 50); + result = processed_events_.count(uuid); + } + } + + return result; +} + + +// trigger actor creation +void SessionModel::item_event_callback(const utility::JsonStore &event, timeline::Item &item) { + auto debug = false; + try { + // if (debug) + // spdlog::warn("timeline::event {}", event.dump(2)); + + auto index = searchRecursive( + QVariant::fromValue(QUuidFromUuid(event.at("uuid").get())), + idRole, + QModelIndex(), + 0, + -1); + + if (not index.isValid()) + throw std::runtime_error( + "Invalid item not found " + to_string(event.at("uuid").get())); + + switch (static_cast(event.at("action"))) { + case timeline::IA_INSERT: + + // check for place holder entry.. + if (debug) + spdlog::warn("timeline::IA_INSERT {}", event.dump(2)); + { + auto tree = indexToTree(index); + if (tree) { + auto new_item = timeline::Item(event.at("item"), &system()); + auto new_node = timelineItemToJson(new_item, system(), true); + + auto replaced = false; + // check children.. + auto place_row = 0; for (auto &i : *tree) { - auto place_row = 0; - auto data = i.data(); + auto data = i.data(); if (data.count("placeholder") and data.at("id") == new_node.at("id")) { i.data() = new_node; replaced = true; @@ -243,178 +1088,240 @@ void SessionModel::item_event_callback(const utility::JsonStore &event, timeline SessionModel::index(rowCount(index) - 1, 0, index), QVector({trackIndexRole})); } + + add_lookup(*indexToTree(index), index); } break; - case timeline::IT_REMOVE: - if (index.isValid()) { - // spdlog::warn("timeline::IT_REMOVE {}", event.dump(2)); - JSONTreeModel::removeRows(event.at("index").get(), 1, index); - if (index.data(typeRole).toString() == QString("Stack")) { - // refresh teack indexes - emit dataChanged( - SessionModel::index(0, 0, index), - SessionModel::index(rowCount(index) - 1, 0, index), - QVector({trackIndexRole})); - } + case timeline::IA_REMOVE: + if (debug) + spdlog::warn("timeline::IA_REMOVE {}", event.dump(2)); + JSONTreeModel::removeRows(event.at("index").get(), 1, index); + if (index.data(typeRole).toString() == QString("Stack")) { + // refresh teack indexes + emit dataChanged( + SessionModel::index(0, 0, index), + SessionModel::index(rowCount(index) - 1, 0, index), + QVector({trackIndexRole})); } break; - case timeline::IT_ENABLE: - if (index.isValid()) { - // spdlog::warn("timeline::IT_ENABLE {}", event.dump(2)); - if (indexToData(index).at("enabled").is_null() or - indexToData(index).at("enabled") != event.value("value", true)) { - indexToData(index)["enabled"] = event.value("value", true); - emit dataChanged(index, index, QVector({enabledRole})); - } + case timeline::IA_ENABLE: + if (debug) + spdlog::warn("timeline::IA_ENABLE {}", event.dump(2)); + if (indexToData(index).at("enabled").is_null() or + indexToData(index).at("enabled") != event.value("value", true)) { + indexToData(index)["enabled"] = event.value("value", true); + emit dataChanged(index, index, QVector({enabledRole})); } break; - case timeline::IT_NAME: - if (index.isValid()) { - // spdlog::warn("timeline::IT_NAME {}", event.dump(2)); - if (indexToData(index).at("name").is_null() or - indexToData(index).at("name") != event.value("value", "")) { - indexToData(index)["name"] = event.value("value", ""); - emit dataChanged(index, index, QVector({nameRole})); - } + case timeline::IA_LOCK: + if (debug) + spdlog::warn("timeline::IA_LOCK {}", event.dump(2)); + if (indexToData(index).at("locked").is_null() or + indexToData(index).at("locked") != event.value("value", true)) { + indexToData(index)["locked"] = event.value("value", true); + emit dataChanged(index, index, QVector({lockedRole})); } break; - case timeline::IT_FLAG: - if (index.isValid()) { - // spdlog::warn("timeline::IT_NAME {}", event.dump(2)); - if (indexToData(index).at("flag").is_null() or - indexToData(index).at("flag") != event.value("value", "")) { - indexToData(index)["flag"] = event.value("value", ""); - emit dataChanged(index, index, QVector({flagColourRole})); - } + case timeline::IA_NAME: + if (debug) + spdlog::warn("timeline::IA_NAME {}", event.dump(2)); + if (indexToData(index).at("name").is_null() or + indexToData(index).at("name") != event.value("value", "")) { + indexToData(index)["name"] = event.value("value", ""); + emit dataChanged(index, index, QVector({nameRole})); } break; - case timeline::IT_PROP: - if (index.isValid()) { - // spdlog::warn("timeline::IT_NAME {}", event.dump(2)); - if (indexToData(index).at("prop").is_null() or - indexToData(index).at("prop") != event.value("value", "")) { - indexToData(index)["prop"] = event.value("value", ""); - emit dataChanged(index, index, QVector({clipMediaUuidRole})); - } + case timeline::IA_FLAG: + if (debug) + spdlog::warn("timeline::IA_FLAG {}", event.dump(2)); + if (indexToData(index).at("flag").is_null() or + indexToData(index).at("flag") != event.value("value", "")) { + indexToData(index)["flag"] = event.value("value", ""); + emit dataChanged(index, index, QVector({flagColourRole})); } break; - case timeline::IT_ACTIVE: - if (index.isValid()) { - // spdlog::warn("timeline::IT_ACTIVE {}", event.dump(2)); - - if (event.at("value2") == true) { - if (indexToData(index).at("active_range").is_null() or - indexToData(index).at("active_range") != event.at("value")) { - indexToData(index)["active_range"] = event.at("value"); - emit dataChanged( - index, - index, - QVector( - {trimmedDurationRole, - rateFPSRole, - activeDurationRole, - trimmedStartRole, - activeStartRole})); - } - } else { - if (not indexToData(index).at("active_range").is_null()) { - indexToData(index)["active_range"] = nullptr; - emit dataChanged( - index, - index, - QVector( - {trimmedDurationRole, - rateFPSRole, - activeDurationRole, - trimmedStartRole, - activeStartRole})); - } - } + case timeline::IA_MARKER: + if (debug) + spdlog::warn("timeline::IA_MARKER {}", event.dump(2)); + if (indexToData(index).at("markers").is_null() or + indexToData(index).at("markers") != event.value("value", R"([])"_json)) { + indexToData(index)["markers"] = event.value("value", R"([])"_json); + emit dataChanged(index, index, QVector({markersRole})); } break; - case timeline::IT_AVAIL: - if (index.isValid()) { - // spdlog::warn("timeline::IT_AVAIL {}", event.dump(2)); + case timeline::IA_PROP: + if (debug) + spdlog::warn("timeline::IA_PROP {}", event.dump(2)); + if (indexToData(index).at("prop").is_null() or + indexToData(index).at("prop") != event.at("value")) { + indexToData(index)["prop"] = event.at("value"); + emit dataChanged(index, index, QVector({propertyRole, clipMediaUuidRole})); + } + break; + case timeline::IA_RANGE: + if (debug) + spdlog::warn("timeline::IA_RANGE {}", event.dump(2)); + { + bool changed = false; if (event.at("value2") == true) { if (indexToData(index).at("available_range").is_null() or indexToData(index).at("available_range") != event.at("value")) { indexToData(index)["available_range"] = event.at("value"); - emit dataChanged( - index, - index, - QVector( - {trimmedDurationRole, - rateFPSRole, - availableDurationRole, - trimmedStartRole, - availableStartRole})); + changed = true; } } else { if (not indexToData(index).at("available_range").is_null()) { indexToData(index)["available_range"] = nullptr; - emit dataChanged( - index, - index, - QVector( - {trimmedDurationRole, - rateFPSRole, - availableDurationRole, - trimmedStartRole, - availableStartRole})); + changed = true; + } + } + + if (event.at("value4") == true) { + if (indexToData(index).at("active_range").is_null() or + indexToData(index).at("active_range") != event.at("value3")) { + indexToData(index)["active_range"] = event.at("value3"); + changed = true; + } + } else { + if (not indexToData(index).at("active_range").is_null()) { + indexToData(index)["active_range"] = nullptr; + changed = true; } } + + if (changed) + emit dataChanged( + index, + index, + QVector( + {trimmedDurationRole, + rateFPSRole, + activeDurationRole, + activeRangeValidRole, + trimmedStartRole, + activeStartRole})); } break; - case timeline::IT_SPLICE: - if (index.isValid()) { - // spdlog::warn("timeline::IT_SPLICE {}", event.dump(2)); - - auto frst = event.at("first").get(); - auto count = event.at("count").get(); - auto dst = event.at("dst").get(); + case timeline::IA_ACTIVE: + if (debug) + spdlog::warn("timeline::IA_ACTIVE {}", event.dump(2)); - // massage values if they'll not work with qt.. - if (dst >= frst and dst <= frst + count - 1) { - dst = frst + count; - // spdlog::warn("FAIL ?"); + if (event.at("value2") == true) { + if (indexToData(index).at("active_range").is_null() or + indexToData(index).at("active_range") != event.at("value")) { + indexToData(index)["active_range"] = event.at("value"); + emit dataChanged( + index, + index, + QVector( + {trimmedDurationRole, + rateFPSRole, + activeDurationRole, + trimmedStartRole, + activeRangeValidRole, + activeStartRole})); + } + } else { + if (not indexToData(index).at("active_range").is_null()) { + indexToData(index)["active_range"] = nullptr; + emit dataChanged( + index, + index, + QVector( + {trimmedDurationRole, + rateFPSRole, + activeDurationRole, + activeRangeValidRole, + trimmedStartRole, + activeStartRole})); } + } + break; - JSONTreeModel::moveRows(index, frst, count, index, dst); + case timeline::IA_AVAIL: + if (debug) + spdlog::warn("timeline::IA_AVAIL {}", event.dump(2)); - if (index.data(typeRole).toString() == QString("Stack")) { - // refresh teack indexes + if (event.at("value2") == true) { + if (indexToData(index).at("available_range").is_null() or + indexToData(index).at("available_range") != event.at("value")) { + indexToData(index)["available_range"] = event.at("value"); emit dataChanged( - SessionModel::index(0, 0, index), - SessionModel::index(rowCount(index) - 1, 0, index), - QVector({trackIndexRole})); + index, + index, + QVector( + {trimmedDurationRole, + rateFPSRole, + activeRangeValidRole, + availableDurationRole, + trimmedStartRole, + availableStartRole})); + } + } else { + if (not indexToData(index).at("available_range").is_null()) { + indexToData(index)["available_range"] = nullptr; + emit dataChanged( + index, + index, + QVector( + {trimmedDurationRole, + rateFPSRole, + activeRangeValidRole, + availableDurationRole, + trimmedStartRole, + availableStartRole})); } } break; - case timeline::IT_ADDR: - if (index.isValid()) { - // spdlog::warn("timeline::IT_ADDR {}", event.dump(2)); - // is the string actor valid here ? - if (event.at("value").is_null() and - not indexToData(index).at("actor").is_null()) { - indexToData(index)["actor"] = nullptr; - emit dataChanged(index, index, QVector({actorRole})); - } else if ( - event.at("value").is_string() and - (not indexToData(index).at("actor").is_string() or - event.at("value") != indexToData(index).at("actor"))) { - indexToData(index)["actor"] = event.at("value"); - emit dataChanged(index, index, QVector({actorRole})); - } + case timeline::IA_SPLICE: { + if (debug) + spdlog::warn("timeline::IA_SPLICE {}", event.dump(2)); + + auto frst = event.at("first").get(); + auto count = event.at("count").get(); + auto dst = event.at("dst").get(); + + // massage values if they'll not work with qt.. + if (dst >= frst and dst <= frst + count - 1) { + dst = frst + count; + // spdlog::warn("FAIL ?"); + } + + JSONTreeModel::moveRows(index, frst, count, index, dst); + + if (index.data(typeRole).toString() == QString("Stack")) { + // refresh teack indexes + emit dataChanged( + SessionModel::index(0, 0, index), + SessionModel::index(rowCount(index) - 1, 0, index), + QVector({trackIndexRole})); + } + } break; + + case timeline::IA_ADDR: + if (debug) + spdlog::warn("timeline::IA_ADDR {}", event.dump(2)); + // is the string actor valid here ? + if (event.at("value").is_null() and not indexToData(index).at("actor").is_null()) { + indexToData(index)["actor"] = nullptr; + emit dataChanged(index, index, QVector({actorRole})); + } else if ( + event.at("value").is_string() and + (not indexToData(index).at("actor").is_string() or + event.at("value") != indexToData(index).at("actor"))) { + indexToData(index)["actor"] = event.at("value"); + emit dataChanged(index, index, QVector({actorRole})); + // add_lookup(*indexToTree(index), index); } break; @@ -423,6 +1330,8 @@ void SessionModel::item_event_callback(const utility::JsonStore &event, timeline break; } + add_processed_event(event.at("event_id").get()); + } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -450,6 +1359,8 @@ QModelIndex SessionModel::insertTimelineGap( "actor": null, "enabled": true, "id": null, + "prop": null, + "markers": null, "name": null, "placeholder": true, "active_range": null, @@ -459,7 +1370,8 @@ QModelIndex SessionModel::insertTimelineGap( auto new_uuid = utility::Uuid::generate(); - auto duration = utility::FrameRateDuration(frames, FrameRate(1.0 / rate)); + auto duration = + utility::FrameRateDuration(frames, FrameRate(fps_to_flicks(rate))); auto new_item = self()->spawn(name, duration, new_uuid); insertion_json["actor"] = actorToString(system(), new_item); @@ -676,14 +1588,17 @@ bool SessionModel::moveRangeTimelineItems( : caf::actor(); scoped_actor sys{system()}; - request_receive( + auto event = request_receive( *sys, track_actor, timeline::move_item_at_frame_atom_v, frame, duration, dest, - insert); + insert, + true); + if (not wait_for_event(timeline::get_event_id(event).back())) + spdlog::warn("event timed out"); result = true; } @@ -699,12 +1614,12 @@ bool SessionModel::alignTimelineItems(const QModelIndexList &indexes, const bool if (indexes.size() > 1) { // index 0 is item to align to with respect to the track. - int align_to = indexes[0].data(parentStartRole).toInt(); + int align_to = startFrameInParent(indexes[0]); if (align_right) align_to += indexes[0].data(trimmedDurationRole).toInt(); for (auto i = 1; i < indexes.size(); i++) { - auto frame = indexes[i].data(parentStartRole).toInt(); + auto frame = startFrameInParent(indexes[i]); if (align_right) frame += indexes[i].data(trimmedDurationRole).toInt(); @@ -762,7 +1677,7 @@ QFuture> SessionModel::handleTimelineIdDropFuture( for (const auto &i : jdrop.at("xstudio/timeline-ids")) { // find media index - auto mind = search_recursive(QUuid::fromString(QStringFromStd(i)), idRole); + auto mind = searchRecursive(QUuid::fromString(QStringFromStd(i)), idRole); if (mind.isValid()) { auto item_uuid = UuidFromQUuid(mind.data(idRole).toUuid()); @@ -847,7 +1762,8 @@ QFuture> SessionModel::handleTimelineIdDropFuture( *sys, std::get<3>(i), timeline::erase_item_atom_v, - std::get<1>(i)); + std::get<1>(i), + false); // we should be able to insert this.. // make sure before is a container... @@ -885,3 +1801,1571 @@ QFuture> SessionModel::handleTimelineIdDropFuture( return results; }); } + + +QModelIndex +SessionModel::bakeTimelineItems(const QModelIndexList &indexes, const QString &qtrackName) { + auto result = QModelIndex(); + + // indexes should contain tracks/clips + if (not indexes.empty()) { + auto tindex = getTimelineIndex(indexes.at(0)); + auto sindex = index(0, 0, index(2, 0, tindex)); + auto trindex = getTimelineTrackIndex(indexes.at(0)); + auto rcount = rowCount(sindex); + auto trackName = StdFromQString(qtrackName); + + if (tindex.isValid() and sindex.isValid()) { + auto timeline_actor = actorFromQString(system(), tindex.data(actorRole).toString()); + auto stack_actor = actorFromQString(system(), sindex.data(actorRole).toString()); + + if (trackName.empty()) { + auto row = 0; + for (const auto &i : indexes) { + auto trackindex = getTimelineTrackIndex(i); + if (trackindex.isValid() and trackindex.row() >= row) { + row = trackindex.row(); + trackName = StdFromQString(trackindex.data(nameRole).toString()); + } + } + } + + if (timeline_actor and stack_actor) { + scoped_actor sys{system()}; + // build list of uuids.. + + auto uuids = UuidVector(); + for (const auto &i : indexes) + uuids.emplace_back(UuidFromQUuid(i.data(idRole).toUuid())); + + try { + auto ntrack = request_receive( + *sys, timeline_actor, timeline::bake_atom_v, uuids); + if (not trackName.empty()) + anon_mail(timeline::item_name_atom_v, trackName).send(ntrack.actor()); + + auto row = trindex.data(typeRole).toString() == "Video Track" + ? trindex.row() + : trindex.row() - 1; + + try { + request_receive( + *sys, + stack_actor, + timeline::insert_item_atom_v, + row, + UuidActorVector({ntrack})); + } catch (const std::exception &err) { + spdlog::warn("{} insert {}", __PRETTY_FUNCTION__, err.what()); + throw; + } + // wait for track index.. to become valid ? + while (rowCount(sindex) == rcount) { + QCoreApplication::processEvents( + QEventLoop::WaitForMoreEvents | QEventLoop::ExcludeUserInputEvents, + 50); + } + result = index(row, 0, sindex); + + try { + request_receive( + *sys, timeline_actor, timeline::link_media_atom_v, false); + } catch (const std::exception &err) { + spdlog::warn("{} link {}", __PRETTY_FUNCTION__, err.what()); + throw; + } + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + } + } + } + + return result; +} + +QModelIndexList SessionModel::modifyItemSelectionVertical( + const QModelIndexList &items, const int up, const int down) { + auto result = QModelIndexList(); + + if (not items.empty()) { + auto first_type = items[0].data(typeRole); + auto parent = items[0].parent(); + // simple... + // find min , max + + auto min_a = 0; + auto min_v = min_a; + auto max_a = rowCount(parent) - 1; + auto max_v = max_a; + + // find bounds.. + for (auto i = 0; i < rowCount(parent); i++) { + auto i_type = index(i, 0, parent).data(typeRole); + if (i_type == "Video Track") { + max_v = i; + min_a = i + 1; + } else { + break; + } + } + + if (first_type == "Audio Track" or first_type == "Video Track") { + + auto selected_video = std::vector>(); + auto selected_audio = std::vector>(); + + // order items by row true = video, false = audio + std::map selection; + + for (const auto &i : items) { + if (i.data(typeRole) == "Video Track") + selection[i.row()] = true; + else + selection[i.row()] = false; + } + + // iterate over selection.. + // populate ranges. + auto is_video = true; + auto start = -1; + auto end = -1; + + for (const auto &i : selection) { + if (start == -1) { + start = end = i.first; + is_video = i.second; + } else { + if (i.first != end + 1 or i.second != is_video) { + if (is_video) + selected_video.emplace_back(std::make_pair(start, end)); + else + selected_audio.emplace_back(std::make_pair(start, end)); + + start = end = i.first; + is_video = i.second; + } else { + end++; + } + } + } + + if (start != -1) { + if (is_video) + selected_video.emplace_back(std::make_pair(start, end)); + else + selected_audio.emplace_back(std::make_pair(start, end)); + } + + if (up != 0) { + for (auto &i : selected_video) { + auto new_start = i.first - up; + if (new_start >= min_v and new_start <= max_v) + i.first = new_start; + else + return items; + } + + for (auto &i : selected_audio) { + auto new_start = i.first - up; + if (new_start >= min_a and new_start <= max_a) + i.first = new_start; + else + return items; + } + } + + if (down != 0) { + for (auto &i : selected_video) { + auto new_end = i.second + down; + if (new_end <= max_v and new_end >= min_v) + i.second = new_end; + else + return items; + } + + for (auto &i : selected_audio) { + auto new_end = i.second + down; + if (new_end <= max_a and new_end >= min_a) + i.second = new_end; + else + return items; + } + } + + for (const auto &i : selected_video) { + for (auto r = i.first; r <= i.second; r++) + result.push_back(index(r, 0, parent)); + } + + for (const auto &i : selected_audio) { + for (auto r = i.first; r <= i.second; r++) + result.push_back(index(r, 0, parent)); + } + + } else if (first_type == "Clip") { + // even more complex... + // build list of vertical regions. + // get bounds of audio/video. + auto stack_index = getTimelineTrackIndex(items[0]).parent(); + auto timeline_index = getTimelineIndex(stack_index); + auto tactor = actorFromQString(system(), timeline_index.data(actorRole).toString()); + + if (timeline_lookup_.count(tactor)) { + auto &titem = timeline_lookup_.at(tactor); + + auto min_a = 0; + auto min_v = min_a; + auto max_a = rowCount(stack_index) - 1; + auto max_v = max_a; + + auto has_audio = false; + auto has_video = false; + + // find bounds.. + for (auto i = 0; i < rowCount(stack_index); i++) { + auto i_type = index(i, 0, stack_index).data(typeRole); + if (i_type == "Video Track") { + has_video = true; + max_v = i; + min_a = i + 1; + } else { + has_audio = true; + break; + } + } + // get selected clip boxes. + auto video_selection_boxes = std::list(); + auto audio_selection_boxes = std::list(); + + auto print_boxes = [](const std::list &boxes) { + for (const auto &i : boxes) { + spdlog::warn( + "l {} t {} r {} b {}", + i.first.first, + i.first.second, + i.second.first, + i.second.second); + } + }; + + for (const auto &i : items) { + if (getTimelineTrackIndex(i).data(typeRole) == "Video Track") + video_selection_boxes.emplace_back( + *(titem.box(UuidFromQUuid(i.data(idRole).toUuid())))); + else + audio_selection_boxes.emplace_back( + *(titem.box(UuidFromQUuid(i.data(idRole).toUuid())))); + } + + auto sort_track_start = [](const timeline::Box &a, const timeline::Box &b) { + return a.first.second < b.first.second or + (a.first.second == b.first.second and a.first.first < b.first.first); + }; + + auto sort_start_track = [](const timeline::Box &a, const timeline::Box &b) { + return a.first.first < b.first.first or + (a.first.first == b.first.first and a.first.second < b.first.second); + }; + + auto sort_end_track = [](const timeline::Box &a, const timeline::Box &b) { + return a.second.first < b.second.first or + (a.second.first == b.second.first and + a.first.second < b.first.second); + }; + + video_selection_boxes.sort(sort_start_track); + audio_selection_boxes.sort(sort_start_track); + + // if(not video_selection_boxes.empty()) { + // spdlog::warn("\nvideo"); + // print_boxes(video_selection_boxes); + // } + // if(not audio_selection_boxes.empty()) { + // spdlog::warn("audio"); + // print_boxes(audio_selection_boxes); + // } + + auto merge_boxes = [=](std::list &boxes) { + auto done = false; + + // merge vertical with same start + while (not done) { + done = true; + boxes.sort(sort_start_track); + + for (auto it = boxes.begin(); it != boxes.end();) { + auto nit = std::next(it, 1); + if (nit != boxes.end()) { + // it left = nit left and it bottom = nit top + if (it->first.first == nit->first.first and + it->second.second == nit->first.second) { + // match but we'll need to deal with under / overhang. + if (it->second.first < nit->second.first) { + boxes.emplace_back(std::make_pair( + std::make_pair(it->second.first, nit->first.second), + std::make_pair( + nit->second.first, nit->second.second))); + it->second.second = nit->second.second; + boxes.erase(nit); + done = false; + break; + + } else if (it->second.first > nit->second.first) { + boxes.emplace_back(std::make_pair( + std::make_pair(nit->second.first, it->first.second), + std::make_pair( + it->second.first, it->second.second))); + + it->second.first = nit->second.first; + it->second.second = nit->second.second; + boxes.erase(nit); + done = false; + break; + } else { + it->second.second = nit->second.second; + boxes.erase(nit); + } + } else { + it++; + } + } else { + it++; + } + } + } + + // merge vertical with same end + done = false; + while (not done) { + done = true; + boxes.sort(sort_end_track); + + for (auto it = boxes.begin(); it != boxes.end();) { + auto nit = std::next(it, 1); + if (nit != boxes.end()) { + // it back = nit back and it bottom = nit top + if (it->second.first == nit->second.first and + it->second.second == nit->first.second) { + // match but we'll need to deal with under / overhang. + if (it->first.first < nit->first.first) { + boxes.emplace_back(std::make_pair( + std::make_pair(it->first.first, it->first.second), + std::make_pair( + nit->first.first, it->second.second))); + it->first.first = nit->first.first; + it->second.second = nit->second.second; + boxes.erase(nit); + done = false; + break; + + } else if (it->first.first > nit->first.first) { + boxes.emplace_back(std::make_pair( + std::make_pair(nit->first.first, nit->first.second), + std::make_pair( + it->first.first, nit->second.second))); + + it->second.second = nit->second.second; + boxes.erase(nit); + done = false; + break; + } else { + it->second.second = nit->second.second; + boxes.erase(nit); + } + } else { + it++; + } + } else { + it++; + } + } + } + }; + + merge_boxes(video_selection_boxes); + merge_boxes(audio_selection_boxes); + + // if(not video_selection_boxes.empty()) { + // spdlog::warn("\nvideo"); + // print_boxes(video_selection_boxes); + // } + // if(not audio_selection_boxes.empty()) { + // spdlog::warn("audio"); + // print_boxes(audio_selection_boxes); + // } + + // expand / contract boxes. + for (auto &i : video_selection_boxes) { + i.first.second -= up; + if (i.first.second < min_v or i.first.second > max_v) + return items; + + i.second.second += down; + if (i.second.second < min_v or i.second.second - 1 > max_v) + return items; + } + + for (auto &i : audio_selection_boxes) { + i.first.second -= up; + if (i.first.second < min_a or i.first.second > max_a) + return items; + + i.second.second += down; + if (i.second.second < min_a or i.second.second - 1 > max_a) + return items; + } + + // if(not video_selection_boxes.empty()) { + // spdlog::warn("\nvideo"); + // print_boxes(video_selection_boxes); + // } + // if(not audio_selection_boxes.empty()) { + // spdlog::warn("audio"); + // print_boxes(audio_selection_boxes); + // } + + // apply new selection.. + std::set new_selection; + + if (not video_selection_boxes.empty()) { + for (const auto &i : video_selection_boxes) + for (const auto &j : getTimelineVideoClipIndexesFromRect( + timeline_index, + i.first.first, + i.first.second, + i.second.first, + i.second.second, + 1, + 1)) { + new_selection.insert(j); + } + } + + if (not audio_selection_boxes.empty()) { + for (const auto &i : audio_selection_boxes) + for (const auto &j : getTimelineAudioClipIndexesFromRect( + timeline_index, + i.first.first, + i.first.second, + i.second.first, + i.second.second, + 1, + 1)) { + new_selection.insert(j); + } + } + + for (const auto &i : new_selection) + result.push_back(i); + } + } + } + + return result; +} + +QModelIndexList SessionModel::modifyItemSelectionHorizontal( + const QModelIndexList &items, const int left, const int right) { + // spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, left, right); + + auto rows = std::map(); + auto dups = std::map>(); + + for (const auto &i : items) { + if (i.data(typeRole) != "Clip") + continue; + + const auto parent_row = i.parent().row(); + + if (not dups.count(parent_row)) + dups[parent_row] = std::set(); + if (dups[parent_row].count(i.row())) + continue; + dups[parent_row].insert(i.row()); + + if (not rows.count(parent_row)) + rows.emplace(parent_row, QModelIndexList({i})); + else + rows[parent_row].push_back(i); + } + + // sort clips + for (auto &i : rows) + std::sort( + i.second.begin(), i.second.end(), [=](const QModelIndex &a, const QModelIndex &b) { + return a.row() < b.row(); + }); + + // for(const auto &i: rows) { + // for(const auto &ii: i.second) { + // spdlog::warn("{} {}", ii.row(), ii.parent().row()); + // } + // } + + auto right_count = right; + // lets go.. + while (right_count > 0) { + for (auto &i : rows) { + // get right most clip. + auto c = i.second.last(); + auto row = c.row() + 1; + const auto parent = c.parent(); + while (row < rowCount(parent)) { + auto ind = index(row, 0, parent); + if (ind.data(typeRole) == "Clip") { + i.second.push_back(ind); + break; + } + row++; + } + } + right_count--; + } + + auto left_count = left; + // lets go.. + while (left_count > 0) { + for (auto &i : rows) { + // get right most clip. + auto c = i.second.first(); + auto row = c.row() - 1; + const auto parent = c.parent(); + while (row >= 0) { + auto ind = index(row, 0, parent); + if (ind.data(typeRole) == "Clip") { + i.second.push_front(ind); + break; + } + row--; + } + } + left_count--; + } + + left_count = left; + right_count = right; + + while (left_count < 0) { + for (auto &i : rows) { + // get right most clip. + if (i.second.size() > 1) { + i.second.pop_front(); + } + } + left_count++; + } + + while (right_count < 0) { + for (auto &i : rows) { + // get right most clip. + if (i.second.size() > 1) { + i.second.pop_back(); + } + } + right_count++; + } + + + auto result = QModelIndexList(); + // prune duplicates. + for (const auto &i : rows) + result += i.second; + + + return result; +} + +// return clips using media +void SessionModel::resetTimelineItemDragFlag(const QModelIndexList &items) { + for (const auto &i : items) { + if (not i.isValid()) + continue; + auto orig_data = mapFromValue(i.data(userDataRole)); + auto data = orig_data; + data["show_drag_left"] = false; + data["show_drag_right"] = false; + data["show_drag_middle"] = false; + data["show_drag_left_left"] = false; + data["show_drag_right_right"] = false; + data["show_rolling"] = false; + + if (orig_data != data) + setData(i, mapFromValue(data), userDataRole); + } +} + +void SessionModel::updateTimelineItemDragFlag( + const QModelIndexList &items, + const bool isRolling, + const bool isRipple, + const bool isOverwrite) { + + // spdlog::warn("updateTimelineItemDragFlag {} {} {} {}", isRolling, isRipple, isOverwrite, + // items.size()); + + for (const auto &i : items) { + if (not i.isValid()) + continue; + + auto orig_data = mapFromValue(i.data(userDataRole)); + // spdlog::warn("{}", orig_data.dump(2)); + + auto data = orig_data; + + if (isRolling) { + data["show_rolling"] = true; + } else { + // if (not isOverwrite) { + data["show_drag_left"] = true; + data["show_drag_right"] = true; + // } + data["show_drag_middle"] = true; + + if (not isRipple and not isOverwrite) { + auto pre_index = index(i.row() - 1, 0, i.parent()); + auto ante_index = index(i.row() + 1, 0, i.parent()); + + if (pre_index.isValid() and + pre_index.data(typeRole).toString() == QString("Clip")) + data["show_drag_left_left"] = true; + + if (ante_index.isValid() and + ante_index.data(typeRole).toString() == QString("Clip")) + data["show_drag_right_right"] = true; + } + } + + if (orig_data != data) { + // spdlog::warn("{}", data.dump(2)); + setData(i, mapFromValue(data), userDataRole); + } + } +} + +void SessionModel::beginTimelineItemDrag( + const QModelIndexList &items, + const QString &mode, + const bool isRipple, + const bool isOverwrite) { + for (const auto &i : items) { + if (not i.isValid()) + continue; + if (i.data(lockedRole).toBool()) + continue; + if (i.parent().data(lockedRole).toBool()) + continue; + + auto orig_data = mapFromValue(i.data(userDataRole)); + auto data = orig_data; + + data["drag_value"] = 0; + + if (mode == QString("track")) { + data["move_y"] = 0; + } else { + // reset all + data["adjust_duration"] = 0; + data["adjust_start"] = 0; + data["is_adjusting_start"] = false; + data["is_adjusting_duration"] = false; + data["is_adjusting_preceeding"] = false; + data["is_adjusting_anteceeding"] = false; + data["is_anteceeding_track"] = false; + data["adjust_track"] = 0; + data["adjust_preceeding_gap"] = 0; + data["adjust_anteceeding_gap"] = 0; + data["move_x"] = 0; + data["move_y"] = 0; + data["is_floating"] = false; + + if (mode == QString("roll")) { + data["is_adjusting_start"] = true; + } else if (mode == QString("leftleft")) { + data["is_adjusting_start"] = true; + data["is_adjusting_duration"] = true; + + auto preceeding = index(i.row() - 1, 0, i.parent()); + auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); + auto data2 = orig_data2; + + data2["adjust_duration"] = 0; + data2["is_adjusting_duration"] = true; + + if (orig_data2 != data2) + setData(preceeding, mapFromValue(data2), userDataRole); + } else if (mode == QString("rightright")) { + data["is_adjusting_duration"] = true; + + auto anteceeding = index(i.row() + 1, 0, i.parent()); + auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); + auto data2 = orig_data2; + + data2["adjust_duration"] = 0; + data2["adjust_start"] = 0; + data2["drag_value"] = 0; + data2["is_adjusting_start"] = true; + data2["is_adjusting_duration"] = true; + + if (orig_data2 != data2) + setData(anteceeding, mapFromValue(data2), userDataRole); + } else if (mode == QString("left")) { + data["is_adjusting_start"] = true; + data["is_adjusting_duration"] = true; + + if (isOverwrite) { + data["is_floating"] = true; + } else { + auto preceeding = index(i.row() - 1, 0, i.parent()); + if (preceeding.isValid()) { + auto type = preceeding.data(typeRole).toString(); + if (type == QString("Gap")) { + data["is_adjusting_preceeding"] = true; + auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); + auto data2 = orig_data2; + + data2["adjust_duration"] = 0; + data2["is_adjusting_duration"] = true; + + if (orig_data2 != data2) + setData(preceeding, mapFromValue(data2), userDataRole); + } + } + } + } else if (mode == QString("right")) { + data["is_adjusting_duration"] = true; + + if (not isRipple) { + if (isOverwrite) { + data["is_floating"] = true; + } else { + auto anteceeding = index(i.row() + 1, 0, i.parent()); + if (anteceeding.isValid()) { + auto type = anteceeding.data(typeRole).toString(); + if (type == QString("Gap")) { + auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); + auto data2 = orig_data2; + + data["is_adjusting_anteceeding"] = true; + + data2["adjust_duration"] = 0; + data2["is_adjusting_duration"] = true; + + if (orig_data2 != data2) + setData(anteceeding, mapFromValue(data2), userDataRole); + } + } else { + data["is_anteceeding_track"] = true; + } + } + } + } else if (mode == QString("middle")) { + if (isOverwrite) { + data["is_floating"] = true; + } else { + auto preceeding = index(i.row() - 1, 0, i.parent()); + auto anteceeding = index(i.row() + 1, 0, i.parent()); + + auto preceeding_type = preceeding.isValid() + ? preceeding.data(typeRole).toString() + : QString("Track"); + auto anteceeding_type = anteceeding.isValid() + ? anteceeding.data(typeRole).toString() + : QString("Track"); + + if (preceeding_type == QString("Gap")) { + auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); + auto data2 = orig_data2; + + data["is_adjusting_preceeding"] = true; + data2["adjust_duration"] = 0; + data2["is_adjusting_duration"] = true; + + if (orig_data2 != data2) + setData(preceeding, mapFromValue(data2), userDataRole); + } else { + data["is_adjusting_preceeding"] = true; + } + + if (not isRipple) { + if (anteceeding_type == QString("Gap")) { + auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); + auto data2 = orig_data2; + + data["is_adjusting_anteceeding"] = true; + data2["adjust_duration"] = 0; + data2["is_adjusting_duration"] = true; + + if (orig_data2 != data2) + setData(anteceeding, mapFromValue(data2), userDataRole); + } else if (anteceeding_type != QString("Track")) { + data["is_adjusting_anteceeding"] = true; + } else { + data["is_anteceeding_track"] = true; + } + } + } + } + } + + + if (orig_data != data) + setData(i, mapFromValue(data), userDataRole); + } +} + +void SessionModel::updateTimelineItemDrag( + const QModelIndexList &items, + const QString &mode, + int frameChange, + int trackChange, + const bool isRipple, + const bool isOverwrite) { + for (const auto &i : items) { + if (not i.isValid()) + continue; + if (i.data(lockedRole).toBool()) + continue; + if (i.parent().data(lockedRole).toBool()) + continue; + + auto orig_data = mapFromValue(i.data(userDataRole)); + auto data = orig_data; + + if (mode == QString("track")) { + + if (i.row() + trackChange < 0) + trackChange = -i.row(); + else if (i.row() + trackChange > rowCount(i.parent())) { + trackChange = (rowCount(i.parent()) - 1) - i.row(); + } + + data["move_y"] = trackChange; + if (orig_data != data) + setData(i, mapFromValue(data), userDataRole); + } else if (mode == QString("roll")) { + auto trimmedStart = i.data(trimmedStartRole).toInt(); + auto availableStart = i.data(availableStartRole).toInt(); + auto trimmedDuration = i.data(trimmedDurationRole).toInt(); + auto availableDuration = i.data(availableDurationRole).toInt(); + + data["adjust_start"] = std::floor( + std::min( + std::max(trimmedStart + frameChange, availableStart), + availableStart + availableDuration - trimmedDuration) - + trimmedStart); + data["drag_value"] = data["adjust_start"]; + + if (orig_data != data) + setData(i, mapFromValue(data), userDataRole); + } else if (mode == QString("leftleft")) { + auto preceeding = index(i.row() - 1, 0, i.parent()); + frameChange = checkAdjust(i, frameChange, true); + frameChange = checkAdjust(preceeding, frameChange, true); + + draggingAdjust(i, frameChange); + draggingAdjust(preceeding, frameChange); + } else if (mode == QString("rightright")) { + auto anteceeding = index(i.row() + 1, 0, i.parent()); + frameChange = checkAdjust(i, frameChange, true); + frameChange = checkAdjust(anteceeding, frameChange, true); + + draggingAdjust(i, frameChange); + draggingAdjust(anteceeding, frameChange); + } else if (mode == QString("left")) { + frameChange = checkAdjust(i, frameChange, false, true); + + if (isOverwrite) { + data["move_x"] = frameChange; + } else if (data["is_adjusting_preceeding"]) { + auto preceeding = index(i.row() - 1, 0, i.parent()); + frameChange = checkAdjust(preceeding, frameChange, false); + draggingAdjust(preceeding, frameChange); + } else { + frameChange = std::max(0, frameChange); + data["adjust_preceeding_gap"] = frameChange; + } + if (orig_data != data) + setData(i, mapFromValue(data), userDataRole); + + draggingAdjust(i, frameChange); + } else if (mode == QString("right")) { + frameChange = checkAdjust(i, frameChange, true); + if (not isOverwrite) { + if (data["is_adjusting_anteceeding"]) { + auto anteceeding = index(i.row() + 1, 0, i.parent()); + frameChange = -checkAdjust(anteceeding, -frameChange, false); + draggingAdjust(anteceeding, -frameChange); + } else if (not isRipple && not data["is_anteceeding_track"]) { + frameChange = std::min(0, frameChange); + data["adjust_anteceeding_gap"] = -frameChange; + if (orig_data != data) + setData(i, mapFromValue(data), userDataRole); + } + } + + draggingAdjust(i, frameChange); + } else if (mode == QString("middle")) { + auto preceeding = index(i.row() - 1, 0, i.parent()); + auto anteceeding = index(i.row() + 1, 0, i.parent()); + auto preceeding_type = + preceeding.isValid() ? preceeding.data(typeRole).toString() : QString("Track"); + auto anteceeding_type = anteceeding.isValid() + ? anteceeding.data(typeRole).toString() + : QString("Track"); + + if (isOverwrite) { + } else if (data["is_adjusting_preceeding"] and preceeding_type == QString("Gap")) + frameChange = checkAdjust(preceeding, frameChange, false); + else + frameChange = std::max(0, frameChange); + + if (not isRipple) { + if (isOverwrite) { + } else if ( + data["is_adjusting_anteceeding"] and anteceeding_type == QString("Gap")) + frameChange = -checkAdjust(anteceeding, -frameChange, false); + else if (data["is_anteceeding_track"]) { + } else + frameChange = std::min(0, frameChange); + } + + if (not isRipple) + data["move_x"] = frameChange; + + // iterate checking each row, then returning last valid row + if (trackChange > 0) { + auto last_valid = 0; + auto ttype = i.parent().data(typeRole); + for (auto row = 0; row <= trackChange; row++) { + auto target_row = index(i.parent().row() + row, 0, i.parent().parent()); + if (target_row.isValid() and target_row.data(typeRole) != ttype) + break; + last_valid = row; + } + trackChange = last_valid; + } else if (trackChange < 0) { + auto last_valid = 0; + auto ttype = i.parent().data(typeRole); + for (auto row = 0; row <= std::abs(trackChange); row++) { + auto target_row = index(i.parent().row() - row, 0, i.parent().parent()); + if (target_row.isValid() and target_row.data(typeRole) != ttype) + break; + last_valid = row; + } + trackChange = -last_valid; + } + + if (isOverwrite) { + data["move_y"] = trackChange; + } else { + data["move_y"] = 0; + } + + if (isOverwrite) { + } else if (data["is_adjusting_preceeding"] and preceeding_type == QString("Gap")) { + draggingAdjust(preceeding, frameChange); + } else if (data["is_adjusting_preceeding"]) { + data["adjust_preceeding_gap"] = frameChange; + } + + if (isOverwrite) { + } else if (data["is_adjusting_anteceeding"] and anteceeding_type == QString("Gap")) + draggingAdjust(anteceeding, -frameChange); + else if (data["is_adjusting_anteceeding"]) + data["adjust_anteceeding_gap"] = -frameChange; + + // data["is_adjust_duration"] = true; + data["drag_value"] = frameChange; + if (orig_data != data) + setData(i, mapFromValue(data), userDataRole); + } + } +} + +void SessionModel::endTimelineItemDrag( + const QModelIndexList &items, const QString &mode, const bool isOverwrite) { + // items need to be persistent or they'll get messed up when we remove stuff. + + auto pitems = std::vector(); + for (const auto &i : items) + pitems.emplace_back(QPersistentModelIndex(i)); + + for (const auto &i : pitems) { + if (not i.isValid()) + continue; + if (i.data(lockedRole).toBool()) + continue; + if (i.parent().data(lockedRole).toBool()) + continue; + + auto orig_data = mapFromValue(i.data(userDataRole)); + auto data = orig_data; + + auto flushChange = [=]() mutable { + data["adjust_duration"] = 0; + data["adjust_start"] = 0; + data["drag_value"] = 0; + data["is_adjusting_start"] = false; + data["is_adjusting_duration"] = false; + data["is_adjusting_preceeding"] = false; + data["is_adjusting_anteceeding"] = false; + data["is_anteceeding_track"] = false; + data["adjust_preceeding_gap"] = 0; + data["adjust_anteceeding_gap"] = 0; + data["adjust_track"] = 0; + data["move_x"] = 0; + data["move_y"] = 0; + data["is_floating"] = false; + + + if (orig_data != data) { + setData(i, mapFromValue(data), userDataRole); + orig_data = data; + } + }; + + if (mode == QString("track")) { + auto val = data["move_y"].get(); + if (val != 0) { + moveTimelineItem(i, val > 0 ? val + 1 : val); + data["move_y"] = 0; + } + if (orig_data != data) { + setData(i, mapFromValue(data), userDataRole); + orig_data = data; + } + } else { + if (mode == QString("roll")) { + auto trimmedStart = i.data(trimmedStartRole).toInt(); + setData( + i, + QVariant::fromValue(trimmedStart + data["adjust_start"].get()), + activeStartRole); + } else if (mode == "leftleft") { + auto preceeding = index(i.row() - 1, 0, i.parent()); + auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); + auto data2 = orig_data2; + auto trimmedStart = i.data(trimmedStartRole).toInt(); + auto trimmedDuration = i.data(trimmedDurationRole).toInt(); + auto startFrame = trimmedStart + (not data["is_adjusting_start"].is_null() and + data["is_adjusting_start"] + ? data["adjust_start"].get() + : 0); + auto durationFrame = + trimmedDuration + (not data["is_adjusting_duration"].is_null() and + data["is_adjusting_duration"] + ? data["adjust_duration"].get() + : 0); + auto trimmedDuration2 = preceeding.data(trimmedDurationRole).toInt(); + auto durationFrame2 = + trimmedDuration2 + (not data2["is_adjusting_duration"].is_null() and + data2["is_adjusting_duration"] + ? data2["adjust_duration"].get() + : 0); + + setData(i, startFrame, activeStartRole); + setData(i, durationFrame, activeDurationRole); + setData(preceeding, durationFrame2, activeDurationRole); + + data2["is_adjusting_start"] = false; + data2["is_adjusting_duration"] = false; + data2["adjust_start"] = 0; + data2["adjust_duration"] = 0; + data2["drag_value"] = 0; + + if (orig_data2 != data2) + setData(preceeding, mapFromValue(data2), userDataRole); + } else if (mode == "rightright") { + auto anteceeding = index(i.row() + 1, 0, i.parent()); + auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); + auto data2 = orig_data2; + auto trimmedDuration = i.data(trimmedDurationRole).toInt(); + auto durationFrame = + trimmedDuration + (not data["is_adjusting_duration"].is_null() and + data["is_adjusting_duration"] + ? data["adjust_duration"].get() + : 0); + auto trimmedStart2 = anteceeding.data(trimmedStartRole).toInt(); + auto trimmedDuration2 = anteceeding.data(trimmedDurationRole).toInt(); + auto startFrame2 = + trimmedStart2 + + (not data2["is_adjusting_start"].is_null() and data2["is_adjusting_start"] + ? data2["adjust_start"].get() + : 0); + auto durationFrame2 = + trimmedDuration2 + (not data2["is_adjusting_duration"].is_null() and + data2["is_adjusting_duration"] + ? data2["adjust_duration"].get() + : 0); + + setData(i, durationFrame, activeDurationRole); + setData(anteceeding, startFrame2, activeStartRole); + setData(anteceeding, durationFrame2, activeDurationRole); + + data2["is_adjusting_start"] = false; + data2["is_adjusting_duration"] = false; + data2["adjust_start"] = 0; + data2["adjust_duration"] = 0; + data2["drag_value"] = 0; + + if (orig_data2 != data2) + setData(anteceeding, mapFromValue(data2), userDataRole); + } else if (mode == "left") { + auto trimmedStart = i.data(trimmedStartRole).toInt(); + auto trimmedDuration = i.data(trimmedDurationRole).toInt(); + auto startFrame = trimmedStart + (not data["is_adjusting_start"].is_null() and + data["is_adjusting_start"] + ? data["adjust_start"].get() + : 0); + auto durationFrame = + trimmedDuration + (not data["is_adjusting_duration"].is_null() and + data["is_adjusting_duration"] + ? data["adjust_duration"].get() + : 0); + setData(i, startFrame, activeStartRole); + setData(i, durationFrame, activeDurationRole); + + // remove material we overwrote + if (isOverwrite) { + // we got smaller, either extend or insert gap + if (durationFrame < trimmedDuration) { + auto gap = trimmedDuration - durationFrame; + auto fps = i.data(rateFPSRole).toDouble(); + + auto previous = index(i.row() - 1, 0, i.parent()); + + if (not previous.isValid() or + previous.data(typeRole) != QVariant::fromValue(QString("Gap"))) { + flushChange(); + insertTimelineGap(i.row(), i.parent(), gap, fps, "Gap"); + } else { + auto gduration = previous.data(trimmedDurationRole).toInt(); + setData(previous, gduration + gap, activeDurationRole); + setData(previous, gduration + gap, availableDurationRole); + } + } else if (durationFrame > trimmedDuration and i.row()) { + // expanding.. + // delete preceeding + auto start = std::max( + 0, startFrameInParent(i) - (durationFrame - trimmedDuration)); + flushChange(); + removeTimelineItems( + getTimelineTrackIndex(i), start, startFrameInParent(i) - start); + } + } + + if (data["is_adjusting_preceeding"]) { + auto preceeding = index(i.row() - 1, 0, i.parent()); + auto orig_data2 = mapFromValue(preceeding.data(userDataRole)); + auto data2 = orig_data2; + auto trimmedDuration2 = preceeding.data(trimmedDurationRole).toInt(); + auto durationFrame2 = + trimmedDuration2 + (not data2["is_adjusting_duration"].is_null() and + data2["is_adjusting_duration"] + ? data2["adjust_duration"].get() + : 0); + + if (durationFrame2 == 0) { + flushChange(); + removeTimelineItems(QModelIndexList({preceeding})); + } else { + setData(preceeding, durationFrame2, activeDurationRole); + setData(preceeding, durationFrame2, availableDurationRole); + data2["is_adjusting_duration"] = false; + data2["adjust_duration"] = 0; + if (orig_data2 != data2) + setData(preceeding, mapFromValue(data2), userDataRole); + } + } else if (data["adjust_preceeding_gap"] > 0) { + auto gap = data["adjust_preceeding_gap"].get(); + auto fps = i.data(rateFPSRole).toDouble(); + flushChange(); + insertTimelineGap(i.row(), i.parent(), gap, fps, "Gap"); + } + } else if (mode == "right") { + auto trimmedDuration = i.data(trimmedDurationRole).toInt(); + auto durationFrame = + trimmedDuration + (not data["is_adjusting_duration"].is_null() and + data["is_adjusting_duration"] + ? data["adjust_duration"].get() + : 0); + setData(i, durationFrame, activeDurationRole); + + // remove material we overwrote + if (isOverwrite) { + // only remove if we're not the last item. + if (durationFrame > trimmedDuration and + rowCount(i.parent()) - 1 != i.row()) { + flushChange(); + + removeTimelineItems( + getTimelineTrackIndex(i), + startFrameInParent(i) + durationFrame, + durationFrame - trimmedDuration); + } else if ( + durationFrame < trimmedDuration and + rowCount(i.parent()) - 1 != i.row()) { + auto gap = trimmedDuration - durationFrame; + auto fps = i.data(rateFPSRole).toDouble(); + + // check next item isn't a gap.. + auto next_item = index(i.row() + 1, 0, i.parent()); + if (next_item.data(typeRole) == QVariant::fromValue(QString("Gap"))) { + auto gduration = next_item.data(trimmedDurationRole).toInt(); + setData(next_item, gduration + gap, activeDurationRole); + setData(next_item, gduration + gap, availableDurationRole); + } else { + flushChange(); + insertTimelineGap(i.row() + 1, i.parent(), gap, fps, "Gap"); + } + } + } + + if (data["is_adjusting_anteceeding"]) { + auto anteceeding = index(i.row() + 1, 0, i.parent()); + auto orig_data2 = mapFromValue(anteceeding.data(userDataRole)); + auto data2 = orig_data2; + auto trimmedDuration2 = anteceeding.data(trimmedDurationRole).toInt(); + auto durationFrame2 = + trimmedDuration2 + (not data2["is_adjusting_duration"].is_null() and + data2["is_adjusting_duration"] + ? data2["adjust_duration"].get() + : 0); + + if (durationFrame2 == 0) { + removeTimelineItems(QModelIndexList({anteceeding})); + } else { + setData(anteceeding, durationFrame2, activeDurationRole); + setData(anteceeding, durationFrame2, availableDurationRole); + + data2["is_adjusting_duration"] = false; + data2["adjust_duration"] = 0; + if (orig_data2 != data2) + setData(anteceeding, mapFromValue(data2), userDataRole); + } + } else if (data["adjust_anteceeding_gap"] > 0) { + auto gap = data["adjust_anteceeding_gap"].get(); + auto fps = i.data(rateFPSRole).toDouble(); + + insertTimelineGap(i.row() + 1, i.parent(), gap, fps, "Gap"); + } + } else if (mode == "middle") { + auto preceeding = index(i.row() - 1, 0, i.parent()); + auto anteceeding = index(i.row() + 1, 0, i.parent()); + auto preceeding_type = preceeding.isValid() + ? preceeding.data(typeRole).toString() + : QString("Track"); + auto anteceeding_type = anteceeding.isValid() + ? anteceeding.data(typeRole).toString() + : QString("Track"); + + if (isOverwrite) { + auto frame_offset = data["move_x"].get(); + auto track_offset = data["move_y"].get(); + auto duration = i.data(trimmedDurationRole).toInt(); + auto start = startFrameInParent(i); + + // order of indexes is important, + // check for correct order .. + if (i == pitems.front()) { + auto sorted = QModelIndexList(items.begin(), items.end()); + + if (frame_offset > 0) { + std::sort( + sorted.begin(), + sorted.end(), + [](QModelIndex &a, QModelIndex &b) { + return a.row() > b.row(); + }); + + if (sorted != items) { + endTimelineItemDrag(sorted, mode, isOverwrite); + return; + } + } else { + std::sort( + sorted.begin(), + sorted.end(), + [](QModelIndex &a, QModelIndex &b) { + return a.row() < b.row(); + }); + + if (sorted != items) { + endTimelineItemDrag(sorted, mode, isOverwrite); + return; + } + } + } + + // ouch... + if (data["is_adjusting_preceeding"] and preceeding_type == QString("Gap")) { + auto data2 = mapFromValue(preceeding.data(userDataRole)); + data2["is_adjusting_duration"] = false; + data2["adjust_duration"] = 0; + setData(preceeding, mapFromValue(data2), userDataRole); + } + if (data["is_adjusting_anteceeding"] and + anteceeding_type == QString("Gap")) { + auto data2 = mapFromValue(anteceeding.data(userDataRole)); + data2["is_adjusting_duration"] = false; + data2["adjust_duration"] = 0; + setData(anteceeding, mapFromValue(data2), userDataRole); + } + flushChange(); + + // this involves a ton of track modifications.. + if (track_offset) { + auto target_track_index = + index(i.parent().row() + track_offset, 0, i.parent().parent()); + + if (track_offset < 0) { + for (auto tr = 0; tr >= track_offset; tr--) { + target_track_index = + index(i.parent().row() + tr, 0, i.parent().parent()); + + if (not target_track_index.isValid()) { + // create new track + auto type_role = i.parent().data(typeRole).toString(); + + target_track_index = insertRowsSync( + type_role == "Video Track" + ? 0 + : rowCount(i.parent().parent()), + 1, + type_role, + type_role, + i.parent().parent())[0]; + } + } + } else if (track_offset > 0) { + for (auto tr = 0; tr <= track_offset; tr++) { + target_track_index = + index(i.parent().row() + tr, 0, i.parent().parent()); + + if (not target_track_index.isValid()) { + // create new track + auto type_role = i.parent().data(typeRole).toString(); + + target_track_index = insertRowsSync( + type_role == "Video Track" + ? 0 + : rowCount(i.parent().parent()), + 1, + type_role, + type_role, + i.parent().parent())[0]; + } + } + } + + auto new_clips = + duplicateTimelineClipsTo(QModelIndexList({i}), target_track_index); + moveRangeTimelineItems( + getTimelineTrackIndex(new_clips[0].parent()), + startFrameInParent(new_clips[0]), + duration, + start + frame_offset, + false); + + removeTimelineItems(QModelIndexList({i})); + // wait for index to become invalidated.. + while (i.isValid()) { + QCoreApplication::processEvents( + QEventLoop::WaitForMoreEvents | + QEventLoop::ExcludeUserInputEvents, + 50); + } + + } else if (frame_offset) + moveRangeTimelineItems( + getTimelineTrackIndex(i.parent()), + start, + duration, + start + frame_offset, + false); + } else { + auto delete_preceeding = false; + auto delete_anteceeding = false; + auto adjustPreceedingGap = data["adjust_preceeding_gap"].get(); + auto adjustAnteceedingGap = data["adjust_anteceeding_gap"].get(); + auto insert_preceeding = + data["is_adjusting_preceeding"] and adjustPreceedingGap; + auto insert_anteceeding = + data["is_adjusting_anteceeding"] and adjustAnteceedingGap; + + // adjust duration of preceeding gap or delete + if (data["is_adjusting_preceeding"] and preceeding_type == QString("Gap")) { + auto trimmedDuration = preceeding.data(trimmedDurationRole).toInt(); + auto data2 = mapFromValue(preceeding.data(userDataRole)); + auto preceedingDurationFrame = + trimmedDuration + (not data2["is_adjusting_duration"].is_null() and + data2["is_adjusting_duration"] + ? data2["adjust_duration"].get() + : 0); + if (preceedingDurationFrame) { + setData(preceeding, preceedingDurationFrame, activeDurationRole); + setData(preceeding, preceedingDurationFrame, availableDurationRole); + } else { + delete_preceeding = true; + } + + data2["is_adjusting_duration"] = false; + data2["adjust_duration"] = 0; + setData(preceeding, mapFromValue(data2), userDataRole); + } + + // adjust duration of anteceeding gap or delete + if (data["is_adjusting_anteceeding"] and + anteceeding_type == QString("Gap")) { + auto trimmedDuration = anteceeding.data(trimmedDurationRole).toInt(); + auto data2 = mapFromValue(anteceeding.data(userDataRole)); + auto anteceedingDurationFrame = + trimmedDuration + (not data2["is_adjusting_duration"].is_null() and + data2["is_adjusting_duration"] + ? data2["adjust_duration"].get() + : 0); + if (anteceedingDurationFrame) { + setData(anteceeding, anteceedingDurationFrame, activeDurationRole); + setData( + anteceeding, anteceedingDurationFrame, availableDurationRole); + } else { + delete_anteceeding = true; + } + data2["is_adjusting_duration"] = false; + data2["adjust_duration"] = 0; + setData(anteceeding, mapFromValue(data2), userDataRole); + } + + flushChange(); + // spdlog::warn("insert_preceeding {} delete_preceeding {} + // insert_anteceeding {} delete_anteceeding {}", insert_preceeding, + // delete_preceeding, insert_anteceeding, delete_anteceeding ); + + // some operations are moves + if (insert_preceeding and delete_anteceeding) + moveTimelineItem(i, 1); + else if (delete_preceeding and insert_anteceeding) + moveTimelineItem(i, -1); + else { + if (delete_preceeding) + removeTimelineItems(QModelIndexList({preceeding})); + + if (delete_anteceeding) + removeTimelineItems(QModelIndexList({anteceeding})); + + if (insert_preceeding) { + auto fps = i.data(rateFPSRole).toDouble(); + insertTimelineGap( + i.row(), i.parent(), adjustPreceedingGap, fps, "Gap"); + } + + if (insert_anteceeding) { + auto fps = i.data(rateFPSRole).toDouble(); + insertTimelineGap( + i.row() + 1, i.parent(), adjustAnteceedingGap, fps, "Gap"); + } + } + } + } + + flushChange(); + } + } +} + + +void SessionModel::draggingAdjust(const QModelIndex &item, const int frameChange) { + if (item.isValid()) { + auto orig_data = mapFromValue(item.data(userDataRole)); + auto data = orig_data; + + auto type_role = item.data(typeRole).toString(); + if (type_role == QString("Clip")) { + auto doffset = frameChange; + + if (not data["is_adjusting_start"].is_null() and data["is_adjusting_start"]) { + data["adjust_start"] = frameChange; + data["drag_value"] = frameChange; + doffset = -doffset; + } + + if (not data["is_adjusting_duration"].is_null() and data["is_adjusting_duration"]) { + data["adjust_duration"] = doffset; + data["drag_value"] = doffset; + } + } else if (type_role == QString("Gap")) { + data["adjust_duration"] = frameChange; + } + + if (orig_data != data) + setData(item, mapFromValue(data), userDataRole); + } +} + +int SessionModel::checkAdjust( + const QModelIndex &item, + const int frameChange, + const bool lockDuration, + const bool lockEnd) { + auto result = frameChange; + + if (item.isValid()) { + auto type_role = item.data(typeRole).toString(); + auto data = mapFromValue(item.data(userDataRole)); + + if (type_role == QString("Clip")) { + auto trimmedStart = item.data(trimmedStartRole).toInt(); + auto trimmedDuration = item.data(trimmedDurationRole).toInt(); + auto availableStart = item.data(availableStartRole).toInt(); + auto availableDuration = item.data(availableDurationRole).toInt(); + + auto doffset = frameChange; + + if (not data["is_adjusting_start"].is_null() and data["is_adjusting_start"]) { + auto tmp = std::min( + availableStart + availableDuration - 1, + std::max(trimmedStart + frameChange, availableStart)); + + if (lockEnd and tmp > trimmedStart + trimmedDuration) + tmp = trimmedStart + trimmedDuration - 1; + + if (trimmedStart != tmp - frameChange) + return checkAdjust(item, tmp - trimmedStart); + + // if adjusting duration as well + doffset = -doffset; + } + + + if (lockDuration and not data["is_adjusting_duration"].is_null() and + data["is_adjusting_duration"]) { + auto startFrame = + not data["is_adjusting_start"].is_null() and data["is_adjusting_start"] + ? data["adjust_start"].get() + : trimmedStart; + auto tmp = std::max( + 1, + std::min( + trimmedDuration + doffset, + availableDuration - (startFrame - availableStart))); + + if (trimmedDuration != tmp - doffset) { + if (not data["is_adjusting_start"].is_null() and data["is_adjusting_start"]) + return checkAdjust(item, -(tmp - trimmedDuration)); + else + return checkAdjust(item, tmp - trimmedDuration); + } + } + } else if (type_role == QString("Gap")) { + auto trimmedDuration = item.data(trimmedDurationRole).toInt(); + auto tmp = std::max(0, trimmedDuration + frameChange); + + if (trimmedDuration != tmp - frameChange) + result = checkAdjust(item, tmp - trimmedDuration); + } + } + + return result; +} diff --git a/src/ui/qml/session/src/session_model_ui.cpp b/src/ui/qml/session/src/session_model_ui.cpp index 8246975af..9a911268d 100644 --- a/src/ui/qml/session/src/session_model_ui.cpp +++ b/src/ui/qml/session/src/session_model_ui.cpp @@ -2,11 +2,11 @@ #include "xstudio/media/media.hpp" #include "xstudio/session/session_actor.hpp" -#include "xstudio/tag/tag.hpp" #include "xstudio/timeline/item.hpp" #include "xstudio/ui/qml/caf_response_ui.hpp" #include "xstudio/ui/qml/job_control_ui.hpp" #include "xstudio/ui/qml/session_model_ui.hpp" +#include "xstudio/ui/qml/QTreeModelToTableModel.hpp" CAF_PUSH_WARNINGS #include @@ -118,6 +118,34 @@ SessionModel::actorUuidFromIndex(const QModelIndex &index, const bool try_parent return result; } +utility::Uuid +SessionModel::containerUuidFromIndex(const QModelIndex &index, const bool try_parent) { + auto result = utility::Uuid(); + + try { + if (index.isValid()) { + nlohmann::json &j = indexToData(index); + result = j.count("container_uuid") and not j.at("container_uuid").is_null() + ? j.at("container_uuid").get() + : utility::Uuid(); + + if (result.is_null() and try_parent) { + QModelIndex pindex = index.parent(); + if (pindex.isValid()) { + nlohmann::json &pj = indexToData(pindex); + result = + pj.count("container_uuid") and not pj.at("container_uuid").is_null() + ? pj.at("container_uuid").get() + : utility::Uuid(); + } + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + return result; +} + void SessionModel::forcePopulate( const utility::JsonTree &tree, const QPersistentModelIndex &search_hint) { @@ -125,8 +153,9 @@ void SessionModel::forcePopulate( if (tjson.count("group_actor") and not tjson.at("group_actor").is_null()) { auto grp = actorFromString(system(), tjson.at("group_actor").get()); - if (grp) - anon_send(grp, broadcast::join_broadcast_atom_v, as_actor()); + if (grp) { + anon_mail(broadcast::join_broadcast_atom_v, as_actor()).send(grp); + } } else if ( tjson.count("actor") and not tjson.at("actor").is_null() and @@ -134,7 +163,7 @@ void SessionModel::forcePopulate( // spdlog::info("request group {}", i); requestData( QVariant::fromValue(QUuidFromUuid(tjson.at("id"))), - Roles::idRole, + JSONTreeModel::Roles::idRole, search_hint, tjson, Roles::groupActorRole); @@ -144,31 +173,52 @@ void SessionModel::forcePopulate( // spdlog::warn("{} {}", type, item.dump(2)); // if subset or playlist, trigger auto population of children - if (type == "Playlist" or type == "Subset" or type == "Timeline") { + if (type == "Playlist" or type == "ContactSheet" or type == "Subset" or + type == "Timeline") { try { + requestData( QVariant::fromValue(QUuidFromUuid(tree.child(0)->data().at("id"))), - Roles::idRole, + JSONTreeModel::Roles::idRole, search_hint, tree.child(0)->data(), - Roles::childrenRole); + JSONTreeModel::Roles::childrenRole); requestData( QVariant::fromValue(QUuidFromUuid(tree.child(1)->data().at("id"))), - Roles::idRole, + JSONTreeModel::Roles::idRole, search_hint, tree.child(1)->data(), - Roles::childrenRole); + JSONTreeModel::Roles::childrenRole); if (type == "Playlist") requestData( QVariant::fromValue(QUuidFromUuid(tree.child(2)->data().at("id"))), - Roles::idRole, + JSONTreeModel::Roles::idRole, search_hint, tree.child(2)->data(), - Roles::childrenRole); + JSONTreeModel::Roles::childrenRole); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } + } else if (type == "Media") { + if (tjson.count("media_status")) { + if (tjson.at("media_status").is_null()) + requestData( + QVariant::fromValue(QUuidFromUuid(tjson.at("id"))), + JSONTreeModel::Roles::idRole, + search_hint, + tjson, + Roles::mediaStatusRole); + } + } else if (type == "MediaSource") { + // for grab of path data.. + // might be over kill ? + requestData( + QVariant::fromValue(QUuidFromUuid(tjson.at("id"))), + JSONTreeModel::Roles::idRole, + search_hint, + tjson, + Roles::pathRole); } } @@ -184,7 +234,7 @@ utility::Uuid SessionModel::refreshId(nlohmann::json &ij) { } void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex &parent_index) { - QVector roles({Roles::childrenRole}); + QVector roles({JSONTreeModel::Roles::childrenRole}); auto changed = false; START_SLOW_WATCHER() @@ -193,17 +243,18 @@ void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex & const auto type = ptree->data().count("type") ? ptree->data().at("type").get() : std::string(); - // spdlog::warn("processChildren {} {} {}", type, ptree->data().dump(2), rj.dump(2)); + // if(type == "Media List") + // spdlog::warn("processChildren {} {} {}", type, ptree->data().dump(2), rj.dump(2)); // spdlog::warn("processChildren {}", tree_to_json( *ptree,"children").dump(2)); // spdlog::warn("processChildren {}", rj.dump(2)); - if (type == "MediaSource" and rj.at(0).at("children").empty() and + + /*if (type == "MediaSource" and rj.at(0).at("children").empty() and rj.at(1).at("children").empty()) { // spdlog::warn("RETRY {}", rj.dump(2)); // force retry emit dataChanged(parent_index, parent_index, roles); return; - } - + }*/ try { if (type == "Session" or type == "Container List" or type == "Media" or @@ -307,6 +358,8 @@ void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex & // if appending we can process them all in one hit.. auto start = ptree->size(); + // spdlog::warn("inserting rows {} {}", i, rjc.size() - 1); + beginInsertRows(parent_index, i, rjc.size() - 1); for (; i < rjc.size(); i++) { @@ -368,6 +421,7 @@ void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex & } else if (not iju.count(rjc.at(i).at(compare_key))) { // insert + // spdlog::warn("inserting individual row {}",i); beginInsertRows(parent_index, i, i); auto new_child = ptree->insert(ptree->child(i), json_to_tree(rjc.at(i), "children")); @@ -399,7 +453,116 @@ void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex & // handle reordering // all rows exist but in wrong order.. try { - auto ordered = false; + + // First, make a vector of the *original* index of each row + // after any re-ordering has happened .... + + bool reorder_done = false; + if (rjc.size() == ptree->size()) { + + // rjc is the new layout for the children of ptree. + + // a map of the ordering key vs. the child index in the + // parent tree + std::map index_by_key; + for (size_t j = 0; j < ptree->size(); j++) { + auto cjson = ptree->child(j)->data(); + if (cjson.count(compare_key)) { + index_by_key[cjson.at(compare_key)] = j; + } + } + + std::vector reordered_src_indeces; + reordered_src_indeces.reserve(rjc.size()); + + // Now we iterate over elements of 'rjc' and get the compare + // key ... find out the index of the corresponding child of + // ptree (whose compare key matches the j'th entry of rjc). + // + // Thus we have a vector telling us how to re-order the + // children of ptree ... the j'th element of the vector gives + // us the src index in ptree. So if reordered_src_indeces[5] = 1, + // say, then the 2nd child of ptree needs to be moved to be + // the 6th child etc. + for (int j = 0; j < rjc.size(); j++) { + if (rjc.at(j).contains(compare_key)) { + auto p = index_by_key.find(rjc.at(j).at(compare_key)); + reordered_src_indeces.push_back(p->second); + } + } + if (reordered_src_indeces.size() == ptree->size()) { + + // if reordered_src_indeces looks like [0,1,2,3,4,5] + // then of course it's already in the correct order + bool needs_reorder = false; + for (int iii = 0; iii < (int)reordered_src_indeces.size(); ++iii) { + if (iii != reordered_src_indeces[iii]) { + needs_reorder = true; + break; + } + } + if (needs_reorder) { + reorder_done = + JSONTreeModel::reorderRows(parent_index, reordered_src_indeces); + } else { + reorder_done = true; + } + } + } + + if (!reorder_done) { + + // the first attempt requires that we have compare key for + // each and every child of rjc and ptree ... not sure if + // that is guaranteed + + // second attempt ... do one by one 'move rows' for re-ordering. + // (Much less efficient than using JSONTreeModel::reorderRows) + std::vector reordered_state; + std::map elements_to_move; + + for (size_t j = 0; j < rjc.size(); j++) { + reordered_state.push_back(j); + if (rjc.at(j).count(compare_key)) { + auto cjson = ptree->child(j)->data(); + if (cjson.count(compare_key)) { + elements_to_move[cjson.at(compare_key)] = j; + } + } + } + + for (int dest_idx = 0; dest_idx < rjc.size(); dest_idx++) { + + if (rjc.at(dest_idx).count(compare_key)) { + + const auto &kk = rjc.at(dest_idx).at(compare_key); + auto p = elements_to_move.find(kk); + if (p != elements_to_move.end()) { + + auto q = std::find( + reordered_state.begin(), reordered_state.end(), p->second); + int source_index = std::distance(reordered_state.begin(), q); + + if (source_index != dest_idx) { + + JSONTreeModel::moveRows( + parent_index, source_index, 1, parent_index, dest_idx); + + reordered_state.erase(q); + q = reordered_state.begin(); + std::advance(q, dest_idx); + reordered_state.insert(q, p->second); + } + } + } + } + } + + // This was the old re-ordering code. Was pretty slow and failed + // in some cases (re-ordering long media lists, for example). + // Keeping for reference. + + /*auto ordered = false; while (not ordered) { ordered = true; for (size_t i = 0; i < rjc.size(); i++) { @@ -411,13 +574,13 @@ void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex & cjson.at(compare_key) != rjc.at(i).at(compare_key)) { ordered = false; // find actual index of model row - // spdlog::warn("{} -> {}", iju[rjc.at(i).at(compare_key)], i); + spdlog::warn("{} -> {}", iju[rjc.at(i).at(compare_key)], i); JSONTreeModel::moveRows( parent_index, iju[rjc.at(i).at(compare_key)], 1, parent_index, - i); + i+1); // for update of item, as listview seems to get confused.. try { @@ -431,15 +594,16 @@ void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex & // rebuild iju iju.clear(); - for (size_t i = 0; i < ptree->size(); i++) { + for (size_t j = 0; j < ptree->size(); j++) { auto cjson = ptree->child(i)->data(); if (cjson.count(compare_key) and not cjson.count("placeholder")) - iju[cjson.at(compare_key).get()] = i; + iju[cjson.at(compare_key).get()] = j; } break; } } - } + }*/ + } catch (const std::exception &err) { spdlog::warn("{} reorder {}", __PRETTY_FUNCTION__, err.what()); } @@ -455,18 +619,9 @@ void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex & if (changed) { // update totals. - auto children = ptree->data().at("children"); - if (type == "Media List") { - if (children.is_array()) { - - setData( - parent_index.parent(), - QVariant::fromValue(int(children.size())), - mediaCountRole); - - } else { - setData(parent_index.parent(), QVariant::fromValue(0), mediaCountRole); - } + if (type == "Media List" and ptree->data().at("children").is_array()) { + // spdlog::warn("mediaCountRole {}", ptree->size()); + setData(parent_index.parent(), QVariant::fromValue(ptree->size()), mediaCountRole); } emit dataChanged(parent_index, parent_index, roles); @@ -480,6 +635,16 @@ void SessionModel::processChildren(const nlohmann::json &rj, const QModelIndex & void SessionModel::finishedDataSlot( const QVariant &search_value, const int search_role, const int role) { + // auto inflight = mapFromValue(search_value).dump() + std::to_string(search_role) + "-" + + // std::to_string(role); + // if (in_flight_requests_.count(inflight)) { + // in_flight_requests_.erase(inflight); + // } +} + +void SessionModel::startedDataSlot( + const QVariant &search_value, const int search_role, const int role) { + auto inflight = mapFromValue(search_value).dump() + std::to_string(search_role) + "-" + std::to_string(role); if (in_flight_requests_.count(inflight)) { @@ -534,37 +699,32 @@ void SessionModel::receivedData( search_start = search_index.row(); } - auto indexes = search_recursive_list(sv, search_role, search_index, search_start, hits); + auto indexes = searchRecursiveList(sv, search_role, search_index, search_start, hits); std::map role_to_key({ - {Roles::pathRole, "path"}, - {Roles::rateFPSRole, "rate"}, - {Roles::formatRole, "format"}, - {Roles::resolutionRole, "resolution"}, - {Roles::pixelAspectRole, "pixel_aspect"}, - {Roles::bitDepthRole, "bit_depth"}, - {Roles::mtimeRole, "mtime"}, - {Roles::nameRole, "name"}, - {Roles::actorUuidRole, "actor_uuid"}, {Roles::actorRole, "actor"}, + {Roles::actorUuidRole, "actor_uuid"}, {Roles::audioActorUuidRole, "audio_actor_uuid"}, - {Roles::imageActorUuidRole, "image_actor_uuid"}, - {Roles::mediaStatusRole, "media_status"}, + {Roles::bitDepthRole, "bit_depth"}, + {Roles::bookmarkUuidsRole, "bookmark_uuids"}, {Roles::flagColourRole, "flag"}, {Roles::flagTextRole, "flag_text"}, + {Roles::formatRole, "format"}, + {Roles::imageActorUuidRole, "image_actor_uuid"}, + {Roles::mediaDisplayInfoRole, "media_display_info"}, + {Roles::mediaStatusRole, "media_status"}, + {Roles::mtimeRole, "mtime"}, + {Roles::nameRole, "name"}, + {Roles::pathRole, "path"}, + {Roles::notificationRole, "notification"}, + {Roles::timecodeAsFramesRole, "timecode_as_frames"}, + {Roles::pathShakeRole, "path_shake"}, + {Roles::pixelAspectRole, "pixel_aspect"}, + {Roles::rateFPSRole, "rate"}, + {Roles::resolutionRole, "resolution"}, {Roles::selectionRole, "playhead_selection"}, {Roles::thumbnailURLRole, "thumbnail_url"}, - {Roles::metadataSet0Role, "metadata_set0"}, - {Roles::metadataSet1Role, "metadata_set1"}, - {Roles::metadataSet2Role, "metadata_set2"}, - {Roles::metadataSet3Role, "metadata_set3"}, - {Roles::metadataSet4Role, "metadata_set4"}, - {Roles::metadataSet5Role, "metadata_set5"}, - {Roles::metadataSet6Role, "metadata_set6"}, - {Roles::metadataSet7Role, "metadata_set7"}, - {Roles::metadataSet8Role, "metadata_set8"}, - {Roles::metadataSet9Role, "metadata_set9"}, - {Roles::metadataSet10Role, "metadata_set10"}, + {Roles::expandedRole, "expanded"}, }); for (auto &index : indexes) { @@ -574,6 +734,7 @@ void SessionModel::receivedData( nlohmann::json &j = indexToData(index); switch (role) { + default: if (role_to_key.count(role)) { if (j.count(role_to_key[role]) and j.at(role_to_key[role]) != result) { @@ -586,6 +747,34 @@ void SessionModel::receivedData( } break; + case Roles::bookmarkUuidsRole: + // force update for bookmarks so we can detect changes in content + if (role_to_key.count(role)) { + if (j.count(role_to_key[role]) and j.at(role_to_key[role]) != result) { + j[role_to_key[role]] = result; + emit dataChanged(index, index, roles); + } else if (not j.count(role_to_key[role])) { + j[role_to_key[role]] = result; + emit dataChanged(index, index, roles); + } else + emit dataChanged(index, index, roles); + } + break; + + case Roles::actorRole: + if (role_to_key.count(role)) { + if (j.count(role_to_key[role]) and j.at(role_to_key[role]) != result) { + j[role_to_key[role]] = result; + emit dataChanged(index, index, roles); + add_lookup(*indexToTree(index), index); + } else if (not j.count(role_to_key[role])) { + j[role_to_key[role]] = result; + emit dataChanged(index, index, roles); + add_lookup(*indexToTree(index), index); + } + } + break; + // this might be inefficient, we need a new event for media changing underneath // us case Roles::mediaStatusRole: @@ -612,6 +801,30 @@ void SessionModel::receivedData( } break; + case Roles::flagTextRole: + case Roles::flagColourRole: { + auto changed = false; + if (j.count(role_to_key[role]) and j.at(role_to_key[role]) != result) { + j[role_to_key[role]] = result; + changed = true; + } else if (not j.count(role_to_key[role])) { + j[role_to_key[role]] = result; + changed = true; + } + if (changed) { + emit dataChanged(index, index, roles); + if (j.count("type") and j.at("type") == "Media") { + // propergate to children + auto rows = rowCount(index); + if (rows) + emit dataChanged( + SessionModel::index(0, 0, index), + SessionModel::index(rows - 1, 0, index), + roles); + } + } + } break; + case Roles::thumbnailURLRole: if (role_to_key.count(role)) { if (j.count(role_to_key[role]) and j.at(role_to_key[role]) != result) { @@ -627,7 +840,7 @@ void SessionModel::receivedData( j["group_actor"] = result; auto grp = actorFromString(system(), result.get()); if (grp) { - anon_send(grp, broadcast::join_broadcast_atom_v, as_actor()); + anon_mail(broadcast::join_broadcast_atom_v, as_actor()).send(grp); // if type is playlist request children, to make sure we're in sync. if (j.at("type").is_string() and j.at("type") == "Playlist") { auto media_list_index = SessionModel::index(0, 0, index); @@ -642,7 +855,7 @@ void SessionModel::receivedData( idRole, index, media_list_index, - childrenRole); + JSONTreeModel::Roles::childrenRole); } } @@ -662,43 +875,51 @@ void SessionModel::receivedData( case JSONTreeModel::Roles::JSONTextRole: if (j.at("type") == "TimelineItem") { // this is an init setup.. + // watchout for duplicate event.. auto owner = actorFromString(system(), j.at("actor_owner").get()); - timeline_lookup_.emplace( - std::make_pair(owner, timeline::Item(result, &system()))); - - timeline_lookup_[owner].bind_item_event_func( - [this](const utility::JsonStore &event, timeline::Item &item) { - item_event_callback(event, item); - }, - true); - - // rebuild json - auto jsn = - timelineItemToJson(timeline_lookup_.at(owner), system(), true); - // spdlog::info("construct timeline object {}", jsn.dump(2)); - // root is myself - auto node = indexToTree(index); - auto new_node = json_to_tree(jsn, children_); - - node->splice(node->end(), new_node.base()); - - // update root.. - j[children_] = nlohmann::json::array(); - - // spdlog::error("{} {}", j["id"], jsn["uuid"]); - - j["id"] = jsn["id"]; - j["actor"] = jsn["actor"]; - j["enabled"] = jsn["enabled"]; - j["transparent"] = jsn["transparent"]; - j["active_range"] = jsn["active_range"]; - j["available_range"] = jsn["available_range"]; - emit dataChanged(index, index, QVector()); + + if (not timeline_lookup_.count(owner)) { + timeline_lookup_.emplace( + std::make_pair(owner, timeline::Item(result, &system()))); + + timeline_lookup_[owner].bind_item_post_event_func( + [this](const utility::JsonStore &event, timeline::Item &item) { + item_event_callback(event, item); + }, + true); + + // rebuild json + auto jsn = + timelineItemToJson(timeline_lookup_.at(owner), system(), true); + + // spdlog::info("construct timeline object {}", jsn.dump(2)); + // root is myself + auto node = indexToTree(index); + auto new_node = json_to_tree(jsn, children_); + + // beginInsertRows(index, 0, 0); + node->splice(node->end(), new_node.base()); + // endInsertRows(); + + // update root.. + j[children_] = nlohmann::json::array(); + j["id"] = jsn["id"]; + j["prop"] = jsn["prop"]; + j["actor_owner"] = jsn["actor"]; + j["enabled"] = jsn["enabled"]; + j["transparent"] = jsn["transparent"]; + j["active_range"] = jsn["active_range"]; + j["available_range"] = jsn["available_range"]; + + add_lookup(*indexToTree(index), index); + emit dataChanged(index, index, QVector()); + // make sure timeline id's are cached! + } } break; - case Roles::childrenRole: + case JSONTreeModel::Roles::childrenRole: processChildren(result, index); break; } @@ -716,14 +937,11 @@ void SessionModel::requestData( const int search_role, const QPersistentModelIndex &search_hint, const QModelIndex &index, - const int role, - const std::map &metadata_paths) const { + const int role) const { // dispatch call to backend to retrieve missing data. - // spdlog::warn("{} {}", role, StdFromQString(roleName(role))); try { - requestData( - search_value, search_role, search_hint, indexToData(index), role, metadata_paths); + requestData(search_value, search_role, search_hint, indexToData(index), role); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } @@ -734,8 +952,11 @@ void SessionModel::requestData( const int search_role, const QPersistentModelIndex &search_hint, const nlohmann::json &data, - const int role, - const std::map &metadata_paths) const { + const int role) const { + + // need some way of throttling events that are spammed when adding a lot of media. + // or the playlist will be repeatedly spammed with child requests. + // dispatch call to backend to retrieve missing data. auto inflight = mapFromValue(search_value).dump() + std::to_string(search_role) + "-" + std::to_string(role); @@ -749,11 +970,14 @@ void SessionModel::requestData( data, role, StdFromQString(roleName(role)), - metadata_paths, request_handler_); connect(tmp, &CafResponse::received, this, &SessionModel::receivedDataSlot); connect(tmp, &CafResponse::finished, this, &SessionModel::finishedDataSlot); + connect(tmp, &CafResponse::started, this, &SessionModel::startedDataSlot); + } else { + // we might miss the event if it happens after the inflight request was sent. + // spdlog::warn("ALREADY INFLIGHT {}", inflight); } } @@ -794,7 +1018,7 @@ nlohmann::json SessionModel::playlistTreeToJson( n["actor_uuid"] = i.value().uuid(); n["container_uuid"] = i.uuid(); result["children"].emplace_back(n); - } else if (type == "Subset" or type == "Timeline") { + } else if (type == "Subset" or type == "Timeline" or type == "ContactSheet") { auto n = createEntry(R"({ "name": null, "actor_uuid": null, @@ -831,6 +1055,7 @@ nlohmann::json SessionModel::playlistTreeToJson( n["children"].push_back(createEntry( R"({"type": "TimelineItem", "name": null, "actor_owner": null})"_json)); n["children"][2]["actor_owner"] = n["actor"]; + n["notification"] = nullptr; } n["children"].push_back(createEntry( @@ -891,9 +1116,11 @@ nlohmann::json SessionModel::sessionTreeToJson( "flag": null, "type": null, "actor": null, + "expanded": null, "busy": false, "media_count": 0, - "error_count": 0 + "error_count": 0, + "notification": null })"_json); n["type"] = i.value().type(); @@ -961,32 +1188,26 @@ nlohmann::json SessionModel::containerDetailToJson( result.erase("children"); } - if (detail.type_ == "Media" or detail.type_ == "MediaSource") { - result["metadata_set0"] = nullptr; - result["metadata_set1"] = nullptr; - result["metadata_set2"] = nullptr; - result["metadata_set3"] = nullptr; - result["metadata_set4"] = nullptr; - result["metadata_set5"] = nullptr; - result["metadata_set6"] = nullptr; - result["metadata_set7"] = nullptr; - result["metadata_set8"] = nullptr; - result["metadata_set9"] = nullptr; - result["metadata_set10"] = nullptr; + if (detail.type_ == "Media" or detail.type_ == "MediaSource" or + detail.type_ == "MediaStream") { result["audio_actor_uuid"] = nullptr; result["image_actor_uuid"] = nullptr; result["media_status"] = nullptr; if (detail.type_ == "Media") { - result["flag"] = nullptr; - result["flag_text"] = nullptr; + result["flag"] = nullptr; + result["flag_text"] = nullptr; + result["bookmark_uuids"] = nullptr; + result["media_display_info"] = nullptr; } else if (detail.type_ == "MediaSource") { - result["thumbnail_url"] = nullptr; - result["rate"] = nullptr; - result["path"] = nullptr; - result["resolution"] = nullptr; - result["pixel_aspect"] = nullptr; - result["bit_depth"] = nullptr; - result["format"] = nullptr; + result["thumbnail_url"] = nullptr; + result["rate"] = nullptr; + result["path"] = nullptr; + result["timecode_as_frames"] = nullptr; + result["path_shake"] = nullptr; + result["resolution"] = nullptr; + result["pixel_aspect"] = nullptr; + result["bit_depth"] = nullptr; + result["format"] = nullptr; } } @@ -1002,6 +1223,7 @@ nlohmann::json SessionModel::createEntry(const nlohmann::json &update) { return result; } + void SessionModel::moveSelectionByIndex(const QModelIndex &index, const int offset) { try { if (index.isValid()) { @@ -1010,7 +1232,7 @@ void SessionModel::moveSelectionByIndex(const QModelIndex &index, const int offs if (j.at("type") == "PlayheadSelection") { auto actor = actorFromString(system(), j.at("actor")); if (actor) { - anon_send(actor, playhead::select_next_media_atom_v, offset); + anon_mail(playhead::select_next_media_atom_v, offset).send(actor); } } } @@ -1019,43 +1241,91 @@ void SessionModel::moveSelectionByIndex(const QModelIndex &index, const int offs } } -void SessionModel::updateSelection(const QModelIndex &index, const QModelIndexList &selection) { +void SessionModel::updateSelection( + const QModelIndex &index, const QModelIndexList &selection, const int qmode) { try { if (index.isValid()) { nlohmann::json &j = indexToData(index); - // spdlog::warn("{}", j.dump(2)); if (j.at("type") == "PlayheadSelection" && j.at("actor").is_string()) { auto actor = actorFromString(system(), j.at("actor")); if (actor) { - UuidList uv; - for (const auto &i : selection) + utility::UuidVector uv; + uv.reserve(selection.size()); + for (const auto &i : selection) { uv.emplace_back(UuidFromQUuid(i.data(actorUuidRole).toUuid())); + } + playhead::SelectionMode mode = playhead::SelectionMode::SM_NO_UPDATE; + + switch (qmode) { + case QItemSelectionModel::Clear: + mode = playhead::SelectionMode::SM_CLEAR; + break; + case QItemSelectionModel::Select: + mode = playhead::SelectionMode::SM_SELECT; + break; + case QItemSelectionModel::Deselect: + mode = playhead::SelectionMode::SM_DESELECT; + break; + case QItemSelectionModel::Toggle: + mode = playhead::SelectionMode::SM_TOGGLE; + break; + case QItemSelectionModel::ClearAndSelect: + mode = playhead::SelectionMode::SM_CLEAR_AND_SELECT; + break; + case QItemSelectionModel::NoUpdate: + default: + break; + } - anon_send(actor, playlist::select_media_atom_v, uv); + anon_mail(playlist::select_media_atom_v, uv, mode).send(actor); } } } } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); - if (index.isValid()) { + /*if (index.isValid()) { nlohmann::json &j = indexToData(index); spdlog::warn("{}", j.dump(2)); - } + }*/ } } +void SessionModel::updateMediaListFilterString( + const QModelIndex &index, const QString &filter_string) { + try { + + if (index.isValid()) { + nlohmann::json &j = indexToData(index); + if (j.at("type") == "PlayheadSelection" && j.at("actor").is_string()) { + auto actor = actorFromString(system(), j.at("actor")); + if (actor) { + anon_mail(playlist::media_filter_string_v, StdFromQString(filter_string)) + .send(actor); + } + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + /*if (index.isValid()) { + nlohmann::json &j = indexToData(index); + spdlog::warn("{}", j.dump(2)); + }*/ + } +} nlohmann::json SessionModel::timelineItemToJson( const timeline::Item &item, caf::actor_system &sys, const bool recurse) { auto result = R"({})"_json; - result["id"] = item.uuid(); - result["actor"] = actorToString(sys, item.actor()); - result["type"] = to_string(item.item_type()); - result["name"] = item.name(); - result["flag"] = item.flag(); - result["prop"] = item.prop(); + result["id"] = item.uuid(); + result["actor"] = actorToString(sys, item.actor()); + result["type"] = to_string(item.item_type()); + result["name"] = item.name(); + result["flag"] = item.flag(); + result["locked"] = item.locked(); + result["markers"] = serialise_markers(item.markers()); + result["prop"] = item.prop(); result["active_range"] = nullptr; result["available_range"] = nullptr; @@ -1063,22 +1333,28 @@ nlohmann::json SessionModel::timelineItemToJson( auto active_range = item.active_range(); auto available_range = item.available_range(); + result["enabled"] = item.enabled(); + result["transparent"] = item.transparent(); + if (active_range) + result["active_range"] = *active_range; + if (available_range) + result["available_range"] = *available_range; + switch (item.item_type()) { case timeline::IT_NONE: break; - case timeline::IT_GAP: + break; case timeline::IT_AUDIO_TRACK: case timeline::IT_VIDEO_TRACK: + result["notification"] = nullptr; + result["group_actor"] = nullptr; + break; case timeline::IT_STACK: + break; case timeline::IT_TIMELINE: + break; case timeline::IT_CLIP: - result["enabled"] = item.enabled(); - result["transparent"] = item.transparent(); - if (active_range) - result["active_range"] = *active_range; - if (available_range) - result["available_range"] = *available_range; break; } @@ -1110,3 +1386,110 @@ nlohmann::json SessionModel::timelineItemToJson( return result; } + +MediaListFilterModel::MediaListFilterModel(QObject *parent) : super(parent) { + setDynamicSortFilter(true); +} + +bool MediaListFilterModel::filterAcceptsRow( + int source_row, const QModelIndex &source_parent) const { + + // filter in Media items only + QVariant t = sourceModel()->data( + sourceModel()->index(source_row, 0, source_parent), SessionModel::Roles::typeRole); + + if (t.toString() != "Media") + return false; + + // return false; + // surely we never run this if sourceModel is nullptr but check just in case. + if (search_string_.isEmpty()) + return true; + + // here we get the mediaDisplayInfoRole data ... this is QList> + // The outer list is because we have multiple media list panels with independently + // configured columns of information - one inner list per media list panel + // The inner list is the data for each column that is shown in the media list + // panel against each media item + QVariant v = sourceModel()->data( + sourceModel()->index(source_row, 0, source_parent), + SessionModel::Roles::mediaDisplayInfoRole); + + + QList cols = v.toList(); + if (cols.size()) { + bool match = false; + // do a string match against any bit of data that can be converted + // to a string + for (const auto &col : cols) { + if (col.toString().contains(search_string_, Qt::CaseInsensitive)) { + match = true; + break; + } + } + return match; + } + return true; +} + +QModelIndex MediaListFilterModel::rowToSourceIndex(const int row) const { + + QModelIndex srcIdx; + if (row == -1) { + // last row + srcIdx = mapToSource(index(rowCount() - 1, 0)); + QTreeModelToTableModel *mdl = dynamic_cast(sourceModel()); + if (mdl) { + srcIdx = mdl->mapToModel(srcIdx); + } + // next item + srcIdx = srcIdx.siblingAtRow(srcIdx.row() + 1); + + } else { + srcIdx = mapToSource(index(row, 0)); + QTreeModelToTableModel *mdl = dynamic_cast(sourceModel()); + if (mdl) { + srcIdx = mdl->mapToModel(srcIdx); + } + } + return srcIdx; +} + +int MediaListFilterModel::sourceIndexToRow(const QModelIndex &idx) const { + + // idx comes from SessionModelUI, which is the sourceModel of OUR sourceModel + QModelIndex remapped = idx; + QTreeModelToTableModel *mdl = dynamic_cast(sourceModel()); + if (mdl) { + // map from SessionModelUI to OUR sourceModel + remapped = mdl->mapFromModel(remapped); + } + // now map from OUR sourceModel to our own index + remapped = mapFromSource(remapped); + if (!remapped.isValid()) + return -1; + return remapped.row(); +} + +int MediaListFilterModel::getRowWithMatchingRoleData( + const QVariant &searchValue, const QString &searchRole) const { + + QTreeModelToTableModel *mdl = dynamic_cast(sourceModel()); + if (mdl) { + // map from SessionModelUI to OUR sourceModel + SessionModel *sessionModel = dynamic_cast(mdl->model()); + if (sessionModel) { + // note - we use this for the auto-scroll in the media list. We have the uuid of the + // on-screen media, we need to find our row in the model. If we are looking at a + // playlist, we don't want to match to media in a subset or timeline that also + // contains the media. The depth=2 in this call means we only search the playlist + // not its children + QModelIndex r = + sessionModel->searchRecursive(searchValue, searchRole, mdl->rootIndex(), 0, 2); + if (r.isValid()) { + return sourceIndexToRow(r); + } + } + } + return -1; +} diff --git a/src/ui/qml/studio/src/CMakeLists.txt b/src/ui/qml/studio/src/CMakeLists.txt index 58853a88e..09ad6c076 100644 --- a/src/ui/qml/studio/src/CMakeLists.txt +++ b/src/ui/qml/studio/src/CMakeLists.txt @@ -1,13 +1,21 @@ SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core + CAF::core + Qt6::Core + Qt6::OpenGLWidgets + xstudio::ui::qml::bookmark + xstudio::ui::qml::conform + xstudio::ui::qml::embedded_python + xstudio::ui::qml::global_store xstudio::ui::qml::helper + xstudio::ui::qml::log + xstudio::ui::qml::session + xstudio::ui::qml::viewport + xstudio::ui::qt::viewport_widget xstudio::utility xstudio::session - xstudio::ui::qt::viewport_widget ) SET(EXTRAMOC ) -create_qml_component(studio 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") +create_qml_component(studio ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/studio/src/export.h b/src/ui/qml/studio/src/export.h deleted file mode 100644 index 767079013..000000000 --- a/src/ui/qml/studio/src/export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef STUDIO_QML_EXPORT_H -#define STUDIO_QML_EXPORT_H - -#ifdef STUDIO_QML_STATIC_DEFINE -# define STUDIO_QML_EXPORT -# define STUDIO_QML_NO_EXPORT -#else -# ifndef STUDIO_QML_EXPORT -# ifdef studio_qml_EXPORTS - /* We are building this library */ -# define STUDIO_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define STUDIO_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef STUDIO_QML_NO_EXPORT -# define STUDIO_QML_NO_EXPORT -# endif -#endif - -#ifndef STUDIO_QML_DEPRECATED -# define STUDIO_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef STUDIO_QML_DEPRECATED_EXPORT -# define STUDIO_QML_DEPRECATED_EXPORT STUDIO_QML_EXPORT STUDIO_QML_DEPRECATED -#endif - -#ifndef STUDIO_QML_DEPRECATED_NO_EXPORT -# define STUDIO_QML_DEPRECATED_NO_EXPORT STUDIO_QML_NO_EXPORT STUDIO_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef STUDIO_QML_NO_DEPRECATED -# define STUDIO_QML_NO_DEPRECATED -# endif -#endif - -#endif /* STUDIO_QML_EXPORT_H */ diff --git a/src/ui/qml/studio/src/include/studio_qml_export.h b/src/ui/qml/studio/src/include/studio_qml_export.h deleted file mode 100644 index 767079013..000000000 --- a/src/ui/qml/studio/src/include/studio_qml_export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef STUDIO_QML_EXPORT_H -#define STUDIO_QML_EXPORT_H - -#ifdef STUDIO_QML_STATIC_DEFINE -# define STUDIO_QML_EXPORT -# define STUDIO_QML_NO_EXPORT -#else -# ifndef STUDIO_QML_EXPORT -# ifdef studio_qml_EXPORTS - /* We are building this library */ -# define STUDIO_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define STUDIO_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef STUDIO_QML_NO_EXPORT -# define STUDIO_QML_NO_EXPORT -# endif -#endif - -#ifndef STUDIO_QML_DEPRECATED -# define STUDIO_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef STUDIO_QML_DEPRECATED_EXPORT -# define STUDIO_QML_DEPRECATED_EXPORT STUDIO_QML_EXPORT STUDIO_QML_DEPRECATED -#endif - -#ifndef STUDIO_QML_DEPRECATED_NO_EXPORT -# define STUDIO_QML_DEPRECATED_NO_EXPORT STUDIO_QML_NO_EXPORT STUDIO_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef STUDIO_QML_NO_DEPRECATED -# define STUDIO_QML_NO_DEPRECATED -# endif -#endif - -#endif /* STUDIO_QML_EXPORT_H */ diff --git a/src/ui/qml/studio/src/qml_setup.cpp b/src/ui/qml/studio/src/qml_setup.cpp new file mode 100644 index 000000000..5cf296d4d --- /dev/null +++ b/src/ui/qml/studio/src/qml_setup.cpp @@ -0,0 +1,135 @@ +#include "xstudio/ui/mouse.hpp" +#include "xstudio/ui/qml/bookmark_model_ui.hpp" //NOLINT +#include "xstudio/ui/qml/conform_ui.hpp" //NOLINT +#include "xstudio/ui/qml/embedded_python_ui.hpp" //NOLINT +#include "xstudio/ui/qml/QTreeModelToTableModel.hpp" //NOLINT +#include "xstudio/ui/qml/global_store_model_ui.hpp" //NOLINT +#include "xstudio/ui/qml/helper_ui.hpp" //NOLINT +#include "xstudio/ui/qml/hotkey_ui.hpp" //NOLINT +#include "xstudio/ui/qml/log_ui.hpp" //NOLINT +#include "xstudio/ui/qml/model_data_ui.hpp" //NOLINT +#include "xstudio/ui/qml/module_data_ui.hpp" //NOLINT +#include "xstudio/ui/qml/qml_viewport.hpp" //NOLINT +#include "xstudio/ui/qml/session_model_ui.hpp" //NOLINT +#include "xstudio/ui/qml/shotgun_provider_ui.hpp" +#include "xstudio/ui/qml/studio_ui.hpp" //NOLINT +#include "xstudio/ui/qml/thumbnail_provider_ui.hpp" + +#include +#include + +void xstudio::ui::qml::setup_xstudio_qml_emgine(QQmlEngine *engine, caf::actor_system &system) { + + const QFont fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); + engine->rootContext()->setContextProperty("systemFixedWidthFontFamily", fixedFont.family()); + + qmlRegisterType("xstudio.qml.semver", 1, 0, "SemVer"); + qmlRegisterType("xstudio.qml.viewport", 1, 0, "XsHotkey"); + qmlRegisterType("xstudio.qml.viewport", 1, 0, "XsHotkeysInfo"); + qmlRegisterType("xstudio.qml.viewport", 1, 0, "XsHotkeyReference"); + + qmlRegisterType("xstudio.qml.helpers", 1, 0, "XsHotkeyArea"); + + qmlRegisterType("xstudio.qml.viewport", 1, 0, "Viewport"); + + qmlRegisterType( + "xstudio.qml.bookmarks", 1, 0, "XsBookmarkCategories"); + qmlRegisterType("xstudio.qml.bookmarks", 1, 0, "XsBookmarkModel"); + qmlRegisterType( + "xstudio.qml.bookmarks", 1, 0, "XsBookmarkFilterModel"); + + qmlRegisterType("xstudio.qml.embedded_python", 1, 0, "EmbeddedPython"); + + qmlRegisterType("xstudio.qml.uuid", 1, 0, "QMLUuid"); + qmlRegisterType("xstudio.qml.clipboard", 1, 0, "Clipboard"); + qmlRegisterType("xstudio.qml.helpers", 1, 0, "XsImagePainter"); + qmlRegisterType("xstudio.qml.helpers", 1, 0, "XsMarkerModel"); + qmlRegisterType("xstudio.qml.helpers", 1, 0, "XsPropertyFollower"); + + qmlRegisterType( + "xstudio.qml.global_store_model", 1, 0, "XsGlobalStoreModel"); + qmlRegisterType( + "xstudio.qml.global_store_model", 1, 0, "XsPreferencesModel"); + qmlRegisterType("xstudio.qml.helpers", 1, 0, "XsModelProperty"); + qmlRegisterType("xstudio.qml.helpers", 1, 0, "XsFilterModel"); + qmlRegisterType("xstudio.qml.helpers", 1, 0, "XsTimeCode"); + qmlRegisterType("xstudio.qml.helpers", 1, 0, "XsModelRowCount"); + qmlRegisterType("xstudio.qml.helpers", 1, 0, "XsModelPropertyMap"); + qmlRegisterType("xstudio.qml.helpers", 1, 0, "XsPreferenceMap"); + qmlRegisterType( + "xstudio.qml.helpers", 1, 0, "XsModelNestedPropertyMap"); + qmlRegisterType("xstudio.qml.helpers", 1, 0, "XsModelPropertyTree"); + qmlRegisterType( + "xstudio.qml.helpers", 1, 0, "QTreeModelToTableModel"); + + + qmlRegisterType("xstudio.qml.conform", 1, 0, "XsConformEngine"); + + qmlRegisterType("xstudio.qml.session", 1, 0, "XsSessionModel"); + + qmlRegisterType("xstudio.qml.models", 1, 0, "XsMenusModel"); + qmlRegisterType("xstudio.qml.models", 1, 0, "XsModuleData"); + qmlRegisterType("xstudio.qml.models", 1, 0, "XsPanelsLayoutModel"); + qmlRegisterType( + "xstudio.qml.models", 1, 0, "XsMediaListColumnsModel"); + qmlRegisterType("xstudio.qml.models", 1, 0, "XsMediaListFilterModel"); + + qmlRegisterType("xstudio.qml.models", 1, 0, "XsViewsModel"); + qmlRegisterType("xstudio.qml.models", 1, 0, "XsPopoutWindowsData"); + qmlRegisterType("xstudio.qml.models", 1, 0, "XsSingletonItemsModel"); + + qmlRegisterType("xstudio.qml.models", 1, 0, "XsMenuModelItem"); + qmlRegisterType("xstudio.qml.models", 1, 0, "XsPanelMenuModelFilter"); + + qRegisterMetaType("QQmlPropertyMap*"); + + // QuickFuture::registerType>(); + + // Add a CafSystemObject to the application - this is QObject that simply + // holds a reference to the actor system so that we can access the system + // in Qt main loop + new CafSystemObject(qApp, system); + + const QUrl url(QStringLiteral("qrc:/main.qml")); + + ; + engine->addImageProvider(QLatin1String("thumbnail"), new ThumbnailProvider); + engine->addImageProvider(QLatin1String("shotgun"), new ShotgunProvider); + engine->rootContext()->setContextProperty( + "applicationDirPath", QGuiApplication::applicationDirPath()); + + auto helper = new Helpers(engine); + engine->rootContext()->setContextProperty("helpers", helper); + + auto studio = new StudioUI(system, qApp); + engine->rootContext()->setContextProperty("studio", studio); + + auto logger = new LogModel(engine); + auto proxylogger = new LogFilterModel(engine); + proxylogger->setSourceModel(logger); + + engine->rootContext()->setContextProperty("CurrentDirPath", QString(QDir::currentPath())); + engine->rootContext()->setContextProperty("logger", proxylogger); + // connect logger. + auto logsink = std::make_shared(logger); + spdlog::get("xstudio")->sinks().push_back(logsink); + + engine->addImportPath("qrc:///"); + engine->addImportPath("qrc:///extern"); + + // gui plugins.. + engine->addImportPath(QStringFromStd(xstudio_plugin_dir("/qml"))); + engine->addPluginPath(QStringFromStd(xstudio_plugin_dir(""))); + engine->addPluginPath(QStringFromStd(xstudio_plugin_dir("/qml"))); + + // env var XSTUDIO_PLUGIN_PATH is search path for plugins. Add + // subfolders named qml for qt to look for .qml files installed + // with plugins + char *plugin_path = std::getenv("XSTUDIO_PLUGIN_PATH"); + if (plugin_path) { + for (const auto &p : xstudio::utility::split(plugin_path, ':')) { + engine->addPluginPath(QStringFromStd(p + "/qml")); + engine->addImportPath(QStringFromStd(p + "/qml")); + } + } +} \ No newline at end of file diff --git a/src/ui/qml/studio/src/studio_ui.cpp b/src/ui/qml/studio/src/studio_ui.cpp index dded1291f..a92bc5eff 100644 --- a/src/ui/qml/studio/src/studio_ui.cpp +++ b/src/ui/qml/studio/src/studio_ui.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include "xstudio/atoms.hpp" #include "xstudio/plugin_manager/plugin_manager.hpp" @@ -17,6 +18,10 @@ using namespace xstudio::ui::qml; StudioUI::StudioUI(caf::actor_system &system, QObject *parent) : QMLActor(parent) { init(system); + + // create the offscreen viewport used for snapshot and note thumbnail + // generation + offscreen_snapshot_viewport(); } StudioUI::~StudioUI() { @@ -32,6 +37,9 @@ StudioUI::~StudioUI() { } system().registry().erase(studio_ui_registry); snapshot_offscreen_viewport_->stop(); + system().registry().erase( + offscreen_viewport_registry); + delete snapshot_offscreen_viewport_; } void StudioUI::init(actor_system &system_) { @@ -93,13 +101,18 @@ void StudioUI::init(actor_system &system_) { [=](utility::event_atom, ui::open_quickview_window_atom, const utility::UuidActorVector &media_items, - const std::string &compare_mode) { + const std::string &compare_mode, + const utility::JsonStore in_point, + const utility::JsonStore out_point) { QStringList media_actors_as_strings; for (const auto &media : media_items) { media_actors_as_strings.push_back( QStringFromStd(actorToString(system(), media.actor()))); } - emit openQuickViewers(media_actors_as_strings, QStringFromStd(compare_mode)); + const int in = in_point.is_number() ? in_point.get() : -1; + const int out = out_point.is_number() ? out_point.get() : -1; + emit openQuickViewers( + media_actors_as_strings, QStringFromStd(compare_mode), in, out); }, [=](utility::event_atom, @@ -129,35 +142,13 @@ void StudioUI::init(actor_system &system_) { // actor based off a QObject - if so it can't do request/receive message // handling with this actor which also lives in the Qt UI thread. offscreen_viewports_.push_back(new xstudio::ui::qt::OffscreenViewport(name)); - anon_send( - requester, - ui::offscreen_viewport_atom_v, - offscreen_viewports_.back()->as_actor()); - }, - [=](std::string) { - loadVideoOutputPlugins(); + anon_mail( + ui::offscreen_viewport_atom_v, offscreen_viewports_.back()->as_actor()) + .send(requester); } }; }); - - // here we tell the studio that we're up and running so it can send us - // any pending 'quickview' requests - auto studio = system().registry().template get(studio_registry); - if (studio) { - anon_send(studio, ui::open_quickview_window_atom_v, as_actor()); - } - - // create the offscreen viewport used for rendering snapshots - snapshot_offscreen_viewport_ = new xstudio::ui::qt::OffscreenViewport("snapshot_viewport"); - system().registry().template put( - offscreen_viewport_registry, snapshot_offscreen_viewport_->as_actor()); - - // we need to delay loading video output plugins by a couple of seconds - // to make sure the UI is up and running before we create offscreen viewports - // etc. that the video output plugin probably wants - delayed_anon_send( - as_actor(), std::chrono::seconds(5), std::string("load video output plugins")); } void StudioUI::setSessionActorAddr(const QString &addr) { @@ -183,24 +174,24 @@ bool StudioUI::clearImageCache() { QUrl StudioUI::userDocsUrl() const { - std::string docs_index = utility::xstudio_root("/docs/user_docs/index.html"); - return QUrl(QString(tr("file://")) + QString(docs_index.c_str())); + std::string docs_index = utility::xstudio_resources_dir("docs/index.html"); + if (docs_index.find("/") == 0) + docs_index.erase(docs_index.begin()); + return QUrl(QString(tr("file:///")) + QStringFromStd(docs_index)); } QUrl StudioUI::apiDocsUrl() const { - std::string docs_index = utility::xstudio_root("/docs/index.html"); - return QUrl(QString(tr("file://")) + QString(docs_index.c_str())); + std::string docs_index = utility::xstudio_resources_dir("docs/api/index.html"); + if (docs_index.find("/") == 0) + docs_index.erase(docs_index.begin()); + return QUrl(QString(tr("file:///")) + QStringFromStd(docs_index)); } QUrl StudioUI::releaseDocsUrl() const { - std::string docs_index = utility::xstudio_root("/docs/user_docs/release_notes/index.html"); - return QUrl(QString(tr("file://")) + QString(docs_index.c_str())); -} - -QUrl StudioUI::hotKeyDocsUrl() const { - std::string docs_index = - utility::xstudio_root("/docs/user_docs/getting_started/hotkeys.html"); - return QUrl(QString(tr("file://")) + QString(docs_index.c_str())); + std::string docs_index = utility::xstudio_resources_dir("docs/user_docs/release_notes/index.html"); + if (docs_index.find("/") == 0) + docs_index.erase(docs_index.begin()); + return QUrl(QString(tr("file:///")) + QStringFromStd(docs_index)); } void StudioUI::newSession(const QString &name) { @@ -208,7 +199,7 @@ void StudioUI::newSession(const QString &name) { auto session = sys->spawn(StdFromQString(name)); auto global = system().registry().template get(global_registry); - sys->anon_send(global, session::session_atom_v, session); + anon_mail(session::session_atom_v, session).send(global); setSessionActorAddr(actorToQString(system(), session)); emit newSessionCreated(session_actor_addr_); @@ -223,17 +214,16 @@ QFuture StudioUI::loadSessionFuture(const QUrl &path, const QVariant &json JsonStore js; if (json.isNull()) { - js = utility::open_session(StdFromQString(path.path())); + js = utility::open_session(UriFromQUrl(path)); } else { js = qvariant_to_json(json); } auto session = sys->spawn(js, UriFromQUrl(path)); auto global = system().registry().template get(global_registry); - sys->anon_send(global, session::session_atom_v, session); + anon_mail(session::session_atom_v, session).send(global); setSessionActorAddr(actorToQString(system(), session)); emit sessionLoaded(session_actor_addr_); - result = true; } catch (const std::exception &err) { @@ -270,7 +260,7 @@ QFuture StudioUI::loadSessionRequestFuture(const QUrl &path) { } catch (...) { // empty.. auto session = sys->spawn(js, UriFromQUrl(path)); - sys->anon_send(global, session::session_atom_v, session); + anon_mail(session::session_atom_v, session).send(global); setSessionActorAddr(actorToQString(system(), session)); emit sessionLoaded(session_actor_addr_); } @@ -354,18 +344,108 @@ void StudioUI::loadVideoOutputPlugins() { plugin_manager::PluginType(plugin_manager::PluginFlags::PF_VIDEO_OUTPUT)); for (const auto &i : details) { - try { - auto video_output_plugin = request_receive( - *sys, pm, plugin_manager::spawn_plugin_atom_v, i.uuid_); - video_output_plugins_.push_back(video_output_plugin); + auto video_output_plugin = request_receive( + *sys, pm, plugin_manager::spawn_plugin_atom_v, i.uuid_); + + self()->monitor( + video_output_plugin, + [this, addr = video_output_plugin.address()](const error &) { + for (auto p = video_output_plugins_.begin(); + p != video_output_plugins_.end(); + ++p) { + if (*p == addr) { + video_output_plugins_.erase(p); + break; + } + } + }); - } catch (const std::exception &err) { - spdlog::info("{}", err.what()); - } + video_output_plugins_.push_back(video_output_plugin); } } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } + + // here we tell the studio that we're up and running so it can send us + // any pending 'quickview' requests. This is only needed if the app itself is + // launched with a quickview flag, which isn't normal usage but we action + // them anyway + auto studio = system().registry().template get(studio_registry); + if (studio) { + // we delay our send because xSTUDIO is still starting up at this point + // The UI isn't fully plugged into the window manager, it seems, as + // without a delay we're finding sometimes on Linux the quickview window + // appears with no titlebar which is a serious bug as it can't be closed + // or hidden. + anon_mail(ui::open_quickview_window_atom_v, as_actor()) + .delay(std::chrono::seconds(1)) + .send(studio); + } +} + +xstudio::ui::qt::OffscreenViewport *StudioUI::offscreen_snapshot_viewport() { + // create an offscreen viewport and send its companion actor to the actor that requested it + if (!snapshot_offscreen_viewport_) { + snapshot_offscreen_viewport_ = new xstudio::ui::qt::OffscreenViewport( + "snapshot_viewport", + false // this flag means we don't have QML overlays in the snapshot viewport + ); + system().registry().put( + offscreen_viewport_registry, snapshot_offscreen_viewport_->as_actor()); + } + return snapshot_offscreen_viewport_; +} + +QString StudioUI::renderScreenShotToDisk( + const QUrl &path, const int compression, const int width, const int height) { + + try { + + scoped_actor sys{system()}; + + request_receive( + *sys, + offscreen_snapshot_viewport()->as_actor(), + viewport::render_viewport_to_image_atom_v, + UriFromQUrl(path), + width, + height); + + } catch (std::exception &e) { + return QString(e.what()); + } + return QString(); +} + +QString StudioUI::renderScreenShotToClipboard(const int width, const int height) { + + try { + + scoped_actor sys{system()}; + + request_receive( + *sys, + offscreen_snapshot_viewport()->as_actor(), + viewport::render_viewport_to_image_atom_v, + width, + height); + + } catch (std::exception &e) { + return QString(e.what()); + } + return QString(); +} + +void StudioUI::setupSnapshotViewport(const QString &playhead_addr) { + + try { + + scoped_actor sys{system()}; + offscreen_snapshot_viewport()->setPlayhead(playhead_addr); + + } catch (std::exception &e) { + spdlog::warn("{} {} ", __PRETTY_FUNCTION__, e.what()); + } } diff --git a/src/ui/qml/tag/src/CMakeLists.txt b/src/ui/qml/tag/src/CMakeLists.txt deleted file mode 100644 index 4e8093aa6..000000000 --- a/src/ui/qml/tag/src/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -SET(LINK_DEPS - ${CAF_LIBRARY_core} - Qt5::Core - xstudio::ui::qml::helper - xstudio::global_store -) - -SET(EXTRAMOC -) - -create_qml_component(tag 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/tag/src/export.h b/src/ui/qml/tag/src/export.h deleted file mode 100644 index b662f8cb2..000000000 --- a/src/ui/qml/tag/src/export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef TAG_QML_EXPORT_H -#define TAG_QML_EXPORT_H - -#ifdef TAG_QML_STATIC_DEFINE -# define TAG_QML_EXPORT -# define TAG_QML_NO_EXPORT -#else -# ifndef TAG_QML_EXPORT -# ifdef tag_qml_EXPORTS - /* We are building this library */ -# define TAG_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define TAG_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef TAG_QML_NO_EXPORT -# define TAG_QML_NO_EXPORT -# endif -#endif - -#ifndef TAG_QML_DEPRECATED -# define TAG_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef TAG_QML_DEPRECATED_EXPORT -# define TAG_QML_DEPRECATED_EXPORT TAG_QML_EXPORT TAG_QML_DEPRECATED -#endif - -#ifndef TAG_QML_DEPRECATED_NO_EXPORT -# define TAG_QML_DEPRECATED_NO_EXPORT TAG_QML_NO_EXPORT TAG_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef TAG_QML_NO_DEPRECATED -# define TAG_QML_NO_DEPRECATED -# endif -#endif - -#endif /* TAG_QML_EXPORT_H */ diff --git a/src/ui/qml/tag/src/include/tag_qml_export.h b/src/ui/qml/tag/src/include/tag_qml_export.h deleted file mode 100644 index b662f8cb2..000000000 --- a/src/ui/qml/tag/src/include/tag_qml_export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef TAG_QML_EXPORT_H -#define TAG_QML_EXPORT_H - -#ifdef TAG_QML_STATIC_DEFINE -# define TAG_QML_EXPORT -# define TAG_QML_NO_EXPORT -#else -# ifndef TAG_QML_EXPORT -# ifdef tag_qml_EXPORTS - /* We are building this library */ -# define TAG_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define TAG_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef TAG_QML_NO_EXPORT -# define TAG_QML_NO_EXPORT -# endif -#endif - -#ifndef TAG_QML_DEPRECATED -# define TAG_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef TAG_QML_DEPRECATED_EXPORT -# define TAG_QML_DEPRECATED_EXPORT TAG_QML_EXPORT TAG_QML_DEPRECATED -#endif - -#ifndef TAG_QML_DEPRECATED_NO_EXPORT -# define TAG_QML_DEPRECATED_NO_EXPORT TAG_QML_NO_EXPORT TAG_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef TAG_QML_NO_DEPRECATED -# define TAG_QML_NO_DEPRECATED -# endif -#endif - -#endif /* TAG_QML_EXPORT_H */ diff --git a/src/ui/qml/tag/src/tag_ui.cpp b/src/ui/qml/tag/src/tag_ui.cpp deleted file mode 100644 index 24aebf4a2..000000000 --- a/src/ui/qml/tag/src/tag_ui.cpp +++ /dev/null @@ -1,159 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include - -#include "xstudio/atoms.hpp" -#include "xstudio/ui/qml/tag_ui.hpp" -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" -#include "xstudio/utility/container.hpp" - -#include -#include - -using namespace caf; -using namespace xstudio; -using namespace xstudio::utility; -using namespace xstudio::tag; -using namespace xstudio::ui::qml; - -TagUI::TagUI(const tag::Tag tag, QObject *parent) : QObject(parent), tag_(std::move(tag)) {} - -TagAttrs::TagAttrs(QObject *parent) : QQmlPropertyMap(this, parent) {} - -void TagAttrs::reset() { - for (const auto &i : keys()) - this->clear(i); -} - - -void TagListUI::addTag(TagUI *tag) { - tags_.append(tag); - emit tagsChanged(); -} - -bool TagListUI::removeTag(const QUuid &id) { - for (auto i = 0; i < tags_.size(); i++) { - if (qobject_cast(tags_[i])->id() == id) { - // spdlog::warn("TAG REMOVED {}", to_string(UuidFromQUuid(id))); - tags_.removeAt(i); - emit tagsChanged(); - return true; - } - } - return false; -} - - -TagManagerUI::TagManagerUI(QObject *parent) : QMLActor(parent), attrs_map_(new TagAttrs(this)) { - - init(CafSystemObject::get_actor_system()); -} - -void TagManagerUI::set_backend(caf::actor backend) { - backend_ = backend; - attrs_map_->reset(); - - scoped_actor sys{system()}; - - if (backend_events_) { - try { - request_receive( - *sys, backend_events_, broadcast::leave_broadcast_atom_v, as_actor()); - } catch ([[maybe_unused]] const std::exception &e) { - // spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - backend_events_ = caf::actor(); - } - - - if (backend_) { - try { - backend_events_ = - request_receive(*sys, backend_, get_event_group_atom_v); - request_receive( - *sys, backend_events_, broadcast::join_broadcast_atom_v, as_actor()); - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - // get current tags. - try { - auto tags = request_receive>(*sys, backend_, get_tags_atom_v); - - for (const auto &i : tags) - add_tag(i); - - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - } -} - -void TagManagerUI::add_tag(const tag::Tag &tag) { - // does link exist.. - - // spdlog::warn("add_tag {} {} {} {}", tag.unique(), tag.type(), to_string(tag.link()), - // to_string(tag.id())); - - auto uuid = QUuidFromUuid(tag.link()).toString(); - if (not attrs_map_->contains(uuid) or attrs_map_->value(uuid).isNull()) { - attrs_map_->insert(uuid, QVariant::fromValue(new QQmlPropertyMap(this))); - } - - auto type_map = - qobject_cast(qvariant_cast(attrs_map_->value(uuid))); - - // check for type.. - auto type = QStringFromStd(tag.type()); - if (not type_map->contains(type) or type_map->value(type).isNull()) { - type_map->insert(type, QVariant::fromValue(new TagListUI(this))); - } - - auto tags = qobject_cast(qvariant_cast(type_map->value(type))); - tags->addTag(new TagUI(tag, this)); -} - -void TagManagerUI::remove_tag(const utility::Uuid &id) { - // trickier.. need to find it.. - // spdlog::warn("remove_tag {}", to_string(id)); - - auto qid = QUuidFromUuid(id); - - for (const auto &i : attrs_map_->keys()) { - auto attrs = - qobject_cast(qvariant_cast(attrs_map_->value(i))); - if (attrs) { - for (const auto &j : attrs->keys()) { - auto tags = - qobject_cast(qvariant_cast(attrs->value(j))); - if (tags && tags->removeTag(qid)) - return; - } - } - } -} - -void TagManagerUI::init(caf::actor_system &system) { - - QMLActor::init(system); - - spdlog::debug("TagManagerUI init"); - - self()->set_default_handler(caf::drop); - - set_message_handler([=](caf::actor_companion * /*self*/) -> caf::message_handler { - return { - [=](utility::event_atom, tag::remove_tag_atom, const utility::Uuid &id) { - remove_tag(id); - }, - [=](utility::event_atom, tag::add_tag_atom, const Tag &tag) { add_tag(tag); }, - [=](utility::event_atom, utility::last_changed_atom, const time_point &) {}, - [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - - [=](const group_down_msg &) { - // caf::aout(self()) << "TagManagerUI down: " << to_string(g.source) << - // std::endl; - }}; - }); -} diff --git a/src/ui/qml/tag/test/CMakeLists.txt b/src/ui/qml/tag/test/CMakeLists.txt deleted file mode 100644 index ca5580c50..000000000 --- a/src/ui/qml/tag/test/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -include(CTest) diff --git a/src/ui/qml/viewport/src/CMakeLists.txt b/src/ui/qml/viewport/src/CMakeLists.txt index 51400dff8..77d2761c4 100644 --- a/src/ui/qml/viewport/src/CMakeLists.txt +++ b/src/ui/qml/viewport/src/CMakeLists.txt @@ -1,16 +1,14 @@ SET(LINK_DEPS - ${CAF_LIBRARY_core} + CAF::core Imath::Imath - Qt5::Core - Qt5::Qml - Qt5::Quick - Qt5::Widgets + Qt6::Core + Qt6::Qml + Qt6::Quick xstudio::module xstudio::playhead xstudio::ui::opengl::viewport xstudio::ui::viewport xstudio::ui::qml::helper - xstudio::ui::qml::playhead xstudio::utility ) @@ -22,4 +20,4 @@ SET(EXTRAMOC find_package(Imath) -create_qml_component(viewport 0.1.0 "${LINK_DEPS}" "${EXTRAMOC}") +create_qml_component(viewport ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${EXTRAMOC}") diff --git a/src/ui/qml/viewport/src/export.h b/src/ui/qml/viewport/src/export.h deleted file mode 100644 index 6c16367e5..000000000 --- a/src/ui/qml/viewport/src/export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef VIEWPORT_QML_EXPORT_H -#define VIEWPORT_QML_EXPORT_H - -#ifdef VIEWPORT_QML_STATIC_DEFINE -# define VIEWPORT_QML_EXPORT -# define VIEWPORT_QML_NO_EXPORT -#else -# ifndef VIEWPORT_QML_EXPORT -# ifdef viewport_qml_EXPORTS - /* We are building this library */ -# define VIEWPORT_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define VIEWPORT_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef VIEWPORT_QML_NO_EXPORT -# define VIEWPORT_QML_NO_EXPORT -# endif -#endif - -#ifndef VIEWPORT_QML_DEPRECATED -# define VIEWPORT_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef VIEWPORT_QML_DEPRECATED_EXPORT -# define VIEWPORT_QML_DEPRECATED_EXPORT VIEWPORT_QML_EXPORT VIEWPORT_QML_DEPRECATED -#endif - -#ifndef VIEWPORT_QML_DEPRECATED_NO_EXPORT -# define VIEWPORT_QML_DEPRECATED_NO_EXPORT VIEWPORT_QML_NO_EXPORT VIEWPORT_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef VIEWPORT_QML_NO_DEPRECATED -# define VIEWPORT_QML_NO_DEPRECATED -# endif -#endif - -#endif /* VIEWPORT_QML_EXPORT_H */ diff --git a/src/ui/qml/viewport/src/hotkey_ui.cpp b/src/ui/qml/viewport/src/hotkey_ui.cpp index 14ae4ed74..152848b80 100644 --- a/src/ui/qml/viewport/src/hotkey_ui.cpp +++ b/src/ui/qml/viewport/src/hotkey_ui.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include "xstudio/atoms.hpp" #include "xstudio/ui/qml/hotkey_ui.hpp" @@ -11,19 +12,6 @@ using namespace xstudio::utility; using namespace xstudio::ui::qml; using namespace xstudio::ui; -QMap hotkey_to_qvariant_map(const Hotkey &hotkey) { - - QMap hk_data; - hk_data[Qt::UserRole + 1] = QVariant(QStringFromStd(hotkey.key())); - hk_data[Qt::UserRole + 2] = QVariant(hotkey.modifiers()); - hk_data[Qt::UserRole + 3] = QStringFromStd(hotkey.hotkey_name()); - hk_data[Qt::UserRole + 4] = QStringFromStd(hotkey.hotkey_origin()); - hk_data[Qt::UserRole + 5] = QStringFromStd(hotkey.hotkey_description()); - hk_data[Qt::UserRole + 6] = QStringFromStd(hotkey.hotkey_sequence()); - - return hk_data; -} - HotkeysUI::HotkeysUI(QObject *parent) : super(parent) { init(CafSystemObject::get_actor_system()); @@ -45,15 +33,7 @@ HotkeysUI::HotkeysUI(QObject *parent) : super(parent) { auto hotkeys = request_receive>( *sys, keyboard_manager, keypress_monitor::register_hotkey_atom_v); - int row = 0; - for (const auto &hk : hotkeys) { - hotkeys_data_.push_back(hotkey_to_qvariant_map(hk)); - const QModelIndex index = createIndex(row, 0); - for (int r = 1; r <= 6; ++r) { - emit dataChanged(index, index, {Qt::UserRole + r}); - } - row++; - } + update_hotkeys_model_data(hotkeys); } catch (const std::exception &err) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); @@ -65,50 +45,66 @@ HotkeysUI::HotkeysUI(QObject *parent) : super(parent) { [=](keypress_monitor::hotkey_event_atom, const std::vector &hotkeys) { update_hotkeys_model_data(hotkeys); - } - - }; + }, + [=](keypress_monitor::hotkey_event_atom, Hotkey & /*hotkey*/) { + // update_hotkeys_model_data(hotkeys); + }, + [=](keypress_monitor::hotkey_event_atom, + const utility::Uuid kotkey_uuid, + const bool pressed, + const std::string &context, + const std::string &window) { + // actual hotkey pressed or release ... we ignore + }}; }); } void HotkeysUI::update_hotkeys_model_data(const std::vector &new_hotkeys_data) { - size_t row = 0; - for (const auto &hk : new_hotkeys_data) { - auto var_data = hotkey_to_qvariant_map(hk); - if (hotkeys_data_.size() > row) { - if (var_data != hotkeys_data_[row]) { - hotkeys_data_[row] = var_data; - const QModelIndex index = createIndex(row, 0); - for (int r = 1; r <= 6; ++r) - emit dataChanged(index, index, {Qt::UserRole + r}); - } - } else { - beginInsertRows( - QModelIndex(), - hotkeys_data_.size(), - static_cast(new_hotkeys_data.size()) - 1); - hotkeys_data_.push_back(var_data); - endInsertRows(); - } - row++; + hotkeys_data_ = new_hotkeys_data; + + // sort by the name of the hotkey + std::sort( + hotkeys_data_.begin(), + hotkeys_data_.end(), + [](const Hotkey &a, const Hotkey &b) -> bool { + return a.hotkey_name() < b.hotkey_name(); + }); + + beginResetModel(); + endResetModel(); + emit rowCountChanged(); + checkCategories(); +} + +void HotkeysUI::checkCategories() { + QSet cats; + for (const auto &hk : hotkeys_data_) { + cats.insert(QStringFromStd(hk.hotkey_origin())); + } + if (cats.values() != categories_) { + categories_ = cats.values(); + emit categoriesChanged(); } } QHash HotkeysUI::roleNames() const { QHash roles; - roles[Qt::UserRole + 1] = "keyboard_key"; - roles[Qt::UserRole + 2] = "modifiers"; - roles[Qt::UserRole + 3] = "hotkey_name"; - roles[Qt::UserRole + 4] = "component"; - roles[Qt::UserRole + 5] = "hotkey_description"; - roles[Qt::UserRole + 6] = "sequence"; - roles[Qt::UserRole + 7] = "error_message"; + for (auto i = hotkeyRoleNames.keyValueBegin(); i != hotkeyRoleNames.keyValueEnd(); ++i) { + roles[i->first] = i->second; + } return roles; } int HotkeysUI::rowCount(const QModelIndex &) const { - return static_cast(hotkeys_data_.size()); + int ct = 0; + const std::string curr_cat(StdFromQString(current_category_)); + for (const auto &hk : hotkeys_data_) { + if (hk.hotkey_origin() == curr_cat) { + ct++; + } + } + return ct; } QVariant HotkeysUI::data(const QModelIndex &index, int role) const { @@ -116,9 +112,28 @@ QVariant HotkeysUI::data(const QModelIndex &index, int role) const { QVariant rt; try { - if ((int)hotkeys_data_.size() > index.row() && - hotkeys_data_[index.row()].contains(role)) { - rt = hotkeys_data_[index.row()][role]; + int ct = 0; + const std::string curr_cat(StdFromQString(current_category_)); + for (const auto &hk : hotkeys_data_) { + if (hk.hotkey_origin() == curr_cat) { + if (ct == index.row()) { + + if (role == keyboardKey) + rt = QStringFromStd(hk.key()); + if (role == keyModifiers) + rt = hk.modifiers(); + if (role == hotkeyName) + rt = QStringFromStd(hk.hotkey_name()); + if (role == hotkeyCategory) + rt = QStringFromStd(hk.hotkey_origin()); + if (role == hotkeyDescription) + rt = QStringFromStd(hk.hotkey_description()); + if (role == hotkeySequence) + rt = QStringFromStd(hk.hotkey_sequence()); + break; + } + ct++; + } } } catch (const std::exception &) { } @@ -138,6 +153,34 @@ bool HotkeysUI::setData(const QModelIndex &index, const QVariant &value, int rol return false; } +QString HotkeysUI::hotkey_sequence(const QVariant &hotkey_uuid) { + QString result; + utility::Uuid hk_uuid; + if (hotkey_uuid.canConvert()) { + hk_uuid = UuidFromQUuid(hotkey_uuid.value()); + } else { + hk_uuid.from_string(StdFromQString(hotkey_uuid.toString())); + } + for (const auto &hk : hotkeys_data_) { + if (hk.uuid() == hk_uuid) { + result = QStringFromStd(hk.hotkey_sequence()); + break; + } + } + return result; +} + +QString HotkeysUI::hotkey_sequence_from_hotkey_name(const QString &hotkey_name) { + QString result; + const std::string nm(StdFromQString(hotkey_name)); + for (const auto &hk : hotkeys_data_) { + if (hk.hotkey_name() == nm) { + result = QStringFromStd(hk.hotkey_sequence()); + break; + } + } + return result; +} HotkeyUI::HotkeyUI(QObject *parent) : QMLActor(parent) { init(CafSystemObject::get_actor_system()); @@ -156,9 +199,21 @@ void HotkeyUI::init(actor_system &system_) { [=](keypress_monitor::hotkey_event_atom, const utility::Uuid &uuid, const bool hotkey_pressed, - const std::string &context) { - if (hotkey_uuid_ == uuid && hotkey_pressed) { - emit activated(); + const std::string &context, + const std::string &window) { + // if the hotkey was pressed in a different parent window to + // the parent of this XsHotkey item, we don't activate + if (hotkey_pressed && StdFromQString(window_name_) != window) + return; + + if (hotkey_uuid_ == QUuidFromUuid(uuid)) { + if (context_.isNull() || context_ == QString("any") || + StdFromQString(context_) == context) { + if (hotkey_pressed) + emit activated(); + else + emit released(); + } } } @@ -173,9 +228,11 @@ void HotkeyUI::init(actor_system &system_) { emit componentNameChanged(); // for default componentName ('xStudio') emit autoRepeatChanged(); // for default componentName ('xStudio') + emit uuidChanged(); } void HotkeyUI::registerHotkey() { + if (sequence_.isNull() || name_.isNull() || component_name_.isNull() || context_.isNull()) { // not ready, some properties not set (yet) return; @@ -203,19 +260,24 @@ void HotkeyUI::registerHotkey() { auto keypress_event_manager = self()->home_system().registry().template get(keyboard_events); + window_name_ = item_window_name(parent()); + Hotkey hk( key, mod, StdFromQString(name_), StdFromQString(component_name_), StdFromQString(description_), - StdFromQString(context_), + StdFromQString(window_name_), autorepeat_, caf::actor_cast(as_actor())); - anon_send(keypress_event_manager, ui::keypress_monitor::register_hotkey_atom_v, hk); + hotkey_uuid_ = QUuidFromUuid(hk.uuid()); + + emit uuidChanged(); - hotkey_uuid_ = hk.uuid(); + anon_mail(ui::keypress_monitor::register_hotkey_atom_v, hk) + .send(keypress_event_manager); } else { } @@ -225,52 +287,146 @@ HotkeyReferenceUI::HotkeyReferenceUI(QObject *parent) : QMLActor(parent) { init(CafSystemObject::get_actor_system()); } +HotkeyReferenceUI::~HotkeyReferenceUI() { + + if (exclusive_) { + exclusive_ = false; + notifyExclusiveChanged(); + } +} + void HotkeyReferenceUI::init(caf::actor_system &system_) { QMLActor::init(system_); scoped_actor sys{system()}; + try { + + auto keyboard_manager = system().registry().template get(keyboard_events); + + auto hotkeys_config_events_group = utility::request_receive( + *sys, + keyboard_manager, + utility::get_event_group_atom_v, + keypress_monitor::hotkey_event_atom_v); + + anon_mail(broadcast::join_broadcast_atom_v, as_actor()) + .send(hotkeys_config_events_group); + + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + set_message_handler([=](actor_companion * /*self_*/) -> message_handler { return { - - }; + [=](keypress_monitor::hotkey_event_atom, const std::vector &hotkeys) { + // hotkeys have been updated + for (const auto &hk : hotkeys) { + if (hk.hotkey_name() == StdFromQString(hotkey_name_)) { + if (QStringFromStd(hk.hotkey_sequence()) != sequence_) { + sequence_ = QStringFromStd(hk.hotkey_sequence()); + Q_EMIT sequenceChanged(); + } + QUuid uuid = QUuidFromUuid(hk.uuid()); + if (uuid != hotkey_uuid_) { + hotkey_uuid_ = uuid; + Q_EMIT uuidChanged(); + if (exclusive_) + notifyExclusiveChanged(); + } + } + } + }, + [=](keypress_monitor::hotkey_event_atom, Hotkey &hotkey) { + // a hotkey has changed + if (hotkey.hotkey_name() == StdFromQString(hotkey_name_)) { + if (QStringFromStd(hotkey.hotkey_sequence()) != sequence_) { + sequence_ = QStringFromStd(hotkey.hotkey_sequence()); + Q_EMIT sequenceChanged(); + } + QUuid uuid = QUuidFromUuid(hotkey.uuid()); + if (uuid != hotkey_uuid_) { + hotkey_uuid_ = uuid; + Q_EMIT uuidChanged(); + if (exclusive_) + notifyExclusiveChanged(); + } + } + }, + [=](keypress_monitor::hotkey_event_atom, + const utility::Uuid kotkey_uuid, + const bool pressed, + const std::string &context, + const std::string &window) { + // actual hotkey pressed or release ... we ignore + if (pressed && QUuidFromUuid(kotkey_uuid) == hotkey_uuid_ && + (context_.empty() || context_ == context)) { + activated(QStringFromStd(context)); + } + }}; }); } -void HotkeyReferenceUI::setHotkeyUuid(const QUuid &uuid) { +void HotkeyReferenceUI::setHotkeyName(const QString &name) { - uuid_ = uuid; - Q_EMIT hotkeyUuidChanged(); + if (hotkey_name_ == name) + return; + hotkey_name_ = name; + Q_EMIT hotkeyNameChanged(); - if (!uuid_.isNull()) { - try { + try { - scoped_actor sys{system()}; + scoped_actor sys{system()}; + + auto keyboard_manager = system().registry().template get(keyboard_events); - auto keyboard_manager = - system().registry().template get(keyboard_events); + auto hotkeys_config_events_group = utility::request_receive( + *sys, + keyboard_manager, + utility::get_event_group_atom_v, + keypress_monitor::hotkey_event_atom_v); - auto hotkeys_config_events_group = utility::request_receive( - *sys, - keyboard_manager, - utility::get_event_group_atom_v, - keypress_monitor::hotkey_event_atom_v); + const auto hk = request_receive( + *sys, keyboard_manager, ui::keypress_monitor::hotkey_atom_v, StdFromQString(name)); - const auto hk = request_receive( - *sys, - keyboard_manager, - ui::keypress_monitor::hotkey_atom_v, - UuidFromQUuid(uuid_)); - QString seq = QStringFromStd(hk.hotkey_sequence()); + QString seq = QStringFromStd(hk.hotkey_sequence()); - if (seq != sequence_) { - sequence_ = seq; - Q_EMIT sequenceChanged(); - } + if (seq != sequence_) { + sequence_ = seq; + Q_EMIT sequenceChanged(); + } - } catch (const std::exception &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + QUuid uuid = QUuidFromUuid(hk.uuid()); + if (uuid != hotkey_uuid_) { + hotkey_uuid_ = uuid; + Q_EMIT uuidChanged(); + if (exclusive_) + notifyExclusiveChanged(); } + + + } catch (const std::exception &err) { + spdlog::debug("{} {}", __PRETTY_FUNCTION__, err.what()); } } + +void HotkeyReferenceUI::setExclusive(const bool exclusive) { + + if (exclusive == exclusive_) + return; + exclusive_ = exclusive; + emit exclusiveChanged(); + notifyExclusiveChanged(); +} + +void HotkeyReferenceUI::notifyExclusiveChanged() { + + auto keyboard_manager = system().registry().template get(keyboard_events); + anon_mail( + keypress_monitor::watch_hotkey_atom_v, + UuidFromQUuid(hotkey_uuid_), + as_actor(), + exclusive_) + .send(keyboard_manager); +} diff --git a/src/ui/qml/viewport/src/include/viewport_qml_export.h b/src/ui/qml/viewport/src/include/viewport_qml_export.h deleted file mode 100644 index 6c16367e5..000000000 --- a/src/ui/qml/viewport/src/include/viewport_qml_export.h +++ /dev/null @@ -1,42 +0,0 @@ - -#ifndef VIEWPORT_QML_EXPORT_H -#define VIEWPORT_QML_EXPORT_H - -#ifdef VIEWPORT_QML_STATIC_DEFINE -# define VIEWPORT_QML_EXPORT -# define VIEWPORT_QML_NO_EXPORT -#else -# ifndef VIEWPORT_QML_EXPORT -# ifdef viewport_qml_EXPORTS - /* We are building this library */ -# define VIEWPORT_QML_EXPORT __declspec(dllexport) -# else - /* We are using this library */ -# define VIEWPORT_QML_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef VIEWPORT_QML_NO_EXPORT -# define VIEWPORT_QML_NO_EXPORT -# endif -#endif - -#ifndef VIEWPORT_QML_DEPRECATED -# define VIEWPORT_QML_DEPRECATED __declspec(deprecated) -#endif - -#ifndef VIEWPORT_QML_DEPRECATED_EXPORT -# define VIEWPORT_QML_DEPRECATED_EXPORT VIEWPORT_QML_EXPORT VIEWPORT_QML_DEPRECATED -#endif - -#ifndef VIEWPORT_QML_DEPRECATED_NO_EXPORT -# define VIEWPORT_QML_DEPRECATED_NO_EXPORT VIEWPORT_QML_NO_EXPORT VIEWPORT_QML_DEPRECATED -#endif - -#if 0 /* DEFINE_NO_DEPRECATED */ -# ifndef VIEWPORT_QML_NO_DEPRECATED -# define VIEWPORT_QML_NO_DEPRECATED -# endif -#endif - -#endif /* VIEWPORT_QML_EXPORT_H */ diff --git a/src/ui/qml/viewport/src/qml_viewport.cpp b/src/ui/qml/viewport/src/qml_viewport.cpp index beba767b2..5d2668bbb 100644 --- a/src/ui/qml/viewport/src/qml_viewport.cpp +++ b/src/ui/qml/viewport/src/qml_viewport.cpp @@ -1,10 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 + +#include + #include "xstudio/ui/qml/qml_viewport_renderer.hpp" // leave space or clang format will cause problems -#include "xstudio/ui/qml/playhead_ui.hpp" #include "xstudio/ui/qml/qml_viewport.hpp" -#include "xstudio/ui/qt/viewport_widget.hpp" #include "xstudio/ui/viewport/viewport.hpp" #include "xstudio/utility/logging.hpp" @@ -44,10 +45,10 @@ int qtModifierToOurs(const Qt::KeyboardModifiers qt_modifiers) { return result; } -/*PointerEvent makePointerEvent(Signature::EventType t, QWheelEvent *event) { +/*PointerEvent makePointerEvent(EventType t, QWheelEvent *event) { return PointerEvent( - Signature::EventType::MouseWheel, + EventType::MouseWheel, static_cast((int)event->buttons()), event->x(), event->y(), @@ -62,59 +63,28 @@ int qtModifierToOurs(const Qt::KeyboardModifiers qt_modifiers) { QMLViewport::QMLViewport(QQuickItem *parent) : QQuickItem(parent), cursor_(Qt::ArrowCursor) { - playhead_ = new PlayheadUI(this); - playhead_->init(CafSystemObject::get_actor_system()); - connect(this, &QQuickItem::windowChanged, this, &QMLViewport::handleWindowChanged); static int index = 0; - viewport_index_ = index++; - renderer_actor = new QMLViewportRenderer(this, viewport_index_); - connect(renderer_actor, SIGNAL(zoomChanged(float)), this, SIGNAL(zoomChanged(float))); - connect( - renderer_actor, - SIGNAL(fpsChanged(QString)), - this, - SIGNAL(fpsExpressionChanged(QString))); - connect(renderer_actor, SIGNAL(scaleChanged(float)), this, SIGNAL(scaleChanged(float))); - connect( - renderer_actor, - SIGNAL(translateChanged(QVector2D)), - this, - SIGNAL(translateChanged(QVector2D))); - connect( - renderer_actor, - SIGNAL(onScreenFrameChanged(int)), - this, - SLOT(setOnScreenImageLogicalFrame(int))); - connect(renderer_actor, SIGNAL(outOfRange(bool)), this, SLOT(setFrameOutOfRange(bool))); + + renderer_actor = new QMLViewportRenderer(this); + + keypress_monitor_ = CafSystemObject::get_actor_system().registry().template get( + xstudio::keyboard_events); connect( renderer_actor, - SIGNAL(zoomChanged(float)), - this, - SIGNAL(imageBoundaryInViewportChanged())); - connect( - renderer_actor, - SIGNAL(translateChanged(QVector2D)), - this, - SIGNAL(imageBoundaryInViewportChanged())); - connect( - renderer_actor, - SIGNAL(scaleChanged(float)), + SIGNAL(translationChanged()), this, - SIGNAL(imageBoundaryInViewportChanged())); + SIGNAL(imageBoundariesInViewportChanged())); connect( - renderer_actor, - SIGNAL(noAlphaChannelChanged(bool)), - this, - SLOT(setNoAlphaChannel(bool))); + renderer_actor, SIGNAL(resolutionsChanged()), this, SIGNAL(imageResolutionsChanged())); connect( this, - SIGNAL(quickViewSource(QStringList, QString)), + SIGNAL(quickViewSource(QStringList, QString, int, int)), renderer_actor, - SLOT(quickViewSource(QStringList, QString))); + SLOT(quickViewSource(QStringList, QString, int, int))); connect( renderer_actor, @@ -134,13 +104,19 @@ QMLViewport::QMLViewport(QQuickItem *parent) : QQuickItem(parent), cursor_(Qt::A this, SIGNAL(snapshotRequestResult(QString))); + connect(this, SIGNAL(visibleChanged()), this, SLOT(onVisibleChanged())); + setAcceptedMouseButtons(Qt::AllButtons); setAcceptHoverEvents(true); + + if (renderer_actor) + renderer_actor->visibleChanged(isVisible()); } QMLViewport::~QMLViewport() { delete renderer_actor; } void QMLViewport::handleWindowChanged(QQuickWindow *win) { + spdlog::debug("QMLViewport::handleWindowChanged"); if (win) { // Send screen info for the first time @@ -161,15 +137,6 @@ void QMLViewport::handleWindowChanged(QQuickWindow *win) { &QMLViewport::cleanup, Qt::DirectConnection); - connect( - win, - &QQuickWindow::frameSwapped, - renderer_actor, - &QMLViewportRenderer::frameSwapped, - Qt::DirectConnection); - - connect(renderer_actor, &QMLViewportRenderer::doRedraw, win, &QQuickWindow::update); - connect( win, &QQuickWindow::screenChanged, @@ -177,79 +144,77 @@ void QMLViewport::handleWindowChanged(QQuickWindow *win) { &QMLViewport::handleScreenChanged, Qt::DirectConnection); - renderer_actor->setWindow(win); + if (renderer_actor) { + connect( + win, + &QQuickWindow::frameSwapped, + renderer_actor, + &QMLViewportRenderer::frameSwapped, + Qt::DirectConnection); + + connect(renderer_actor, &QMLViewportRenderer::doRedraw, win, &QQuickWindow::update); + + renderer_actor->setWindow(win); + } // This is crucial - we draw the viewport before QML renders to the screen. // If we don't turn off clear then the viewport gets wiped - win->setClearBeforeRendering(false); + // win->setColor(QColor(0,0,0,0)); m_window = win; } } -void QMLViewport::linkToViewport(QObject *other_viewport) { - - auto other = dynamic_cast(other_viewport); - if (other) { - QMLViewportRenderer *otherActor = other->viewportActor(); - renderer_actor->linkToViewport(otherActor); - } else { - qDebug() << "QMLViewport::linkToViewport failed because " << other_viewport - << " is not derived from QMLViewport."; - } -} - void QMLViewport::handleScreenChanged(QScreen *screen) { spdlog::debug("QMLViewport::handleScreenChanged"); - renderer_actor->setScreenInfos( - screen->name(), - screen->model(), - screen->manufacturer(), - screen->serialNumber(), - screen->refreshRate()); + if (renderer_actor) + renderer_actor->setScreenInfos( + screen->name(), + screen->model(), + screen->manufacturer(), + screen->serialNumber(), + screen->refreshRate()); } - -PointerEvent -QMLViewport::makePointerEvent(Signature::EventType t, QMouseEvent *event, int force_modifiers) { +void QMLViewport::sendPointerEvent(EventType t, QMouseEvent *event, int force_modifiers) { PointerEvent p( t, static_cast((int)event->buttons()), - event->x(), - event->y(), - width(), // FIXME should be width, but this function appears to never be called. - height(), // FIXME should be height + int(round(event->position().x() * window()->effectiveDevicePixelRatio())), + int(round(event->position().y() * window()->effectiveDevicePixelRatio())), + int(round(float(width()) * window()->effectiveDevicePixelRatio())), + int(round(float(height()) * window()->effectiveDevicePixelRatio())), qtModifierToOurs(event->modifiers()) + force_modifiers, - fmt::format("viewport{0}", viewport_index_)); + renderer_actor ? renderer_actor->std_name() : ""); p.w_ = utility::clock::now(); - return p; + if (renderer_actor) + renderer_actor->pointerEvent(p); } -PointerEvent QMLViewport::makePointerEvent( - Signature::EventType t, int buttons, int x, int y, int w, int h, int modifiers) { +void QMLViewport::sendPointerEvent(QHoverEvent *event) { - return PointerEvent( - t, - static_cast(buttons), - x, - y, - w, - h, - modifiers, - fmt::format("viewport{0}", viewport_index_)); + PointerEvent p( + EventType::Move, + static_cast(0), + int(round(float(event->position().x()) * window()->effectiveDevicePixelRatio())), + int(round(float(event->position().y()) * window()->effectiveDevicePixelRatio())), + int(round(float(width()) * window()->effectiveDevicePixelRatio())), + int(round(float(height()) * window()->effectiveDevicePixelRatio())), + Signature::Modifier::NoModifier, + renderer_actor ? renderer_actor->std_name() : ""); + p.w_ = utility::clock::now(); + if (renderer_actor) + renderer_actor->pointerEvent(p); } -static QOpenGLContext *__aa = nullptr; -static QOpenGLContext *__bb = nullptr; - void QMLViewport::sync() { // TODO: this is a little clunky. Can we do this somewhere else? - if (!connected_) { + if (!connected_ && renderer_actor) { connect( window(), - &QQuickWindow::beforeRendering, + &QQuickWindow::beforeRenderPassRecording, renderer_actor, &QMLViewportRenderer::paint, Qt::DirectConnection); @@ -261,29 +226,17 @@ void QMLViewport::sync() { // Tell the renderer the viewport coordinates. These are the 4 corners of the viewport // within the overall GL viewport, - renderer_actor->setSceneCoordinates( - mapToScene(boundingRect().topLeft()), - mapToScene(boundingRect().topRight()), - mapToScene(boundingRect().bottomRight()), - mapToScene(boundingRect().bottomLeft()), - window()->size(), - window()->devicePixelRatio()); - - /*static bool share = false; - if (window() && !share) { - // //qDebug() << this << " OGL CONTEXT " << window()->openglContext() << "\n"; - if (window()->openglContext()) { - if (!__aa) - __aa = window()->openglContext(); - if (__aa && __aa != window()->openglContext()) { - __bb = window()->openglContext(); - window()->openglContext()->setShareContext(__aa); - share = true; - } - //qDebug() << this << " SHARE " << window()->openglContext()->shareContext() << - "\n"; - } - }*/ + if (renderer_actor) { + renderer_actor->setSceneCoordinates( + mapToScene(boundingRect().topLeft()), + mapToScene(boundingRect().topRight()), + mapToScene(boundingRect().bottomRight()), + mapToScene(boundingRect().bottomLeft()), + window()->size(), + window()->effectiveDevicePixelRatio()); + + renderer_actor->prepareRenderData(); + } } void QMLViewport::cleanup() { @@ -314,68 +267,48 @@ void QMLViewport::hoverLeaveEvent(QHoverEvent *event) { QQuickItem::hoverLeaveEvent(event); } +QPointF QMLViewport::toViewportCoords(const QPointF &in) const { -void QMLViewport::mousePressEvent(QMouseEvent *event) { + if (!renderer_actor) return in; + return renderer_actor->toViewportCoords(QPointF( + 2.0f * (in.x() - float(width()) * 0.5f) / float(width()), + 2.0f * ((height() - in.y()) - float(height()) * 0.5f) / float(height()))); +} - mouse_position = event->pos(); - emit(mouseChanged()); - mouse_buttons = event->buttons(); - emit(mouseButtonsChanged()); +void QMLViewport::mousePressEvent(QMouseEvent *event) { + mouse_position = event->position(); + emit mousePress(toViewportCoords(mouse_position), event->buttons(), event->modifiers()); + emit mousePressScreenPixels(mouse_position, event->buttons(), event->modifiers()); event->accept(); - - if (renderer_actor) { - renderer_actor->pointerEvent(makePointerEvent(Signature::EventType::ButtonDown, event)); - } + sendPointerEvent(EventType::ButtonDown, event); } void QMLViewport::mouseReleaseEvent(QMouseEvent *event) { - mouse_position = event->pos(); - emit(mouseChanged()); - mouse_buttons = event->buttons(); - emit(mouseButtonsChanged()); + mouse_position = event->position(); + emit mouseRelease(event->buttons()); event->accept(); - if (renderer_actor) { - renderer_actor->pointerEvent( - makePointerEvent(Signature::EventType::ButtonRelease, event)); - } + sendPointerEvent(EventType::ButtonRelease, event); } void QMLViewport::hoverMoveEvent(QHoverEvent *event) { - mouse_position = event->pos(); - - emit(mouseChanged()); - - if (renderer_actor) { - - renderer_actor->pointerEvent(makePointerEvent( - Signature::EventType::Move, - 0, - event->pos().x(), - event->pos().y(), - width(), - height(), - 0)); - } + mouse_position = event->position(); + emit(mousePositionChanged( + toViewportCoords(mouse_position), event->buttons(), event->modifiers())); + sendPointerEvent(event); QQuickItem::hoverMoveEvent(event); } void QMLViewport::mouseMoveEvent(QMouseEvent *event) { mouse_position = event->pos(); - - emit(mouseChanged()); - - if (renderer_actor) { - renderer_actor->pointerEvent(makePointerEvent( - event->buttons() ? Signature::EventType::Drag : Signature::EventType::Move, - event, - 0)); - } + emit(mousePositionChanged( + toViewportCoords(mouse_position), event->buttons(), event->modifiers())); + sendPointerEvent(event->buttons() ? EventType::Drag : EventType::Move, event, 0); update(); // event->ignore(); // QQuickItem::mouseMoveEvent(event); @@ -383,45 +316,58 @@ void QMLViewport::mouseMoveEvent(QMouseEvent *event) { void QMLViewport::mouseDoubleClickEvent(QMouseEvent *event) { - mouse_position = event->pos(); - emit(mouseChanged()); - if (renderer_actor) { - renderer_actor->pointerEvent( - makePointerEvent(Signature::EventType::DoubleClick, event)); - } + mouse_position = event->position(); + emit mouseDoubleClick( + toViewportCoords(mouse_position), event->buttons(), event->modifiers()); + sendPointerEvent(EventType::DoubleClick, event); } void QMLViewport::keyPressEvent(QKeyEvent *key_event) { - if (key_event) { - // I don't think we need this, as key events are forwarded in - // QMLViewport::event - // renderer_actor->rawKeyDown(key_event->key(), key_event->isAutoRepeat()); - } - renderer_actor->keyboardTextEntry(key_event->text()); + anon_mail( + ui::keypress_monitor::text_entry_atom_v, + StdFromQString(key_event->text()), + renderer_actor ? renderer_actor->std_name() : "", + StdFromQString(m_window->objectName())) + .send(keypress_monitor_); } void QMLViewport::keyReleaseEvent(QKeyEvent *key_event) { - if (key_event && !key_event->isAutoRepeat()) { - renderer_actor->rawKeyUp(key_event->key()); + if (!key_event->isAutoRepeat()) { + anon_mail( + ui::keypress_monitor::key_up_atom_v, + key_event->key(), + renderer_actor ? renderer_actor->std_name() : "", + StdFromQString(m_window->objectName())) + .send(keypress_monitor_); } } bool QMLViewport::event(QEvent *event) { - // qDebug() << event; - - /* This is where keyboard events are captured and sent to the backend!! */ if (event->type() == QEvent::KeyPress) { + auto key_event = dynamic_cast(event); if (key_event) { - renderer_actor->rawKeyDown(key_event->key(), key_event->isAutoRepeat()); + anon_mail( + ui::keypress_monitor::key_down_atom_v, + key_event->key(), + renderer_actor ? renderer_actor->std_name() : "", + StdFromQString(m_window->objectName()), + key_event->isAutoRepeat()) + .send(keypress_monitor_); } } else if (event->type() == QEvent::KeyRelease) { + auto key_event = dynamic_cast(event); if (key_event && !key_event->isAutoRepeat()) { - renderer_actor->rawKeyUp(key_event->key()); + anon_mail( + ui::keypress_monitor::key_up_atom_v, + key_event->key(), + renderer_actor ? renderer_actor->std_name() : "", + StdFromQString(m_window->objectName())) + .send(keypress_monitor_); } } else if ( event->type() == QEvent::Leave || event->type() == QEvent::HoverLeave || @@ -436,78 +382,43 @@ bool QMLViewport::event(QEvent *event) { // (in terms of whether a key is held down) but a false positive // (i.e. we think a key is down when it isn't) could really mess up the // UI behaviour - renderer_actor->allKeysUp(); + anon_mail( + ui::keypress_monitor::all_keys_up_atom_v, + renderer_actor ? renderer_actor->std_name() : "") + .send(keypress_monitor_); + } else if (event->type() == QEvent::HoverEnter) { + forceActiveFocus(Qt::MouseFocusReason); } return QQuickItem::event(event); } -float QMLViewport::zoom() { - if (renderer_actor) - return renderer_actor->zoom(); - return 1.0f; -} - -void QMLViewport::setZoom(const float z) { - if (renderer_actor) - renderer_actor->setZoom(z); -} - -void QMLViewport::revertFitZoomToPrevious(const bool ignoreOtherViewport) { - if (renderer_actor) - renderer_actor->revertFitZoomToPrevious(); - window()->update(); -} - -QString QMLViewport::fpsExpression() { return renderer_actor->fpsExpression(); } - -float QMLViewport::scale() { return renderer_actor->scale(); } - -QVector2D QMLViewport::translate() { return renderer_actor->translate(); } - -QObject *QMLViewport::playhead() { return static_cast(playhead_); } - -void QMLViewport::setOnScreenImageLogicalFrame(const int frame_num) { - if (frame_num != on_screen_logical_frame_) { - on_screen_logical_frame_ = frame_num; - emit onScreenImageLogicalFrameChanged(); - } -} - -void QMLViewport::setScale(const float s) { renderer_actor->setScale(s); } - -void QMLViewport::setTranslate(const QVector2D &t) { renderer_actor->setTranslate(t); } - void QMLViewport::wheelEvent(QWheelEvent *event) { // make a mouse wheel event and pass to viewport to process PointerEvent ev( - Signature::EventType::MouseWheel, + EventType::MouseWheel, static_cast((int)event->buttons()), event->position().x(), event->position().y(), width(), // FIXME should be width, but this function appears to never be called. height(), // FIXME should be height qtModifierToOurs(event->modifiers()), - fmt::format("viewport{0}", viewport_index_), + renderer_actor ? renderer_actor->std_name() : "", std::make_pair(event->angleDelta().rx(), event->angleDelta().ry()), std::make_pair(event->pixelDelta().rx(), event->pixelDelta().ry())); - if (!renderer_actor->pointerEvent(ev) && playhead_) { + if (renderer_actor && !renderer_actor->pointerEvent(ev) && renderer_actor->playhead()) { // If viewport hasn't acted on the mouse wheel event (because user pref // to zoom with mouse wheel is false), assume that we can instead use it // to step the playhead - playhead_->setPlaying(false); - playhead_->step(event->angleDelta().ry() > 0 ? 1 : -1); + anon_mail(playhead::play_atom_v, false).send(renderer_actor->playhead()); + anon_mail(playhead::step_atom_v, event->angleDelta().ry() > 0 ? 1 : -1) + .send(renderer_actor->playhead()); } QQuickItem::wheelEvent(event); } -void QMLViewport::setPlayhead(caf::actor playhead) { - spdlog::debug("QMLViewport::setPlayhead"); - playhead_->set_backend(playhead); -} - void QMLViewport::hideCursor() { if (!cursor_hidden) { setCursor(QCursor(Qt::BlankCursor)); @@ -521,36 +432,6 @@ void QMLViewport::showCursor() { } } -QSize QMLViewport::imageResolution() { - Imath::V2i resolution = renderer_actor->imageResolutionCoords(); - return QSize(resolution.x, resolution.y); -} - -QRectF QMLViewport::imageBoundaryInViewport() { - QRectF r = renderer_actor->imageBoundsInViewportPixels(); - return QRectF( - r.x() * width(), r.y() * height(), r.width() * width(), r.height() * height()); -} - -QVector2D QMLViewport::bboxCornerInViewport(const int min_x, const int min_y) { - Imath::V2f corner_in_viewport = renderer_actor->imageCoordsToViewport(min_x, min_y); - return QVector2D(corner_in_viewport.x * width(), corner_in_viewport.y * height()); -} - -void QMLViewport::setFrameOutOfRange(bool frame_out_of_range) { - if (frame_out_of_range != frame_out_of_range_) { - frame_out_of_range_ = frame_out_of_range; - emit frameOutOfRangeChanged(); - } -} - -void QMLViewport::setNoAlphaChannel(bool no_alpha_channel) { - if (no_alpha_channel != no_alpha_channel_) { - no_alpha_channel_ = no_alpha_channel; - emit noAlphaChannelChanged(); - } -} - class CleanupJob : public QRunnable { public: /* N.B. - we use a shared_ptr to manage the deletion of the viewport. The @@ -565,6 +446,8 @@ class CleanupJob : public QRunnable { void QMLViewport::releaseResources() { + if (!renderer_actor) + return; /* This is the recommended way to delete the object that manages OpenGL resources. Scheduling a render job means that it is run when the OpenGL context is valid and as such in the destructor of the ViewportRenderer @@ -574,19 +457,6 @@ void QMLViewport::releaseResources() { renderer_actor = nullptr; } -void QMLViewport::renderImageToFile( - const QUrl filePath, - const int format, - const int compression, - const int width, - const int height, - const bool bakeColor) { - - renderer_actor->renderImageToFile( - filePath, playhead_->backend(), format, compression, width, height, bakeColor); -} - - void QMLViewport::sendResetShortcut() { /* This emulates keyboard events for Ctrl+R (viewer reset), and is called from QML when * clicking the menu option */ @@ -623,18 +493,47 @@ void QMLViewport::setOverrideCursor(const Qt::CursorShape cname) { this->setCursor(QCursor(cname)); } -void QMLViewport::setRegularCursor(const Qt::CursorShape cname) { +QString QMLViewport::name() const { return renderer_actor ? renderer_actor->name() : ""; } + +void QMLViewport::setPlayhead(const QString actorAddress) { - cursor_ = QCursor(cname); - this->setCursor(cursor_); + if (actorAddress != "") { + caf::actor playhead = + actorFromQString(CafSystemObject::get_actor_system(), actorAddress); + if (playhead && renderer_actor) { + renderer_actor->set_playhead(playhead); + } else { + spdlog::warn( + "{} bad playhead actor address: {}", + __PRETTY_FUNCTION__, + StdFromQString(actorAddress)); + } + } +} + +void QMLViewport::reset() { + if (renderer_actor) + renderer_actor->reset(); } -QString QMLViewport::name() const { return renderer_actor->name(); } +QString QMLViewport::playheadActorAddress() { -void QMLViewport::setIsQuickViewer(bool is_quick_viewer) { - if (is_quick_viewer != is_quick_viewer_) { - renderer_actor->setIsQuickViewer(is_quick_viewer); - is_quick_viewer_ = is_quick_viewer; - emit isQuickViewerChanged(); - } -} \ No newline at end of file + if (renderer_actor) + return actorToQString(CafSystemObject::get_actor_system(), renderer_actor->playhead()); + return QString(); +} + +void QMLViewport::onVisibleChanged() { + if (renderer_actor) + renderer_actor->visibleChanged(isVisible()); +} + +QVariantList QMLViewport::imageResolutions() { + return renderer_actor ? renderer_actor->imageResolutions() : QVariantList(); +} + +QVariantList QMLViewport::imageBoundariesInViewport() { + if (renderer_actor) + return renderer_actor->imageBoundariesInViewport(); + return QVariantList(); +} diff --git a/src/ui/qml/viewport/src/qml_viewport_renderer.cpp b/src/ui/qml/viewport/src/qml_viewport_renderer.cpp index 745697707..5652106cc 100644 --- a/src/ui/qml/viewport/src/qml_viewport_renderer.cpp +++ b/src/ui/qml/viewport/src/qml_viewport_renderer.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 +#include + #include "xstudio/ui/qml/qml_viewport_renderer.hpp" #include "xstudio/ui/qml/qml_viewport.hpp" #include "xstudio/media_reader/media_reader.hpp" -#include "xstudio/ui/qml/playhead_ui.hpp" #include @@ -11,16 +12,18 @@ using namespace xstudio::ui::qml; using namespace xstudio::ui::viewport; using namespace xstudio; -namespace {} // namespace +namespace { +static int ctt = 0; +} // namespace + // N.B. we don't pass in 'parent' as the parent of the base class. The owner // of this class must schedule its destruction directly rather than rely on // Qt object child destruction. -QMLViewportRenderer::QMLViewportRenderer(QObject *parent, const int viewport_index) - : QMLActor(nullptr), m_window(nullptr), viewport_index_(viewport_index) { +QMLViewportRenderer::QMLViewportRenderer(QObject *parent) + : QMLActor(nullptr), m_window(nullptr) { viewport_qml_item_ = dynamic_cast(parent); - init_system(); } @@ -48,37 +51,44 @@ void QMLViewportRenderer::init_renderer() { void QMLViewportRenderer::paint() { - // TODO: again, this init call probably shouldn't happen in the main - // draw call. see above. + if (viewport_qml_item_ && viewport_qml_item_->isVisible() && viewport_renderer_) { - if (!init_done) { - init_done = true; - init_renderer(); - } + m_window->beginExternalCommands(); - glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS); - viewport_renderer_->render(); - glPopClientAttrib(); + // TODO: again, this init call probably shouldn't happen in the main + // draw call. see above. - // TODO: this is in the wrong place, we should check this *before* the - // redraw - const QRectF bounds = imageBoundsInViewportPixels(); - if (bounds != imageBounds_) { - imageBounds_ = bounds; - emit(zoomChanged(viewport_renderer_->pixel_zoom())); - } + if (!init_done) { + init_done = true; + init_renderer(); + } + glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS); + viewport_renderer_->render(); + glPopClientAttrib(); - m_window->resetOpenGLState(); - if (viewport_renderer_->playing()) { - emit(doRedraw()); + // m_window->resetOpenGLState(); + if (viewport_renderer_->playing()) { + emit(doRedraw()); + } + + m_window->endExternalCommands(); } } void QMLViewportRenderer::frameSwapped() { - viewport_renderer_->framebuffer_swapped(utility::clock::now()); + if (viewport_renderer_) + viewport_renderer_->framebuffer_swapped(utility::clock::now()); } -void QMLViewportRenderer::setWindow(QQuickWindow *window) { m_window = window; } +void QMLViewportRenderer::setWindow(QQuickWindow *window) { + + if (window && m_window != window) { + m_window = window; + + // window has changed - we need a new xstudio viewport + make_xstudio_viewport(); + } +} void QMLViewportRenderer::setSceneCoordinates( const QPointF topleft, @@ -93,38 +103,49 @@ void QMLViewportRenderer::setSceneCoordinates( // Qml item changes. viewport_coords_.set returns true only when // these values have changed since the last call + // devicePixelRatio allows us to account for high DPI scaling, giving + // us the window size in actual device pixels + if (viewport_coords_.set(topleft, topright, bottomright, bottomleft, sceneSize)) { - anon_send( - self(), + anon_mail( viewport_set_scene_coordinates_atom_v, - Imath::V2f(topleft.x(), topleft.y()), - Imath::V2f(topright.x(), topright.y()), - Imath::V2f(bottomright.x(), bottomright.y()), - Imath::V2f(bottomleft.x(), bottomleft.y()), - Imath::V2i(sceneSize.width(), sceneSize.height()), - devicePixelRatio); + float(topleft.x()), + float(topleft.y()), + float(topright.x() - topleft.x()), + float(bottomleft.y() - topleft.y()), + float(sceneSize.width()) , + float(sceneSize.height()), + devicePixelRatio) + .send(self()); } } +void QMLViewportRenderer::prepareRenderData() { + if (viewport_renderer_) + viewport_renderer_->prepare_render_data(); +} + void QMLViewportRenderer::init_system() { QMLActor::init(CafSystemObject::get_actor_system()); spdlog::debug("QMLViewportRenderer init"); +} + +void QMLViewportRenderer::make_xstudio_viewport() { - self()->set_default_handler(caf::drop); + if (viewport_renderer_) + delete viewport_renderer_; utility::JsonStore jsn; - jsn["base"] = utility::JsonStore(); + jsn["base"] = utility::JsonStore(); + jsn["window_id"] = StdFromQString(m_window->objectName()); /* Here we create the all important Viewport class that actually draws images to the screen */ - viewport_renderer_ = new ui::viewport::Viewport( - jsn, - as_actor(), - viewport_index_, - ui::viewport::ViewportRendererPtr( - new opengl::OpenGLViewportRenderer(viewport_index_, false))); + viewport_renderer_ = new ui::viewport::Viewport(jsn, as_actor()); + + viewport_renderer_->set_visibility(viewport_qml_item_->isVisible()); /* Provide a callback so the Viewport can tell this class when some property of the viewport has changed and such events can be propagated to other QT components, for example */ @@ -133,11 +154,7 @@ void QMLViewportRenderer::init_system() { }; viewport_renderer_->set_change_callback(callback); - // update PlayheadUI object owned by the 'QMLViewport' - auto *vp = dynamic_cast(parent()); - if (vp) { - vp->setPlayhead(viewport_renderer_->playhead()); - } + viewport_qml_item_->setPlayheadUuid(QUuidFromUuid(viewport_renderer_->playhead_uuid())); /* The Viewport object provides a message handler that will process update events like new frame buffers coming from the playhead and so-on. Instead of being an actor itself, the @@ -200,11 +217,14 @@ void QMLViewportRenderer::init_system() { and released */ keypress_monitor_ = system().registry().template get(keyboard_events); } +void QMLViewportRenderer::set_playhead(caf::actor playhead) { + if (viewport_renderer_) + viewport_renderer_->set_playhead(playhead); +} -void QMLViewportRenderer::set_playhead(PlayheadUI *playhead) { - - spdlog::debug("QMLViewportRenderer::set_playhead"); - viewport_renderer_->set_playhead(playhead ? playhead->backend() : caf::actor()); +void QMLViewportRenderer::reset() { + if (viewport_renderer_) + viewport_renderer_->reset(); } bool QMLViewportRenderer::pointerEvent(const PointerEvent &e) { @@ -212,7 +232,7 @@ bool QMLViewportRenderer::pointerEvent(const PointerEvent &e) { // make a mutable copy, so we can add more coordinate info in // 'process_pointer_event' PointerEvent _e = e; - if (viewport_renderer_->process_pointer_event(_e)) { + if (viewport_renderer_ && viewport_renderer_->process_pointer_event(_e)) { // pointer event will be consumed if the user is doing interactive // pan/zoom, for example. Force a redraw. if (m_window) @@ -221,32 +241,31 @@ bool QMLViewportRenderer::pointerEvent(const PointerEvent &e) { } else { // viewport did not consume pointer event, forward it on to the // mouse/leyboard event manager - anon_send(keypress_monitor_, ui::keypress_monitor::mouse_event_atom_v, _e); + anon_mail(ui::keypress_monitor::mouse_event_atom_v, _e).send(keypress_monitor_); } return false; // pointer event not used } -float QMLViewportRenderer::zoom() { return viewport_renderer_->pixel_zoom(); } - -void QMLViewportRenderer::setZoom(const float f) { - anon_send(self(), viewport_pixel_zoom_atom_v, f); -} - -Imath::V2i QMLViewportRenderer::imageResolutionCoords() { - return viewport_renderer_->image_resolution(); -} - -QRectF QMLViewportRenderer::imageBoundsInViewportPixels() const { - Imath::Box2f box = viewport_renderer_->image_bounds_in_viewport_pixels(); - return QRectF(box.min.x, box.min.y, box.max.x - box.min.x, box.max.y - box.min.y); +QVariantList QMLViewportRenderer::imageResolutions() const { + QVariantList v; + const auto &image_resolutions = viewport_renderer_->image_resolutions(); + for (const auto &r : image_resolutions) { + v.append(QSize(r.x, r.y)); + } + return v; } -void QMLViewportRenderer::revertFitZoomToPrevious() { - viewport_renderer_->revert_fit_zoom_to_previous(); -} +QVariantList QMLViewportRenderer::imageBoundariesInViewport() const { -Imath::V2f QMLViewportRenderer::imageCoordsToViewport(const int x, const int y) { - return viewport_renderer_->image_coordinate_to_viewport_coordinate(x, y); + QVariantList v; + const std::vector image_boxes = + viewport_renderer_->image_bounds_in_viewport_pixels(); + for (const auto &box : image_boxes) { + QRectF imageBoundsInViewportPixels( + box.min.x, box.min.y, box.max.x - box.min.x, box.max.y - box.min.y); + v.append(imageBoundsInViewportPixels); + } + return v; } bool QMLViewportRenderer::ViewportCoords::set( @@ -264,60 +283,8 @@ bool QMLViewportRenderer::ViewportCoords::set( return true; } -void QMLViewportRenderer::rawKeyDown(const int key, const bool auto_repeat) { - // key events from QT are passed from the QMLViewport to this class, which sends a caf - // message to itself qhich is handled in the message handlers provided by Viewport object. - // The viewport passes the message to a KeyPressMonitor actor, which uses some simple timing - // to filter out key events that are auto-repeats generated by the system when the user - // holds a key down. The goal is that we get one message coming back to this class when the - // key is pressed and one when it is released and all auto-repeat keypress events are eaten. - anon_send( - keypress_monitor_, - ui::keypress_monitor::key_down_atom_v, - key, - viewport_renderer_->name(), - auto_repeat); -} - -void QMLViewportRenderer::keyboardTextEntry(const QString text) { - anon_send( - keypress_monitor_, - ui::keypress_monitor::text_entry_atom_v, - text.toStdString(), - viewport_renderer_->name()); -} - -void QMLViewportRenderer::rawKeyUp(const int key) { - // see above - anon_send( - keypress_monitor_, - ui::keypress_monitor::key_up_atom_v, - key, - viewport_renderer_->name()); -} - -void QMLViewportRenderer::allKeysUp() { - anon_send( - keypress_monitor_, - ui::keypress_monitor::all_keys_up_atom_v, - viewport_renderer_->name()); -} - -void QMLViewportRenderer::setScale(const float s) { - anon_send(self(), viewport_scale_atom_v, s); -} - -void QMLViewportRenderer::setTranslate(const QVector2D &t) { - anon_send(self(), viewport_pan_atom_v, t.x(), t.y()); -} - -float QMLViewportRenderer::scale() { return viewport_renderer_->scale(); } - -QVector2D QMLViewportRenderer::translate() { - return QVector2D(viewport_renderer_->pan().x, viewport_renderer_->pan().y); -} - -void QMLViewportRenderer::quickViewSource(QStringList mediaActors, QString compareMode) { +void QMLViewportRenderer::quickViewSource( + QStringList mediaActors, QString compareMode, int in_pt, int out_pt) { std::vector media; for (const auto &media_actor_as_string : mediaActors) { @@ -328,7 +295,8 @@ void QMLViewportRenderer::quickViewSource(QStringList mediaActors, QString compa } } if (!media.empty()) { - anon_send(self(), quickview_media_atom_v, media, StdFromQString(compareMode)); + anon_mail(quickview_media_atom_v, media, StdFromQString(compareMode), in_pt, out_pt) + .send(self()); } } @@ -336,26 +304,17 @@ void QMLViewportRenderer::receive_change_notification(Viewport::ChangeCallbackId if (id == Viewport::ChangeCallbackId::Redraw) { m_window->update(); - } else if (id == Viewport::ChangeCallbackId::ZoomChanged) { - emit zoomChanged(zoom()); - } else if (id == Viewport::ChangeCallbackId::ScaleChanged) { - emit scaleChanged(scale()); - } else if (id == Viewport::ChangeCallbackId::FrameRateChanged) { - fps_expression_ = QStringFromStd(viewport_renderer_->frame_rate_expression()); - emit fpsChanged(fps_expression_); - } else if (id == Viewport::ChangeCallbackId::OutOfRangeChanged) { - emit outOfRange(viewport_renderer_->frame_out_of_range()); - } else if (id == Viewport::ChangeCallbackId::OnScreenFrameChanged) { - emit onScreenFrameChanged(viewport_renderer_->on_screen_frame()); } else if (id == Viewport::ChangeCallbackId::TranslationChanged) { - emit translateChanged( - QVector2D(viewport_renderer_->pan().x, viewport_renderer_->pan().y)); + emit translationChanged(); } else if (id == Viewport::ChangeCallbackId::PlayheadChanged) { - if (viewport_qml_item_) { - viewport_qml_item_->setPlayhead(viewport_renderer_->playhead()); + if (viewport_qml_item_ && viewport_renderer_) { + viewport_qml_item_->setPlayheadUuid( + QUuidFromUuid(viewport_renderer_->playhead_uuid())); } - } else if (id == Viewport::ChangeCallbackId::NoAlphaChannelChanged) { - emit noAlphaChannelChanged(viewport_renderer_->no_alpha_channel()); + } else if (id == Viewport::ChangeCallbackId::ImageBoundsChanged) { + emit translationChanged(); + } else if (id == Viewport::ChangeCallbackId::ImageResolutionsChanged) { + emit resolutionsChanged(); } } @@ -365,55 +324,26 @@ void QMLViewportRenderer::setScreenInfos( QString manufacturer, QString serialNumber, double refresh_rate) { - viewport_renderer_->set_screen_infos( - name.toStdString(), - model.toStdString(), - manufacturer.toStdString(), - serialNumber.toStdString(), - refresh_rate); + if (viewport_renderer_) + viewport_renderer_->set_screen_infos( + name.toStdString(), + model.toStdString(), + manufacturer.toStdString(), + serialNumber.toStdString(), + refresh_rate); } -void QMLViewportRenderer::linkToViewport(QMLViewportRenderer *other_viewport) { - viewport_renderer_->link_to_viewport(other_viewport->as_actor()); -} +void QMLViewportRenderer::setIsQuickViewer(const bool is_quick_viewer) {} -void QMLViewportRenderer::renderImageToFile( - const QUrl filePath, - caf::actor playhead, - const int format, - const int compression, - const int width, - const int height, - const bool bakeColor) { - - caf::scoped_actor sys{system()}; - try { - - auto offscreen_viewport = - system().registry().template get(offscreen_viewport_registry); - - if (offscreen_viewport) { - - std::cerr << "A\n"; - utility::request_receive( - *sys, offscreen_viewport, viewport::viewport_playhead_atom_v, playhead); - std::cerr << "B\n"; - - utility::request_receive( - *sys, - offscreen_viewport, - viewport::render_viewport_to_image_atom_v, - UriFromQUrl(filePath), - width, - height); - std::cerr << "C\n"; - - } else { - emit snapshotRequestResult(QString("Offscreen viewport renderer was not found.")); - } - } catch (std::exception &e) { - emit snapshotRequestResult(QString(e.what())); - } +void QMLViewportRenderer::visibleChanged(const bool is_visible) { + if (viewport_renderer_) + viewport_renderer_->set_visibility(is_visible); } -void QMLViewportRenderer::setIsQuickViewer(const bool is_quick_viewer) {} +QPointF QMLViewportRenderer::toViewportCoords(const QPointF &in) const { + Imath::V4f ptr(in.x(), in.y(), 0.0f, 1.0f); + + if (viewport_renderer_) + ptr *= viewport_renderer_->projection_matrix(); + return QPointF(ptr.x / ptr.w, -ptr.y / ptr.w); +} diff --git a/src/ui/qml/viewport/test/CMakeLists.txt b/src/ui/qml/viewport/test/CMakeLists.txt index 4dfcb3c78..4fccc5b5c 100644 --- a/src/ui/qml/viewport/test/CMakeLists.txt +++ b/src/ui/qml/viewport/test/CMakeLists.txt @@ -1,12 +1,12 @@ include(CTest) -# add_executable(viewport_qml_test viewport_ui_test.cpp qml.qrc) +# add_executable(viewport_qml_test viewport_ui_test.cpp) # default_options_gtest(viewport_qml_test) # target_link_libraries(viewport_qml_test # PUBLIC # xstudio::ui::qml::viewport -# Qt5::Gui -# Qt5::Quick +# Qt6::Gui +# Qt6::Quick # ${GTEST_LDFLAGS} # ) # add_test(viewport_qml_tests viewport_qml_test) diff --git a/src/ui/qml/viewport/test/main.qml b/src/ui/qml/viewport/test/main.qml index 8d374fc6b..82b4e65c6 100644 --- a/src/ui/qml/viewport/test/main.qml +++ b/src/ui/qml/viewport/test/main.qml @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.3 +import QtQuick + +import QtQuick.Layouts + import xstudio.qml.json_store 1.0 Window { diff --git a/src/ui/qt/CMakeLists.txt b/src/ui/qt/CMakeLists.txt index 04aad249b..d3c4fb3ad 100644 --- a/src/ui/qt/CMakeLists.txt +++ b/src/ui/qt/CMakeLists.txt @@ -3,7 +3,7 @@ set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) -find_package(Qt5 COMPONENTS Core Gui Widgets OpenGL Qml Quick QUIET) +find_package(Qt6 COMPONENTS Core Gui OpenGLWidgets Qml Quick QUIET) # QT5_ADD_RESOURCES(PROTOTYPE_RCS) if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU") @@ -11,7 +11,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU" set(DCMAKE_EXE_LINKER_FLAGS "${DCMAKE_EXE_LINKER_FLAGS} -fpic") endif() -# if (Qt5_POSITION_INDEPENDENT_CODE) +# if (Qt6_POSITION_INDEPENDENT_CODE) # SET(CMAKE_POSITION_INDEPENDENT_CODE ON) # endif() diff --git a/src/ui/qt/viewport_widget/src/CMakeLists.txt b/src/ui/qt/viewport_widget/src/CMakeLists.txt index 216f69612..65eddd60c 100644 --- a/src/ui/qt/viewport_widget/src/CMakeLists.txt +++ b/src/ui/qt/viewport_widget/src/CMakeLists.txt @@ -1,12 +1,14 @@ -project(viewport_widget VERSION 0.1.0 LANGUAGES CXX) +project(viewport_widget VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) -QT5_WRAP_CPP(VIEWPORT_OPENGL_UI_MOC_SRC +QT6_WRAP_CPP(VIEWPORT_OPENGL_UI_MOC_SRC "${ROOT_DIR}/include/xstudio/ui/qt/viewport_widget.hpp" ) -QT5_WRAP_CPP(offscreen_viewport_MOC_SRC +QT6_WRAP_CPP(offscreen_viewport_MOC_SRC "${ROOT_DIR}/include/xstudio/ui/qt/offscreen_viewport.hpp" ) +find_package(Qt6 COMPONENTS Core Quick Gui OpenGL OpenGLWidgets Concurrent REQUIRED) + # find_package(OpenGL REQUIRED) # find_package(GLEW REQUIRED) @@ -28,14 +30,14 @@ default_options_qt(${PROJECT_NAME}) target_link_libraries(${PROJECT_NAME} PUBLIC - ${CAF_LIBRARY_core} - Qt5::Widgets - Qt5::OpenGL - Qt5::Core - Qt5::Qml - Qt5::Quick + CAF::core + Qt6::OpenGLWidgets + Qt6::OpenGL + Qt6::Core + Qt6::Qml + Qt6::Quick xstudio::ui::opengl::viewport xstudio::ui::viewport xstudio::thumbnail xstudio::ui::qml::helper -) +) \ No newline at end of file diff --git a/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp b/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp index db93fdb21..ff1e1158b 100644 --- a/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp +++ b/src/ui/qt/viewport_widget/src/offscreen_viewport.cpp @@ -1,8 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 +#ifdef __apple__ +#include +#else #include #include +#endif #include +#include #include "xstudio/ui/qt/offscreen_viewport.hpp" @@ -20,97 +25,195 @@ #include #include #include +#include +#include +#include +#include +#include +#include + using namespace caf; using namespace xstudio; using namespace xstudio::ui; using namespace xstudio::ui::qt; using namespace xstudio::ui::viewport; +using namespace xstudio::ui::qml; namespace fs = std::filesystem; namespace { -static void threaded_memcpy(void *_dst, void *_src, size_t n, int n_threads) { - std::vector memcpy_threads; - size_t step = ((n / n_threads) / 4096) * 4096; +// Simple class to split large memcopy across a pool of threads. +// +// More testing needed to check when this actually benefits us and on what +// platforms, but for copying from memory mapped texture buffers into CPU +// RAM it is required for high frame-rate offscreen rendering (e.g. 4k 60Hz +// display on SDI card) +// +class ThreadedMemCopy { + public: + ThreadedMemCopy() : num_threads_(8) { + for (int i = 0; i < num_threads_; ++i) { + threads_.emplace_back(std::thread(&ThreadedMemCopy::run, this)); + } + } - uint8_t *dst = (uint8_t *)_dst; - uint8_t *src = (uint8_t *)_src; + ~ThreadedMemCopy() { - for (int i = 0; i < n_threads; ++i) { - memcpy_threads.emplace_back(memcpy, dst, src, std::min(n, step)); - dst += step; - src += step; - n -= step; - } + for (auto &t : threads_) { + // when any thread picks up an em + { + std::lock_guard lk(m); + queue.emplace_back(nullptr, nullptr, 0); + } + cv.notify_one(); + } - // ensure any threads still running to copy data to this texture are done - for (auto &t : memcpy_threads) { - if (t.joinable()) + for (auto &t : threads_) { t.join(); + } + } + + std::vector threads_; + + struct Job { + Job(void *d, void *s, size_t _n) : dst(d), src(s), n(_n) {} + Job(const Job &o) = default; + void *dst; + void *src; + size_t n; + + void do_job() { memcpy(dst, src, n); } + }; + + Job get_job() { + std::unique_lock lk(m); + if (queue.empty()) { + cv.wait(lk, [=] { return !queue.empty(); }); + } + auto rt = queue.front(); + queue.pop_front(); + return rt; } + + void do_memcpy(void *_dst, void *_src, size_t n) { + + size_t step = (((n / num_threads_) / 4096) + 1) * 4096; + + uint8_t *dst = (uint8_t *)_dst; + uint8_t *src = (uint8_t *)_src; + + while (1) { + { + std::lock_guard lk(m); + queue.emplace_back(dst, src, std::min(n, step)); + } + cv.notify_one(); + dst += step; + src += step; + if (n < step) + break; + n -= step; + } + + std::unique_lock lk(m); + if (!queue.empty()) { + cv2.wait(lk, [=] { return queue.empty(); }); + } + } + + void run() { + while (1) { + + // this blocks until there is something in queue for us + Job j = get_job(); + if (!j.dst) + break; // exit + j.do_job(); + cv2.notify_one(); + } + } + + std::mutex m; + std::condition_variable cv, cv2; + std::deque queue; + const int num_threads_; +}; + +static ThreadedMemCopy threaded_memcopy; +static std::mutex threaded_memcopy_m; + +static void threaded_memcpy(void *_dst, void *_src, size_t n) { + + std::unique_lock lk(threaded_memcopy_m); + threaded_memcopy.do_memcpy(_dst, _src, n); } -static std::map format_to_gl_tex_format = { - {viewport::ImageFormat::RGBA_8, GL_RGBA8}, - {viewport::ImageFormat::RGBA_10_10_10_2, GL_RGBA8}, - {viewport::ImageFormat::RGBA_16, GL_RGBA16}, - {viewport::ImageFormat::RGBA_16F, GL_RGBA16F}, - {viewport::ImageFormat::RGBA_32F, GL_RGBA32F}}; - -static std::map format_to_gl_pixe_type = { - {viewport::ImageFormat::RGBA_8, GL_UNSIGNED_BYTE}, - {viewport::ImageFormat::RGBA_10_10_10_2, GL_UNSIGNED_BYTE}, - {viewport::ImageFormat::RGBA_16, GL_UNSIGNED_SHORT}, - {viewport::ImageFormat::RGBA_16F, GL_HALF_FLOAT}, - {viewport::ImageFormat::RGBA_32F, GL_FLOAT}}; - -static std::map format_to_bytes_per_pixel = { - {viewport::ImageFormat::RGBA_8, 4}, - {viewport::ImageFormat::RGBA_10_10_10_2, 4}, - {viewport::ImageFormat::RGBA_16, 8}, - {viewport::ImageFormat::RGBA_16F, 8}, - {viewport::ImageFormat::RGBA_32F, 16}}; +static std::map format_to_gl_tex_format = { + {ImageFormat::RGBA_8, GL_RGBA8}, + {ImageFormat::RGBA_10_10_10_2, GL_RGBA8}, + {ImageFormat::RGBA_16, GL_RGBA16}, + {ImageFormat::RGBA_16F, GL_RGBA16F}, + {ImageFormat::RGBA_32F, GL_RGBA32F}}; + +static std::map format_to_gl_pixe_type = { + {ImageFormat::RGBA_8, GL_UNSIGNED_BYTE}, + {ImageFormat::RGBA_10_10_10_2, GL_UNSIGNED_BYTE}, + {ImageFormat::RGBA_16, GL_UNSIGNED_SHORT}, + {ImageFormat::RGBA_16F, GL_HALF_FLOAT}, + {ImageFormat::RGBA_32F, GL_FLOAT}}; + +static std::map format_to_bytes_per_pixel = { + {ImageFormat::RGBA_8, 4}, + {ImageFormat::RGBA_10_10_10_2, 4}, + {ImageFormat::RGBA_16, 8}, + {ImageFormat::RGBA_16F, 8}, + {ImageFormat::RGBA_32F, 16}}; } // namespace -OffscreenViewport::OffscreenViewport(const std::string name) : super() { +OffscreenViewport::OffscreenViewport(const std::string name, bool include_qml_overlays) + : super(), include_qml_overlays_(include_qml_overlays) { // This class is a QObject with a caf::actor 'companion' that allows it // to receive and send caf messages - here we run necessary initialisation // of the companion actor - super::init(qml::CafSystemObject::get_actor_system()); + super::init(xstudio::ui::qml::CafSystemObject::get_actor_system()); - scoped_actor sys{qml::CafSystemObject::get_actor_system()}; + scoped_actor sys{xstudio::ui::qml::CafSystemObject::get_actor_system()}; // Now we create our OpenGL xSTudio viewport - this has 'Viewport(Module)' as // its base class that provides various caf message handlers that are added // to our companion actor's 'behaviour' to create a fully functioning // viewport that can receive caf messages including framebuffers and also // to render the viewport into our GLContext - static int offscreen_idx = -1; utility::JsonStore jsn; jsn["base"] = utility::JsonStore(); - viewport_renderer_ = new Viewport( - jsn, - as_actor(), - offscreen_idx--, - ViewportRendererPtr(new opengl::OpenGLViewportRenderer(true, false)), - name); + jsn["window_id"] = name; + viewport_renderer_ = new Viewport(jsn, as_actor(), name); /* Provide a callback so the Viewport can tell this class when some property of the viewport has changed and such events can be propagated to other QT components, for example */ auto callback = [this](auto &&PH1) { receive_change_notification(std::forward(PH1)); }; - // viewport_renderer_->set_change_callback(callback); + viewport_renderer_->set_change_callback(callback); - self()->set_down_handler([=](down_msg &msg) { - if (msg.source == video_output_actor_) { - video_output_actor_ = caf::actor(); - } - }); + // join studio events, so we know when a new session has been created + auto grp = utility::request_receive( + *sys, + system().registry().template get(studio_registry), + utility::get_event_group_atom_v); + + utility::request_receive(*sys, grp, broadcast::join_broadcast_atom_v, as_actor()); + + session_actor_addr_ = actorToQString( + system(), + utility::request_receive( + *sys, + system().registry().template get(studio_registry), + session::session_atom_v)); // Here we set-up the caf message handler for this class by combining the // message handler from OpenGLViewportRenderer with our own message handlers for offscreen @@ -119,7 +222,7 @@ OffscreenViewport::OffscreenViewport(const std::string name) : super() { return viewport_renderer_->message_handler().or_else(caf::message_handler{ // insert additional message handlers here - [=](viewport::render_viewport_to_image_atom, const int width, const int height) + [=](render_viewport_to_image_atom, const int width, const int height) -> result { try { // copies a QImage to the Clipboard @@ -130,7 +233,7 @@ OffscreenViewport::OffscreenViewport(const std::string name) : super() { } }, - [=](viewport::render_viewport_to_image_atom, + [=](render_viewport_to_image_atom, const caf::uri path, const int width, const int height) -> result { @@ -142,7 +245,7 @@ OffscreenViewport::OffscreenViewport(const std::string name) : super() { } }, - [=](viewport::render_viewport_to_image_atom, + [=](render_viewport_to_image_atom, const thumbnail::THUMBNAIL_FORMAT format, const int width, const int height) -> result { @@ -153,7 +256,7 @@ OffscreenViewport::OffscreenViewport(const std::string name) : super() { } }, - [=](viewport::render_viewport_to_image_atom, + [=](render_viewport_to_image_atom, caf::actor media_actor, const int media_frame, const thumbnail::THUMBNAIL_FORMAT format, @@ -170,11 +273,64 @@ OffscreenViewport::OffscreenViewport(const std::string name) : super() { return r; }, + [=](render_viewport_to_image_atom, + caf::actor media_actor, + const int media_frame, + const int width, + const int height, + const caf::uri path) -> result { + try { + + media_reader::ImageBufPtr image = + renderMediaFrameToImage(media_actor, media_frame, width, height); + auto p = fs::path(xstudio::utility::uri_to_posix_path(path)); + std::string ext = xstudio::utility::ltrim_char( +#ifdef _WIN32 + xstudio::utility::to_upper_path(p.extension()), +#else + xstudio::utility::to_upper(p.extension()), +#endif + '.'); // yuk! + + if (ext == "EXR") { + this->exportToEXR(image, path); + } else { + this->exportToCompressedFormat(image, path, ext); + } + + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } + return true; + }, + + [=](render_viewport_to_image_atom, + caf::actor media_actor, + const timebase::flicks playhead_timepoint, + const thumbnail::THUMBNAIL_FORMAT format, + const int width, + const bool auto_scale, + const bool show_annotations) -> result { + thumbnail::ThumbnailBufferPtr r; + try { + r = renderMediaFrameToThumbnail( + media_actor, + playhead_timepoint, + format, + width, + auto_scale, + show_annotations); + } catch (std::exception &e) { + return caf::make_error(xstudio_error::error, e.what()); + } + return r; + }, + [=](video_output_actor_atom, caf::actor video_output_actor, int outputWidth, int outputHeight, - viewport::ImageFormat format) { + ImageFormat format) { video_output_actor_ = video_output_actor; vid_out_width_ = outputWidth; vid_out_height_ = outputHeight; @@ -185,10 +341,66 @@ OffscreenViewport::OffscreenViewport(const std::string name) : super() { video_output_actor_ = video_output_actor; }, - [=](render_viewport_to_image_atom) { + [=](render_viewport_to_image_atom, const utility::time_point &tp) { // force a redraw - receive_change_notification(Viewport::ChangeCallbackId::Redraw); - } + if (video_output_actor_) { + + if (last_rendered_frame_ && !viewport_renderer_->playing()) { + // no need to re-render if Redraw callback hasn't + // arrived since we last rendered + anon_mail(last_rendered_frame_).send(video_output_actor_); + + } else { + + // we store the image buffers that we have rendered into. WHy? + // Because we can re-use them and since vid_out_width_ and + // vid_out_height_ don't change (much) we don't need to re-allocate + // image buffers. + // + // We use the use_count() to check if a buffer can be re-used. + // + // Note - last_rendered_frame_ might be reset again (via + // recieve_change_callback) during call to renderToImageBuffer so I + // don't use it directly here + media_reader::ImageBufPtr new_frame; + for (auto &buf : output_buffers_) { + if (buf.use_count() == 1) { + new_frame = buf; + break; + } + } + if (!new_frame) { + new_frame.reset(new media_reader::ImageBuffer()); + output_buffers_.push_back(new_frame); + } + + try { + renderToImageBuffer( + vid_out_width_, + vid_out_height_, + new_frame, + vid_out_format_, + false, + tp); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + anon_mail(new_frame).send(video_output_actor_); + last_rendered_frame_ = new_frame; + } + } + }, + + // event coming from session actor + [=](utility::event_atom, session::session_atom, caf::actor session) { + session_actor_addr_ = actorToQString(system(), session); + }, + + // event coming from session actor (ignore) + [=](utility::event_atom, + session::session_request_atom, + const std::string &path, + const utility::JsonStore &js) {} }); }); @@ -198,21 +410,38 @@ OffscreenViewport::OffscreenViewport(const std::string name) : super() { OffscreenViewport::~OffscreenViewport() { +} + +void OffscreenViewport::cleanup() { + + // cleanup is called by our thread on completion, so we can delete + // ouselves whilst still in the Thread. Qt doesn't let us kill object + // living in one thread from another thread. + // gl context must be current for cleanup gl_context_->makeCurrent(surface_); + if (render_control_) + render_control_->invalidate(); delete viewport_renderer_; - glDeleteTextures(1, &texId_); - glDeleteFramebuffers(1, &fboId_); - glDeleteTextures(1, &depth_texId_); + if (texId_) { + glDeleteTextures(1, &texId_); + glDeleteFramebuffers(1, &fboId_); + glDeleteTextures(1, &depth_texId_); + } + + // teardown the QML gubbins + delete render_control_; + delete root_qml_overlays_item_; + delete qml_component_; + delete helper_; + delete quick_win_; + delete qml_engine_; delete gl_context_; delete surface_; video_output_actor_ = caf::actor(); -} - - -void OffscreenViewport::autoDelete() { delete this; } +} void OffscreenViewport::initGL() { @@ -229,28 +458,52 @@ void OffscreenViewport::initGL() { gl_context_ = new QOpenGLContext(nullptr); // m_window->openglContext(); gl_context_->setFormat(format); if (!gl_context_) - throw std::runtime_error( - "OffscreenViewport::initGL - could not create QOpenGLContext."); + throw std::runtime_error("OffscreeninitGL - could not create QOpenGLContext."); if (!gl_context_->create()) { - throw std::runtime_error("OffscreenViewport::initGL - failed to creat GL Context " + throw std::runtime_error("OffscreeninitGL - failed to creat GL Context " "for offscreen rendering."); } + // This offscreen viewport runs in its own thread + thread_ = new QThread(); + // we also require a QSurface to use the GL context surface_ = new QOffscreenSurface(nullptr, nullptr); surface_->setFormat(format); surface_->create(); - // gl_context_->makeCurrent(surface_); + // Here we set-up the gubbins necessary for rendering QML graphics + // into the viewport + render_control_ = new QQuickRenderControl(); + quick_win_ = new QQuickWindow(render_control_); + qml_engine_ = new QQmlEngine; + if (!qml_engine_->incubationController()) + qml_engine_->setIncubationController(quick_win_->incubationController()); + qml_engine_->addImportPath("qrc:///"); + qml_engine_->addImportPath("qrc:///extern"); - // we also require a QSurface to use the GL context - surface_ = new QOffscreenSurface(nullptr, nullptr); - surface_->setFormat(format); - surface_->create(); + connect(render_control_, SIGNAL(sceneChanged()), this, SLOT(sceneChanged())); + connect(render_control_, SIGNAL(renderRequested()), this, SLOT(sceneChanged())); + + // gui plugins.. + qml_engine_->addImportPath(QStringFromStd(utility::xstudio_plugin_dir("/qml"))); + qml_engine_->addPluginPath(QStringFromStd(utility::xstudio_plugin_dir(""))); - thread_ = new QThread(); gl_context_->moveToThread(thread_); + qml_engine_->moveToThread(thread_); + render_control_->moveToThread(thread_); moveToThread(thread_); + render_control_->prepareThread(thread_); + + connect( + quick_win_, + &QQuickWindow::beforeRenderPassRecording, + this, + &OffscreenViewport::renderViewportUnderQML, + Qt::DirectConnection); + + quick_win_->setColor(QColor(0,1,0,0)); + thread_->start(); // Note - the only way I seem to be able to 'cleanly' exit is @@ -259,7 +512,8 @@ void OffscreenViewport::initGL() { // to destroy thread_ ... calling deleteLater() directly or // using finished signal has no effect. - connect(thread_, SIGNAL(finished()), this, SLOT(autoDelete())); + connect(thread_, &QThread::finished, thread_, &QThread::deleteLater); + connect(thread_, &QThread::finished, this, &OffscreenViewport::cleanup); // this has no effect! // connect(thread_, SIGNAL(finished()), this, SLOT(deleteLater())); @@ -269,41 +523,48 @@ void OffscreenViewport::initGL() { void OffscreenViewport::stop() { thread_->quit(); thread_->wait(); + delete thread_; } +void OffscreenViewport::sceneChanged() { last_rendered_frame_.reset(); } + void OffscreenViewport::renderSnapshot(const int width, const int height, const caf::uri path) { - // initGL(); + initGL(); // temp hack - put in a 500ms delay so the playhead can update the // annotations plugin with the annotations data. // std::this_thread::sleep_for(std::chrono::milliseconds(500)); - if (path.empty()) { - throw std::runtime_error("Invalid (empty) file path."); - } if (width <= 0 || height <= 0) { throw std::runtime_error("Invalid image dimensions."); } media_reader::ImageBufPtr image(new media_reader::ImageBuffer()); - renderToImageBuffer(width, height, image, viewport::ImageFormat::RGBA_16F); - auto p = fs::path(xstudio::utility::uri_to_posix_path(path)); + renderToImageBuffer(width, height, image, ImageFormat::RGBA_16F, true); + + if (path.empty()) { + // we can call this with empty path - image is copied to clipboard + this->exportToCompressedFormat(image, path, ""); + } else { - std::string ext = xstudio::utility::ltrim_char( + auto p = fs::path(xstudio::utility::uri_to_posix_path(path)); + + std::string ext = xstudio::utility::ltrim_char( #ifdef _WIN32 - xstudio::utility::to_upper_path(p.extension()), + xstudio::utility::to_upper_path(p.extension()), #else - xstudio::utility::to_upper(p.extension()), + xstudio::utility::to_upper(p.extension()), #endif - '.'); // yuk! + '.'); // yuk! - if (ext == "EXR") { - this->exportToEXR(image, path); - } else { - this->exportToCompressedFormat(image, path, ext); + if (ext == "EXR") { + this->exportToEXR(image, path); + } else { + this->exportToCompressedFormat(image, path, ext); + } } } @@ -349,7 +610,13 @@ void OffscreenViewport::exportToEXR(const media_reader::ImageBufPtr &buf, const header.dataWindow() = header.displayWindow() = box; header.compression() = Imf::PIZ_COMPRESSION; Imf::RgbaOutputFile outFile(utility::uri_to_posix_path(path).c_str(), header); - outFile.setFrameBuffer((Imf::Rgba *)buf->buffer(), 1, dim.x); + Imf::Rgba *bptr = (Imf::Rgba *)buf->buffer(); + bptr += (dim.y - 1) * dim.x; // move to final scanline + outFile.setFrameBuffer( + bptr, + 1, // pix stride + -dim.x // line stride (i.e.) step backwards through buffer + ); outFile.writePixels(dim.y); } @@ -378,15 +645,15 @@ void OffscreenViewport::exportToCompressedFormat( } } - QApplication::clipboard()->setImage(im, QClipboard::Clipboard); - /*int compLevel = ext == "TIF" || ext == "TIFF" ? std::max(compression, 1) : (10 - compression) * 10;*/ // TODO : check m_filePath for extension, if not, add to it. Do it on QML side after merging // with new UI branch - if (path.empty()) + if (path.empty()) { + QApplication::clipboard()->setImage(im, QClipboard::Clipboard); return; + } QImageWriter writer(xstudio::utility::uri_to_posix_path(path).c_str()); // writer.setCompression(compLevel); @@ -395,8 +662,28 @@ void OffscreenViewport::exportToCompressedFormat( } } -void OffscreenViewport::setupTextureAndFrameBuffer( - const int width, const int height, const viewport::ImageFormat format) { +void OffscreenViewport::renderViewportUnderQML() { + + quick_win_->beginExternalCommands(); + + glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS); + + viewport_renderer_->init(); + + if (image_to_render_) { + viewport_renderer_->render(image_to_render_); + } else { + viewport_renderer_->render(); + } + + glPopClientAttrib(); + + quick_win_->endExternalCommands(); + +} + +bool OffscreenViewport::setupTextureAndFrameBuffer( + const int width, const int height, const ImageFormat format) { if (tex_width_ == width && tex_height_ == height && format == vid_out_format_) { // bind framebuffer @@ -404,7 +691,7 @@ void OffscreenViewport::setupTextureAndFrameBuffer( glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texId_, 0); glFramebufferTexture2D( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depth_texId_, 0); - return; + return false; } if (texId_) { @@ -417,10 +704,6 @@ void OffscreenViewport::setupTextureAndFrameBuffer( tex_height_ = height; vid_out_format_ = format; - utility::JsonStore j; - j["pack_rgb_10_bit"] = format == viewport::RGBA_10_10_10_2; - viewport_renderer_->set_aux_shader_uniforms(j); - // create texture glGenTextures(1, &texId_); glBindTexture(GL_TEXTURE_2D, texId_); @@ -487,62 +770,224 @@ void OffscreenViewport::setupTextureAndFrameBuffer( glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texId_, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depth_texId_, 0); + + return true; +} + +bool OffscreenViewport::loadQMLOverlays() { + + if (overlays_loaded_) + return bool(root_qml_overlays_item_); + + overlays_loaded_ = true; + + qml_component_ = + new QQmlComponent(qml_engine_, "qrc:/views/viewport/XsOffscreenViewportOverlays.qml"); + qml_component_->moveToThread(thread_); + + if (qml_component_->isError()) { + const QList errorList = qml_component_->errors(); + for (const QQmlError &error : errorList) + qWarning() << error.url() << error.line() << error; + return false; + ; + } + + QObject *rootObject = qml_component_->create(); + if (qml_component_->isError()) { + const QList errorList = qml_component_->errors(); + for (const QQmlError &error : errorList) + qWarning() << error.url() << error.line() << error; + return false; + ; + } + + root_qml_overlays_item_ = qobject_cast(rootObject); + if (!root_qml_overlays_item_) { + qWarning("run: Not a QQuickItem"); + delete rootObject; + return false; + } + + // The root item is ready. Associate it with the window. + root_qml_overlays_item_->setParentItem(quick_win_->contentItem()); + + quick_win_->setColor(QColor(1, 1, 0, 1)); + + quick_win_->setGraphicsDevice( + QQuickGraphicsDevice::fromOpenGLContext(gl_context_) + ); + + render_control_->initialize(); + + helper_ = new qml::Helpers(qml_engine_, this); + helper_->moveToThread(thread_); + + QVariant v; + v.fromValue((QObject *)&helper_); + root_qml_overlays_item_->setProperty("helpers", v); + + root_qml_overlays_item_->setProperty( + "name", qml::QStringFromStd(viewport_renderer_->name())); + + // Update item and rendering related geometries. + return true; } void OffscreenViewport::renderToImageBuffer( const int w, const int h, - media_reader::ImageBufPtr &image, - const viewport::ImageFormat format) { + media_reader::ImageBufPtr &destination_image, + const ImageFormat format, + const bool sync_fetch_playhead_image, + const utility::time_point &tp, + const media_reader::ImageBufPtr &image_to_use) { auto t0 = utility::clock::now(); // ensure our GLContext is current - gl_context_->makeCurrent(surface_); - if (!gl_context_->isValid()) { - throw std::runtime_error( - "OffscreenViewport::renderToImageBuffer - GL Context is not valid."); + if (!gl_context_->makeCurrent(surface_) || !gl_context_->isValid()) { + throw std::runtime_error("OffscreenrenderToImageBuffer - GL Context is not valid."); } - setupTextureAndFrameBuffer(w, h, format); + // No QML .. much simpler. Just set-up and render our xstudio viewport + + glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS); // intialises shaders and textures where necessary viewport_renderer_->init(); + const bool updateTarget =setupTextureAndFrameBuffer(w, h, format); + auto t1 = utility::clock::now(); - // Clearup before render, probably useless for a new buffer - // glClearColor(0.0, 1.0, 0.0, 0.0); - // glClear(GL_COLOR_BUFFER_BIT); + glPopClientAttrib(); - glViewport(0, 0, w, h); // This essential call tells the viewport renderer how to project the // viewport area into the glViewport window. - viewport_renderer_->set_scene_coordinates( - Imath::V2f(0.0f, 0.0), - Imath::V2f(w, 0.0), - Imath::V2f(w, h), - Imath::V2f(0.0f, h), - Imath::V2i(w, h), - 1.0f); + viewport_renderer_->set_geometry( + 0.0f, // x offset + 0.0f, // y offset + w, // viewport width in window + h, // viewport height in window + w, // window width + h, // window height, + 1.0f // pixel scaling (high DPI support) + ); + + if (include_qml_overlays_ && loadQMLOverlays()) { + + // If we are rendering a supplied image, store it for when we do + // the render, or we call 'prepare_render_data' which pre-fetches the image + // buffer from the playhead attached to the viewport and stores. This + // is all handled by the Viewport instance + if (image_to_use) { + image_to_render_ = image_to_use; + } else { + if (sync_fetch_playhead_image) { + viewport_renderer_->prepare_render_data(utility::clock::now(), true); + } else if (tp != utility::time_point()) { + viewport_renderer_->prepare_render_data(tp); + } else { + viewport_renderer_->prepare_render_data(); + } + } + + glActiveTexture(GL_TEXTURE0); + + // now do some set-up for QML engine + if (updateTarget) { + quick_win_->setRenderTarget(QQuickRenderTarget::fromOpenGLTexture(texId_, QSize(w, h))); + } + + root_qml_overlays_item_->setWidth(w); + root_qml_overlays_item_->setHeight(h); + + // convert the image boundary in the viewport into plain pixels + const std::vector image_boxes = + viewport_renderer_->image_bounds_in_viewport_pixels(); + QVariantList v; + for (const auto &box : image_boxes) { + QRectF imageBoundsInViewportPixels( + box.min.x, box.min.y, box.max.x - box.min.x, box.max.y - box.min.y); + v.append(imageBoundsInViewportPixels); + } + + + // these properties on XsOffscreenViewportOverlays mirror the same + // properties provided by XsViewport - some overlay/HUD QML items access + // these properties so they know how to compute their geometrty in + // the QML coordinates to overlay the xSTUDIO image. + root_qml_overlays_item_->setProperty("imageBoundariesInViewport", v); + + const std::vector resolutions = viewport_renderer_->image_resolutions(); + QVariantList rs; + for (const auto &r : resolutions) { + rs.append(QSize(r.x, r.y)); + } + root_qml_overlays_item_->setProperty("imageResolutions", rs); + + root_qml_overlays_item_->setProperty("sessionActorAddr", session_actor_addr_); + quick_win_->setWidth(w); + quick_win_->setHeight(h); + quick_win_->setGeometry(0, 0, w, h); + + auto t2 = utility::clock::now(); + + render_control_->polishItems(); + render_control_->beginFrame(); + render_control_->sync(); + + // note we have a signal/slot connection that causes renderViewportUnderQML + // to be called at the right moment so the xstudio Viewport can be drawn before + // the QML is rendered + render_control_->render(); + render_control_->endFrame(); + + } else { + + // Clearup before render, probably useless for a new buffer + glViewport(0, 0, w, h); + + if (image_to_use) { + viewport_renderer_->render(image_to_use); + } else { + if (sync_fetch_playhead_image) { + viewport_renderer_->prepare_render_data(utility::clock::now(), true); + } else if (tp != utility::time_point()) { + viewport_renderer_->prepare_render_data(tp); + } else { + viewport_renderer_->prepare_render_data(); + } + viewport_renderer_->render(); + } + + auto t2 = utility::clock::now(); + + glActiveTexture(GL_TEXTURE0); + + } + + glFlush(); - viewport_renderer_->render(); + + auto t3 = utility::clock::now(); // Not sure if this is necessary // glFinish(); - auto t2 = utility::clock::now(); // unbind glBindFramebuffer(GL_FRAMEBUFFER, 0); + size_t pix_buf_size = w * h * format_to_bytes_per_pixel[vid_out_format_]; // init RGBA float array - image->allocate(pix_buf_size); - image->set_image_dimensions(Imath::V2i(w, h)); - image.when_to_display_ = utility::clock::now(); - image->params()["pixel_format"] = (int)format; + destination_image->allocate(pix_buf_size); + destination_image->set_image_dimensions(Imath::V2i(w, h)); + destination_image.when_to_display_ = utility::clock::now(); + destination_image->params()["pixel_format"] = (int)format; if (!pixel_buffer_object_) { glGenBuffers(1, &pixel_buffer_object_); @@ -557,24 +1002,27 @@ void OffscreenViewport::renderToImageBuffer( glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texId_); + int skip_rows, skip_pixels, row_length, alignment; + glGetIntegerv(GL_PACK_SKIP_ROWS, &skip_rows); + glGetIntegerv(GL_PACK_SKIP_PIXELS, &skip_pixels); + glGetIntegerv(GL_PACK_ROW_LENGTH, &row_length); + glGetIntegerv(GL_PACK_ALIGNMENT, &alignment); + glPixelStorei(GL_PACK_SKIP_ROWS, 0); glPixelStorei(GL_PACK_SKIP_PIXELS, 0); glPixelStorei(GL_PACK_ROW_LENGTH, w); glPixelStorei(GL_PACK_ALIGNMENT, 1); - auto t3 = utility::clock::now(); - glBindFramebuffer(GL_FRAMEBUFFER, 0); glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, format_to_gl_pixe_type[vid_out_format_], nullptr); - glBindBuffer(GL_PIXEL_PACK_BUFFER, pixel_buffer_object_); void *mappedBuffer = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY); auto t4 = utility::clock::now(); - threaded_memcpy(image->buffer(), mappedBuffer, pix_buf_size, 8); + threaded_memcpy(destination_image->buffer(), mappedBuffer, pix_buf_size); // now mapped buffer contains the pixel data @@ -582,39 +1030,31 @@ void OffscreenViewport::renderToImageBuffer( auto t5 = utility::clock::now(); auto tt = utility::clock::now(); - /*std::cerr << "glBindBuffer " - << std::chrono::duration_cast(t1-t0).count() << " " - << std::chrono::duration_cast(t2-t1).count() << " " - << std::chrono::duration_cast(t3-t2).count() << " " - << std::chrono::duration_cast(t4-t3).count() << " " - << std::chrono::duration_cast(t5-t4).count() << " : " - << std::chrono::duration_cast(t5-t0).count() << "\n";*/ -} + // TODO: Gather stats on draw times etc and send to video_output_actor_ + // so it can monitor performance -void OffscreenViewport::receive_change_notification(Viewport::ChangeCallbackId id) { + /*std::cerr << "Draw time " << + std::chrono::duration_cast(t2-t1).count() << "\n"; std::cerr << + "Overlays time " << std::chrono::duration_cast(t3-t2).count() << + "\n"; std::cerr << "Map buffer time " << + std::chrono::duration_cast(t4-t3).count() << "\n"; std::cerr << + "Copy buffer time " << std::chrono::duration_cast(t5-t4).count() + << "\n";*/ - if (id == Viewport::ChangeCallbackId::Redraw) { + glBindTexture(GL_TEXTURE_2D, 0); - if (video_output_actor_) { + glPixelStorei(GL_PACK_SKIP_ROWS, skip_rows); + glPixelStorei(GL_PACK_SKIP_PIXELS, skip_pixels); + glPixelStorei(GL_PACK_ROW_LENGTH, row_length); + glPixelStorei(GL_PACK_ALIGNMENT, alignment); +} - std::vector output_buffers_; - media_reader::ImageBufPtr ready_buf; - for (auto &buf : output_buffers_) { - if (buf.use_count() == 1) { - ready_buf = buf; - break; - } - } - if (!ready_buf) { - ready_buf.reset(new media_reader::ImageBuffer()); - output_buffers_.push_back(ready_buf); - } +void OffscreenViewport::receive_change_notification(Viewport::ChangeCallbackId id) { - renderToImageBuffer(vid_out_width_, vid_out_height_, ready_buf, vid_out_format_); - anon_send(video_output_actor_, ready_buf); - } - } + // something has changed that will affect the rendered output. clear + // last_rendered_frame_ + last_rendered_frame_.reset(); } void OffscreenViewport::make_conversion_lut() { @@ -698,13 +1138,13 @@ thumbnail::ThumbnailBufferPtr OffscreenViewport::renderToThumbnail( throw std::runtime_error(err.c_str()); } - float effective_image_height = float(image_dims.y) / image->pixel_aspect(); + float effective_image_height = float(image_dims.y) / image.frame_id().pixel_aspect(); if (width <= 0 || auto_scale) { - viewport_renderer_->set_fit_mode(viewport::FitMode::One2One); + viewport_renderer_->set_fit_mode(FitMode::One2One); return renderToThumbnail(format, image_dims.x, int(round(effective_image_height))); } else { - viewport_renderer_->set_fit_mode(viewport::FitMode::Best); + viewport_renderer_->set_fit_mode(FitMode::Best); return renderToThumbnail( format, width, int(round(width * effective_image_height / image_dims.x))); } @@ -712,13 +1152,52 @@ thumbnail::ThumbnailBufferPtr OffscreenViewport::renderToThumbnail( thumbnail::ThumbnailBufferPtr OffscreenViewport::renderToThumbnail( const thumbnail::THUMBNAIL_FORMAT format, const int width, const int height) { - media_reader::ImageBufPtr image(new media_reader::ImageBuffer()); - renderToImageBuffer(width, height, image, viewport::ImageFormat::RGBA_16F); + + media_reader::ImageBufPtr image = renderToImageBuf(width, height); thumbnail::ThumbnailBufferPtr r = rgb96thumbFromHalfFloatImage(image); r->convert_to(format); return r; } +media_reader::ImageBufPtr +OffscreenViewport::renderToImageBuf(const int width, const int height) { + + media_reader::ImageBufPtr image2 = viewport_renderer_->get_onscreen_image(); + if (!image2) { + std::string err(fmt::format( + "{} Failed to pull images to offscreen renderer.", __PRETTY_FUNCTION__)); + throw std::runtime_error(err.c_str()); + } + media_reader::ImageBufPtr image(new media_reader::ImageBuffer()); + renderToImageBuffer( + width, height, image, ImageFormat::RGBA_16F, true, utility::clock::now(), image2); + return image; +} + +media_reader::ImageBufPtr OffscreenViewport::renderMediaFrameToImage( + caf::actor media_actor, const int media_frame, const int width, const int height) { + + if (!local_playhead_) { + auto a = caf::actor_cast(as_actor()); + local_playhead_ = + a->spawn("Offscreen Viewport Local Playhead"); + + a->link_to(local_playhead_); + } + // first, set the local playhead to be our image source + viewport_renderer_->set_playhead(local_playhead_); + + scoped_actor sys{as_actor()->home_system()}; + + // now set the media source on the local playhead + utility::request_receive( + *sys, local_playhead_, playhead::source_atom_v, std::vector({media_actor})); + + // now move the playhead to requested frame + utility::request_receive(*sys, local_playhead_, playhead::jump_atom_v, media_frame); + + return renderToImageBuf(width, height); +} thumbnail::ThumbnailBufferPtr OffscreenViewport::renderMediaFrameToThumbnail( caf::actor media_actor, @@ -727,10 +1206,12 @@ thumbnail::ThumbnailBufferPtr OffscreenViewport::renderMediaFrameToThumbnail( const int width, const bool auto_scale, const bool show_annotations) { + if (!local_playhead_) { auto a = caf::actor_cast(as_actor()); local_playhead_ = a->spawn("Offscreen Viewport Local Playhead"); + a->link_to(local_playhead_); } // first, set the local playhead to be our image source @@ -745,5 +1226,35 @@ thumbnail::ThumbnailBufferPtr OffscreenViewport::renderMediaFrameToThumbnail( // now move the playhead to requested frame utility::request_receive(*sys, local_playhead_, playhead::jump_atom_v, media_frame); - return renderToThumbnail(format, auto_scale, show_annotations); + return renderToThumbnail(format, width, auto_scale, show_annotations); +} + +thumbnail::ThumbnailBufferPtr OffscreenViewport::renderMediaFrameToThumbnail( + caf::actor media_actor, + const timebase::flicks playhead_position_flicks, + const thumbnail::THUMBNAIL_FORMAT format, + const int width, + const bool auto_scale, + const bool show_annotations) { + if (!local_playhead_) { + auto a = caf::actor_cast(as_actor()); + local_playhead_ = + a->spawn("Offscreen Viewport Local Playhead"); + a->link_to(local_playhead_); + } + + // first, set the local playhead to be our image source + viewport_renderer_->set_playhead(local_playhead_); + + scoped_actor sys{as_actor()->home_system()}; + + // now set the media source on the local playhead + utility::request_receive( + *sys, local_playhead_, playhead::source_atom_v, std::vector({media_actor})); + + // now move the playhead to requested frame + utility::request_receive( + *sys, local_playhead_, playhead::jump_atom_v, playhead_position_flicks); + + return renderToThumbnail(format, width, auto_scale, show_annotations); } diff --git a/src/ui/qt/viewport_widget/src/viewport_widget.cpp b/src/ui/qt/viewport_widget/src/viewport_widget.cpp index 6d0cb12b4..a7745da08 100644 --- a/src/ui/qt/viewport_widget/src/viewport_widget.cpp +++ b/src/ui/qt/viewport_widget/src/viewport_widget.cpp @@ -1,10 +1,59 @@ // SPDX-License-Identifier: Apache-2.0 #include "xstudio/media_reader/media_reader.hpp" #include "xstudio/ui/qt/viewport_widget.hpp" +#include "xstudio/ui/qml/helper_ui.hpp" +#include + +CAF_PUSH_WARNINGS +#include +#include +CAF_POP_WARNINGS using namespace xstudio::ui::qt; +using namespace xstudio::ui::qml; +using namespace xstudio::ui; + +namespace { + +int qtModifierToOurs(const Qt::KeyboardModifiers qt_modifiers) { + int result = Signature::Modifier::NoModifier; + + if (qt_modifiers & Qt::ShiftModifier) + result |= Signature::Modifier::ShiftModifier; + if (qt_modifiers & Qt::ControlModifier) + result |= Signature::Modifier::ControlModifier; + if (qt_modifiers & Qt::AltModifier) + result |= Signature::Modifier::AltModifier; + if (qt_modifiers & Qt::MetaModifier) + result |= Signature::Modifier::MetaModifier; + if (qt_modifiers & Qt::KeypadModifier) + result |= Signature::Modifier::KeypadModifier; + if (qt_modifiers & Qt::GroupSwitchModifier) + result |= Signature::Modifier::GroupSwitchModifier; -ViewportGLWidget::ViewportGLWidget(QWidget *parent) : super(parent) {} + return result; +} + +} // namespace + +ViewportGLWidget::ViewportGLWidget( + QWidget *parent, + const bool live_viewport, + const QString window_name, + const QString viewport_name) + : super(parent), + live_viewport_(live_viewport), + window_name_(StdFromQString(window_name)), + viewport_name_(StdFromQString(viewport_name)) { + + if (live_viewport) { + QObject::connect( + this, &QOpenGLWidget::frameSwapped, this, &ViewportGLWidget::frameBufferSwapped); + setFocusPolicy(Qt::StrongFocus); + } +} + +ViewportGLWidget::~ViewportGLWidget() { keypress_monitor_ = caf::actor(); } void ViewportGLWidget::set_playhead(caf::actor playhead) { if (the_viewport_) @@ -14,18 +63,48 @@ void ViewportGLWidget::set_playhead(caf::actor playhead) { void ViewportGLWidget::initializeGL() { the_viewport_->init(); } void ViewportGLWidget::resizeGL(int w, int h) { - anon_send( - self(), + + anon_mail( ui::viewport::viewport_set_scene_coordinates_atom_v, - Imath::V2f(0, 0), - Imath::V2f(w, 0), - Imath::V2f(w, h), - Imath::V2f(0, h), - Imath::V2i(w, h), - 1.0f); + 0.0f, + 0.0f, + float(w), + float(h), + float(w), + float(h), + 1.0f) + .send(self()); +} + +void ViewportGLWidget::paintGL() { + + if (live_viewport_) { + // if we're NOT a live viewport (e.g. offscreen viewport for rendering + // thumbnails/snapshots) then prepare_render_data has already been + // called with the appropriate image buffer etc. For a live viewport + // e.g. in an embedded UI we must call prepare_render_data before each + // redraw to fetch the image(s) from the playhead that will be drawn + the_viewport_->prepare_render_data(); + } + the_viewport_->render(); + + // during playback we request redraw in a tight loop. The reason is that to + // achieve frame accurate sync with correct pulldown to the system video + // refresh beat xSTUDIO needs to perform a redraw on every video refresh + if (the_viewport_->playing()) + update(); +} + +void ViewportGLWidget::frameBufferSwapped() { + if (live_viewport_) { + // this call is crucial for stable playback - we tell the playback engine + // when the last image was put on the screen so it can infer the video + // refresh beat and work out which image should be put on screen at the + // next redraw + the_viewport_->framebuffer_swapped(utility::clock::now()); + } } -void ViewportGLWidget::paintGL() { the_viewport_->render(); } void ViewportGLWidget::receive_change_notification(viewport::Viewport::ChangeCallbackId) { update(); @@ -35,16 +114,11 @@ void ViewportGLWidget::init(caf::actor_system &system) { super::init(system); - self()->set_default_handler(caf::drop); - utility::JsonStore jsn; - jsn["base"] = utility::JsonStore(); + jsn["base"] = utility::JsonStore(); + jsn["window_id"] = window_name_; - the_viewport_.reset(new ui::viewport::Viewport( - jsn, - as_actor(), - true, - viewport::ViewportRendererPtr(new opengl::OpenGLViewportRenderer(true, false)))); + the_viewport_.reset(new ui::viewport::Viewport(jsn, as_actor())); auto callback = [this](auto &&PH1) { receive_change_notification(std::forward(PH1)); @@ -55,4 +129,162 @@ void ViewportGLWidget::init(caf::actor_system &system) { set_message_handler([=](caf::actor_companion * /*self*/) -> caf::message_handler { return the_viewport_->message_handler(); }); + + keypress_monitor_ = system.registry().template get(keyboard_events); +} + +QString ViewportGLWidget::name() { return QString::fromUtf8(the_viewport_->name().c_str()); } + +void ViewportGLWidget::mousePressEvent(QMouseEvent *event) { + + sendPointerEvent(EventType::ButtonDown, event); +} + +void ViewportGLWidget::mouseReleaseEvent(QMouseEvent *event) { + + sendPointerEvent(EventType::ButtonRelease, event); +} + +void ViewportGLWidget::mouseMoveEvent(QMouseEvent *event) { + + sendPointerEvent(event->buttons() ? EventType::Drag : EventType::Move, event, 0); +} + +void ViewportGLWidget::mouseDoubleClickEvent(QMouseEvent *event) { + + sendPointerEvent(EventType::DoubleClick, event); +} + +void ViewportGLWidget::keyPressEvent(QKeyEvent *key_event) { + + anon_mail( + ui::keypress_monitor::text_entry_atom_v, + StdFromQString(key_event->text()), + the_viewport_->name(), + window_name_) + .send(keypress_monitor_); +} + +void ViewportGLWidget::keyReleaseEvent(QKeyEvent *key_event) { + + if (!key_event->isAutoRepeat()) { + anon_mail( + ui::keypress_monitor::key_up_atom_v, + key_event->key(), + the_viewport_->name(), + window_name_) + .send(keypress_monitor_); + } +} + +bool ViewportGLWidget::event(QEvent *event) { + + if (event->type() == QEvent::KeyPress) { + + auto key_event = dynamic_cast(event); + if (key_event) { + anon_mail( + ui::keypress_monitor::key_down_atom_v, + key_event->key(), + the_viewport_->name(), + window_name_, + key_event->isAutoRepeat()) + .send(keypress_monitor_); + } + } else if (event->type() == QEvent::KeyRelease) { + + auto key_event = dynamic_cast(event); + if (key_event && !key_event->isAutoRepeat()) { + anon_mail( + ui::keypress_monitor::key_up_atom_v, + key_event->key(), + the_viewport_->name(), + window_name_) + .send(keypress_monitor_); + } + } else if ( + event->type() == QEvent::Leave || event->type() == QEvent::HoverLeave || + event->type() == QEvent::DragLeave || event->type() == QEvent::GraphicsSceneDragLeave || + event->type() == QEvent::GraphicsSceneHoverLeave) { + // It's possible to receive a KeyPress but not key release if the mouse + // leaves the window/widget before the user releases the key. This is + // a headache for us because we need to track key pressed state, not + // only key down. We have to assume all keys are released (even if they + // aren't) when we the mouse leaves the focus as we won't get the + // mouse release event. It doesn't matter if we have false negatives + // (in terms of whether a key is held down) but a false positive + // (i.e. we think a key is down when it isn't) could really mess up the + // UI behaviour + anon_mail(ui::keypress_monitor::all_keys_up_atom_v, the_viewport_->name()) + .send(keypress_monitor_); + } + + return super::event(event); +} + +void ViewportGLWidget::wheelEvent(QWheelEvent *event) { + + // make a mouse wheel event and pass to viewport to process + PointerEvent ev( + EventType::MouseWheel, + static_cast((int)event->buttons()), + event->position().x(), + event->position().y(), + width(), // FIXME should be width, but this function appears to never be called. + height(), // FIXME should be height + qtModifierToOurs(event->modifiers()), + the_viewport_->name(), + std::make_pair(event->angleDelta().rx(), event->angleDelta().ry()), + std::make_pair(event->pixelDelta().rx(), event->pixelDelta().ry())); + + if (!the_viewport_->process_pointer_event(ev) && the_viewport_->playhead()) { + // If viewport hasn't acted on the mouse wheel event (because user pref + // to zoom with mouse wheel is false), assume that we can instead use it + // to step the playhead + anon_mail(playhead::play_atom_v, false).send(the_viewport_->playhead()); + anon_mail(playhead::step_atom_v, event->angleDelta().ry() > 0 ? 1 : -1) + .send(the_viewport_->playhead()); + } + + QOpenGLWidget::wheelEvent(event); +} + +void ViewportGLWidget::sendPointerEvent(EventType t, QMouseEvent *event, int force_modifiers) { + + PointerEvent p( + t, + static_cast((int)event->buttons()), + event->position().x(), + event->position().y(), + width(), + height(), + qtModifierToOurs(event->modifiers()) + force_modifiers, + the_viewport_->name()); + p.w_ = utility::clock::now(); + + if (the_viewport_->process_pointer_event(p)) { + update(); + } else { + anon_mail(ui::keypress_monitor::mouse_event_atom_v, p).send(keypress_monitor_); + } +} + +void ViewportGLWidget::sendPointerEvent(QHoverEvent *event) { + + PointerEvent p( + EventType::Move, + static_cast(0), + event->position().x(), + event->position().y(), + width(), + height(), + Signature::Modifier::NoModifier, + the_viewport_->name()); + p.w_ = utility::clock::now(); + + if (the_viewport_->process_pointer_event(p)) { + update(); + } else { + anon_mail(ui::keypress_monitor::mouse_event_atom_v, p).send(keypress_monitor_); + } } diff --git a/src/ui/viewport/src/CMakeLists.txt b/src/ui/viewport/src/CMakeLists.txt index f641f4fb4..05d309131 100644 --- a/src/ui/viewport/src/CMakeLists.txt +++ b/src/ui/viewport/src/CMakeLists.txt @@ -1,14 +1,15 @@ -project(viewport_ui VERSION 0.1.0 LANGUAGES CXX) +project(viewport_ui VERSION ${XSTUDIO_GLOBAL_VERSION} LANGUAGES CXX) find_package(OpenEXR) find_package(Imath) set(SOURCES - hud_plugin.cpp viewport.cpp viewport_frame_queue_actor.cpp fps_monitor.cpp keypress_monitor.cpp + video_output_plugin.cpp + viewport_layout_plugin.cpp ) add_library(${PROJECT_NAME} SHARED ${SOURCES}) @@ -18,13 +19,14 @@ default_options(${PROJECT_NAME}) target_link_libraries(${PROJECT_NAME} PUBLIC + xstudio::audio_output xstudio::module xstudio::plugin_manager xstudio::utility xstudio::playhead OpenEXR::OpenEXR Imath::Imath - caf::core + CAF::core ) add_library(${PROJECT_NAME}_static STATIC ${SOURCES}) @@ -37,5 +39,5 @@ target_link_libraries(${PROJECT_NAME}_static xstudio::utility_static OpenEXR::OpenEXR Imath::Imath - caf::core + CAF::core ) diff --git a/src/ui/viewport/src/fps_monitor.cpp b/src/ui/viewport/src/fps_monitor.cpp index 646a4f5d2..6a7f13ec0 100644 --- a/src/ui/viewport/src/fps_monitor.cpp +++ b/src/ui/viewport/src/fps_monitor.cpp @@ -4,7 +4,6 @@ #include "xstudio/broadcast/broadcast_actor.hpp" #include "xstudio/colour_pipeline/colour_pipeline.hpp" #include "xstudio/media_reader/media_reader.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/uuid.hpp" @@ -27,7 +26,7 @@ std::string make_fps_display_string( // FPS is 23.976 then we ideally need 3 decimal places, but to fit the values into the // box in the UI we're truncating to 2 decimal places - if (!target) { + if (!target.count()) { return std::string("--/--"); } @@ -39,7 +38,7 @@ std::string make_fps_display_string( std::array rbuf; - if (actual) { + if (actual.count()) { if (round(target_fps) == target_fps) { // one decimal place sprintf(rbuf.data(), "%.1f/%.1f", rounded_actual, target_fps); @@ -53,7 +52,7 @@ std::string make_fps_display_string( sprintf(rbuf.data(), "--.-/%.1f", target_fps); } else { - sprintf(rbuf.data(), "--.-/%.2f", target_fps); + sprintf(rbuf.data(), "--.--/%.2f", target_fps); } } return std::string(rbuf.data()); @@ -62,8 +61,6 @@ std::string make_fps_display_string( FpsMonitor::FpsMonitor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { - set_default_handler(caf::drop); - // connect to view port. fps_event_grp_ = spawn(this); link_to(fps_event_grp_); @@ -89,8 +86,9 @@ FpsMonitor::FpsMonitor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { const FrameRate r = caluculate_actual_fps_measure(); expr = make_fps_display_string(r, target_playhead_rate_, velocity_); caf::scoped_actor sys(this->system()); - sys->delayed_send( - this, std::chrono::milliseconds(500), update_actual_fps_atom_v); + sys->mail(update_actual_fps_atom_v) + .delay(std::chrono::milliseconds(500)) + .send(this); } else { std::array buf; if (forward_) { @@ -100,7 +98,7 @@ FpsMonitor::FpsMonitor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { } expr = std::string(buf.data()); } - send(fps_event_grp_, utility::event_atom_v, fps_meter_update_atom_v, expr); + mail(utility::event_atom_v, fps_meter_update_atom_v, expr).send(fps_event_grp_); }, [=](utility::event_atom, playhead::play_atom, const bool is_playing) { playing_ = is_playing; @@ -109,28 +107,31 @@ FpsMonitor::FpsMonitor(caf::actor_config &cfg) : caf::event_based_actor(cfg) { } else { velocity_multiplier_ = 1.0; } - anon_send(this, update_actual_fps_atom_v); + anon_mail(update_actual_fps_atom_v).send(this); }, [=](playhead::show_atom) { frame_queued_for_display_ = true; }, [=](utility::event_atom, velocity_multiplier_atom, const float multiplier) { velocity_multiplier_ = multiplier; - anon_send(this, update_actual_fps_atom_v); + anon_mail(update_actual_fps_atom_v).send(this); }, [=](utility::event_atom, velocity_atom, const float v) { velocity_ = v; - anon_send(this, update_actual_fps_atom_v); + anon_mail(update_actual_fps_atom_v).send(this); }, [=](utility::event_atom, playhead::play_forward_atom, const bool fwd) { forward_ = fwd; - anon_send(this, update_actual_fps_atom_v); + anon_mail(update_actual_fps_atom_v).send(this); }, [=](utility::event_atom, playhead::actual_playback_rate_atom, const utility::FrameRate &rate) { target_playhead_rate_ = rate; - anon_send(this, update_actual_fps_atom_v); + anon_mail(update_actual_fps_atom_v).send(this); + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }, - [=](const error &err) mutable { aout(this) << err << std::endl; }); + [=](caf::message) {}); } void FpsMonitor::reset_actual_fps_measure() { viewport_frame_update_timepoints_.clear(); } @@ -217,5 +218,5 @@ void FpsMonitor::connect_to_playhead(caf::actor &playhead) { } catch ([[maybe_unused]] std::exception &e) { } - anon_send(this, update_actual_fps_atom_v); + anon_mail(update_actual_fps_atom_v).send(this); } \ No newline at end of file diff --git a/src/ui/viewport/src/fps_monitor.hpp b/src/ui/viewport/src/fps_monitor.hpp index af8155ac3..b818683eb 100644 --- a/src/ui/viewport/src/fps_monitor.hpp +++ b/src/ui/viewport/src/fps_monitor.hpp @@ -8,8 +8,8 @@ #include #include -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/chrono.hpp" +#include "xstudio/utility/frame_rate.hpp" using namespace caf; diff --git a/src/ui/viewport/src/hud_plugin.cpp b/src/ui/viewport/src/hud_plugin.cpp deleted file mode 100644 index 6a09c905f..000000000 --- a/src/ui/viewport/src/hud_plugin.cpp +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#include - -#include - -#include "xstudio/ui/viewport/hud_plugin.hpp" - -using namespace xstudio::utility; -using namespace xstudio::ui::viewport; -using namespace xstudio::ui; -using namespace xstudio; - -HUDPluginBase::HUDPluginBase( - caf::actor_config &cfg, std::string name, const utility::JsonStore &init_settings) - : plugin::StandardPlugin(cfg, name, init_settings) { - - enabled_ = add_boolean_attribute(name, name, true); - enabled_->expose_in_ui_attrs_group("hud_element_toggles"); - // this means the 'settings' button will not be visible in the HUD pop-up menu against this - // HUD tool. (See XsHudToolbarButton .. visibility on the settings button is hooked to - // 'disabled_value' property that comes through via the model data) - enabled_->set_role_data(module::Attribute::DisabledValue, true); - - message_handler_ = {[=](enable_hud_atom, bool enabled) { - globally_enabled_ = enabled; - redraw_viewport(); - }}; -} - -void HUDPluginBase::add_hud_settings_attribute(module::Attribute *attr) { - attr->expose_in_ui_attrs_group(Module::name() + " Settings"); - // this means the 'settings' button WILL be visible! - enabled_->set_role_data(module::Attribute::DisabledValue, false); -} - -void HUDPluginBase::hud_element_qml( - const std::string qml_code, const HUDElementPosition position) { - auto attr = add_qml_code_attribute("OverlayCode", qml_code); - - if (position == BottomLeft) { - attr->expose_in_ui_attrs_group("hud_elements_bottom_left"); - } else if (position == BottomCenter) { - attr->expose_in_ui_attrs_group("hud_elements_bottom_center"); - } else if (position == BottomRight) { - attr->expose_in_ui_attrs_group("hud_elements_bottom_right"); - } else if (position == TopLeft) { - attr->expose_in_ui_attrs_group("hud_elements_top_left"); - } else if (position == TopCenter) { - attr->expose_in_ui_attrs_group("hud_elements_top_center"); - } else if (position == TopRight) { - attr->expose_in_ui_attrs_group("hud_elements_top_right"); - } -} \ No newline at end of file diff --git a/src/ui/viewport/src/keypress_monitor.cpp b/src/ui/viewport/src/keypress_monitor.cpp index 265905722..699a889ee 100644 --- a/src/ui/viewport/src/keypress_monitor.cpp +++ b/src/ui/viewport/src/keypress_monitor.cpp @@ -1,4 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 +#include + #include "xstudio/ui/viewport/keypress_monitor.hpp" #include "xstudio/atoms.hpp" #include "xstudio/ui/mouse.hpp" @@ -20,17 +22,6 @@ KeypressMonitor::KeypressMonitor(caf::actor_config &cfg) : caf::event_based_acto hotkey_config_events_group_ = spawn(this); link_to(hotkey_config_events_group_); - set_down_handler([=](down_msg &msg) { - if (actor_grabbing_all_mouse_input_.find(caf::actor_cast(msg.source)) != - actor_grabbing_all_mouse_input_.end()) { - actor_grabbing_all_mouse_input_.erase( - actor_grabbing_all_mouse_input_.find(caf::actor_cast(msg.source))); - } - if (msg.source == actor_grabbing_all_keyboard_input_) { - actor_grabbing_all_keyboard_input_ = caf::actor(); - } - }); - behavior_.assign( [=](utility::get_event_group_atom) -> caf::actor { return keyboard_events_group_; }, [=](utility::get_event_group_atom, hotkey_event_atom) -> caf::actor { @@ -40,39 +31,41 @@ KeypressMonitor::KeypressMonitor(caf::actor_config &cfg) : caf::event_based_acto held_keys_.clear(); held_keys_changed(context); }, - [=](key_down_atom, int key, const std::string &context, const bool auto_repeat) { + [=](key_down_atom, + int key, + const std::string &context, + const std::string &window, + const bool auto_repeat) { if (actor_grabbing_all_keyboard_input_) { - anon_send( - actor_grabbing_all_keyboard_input_, - key_down_atom_v, - key, - context, - auto_repeat); + anon_mail(key_down_atom_v, key, context, auto_repeat) + .send(actor_grabbing_all_keyboard_input_); } else { held_keys_.insert(key); - held_keys_changed(context, auto_repeat); + held_keys_changed(context, auto_repeat, window); } }, - [=](key_up_atom, int key, const std::string &context) { + [=](key_up_atom, int key, const std::string &context, const std::string &window) { if (held_keys_.find(key) != held_keys_.end()) { held_keys_.erase(held_keys_.find(key)); held_keys_changed(context); } }, - [=](text_entry_atom, const std::string &text, const std::string &context) { + [=](text_entry_atom, + const std::string &text, + const std::string &context, + const std::string &window) { if (actor_grabbing_all_keyboard_input_) { - anon_send(actor_grabbing_all_keyboard_input_, text_entry_atom_v, text, context); + anon_mail(text_entry_atom_v, text, context) + .send(actor_grabbing_all_keyboard_input_); } else { - send(keyboard_events_group_, text_entry_atom_v, text, context); + mail(text_entry_atom_v, text, context).send(keyboard_events_group_); } }, [=](mouse_event_atom, const PointerEvent &e) { if (actor_grabbing_all_mouse_input_.size()) { - for (auto &a : actor_grabbing_all_mouse_input_) { - anon_send(a, mouse_event_atom_v, e); - } + anon_mail(mouse_event_atom_v, e).send(actor_grabbing_all_mouse_input_.front()); } else { - send(keyboard_events_group_, mouse_event_atom_v, e); + mail(mouse_event_atom_v, e).send(keyboard_events_group_); } }, [=](module::grab_all_keyboard_input_atom, caf::actor actor, const bool grab) { @@ -83,13 +76,36 @@ KeypressMonitor::KeypressMonitor(caf::actor_config &cfg) : caf::event_based_acto } }, [=](module::grab_all_mouse_input_atom, caf::actor actor, const bool grab) { + auto p = std::find( + actor_grabbing_all_mouse_input_.begin(), + actor_grabbing_all_mouse_input_.end(), + actor); + + if (p != actor_grabbing_all_mouse_input_.end()) { + actor_grabbing_all_mouse_input_.erase(p); + } + if (grab) { - actor_grabbing_all_mouse_input_.insert(actor); - } else if ( - actor_grabbing_all_mouse_input_.find(actor) != - actor_grabbing_all_mouse_input_.end()) { - actor_grabbing_all_mouse_input_.erase( - actor_grabbing_all_mouse_input_.find(actor)); + actor_grabbing_all_mouse_input_.insert( + actor_grabbing_all_mouse_input_.begin(), actor); + } + }, + + [=](watch_hotkey_atom, const utility::Uuid &hk_uuid, caf::actor watcher) { + auto p = active_hotkeys_.find(hk_uuid); + if (p != active_hotkeys_.end()) { + p->second.add_watcher(caf::actor_cast(watcher)); + } + }, + + [=](watch_hotkey_atom, + const utility::Uuid &hk_uuid, + caf::actor watcher, + bool exclusive_watcher) { + auto p = active_hotkeys_.find(hk_uuid); + if (p != active_hotkeys_.end()) { + p->second.exclusive_watcher( + caf::actor_cast(watcher), exclusive_watcher); } }, @@ -104,10 +120,35 @@ KeypressMonitor::KeypressMonitor(caf::actor_config &cfg) : caf::event_based_acto active_hotkeys_[hk.uuid()] = hk; } + for (auto &p : hk.watchers()) { + monitor( + caf::actor_cast(p), + [this, addr = caf::actor_cast(p).address()](const error &) { + auto p = std::find( + actor_grabbing_all_mouse_input_.begin(), + actor_grabbing_all_mouse_input_.end(), + caf::actor_cast(addr)); + if (p != actor_grabbing_all_mouse_input_.end()) { + actor_grabbing_all_mouse_input_.erase(p); + } + + if (addr == actor_grabbing_all_keyboard_input_) { + actor_grabbing_all_keyboard_input_ = caf::actor(); + } else { + caf::actor_addr w = caf::actor_cast(addr); + for (auto &p : active_hotkeys_) { + p.second.watcher_died(w); + } + } + }); + } + std::vector hks; for (const auto &p : active_hotkeys_) hks.push_back(p.second); - send(hotkey_config_events_group_, hotkey_event_atom_v, hks); + mail(hotkey_event_atom_v, active_hotkeys_[hk.uuid()]) + .send(hotkey_config_events_group_); + mail(hotkey_event_atom_v, hks).send(hotkey_config_events_group_); }, [=](register_hotkey_atom) -> std::vector { @@ -122,11 +163,35 @@ KeypressMonitor::KeypressMonitor(caf::actor_config &cfg) : caf::event_based_acto if (p != active_hotkeys_.end()) { return p->second; } - return make_error(xstudio_error::error, "Invalid hotkey uuid"); + return make_error( + xstudio_error::error, "Invalid hotkey uuid '" + to_string(kotkey_uuid) + "'"); + }, + + [=](hotkey_atom, const std::string &hotkey_name) -> result { + auto p = active_hotkeys_.begin(); + while (p != active_hotkeys_.end()) { + if (p->second.hotkey_name() == hotkey_name) { + return p->second; + } + p++; + } + return make_error( + xstudio_error::error, "Invalid hotkey name '" + hotkey_name + "'"); + }, + + [=](keypress_monitor::hotkey_event_atom, + const utility::Uuid kotkey_uuid, + const bool pressed, + const std::string &context, + const std::string &window) { + mail(hotkey_event_atom_v, kotkey_uuid, pressed, context, window) + .send(hotkey_config_events_group_); }, [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, - [=](const error &err) mutable { aout(this) << err << std::endl; }); + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); } void KeypressMonitor::on_exit() { @@ -135,7 +200,8 @@ void KeypressMonitor::on_exit() { actor_grabbing_all_mouse_input_.clear(); } -void KeypressMonitor::held_keys_changed(const std::string &context, const bool auto_repeat) { +void KeypressMonitor::held_keys_changed( + const std::string &context, const bool auto_repeat, const std::string &window) { if (actor_grabbing_all_keyboard_input_) { /*auto addr = caf::actor_cast(actor_grabbing_all_keyboard_input_); @@ -146,7 +212,8 @@ void KeypressMonitor::held_keys_changed(const std::string &context, const bool a }*/ } else { for (auto &p : active_hotkeys_) { - p.second.update_state(held_keys_, context, auto_repeat); + p.second.update_state( + held_keys_, context, window, auto_repeat, caf::actor_cast(this)); } } } diff --git a/src/ui/viewport/src/video_output_plugin.cpp b/src/ui/viewport/src/video_output_plugin.cpp new file mode 100644 index 000000000..aa5955213 --- /dev/null +++ b/src/ui/viewport/src/video_output_plugin.cpp @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include "xstudio/ui/viewport/video_output_plugin.hpp" + +using namespace xstudio::ui::viewport; +using namespace xstudio::plugin; + +VideoOutputPlugin::VideoOutputPlugin( + caf::actor_config &cfg, + const utility::JsonStore &init_settings, + const std::string &plugin_name) + : xstudio::plugin::StandardPlugin(cfg, plugin_name, init_settings), + init_settings_store_(init_settings) { + + message_handler_extensions_ = { + [=](offscreen_viewport_atom, caf::actor offscreen_vp) { + + }, + [=](media_reader::ImageBufPtr incoming) { incoming_video_frame_callback(incoming); }, + [=](const utility::JsonStore &data) { receive_status_callback(data); }, + [=](caf::error &err) {}}; + + // this call is essential to set-up the base class + make_behavior(); +} + +void VideoOutputPlugin::on_exit() { + exit_cleanup(); + StandardPlugin::on_exit(); +} + +void VideoOutputPlugin::finalise() { + + // this ensures 'Attributes' created by derived class get exposed in the UI layer + connect_to_ui(); + + // Get the 'StudioUI' which lives in the Qt context and therefore is able to + // create offscreen viewports for us + auto studio_ui = system().registry().template get(studio_ui_registry); + + // tell the studio actor to create an offscreen viewport. It will send + // us the resulting actor asynchronously as a message which our message + // handler above will receive + mail(offscreen_viewport_atom_v, Module::name() + " viewport") + .request(studio_ui, infinite) + .then( + [=](caf::actor offscreen_vp) { + // this is the offscreen renderer that we asked for below. + offscreen_viewport_ = offscreen_vp; + + // sending this message forces the new viewport attach to the current + // on screen playhead + anon_mail(viewport_playhead_atom_v, true).send(offscreen_viewport_); + + // now we have an offscreen viewport to send us frame buffers + // we can initialise the card and start output + initialise(); + + spawn_audio_output_actor(init_settings_store_); + }, + [=](caf::error &err) mutable { + spdlog::critical( + "{} in plugin {} : {}", + __PRETTY_FUNCTION__, + Module::name(), + to_string(err)); + }); +} + + +void VideoOutputPlugin::start(int frame_width, int frame_height) { + + if (!offscreen_viewport_) + return; + + mail( + video_output_actor_atom_v, + caf::actor_cast(this), + frame_width, + frame_height, + viewport::RGBA_16 // viewport::RGBA_10_10_10_2 // *see note below + ) + .send(offscreen_viewport_); +} + +void VideoOutputPlugin::stop() { + + mail(video_output_actor_atom_v, caf::actor()).send(offscreen_viewport_); +} + +void VideoOutputPlugin::video_frame_consumed(const utility::time_point &frame_display_time) { + + // this informs the viewport the timepoint when video frames are going on-screen. It uses + // this to infer the re-fresh rate of the display and therefore do an accurate 'pulldown' + // when evaluating the playhead position for the next frame to go on-screen. + anon_mail(ui::fps_monitor::framebuffer_swapped_atom_v, frame_display_time) + .send(offscreen_viewport_); +} + +void VideoOutputPlugin::request_video_frame(const utility::time_point &frame_display_time) { + + // Make an explicit request for the viewport to render a frame that will go on-screen at the + // given time. The resulting frame is delivered to the subclass via + // incoming_video_frame_callback + anon_mail( + ui::viewport::render_viewport_to_image_atom_v, + frame_display_time + std::chrono::milliseconds(video_delay_millisecs_)) + .send(offscreen_viewport_); +} + +void VideoOutputPlugin::send_status(const utility::JsonStore &data) { + + anon_mail(data).send(caf::actor_cast(this)); +} + +void VideoOutputPlugin::sync_geometry_to_main_viewport(const bool sync) { + + caf::scoped_actor sys(system()); + try { + + utility::request_receive( + *sys, + offscreen_viewport_, + module::change_attribute_value_atom_v, + "Sync To Main Viewport", + utility::JsonStore(sync), + true); + + if (!sync) { + anon_mail(ui::viewport::fit_mode_atom_v, previous_fit_mode_) + .send(offscreen_viewport_); + return; + } + + previous_fit_mode_ = utility::request_receive( + *sys, offscreen_viewport_, ui::viewport::fit_mode_atom_v); + + auto playhead_events_actor = + system().registry().template get(global_playhead_events_actor); + + auto main_viewport = utility::request_receive( + *sys, playhead_events_actor, ui::viewport::active_viewport_atom_v); + + if (!main_viewport) + return; + + auto pan = utility::request_receive( + *sys, main_viewport, ui::viewport::viewport_pan_atom_v); + + auto zoom = utility::request_receive( + *sys, main_viewport, ui::viewport::viewport_scale_atom_v); + + anon_mail(ui::viewport::viewport_pan_atom_v, pan).send(offscreen_viewport_); + anon_mail(ui::viewport::viewport_scale_atom_v, zoom).send(offscreen_viewport_); + + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + +void VideoOutputPlugin::display_info( + const std::string &name, + const std::string &model, + const std::string &manufacturer, + const std::string &serialNumber) { + anon_mail(screen_info_atom_v, name, model, manufacturer, serialNumber) + .send(offscreen_viewport_); +} diff --git a/src/ui/viewport/src/viewport.cpp b/src/ui/viewport/src/viewport.cpp index 6e9cf8a18..540fde497 100644 --- a/src/ui/viewport/src/viewport.cpp +++ b/src/ui/viewport/src/viewport.cpp @@ -1,5 +1,6 @@ -// SPDX-License-Identifier: Apache-2.0 #include +#include + #include #include @@ -19,22 +20,21 @@ using namespace xstudio::ui; using namespace xstudio; namespace { -/* + static Imath::M44f matrix_from_square(const float *in) { - const Imath::V2f * corners = (const Imath::V2f *)in; - const float xscale = corners[1].x - corners[0].x; - const float yscale = corners[2].y - corners[1].y; - const float x_offset = corners[0].x + corners[1].x; - const float y_offset = corners[2].y + corners[0].y; + const Imath::V2f *corners = (const Imath::V2f *)in; + const float xscale = corners[1].x - corners[0].x; + const float yscale = corners[2].y - corners[1].y; + const float x_offset = corners[0].x + corners[1].x; + const float y_offset = corners[2].y + corners[0].y; Imath::M44f i; i.makeIdentity(); - i.translate(Imath::V3f(x_offset/2.0f, y_offset/2.0f, 0.0f)); - i.scale(Imath::V3f(xscale/2.0f,-yscale/2.0f,1.0f)); + i.translate(Imath::V3f(x_offset / 2.0f, y_offset / 2.0f, 0.0f)); + i.scale(Imath::V3f(xscale / 2.0f, -yscale / 2.0f, 1.0f)); return i; - } -*/ + Imath::M44f matrix_from_corners(const float *in) { // A simplified version of the cornerpin transform matrix @@ -99,52 +99,52 @@ Imath::M44f matrix_from_corners(const float *in) { mqr[2][2]); } +std::string make_viewport_name() { + static int idx = 0; + return fmt::format("viewport{0}", idx++); +} + } // namespace Viewport::Viewport( - const utility::JsonStore &state_data, - caf::actor parent_actor, - const int viewport_index, - ViewportRendererPtr the_renderer, - const std::string &_name) - : Module( - _name.empty() ? (viewport_index >= 0 - ? fmt::format("viewport{0}", viewport_index) - : fmt::format("offscreen_viewport{0}", abs(viewport_index))) - : _name), - parent_actor_(std::move(parent_actor)), - viewport_index_(viewport_index), - the_renderer_(std::move(the_renderer)) { + const utility::JsonStore &state_data, caf::actor parent_actor, const std::string &_name) + : Module(_name.empty() ? make_viewport_name() : _name), + parent_actor_(std::move(parent_actor)) { + + if (state_data.contains("window_id") && state_data["window_id"].is_string()) { + window_id_ = state_data["window_id"].get(); + } + + if (window_id_ == "xstudio_quickview_window") { + // This is a hack - I've not worked out how to make unique window ID on construction + // through QML. But each quickview window needs a unique window_id_ + // so they get their own OpenGL resource object (so it has its own + // texture etc.) + static int idx = 0; + window_id_ = window_id_ + fmt::format("{}", idx++); + } // TODO: set these up via Json prefs coming in from framework // so pointer controls are user configurable static const Signature zoom_pointer_event_sig{ - Signature::EventType::Drag, - Signature::Button::Middle, - Signature::Modifier::ControlModifier}; + EventType::Drag, Signature::Button::Middle, Signature::Modifier::ControlModifier}; static const Signature pan_pointer_event_sig{ - Signature::EventType::Drag, Signature::Button::Middle, Signature::Modifier::NoModifier}; + EventType::Drag, Signature::Button::Middle, Signature::Modifier::NoModifier}; static const Signature wheel_zoom_pointer_event_sig{ - Signature::EventType::MouseWheel, - Signature::Button::None, - Signature::Modifier::NoModifier}; + EventType::MouseWheel, Signature::Button::None, Signature::Modifier::NoModifier}; static const Signature reset_zoom_pointer_event_sig{ - Signature::EventType::DoubleClick, + EventType::DoubleClick, Signature::Button::Left, Signature::Modifier::ZoomActionModifier}; static const Signature reset_pan_pointer_event_sig{ - Signature::EventType::DoubleClick, + EventType::DoubleClick, Signature::Button::Left, Signature::Modifier::PanActionModifier}; static const Signature force_zoom_pointer_event_sig{ - Signature::EventType::Drag, - Signature::Button::Left, - Signature::Modifier::ZoomActionModifier}; + EventType::Drag, Signature::Button::Left, Signature::Modifier::ZoomActionModifier}; static const Signature force_pan_pointer_event_sig{ - Signature::EventType::Drag, - Signature::Button::Left, - Signature::Modifier::PanActionModifier}; + EventType::Drag, Signature::Button::Left, Signature::Modifier::PanActionModifier}; static const xstudio::utility::JsonStore default_settings = nlohmann::json( {{"bg_colour", {0.18f, 0.18f, 0.18f}}, @@ -176,6 +176,7 @@ Viewport::Viewport( state_.translate_.y = (state_.mirror_mode_ & MirrorMode::Flop ? -delta_trans.y : delta_trans.y) + interact_start_state_.translate_.y; + set_fit_mode(FitMode::Free, false); update_matrix(); return true; }; @@ -193,6 +194,7 @@ Viewport::Viewport( state_.scale_ = interact_start_state_.scale_ * scale_factor; const Imath::V4f anchor_before = interact_start_state_.pointer_position_ * interact_start_inv_projection_matrix_; + set_fit_mode(FitMode::Free, false); update_matrix(); const Imath::V4f anchor_after = interact_start_state_.pointer_position_ * inv_projection_matrix_; @@ -215,6 +217,7 @@ Viewport::Viewport( float(pointer_event.angle_delta().second) * settings_["pointer_wheel_senistivity"].get() / 1000.0f); // basic pointer wheel causes an angle delta of 120 degrees. + set_fit_mode(FitMode::Free, false); update_matrix(); const Imath::V4f anchor_after = state_.pointer_position_ * inv_projection_matrix_; state_.translate_.x += (anchor_after.x - anchor_before.x) / state_.scale_; @@ -241,9 +244,9 @@ Viewport::Viewport( pointer_event_handlers_[force_pan_pointer_event_sig] = pointer_event_handlers_[pan_pointer_event_sig]; - zoom_mode_toggle_ = add_boolean_attribute("Zoom (Z)", "Zm", false); + zoom_mode_toggle_ = add_boolean_attribute("Zoom", "Zm", false); - pan_mode_toggle_ = add_boolean_attribute("Pan (X)", "Pan", false); + pan_mode_toggle_ = add_boolean_attribute("Pan", "Pan", false); fit_mode_ = add_string_choice_attribute( "Fit (F)", @@ -256,17 +259,22 @@ Viewport::Viewport( "Mirror", "Mirr", "Off", - {"Flip", "Flop", "Both", "Off"}, - {"Flip", "Flop", "Both", "Off"}); + {"Mirror Horizontally", "Mirror Vertically", "Mirror Both", "Off"}, + {"Mirror Horizontally", "Mirror Vertically", "Mirror Both", "Off"}); + + // window_id_ will be "xstudio_main_window" for any viewport embedded in + // the main interface or "xstudio_popout_window" for the pop-out window. + // These viewports should be zoom/pan/fit/compare mode synced. All other + // viewport must not sync these settings. + sync_to_main_viewport_ = add_boolean_attribute( + "Sync To Main Viewport", + "Sync To Main Viewport", + window_id_ == "xstudio_main_window" || window_id_ == "xstudio_popout_window"); filter_mode_preference_ = add_string_choice_attribute( "Viewport Filter Mode", "Vp. Filtering", ViewportRenderer::pixel_filter_mode_names); filter_mode_preference_->set_preference_path("/ui/viewport/filter_mode"); - if (viewport_index_ == 0) - filter_mode_preference_->set_role_data( - module::Attribute::Groups, nlohmann::json{"viewport_pixel_filter"}); - static const std::vector> texture_mode_names = { {0, "Image Texture", "Image Texture", true}, {1, "SSBO", "SSBO", true}}; @@ -274,9 +282,6 @@ Viewport::Viewport( texture_mode_preference_ = add_string_choice_attribute("GPU Texture Mode", "Tex. Mode", texture_mode_names); texture_mode_preference_->set_preference_path("/ui/viewport/texture_mode"); - if (viewport_index_ == 0) - texture_mode_preference_->set_role_data( - module::Attribute::Groups, nlohmann::json{"viewport_texture_mode"}); mouse_wheel_behaviour_ = add_string_choice_attribute( "Mouse Wheel Behaviour", @@ -285,21 +290,14 @@ Viewport::Viewport( {"Scrub Timeline", "Zoom Viewer"}, {"Scrub Timeline", "Zoom Viewer"}); mouse_wheel_behaviour_->set_preference_path("/ui/viewport/viewport_mouse_wheel_behaviour"); - if (viewport_index_ == 0) - mouse_wheel_behaviour_->set_role_data( - module::Attribute::Groups, nlohmann::json{"viewport_mouse_wheel_behaviour_attr"}); - // we give a unique 'toolbar_name' per viewport. This is set on the 'Groups' + // we give a unique 'toolbar_name' per viewport. This is set on the 'UIDataModels' // role data of some attributes. We make the toolbar_name_ available in qml // as a property of the Viewport item, which can then see those attributes // as part of a data model. std::string toolbar_name = name() + "_toolbar"; - zoom_mode_toggle_->set_role_data( - module::Attribute::Groups, nlohmann::json{"viewport_zoom_and_pan_modes"}); - - pan_mode_toggle_->set_role_data( - module::Attribute::Groups, nlohmann::json{"viewport_zoom_and_pan_modes"}); + std::string other_attrs_model = name() + "_attrs"; mirror_mode_->set_role_data( module::Attribute::ToolTip, @@ -322,63 +320,39 @@ Viewport::Viewport( "Set how image is fit to window. In free mode: drag middle mouse button to pan, hold " "Ctrl key and middle mouse drag to zoom or roll mouse wheel to zoom."); - if (viewport_index_ >= 0) { - add_multichoice_attr_to_menu(fit_mode_, name() + "_context_menu_section0", "Fit"); - add_multichoice_attr_to_menu(mirror_mode_, name() + "_context_menu_section0", "Mirror"); - } - - auto source = add_qml_code_attribute( - "Src", - fmt::format( - R"( - import xStudio 1.0 - XsSourceToolbarButton {{ - anchors.fill: parent - toolbar_name: "{}" - }} - )", - toolbar_name)); - zoom_mode_toggle_->set_role_data(module::Attribute::ToolbarPosition, 5.0f); pan_mode_toggle_->set_role_data(module::Attribute::ToolbarPosition, 6.0f); fit_mode_->set_role_data(module::Attribute::ToolbarPosition, 7.0f); mirror_mode_->set_role_data(module::Attribute::ToolbarPosition, 8.0f); - source->set_role_data(module::Attribute::ToolbarPosition, 12.0f); - frame_error_message_ = add_string_attribute("frame_error", "frame_error", ""); - frame_error_message_->set_role_data( - module::Attribute::Groups, nlohmann::json{"viewport_frame_error_message"}); + frame_rate_expr_ = add_string_attribute("Frame Rate", "Frame Rate", "--/--"); + custom_cursor_name_ = add_string_attribute("Custom Cursor Name", "Custom Cursor Name", ""); - hud_toggle_ = add_boolean_attribute("Hud", "Hud", true); + // HUD needs custom QML code for instatiation into the toolbar as it's + // not a simple switch or slider or multichoice + hud_toggle_ = add_boolean_attribute("HUD", "HUD", true); hud_toggle_->set_tool_tip("Access HUD controls"); - hud_toggle_->expose_in_ui_attrs_group("hud_toggle"); hud_toggle_->set_preference_path("/ui/viewport/enable_hud"); - // here we set custom QML code to implement a custom widget that is inserted - // into the viewer toolbox. + hud_toggle_->set_role_data(module::Attribute::ToolbarPosition, 0.0f); + hud_toggle_->set_role_data(module::Attribute::Type, "QmlCode"); hud_toggle_->set_role_data( module::Attribute::QmlCode, - R"( - import xStudio 1.0 - XsHudToolbarButton { - id: control - anchors.fill: parent - } - )"); - hud_toggle_->set_role_data(module::Attribute::ToolbarPosition, 0.0f); + R"(import xStudio 1.0 + XsViewerHUDButton {})"); if (parent_actor_) { module::Module::set_parent_actor_addr(caf::actor_cast(parent_actor_)); - auto a = caf::actor_cast(parent_actor_); - caf::scoped_actor sys(a->system()); - fps_monitor_ = sys->spawn(); - bool is_offscreen = viewport_index_ < 0; - if (!is_offscreen) - connect_to_ui(); - instance_overlay_plugins(); + global_playhead_events_group_ = + self()->home_system().registry().template get( + global_playhead_events_actor); + caf::scoped_actor sys(self()->home_system()); + fps_monitor_ = sys->spawn(); + connect_to_ui(); get_colour_pipeline(); + instance_overlay_plugins(); // join the FPS monitor event group auto group = @@ -388,50 +362,81 @@ Viewport::Viewport( // register with the global playhead events actor so other parts of the // application can talk directly to us - auto ph_events = - a->system().registry().template get(global_playhead_events_actor); - anon_send(ph_events, viewport_atom_v, name(), parent_actor_); + anon_mail(viewport_atom_v, name(), parent_actor_).send(global_playhead_events_group_); } - set_fit_mode(FitMode::Best); + set_fit_mode(FitMode::Best, false); // force update our internal filter mode enum attribute_changed( filter_mode_preference_->get_role_data(module::Attribute::UuidRole), module::Attribute::Value); - make_attribute_visible_in_viewport_toolbar(zoom_mode_toggle_); - make_attribute_visible_in_viewport_toolbar(pan_mode_toggle_); make_attribute_visible_in_viewport_toolbar(fit_mode_); make_attribute_visible_in_viewport_toolbar(mirror_mode_); make_attribute_visible_in_viewport_toolbar(hud_toggle_); - make_attribute_visible_in_viewport_toolbar(source); + + module::QmlCodeAttribute *source_selector = add_qml_code_attribute( + "Source Selector", + R"( + import xStudio 1.0 + XsViewerSourceSelectorButton { + anchors.fill: parent + } + )"); + source_selector->set_role_data(module::Attribute::ToolbarPosition, 20.0f); + make_attribute_visible_in_viewport_toolbar(source_selector); std::string mini_toolbar_name = name() + "_actionbar"; expose_attribute_in_model_data(zoom_mode_toggle_, mini_toolbar_name); expose_attribute_in_model_data(pan_mode_toggle_, mini_toolbar_name); + expose_attribute_in_model_data(zoom_mode_toggle_, other_attrs_model); + expose_attribute_in_model_data(pan_mode_toggle_, other_attrs_model); + expose_attribute_in_model_data(frame_rate_expr_, other_attrs_model); + expose_attribute_in_model_data(custom_cursor_name_, other_attrs_model); // we call this base-class method to set-up our attributes so that they // show up in our toolbar - connect_to_viewport(name(), toolbar_name, true); + connect_to_viewport(name(), toolbar_name, true, parent_actor_); + + + if (sync_to_main_viewport_->value()) { + auto_connect_to_global_selected_playhead(); + } - auto_connect_to_playhead(true); + if (playhead_uuid_.is_null()) { + set_compare_mode("Off"); + } } Viewport::~Viewport() { caf::scoped_actor sys(self()->home_system()); sys->send_exit(fps_monitor_, caf::exit_reason::user_shutdown); - sys->send_exit(display_frames_queue_actor_, caf::exit_reason::user_shutdown); sys->send_exit(colour_pipeline_, caf::exit_reason::user_shutdown); if (quickview_playhead_) { sys->send_exit(quickview_playhead_, caf::exit_reason::user_shutdown); } + sys->send_exit(display_frames_queue_actor_, caf::exit_reason::user_shutdown); + display_frames_queue_actor_ = caf::actor(); } -void Viewport::link_to_viewport(caf::actor other_viewport) { +void Viewport::auto_connect_to_global_selected_playhead() { + + // We run this if we want the viewport to always automatically + // connect to the playhead of the main selected playlist/timeline/subset. + + // So quickviewer viewports and the offscreen viewport used for rendering + // snapshots should NOT auto_connect - other_viewports_.push_back(other_viewport); - anon_send(other_viewport, other_viewport_atom_v, parent_actor_, colour_pipeline_); + // this means we get events about the global selected playhead + // changing + + // connect to the current playhead now + caf::scoped_actor sys(self()->home_system()); + auto playhead = request_receive( + *sys, global_playhead_events_group_, viewport::viewport_playhead_atom_v); + if (playhead) + set_playhead(playhead); } void Viewport::register_hotkeys() { @@ -441,35 +446,91 @@ void Viewport::register_hotkeys() { NoModifier, "Activate viewport zoom mode", "Press and hold to turn on viewport zooming - when on, scrub mouse left/right to zoom " - "into or out of the image"); + "into or out of the image", + false, + "Viewer"); pan_hotkey_ = register_hotkey( int('X'), NoModifier, "Activate viewport pan mode", "Press and hold to turn on viewport panning - when on, click and drag to pan the image " - "within the viewport"); + "within the viewport", + false, + "Viewer"); reset_hotkey_ = register_hotkey( int('R'), ControlModifier, "Reset Viewport", - "Resets the viewport zoom/fit to your last 'Fit' mode setting"); + "Resets the viewport zoom/fit to your last 'Fit' mode setting", + false, + "Viewer"); fit_mode_hotkey_ = register_hotkey( int('F'), NoModifier, "Toggle Fit Mode On/Off", "Toggles the viewport zoom/fit between your last Fit Mode setting and whatever " - "zoom/pan you had previously."); + "zoom/pan you had previously.", + false, + "Viewer"); mirror_mode_hotkey_ = register_hotkey( int('F'), ShiftModifier, "Activate mirror mode", - "Toggles the mirror mode from Flip / Flop / Both / Off "); + "Toggles the mirror mode from Flip / Flop / Both / Off ", + false, + "Viewer"); + + hover_select_ = register_hotkey( + "F7", + "Pointer select media in viewport", + "While holding this key, the image in the viewport under the mouse pointer sets the " + "'hero' media in the selected media set.", + false, + "Viewer"); + + toggle_hug_ = register_hotkey( + "H", + "Toggle the HUD overlays on/off", + "This hotkey will enable/disable the entire set of HUD overlays.", + false, + "Viewer"); + + zoom_mode_toggle_->set_role_data(module::Attribute::HotkeyUuid, zoom_hotkey_); + pan_mode_toggle_->set_role_data(module::Attribute::HotkeyUuid, pan_hotkey_); + + setup_menus(); +} + +void Viewport::setup_menus() { + + // Here attrs are added to a menu model with a unique name for this viewport + // In the QML code (XsViewportContextMenu.qml) this menu model is used + // to create the pop-up menu you get when right clicking in the viewport + + std::string context_menu_model_name = name() + "_context_menu"; + + // Divider + insert_menu_item(context_menu_model_name, "", "", 7.5f, nullptr, true); + + insert_menu_item(context_menu_model_name, "Mirror", "", 8.0f, mirror_mode_, false); + + insert_menu_item(context_menu_model_name, "Fit", "", 9.0f, fit_mode_, false); + + reset_menu_item_ = insert_menu_item( + context_menu_model_name, "Reset Viewer", "", 10.0f, nullptr, false, reset_hotkey_); + + // Divider + insert_menu_item(context_menu_model_name, "", "", 10.5f, nullptr, true); } +void Viewport::event_callback(const ChangeCallbackId ev) { + needs_redraw_ = true; + event_callback_(ev); +} xstudio::utility::JsonStore Viewport::serialise() const { utility::JsonStore jsn; @@ -520,13 +581,11 @@ void Viewport::set_pointer_event_viewport_coords(PointerEvent &pointer_event) { // coordinates, which tells us how much the effective viewport zoom is in // display pixels. We can use this when detecting if the mouse cursor is within // N screen pixels of something in the viewport regardless of the zoom - Imath::V4f d_zero = Imath::V4f(1.0f / state_.size_.x, 0.0f, 0.0f, 1.0f) * - projection_matrix_ * fit_mode_matrix_.inverse(); - Imath::V4f delta = - Imath::V4f(0.0f, 0.0f, 0.0f, 1.0f) * projection_matrix_ * fit_mode_matrix_.inverse(); + Imath::V4f d_zero = + Imath::V4f(1.0f / state_.size_.x, 0.0f, 0.0f, 1.0f) * projection_matrix_; + Imath::V4f delta = Imath::V4f(0.0f, 0.0f, 0.0f, 1.0f) * projection_matrix_; Imath::V4f p = state_.pointer_position_; - p *= fit_mode_matrix_.inverse(); pointer_event.set_pos_in_coord_sys(p.x, p.y, (d_zero - delta).x); } @@ -549,11 +608,19 @@ bool Viewport::process_pointer_event(PointerEvent &pointer_event) { rt_val = true; } - if (pointer_event.type() == Signature::EventType::ButtonDown) // pointer button down + if (pointer_event.type() == EventType::ButtonDown) // pointer button down { interact_start_state_ = state_; interact_start_projection_matrix_ = projection_matrix_; interact_start_inv_projection_matrix_ = inv_projection_matrix_; + + if (pointer_event.buttons() == ui::Signature::Button::Left && compare_mode_ == "Grid") { + + pointer_select_media(pointer_event); + } + } else if (hover_image_select_ && pointer_event.type() == EventType::Move) { + + pointer_select_media(pointer_event); } if (pointer_event_handlers_.find(pointer_event.signature()) != @@ -563,11 +630,6 @@ bool Viewport::process_pointer_event(PointerEvent &pointer_event) { const Imath::V3f old_translate = state_.translate_; if (pointer_event_handlers_[pointer_event.signature()](pointer_event)) { - // Send message to other_viewport_ and pass zoom/pan - for (auto &o : other_viewports_) { - anon_send(o, viewport_pan_atom_v, state_.translate_.x, state_.translate_.y); - anon_send(o, viewport_scale_atom_v, state_.scale_); - } if (state_.translate_.x != 0.0f || state_.translate_.y != 0.0f || state_.scale_ != 1.0f) { @@ -577,9 +639,6 @@ bool Viewport::process_pointer_event(PointerEvent &pointer_event) { previous_fit_zoom_state_.scale_ = old_scale; state_.fit_mode_ = Free; fit_mode_->set_value("Off"); - for (auto &o : other_viewports_) { - anon_send(o, fit_mode_atom_v, Free); - } } } @@ -590,12 +649,13 @@ bool Viewport::process_pointer_event(PointerEvent &pointer_event) { return rt_val; } -bool Viewport::set_scene_coordinates( - const Imath::V2f topleft, - const Imath::V2f topright, - const Imath::V2f bottomright, - const Imath::V2f bottomleft, - const Imath::V2i scene_size, +bool Viewport::set_geometry( + const float x, + const float y, + const float width, + const float height, + const float window_width, + const float window_height, const float devicePixelRatio) { // These coordinates describe the quad into which the viewport @@ -605,82 +665,74 @@ bool Viewport::set_scene_coordinates( // qml scene. std::vector render_rect = { - topleft.x * 2.0f / float(scene_size.x) - 1.0f, - -(topleft.y * 2.0f / float(scene_size.y) - 1.0f), - topright.x * 2.0f / float(scene_size.x) - 1.0f, - -(topright.y * 2.0f / float(scene_size.y) - 1.0f), - bottomright.x * 2.0f / float(scene_size.x) - 1.0f, - -(bottomright.y * 2.0f / float(scene_size.y) - 1.0f), - bottomleft.x * 2.0f / float(scene_size.x) - 1.0f, - -(bottomleft.y * 2.0f / float(scene_size.y) - 1.0f)}; + x * 2.0f / window_width - 1.0f, + -(y * 2.0f / window_height - 1.0f), + (x + width) * 2.0f / window_width - 1.0f, + -(y * 2.0f / window_height - 1.0f), + (x + width) * 2.0f / window_width - 1.0f, + -((y + height) * 2.0f / window_height - 1.0f), + x * 2.0f / window_width - 1.0f, + -((y + height) * 2.0f / window_height - 1.0f)}; // now we've got the quad we convert to a transform matrix to be used at // render time to plot the viewport correctly into its window within the // fuill glviewport (which corresponds to the QQuickWindow) - Imath::M44f vp2c = matrix_from_corners(render_rect.data()); + Imath::M44f vp2c = matrix_from_square(render_rect.data()); + //vp2c.scale(Imath::V3f(devicePixelRatio, devicePixelRatio, devicePixelRatio)); - if (vp2c != viewport_to_canvas_ || (bottomright - bottomleft).length() != state_.size_.x || - (bottomleft - topleft).length() != state_.size_.y) { + if (vp2c != viewport_to_canvas_ || width != state_.size_.x || height != state_.size_.y) { viewport_to_canvas_ = vp2c; - set_size( - (bottomright - bottomleft).length(), - (bottomleft - topleft).length(), - devicePixelRatio); + set_size(width, height, window_width, window_height, devicePixelRatio); return true; } return false; } -void Viewport::update_fit_mode_matrix( - const int image_width, const int image_height, const float pixel_aspect) { - // update current image dims if known - if (image_height) { - state_.image_aspect_ = float(image_width * pixel_aspect) / float(image_height); - state_.image_size_ = Imath::V2i(image_width, image_height); - } +void Viewport::apply_fit_mode() { // exit if window size is as-yet unknown if (!size().x) return; - fit_mode_matrix_.makeIdentity(); - const float a_ratio = state_.image_aspect_ * size().y / (size().x); - - float tx = 0.0f; - float ty = 0.0f; + const float a_ratio = state_.layout_aspect_ * size().y / (size().x); if (fit_mode() == Width) { - state_.fit_mode_zoom_ = 1.0f; + state_.translate_ = Imath::V3f(0.0f, 0.0f, 0.0f); + state_.scale_ = 1.0f; } else if (fit_mode() == Height) { - state_.fit_mode_zoom_ = a_ratio; + state_.translate_ = Imath::V3f(0.0f, 0.0f, 0.0f); + state_.scale_ = a_ratio; } else if (fit_mode() == Best) { + state_.translate_ = Imath::V3f(0.0f, 0.0f, 0.0f); if (a_ratio < 1.0f) { - state_.fit_mode_zoom_ = a_ratio; + state_.scale_ = a_ratio; } else { - state_.fit_mode_zoom_ = 1.0f; + state_.scale_ = 1.0f; } } else if (fit_mode() == Fill) { if (a_ratio > 1.0f) { - state_.fit_mode_zoom_ = a_ratio; + state_.scale_ = a_ratio; } else { - state_.fit_mode_zoom_ = 1.0f; + state_.scale_ = 1.0f; } } else if (fit_mode() == One2One && state_.image_size_.x) { // for 1:1 to work when we have high DPI display scaling (e.g. with // QT_SCALE_FACTOR!=1.0) we need to account for the pixel ratio - int screen_pix_size_x = (int)round(float(size().x) * devicePixelRatio_); - int screen_pix_size_y = (int)round(float(size().y) * devicePixelRatio_); + int screen_pix_size_x = (int)round(size().x); + int screen_pix_size_y = (int)round(size().y); - state_.fit_mode_zoom_ = float(state_.image_size_.x) / screen_pix_size_x; + state_.scale_ = float(state_.image_size_.x) / screen_pix_size_x; + + state_.translate_ = Imath::V3f(0.0f, 0.0f, 0.0f); // in 1:1 fit mode, if the image has an odd number of pixels and the // viewport an even number of pixels (or vice versa) in either axis it causes a problem: @@ -689,20 +741,16 @@ void Viewport::update_fit_mode_matrix( // the 'wrong' pixel and thus we get a nasty aliasing pattern arising in the plot. To // overcome this I add a half pixel shift in the image position if ((state_.image_size_.x & 1) != (int(round(screen_pix_size_x)) & 1)) { - tx = 0.5f / screen_pix_size_x; + state_.translate_.x = 0.5f / screen_pix_size_x; } if ((state_.image_size_.y & 1) != (int(round(screen_pix_size_y)) & 1)) { - ty = 0.5f / screen_pix_size_y; + state_.translate_.y = 0.5f / screen_pix_size_y; } } - - fit_mode_matrix_.makeIdentity(); - fit_mode_matrix_.scale(Imath::V3f(state_.fit_mode_zoom_, state_.fit_mode_zoom_, 1.0f)); - fit_mode_matrix_.translate(Imath::V3f(tx, ty, 0.0f)); } float Viewport::pixel_zoom() const { - return state_.fit_mode_zoom_ * state_.scale_ * float(state_.size_.x) / state_.image_size_.x; + return state_.scale_ * float(state_.size_.x) / state_.image_size_.x; } void Viewport::set_scale(const float scale) { @@ -710,9 +758,11 @@ void Viewport::set_scale(const float scale) { update_matrix(); } -void Viewport::set_size(const float w, const float h, const float devicePixelRatio) { - state_.size_ = Imath::V2f(w, h); - devicePixelRatio_ = devicePixelRatio; +void Viewport::set_size( + const float w, const float h, const float window_width, const float window_height, const float pixelRatio) { + state_.window_size_ = Imath::V2f(window_width, window_height); + state_.size_ = Imath::V2f(w, h); + state_.devicePixelRatio_ = pixelRatio; update_matrix(); } @@ -722,20 +772,17 @@ void Viewport::set_pan(const float x_pan, const float y_pan) { update_matrix(); } -void Viewport::set_fit_mode(const FitMode md) { +void Viewport::set_fit_mode(const FitMode md, const bool sync) { - if (state_.fit_mode_ != md) { + if (state_.fit_mode_ != md && ((state_.fit_mode_ == FitMode::Free && md != FitMode::Free) || (state_.fit_mode_ != FitMode::Free && md == FitMode::Free))) { + // We onlt store the previous fit if we're going from 'Free' to another + // mode or vice-versa. previous_fit_zoom_state_.fit_mode_ = state_.fit_mode_; previous_fit_zoom_state_.translate_ = state_.translate_; previous_fit_zoom_state_.scale_ = state_.scale_; } - if (md == Free && state_.fit_mode_ != Free) { - - state_.scale_ = state_.fit_mode_zoom_; - state_.fit_mode_zoom_ = 1.0f; - - } else if (md != Free) { + if (md != Free) { // With active fit mode translate is held at 0.0,0.0 and scale // is held at 1.0 .. the additional matrix to apply the fit mode @@ -760,41 +807,17 @@ void Viewport::set_fit_mode(const FitMode md) { fit_mode_->set_value("Off"); update_matrix(); - - event_callback_(FitModeChanged); - event_callback_(ZoomChanged); - event_callback_(Redraw); + event_callback(Redraw); } void Viewport::set_mirror_mode(const MirrorMode md) { state_.mirror_mode_ = md; update_matrix(); - event_callback_(MirrorModeChanged); - event_callback_(Redraw); -} - -void Viewport::set_pixel_zoom(const float zoom) { - if (state_.size_.x && state_.fit_mode_zoom_) { - const float old_scale = state_.scale_; - const Imath::V3f old_translate = state_.translate_; - - state_.scale_ = - state_.image_size_.x * zoom / (state_.fit_mode_zoom_ * float(state_.size_.x)); - if (state_.translate_.x != 0.0f || state_.translate_.y != 0.0f || - state_.scale_ != 1.0f) { - if (state_.fit_mode_ != Free) { - previous_fit_zoom_state_.fit_mode_ = state_.fit_mode_; - previous_fit_zoom_state_.translate_ = old_translate; - previous_fit_zoom_state_.scale_ = old_scale; - } - state_.fit_mode_ = Free; - fit_mode_->set_value("Off"); - } - update_matrix(); - } + event_callback(Redraw); } void Viewport::revert_fit_zoom_to_previous(const bool synced) { + if (previous_fit_zoom_state_.scale_ == 0.0f) return; // previous state not set std::swap(state_.fit_mode_, previous_fit_zoom_state_.fit_mode_); @@ -815,20 +838,17 @@ void Viewport::revert_fit_zoom_to_previous(const bool synced) { fit_mode_->set_value("Off", false); update_matrix(); - event_callback_(FitModeChanged); - event_callback_(ZoomChanged); - event_callback_(Redraw); - - if (state_.fit_mode_ == FitMode::Free && !synced) { - for (auto &o : other_viewports_) { - anon_send(o, fit_mode_atom_v, "revert"); - } - } + event_callback(Redraw); } void Viewport::switch_mirror_mode() { - if (mirror_mode_->value() != "Flop") - mirror_mode_->set_value("Flop"); + + if (mirror_mode_->value() == "Off") + mirror_mode_->set_value("Mirror Horizontally"); + else if (mirror_mode_->value() == "Mirror Horizontally") + mirror_mode_->set_value("Mirror Vertically"); + else if (mirror_mode_->value() == "Mirror Vertically") + mirror_mode_->set_value("Mirror Both"); else mirror_mode_->set_value("Off"); } @@ -842,20 +862,6 @@ Imath::V4f Viewport::normalised_pointer_position() const { 1.0f); } -Imath::V2f Viewport::image_coordinate_to_viewport_coordinate(const int x, const int y) const { - - const float aspect = float(state_.image_size_.y) / float(state_.image_size_.x); - - float norm_x = float(x) / state_.image_size_.x; - float norm_y = float(y) / state_.image_size_.y; - - Imath::V4f a(norm_x * 2.0f - 1.0f, (norm_y * 2.0f - 1.0f) * aspect, 0.0f, 1.0f); - - a *= fit_mode_matrix() * inv_projection_matrix(); - - return Imath::V2f((a.x / a.w + 1.0f) / 2.0f, (-a.y / a.w + 1.0f) / 2.0f); -} - Imath::V2f Viewport::pointer_position() const { return Imath::V2f(state_.pointer_position_.x, state_.pointer_position_.y); } @@ -864,9 +870,23 @@ Imath::V2i Viewport::raw_pointer_position() const { return state_.raw_pointer_po void Viewport::update_matrix() { + if (broadcast_fit_details_) { + anon_mail( + fit_mode_atom_v, + state_.fit_mode_, + state_.mirror_mode_, + state_.scale_, + pan(), + name(), + window_id_) + .send(global_playhead_events_group_); + } + const float flipFactor = (state_.mirror_mode_ & MirrorMode::Flip) ? -1.0f : 1.0f; const float flopFactor = (state_.mirror_mode_ & MirrorMode::Flop) ? -1.0f : 1.0f; + apply_fit_mode(); + inv_projection_matrix_.makeIdentity(); inv_projection_matrix_.scale(Imath::V3f(1.0f, -1.0f, 1.0f)); inv_projection_matrix_.scale(Imath::V3f(1.0, state_.size_.x / state_.size_.y, 1.0f)); @@ -883,37 +903,79 @@ void Viewport::update_matrix() { projection_matrix_.scale(Imath::V3f(1.0, state_.size_.y / state_.size_.x, 1.0f)); projection_matrix_.scale(Imath::V3f(1.0f, -1.0f, 1.0f)); - update_fit_mode_matrix(); + calc_image_bounds_in_viewport_pixels(); } -Imath::Box2f Viewport::image_bounds_in_viewport_pixels() const { - const Imath::M44f m = fit_mode_matrix() * inv_projection_matrix(); +void Viewport::calc_image_bounds_in_viewport_pixels() { + + const Imath::M44f m = inv_projection_matrix(); + + if (!on_screen_frames_ || !on_screen_frames_->layout_data()) { + if (!image_bounds_in_viewport_pixels_.empty()) { + image_bounds_in_viewport_pixels_.clear(); + event_callback(ImageBoundsChanged); + } + return; + } - const float aspect = float(state_.image_size_.y) / float(state_.image_size_.x); + const auto old = image_bounds_in_viewport_pixels_; + const auto &im_order = on_screen_frames_->layout_data()->image_draw_order_hint_; + image_bounds_in_viewport_pixels_.clear(); + for (const auto &i : im_order) { - Imath::Vec4 a(-1.0f, -aspect, 0.0f, 1.0f); - a *= m; + const media_reader::ImageBufPtr &im = on_screen_frames_->onscreen_image(i); + const float aspect = 1.0f / image_aspect(im); - Imath::Vec4 b(1.0f, aspect, 0.0f, 1.0f); - b *= m; + Imath::Vec4 a(-1.0f, -aspect, 0.0f, 1.0f); + a *= im.layout_transform() * m; - // note projection matrix includes the 'Flip' mode so bottom left corner - // of image might be drawn top right etc. + Imath::Vec4 b(1.0f, aspect, 0.0f, 1.0f); + b *= im.layout_transform() * m; - const float x0 = (a.x / a.w + 1.0f) / 2.0f; - const float x1 = (b.x / b.w + 1.0f) / 2.0f; - const float y0 = (-a.y / a.w + 1.0f) / 2.0f; - const float y1 = (-b.y / b.w + 1.0f) / 2.0f; + // note projection matrix includes the 'Flip' mode so bottom left corner + // of image might be drawn top right etc. - Imath::V2f topLeft(std::min(x0, x1), std::max(y0, y1)); - Imath::V2f bottomRight(std::max(x0, x1), std::min(y0, y1)); + const float x0 = (a.x / a.w + 1.0f) / 2.0f; + const float x1 = (b.x / b.w + 1.0f) / 2.0f; + const float y0 = (-a.y / a.w + 1.0f) / 2.0f; + const float y1 = (-b.y / b.w + 1.0f) / 2.0f; - return Imath::Box2f(topLeft, bottomRight); + Imath::V2f bottomLeft( + std::min(x0, x1) * state_.size_.x / state_.devicePixelRatio_, std::min(y0, y1) * state_.size_.y / state_.devicePixelRatio_); + Imath::V2f topRight( + std::max(x0, x1) * state_.size_.x / state_.devicePixelRatio_, std::max(y0, y1) * state_.size_.y / state_.devicePixelRatio_); + + image_bounds_in_viewport_pixels_.emplace_back(Imath::Box2f(bottomLeft, topRight)); + } + + if (old != image_bounds_in_viewport_pixels_) { + event_callback(ImageBoundsChanged); + } } -Imath::V2i Viewport::image_resolution() const { - Imath::V2i resolution(state_.image_size_.x, state_.image_size_.y); - return resolution; +void Viewport::update_image_resolutions() { + + if (!on_screen_frames_ || !on_screen_frames_->layout_data()) { + if (!image_resolutions_.empty()) { + image_resolutions_.clear(); + event_callback(ImageResolutionsChanged); + } + return; + } + auto old = image_resolutions_; + const auto &im_order = on_screen_frames_->layout_data()->image_draw_order_hint_; + image_resolutions_.clear(); + for (const auto &i : im_order) { + const media_reader::ImageBufPtr &im = on_screen_frames_->onscreen_image(i); + if (im) { + image_resolutions_.push_back(im->image_size_in_pixels()); + } else { + image_resolutions_.push_back(Imath::V2i(1920, 1080)); + } + } + if (image_resolutions_ != old) { + event_callback(ImageResolutionsChanged); + } } std::list> Viewport::fit_modes() { @@ -929,30 +991,25 @@ std::list> Viewport::fit_modes() { caf::message_handler Viewport::message_handler() { - auto a = caf::actor_cast(parent_actor_); - if (a) { - keyboard_events_actor_ = a->system().registry().get(keyboard_events); - } + keyboard_events_actor_ = self()->home_system().registry().get(keyboard_events); return caf::message_handler( {[=](::viewport_set_scene_coordinates_atom, - const Imath::V2f &topleft, - const Imath::V2f &topright, - const Imath::V2f &bottomright, - const Imath::V2f &bottomleft, - const Imath::V2i &scene_size, + const float x, + const float y, + const float width, + const float height, + const float window_width, + const float window_height, const float devicePixelRatio) { - float zoom = pixel_zoom(); - if (set_scene_coordinates( - topleft, - topright, - bottomright, - bottomleft, - scene_size, - devicePixelRatio)) { - if (zoom != pixel_zoom()) { - event_callback_(ZoomChanged); - } + if (set_geometry( + x*devicePixelRatio, + y*devicePixelRatio, + width*devicePixelRatio, + height*devicePixelRatio, + window_width*devicePixelRatio, + window_height*devicePixelRatio, + devicePixelRatio)) { event_callback_(Redraw); } }, @@ -960,59 +1017,46 @@ caf::message_handler Viewport::message_handler() { [=](fit_mode_atom) -> FitMode { return fit_mode(); }, - [=](fit_mode_atom, const FitMode mode) { set_fit_mode(mode); }, - - [=](fit_mode_atom, const std::string action) { - if (action == "revert") { - revert_fit_zoom_to_previous(true); - } - }, - - [=](other_viewport_atom, caf::actor other_view, bool link) { - if (link) { - - auto p = other_viewports_.begin(); - while (p != other_viewports_.end()) { - if (*p == other_view) { - return; - } - p++; + [=](fit_mode_atom, const FitMode mode) { set_fit_mode(mode, false); }, + + [=](fit_mode_atom, + const FitMode mode, + const MirrorMode mirror_mode, + const float scale, + const Imath::V2f pan, + const std::string &viewport_name, + const std::string &window_id) { + if (viewport_name == name()) + return; + + if (sync_to_main_viewport_->value() && + (window_id == "xstudio_popout_window" || + window_id == "xstudio_main_window")) { + + broadcast_fit_details_ = false; + + if (mode == FitMode::Free) { + state_.translate_ = Imath::V3f(pan.x, pan.y, 0.0f); + state_.scale_ = scale; + fit_mode_->set_value("Off", false); + state_.fit_mode_ = mode; + update_matrix(); + } else { + set_fit_mode(mode); } - other_viewports_.push_back(other_view); - link_to_module(other_view, true, true, true); - - } else { - auto p = other_viewports_.begin(); - while (p != other_viewports_.end()) { - if (*p == other_view) { - p = other_viewports_.erase(p); - } else { - p++; - } + if (mirror_mode == MirrorMode::Flip) { + mirror_mode_->set_value("Mirror Horizontally"); + } else if (mirror_mode == MirrorMode::Flop) { + mirror_mode_->set_value("Mirror Vertically"); + } else if (mirror_mode == MirrorMode::Both) { + mirror_mode_->set_value("Mirror Both"); + } else { + mirror_mode_->set_value("Off"); } - unlink_module(other_view); - } - }, + broadcast_fit_details_ = true; - [=](other_viewport_atom, - caf::actor other_view, - caf::actor other_colour_pipeline) { - other_viewports_.push_back(other_view); - link_to_module(other_view, true, true, true); - - if (other_colour_pipeline) { - // here we link up the colour pipelines of the two viewports - anon_send( - colour_pipeline_, - module::link_module_atom_v, - other_colour_pipeline, - false, // link all attrs - true, // two way link (change in one is synced to other, both ways) - viewport_index_ == - 0 // push sync (if we are main viewport, sync the - // attrs on the other colour pipelin to ourselves) - ); + event_callback(Redraw); } }, @@ -1024,15 +1068,12 @@ caf::message_handler Viewport::message_handler() { if (!playing || (playing != playing_)) { playing_ = playing; } - event_callback_(Redraw); + event_callback(Redraw); }, [=](utility::event_atom, ui::fps_monitor::fps_meter_update_atom, - const std::string &fps_expr) { - frame_rate_expr_ = fps_expr; - event_callback_(FrameRateChanged); - }, + const std::string &fps_expr) { frame_rate_expr_->set_value(fps_expr); }, [=](utility::serialise_atom) -> utility::JsonStore { utility::JsonStore jsn; @@ -1042,62 +1083,111 @@ caf::message_handler Viewport::message_handler() { [=](viewport_pan_atom) -> Imath::V2f { return pan(); }, - [=](viewport_pan_atom, const float xpan, const float ypan) { - // To use - set_pan(xpan, ypan); - event_callback_(TranslationChanged); - event_callback_(Redraw); + [=](viewport_pan_atom, const Imath::V2f pan) { + set_pan(pan.x, pan.y); + event_callback(Redraw); }, - [=](viewport_playhead_atom, caf::actor playhead, bool pin) -> bool { - playhead_pinned_ = pin; - set_playhead(playhead); - return true; + [=](viewport_pan_atom, + const float xpan, + const float ypan, + const std::string &viewport_name, + const std::string &window_id) { + // To use + // we only sync changes in main window to popout and vice versa. Two + // viewport in the same window do not sync. quickview windows do not sync + if (sync_to_main_viewport_->value() && + (window_id == "xstudio_popout_window" || + window_id == "xstudio_main_window")) { + set_pan(xpan, ypan); + event_callback(Redraw); + } }, - [=](viewport_playhead_atom, caf::actor playhead) -> bool { - if (!playhead_pinned_) - set_playhead(playhead); - return true; - }, + [=](viewport_playhead_atom, caf::actor playhead) { set_playhead(playhead); }, - [=](utility::event_atom, viewport_playhead_atom, caf::actor playhead) { - if (!playhead_pinned_) - set_playhead(playhead); + [=](viewport_playhead_atom) -> caf::actor { + return caf::actor_cast(playhead_addr_); }, - [=](viewport_playhead_atom) -> caf::actor_addr { return playhead_addr_; }, - - [=](viewport_pixel_zoom_atom, const float zoom) { - const FitMode fm = fit_mode(); - set_pixel_zoom(zoom); - event_callback_(ZoomChanged); - if (fm != fit_mode()) { - event_callback_(FitModeChanged); - } + [=](viewport_playhead_atom, bool autoconnect) { + auto_connect_to_global_selected_playhead(); }, - [=](viewport_scale_atom) -> float { return pixel_zoom(); }, + [=](viewport_scale_atom) -> float { return state_.scale_; }, - [=](viewport_scale_atom, const float scale) { - // To use + [=](viewport_scale_atom, float scale) { set_scale(scale); - event_callback_(ZoomChanged); - event_callback_(ScaleChanged); - event_callback_(Redraw); + event_callback(Redraw); + }, + + [=](viewport_scale_atom, + const float scale, + const std::string &viewport_name, + const std::string &window_id) { + // we only sync changes in main window to popout and vice versa. Two + // viewport in the same window do not sync. quickview windows do not sync + if (sync_to_main_viewport_->value() && + (window_id == "xstudio_popout_window" || + window_id == "xstudio_main_window")) { + set_scale(scale); + event_callback(Redraw); + } }, [=](quickview_media_atom, std::vector &media_items, std::string compare_mode) { quickview_media(media_items, compare_mode); }, + [=](quickview_media_atom, + std::vector &media_items, + std::string compare_mode, + const float in, + const float out) { quickview_media(media_items, compare_mode); }, + + [=](quickview_media_atom, + std::vector &media_items, + std::string compare_mode, + const int in, + const int out) { quickview_media(media_items, compare_mode, in, out); }, + [=](ui::fps_monitor::framebuffer_swapped_atom, const utility::time_point swap_time) { framebuffer_swapped(swap_time); }, - [=](aux_shader_uniforms_atom, - const utility::JsonStore &shader_extras, - const bool overwrite_and_clear) { - set_aux_shader_uniforms(shader_extras, overwrite_and_clear); + [=](playhead::redraw_viewport_atom) { event_callback(Redraw); }, + + [=](turn_off_overlay_interaction_atom, const utility::Uuid &requester_id) { + // send a message to overlays to disable themselves + for (auto &p : overlay_plugin_instances_) { + anon_mail( + ui::viewport::turn_off_overlay_interaction_atom_v, requester_id) + .send(p.second); + } + }, + + [=](ui::viewport::viewport_cursor_atom, const std::string &cursor_name) { + custom_cursor_name_->set_value(cursor_name); + }, + + [=](ui::viewport::viewport_visibility_atom) -> bool { return is_visible_; }, + + [=](utility::name_atom, const bool /*window name*/) -> std::string { + return window_id_; + }, + + [=](screen_info_atom, + const std::string &name, + const std::string &model, + const std::string &manufacturer, + const std::string &serialNumber) { + anon_mail(screen_info_atom_v, name, model, manufacturer, serialNumber) + .send(colour_pipeline_); + }, + + [=](playhead::compare_mode_atom, const std::string &compare_mode) { + // the message comes from current playhead when compare mode + // is changed + set_compare_mode(compare_mode); }, [=](const error &err) mutable { std::cerr << "ERR " << to_string(err) << "\n"; } @@ -1108,8 +1198,12 @@ caf::message_handler Viewport::message_handler() { void Viewport::set_playhead(caf::actor playhead, const bool wait_for_refresh) { - // if null playhead stop here. - if (!parent_actor_) { + if (!parent_actor_ || quickview_playhead_) { + // Note that 'set_playhead' will be called if the user switches the + // playlist that they are viewing in the main session window (with the + // playhead of the playlist). However, if we have a quickview_playhead_ + // then we must be a quickview viewport that is pinned to one piece of + // media and we therefore take no action. return; } @@ -1118,16 +1212,15 @@ void Viewport::set_playhead(caf::actor playhead, const bool wait_for_refresh) { if (old_playhead && old_playhead == playhead) { return; } else if (old_playhead) { - anon_send( - old_playhead, - connect_to_viewport_toolbar_atom_v, - name(), - name() + "_toolbar", - false); + anon_mail(module::disconnect_from_ui_atom_v).send(old_playhead); + if (window_id_ != "snapshot_viewport") { + anon_mail( + connect_to_viewport_toolbar_atom_v, name(), name() + "_toolbar", self(), false) + .send(old_playhead); + } } - auto a = caf::actor_cast(parent_actor_); - caf::scoped_actor sys(a->system()); + caf::scoped_actor sys(self()->home_system()); try { @@ -1139,16 +1232,22 @@ void Viewport::set_playhead(caf::actor playhead, const bool wait_for_refresh) { *sys, playhead_viewport_events_group_, broadcast::leave_broadcast_atom_v, - a); + self()); } catch (...) { } } if (!playhead) { playhead_addr_ = caf::actor_addr(); + playhead_uuid_ = utility::Uuid(); + event_callback(PlayheadChanged); return; } + if (is_visible_) { + anon_mail(module::connect_to_ui_atom_v).send(playhead); + } + // and join the new playhead's broacast events group that concern the // viewport playhead_viewport_events_group_ = utility::request_receive( @@ -1156,17 +1255,20 @@ void Viewport::set_playhead(caf::actor playhead, const bool wait_for_refresh) { if (playhead_viewport_events_group_) utility::request_receive( - *sys, playhead_viewport_events_group_, broadcast::join_broadcast_atom_v, a); + *sys, + playhead_viewport_events_group_, + broadcast::join_broadcast_atom_v, + self()); + + playhead_uuid_ = + utility::request_receive(*sys, playhead, utility::uuid_atom_v); - // Get the 'key' child playhead UUID - auto curr_playhead_uuid = utility::request_receive( - *sys, playhead, playhead::key_child_playhead_atom_v); // Let the fps monitor join the new playhead too - sys->anon_send(fps_monitor_, fps_monitor::connect_to_playhead_atom_v, playhead); + anon_mail(fps_monitor::connect_to_playhead_atom_v, playhead).send(fps_monitor_); // Let the fps monitor join the new playhead too - sys->anon_send(colour_pipeline_, viewport_playhead_atom_v, playhead); + anon_mail(viewport_playhead_atom_v, playhead).send(colour_pipeline_); // for off screen rendering, we need to make sure that we've fetched images // from the playhead ... @@ -1174,45 +1276,57 @@ void Viewport::set_playhead(caf::actor playhead, const bool wait_for_refresh) { // here we tell any viewport plugins that the playhead has changed for (auto p : overlay_plugin_instances_) { + utility::request_receive( *sys, p.second, module::current_viewport_playhead_atom_v, + name(), caf::actor_cast(playhead)); } // pass the new playhead to the actor that manages the queue of images // to be display now and in the near future utility::request_receive( - *sys, display_frames_queue_actor_, viewport_playhead_atom_v, playhead, true); - + *sys, + display_frames_queue_actor_, + viewport_playhead_atom_v, + UuidActor(playhead_uuid_, playhead), + true); } else { - anon_send(display_frames_queue_actor_, viewport_playhead_atom_v, playhead, false); + anon_mail(viewport_playhead_atom_v, UuidActor(playhead_uuid_, playhead), false) + .send(display_frames_queue_actor_); // here we tell any viewport plugins that the playhead has changed for (auto p : overlay_plugin_instances_) { - anon_send( - p.second, + anon_mail( module::current_viewport_playhead_atom_v, - caf::actor_cast(playhead)); + name(), + caf::actor_cast(playhead)) + .send(p.second); } } - if (viewport_index_ == 0) { - auto ph_events = - a->system().registry().template get(global_playhead_events_actor); - // tell the playhead events actor that the on-screen playhead has changed - // (in case the viewport playhead was set directly rather than from - // the playhead events actor itself). We only do this for the 'main' - // viewport, however (index == 0) - anon_send(ph_events, viewport::viewport_playhead_atom_v, playhead); + set_compare_mode(utility::request_receive( + *sys, playhead, playhead::compare_mode_atom_v)); + + // tell the playhead events actor that the on-screen playhead has changed + anon_mail(viewport::viewport_playhead_atom_v, name(), playhead) + .send(global_playhead_events_group_); + + if (window_id_ != "snapshot_viewport") { + // this message is also crucial - the playhead needs to connect to the viewport's + // toolbar so that playhead attributes like compare mode, rate, source are exposed + // in the UI via the viewport toolbar. For the offscreen snapshot viewer, we do NOT + // do the connection as there is nothing about the snapshot viewer exposed in the UI + // layer + anon_mail( + connect_to_viewport_toolbar_atom_v, name(), name() + "_toolbar", self(), true) + .send(playhead); } - anon_send( - playhead, connect_to_viewport_toolbar_atom_v, name(), name() + "_toolbar", true); - } catch (const std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } @@ -1221,12 +1335,12 @@ void Viewport::set_playhead(caf::actor playhead, const bool wait_for_refresh) { // sending a jump message forces the playhead to re-broadcast the image buffers // and in turn causes a redraw/update on this viewport if (playhead) - sys->anon_send(playhead, playhead::jump_atom_v); + anon_mail(playhead::jump_atom_v).send(playhead); playhead_addr_ = caf::actor_cast(playhead); // trigger stuff in the UI layer if necessary, like connecting the playheadUI // to the new viewport playhead - event_callback_(PlayheadChanged); + event_callback(PlayheadChanged); } void Viewport::attribute_changed(const utility::Uuid &attr_uuid, const int role) { @@ -1236,68 +1350,118 @@ void Viewport::attribute_changed(const utility::Uuid &attr_uuid, const int role) const std::string mode = fit_mode_->value(); if (mode == "1:1") - set_fit_mode(FitMode::One2One); + set_fit_mode(FitMode::One2One, is_visible_); else if (mode == "Best") - set_fit_mode(FitMode::Best); + set_fit_mode(FitMode::Best, is_visible_); else if (mode == "Width") - set_fit_mode(FitMode::Width); + set_fit_mode(FitMode::Width, is_visible_); else if (mode == "Height") - set_fit_mode(FitMode::Height); + set_fit_mode(FitMode::Height, is_visible_); else if (mode == "Fill") - set_fit_mode(FitMode::Fill); + set_fit_mode(FitMode::Fill, is_visible_); else - set_fit_mode(FitMode::Free); - + set_fit_mode(FitMode::Free, is_visible_); } else if (attr_uuid == zoom_mode_toggle_->uuid() && role == module::Attribute::Value) { if (zoom_mode_toggle_->value()) { pan_mode_toggle_->set_value(false); + custom_cursor_name_->set_value("://cursors/magnifier_cursor.svg"); + } else if (!pan_mode_toggle_->value()) { + custom_cursor_name_->set_value(""); } } else if (attr_uuid == pan_mode_toggle_->uuid() && role == module::Attribute::Value) { if (pan_mode_toggle_->value()) { zoom_mode_toggle_->set_value(false); + custom_cursor_name_->set_value("Qt.OpenHandCursor"); + } else if (!zoom_mode_toggle_->value()) { + custom_cursor_name_->set_value(""); } } else if (attr_uuid == filter_mode_preference_->uuid()) { const std::string filter_mode_pref = filter_mode_preference_->value(); for (auto opt : ViewportRenderer::pixel_filter_mode_names) { - if (filter_mode_pref == std::get<1>(opt)) { - the_renderer_->set_render_hints(std::get<0>(opt)); + if (active_renderer_ && filter_mode_pref == std::get<1>(opt)) { + active_renderer_->set_render_hints(std::get<0>(opt)); + } + } + event_callback(Redraw); + + } else if (attr_uuid == hud_toggle_->uuid() && role != module::Attribute::UIDataModels) { + + // HUD toggle should be synced between viewports viewing the same + // playhead - except the 'UIDataModels' role because otherwise when one hud + // button adds itself to a particular viewport toolbar, all the other + // hud buttons for the other viewports will get added too. + // + // TODO: Make separate HUDManagerActor that takes care of all the HUD + // plugins and interfaces between them and the viewport. Would be much + // neater. + try { + + caf::scoped_actor sys(self()->home_system()); + + // get other viewports connected to our playhead + auto other_viewports = request_receive>( + *sys, + global_playhead_events_group_, + viewport_playhead_atom_v, + caf::actor_cast(playhead_addr_), + true); + + // sync HUD setting to all linked viewports + for (auto &o : other_viewports) { + anon_mail( + module::change_attribute_value_atom_v, + "HUD", + role, + true, + utility::JsonStore(hud_toggle_->role_data_as_json(role)), + caf::actor_cast(self())) + .send(o); } + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } - event_callback_(Redraw); - } else if (attr_uuid == hud_toggle_->uuid()) { for (auto &p : hud_plugin_instances_) { - anon_send(p.second, enable_hud_atom_v, hud_toggle_->value()); + anon_mail(hud_settings_atom_v, hud_toggle_->value()).send(p.second); } + } else if (attr_uuid == mirror_mode_->uuid()) { const std::string mode = mirror_mode_->value(); - if (mode == "Flip") + if (mode == "Mirror Horizontally") set_mirror_mode(MirrorMode::Flip); - else if (mode == "Flop") + else if (mode == "Mirror Vertically") set_mirror_mode(MirrorMode::Flop); - else if (mode == "Both") + else if (mode == "Mirror Both") set_mirror_mode(MirrorMode::Both); else set_mirror_mode(MirrorMode::Off); } } +void Viewport::menu_item_activated( + const utility::JsonStore &menu_item_data, const std::string &user_data) { + + if (menu_item_data.contains("uuid") && + menu_item_data["uuid"].get() == reset_menu_item_) { + reset(); + } + + Module::menu_item_activated(menu_item_data, user_data); +} + void Viewport::update_attrs_from_preferences(const utility::JsonStore &j) { Module::update_attrs_from_preferences(j); - // TODO: proper preferences handling for the viewport renderer class - utility::JsonStore p; - p["texture_mode"] = texture_mode_preference_->value(); - the_renderer_->set_prefs(p); } -void Viewport::hotkey_pressed(const utility::Uuid &hotkey_uuid, const std::string &context) { +void Viewport::hotkey_pressed( + const utility::Uuid &hotkey_uuid, const std::string &context, const std::string &window) { if (!context.empty() && context != name()) return; @@ -1312,13 +1476,16 @@ void Viewport::hotkey_pressed(const utility::Uuid &hotkey_uuid, const std::strin pan_mode_toggle_->set_value(true); zoom_mode_toggle_->set_role_data(module::Attribute::Activated, false); zoom_mode_toggle_->set_value(false); - } else if (hotkey_uuid == reset_hotkey_) { - if (fit_mode() == FitMode::Free) - revert_fit_zoom_to_previous(); } else if (hotkey_uuid == fit_mode_hotkey_) { revert_fit_zoom_to_previous(); } else if (hotkey_uuid == mirror_mode_hotkey_) { switch_mirror_mode(); + } else if (hotkey_uuid == reset_hotkey_) { + reset(); + } else if (hotkey_uuid == hover_select_) { + hover_image_select_ = true; + } else if (hotkey_uuid == toggle_hug_) { + hud_toggle_->set_value(!hud_toggle_->value()); } } @@ -1330,195 +1497,196 @@ void Viewport::hotkey_released( } else if (hotkey_uuid == pan_hotkey_) { pan_mode_toggle_->set_role_data(module::Attribute::Activated, false); pan_mode_toggle_->set_value(false); + } else if (hotkey_uuid == hover_select_) { + hover_image_select_ = false; } } -void Viewport::update_onscreen_frame_info(const media_reader::ImageBufPtr &frame) { +void Viewport::reset() { - // this should be called by the subclass of this Viewport class just - // before or after the viewport is drawn or redrawn with the given frame + if (fit_mode() == FitMode::Free) + revert_fit_zoom_to_previous(); - if (!frame) { - on_screen_frame_buffer_.reset(); - about_to_go_on_screen_frame_buffer_.reset(); - on_screen_frame_ = 0; - event_callback_(OnScreenFrameChanged); - return; - } + pan_mode_toggle_->set_value(false); + zoom_mode_toggle_->set_value(false); + hover_image_select_ = false; - about_to_go_on_screen_frame_buffer_ = frame; + if (colour_pipeline_) + anon_mail(module::reset_module_atom_v).send(colour_pipeline_); + caf::actor playhead(caf::actor_cast(playhead_addr_)); + if (playhead) + anon_mail(module::reset_module_atom_v).send(playhead); +} - // check if the frame buffer has some error message attached - if (frame->error_state()) { - frame_error_message_->set_value(frame->error_message()); - } else { - frame_error_message_->set_value(""); - } +media_reader::ImageBufPtr Viewport::get_onscreen_image() { - // update the 'on_screen_frame_' attr, which is propagated to the QML - // layer - if (frame->params().find("playhead_frame") != frame->params().end()) { - on_screen_frame_ = frame->params()["playhead_frame"].get(); - event_callback_(OnScreenFrameChanged); - } + media_reader::ImageBufPtr result; + if (!parent_actor_) + return result; + caf::scoped_actor sys(self()->home_system()); - // - if (frame->params().find("HELD_FRAME") != frame->params().end()) { - if (frame_out_of_range_ != frame->params()["HELD_FRAME"]) { - frame_out_of_range_ = frame->params()["HELD_FRAME"]; - event_callback_(OutOfRangeChanged); + try { + + auto image_set = request_receive_wait( + *sys, + display_frames_queue_actor_, + std::chrono::seconds(10), + viewport_get_next_frames_for_display_atom_v, + true); + if (image_set) { + result = image_set->hero_image(); + } else { + throw std::runtime_error("Image set empty."); } - } else if (frame_out_of_range_) { - frame_out_of_range_ = false; - event_callback_(OutOfRangeChanged); - } - if (frame->has_alpha() == no_alpha_channel_) { + } catch (const std::exception &e) { - no_alpha_channel_ = !frame->has_alpha(); - event_callback_(NoAlphaChannelChanged); + spdlog::critical("{} : {} {}", name(), __PRETTY_FUNCTION__, e.what()); } + return result; } -void Viewport::framebuffer_swapped(const utility::time_point swap_time) { +void Viewport::update_onscreen_frame_info(const media_reader::ImageBufDisplaySetPtr &images) { - anon_send( - display_frames_queue_actor_, - ui::fps_monitor::framebuffer_swapped_atom_v, - swap_time, - screen_refresh_period_, - viewport_index_); + on_screen_frames_ = images; + needs_redraw_ = false; - static auto tp = utility::clock::now(); - auto t0 = utility::clock::now(); + if (images && images->images_layout_hash() != image_bounds_hash_) { + calc_image_bounds_in_viewport_pixels(); + update_image_resolutions(); + image_bounds_hash_ = images->images_layout_hash(); + } - if (about_to_go_on_screen_frame_buffer_ != on_screen_frame_buffer_) { + // this should be called by the subclass of this Viewport class just + // before or after the viewport is drawn or redrawn with the given frame + if (!images || !images->hero_image()) { + next_on_screen_hero_frame_.reset(); + on_screen_hero_frame_.reset(); + return; + } - on_screen_frame_buffer_ = about_to_go_on_screen_frame_buffer_; + if (state_.layout_aspect_ != images->layout_aspect() || + state_.image_size_ != images->hero_image()->image_size_in_pixels()) { + state_.image_size_ = images->hero_image()->image_size_in_pixels(); + state_.layout_aspect_ = images->layout_aspect(); + update_matrix(); + } - int f = 0; - if (on_screen_frame_buffer_ && - on_screen_frame_buffer_->params().find("playhead_frame") != - on_screen_frame_buffer_->params().end()) { - f = on_screen_frame_buffer_->params()["playhead_frame"].get(); - } + // 'images' are not actually onscreen until framebuffers have been swapped + // so we take a copy of frame and update on_screen_hero_frame_ after + // the framebuffers are swapped + next_on_screen_hero_frame_ = images->hero_image(); +} - /*static std::map ff; - if ((f-ff[viewport_index_]) != 1) { +void Viewport::framebuffer_swapped(const utility::time_point swap_time) { - std::cerr << name() << " frame missed " << f << " " << ff[viewport_index_] << " " << - std::chrono::duration_cast(t0-tp).count() << "\n"; + anon_mail(ui::fps_monitor::framebuffer_swapped_atom_v, swap_time, screen_refresh_period_) + .send(display_frames_queue_actor_); + /*if (on_screen_hero_frame_) { + int f = on_screen_hero_frame_.playhead_logical_frame(); + static auto f0 = f; + if (f != f0 && (f-f0) != 1) { + std::cerr << " dropped "; } + f0 = f; + auto t = utility::clock::now(); + static auto t0 = t; + std::cerr << std::chrono::duration_cast(t-t0).count() << " "; + t0 = t; + } else { std::cerr << "."; }*/ - ff[viewport_index_] = f;*/ + if (next_on_screen_hero_frame_ != on_screen_hero_frame_) { - anon_send( - fps_monitor(), + on_screen_hero_frame_ = next_on_screen_hero_frame_; + anon_mail( ui::fps_monitor::framebuffer_swapped_atom_v, - utility::clock::now(), - f); - - } else { - - /*std::cerr << name() << " frame repeated " << - std::chrono::duration_cast(t0-tp).count() << "\n";*/ + swap_time, + on_screen_hero_frame_.playhead_logical_frame()) + .send(fps_monitor()); } - - tp = t0; } -media_reader::ImageBufPtr Viewport::get_onscreen_image() { - std::vector next_images; - get_frames_for_display(next_images); - if (next_images.empty()) { - return media_reader::ImageBufPtr(); - } - return next_images[0]; -} +media_reader::ImageBufDisplaySetPtr Viewport::get_frames_for_display( + const bool force_playhead_sync, const utility::time_point &when_being_displayed) { -void Viewport::get_frames_for_display(std::vector &next_images) { + media_reader::ImageBufDisplaySetPtr result; if (!parent_actor_) - return; - auto a = caf::actor_cast(parent_actor_); - caf::scoped_actor sys(a->system()); + return result; + caf::scoped_actor sys(self()->home_system()); try { - auto t0 = utility::clock::now(); - - next_images = request_receive_wait>( - *sys, - display_frames_queue_actor_, - std::chrono::milliseconds(1000), - viewport_get_next_frames_for_display_atom_v); - - for (auto &image : next_images) { - - image.colour_pipe_data_ = - request_receive_wait( - *sys, - colour_pipeline_, - std::chrono::milliseconds(1000), - colour_pipeline::get_colour_pipe_data_atom_v, - image.frame_id()); + result = when_being_displayed == utility::time_point() + ? request_receive_wait( + *sys, + display_frames_queue_actor_, + std::chrono::milliseconds(1000), + viewport_get_next_frames_for_display_atom_v, + force_playhead_sync) + : request_receive_wait( + *sys, + display_frames_queue_actor_, + std::chrono::milliseconds(1000), + viewport_get_next_frames_for_display_atom_v, + when_being_displayed); + + // TED - note, I've removed the check on image_keys_hash to test if + // the images have changed. Reason is that bookmarks might change which + // (at the moment) doesn't change the image hash. + + /*size_t image_set_hash = + result ? result->images_layout_hash() + result->images_keys_hash() : 0; + if (last_images_hash_ != image_set_hash) { + last_images_hash_ = image_set_hash; + // pass on-screen images to overlay plugins + for (auto p : overlay_plugin_instances_) { + anon_mail(playhead::show_atom_v, result, name(), playing_).send(p.second); + } + }*/ - image.colour_pipe_uniforms_ = request_receive_wait( - *sys, - colour_pipeline_, - std::chrono::milliseconds(1000), - colour_pipeline::colour_operation_uniforms_atom_v, - image); + for (auto p : overlay_plugin_instances_) { + anon_mail(playhead::show_atom_v, result, name(), playing_).send(p.second); } - if (next_images.size()) { + } catch (const std::exception &e) { - auto image = next_images.front(); - // for active 'fit modes' to work we need to tell the viewport the dimensions of the - // image that is about to be drawn. - auto image_dims = image->image_size_in_pixels(); - update_fit_mode_matrix(image_dims.x, image_dims.y, image->pixel_aspect()); - } + spdlog::warn("{} : {} {}", name(), __PRETTY_FUNCTION__, e.what()); + } - std::vector going_on_screen; - if (next_images.size()) { + return result; +} - for (auto p : overlay_plugin_instances_) { +media_reader::ImageBufDisplaySetPtr +Viewport::prepare_image_for_display(const media_reader::ImageBufPtr &image_buf) const { - utility::Uuid overlay_actor_uuid = p.first; - caf::actor overlay_actor = p.second; + media_reader::ImageBufDisplaySetPtr result; - auto bdata = request_receive( - *sys, - overlay_actor, - prepare_overlay_render_data_atom_v, - next_images.front(), - name()); + if (!parent_actor_) + return result; + caf::scoped_actor sys(self()->home_system()); - next_images.front().add_plugin_blind_data2(overlay_actor_uuid, bdata); - } + try { - going_on_screen.push_back(next_images.front()); - } + result = request_receive_wait( + *sys, + display_frames_queue_actor_, + std::chrono::milliseconds(1000), + viewport_get_next_frames_for_display_atom_v, + image_buf); - // pass on-screen images to overlay plugins - for (auto p : overlay_plugin_instances_) { - anon_send(p.second, playhead::show_atom_v, going_on_screen, name(), playing_); - } + } catch (const std::exception &e) { - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + spdlog::warn("{} : {} {}", name(), __PRETTY_FUNCTION__, e.what()); } - t1_ = utility::clock::now(); + + return result; } void Viewport::instance_overlay_plugins() { - if (!parent_actor_) - return; - auto a = caf::actor_cast(parent_actor_); - caf::scoped_actor sys(a->system()); + caf::scoped_actor sys(self()->home_system()); try { @@ -1526,10 +1694,10 @@ void Viewport::instance_overlay_plugins() { // Some plugins need to know which viewport they belong to so we pass // in that info at construction ... utility::JsonStore plugin_init_data; - plugin_init_data["viewport_index"] = viewport_index_; // get the OCIO colour pipeline plugin (the only one implemented right now) - auto pm = a->system().registry().template get(plugin_manager_registry); + auto pm = + self()->home_system().registry().template get(plugin_manager_registry); auto overlay_plugin_details = request_receive>( *sys, @@ -1547,28 +1715,27 @@ void Viewport::instance_overlay_plugins() { auto overlay_actor = request_receive( *sys, pm, plugin_manager::spawn_plugin_atom_v, pd.uuid_, plugin_init_data); - if (viewport_index_ >= 0) { - anon_send( - overlay_actor, - connect_to_viewport_toolbar_atom_v, - name(), - name() + "_toolbar", - true); - anon_send(overlay_actor, module::connect_to_ui_atom_v); - } + anon_mail( + connect_to_viewport_toolbar_atom_v, + name(), + name() + "_toolbar", + self(), + true) + .send(overlay_actor); + anon_mail(module::connect_to_ui_atom_v).send(overlay_actor); auto overlay_renderer = request_receive( - *sys, overlay_actor, overlay_render_function_atom_v, viewport_index_); + *sys, overlay_actor, overlay_render_function_atom_v, name()); if (overlay_renderer) { - the_renderer_->add_overlay_renderer(pd.uuid_, overlay_renderer); + viewport_overlay_renderers_[pd.uuid_] = overlay_renderer; } auto pre_render_hook = request_receive( - *sys, overlay_actor, pre_render_gpu_hook_atom_v, viewport_index_); + *sys, overlay_actor, pre_render_gpu_hook_atom_v, name()); if (pre_render_hook) { - the_renderer_->add_pre_renderer_hook(pd.uuid_, pre_render_hook); + overlay_pre_render_hooks_[pd.uuid_] = pre_render_hook; } overlay_plugin_instances_[pd.uuid_] = overlay_actor; @@ -1589,146 +1756,86 @@ void Viewport::instance_overlay_plugins() { auto overlay_actor = request_receive( *sys, pm, plugin_manager::spawn_plugin_atom_v, pd.uuid_, plugin_init_data); - if (viewport_index_ >= 0) { - anon_send( - overlay_actor, - connect_to_viewport_toolbar_atom_v, - name(), - name() + "_toolbar", - true); - anon_send(overlay_actor, module::connect_to_ui_atom_v); - } + anon_mail( + connect_to_viewport_toolbar_atom_v, + name(), + name() + "_toolbar", + self(), + true) + .send(overlay_actor); + anon_mail(module::connect_to_ui_atom_v).send(overlay_actor); auto overlay_renderer = request_receive( - *sys, overlay_actor, overlay_render_function_atom_v, viewport_index_); + *sys, overlay_actor, overlay_render_function_atom_v, name()); if (overlay_renderer) { - the_renderer_->add_overlay_renderer(pd.uuid_, overlay_renderer); + viewport_overlay_renderers_[pd.uuid_] = overlay_renderer; } auto pre_render_hook = request_receive( - *sys, overlay_actor, pre_render_gpu_hook_atom_v, viewport_index_); + *sys, overlay_actor, pre_render_gpu_hook_atom_v, name()); if (pre_render_hook) { - the_renderer_->add_pre_renderer_hook(pd.uuid_, pre_render_hook); + overlay_pre_render_hooks_[pd.uuid_] = pre_render_hook; } overlay_plugin_instances_[pd.uuid_] = overlay_actor; hud_plugin_instances_[pd.uuid_] = overlay_actor; - anon_send(overlay_actor, enable_hud_atom_v, hud_toggle_->value()); + anon_mail(hud_settings_atom_v, hud_toggle_->value()).send(overlay_actor); } } - display_frames_queue_actor_ = - sys->spawn(overlay_plugin_instances_, viewport_index_); + display_frames_queue_actor_ = sys->spawn( + self(), overlay_plugin_instances_, name(), colour_pipeline_); } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } -} -media_reader::ImageBufPtr Viewport::get_image_from_playhead(caf::actor playhead) { - - caf::scoped_actor sys(self()->home_system()); - - // set the playhead on overlay actors that do annotations, masks for example - for (auto p : overlay_plugin_instances_) { - - utility::request_receive( - *sys, - p.second, - module::current_viewport_playhead_atom_v, - caf::actor_cast(playhead)); - } - - // get the current image for the playhead - auto image = utility::request_receive( - *sys, playhead, playhead::buffer_atom_v); - - if (image && image->error_state() == xstudio::media_reader::HAS_ERROR) { - throw std::runtime_error(image->error_message()); - } else if (!image) { - throw std::runtime_error( - "OffscreenViewport::renderToThumbnail - Playhead returned a null image."); - } - - image.colour_pipe_data_ = request_receive_wait( - *sys, - colour_pipeline_, - std::chrono::milliseconds(1000), - colour_pipeline::get_colour_pipe_data_atom_v, - image.frame_id()); - - image.colour_pipe_uniforms_ = request_receive_wait( - *sys, - colour_pipeline_, - std::chrono::milliseconds(1000), - colour_pipeline::colour_operation_uniforms_atom_v, - image); - - // get the overlay plugins to generate their data for onscreen rendering - // (e.g. annotations strokes) and add to the image - for (auto p : overlay_plugin_instances_) { - - utility::Uuid overlay_actor_uuid = p.first; - caf::actor overlay_actor = p.second; - - auto bdata = request_receive( - *sys, overlay_actor, prepare_overlay_render_data_atom_v, image, true); - - image.add_plugin_blind_data(overlay_actor_uuid, bdata); - - auto bdata2 = request_receive( - *sys, overlay_actor, prepare_overlay_render_data_atom_v, image, name()); + if (!display_frames_queue_actor_) { + display_frames_queue_actor_ = sys->spawn( + self(), overlay_plugin_instances_, name(), colour_pipeline_); + } - image.add_plugin_blind_data2(overlay_actor_uuid, bdata2); + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } - return image; + auto a = caf::actor_cast(self()); + a->link_to(display_frames_queue_actor_); } void Viewport::get_colour_pipeline() { try { - if (!parent_actor_) - return; - auto a = caf::actor_cast(parent_actor_); - caf::scoped_actor sys(a->system()); + caf::scoped_actor sys(self()->home_system()); auto colour_pipe_manager = - a->system().registry().get(colour_pipeline_registry); + self()->home_system().registry().get(colour_pipeline_registry); auto colour_pipe = request_receive( *sys, colour_pipe_manager, xstudio::colour_pipeline::colour_pipeline_atom_v, - name()); + name(), + window_id_); if (colour_pipeline_ != colour_pipe) { colour_pipeline_ = colour_pipe; auto colour_pipe_gpu_hook = request_receive( - *sys, colour_pipeline_, pre_render_gpu_hook_atom_v, viewport_index_); + *sys, colour_pipeline_, pre_render_gpu_hook_atom_v, name()); if (colour_pipe_gpu_hook) { - the_renderer_->add_pre_renderer_hook( - utility::Uuid("4aefe9d8-a53d-46a3-9237-9ff686790c46"), - colour_pipe_gpu_hook); + overlay_pre_render_hooks_[utility::Uuid( + "4aefe9d8-a53d-46a3-9237-9ff686790c46")] = colour_pipe_gpu_hook; } } // negative index is offscreen - anon_send(colour_pipeline_, module::connect_to_ui_atom_v); - anon_send( - colour_pipeline_, + anon_mail(module::connect_to_ui_atom_v).send(colour_pipeline_); + anon_mail( colour_pipeline::connect_to_viewport_atom_v, - self(), name(), name() + "_toolbar", - true); - - anon_send( - display_frames_queue_actor_, - colour_pipeline::colour_pipeline_atom_v, - colour_pipeline_); + true, + self()) + .send(colour_pipeline_); } catch (std::exception &e) { spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); @@ -1741,99 +1848,260 @@ void Viewport::set_screen_infos( const std::string &manufacturer, const std::string &serialNumber, const double refresh_rate) { - anon_send( - colour_pipeline_, + + anon_mail( /*utility::event_atom_v,*/ xstudio::ui::viewport::screen_info_atom_v, name, model, manufacturer, - serialNumber); - if (refresh_rate) - screen_refresh_period_ = timebase::to_flicks(1.0 / refresh_rate); + serialNumber) + .send(colour_pipeline_); + + // N.B. The screen refresh rate would be stored here, and passed on to + // the ViewportFrameQueueActor. It uses the refresh rate for crucial estimation + // of the playhead position for the next redraw for accurate frame timing. + // However, I am finding the the refresh_rate value that comes from the + // QScreen object in the QT layer is not reliable! When running PCOIP, for + // example, it gives me a weird refresh value of 29.9553hz. Measuring the + // refresh returns 60Hz, however (confirmed with glxgears). Therefore I am + // disabling the use of the display refresh_rate value that we get from Qt. + // Instead the ViewportFrameQueueActor will do a live statistical measurement + // of the refresh period. + + /*screen_refresh_period_ = timebase::to_flicks(1.0 / refresh_rate);*/ } -void Viewport::quickview_media(std::vector &media_items, std::string compare_mode) { +void Viewport::quickview_media( + std::vector &media_items, + std::string compare_mode, + const int in_pt, + const int out_pt) { - // Check if the compare mode is valid.. - if (compare_mode == "") - compare_mode = "Off"; - bool valid_compare_mode = false; - for (const auto &cmp : playhead::PlayheadBase::compare_mode_names) { - if (compare_mode == std::get<1>(cmp)) { - valid_compare_mode = true; - break; - } - } - if (!valid_compare_mode) { - spdlog::warn( - "{} Invalid compare mode passed with --quick-view option: {}", - __PRETTY_FUNCTION__, - compare_mode); - return; - } - - auto a = caf::actor_cast(parent_actor_); - caf::scoped_actor sys(a->system()); + caf::scoped_actor sys(self()->home_system()); - if (!quickview_playhead_) { + auto playhead = quickview_playhead_; + if (!playhead) { // create a new quickview playhead, or use existing one. - quickview_playhead_ = sys->spawn("QuickviewPlayhead"); + playhead = sys->spawn( + "QuickviewPlayhead", playhead::INDEPENDENT_AUDIO); } // set the compare mode - anon_send( - quickview_playhead_, + anon_mail( module::change_attribute_request_atom_v, std::string("Compare"), (int)module::Attribute::Value, - utility::JsonStore(compare_mode)); + utility::JsonStore(compare_mode)) + .send(playhead); + + // make the playhead view the media (blocking request) + request_receive(*sys, playhead, playhead::source_atom_v, media_items); - // make the playhead view the media - anon_send(quickview_playhead_, playhead::source_atom_v, media_items); + // view the playhead + set_playhead(playhead, true); + + if (in_pt != -1) { + anon_mail(playhead::simple_loop_start_atom_v, in_pt).send(playhead); + } + + if (out_pt != -1) { + anon_mail(playhead::simple_loop_end_atom_v, out_pt).send(playhead); + } + + if (in_pt != -1 || out_pt != -1) { + anon_mail(playhead::use_loop_range_atom_v, true).send(playhead); + } // view the playhead - set_playhead(quickview_playhead_, true); + set_playhead(playhead, true); - playhead_pinned_ = true; + quickview_playhead_ = playhead; } -void Viewport::auto_connect_to_playhead(bool auto_connect) { +void Viewport::set_visibility(bool is_visible) { - listen_to_playhead_events(auto_connect); + is_visible_ = is_visible; + auto playhead = caf::actor_cast(playhead_addr_); + if (is_visible_ && playhead) { + anon_mail(module::connect_to_ui_atom_v).send(playhead); + } +} - if (auto_connect) { +utility::JsonStore ViewportRenderer::core_shader_params( + const media_reader::ImageBufPtr &image_to_be_drawn, + const Imath::M44f &window_to_viewport_matrix, + const Imath::M44f &viewport_to_image_space, + const float viewport_du_dx, + const utility::JsonStore &layout_data, + const int image_index) const { + + utility::JsonStore shader_params; + shader_params["to_coord_system"] = viewport_to_image_space.inverse(); + shader_params["to_canvas"] = window_to_viewport_matrix; + shader_params["use_alpha"] = false; + + if (image_to_be_drawn) { + // here we can work out the ratio of image pixels to screen pixels + const float image_pix_to_screen_pix = + image_to_be_drawn->image_size_in_pixels().x * viewport_du_dx; + if (render_hints_ == AlwaysBilinear) + shader_params["use_bilinear_filtering"] = + image_pix_to_screen_pix < 0.99999f || image_pix_to_screen_pix > 1.00001f; + else if (render_hints_ == BilinearWhenZoomedOut) + shader_params["use_bilinear_filtering"] = + image_pix_to_screen_pix > 1.00001f; // filter_mode_ == BilinearWhenZoomedOut + else { + shader_params["use_bilinear_filtering"] = false; + } + shader_params["image_aspect"] = image_aspect(image_to_be_drawn); + shader_params["image_transform_matrix"] = image_to_be_drawn.layout_transform(); + } + return shader_params; +} - // fetch the current playhead (if there is one) and connect to it - auto a = caf::actor_cast(parent_actor_); - if (!a) - return; - caf::scoped_actor sys(a->system()); +void Viewport::render() const { + + if (render_data_ && render_data_->renderer) { + + render_data_->renderer->render( + render_data_->images, + render_data_->window_to_viewport_matrix, + render_data_->projection_matrix, + render_data_->window_size, + render_data_->overlay_renderers); + } +} + +void Viewport::render(const utility::time_point &when_going_on_screen) { + // rendering in the same thread, with estimate of when image goes + // on screen - used by offscreen renderer + prepare_render_data(when_going_on_screen); + render(); +} + +void Viewport::render(const media_reader::ImageBufPtr &image_buf) { + + // rendering in the same thread, rendering a single image + // - used by offscreen renderer + + RenderData *rdata = new RenderData; + rdata->images = prepare_image_for_display(image_buf); + update_onscreen_frame_info(rdata->images); + rdata->window_to_viewport_matrix = window_to_viewport_matrix(); + rdata->projection_matrix = projection_matrix(); + rdata->overlay_renderers = viewport_overlay_renderers_; + rdata->renderer = active_renderer_; + rdata->window_size = state_.window_size_; + render_data_.reset(rdata); - auto ph_events = - a->system().registry().template get(global_playhead_events_actor); + render(); +} + +void Viewport::prepare_render_data( + const utility::time_point &when_going_on_screen, const bool sync_to_playhead) { + // here we make data for rendering in separate thread + RenderData *rdata = new RenderData; + rdata->images = get_frames_for_display(sync_to_playhead, when_going_on_screen); + update_onscreen_frame_info(rdata->images); + rdata->window_to_viewport_matrix = window_to_viewport_matrix(); + rdata->projection_matrix = projection_matrix(); + rdata->overlay_renderers = viewport_overlay_renderers_; + rdata->renderer = active_renderer_; + rdata->window_size = state_.window_size_; + render_data_.reset(rdata); +} + +void Viewport::set_compare_mode(const std::string &compare_mode) { + + if (compare_mode_ == compare_mode) + return; - auto playhead = - request_receive(*sys, ph_events, viewport::viewport_playhead_atom_v); + compare_mode_ = compare_mode; - if (playhead) - set_playhead(playhead); + try { + + caf::scoped_actor sys(self()->home_system()); + + auto layouts_manager = + self()->home_system().registry().template get(viewport_layouts_manager); + + // get the actor that provides the layout + auto layout_actor = request_receive( + *sys, + layouts_manager, + viewport_layout_atom_v, + compare_mode, + sync_to_main_viewport_->value(), + name()); + + // pass it over to the frame queue actor, which sends the image + // set to the layout actor to do the actual layout + anon_mail(viewport_layout_atom_v, layout_actor, compare_mode) + .send(display_frames_queue_actor_); + + // fetch the full prefs dict - the viewport renderer may require some + // prefs for configuration + JsonStore prefs_jsn; + auto prefs = global_store::GlobalStoreHelper(self()->home_system()); + prefs.get_group(prefs_jsn); + + // get the viewport renderer for the layout/compare mode + active_renderer_ = request_receive( + *sys, layout_actor, viewport_renderer_atom_v, window_id_, prefs_jsn); + + active_renderer_->set_pre_renderer_hooks(overlay_pre_render_hooks_); + + update_matrix(); + event_callback(Redraw); + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); } } -void Viewport::set_aux_shader_uniforms( - const utility::JsonStore &j, const bool clear_and_overwrite) { - if (clear_and_overwrite) { - aux_shader_uniforms_ = j; - } else if (j.is_object()) { - for (auto o = j.begin(); o != j.end(); ++o) { - aux_shader_uniforms_[o.key()] = o.value(); + +void Viewport::pointer_select_media(const PointerEvent &pointer_event) { + + if (!(pointer_event.modifiers() == Signature::Modifier::ControlModifier || + pointer_event.modifiers() == Signature::Modifier::NoModifier)) + return; + + if (!on_screen_frames_ || !on_screen_frames_->layout_data()) + return; + + const auto &im_order = on_screen_frames_->layout_data()->image_draw_order_hint_; + + if (im_order.size() != image_bounds_in_viewport_pixels_.size()) + return; + + // lazy! + thread_local utility::Uuid last_im_id; + + // lreverse oop through image bounds (last one is drawn on-top) + // these are the images visible in the layout + for (int i = (int(image_bounds_in_viewport_pixels_.size()) - 1); i >= 0; --i) { + + const auto &im_bounds = image_bounds_in_viewport_pixels_[i]; + + // is the mouse inside the image boundary? + if (im_bounds.min.x <= pointer_event.x() && im_bounds.max.x >= pointer_event.x() && + im_bounds.min.y <= pointer_event.y() && im_bounds.max.y >= pointer_event.y()) { + + // .. yes. send the corresponding media UUID to the playhead + + // resolve the image idx in the layout to the image index in the + // onscreen image set ... + const media_reader::ImageBufPtr &im = + on_screen_frames_->onscreen_image(im_order[i]); + if (im && playhead_addr_ && last_im_id != im.frame_id().media_uuid()) { + last_im_id = im.frame_id().media_uuid(); + anon_mail( + playlist::select_media_atom_v, + im.frame_id().media_uuid(), + i, + pointer_event.modifiers() == Signature::Modifier::ControlModifier) + .send(caf::actor_cast(playhead_addr_)); + break; + } } - } else { - spdlog::warn( - "{} Invalid shader uniforms data:\n\"{}\".\n\nIt must be a dictionary of key/value " - "pairs.", - __PRETTY_FUNCTION__, - j.dump(2)); } - - the_renderer_->set_aux_shader_uniforms(aux_shader_uniforms_); } diff --git a/src/ui/viewport/src/viewport_frame_queue_actor.cpp b/src/ui/viewport/src/viewport_frame_queue_actor.cpp index 17dbe83dd..1652895bf 100644 --- a/src/ui/viewport/src/viewport_frame_queue_actor.cpp +++ b/src/ui/viewport/src/viewport_frame_queue_actor.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include "xstudio/atoms.hpp" +#include "xstudio/media_reader/image_buffer_set.hpp" #include "xstudio/ui/viewport/viewport_frame_queue_actor.hpp" -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/uuid.hpp" @@ -12,97 +12,91 @@ using namespace xstudio; ViewportFrameQueueActor::ViewportFrameQueueActor( caf::actor_config &cfg, + caf::actor viewport, std::map overlay_actors, - const int viewport_index) + const std::string &viewport_name, + caf::actor colour_pipeline) : caf::event_based_actor(cfg), - overlay_actors_(std::move(overlay_actors)), - viewport_index_(viewport_index) { + viewport_(viewport), + viewport_overlay_plugins_(std::move(overlay_actors)), + viewport_name_(viewport_name), + colour_pipeline_(colour_pipeline) { print_on_exit(this, "ViewportFrameQueueActor"); - set_default_handler(caf::drop); - - set_down_handler([=](down_msg &msg) { - // find in playhead list.. - if (msg.source == playhead_) { - demonitor(playhead_); - playhead_ = caf::actor(); - frames_to_draw_per_playhead_.clear(); - } - }); - behavior_.assign( [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, [=](viewport_playhead_atom, - caf::actor playhead, + utility::UuidActor playhead, const bool prefetch_inital_image) -> result { - auto rp = make_response_promise(); - // join the playhead's broadcast grop - request(playhead, infinite, utility::get_group_atom_v) - .then( - [=](caf::actor broadcast_group) mutable { - caf::scoped_actor sys(system()); - - if (playhead_broadcast_group_) { - try { - request_receive( - *sys, - playhead_broadcast_group_, - broadcast::leave_broadcast_atom_v, - this); - } catch (...) { - } - } - playhead_broadcast_group_ = broadcast_group; - request_receive( - *sys, - playhead_broadcast_group_, - broadcast::join_broadcast_atom_v, - this); - }, - [=](const error &err) mutable { - - }); - - // Get the 'key' child playhead UUID - request(playhead, infinite, playhead::key_child_playhead_atom_v) - .then( - [=](const utility::Uuid &curr_playhead_uuid) mutable { - current_playhead_ = curr_playhead_uuid; - // fetch a frame so we have somethign to show immediately - if (prefetch_inital_image) { - request(playhead, infinite, playhead::buffer_atom_v) - .then( - [=](media_reader::ImageBufPtr buf) mutable { - queue_image_buffer_for_drawing(buf, curr_playhead_uuid); - rp.deliver(true); - drop_old_frames( - utility::clock::now() - - std::chrono::milliseconds(100)); - }, - [=](const error &err) mutable { rp.deliver(err); }); - } else { - rp.deliver(true); - } - }, - [=](const error &err) mutable { rp.deliver(err); }); - - if (playhead_) - demonitor(playhead_); - playhead_ = playhead; - monitor(playhead_); - return rp; + return set_playhead(playhead, prefetch_inital_image); }, [=](playhead::child_playheads_deleted_atom, const std::vector &child_playhead_uuids) { - child_playheads_deleted(child_playhead_uuids); + for (const auto &uuid : child_playhead_uuids) { + deleted_playheads_.insert(uuid); + } }, [=](playhead::key_child_playhead_atom, - const utility::Uuid &playhead_uuid, - const utility::time_point &tp) { current_playhead_ = playhead_uuid; }, + const utility::UuidVector &child_playhead_uuids) { + sub_playhead_ids_ = child_playhead_uuids; + }, + + [=](playhead::key_child_playhead_atom, + utility::UuidActor sub_playhead, + const utility::time_point tp) { + // playhead is telling us its key sub-playhead has changed. Request + // an image buffer from the sub-playhead. + + // The logic here is to try and ensure the viewport updates with an + // imag when the user is quickly scrolling through the media list. + // Every time the media selection in the UI changes, the playhead + // creates a new subplayhead for each bit of media selected and + // destroys the old sub-playheads for media that was previously + // selected but is no longer selected. + + // It might be that the sub-playheads are created at a faster rate + // than the media reader can load/decode a frame for the given + // media for display. + + // So, we do our best here. We just need to make sure we are showing + // the most recent frame that has come from the subplayhead(s). + + // check if we've got this switch message out-of-order, i.e. we have + // already processed a message that was sent *AFTER* the one we are + // processing now. + if (tp < last_playhead_switch_tp_) + return; + last_playhead_switch_tp_ = tp; + + mail(playhead::buffer_atom_v) + .request(sub_playhead.actor(), infinite) + .then( + [=](media_reader::ImageBufPtr &intial_frame) { + // ensure we only store this intial_frame for the viewport + // and set the playhead ID if it's come via a message that came + // *after* the last time we set the current_key_sub_playhead_id_ + if (tp >= last_playhead_set_tp_) { + + queue_image_buffer_for_drawing(intial_frame, sub_playhead.uuid()); + if (current_key_sub_playhead_id_ != sub_playhead.uuid()) { + previous_key_sub_playhead_id_ = current_key_sub_playhead_id_; + } + current_key_sub_playhead_id_ = sub_playhead.uuid(); + clear_images_from_old_playheads(); + last_playhead_set_tp_ = tp; + } + }, + [=](caf::error &err) { + // playhead failed to return an image for the given sub playhead id. + // Ignore the error, as it's quite likely due to the sub playhead + // exiting as the parent playhead rebuilt itself as user rapidly + // switched the media they were viewing + }); + }, [=](playhead::colour_pipeline_lookahead_atom, const media::AVFrameIDsAndTimePoints &frame_ids_for_colour_mgmnt_lookeahead) { @@ -117,11 +111,20 @@ ViewportFrameQueueActor::ViewportFrameQueueActor( // colour pipe to do its thing at draw time. We assume that the colour_pipeline_ has // an effective local cacheing system so when we do actually need that data // immediately at draw time it will be available immediately. - if (colour_pipeline_ && viewport_index_ >= 0) { - anon_send( - colour_pipeline_, - colour_pipeline::get_colour_pipe_data_atom_v, - frame_ids_for_colour_mgmnt_lookeahead); + anon_mail( + colour_pipeline::get_colour_pipe_data_atom_v, + frame_ids_for_colour_mgmnt_lookeahead) + .send(colour_pipeline_); + + // While we are at it, we can also send these frameIDs to other overlay plugins that + // might want to fetch/compute data for media that is about to go on-screen. + // For example, the Media Metadata HUD plugin uses this to fetch metadata for the + // media that is due on screen soon. + for (auto p : viewport_overlay_plugins_) { + anon_mail( + playhead::colour_pipeline_lookahead_atom_v, + frame_ids_for_colour_mgmnt_lookeahead) + .send(p.second); } }, @@ -129,21 +132,17 @@ ViewportFrameQueueActor::ViewportFrameQueueActor( [=](playhead::play_forward_atom, const bool forward) { playing_forwards_ = forward; }, - [=](colour_pipeline::colour_pipeline_atom, caf::actor colour_pipeline) { - colour_pipeline_ = colour_pipeline; - }, - [=](ui::fps_monitor::framebuffer_swapped_atom, const utility::time_point &message_send_tp, - const timebase::flicks video_refresh_rate_hint, - const int viewport_index) { + const timebase::flicks video_refresh_rate_hint) { // this incoming message originates from the video layer and 'message_send_tp' // should be, as accurately as possible, the actual time that the framebuffer was // swapped to the screen. - - video_refresh_data_.refresh_history_.push_back(message_send_tp); - if (video_refresh_data_.refresh_history_.size() > 128) { - video_refresh_data_.refresh_history_.pop_front(); + if (playing_) { + video_refresh_data_.refresh_history_.push_back(message_send_tp); + if (video_refresh_data_.refresh_history_.size() > 128) { + video_refresh_data_.refresh_history_.pop_front(); + } } video_refresh_data_.refresh_rate_hint_ = video_refresh_rate_hint; video_refresh_data_.last_video_refresh_ = message_send_tp; @@ -158,6 +157,7 @@ ViewportFrameQueueActor::ViewportFrameQueueActor( playing_ = is_playing; queue_image_buffer_for_drawing(buf, playhead_uuid); drop_old_frames(utility::clock::now() - std::chrono::milliseconds(100)); + anon_mail(playhead::redraw_viewport_atom_v).send(viewport_); }, // these are frame bufs that we expect to draw in the very near future @@ -173,40 +173,81 @@ ViewportFrameQueueActor::ViewportFrameQueueActor( drop_old_frames(utility::clock::now() - std::chrono::milliseconds(100)); }, - [=](viewport_get_next_frames_for_display_atom) - -> result> { - auto rp = make_response_promise>(); - - std::vector r; - get_frames_for_display(current_playhead_, r); - if (r.size() != 1 || playing_) { - rp.deliver(r); - } else if (r[0]) { + [=](viewport_get_next_frames_for_display_atom, + const utility::time_point &when_going_on_screen) + -> result { + auto rp = make_response_promise(); + get_frames_for_display(rp, when_going_on_screen); + return rp; + }, - update_image_blind_data_and_deliver(r[0], rp); + [=](viewport_get_next_frames_for_display_atom) + -> result { + auto rp = make_response_promise(); + get_frames_for_display(rp); + return rp; + }, + [=](viewport_get_next_frames_for_display_atom, + const bool force_sync) -> result { + auto rp = make_response_promise(); + if (!force_sync || !playhead_) { + // here the result is based on the frames that the playhead is + // broadcasting asynchronously to us + rp.delegate( + caf::actor_cast(this), + viewport_get_next_frames_for_display_atom_v); } else { - rp.deliver(r); + get_frames_for_display_sync(rp); } + return rp; + }, + [=](viewport_get_next_frames_for_display_atom, + const media_reader::ImageBufPtr &lone_image) + -> result { + // If something wants the viewport to render a single image that has + // been fetched independently (for example this is how offscreen + // generation of thumbnail images is donw) we need to run this + // through our routine that adds colour management and overlay data + // to the ImageBuffer and buid an ImageBufDisplaySetPtr for rendering. + auto rp = make_response_promise(); + static const utility::Uuid dummy_playhead_id = utility::Uuid::generate(); + media_reader::ImageBufDisplaySet *result = + new media_reader::ImageBufDisplaySet({dummy_playhead_id}); + result->add_on_screen_image(dummy_playhead_id, lone_image); + append_overlays_data(rp, result); return rp; }, + [=](viewport_layout_atom, caf::actor layout_manager, const std::string &layout_name) { + viewport_layout_manager_ = layout_manager; + viewport_layout_mode_name_ = layout_name; + }, + [=](utility::event_atom, playhead::media_source_atom, caf::actor media_actor, const utility::Uuid &media_uuid) { - if (colour_pipeline_) { - anon_send( - colour_pipeline_, - utility::event_atom_v, - playhead::media_source_atom_v, - media_actor, - media_uuid); - } + anon_mail( + utility::event_atom_v, playhead::media_source_atom_v, media_actor, media_uuid) + .send(colour_pipeline_); + }, + [=](utility::event_atom, playhead::velocity_atom, const float velocity) { + playhead_velocity_ = velocity; + }, + [=](const error &err) mutable { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); }, + [=](caf::message) {}); +} + +ViewportFrameQueueActor::~ViewportFrameQueueActor() {} - [=](const error &err) mutable { aout(this) << err << std::endl; }); +void ViewportFrameQueueActor::on_exit() { + viewport_layout_manager_ = caf::actor(); + playhead_ = utility::UuidActor(); + caf::event_based_actor::on_exit(); } xstudio::media_reader::ImageBufPtr ViewportFrameQueueActor::get_least_old_image_in_set( @@ -216,11 +257,11 @@ xstudio::media_reader::ImageBufPtr ViewportFrameQueueActor::get_least_old_image_ return media_reader::ImageBufPtr(); media_reader::ImageBufPtr least_old_buf; auto r = frames_queued_for_display.begin(); - least_old_buf = *r; + least_old_buf = r->second; r++; while (r != frames_queued_for_display.end()) { - if ((*r).when_to_display_ > least_old_buf.when_to_display_) { - least_old_buf = *r; + if (r->second.when_to_display_ > least_old_buf.when_to_display_) { + least_old_buf = r->second; } r++; } @@ -237,7 +278,7 @@ void ViewportFrameQueueActor::drop_old_frames(const utility::time_point out_of_d get_least_old_image_in_set(frames_queued_for_display); auto r = frames_queued_for_display.begin(); while (r != frames_queued_for_display.end()) { - if ((*r).when_to_display_ < out_of_date_threshold) { + if (r->second.when_to_display_ < out_of_date_threshold) { r = frames_queued_for_display.erase(r); } else { r++; @@ -246,9 +287,8 @@ void ViewportFrameQueueActor::drop_old_frames(const utility::time_point out_of_d // if all the images in the queue are older than out_of_date_threshold then // we add back in the 'least old' buffer so we have something to show. if (frames_queued_for_display.empty() && least_old_buf) { - frames_queued_for_display.insert(frames_queued_for_display.end(), least_old_buf); + frames_queued_for_display[least_old_buf.timeline_timestamp()] = least_old_buf; } - std::sort(frames_queued_for_display.begin(), frames_queued_for_display.end()); } } @@ -256,213 +296,296 @@ void ViewportFrameQueueActor::queue_image_buffer_for_drawing( media_reader::ImageBufPtr &buf, const utility::Uuid &playhead_id) { auto &frames_queued_for_display = frames_to_draw_per_playhead_[playhead_id]; + frames_queued_for_display[buf.timeline_timestamp()] = buf; +} - auto p = frames_queued_for_display.begin(); - while (p != frames_queued_for_display.end()) { - // there can only be one image in the display queue for a given - // timeline timestamp. Remove images already in the queue so the - // incoming can be added. - if ((*p).timeline_timestamp() == buf.timeline_timestamp()) { - p = frames_queued_for_display.erase(p); - } else { - p++; +void ViewportFrameQueueActor::get_frames_for_display_sync( + caf::typed_response_promise rp) { + + // in 'force_sync' mode we request and wait for the playhead + // to read all images for the current set of on-screen sources + media_reader::ImageBufDisplaySet *result = + new media_reader::ImageBufDisplaySet(sub_playhead_ids_); + auto count = std::make_shared(sub_playhead_ids_.size()); + int k_idx = 0; + for (const auto playhead_id : sub_playhead_ids_) { + + if (current_key_sub_playhead_id_ == playhead_id) { + result->set_hero_sub_playhead_index(k_idx); } + k_idx++; + // here we fetch the on-screen image buffer for the given sub-playhead + // from the playhead + const auto id = playhead_id; + mail(playhead::buffer_atom_v, playhead_id) + .request(playhead_.actor(), infinite) + .then( + [=](const media_reader::ImageBufPtr &buf) mutable { + if (buf) { + result->add_on_screen_image(id, buf); + } + (*count)--; + if (!(*count)) { + // we have all images, now proceed to add extra data + append_overlays_data(rp, result); + } + }, + [=](caf::error &err) mutable { + rp.deliver(err); + (*count)--; + if (!(*count)) { + append_overlays_data(rp, result); + } + }); } - - frames_queued_for_display.push_back(buf); } void ViewportFrameQueueActor::get_frames_for_display( - const utility::Uuid &playhead_id, std::vector &next_images) { + caf::typed_response_promise rp, + const utility::time_point &when_going_on_screen) { + // evaluate the position of the playhead at the timepoint when the viewport + // redraw happens (or, more precisely, when the buffer that it is drawn + // to is swapped to the display) + const auto playhead_position = when_going_on_screen == utility::time_point() + ? predicted_playhead_position_at_next_video_refresh() + : predicted_playhead_position(when_going_on_screen); - if (frames_to_draw_per_playhead_.find(playhead_id) == frames_to_draw_per_playhead_.end()) { - // no images queued for display for the indicated playhead - return; - } + media_reader::ImageBufDisplaySet *result = + new media_reader::ImageBufDisplaySet(sub_playhead_ids_); + int k_idx = 0; + for (const auto playhead_id : sub_playhead_ids_) { - // find the entry in our queue of frames to draw whose display timestamp - // is closest to 'now' - auto &frames_queued_for_display = frames_to_draw_per_playhead_[playhead_id]; - if (frames_queued_for_display.empty()) { - return; - } + if (current_key_sub_playhead_id_ == playhead_id) { + result->set_hero_sub_playhead_index(k_idx); + } + if (previous_key_sub_playhead_id_ == playhead_id) { + result->set_previous_hero_sub_playhead_index(k_idx); + } + k_idx++; - const auto playhead_position = predicted_playhead_position_at_next_video_refresh(); + if (frames_to_draw_per_playhead_.find(playhead_id) == + frames_to_draw_per_playhead_.end()) { + // no images queued for display for the indicated playhead + continue; + } - auto r = std::lower_bound( - frames_queued_for_display.begin(), frames_queued_for_display.end(), playhead_position); + // find the entry in our queue of frames to draw whose display timestamp + // is closest to 'now' + auto &frames_queued_for_display = frames_to_draw_per_playhead_[playhead_id]; + if (frames_queued_for_display.empty()) { + continue; + } - if (r == frames_queued_for_display.end()) { - // No entry in the queue has a higher display timestamp - // than the current playhead position, so pick the last - r--; - } else if ( - r != frames_queued_for_display.begin() && - (*r).timeline_timestamp() != playhead_position) { + auto r = frames_queued_for_display.upper_bound(playhead_position); - // upper bound gives us the first frame whose timeline - // timestamp is *after* playhead_position, so we decrement - // it once to get the frame that should be on screen. - r--; - } + if (r == frames_queued_for_display.end()) { + // No entry in the queue has a higher display timestamp + // than the current playhead position, so pick the last + r--; + } else if ( + r != frames_queued_for_display.begin() && + r->second.timeline_timestamp() != playhead_position) { - next_images.push_back(*r); + // upper bound gives us the first frame whose timeline + // timestamp is *after* playhead_position, so we decrement + // it once to get the frame that should be on screen. + r--; + } - auto r_next = r; - if (playing_forwards_) { - r_next++; - while (r_next != frames_queued_for_display.end() && next_images.size() < 4) { + result->add_on_screen_image(playhead_id, r->second); - next_images.push_back(*r_next); + // now we add 'future frames' - i.e. frames that are not onscreen now + // but will be going on-screen next. We supply these to the viewport so + // that it can do asynchronous transfers of data to VRAM, i.e. copying + // the image data to the GPU for the next image(s) while the current image + // is being drawn to the screen. + auto r_next = r; + if (playing_forwards_) { r_next++; - } - } else { - while (r_next != frames_queued_for_display.begin() && next_images.size() < 4) { - r_next--; - next_images.push_back(*r_next); + while (r_next != frames_queued_for_display.end() && + result->num_future_images(playhead_id) < 2) { + + result->append_future_image(playhead_id, r_next->second); + r_next++; + if (r_next == frames_queued_for_display.end()) { + r_next = frames_queued_for_display.begin(); + } + } + } else { + while (r_next != frames_queued_for_display.begin() && + result->num_future_images(playhead_id) < 2) { + r_next--; + result->append_future_image(playhead_id, r_next->second); + if (r_next == frames_queued_for_display.begin()) { + r_next = frames_queued_for_display.end(); + r_next--; + } + } } } // now that we have picked frames for display, the first frame in 'next_images' // is the one that should be on-screen now ... and frames that should have been // displayed before this one can be dropped from the queue - if (next_images.size()) { - drop_old_frames((*next_images.begin()).when_to_display_); + if (!result->empty()) { + // drop_old_frames(result->when_to_display()); } - auto sys = caf::scoped_actor(home_system()); - for (auto p : overlay_actors_) { - - utility::Uuid overlay_actor_uuid = p.first; - caf::actor overlay_actor = p.second; - for (media_reader::ImageBufPtr &im : next_images) { - try { - auto bdata = request_receive( - *sys, overlay_actor, prepare_overlay_render_data_atom_v, im, false); - im.add_plugin_blind_data(overlay_actor_uuid, bdata); - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - } - } + append_overlays_data(rp, result); } -void ViewportFrameQueueActor::child_playheads_deleted( - const std::vector &child_playhead_uuids) { - - for (const auto &uuid : child_playhead_uuids) { - - auto it = frames_to_draw_per_playhead_.find(uuid); - if (it != frames_to_draw_per_playhead_.end()) { - frames_to_draw_per_playhead_.erase(it); - } +void ViewportFrameQueueActor::append_overlays_data( + caf::typed_response_promise rp, + media_reader::ImageBufDisplaySet *result) { + + // In the next steps we are doing a-sync requests to get data added to + // 'result'... we make a counter of the number of requests we'll be making + // and decrement it until it hits zero then we know we are finished. The + // counter has to be a shared pointer as the lambda request response handlers + // make their own copy of response_count. If it weren't a shared pointer the + // handler would be decrementing their copy, not a global (shared) counter. + auto response_count = + std::make_shared(viewport_overlay_plugins_.size() * result->num_onscreen_images()); + if (!*response_count) { + result->finalise(); + rp.deliver(media_reader::ImageBufDisplaySetPtr(result)); + return; } -} -void ViewportFrameQueueActor::add_blind_data_to_image_in_queue( - const media_reader::ImageBufPtr &image, - const utility::BlindDataObjectPtr &bdata, - const utility::Uuid &overlay_actor_uuid) { - - for (auto &per_playhead : frames_to_draw_per_playhead_) { - OrderedImagesToDraw frames_queued_for_display = per_playhead.second; - bool changed = false; - for (auto &im : frames_queued_for_display) { - if (im == image) { - im.add_plugin_blind_data(overlay_actor_uuid, bdata); - changed = true; + // In this loop, we add colour management data to each of the images that + // is about to go on-screen. Within the loop, we then give any overlay + // plugins an opportunity to add data to the images that they will need + // at draw time to draw graphics onto the screen + for (int img_idx = 0; img_idx < result->num_onscreen_images(); ++img_idx) { + + if (!result->onscreen_image(img_idx)) { + // no image ? Not expected, but we'll handle this just in case. Skip + // adding overlay data or colour data as there is no image. + *response_count = *response_count - (viewport_overlay_plugins_.size()); + if (!*response_count) { + result->finalise(); + rp.deliver(media_reader::ImageBufDisplaySetPtr(result)); + break; + } else { + continue; } } - if (changed) - per_playhead.second = frames_queued_for_display; + + append_overlays_data(rp, img_idx, result, response_count); } } -void ViewportFrameQueueActor::update_blind_data( - const std::vector bufs, const bool wait) { - - // any overlay plugins that need to do computation before drawing - // to the screen are given the opportunity to do that now, ahead - // of time - if (wait) { - auto sys = caf::scoped_actor(home_system()); - for (auto p : overlay_actors_) { - - utility::Uuid overlay_actor_uuid = p.first; - caf::actor overlay_actor = p.second; - for (const media_reader::ImageBufPtr im : bufs) { - try { - auto bdata = request_receive( - *sys, overlay_actor, prepare_overlay_render_data_atom_v, im, false); - add_blind_data_to_image_in_queue(im, bdata, overlay_actor_uuid); - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - } - } - - } else { - for (auto p : overlay_actors_) { - - utility::Uuid overlay_actor_uuid = p.first; - caf::actor overlay_actor = p.second; - for (const media_reader::ImageBufPtr im : bufs) { - request(overlay_actor, infinite, prepare_overlay_render_data_atom_v, im, false) +void ViewportFrameQueueActor::append_overlays_data( + caf::typed_response_promise rp, + const int img_idx, + media_reader::ImageBufDisplaySet *result, + std::shared_ptr response_count) { + + auto check_and_respond = [=]() mutable { + (*response_count)--; + if (!*response_count) { + media_reader::ImageBufDisplaySetPtr v(result); + if (viewport_layout_manager_) { + // if we have a layout manager, get it to provide the transforms + // for the layout. Some layout plugins are provided by python, + // and could be slower so we have 0.25s timeout + result->finalise(); + mail(viewport_layout_atom_v, viewport_layout_mode_name_, v) + .request(viewport_layout_manager_, std::chrono::milliseconds(250)) .then( - [=](const utility::BlindDataObjectPtr &bdata) { - add_blind_data_to_image_in_queue(im, bdata, overlay_actor_uuid); + [=](const media_reader::ImageSetLayoutDataPtr &layout_data) mutable { + result->set_layout_data(layout_data); + + // here we set the layout transform matrix on the + // image buffers, if available + for (int i = 0; i < result->num_onscreen_images(); ++i) { + if (i <= (int)layout_data->image_transforms_.size()) { + media_reader::ImageBufPtr &im = result->onscreen_image_m(i); + im.set_layout_transform(layout_data->image_transforms_[i]); + } + } + + rp.deliver(v); }, - [=](caf::error &err) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + [=](caf::error &err) mutable { + spdlog::warn("C {} {}", __PRETTY_FUNCTION__, to_string(err)); + result->finalise(); + rp.deliver(v); }); + } else { + result->finalise(); + rp.deliver(v); } } - } -} - -void ViewportFrameQueueActor::update_image_blind_data_and_deliver( - const media_reader::ImageBufPtr &buf, - caf::typed_response_promise> rp) { + }; - // make a copy of our pointer to the image buffer - auto image_cpy = std::make_shared(buf); + auto get_colour_pip_data = [=]() mutable { + mail(colour_pipeline::get_colour_pipe_data_atom_v, result->onscreen_image(img_idx)) + .request(colour_pipeline_, infinite) + .then( + [=](media_reader::ImageBufPtr image_with_colour_data) mutable { + result->set_on_screen_image(img_idx, image_with_colour_data); + check_and_respond(); + }, + [=](caf::error &err) mutable { + spdlog::warn("B {} {}", __PRETTY_FUNCTION__, to_string(err)); + check_and_respond(); + }); + }; - // we need a ref counted counter to keep track of how many responses we expect - auto ct = std::make_shared((int)overlay_actors_.size()); + for (auto p : viewport_overlay_plugins_) { - for (auto p : overlay_actors_) { + utility::Uuid overlay_plugin_uuid = p.first; + caf::actor overlay_plugin = p.second; - utility::Uuid overlay_actor_uuid = p.first; - caf::actor overlay_actor = p.second; - request(overlay_actor, infinite, prepare_overlay_render_data_atom_v, *image_cpy, false) + mail( + prepare_overlay_render_data_atom_v, + result->onscreen_image(img_idx), + viewport_name_, + playhead_.uuid(), + result->hero_sub_playhead_index() == img_idx) + .request(overlay_plugin, infinite) .then( [=](const utility::BlindDataObjectPtr &bdata) mutable { - image_cpy->add_plugin_blind_data(overlay_actor_uuid, bdata); - (*ct)--; - if (!*ct) { - rp.deliver(std::vector({*image_cpy})); - } + result->onscreen_image_m(img_idx).add_plugin_blind_data( + overlay_plugin_uuid, bdata); + get_colour_pip_data(); }, [=](caf::error &err) mutable { - *ct = 100; - rp.deliver(err); + spdlog::warn("A {} {}", __PRETTY_FUNCTION__, to_string(err)); + get_colour_pip_data(); }); } } -timebase::flicks ViewportFrameQueueActor::predicted_playhead_position_at_next_video_refresh() { +void ViewportFrameQueueActor::clear_images_from_old_playheads() { + + for (const auto &uuid : deleted_playheads_) { + + // even if current_key_sub_playhead_id_ has been deleted, we don't + // want to clear its images as we need to hang onto them for display + // until we get a new current_key_sub_playhead_id_ (with images) + if (uuid == current_key_sub_playhead_id_) + continue; + + auto it = frames_to_draw_per_playhead_.find(uuid); + if (it != frames_to_draw_per_playhead_.end()) { + frames_to_draw_per_playhead_.erase(it); + } + } + deleted_playheads_.clear(); +} +timebase::flicks +ViewportFrameQueueActor::predicted_playhead_position(const utility::time_point &when) { if (!playhead_) return timebase::flicks(0); const timebase::flicks video_refresh_period = compute_video_refresh(); - const utility::time_point next_video_refresh_tp = next_video_refresh(video_refresh_period); - - caf::scoped_actor sys(system()); try { @@ -471,10 +594,10 @@ timebase::flicks ViewportFrameQueueActor::predicted_playhead_position_at_next_vi // show we actually show. auto estimate_playhead_position_at_next_redraw = request_receive_wait( *sys, - playhead_, + playhead_.actor(), std::chrono::milliseconds(100), playhead::position_atom_v, - next_video_refresh_tp, + when, video_refresh_period); if (!playing_) @@ -492,49 +615,46 @@ timebase::flicks ViewportFrameQueueActor::predicted_playhead_position_at_next_vi // The key is that we only change this phase adjustment occasionally as the phase // between the playhead and the video refresh beats drifts. + const long playhead_velocity_ct = + long(double(video_refresh_period.count()) * playhead_velocity_); timebase::flicks phase_adjusted_tp = estimate_playhead_position_at_next_redraw + playhead_vid_sync_phase_adjust_; timebase::flicks rounded_phase_adjusted_tp = timebase::flicks( - video_refresh_period.count() * - (phase_adjusted_tp.count() / video_refresh_period.count())); + playhead_velocity_ct * (phase_adjusted_tp.count() / playhead_velocity_ct)); const double phase = timebase::to_seconds(phase_adjusted_tp - rounded_phase_adjusted_tp) / timebase::to_seconds(video_refresh_period); if (phase < 0.1 || phase > 0.9) { - playhead_vid_sync_phase_adjust_ = timebase::flicks( - video_refresh_period.count() / 2 - - estimate_playhead_position_at_next_redraw.count() + - video_refresh_period.count() * - (estimate_playhead_position_at_next_redraw.count() / - video_refresh_period.count())); + playhead_velocity_ct / 2 - estimate_playhead_position_at_next_redraw.count() + + playhead_velocity_ct * + (estimate_playhead_position_at_next_redraw.count() / playhead_velocity_ct)); phase_adjusted_tp = estimate_playhead_position_at_next_redraw + playhead_vid_sync_phase_adjust_; rounded_phase_adjusted_tp = timebase::flicks( - video_refresh_period.count() * - (phase_adjusted_tp.count() / video_refresh_period.count())); - - { - timebase::flicks phase_adjusted_tp = - estimate_playhead_position_at_next_redraw + playhead_vid_sync_phase_adjust_; - timebase::flicks rounded_phase_adjusted_tp = timebase::flicks( - video_refresh_period.count() * - (phase_adjusted_tp.count() / video_refresh_period.count())); - const double phase = - timebase::to_seconds(phase_adjusted_tp - rounded_phase_adjusted_tp) / - timebase::to_seconds(video_refresh_period); - } + playhead_velocity_ct * (phase_adjusted_tp.count() / playhead_velocity_ct)); } return rounded_phase_adjusted_tp; } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + spdlog::debug("{} {}", __PRETTY_FUNCTION__, e.what()); } return timebase::flicks(0); } + +timebase::flicks ViewportFrameQueueActor::predicted_playhead_position_at_next_video_refresh() { + + if (!playhead_) + return timebase::flicks(0); + + const timebase::flicks video_refresh_period = compute_video_refresh(); + const utility::time_point next_video_refresh_tp = next_video_refresh(video_refresh_period); + return predicted_playhead_position(next_video_refresh_tp); +} + xstudio::utility::time_point ViewportFrameQueueActor::next_video_refresh( const timebase::flicks &video_refresh_period) const { @@ -654,6 +774,10 @@ timebase::flicks ViewportFrameQueueActor::compute_video_refresh() const { double ViewportFrameQueueActor::average_video_refresh_period() const { + if (video_refresh_data_.refresh_history_.size() < 64) { + return 1.0 / 60.0; + } + // Here, take the delta time between subsequent video refresh messages // and take the average. Ignore the lowest 8 and highest 8 deltas .. std::vector deltas; @@ -669,7 +793,7 @@ double ViewportFrameQueueActor::average_video_refresh_period() const { std::sort(deltas.begin(), deltas.end()); auto r = deltas.begin() + 8; - int ct = deltas.size() - 16; + int ct = int(deltas.size()) - 16; timebase::flicks t(0); while (ct--) { t += *(r++); @@ -677,3 +801,75 @@ double ViewportFrameQueueActor::average_video_refresh_period() const { return timebase::to_seconds(t) / (double(deltas.size() - 16)); } + +caf::typed_response_promise ViewportFrameQueueActor::set_playhead( + utility::UuidActor playhead, const bool prefetch_inital_image) { + + auto self = caf::actor_cast(this); + + auto rp = make_response_promise(); + // join the playhead's broadcast group - image buffers are streamed to + // us via the broacast group + auto do_join = [=]() mutable { + mail(broadcast::join_broadcast_atom_v, self) + .request(playhead.actor(), infinite) + .then( + [=](const bool) mutable { + // Get the 'key' child playhead UUID + mail(playhead::key_child_playhead_atom_v) + .request(playhead.actor(), infinite) + .then( + [=](utility::UuidVector curr_playhead_uuids) mutable { + monitor_.dispose(); + playhead_ = playhead; + + monitor_ = monitor( + playhead_.actor(), + [this, addr = playhead_.actor().address()](const error &err) { + if (addr == playhead_.actor()) { + playhead_ = utility::UuidActor(); + frames_to_draw_per_playhead_.clear(); + } + }); + + // this message will make the playhead re-broadcaset the + // media_source_atom event to it's 'broacast' group (of which we are + // a member). This info from the playhead is received in a message + // handler below and we send on the info about the media source to + // our colour pipeline which needs to do some set-up. + mail(playhead::media_source_atom_v, true, true) + .send(playhead_.actor()); + mail(playhead::jump_atom_v).send(playhead_.actor()); + mail(playhead::velocity_atom_v) + .request(playhead_.actor(), infinite) + .then( + [=](float v) { playhead_velocity_ = v; }, + [=](caf::error &err) {}); + + if (curr_playhead_uuids.empty()) + return; + current_key_sub_playhead_id_ = curr_playhead_uuids.back(); + curr_playhead_uuids.pop_back(); + sub_playhead_ids_ = curr_playhead_uuids; + rp.deliver(true); + }, + [=](const error &err) mutable { rp.deliver(err); }); + }, + [=](const error &err) mutable { rp.deliver(err); }); + }; + + if (playhead_) { + // it's crucial that we stop listening to the previous playhead! + mail(broadcast::leave_broadcast_atom_v, self).request(playhead_.actor(), infinite).then( + [=](bool) mutable { + do_join(); + }, + [=](caf::error &) mutable { + do_join(); + }); + } else { + do_join(); + } + + return rp; +} \ No newline at end of file diff --git a/src/ui/viewport/src/viewport_layout_plugin.cpp b/src/ui/viewport/src/viewport_layout_plugin.cpp new file mode 100644 index 000000000..44416e39a --- /dev/null +++ b/src/ui/viewport/src/viewport_layout_plugin.cpp @@ -0,0 +1,457 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include + +#include "xstudio/ui/viewport/viewport_layout_plugin.hpp" +#include "xstudio/ui/viewport/viewport_renderer_base.hpp" +#include "xstudio/utility/string_helpers.hpp" +#include "xstudio/utility/helpers.hpp" + +using namespace xstudio; +using namespace xstudio::utility; +using namespace xstudio::ui::viewport; + +ViewportLayoutPlugin::ViewportLayoutPlugin( + caf::actor_config &cfg, const utility::JsonStore &init_settings) + : plugin::StandardPlugin( + cfg, init_settings.value("name", "ViewportLayoutPlugin"), init_settings), + is_python_plugin_(init_settings.value("is_python", false)) { + init(); +} + +void ViewportLayoutPlugin::init() { + + handler_ = { + [=](utility::get_event_group_atom) -> caf::actor { + if (!event_group_) { + event_group_ = spawn(this); + link_to(event_group_); + } + return event_group_; + }, + [=](viewport_renderer_atom, + const std::string window_id, + const utility::JsonStore &prefs) -> ViewportRendererPtr { + return ViewportRendererPtr(make_renderer(window_id, prefs)); + }, + [=](viewport_layout_atom, + const std::string &layout_mode, + const JsonStore &python_plugin_layout) { + // this comes in from Python - we can't get exchange size_t between + // C++ and python, PyBind and caf get their knickers in a twist trying + // to infer the integer type, so the hash is stringified at both + // ends. + try { + + size_t hash; + auto h = python_plugin_layout["hash"].get(); + sscanf(h.c_str(), "%zu", &hash); + std::ignore = + mail(viewport_layout_atom_v, layout_mode, python_plugin_layout, hash) + .delegate(caf::actor_cast(this)); + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + }, + [=](viewport_layout_atom, + const std::string &layout_mode, + const JsonStore &python_plugin_layout, + size_t hash) { + media_reader::ImageSetLayoutDataPtr layout_data = + python_layout_data_to_ours(python_plugin_layout); + if (layout_data) { + layouts_cache_[layout_mode][hash] = layout_data; + auto p = pending_responses_.find(hash); + if (p != pending_responses_.end()) { + for (auto &rp : p->second) { + rp.deliver(layout_data); + } + pending_responses_.erase(p); + } + } + }, + [=](viewport_layout_atom, + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set) + -> result { + auto rp = make_response_promise(); + auto p = layouts_cache_[layout_mode].find(image_set->images_layout_hash()); + if (p == layouts_cache_[layout_mode].end()) { + __do_layout(layout_mode, image_set, rp); + } else { + rp.deliver(p->second); + } + return rp; + }, + [=](viewport_layout_atom, + const std::string &layout_name, + const float menu_position, + const xstudio::playhead::AssemblyMode mode, + const xstudio::playhead::AutoAlignMode auto_align) { + // used by Python ViewportLayoutPlugin api + add_layout_mode(layout_name, menu_position, mode, auto_align); + }, + [=](viewport_layout_atom, + const std::string &layout_name, + const utility::Uuid &attr_id) { + + // used by python layout plugin so that an attribute can be + // made visible in the layou settings controls + auto attr = get_attribute(attr_id); + if (attr) { + add_layout_settings_attribute(attr, layout_name); + } + } + }; + + make_behavior(); + + connect_to_ui(); + + layouts_manager_ = system().registry().template get(viewport_layouts_manager); + gobal_playhead_events_ = + system().registry().template get(global_playhead_events_actor); + anon_mail( + ui::viewport::viewport_layout_atom_v, caf::actor_cast(this), Module::name()) + .send(layouts_manager_); +} + +void ViewportLayoutPlugin::on_exit() { + layouts_manager_ = caf::actor(); + gobal_playhead_events_ = caf::actor(); + plugin::StandardPlugin::on_exit(); +} + +void ViewportLayoutPlugin::do_layout( + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set, + media_reader::ImageSetLayoutData &layout_data) { + // For the default layout, the image(s) are not transformed in any way. + // We just need to set the aspect of the layout, which is the same as + // the image aspect for the hero image + const media_reader::ImageBufPtr &hero_image = + image_set->onscreen_image(image_set->hero_sub_playhead_index()); + if (hero_image) { + layout_data.layout_aspect_ = hero_image.frame_id().pixel_aspect() * + hero_image->image_size_in_pixels().x / + hero_image->image_size_in_pixels().y; + } else { + layout_data.layout_aspect_ = 16.0 / 9.0f; + } + + // this fills image_transform_matrices with unity matrices + layout_data.image_transforms_.resize(image_set->num_onscreen_images()); + + // we only draw the 'hero' image. No compositing or any other layout stuff + // to do. + layout_data.image_draw_order_hint_ = + std::vector(1, image_set->hero_sub_playhead_index()); + + layout_data.draw_hero_overlays_only_ = true; + +} + +void ViewportLayoutPlugin::__do_layout( + const std::string &layout_mode, + const media_reader::ImageBufDisplaySetPtr &image_set, + caf::typed_response_promise rp) { + + // if we have an event_group_ actor, this means there is a Python + // plugin for doing viewport layouts. We send a message to the event_group_ + // which calls the do_layout' function of + if (is_python_plugin_ && event_group_) { + // We need python side to receive the hash for the image layout inputs (image sizes and + // pixel aspects) but the hash is size_t which we can't interchange directly with python + // as it handles integers differently, so we stringify the hash. Python plugin then + // sends back the layout data with the hash as a string which we need to decode to + // size_t again. + mail( + viewport_layout_atom_v, + layout_mode, + image_set->as_json(), + fmt::format("{}", image_set->images_layout_hash())) + .send(event_group_); + pending_responses_[image_set->images_layout_hash()].push_back(rp); + return; + } + + auto layout_data = new media_reader::ImageSetLayoutData; + + // this will callback to the plugins' implementation (or our default + // implementation) above. + do_layout(layout_mode, image_set, *layout_data); + + auto r = media_reader::ImageSetLayoutDataPtr(layout_data); + rp.deliver(r); + layouts_cache_[layout_mode][image_set->images_layout_hash()] = r; +} + +/*void HUDPluginBase::hud_element_qml( + const std::string qml_code, const HUDElementPosition position) { + + hud_data_->set_role_data(module::Attribute::QmlCode, qml_code); + + if (position == FullScreen) { + hud_data_->expose_in_ui_attrs_group("hud_elements_fullscreen"); + return; + } else if (!hud_item_position_) { + // add settings attribute for the position of the HUD element + hud_item_position_ = add_string_choice_attribute( + "Position On Screen", + "Position On Screen", + position_names_[position], + utility::map_value_to_vec(position_names_)); + add_hud_settings_attribute(hud_item_position_); + hud_item_position_->set_preference_path( + fmt::format("/plugin/{}/hud_item_position", plugin_underscore_name_)); + } + + if (hud_item_position_) { + attribute_changed(hud_item_position_->uuid(), module::Attribute::Value); + } +}*/ + +media_reader::ImageSetLayoutDataPtr +ViewportLayoutPlugin::python_layout_data_to_ours(const utility::JsonStore &python_data) const { + auto result = new media_reader::ImageSetLayoutData; + + try { + + + if (python_data.contains("layout_aspect_ratio")) { + result->layout_aspect_ = python_data["layout_aspect_ratio"].get(); + } + + if (python_data.contains("draw_hero_overlays_only")) { + result->draw_hero_overlays_only_ = python_data["draw_hero_overlays_only"].get(); + } else { + result->draw_hero_overlays_only_ = false; + } + + if (python_data.contains("image_draw_order")) { + if (python_data["image_draw_order"].is_array()) { + for (const auto &v : python_data["image_draw_order"]) { + result->image_draw_order_hint_.push_back(v.get()); + } + } else { + throw std::runtime_error("image_draw_order entry must be an array"); + } + } else { + throw std::runtime_error("expected image_draw_order entry"); + } + + if (python_data.contains("transforms")) { + if (python_data["transforms"].is_array()) { + for (const auto &v : python_data["transforms"]) { + if (v.is_array() && v.size() == 3) { + float tx = v[0].get(); + float ty = v[1].get(); + float s = v[2].get(); + Imath::M44f m; + m.translate(Imath::V3f(tx, ty, 0.0f)); + m.scale(Imath::V3f(s, s, 1.0f)); + result->image_transforms_.push_back(m); + } else { + throw std::runtime_error( + "Elements of transforms entry must be an array of 3 floats."); + } + } + } else { + throw std::runtime_error("transforms entry must be an array"); + } + } else { + throw std::runtime_error("expected transforms entry"); + } + + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + return media_reader::ImageSetLayoutDataPtr(result); +} + +void ViewportLayoutPlugin::add_layout_settings_attribute( + module::Attribute *attr, const std::string &layout_name) { + attr->expose_in_ui_attrs_group(layout_name + " Settings"); + // this means the 'settings' button WILL be visible! + layouts_with_settings_.insert(layout_name); + for (auto &p : layout_names_) { + if (p.second->value() == layout_name) { + p.second->set_role_data(module::Attribute::UserData, true); + } + } +} + +void ViewportLayoutPlugin::add_viewport_layout_qml_overlay( + const std::string &layout_name, const std::string &qml_code) { + + auto attr = add_boolean_attribute(layout_name, layout_name, true); + attr->set_role_data(module::Attribute::QmlCode, qml_code); + attr->expose_in_ui_attrs_group("viewport_overlay_plugins"); +} + + +void ViewportLayoutPlugin::add_layout_mode( + const std::string &name, + const float menu_position, + const playhead::AssemblyMode mode, + const playhead::AutoAlignMode default_auto_align) { + + mail( + viewport_layout_atom_v, + caf::actor_cast(this), + name, + mode, + default_auto_align) + .request(layouts_manager_, infinite) + .then( + [=](bool accepted) { + if (accepted) { + auto layout_toggle = add_string_attribute(name, name, ""); + layout_toggle->expose_in_ui_attrs_group("viewport_layouts"); + layout_toggle->set_role_data( + module::Attribute::ToolbarPosition, menu_position); + layout_names_[layout_toggle->uuid()] = layout_toggle; + if (layouts_with_settings_.find(name) != layouts_with_settings_.end()) { + layout_toggle->set_role_data(module::Attribute::UserData, true); + } else { + layout_toggle->set_role_data(module::Attribute::UserData, false); + } + } + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); +} + +void ViewportLayoutPlugin::attribute_changed( + const utility::Uuid &attribute_uuid, const int role) { + // this forces re-computation of the layout geometry + layouts_cache_.clear(); + // now force viewports to redraw + anon_mail(playhead::redraw_viewport_atom_v).send(gobal_playhead_events_); + // anon_mail(playhead::redraw_viewport_atom_v).send(gobal_playhead_events_); + + StandardPlugin::attribute_changed(attribute_uuid, role); +} + +ViewportLayoutManager::ViewportLayoutManager(caf::actor_config &cfg) + : caf::event_based_actor(cfg) { + spdlog::debug("Created ViewportLayoutManager"); + print_on_exit(this, "ViewportLayoutManager"); + + system().registry().put(viewport_layouts_manager, this); + event_group_ = spawn(this); + link_to(event_group_); + + behavior_ = { + + [=](broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + [=](utility::get_event_group_atom) -> caf::actor { return event_group_; }, + [=](broadcast::join_broadcast_atom, caf::actor joiner) { + return mail(broadcast::join_broadcast_atom_v, joiner).delegate(event_group_); + }, + [=](broadcast::leave_broadcast_atom, caf::actor joiner) { + return mail(broadcast::leave_broadcast_atom_v, joiner).delegate(event_group_); + }, + [=](ui::viewport::viewport_layout_atom, + caf::actor layout_actor, + const std::string &name) { + // this message is sent by instances of ViewportLayoutPlugin + // on construction + viewport_layout_plugins_[name] = layout_actor; + link_to(layout_actor); + }, + [=](ui::viewport::viewport_layout_atom, + caf::actor layout_actor, + const std::string &layout_name, + const playhead::AssemblyMode mode, + const playhead::AutoAlignMode default_align_mode) -> result { + // Here a layout actor is registering a layout with us + if (viewport_layouts_.contains(layout_name)) { + return make_error( + xstudio_error::error, + fmt::format( + "A viewport layout name \"{}\" is already registered.", layout_name)); + } + viewport_layouts_[layout_name] = + std::make_pair(layout_actor, std::make_pair(default_align_mode, mode)); + return true; + }, + [=](viewport_layout_atom, + const std::string &layout_name, + const bool /*shared_instance*/, + const std::string /*viewport_name*/) -> result { + if (not viewport_layouts_.contains(layout_name)) { + return make_error( + xstudio_error::error, + fmt::format( + "Requested viewport layout named \"{}\" which is not registered.", + layout_name)); + } + return viewport_layouts_[layout_name].first; + }, + [=](playhead::compare_mode_atom, const std::string &layout_name) + -> result< + std::pair> { + if (not viewport_layouts_.contains(layout_name)) { + return make_error( + xstudio_error::error, + fmt::format( + "Requested viewport layout named \"{}\" which is not registered.", + layout_name)); + } + return viewport_layouts_[layout_name].second; + }}; + + spawn_plugins(); +} + +ViewportLayoutManager::~ViewportLayoutManager() {} + +void ViewportLayoutManager::on_exit() { + system().registry().erase(viewport_layouts_manager); + viewport_layouts_.clear(); + caf::event_based_actor::on_exit(); +} + +void ViewportLayoutManager::spawn_plugins() { + + // get the plugin manager + auto pm = system().registry().template get(plugin_manager_registry); + + const auto ptype = + plugin_manager::PluginType(plugin_manager::PluginFlags::PF_VIEWPORT_RENDERER); + + // SPAWN C++ PLUGINS (Python plugins are loaded in python startup script) + + // get details of viewport layout plugins + mail(utility::detail_atom_v, ptype) + .request(pm, infinite) + .then( + + [=](const std::vector &renderer_plugin_details) { + // loop over plugin details + for (const auto &pd : renderer_plugin_details) { + + // instance the plugin. Each plugin automatically registeres + // itself with this class on construction (see above) + utility::JsonStore j; + j["name"] = pd.name_; + j["is_python_plugin"] = false; + mail(plugin_manager::spawn_plugin_atom_v, pd.uuid_, j) + .request(pm, infinite) + .then( + [=](caf::actor) {}, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); + } + }, + [=](caf::error &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + }); +} \ No newline at end of file diff --git a/src/ui/viewport/test/viewport_serialize_test.cpp b/src/ui/viewport/test/viewport_serialize_test.cpp index 34b43bfac..85f7b0d27 100644 --- a/src/ui/viewport/test/viewport_serialize_test.cpp +++ b/src/ui/viewport/test/viewport_serialize_test.cpp @@ -88,7 +88,7 @@ TEST(PointerEventSerializerTest, Test) { binary_serializer bs{f.system, buf}; PointerEvent u1( - Signature::EventType::ButtonRelease, + EventType::ButtonRelease, Signature::Button::Middle, 10, 20, diff --git a/src/utility/src/CMakeLists.txt b/src/utility/src/CMakeLists.txt index 38b4e8821..087d0ac09 100644 --- a/src/utility/src/CMakeLists.txt +++ b/src/utility/src/CMakeLists.txt @@ -1,8 +1,10 @@ -find_package(spdlog REQUIRED) +find_package(spdlog CONFIG REQUIRED) find_package(fmt REQUIRED) find_package(Imath REQUIRED) find_package(nlohmann_json REQUIRED) find_package(ZLIB REQUIRED) +# find_package(OpenSSL) + if(WIN32) # Not Required elseif(UNIX AND NOT APPLE) @@ -13,28 +15,37 @@ endif() SET(LINK_DEPS PUBLIC - caf::core - fmt::fmt - Imath::Imath + CAF::core + Imath::Imath nlohmann_json::nlohmann_json spdlog::spdlog - uuid + fmt::fmt ZLIB::ZLIB + # OpenSSL::SSL ) SET(STATIC_LINK_DEPS - caf::core - fmt::fmt + CAF::core Imath::Imath nlohmann_json::nlohmann_json spdlog::spdlog - uuid + fmt::fmt ZLIB::ZLIB + # OpenSSL::SSL ) -if(UNIX AND NOT APPLE) +if(APPLE) + list(APPEND LINK_DEPS "-framework CoreFoundation") + list(APPEND STATIC_LINK_DEPS "-framework CoreFoundation") +elseif(WIN32) + list(APPEND LINK_DEPS uuid) + list(APPEND STATIC_LINK_DEPS uuid) +else() + # Linux! + list(APPEND LINK_DEPS uuid) + list(APPEND STATIC_LINK_DEPS uuid) list(APPEND LINK_DEPS stdc++fs) list(APPEND STATIC_LINK_DEPS stdc++fs) endif() -create_component_static(utility 0.1.0 "${LINK_DEPS}" "${STATIC_LINK_DEPS}") \ No newline at end of file +create_component_static(utility ${XSTUDIO_GLOBAL_VERSION} "${LINK_DEPS}" "${STATIC_LINK_DEPS}") \ No newline at end of file diff --git a/src/utility/src/authentication.cpp b/src/utility/src/authentication.cpp new file mode 100644 index 000000000..286b7869e --- /dev/null +++ b/src/utility/src/authentication.cpp @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/utility/authentication.hpp" \ No newline at end of file diff --git a/src/utility/src/broadcast_actor.cpp b/src/utility/src/broadcast_actor.cpp new file mode 100644 index 000000000..f55a00d9e --- /dev/null +++ b/src/utility/src/broadcast_actor.cpp @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/broadcast/broadcast_actor.hpp" +#include "xstudio/atoms.hpp" +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/logging.hpp" +#include "xstudio/utility/uuid.hpp" + +using namespace xstudio; +using namespace xstudio::broadcast; +using namespace xstudio::utility; +using namespace caf; + +static std::atomic count{0}; +static std::atomic actor_count{0}; + +BroadcastActor::BroadcastActor(caf::actor_config &cfg, caf::actor owner) + : caf::event_based_actor(cfg) { + // count++; + // actor_count++; + if (owner) { + monitor(owner, [this, addr = owner.address()](const error &) { + owner_ = caf::actor_addr(); + quit(); + }); + owner_ = caf::actor_cast(owner); + } + init(); +} + +void BroadcastActor::monitor_subscriber(const caf::actor &actor) { + auto act_addr = caf::actor_cast(actor); + + if (auto sit = monitor_.find(act_addr); sit == std::end(monitor_)) { + monitor_[act_addr] = monitor(actor, [this, addr = actor.address()](const error &) { + auto actor_addr = caf::actor_cast(addr); + if (auto mit = monitor_.find(actor_addr); mit != std::end(monitor_)) + monitor_.erase(mit); + + if (actor_addr && subscribers_.count(actor_addr)) + subscribers_.erase(actor_addr); + }); + } +} + + +// BroadcastActor::~BroadcastActor(){ +// count--; +// spdlog::error("{} count {} actor_count {}", __PRETTY_FUNCTION__, count, actor_count); +// } + +caf::message_handler BroadcastActor::default_event_handler() { + return {[=](broadcast_down_atom, const caf::actor_addr &) {}}; +} + +void BroadcastActor::init() { + // print_on_create(this, "BroadcastActor"); + // print_on_exit(this, "BroadcastActor"); + + // set_default_handler( + // [this](caf::scheduled_actor *, caf::message &msg) -> caf::skippable_result { + // // UNCOMMENT TO DEBUG UNEXPECT MESSAGES + + // // spdlog::warn("Got broadcast from {} {}", to_string(current_sender()), + // // to_string(msg) + // // ); + + // if (current_sender() == nullptr or not current_sender()) { + // for (const auto &i : subscribers_) { + // // spdlog::warn("{} {} Anon Forward {}", + // // to_string(caf::actor_cast(this)), + // // to_string(current_sender()), + // to_string(caf::actor_cast(i))); try { + // mail(msg).send(caf::actor_cast(i)); + // } catch (...) { + // } + // } + // } else { + // for (const auto &i : subscribers_) { + // // we need to send as if we were delegating.. + // try { + // // spdlog::warn("{} {} Forward to {}", + // // to_string(caf::actor_cast(this)), + // // to_string(current_sender()), + // // to_string(caf::actor_cast(i))); + + // send_as( + // caf::actor_cast(current_sender()), + // caf::actor_cast(i), + // msg); + // } catch (...) { + // } + // } + // } + // return message{}; + // }); + + behavior_.assign( + [=](xstudio::broadcast::broadcast_down_atom, const caf::actor_addr &) {}, + [=](leave_broadcast_atom) -> bool { + auto subscriber = caf::actor_cast(current_sender()); + if (subscriber && subscribers_.count(subscriber)) { + + if (auto mit = + monitor_.find(caf::actor_cast(current_sender())); + mit != std::end(monitor_)) { + mit->second.dispose(); + monitor_.erase(mit); + } + + subscribers_.erase(subscriber); + // spdlog::warn("subscriber leaving {} {}", + // to_string(caf::actor_cast(this)),to_string(current_sender())); + } + return true; + }, + [=](leave_broadcast_atom, caf::actor sub) -> bool { + auto subscriber = caf::actor_cast(sub); + if (subscriber && subscribers_.count(subscriber)) { + + if (auto mit = monitor_.find(caf::actor_cast(sub)); + mit != std::end(monitor_)) { + mit->second.dispose(); + monitor_.erase(mit); + } + + subscribers_.erase(subscriber); + // spdlog::warn("subscriber leaving {} {}", + // to_string(caf::actor_cast(this)),to_string(sub)); + } + return true; + }, + [=](join_broadcast_atom) -> bool { + auto subscriber = caf::actor_cast(current_sender()); + if (subscriber && not subscribers_.count(subscriber)) { + monitor_subscriber(caf::actor_cast(current_sender())); + subscribers_.insert(subscriber); + // spdlog::warn("new subscriber {} {} {}", + // to_string(caf::actor_cast(this)), to_string(current_sender()), + // to_string(subscriber)); + } + return true; + }, + [=](join_broadcast_atom, caf::actor sub) -> bool { + auto subscriber = caf::actor_cast(sub); + + if (subscriber && not subscribers_.count(subscriber)) { + + monitor_subscriber(sub); + subscribers_.insert(subscriber); + // spdlog::warn("new subscriber {} {} {}", + // to_string(caf::actor_cast(this)), to_string(sub), + // to_string(subscriber)); + } + return true; + }, + [=](caf::message &msg) { + // UNCOMMENT TO DEBUG UNEXPECT MESSAGES + + // spdlog::warn("Got broadcast from {} {}", to_string(current_sender()), + // to_string(msg) + // ); + + if (current_sender() == nullptr or not current_sender()) { + for (const auto &i : subscribers_) { + // spdlog::warn("{} {} Anon Forward {}", + // to_string(caf::actor_cast(this)), + // to_string(current_sender()), to_string(caf::actor_cast(i))); + try { + mail(msg).send(caf::actor_cast(i)); + } catch (...) { + } + } + } else { + for (const auto &i : subscribers_) { + // we need to send as if we were delegating.. + try { + // spdlog::warn("{} {} Forward to {}", + // to_string(caf::actor_cast(this)), + // to_string(current_sender()), + // to_string(caf::actor_cast(i))); + + send_as( + caf::actor_cast(current_sender()), + caf::actor_cast(i), + msg); + } catch (...) { + } + } + } + return message{}; + }); +} + +void BroadcastActor::on_exit() { + // spdlog::warn("notify subscribers or shutdown"); + for (const auto &i : subscribers_) { + try { + auto sub = caf::actor_cast(i); + if (sub) + anon_mail(broadcast_down_atom_v, caf::actor_cast(this)) + .send(sub); + } catch (...) { + } + } + subscribers_.clear(); + // actor_count--; + // spdlog::error("{} count {} actor_count {}", __PRETTY_FUNCTION__, count, actor_count); +} diff --git a/src/utility/src/container.cpp b/src/utility/src/container.cpp index 59cd1894c..315a893f7 100644 --- a/src/utility/src/container.cpp +++ b/src/utility/src/container.cpp @@ -5,10 +5,33 @@ #include "xstudio/utility/container.hpp" #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" +#include "xstudio/broadcast/broadcast_actor.hpp" using namespace xstudio; using namespace xstudio::utility; +static std::map cnt_map; +static std::mutex cnt_mutex; + + +void Container::register_container(const Container &cnt) { + std::lock_guard m(cnt_mutex); + cnt_map[cnt.uuid()] = cnt.type(); + // spdlog::warn("register {} {}",to_string(cnt.uuid()),cnt.type()); +} + +void Container::unregister_container(const Container &cnt) { + std::lock_guard m(cnt_mutex); + // spdlog::error("unregistered {} {}", to_string(cnt.uuid()), cnt.type()); + cnt_map.erase(cnt.uuid()); + + // if (cnt_map.size() < 30) { + // for (const auto &i : cnt_map) + // spdlog::error("NOT unregistered {} {}", to_string(i.first), i.second); + // } +} + + void Container::set_file_version(const std::string &version, const bool warn) { semver::version nv(version); if (nv > file_version_ and warn) @@ -50,6 +73,54 @@ Container Container::duplicate() const { return result; } +caf::message_handler Container::default_event_handler() { + return caf::message_handler( + {[=](utility::event_atom, utility::name_atom, const std::string &) {}, + [=](utility::event_atom, utility::last_changed_atom, const time_point &) {}}); +} + +caf::message_handler Container::container_message_handler(caf::event_based_actor *act) { + actor_ = act; + event_group_ = actor_->spawn(actor_); + actor_->link_to(event_group_); + + return caf::message_handler({ + [=](name_atom, const std::string &name) { // make_set_name_handler + name_ = name; + actor_->mail(event_atom_v, name_atom_v, name).send(event_group_); + send_changed(); + }, + [=](name_atom) -> std::string { return name_; }, // make_get_name_handler + [=](last_changed_atom) -> time_point { + return last_changed_; + }, // make_last_changed_getter + [=](last_changed_atom, const time_point &last_changed) { // make_last_changed_setter + if (last_changed > last_changed_ or last_changed == time_point()) { + last_changed_ = last_changed; + actor_->mail(utility::event_atom_v, last_changed_atom_v, last_changed_) + .send(event_group_); + } + }, + [=](utility::event_atom, + last_changed_atom, + const time_point &last_changed) { // make_last_changed_event_handler + if (last_changed > last_changed_) { + last_changed_ = last_changed; + actor_->mail(utility::event_atom_v, last_changed_atom_v, last_changed_) + .send(event_group_); + } + }, + [=](uuid_atom) -> Uuid { return uuid_; }, // make_get_uuid_handler + [=](type_atom) -> std::string { return type_; }, // make_get_type_handler + [=](get_event_group_atom) -> caf::actor { + return event_group_; + }, // make_get_event_group_handler + [=](detail_atom) -> ContainerDetail { + return detail(actor_, event_group_); + } // make_get_detail_handler + }); +} + JsonStore Container::serialise() const { utility::JsonStore jsn; diff --git a/src/utility/src/edit_list.cpp b/src/utility/src/edit_list.cpp deleted file mode 100644 index df717851b..000000000 --- a/src/utility/src/edit_list.cpp +++ /dev/null @@ -1,599 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -#ifdef _WIN32 -#include -#else -#include -#endif -#include -#include - -#include "xstudio/utility/edit_list.hpp" -#include "xstudio/utility/logging.hpp" - -using namespace xstudio::utility; - -void EditList::extend(const EditList &o) { sl_.insert(sl_.end(), o.sl_.begin(), o.sl_.end()); } - -std::vector> -EditList::frame_durations(const TimeSourceMode tsm, const FrameRate &fr) const { - std::vector> frames; - - switch (tsm) { - case TimeSourceMode::FIXED: - case TimeSourceMode::DYNAMIC: - // sum frames. - for (const auto &i : sl_) - frames.emplace_back(std::make_pair( - i.media_uuid_, static_cast(i.frame_rate_and_duration_.frames()))); - break; - case TimeSourceMode::REMAPPED: - for (const auto &i : sl_) - frames.emplace_back(std::make_pair( - i.media_uuid_, static_cast(i.frame_rate_and_duration_.frames(fr)))); - break; - } - return frames; -} - -std::vector> -EditList::second_durations(const TimeSourceMode tsm, const FrameRate &fr) const { - std::vector> seconds; - - switch (tsm) { - case TimeSourceMode::FIXED: - // sum frames. - for (const auto &i : sl_) - seconds.emplace_back( - std::make_pair(i.media_uuid_, i.frame_rate_and_duration_.seconds(fr))); - break; - - case TimeSourceMode::DYNAMIC: - case TimeSourceMode::REMAPPED: - for (const auto &i : sl_) - seconds.emplace_back( - std::make_pair(i.media_uuid_, i.frame_rate_and_duration_.seconds())); - break; - } - - return seconds; -} - -std::vector> -EditList::flick_durations(const TimeSourceMode tsm, const FrameRate &fr) const { - std::vector> _flicks; - switch (tsm) { - case TimeSourceMode::FIXED: - // sum frames. - for (const auto &i : sl_) - _flicks.emplace_back( - std::make_pair(i.media_uuid_, i.frame_rate_and_duration_.duration(fr))); - break; - - case TimeSourceMode::REMAPPED: - case TimeSourceMode::DYNAMIC: - for (const auto &i : sl_) - _flicks.emplace_back( - std::make_pair(i.media_uuid_, i.frame_rate_and_duration_.duration())); - break; - } - - return _flicks; -} - - -size_t EditList::duration_frames(const TimeSourceMode tsm, const FrameRate &fr) const { - long int frames = 0; - - switch (tsm) { - case TimeSourceMode::FIXED: - case TimeSourceMode::DYNAMIC: - // sum frames. - for (const auto &i : sl_) - frames += i.frame_rate_and_duration_.frames(); - - break; - case TimeSourceMode::REMAPPED: - for (const auto &i : sl_) - frames += i.frame_rate_and_duration_.frames(fr); - break; - } - - return static_cast(frames); -} - -double EditList::duration_seconds(const TimeSourceMode tsm, const FrameRate &fr) const { - double seconds = 0.0; - - switch (tsm) { - case TimeSourceMode::FIXED: - // sum frames. - for (const auto &i : sl_) - seconds += i.frame_rate_and_duration_.seconds(fr); - break; - - case TimeSourceMode::DYNAMIC: - case TimeSourceMode::REMAPPED: - for (const auto &i : sl_) - seconds += i.frame_rate_and_duration_.seconds(); - break; - } - - return seconds; -} - -timebase::flicks -EditList::duration_flicks(const TimeSourceMode tsm, const FrameRate &fr) const { - timebase::flicks _flicks = timebase::k_flicks_zero_seconds; - - switch (tsm) { - case TimeSourceMode::FIXED: - // sum frames. - for (const auto &i : sl_) - _flicks += i.frame_rate_and_duration_.duration(fr); - break; - - case TimeSourceMode::REMAPPED: - case TimeSourceMode::DYNAMIC: - for (const auto &i : sl_) - _flicks += i.frame_rate_and_duration_.duration(); - break; - } - - return _flicks; -} - -EditListSection EditList::media_frame(const int logical_frame, int &media_frame) const { - if (empty()) - throw std::runtime_error("No frames"); - - int tot = 0; - bool found = false; - EditListSection s; - for (const auto &i : sl_) { - if ((logical_frame - tot) < i.frame_rate_and_duration_.frames()) { - s = i; - media_frame = logical_frame - tot; - found = true; - break; - } else - tot += i.frame_rate_and_duration_.frames(); - } - - if (not found) { - throw std::runtime_error("No frames left"); - } - - return s; -} - -timebase::flicks EditList::media_flick_to_flicks( - const utility::Uuid &media_source, - const timebase::flicks media_flick, - const TimeSourceMode /*tsm*/, - const FrameRate & /*rate*/) const { - - timebase::flicks tot(timebase::k_flicks_zero_seconds); - bool found = false; - for (const auto &i : sl_) { - if (i.media_uuid_ == media_source) { - found = true; - // if (tsm != TimeSourceMode::FIXED) { - tot += media_flick; - // } else { - // tot+= rate.to_flicks()*logical_frame; - // } - break; - } - tot += i.frame_rate_and_duration_.duration(); - } - if (not found) { - throw std::runtime_error("EditList::media_flick_to_flicks - media uuid not found."); - } - return tot; -} - - -timebase::flicks EditList::media_frame_to_flicks( - const utility::Uuid &media_source, - const int logical_frame, - const TimeSourceMode tsm, - const FrameRate &rate) const { - timebase::flicks tot(timebase::k_flicks_zero_seconds); - bool found = false; - for (const auto &i : sl_) { - if (i.media_uuid_ == media_source) { - found = true; - if (tsm != TimeSourceMode::FIXED) { - tot += i.frame_rate_and_duration_.rate_.to_flicks() * logical_frame; - } else { - tot += rate.to_flicks() * logical_frame; - } - break; - } - tot += i.frame_rate_and_duration_.duration(); - } - - if (not found) { - throw std::runtime_error("EditList::media_frame_to_flicks - media uuid not found."); - } - return tot; -} - -// yup this one is a monster.. - -int EditList::step( - const TimeSourceMode tsm, - const FrameRate &rate, - const bool forward, - const float velocity, - const FrameRateDuration &frame, - FrameRateDuration &new_frame, - timebase::flicks &new_seconds) const { - FrameRateDuration tmp_frame; - FrameRateDuration seek_frame; - int tmp_logical_frame = 0; - - seek_frame.set_rate(frame.rate()); - seek_frame.set_duration(frame.duration()); - seek_frame.step(forward, velocity); - - tmp_frame = seek_frame; - tmp_frame.set_rate(frame.rate()); - - tmp_logical_frame = logical_frame(tsm, rate, tmp_frame); - - new_frame = tmp_frame; - new_seconds = flicks_from_logical(tmp_logical_frame, tsm, rate); - return tmp_logical_frame; -} - -int EditList::step( - const TimeSourceMode tsm, - const FrameRate &override_rate, - const bool forward, - const float velocity, - const int _logical_frame, - const int playhead_in_frame, - const int playhead_out_frame, - timebase::flicks &step_period) const { - - const int in_frame = std::max(0, playhead_in_frame); - const int out_frame = - std::min((int)duration_frames(tsm, override_rate) - 1, playhead_out_frame); - - /* This needs more work to deal with velocity and remapped tsm and ping/pong - and also MAPPED tsm is not working yet*/ - int new_logical_frame = _logical_frame; - - auto step = - std::chrono::duration_cast( - (tsm == TimeSourceMode::DYNAMIC ? frame_rate_at_frame(_logical_frame).to_flicks() - : override_rate.to_flicks())) / - velocity; - - step_period = timebase::flicks(0) + std::chrono::duration_cast(step); - - if (tsm == TimeSourceMode::REMAPPED) { - - // what time does the frame we have map to? - const timebase::flicks ctime = - flicks_from_frame(TimeSourceMode::DYNAMIC, _logical_frame); - - // now get the frame stepped by the remap rate - new_logical_frame = logical_frame( - TimeSourceMode::DYNAMIC, ctime + override_rate.to_flicks(), override_rate); - - } else { - - if (forward) { - new_logical_frame = _logical_frame + 1; - if (new_logical_frame > out_frame) { - new_logical_frame = in_frame; - } - } else { - new_logical_frame = _logical_frame - 1; - if (new_logical_frame < in_frame) { - new_logical_frame = out_frame; - } - } - } - - return new_logical_frame; -} - -timebase::flicks EditList::flicks_from_logical( - const int logical_frame, const TimeSourceMode tsm, const FrameRate &fr) const { - timebase::flicks tmp_seconds(timebase::k_flicks_zero_seconds); - - switch (tsm) { - case TimeSourceMode::FIXED: - tmp_seconds = fr * logical_frame; - break; - - case TimeSourceMode::REMAPPED: - case TimeSourceMode::DYNAMIC: - // seconds need to be calculated.. - { - int tot = 0; - for (const auto &i : sl_) { - if ((logical_frame - tot) < i.frame_rate_and_duration_.frames()) { - // get flicks for frames.. - tmp_seconds += i.frame_rate_and_duration_.rate() * (logical_frame - tot); - break; - } else { - tmp_seconds += i.frame_rate_and_duration_.duration(); - tot += i.frame_rate_and_duration_.frames(); - } - } - } - break; - } - - // spdlog::debug("{}",logical_frame,timebase::to_seconds(tmp_seconds)); - - return tmp_seconds; -} - -int EditList::logical_frame( - const TimeSourceMode tsm, - const utility::FrameRate &rate, - const utility::FrameRateDuration &frame) const { - int tmp_logical_frame = 0; - - switch (tsm) { - case TimeSourceMode::FIXED: - case TimeSourceMode::DYNAMIC: - tmp_logical_frame = frame.frames(); - break; - - case TimeSourceMode::REMAPPED: { - double target_seconds = frame.seconds(rate); - double tot = 0; - - for (const auto &i : sl_) { - if ((target_seconds - tot) < i.frame_rate_and_duration_.seconds()) { - // get flicks for frames.. - tmp_logical_frame += std::min( - i.frame_rate_and_duration_.frames() - 1, - i.frame_rate_and_duration_.frame( - FrameRateDuration(target_seconds - tot, frame.rate()), true)); - break; - } else { - tmp_logical_frame += i.frame_rate_and_duration_.frames(); - tot += i.frame_rate_and_duration_.seconds(); - } - } - } break; - } - - if (!empty()) { - if (tmp_logical_frame < 0 or - tmp_logical_frame >= static_cast(duration_frames(TimeSourceMode::FIXED, rate))) - throw std::runtime_error("No frames left"); - } - - return tmp_logical_frame; -} - -int EditList::logical_frame( - const TimeSourceMode tsm, - const timebase::flicks target_flicks, - const utility::FrameRate &fixed_rate) const { - - int tmp_logical_frame = 0; - - if (tsm == TimeSourceMode::FIXED) { - - tmp_logical_frame = FrameRateDuration(target_flicks, fixed_rate).frames(); - - } else { // TimeSourceMode::DYNAMIC or TimeSourceMode::REMAPPED - - timebase::flicks tot(0); - - for (const auto &i : sl_) { - - const FrameRateDuration §ion_duration_and_rate = i.frame_rate_and_duration_; - - if ((target_flicks - tot) < section_duration_and_rate.duration()) { - - int frames = tsm == TimeSourceMode::REMAPPED - ? section_duration_and_rate.frame( - FrameRateDuration(target_flicks - tot, fixed_rate), true) - : section_duration_and_rate.frame(target_flicks - tot); - - tmp_logical_frame += std::min(section_duration_and_rate.frames() - 1, frames); - break; - } else { - tmp_logical_frame += section_duration_and_rate.frames(); - tot += section_duration_and_rate.duration(); - } - } - } - - if (!empty()) { - if (tmp_logical_frame < 0 or - tmp_logical_frame >= - static_cast(duration_frames(TimeSourceMode::FIXED, fixed_rate))) { - throw std::runtime_error("No frames left"); - } - } - - return tmp_logical_frame; -} - -EditListSection EditList::skip_sections(int ref_frame, int skip_by) const { - if (empty()) - throw std::runtime_error("No frames"); - - int tot = 0; - EditListSection s = sl_[0]; - // find the section corresponding to 'ref_frame', then jump from this to - // another section, jumping through 'skip_by' sections. - for (auto i = sl_.begin(); i != sl_.end(); i++) { - if ((ref_frame - tot) < (*i).frame_rate_and_duration_.frames()) { - if (skip_by >= 0) { - while (skip_by--) { - i++; - if (i == sl_.end()) { - --i; - break; - } - } - s = (*i); - } else { - while (skip_by++) { - --i; - if (i == sl_.begin()) { - break; - } - } - s = (*i); - } - break; - } else { - tot += (*i).frame_rate_and_duration_.frames(); - } - } - return s; -} - -timebase::flicks EditList::flicks_from_frame( - const TimeSourceMode tsm, const int logical_frame, const utility::FrameRate &rate) const { - timebase::flicks result(0); - if (tsm == TimeSourceMode::FIXED || tsm == TimeSourceMode::REMAPPED) { - result = FrameRateDuration(logical_frame, rate).duration(); - } else // DYNAMIC - { - if ((int)duration_frames(tsm, rate) >= logical_frame) { - - int ltot = 0; - for (const auto &i : sl_) { - if ((logical_frame - ltot) < i.frame_rate_and_duration_.frames()) { - result += timebase::flicks( - i.frame_rate_and_duration_.rate().to_flicks().count() * - (logical_frame - ltot)); - break; - } else { - ltot += i.frame_rate_and_duration_.frames(); - result += i.frame_rate_and_duration_.duration(); - } - } - } else { - - const int overrun_frames = logical_frame - duration_frames(tsm, rate); - result = duration_flicks(tsm, rate); - if (sl_.size()) { - const FrameRateDuration &final_section = sl_.back().frame_rate_and_duration_; - result += - timebase::flicks(overrun_frames * final_section.rate().to_flicks().count()); - } - } - } - return result; -} - - -FrameRate EditList::frame_rate_at_frame(const int logical_frame) const { - FrameRate result; - int frame = 0; - for (const auto &i : sl_) { - - const FrameRateDuration §ion_duration_and_rate = i.frame_rate_and_duration_; - if (logical_frame < (frame + section_duration_and_rate.frames())) { - result = section_duration_and_rate.rate(); - break; - } - frame += section_duration_and_rate.frames(); - } - if (!result && !empty()) { - return sl_.back().frame_rate_and_duration_.rate(); - } - return result; -} - -void EditList::set_uuid(const Uuid &uuid) { - for (auto &i : sl_) - i.media_uuid_ = uuid; -} - -std::pair EditList::flicks_range_to_logical_frame_range( - const timebase::flicks &in, - const timebase::flicks &out, - const TimeSourceMode tsm, - const FrameRate &override_rate) const { - - std::pair rt(0, duration_frames(tsm, override_rate)); - - if (in != timebase::flicks(std::numeric_limits::lowest())) { - try { - rt.first = logical_frame(tsm, in, override_rate); - } catch (...) { - } // ignore out of range - } - if (out != timebase::flicks(std::numeric_limits::max())) { - try { - rt.second = logical_frame(tsm, out, override_rate); - } catch (...) { - } // ignore out of range - } - - return rt; -} - -EditListSection EditList::next_section( - const timebase::flicks &ref_time, - const int skip_sections, - const TimeSourceMode tsm, - const FrameRate &rate) const { - - timebase::flicks t(0); - int current_section_index = std::numeric_limits::max(); - for (size_t idx = 0; idx < sl_.size(); ++idx) { - - const FrameRateDuration §ion_duration_and_rate = sl_[idx].frame_rate_and_duration_; - const timebase::flicks section_duration = section_duration_and_rate.duration( - tsm == TimeSourceMode::FIXED ? rate : FrameRate()); - - if ((t + section_duration) > ref_time) { - current_section_index = static_cast(idx); - break; - } - t += section_duration; - } - - if (current_section_index == std::numeric_limits::max()) { - throw std::runtime_error( - "EditList::next_section error: ref time is at or beyond last section."); - } - - const int skip_to_index = std::max( - int(0), - std::min(static_cast(sl_.size() - 1), current_section_index + skip_sections)); - - return sl_[skip_to_index]; -} - -timebase::flicks EditList::section_start_time( - const utility::Uuid &media_uuid, - const TimeSourceMode tsm, - const FrameRate &override_rate, - FrameRate &out_rate) const { - - bool found = false; - timebase::flicks result(0); - for (const auto &i : sl_) { - if (i.media_uuid_ == media_uuid) { - found = true; - out_rate = tsm == TimeSourceMode::FIXED ? override_rate - : i.frame_rate_and_duration_.rate(); - break; - } - result += i.frame_rate_and_duration_.duration( - tsm == TimeSourceMode::FIXED ? override_rate : FrameRate()); - } - if (!found) { - throw std::runtime_error("EditList::section_start_time - supplied media uuid is not in " - "this source time list."); - } - return result; -} diff --git a/src/utility/src/frame_list.cpp b/src/utility/src/frame_list.cpp index c9dcee03c..584860a2d 100644 --- a/src/utility/src/frame_list.cpp +++ b/src/utility/src/frame_list.cpp @@ -1,6 +1,4 @@ // SPDX-License-Identifier: Apache-2.0 -#include "xstudio/utility/helpers.hpp" - #include #include @@ -9,6 +7,7 @@ #include #include "xstudio/utility/frame_list.hpp" +#include "xstudio/utility/helpers.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/string_helpers.hpp" @@ -52,7 +51,7 @@ int FrameGroup::frame(const size_t index, const bool implied, const bool valid) if (implied) { if (valid) { // find previous valid frame. - _frame = (int)((index / step_) * step_) + start_; + _frame = ((index / step_) * step_) + start_; } else _frame = start_ + index; } else { @@ -280,7 +279,7 @@ xstudio::utility::frame_groups_from_sequence_spec(const caf::uri &from_path) { #endif if (std::regex_match(entryPath, m, path_re)) { int frame = std::atoi(m[1].str().c_str()); - if (fmt::format(path, frame) == entry.path()) { + if (fmt::format(fmt::runtime(path), frame) == entry.path()) { frames.insert(frame); } } diff --git a/src/utility/src/frame_rate_and_duration.cpp b/src/utility/src/frame_rate_and_duration.cpp index 01d781614..c08ea8d9d 100644 --- a/src/utility/src/frame_rate_and_duration.cpp +++ b/src/utility/src/frame_rate_and_duration.cpp @@ -1,7 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -#ifdef _WIN32 -#include -#else +#ifdef __linux__ #include #endif #include @@ -29,9 +27,9 @@ double FrameRateDuration::seconds(const FrameRate &override) const { int FrameRateDuration::frames(const FrameRate &override) const { long int frames = 0; if (override.count()) { - frames = (long)std::round(duration_ / override); + frames = (long)std::round(duration_.count() / override.count()); } else if (rate_.count()) { - frames = (long)std::round(duration_ / rate_); + frames = (long)std::round(duration_.count() / rate_.count()); } return static_cast(frames); } @@ -64,11 +62,9 @@ int FrameRateDuration::frame(const FrameRateDuration &rt, const bool remapped) c void xstudio::utility::from_json(const nlohmann::json &j, FrameRateDuration &rt) { FrameRate r; - long int ticks; - + uint64_t ticks; j.at("rate").get_to(r); j.at("duration").get_to(ticks); - rt.set_duration(timebase::flicks(ticks)); rt.set_rate(r); } @@ -84,6 +80,21 @@ FrameRateDuration FrameRateDuration::operator-(const FrameRateDuration &other) { return FrameRateDuration(frames() - frame(other), rate_); } +FrameRateDuration FrameRateDuration::operator+(const FrameRateDuration &other) { + return FrameRateDuration(frames() + frame(other), rate_); +} + +FrameRateDuration &FrameRateDuration::operator+=(const FrameRateDuration &other) { + set_frames(frames() + other.frames()); + return *this; +} + +FrameRateDuration &FrameRateDuration::operator-=(const FrameRateDuration &other) { + set_frames(frames() - other.frames()); + return *this; +} + + FrameRateDuration FrameRateDuration::subtract_frames(const FrameRateDuration &other, const bool remapped) const { return FrameRateDuration(frames() - other.frames(remapped ? FrameRate() : rate_), rate_); diff --git a/src/utility/src/helpers.cpp b/src/utility/src/helpers.cpp index 471d6e333..e8b36e16d 100644 --- a/src/utility/src/helpers.cpp +++ b/src/utility/src/helpers.cpp @@ -1,7 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -#include "xstudio/utility/helpers.hpp" - -#ifdef __linux__ +#ifndef _WIN32 #define __USE_POSIX #include #include @@ -16,10 +14,15 @@ #include -// #include -// #include +#ifdef __apple__ +#include +#endif + +/*#include +#include */ #include "xstudio/utility/frame_list.hpp" +#include "xstudio/utility/helpers.hpp" #include "xstudio/utility/sequence.hpp" #include "xstudio/utility/string_helpers.hpp" @@ -41,6 +44,51 @@ namespace fs = std::filesystem; // } // } +namespace xstudio { +namespace utility { + static std::vector> s_forward_remap_regex; + static std::vector> s_backward_remap_regex; +} // namespace utility +} // namespace xstudio + +void xstudio::utility::setup_filepath_remap_regex(const utility::JsonStore &j) { + try { + if (!j.is_object()) + throw std::runtime_error("Json should be a dict"); + + for (auto p = j.begin(); p != j.end(); ++p) { + auto f = p.value(); + if (!f.is_array()) continue; + for (const auto &e : f) { + if (e.size() != 3 || !e[0].is_string() || !e[1].is_string() || + !e[2].is_boolean()) + throw std::runtime_error( + "Elements of Json should be an array with 3 elemens [str, str, bool]"); + } + } + + s_forward_remap_regex.clear(); + s_backward_remap_regex.clear(); + + for (auto p = j.begin(); p != j.end(); ++p) { + auto f = p.value(); + if (!f.is_array()) continue; + for (const auto &e : f) { + if (e[2].get()) + s_forward_remap_regex.emplace_back( + e[0].get(), e[1].get()); + else { + s_backward_remap_regex.emplace_back( + e[0].get(), e[1].get()); + } + } + } + + } catch (std::exception &e) { + spdlog::warn("{} {} -- \n\n{}\n\n", __PRETTY_FUNCTION__, e.what(), j.dump(2)); + } +} + static std::shared_ptr s_actor_system_singleton; caf::actor_system &ActorSystemSingleton::actor_system_ref() { @@ -84,7 +132,7 @@ std::string xstudio::utility::actor_to_string(caf::actor_system &sys, const caf: result = utility::make_hex_string(std::begin(buf), std::end(buf)); } catch (const std::exception &err) { - spdlog::debug("{} {}", __PRETTY_FUNCTION__, err.what()); + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); } return result; @@ -111,7 +159,10 @@ xstudio::utility::actor_from_string(caf::actor_system &sys, const std::string &s void xstudio::utility::join_broadcast(caf::event_based_actor *source, caf::actor actor) { - source->request(actor, caf::infinite, broadcast::join_broadcast_atom_v) + if (!actor) + return; + source->mail(broadcast::join_broadcast_atom_v) + .request(actor, caf::infinite) .then( [=](const bool) mutable {}, [=](const error &err) mutable { @@ -120,7 +171,10 @@ void xstudio::utility::join_broadcast(caf::event_based_actor *source, caf::actor } void xstudio::utility::join_broadcast(caf::blocking_actor *source, caf::actor actor) { - source->request(actor, caf::infinite, broadcast::join_broadcast_atom_v) + if (!actor) + return; + source->mail(broadcast::join_broadcast_atom_v) + .request(actor, caf::infinite) .receive( [=](const bool) mutable {}, [=](const error &err) mutable { @@ -129,7 +183,10 @@ void xstudio::utility::join_broadcast(caf::blocking_actor *source, caf::actor ac } void xstudio::utility::leave_broadcast(caf::blocking_actor *source, caf::actor actor) { - source->request(actor, caf::infinite, broadcast::leave_broadcast_atom_v) + if (!actor || !caf::actor_cast(source)) + return; + source->mail(broadcast::leave_broadcast_atom_v) + .request(actor, caf::infinite) .receive( [=](const bool) mutable {}, [=](const error &err) mutable { @@ -138,19 +195,28 @@ void xstudio::utility::leave_broadcast(caf::blocking_actor *source, caf::actor a } void xstudio::utility::leave_broadcast(caf::event_based_actor *source, caf::actor actor) { - source->request(actor, caf::infinite, broadcast::leave_broadcast_atom_v) + if (!actor || !caf::actor_cast(source)) + return; + source->mail(broadcast::leave_broadcast_atom_v) + .request(actor, caf::infinite) .then( [=](const bool) mutable {}, [=](const error &err) mutable { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, to_string(err)); + spdlog::warn("{} {} {}", __PRETTY_FUNCTION__, to_string(err), to_string(actor)); }); } void xstudio::utility::join_event_group(caf::event_based_actor *source, caf::actor actor) { - source->request(actor, caf::infinite, utility::get_event_group_atom_v) + if (!actor) + return; + source->mail(utility::get_event_group_atom_v) + .request(actor, caf::infinite) .then( [=](caf::actor grp) mutable { - source->request(grp, caf::infinite, broadcast::join_broadcast_atom_v) + if (!grp) + return; + source->mail(broadcast::join_broadcast_atom_v) + .request(grp, caf::infinite) .then( [=](const bool) mutable {}, [=](const error &err) mutable { @@ -163,10 +229,16 @@ void xstudio::utility::join_event_group(caf::event_based_actor *source, caf::act } void xstudio::utility::leave_event_group(caf::event_based_actor *source, caf::actor actor) { - source->request(actor, caf::infinite, utility::get_event_group_atom_v) + if (!actor) + return; + source->mail(utility::get_event_group_atom_v) + .request(actor, caf::infinite) .then( [=](caf::actor grp) mutable { - source->request(grp, caf::infinite, broadcast::leave_broadcast_atom_v) + if (!grp) + return; + source->mail(broadcast::leave_broadcast_atom_v) + .request(grp, caf::infinite) .then( [=](const bool) mutable {}, [=](const error &err) mutable { @@ -199,8 +271,8 @@ void xstudio::utility::print_on_exit( }); } -std::string xstudio::utility::exec(const std::vector &cmd, int &exit_code) { - /*reproc::process process; +/*std::string xstudio::utility::exec(const std::vector &cmd, int &exit_code) { + reproc::process process; std::error_code ec = process.start(cmd); if (ec == std::errc::no_such_file_or_directory) { @@ -227,9 +299,12 @@ std::string xstudio::utility::exec(const std::vector &cmd, int &exi return ec.message(); } - return output;*/ - return std::string(); -} + return output; +}*/ + +static std::set doink; +static std::set doink2; +static std::mutex mmmm; std::string xstudio::utility::uri_to_posix_path(const caf::uri &uri) { if (uri.path().data()) { @@ -240,26 +315,38 @@ std::string xstudio::utility::uri_to_posix_path(const caf::uri &uri) { path = "/" + path; } #endif + #ifdef _WIN32 - std::size_t pos = path.find("/"); - if (pos == 0) { + static const std::regex drive_letter_with_unwanted_leading_fwd_slash( + R"(^\/[A-Z]\:)", std::regex::optimize); + std::cmatch m; + if (std::regex_search(path.c_str(), m, drive_letter_with_unwanted_leading_fwd_slash)) { // Remove the leading / path.erase(0, 1); } - /* - // Remove the leading '[protocol]:' part - std::size_t pos = path.find(":"); - if (pos != std::string::npos) { - path.erase(0, pos + 1); // +1 to erase the colon - } - */ #endif - return path; + return forward_remap_file_path(path); } return ""; } +std::string xstudio::utility::forward_remap_file_path(const std::string path) { + auto _path = path; + for (const auto &remap_regex : s_forward_remap_regex) { + _path = std::regex_replace(_path, remap_regex.first, remap_regex.second); + } + return _path; +} + +std::string xstudio::utility::reverse_remap_file_path(const std::string path) { + auto _path = path; + for (const auto &remap_regex : s_backward_remap_regex) { + _path = std::regex_replace(_path, remap_regex.first, remap_regex.second); + } + return _path; +} + std::string xstudio::utility::uri_encode(const std::string &s) { std::string result; result.reserve(s.size()); @@ -268,6 +355,12 @@ std::string xstudio::utility::uri_encode(const std::string &s) { for (size_t i = 0; s[i]; i++) { switch (s[i]) { + case '#': + result += "%23"; + break; + case '%': + result += "%25"; + break; case ' ': result += "%20"; break; @@ -350,16 +443,17 @@ xstudio::utility::uri_framelist_as_sequence(const caf::uri &uri, const FrameList auto fl = frame_list.frames(); uris.reserve(fl.size()); for (const auto i : fl) { - auto new_uri = - caf::make_uri(uri_encode(fmt::format(uri_decode(to_string(uri)), i))); + auto new_uri = caf::make_uri( + uri_encode(fmt::format(fmt::runtime(uri_decode(to_string(uri))), i))); if (not new_uri) { spdlog::warn( "{} {} {}", to_string(uri), uri_decode(to_string(uri)), - uri_encode(fmt::format(uri_decode(to_string(uri)), i))); + uri_encode(fmt::format(fmt::runtime(uri_decode(to_string(uri))), i))); throw std::runtime_error( - "Invalid uri " + uri_encode(fmt::format(uri_decode(to_string(uri)), i))); + "Invalid uri " + + uri_encode(fmt::format(fmt::runtime(uri_decode(to_string(uri))), i))); } uris.push_back(*new_uri); @@ -450,6 +544,7 @@ caf::uri xstudio::utility::parse_cli_posix_path( } caf::uri xstudio::utility::posix_path_to_uri(const std::string &path, const bool abspath) { + auto p = path; if (abspath) { @@ -466,6 +561,9 @@ caf::uri xstudio::utility::posix_path_to_uri(const std::string &path, const bool #endif } + p = reverse_remap_file_path(path); + + // spdlog::warn("posix_path_to_uri: {} -> {}", path, p); // A valid file URI must therefore begin with either file:/path (no hostname), file:///path @@ -475,7 +573,43 @@ caf::uri xstudio::utility::posix_path_to_uri(const std::string &path, const bool if (not p.empty() && p[0] != '/') return caf::uri_builder().scheme("file").path(p).make(); - return caf::uri_builder().scheme("file").host("localhost").path(p).make(); + auto result = caf::uri_builder().scheme("file").host("localhost").path(p).make(); + + return result; + + // NOTE: the 'localhost' hostname does not work with some readers, for + // example ffmpeg. Currently our 'uri_to_posix_path' func simply strips the + // localhost back off before it gets to ffmpeg. It would be better if we + // used uris more directly without going back and forth via + // uri_to_posix_path / posix_path_to_uri so much. + + // The code below is a first stab at doing this but commenting it out + // for now until v2.0 is deployed. + + /*if (p.find("///") == 0) { + return caf::uri_builder().scheme("file").host("").path(p).make(); + } else if (p.find("//") == 0) { + return caf::uri_builder().scheme("file").host("").path("/" + p).make(); + } else if (p.find("/") == 0) { + return caf::uri_builder().scheme("file").host("").path("//" + p).make(); + } + return caf::uri_builder().scheme("file").host("").path("//" + p).make();*/ +} + +std::vector xstudio::utility::uri_subfolders(const caf::uri parent_uri) { + + std::vector result; + try { + auto c = fs::directory_iterator(uri_to_posix_path(parent_uri)); + for (const auto &entry : c) { + if (fs::is_directory(entry)) { + result.emplace_back(posix_path_to_uri(entry.path().string())); + } + } + } catch (const std::exception &err) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, err.what()); + } + return result; } std::vector> @@ -492,22 +626,27 @@ xstudio::utility::scan_posix_path(const std::string &path, const int depth) { try { std::vector files; for (const auto &entry : fs::directory_iterator(path)) { - if (!entry.path().filename().empty() && - entry.path().filename().string()[0] == '.') + +#ifdef _WIN32 + const std::string _path = entry.path().string(); + const std::string _filename = entry.path().filename().string(); + if (not _filename.empty() and _filename[0] == '.') continue; if (fs::is_directory(entry) && (depth > 0 || depth < 0)) { -#ifdef _WIN32 - auto more = scan_posix_path(entry.path().string(), depth - 1); -#else - auto more = scan_posix_path(entry.path(), depth - 1); -#endif + auto more = scan_posix_path(_path, depth - 1); items.insert(items.end(), more.begin(), more.end()); } else if (fs::is_regular_file(entry)) -#ifdef _WIN32 - files.push_back( - std::regex_replace(entry.path().string(), std::regex("[\]"), "/")); + files.push_back(std::regex_replace(_path, std::regex("[\]"), "/")); #else - files.push_back(entry.path()); + const std::string _path = entry.path(); + const std::string _filename = entry.path().filename(); + if (not _filename.empty() and _filename[0] == '.') + continue; + if (fs::is_directory(entry) && (depth > 0 || depth < 0)) { + auto more = scan_posix_path(_path, depth - 1); + items.insert(items.end(), more.begin(), more.end()); + } else if (fs::is_regular_file(entry)) + files.push_back(_path); #endif } auto file_items = uri_from_file_list(files); @@ -518,12 +657,21 @@ xstudio::utility::scan_posix_path(const std::string &path, const int depth) { } else if (fs::is_regular_file(p)) { items.emplace_back(std::make_pair(posix_path_to_uri(path), FrameList())); } else { + FrameList fl; - auto uri = xstudio::utility::parse_cli_posix_path(path, fl, true); - if (not uri.empty()) { - items.emplace_back(std::make_pair(uri, fl)); + if (path.find("http") == 0) { + // TODO: extend parse_cli_posix_path to handle http protocol. + auto uri = caf::make_uri(path); + if (uri) { + items.emplace_back(std::make_pair(*uri, fl)); + } } else { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, path); + auto uri = xstudio::utility::parse_cli_posix_path(path, fl, true); + if (not uri.empty()) { + items.emplace_back(std::make_pair(uri, fl)); + } else { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, path); + } } } } catch (const std::exception &err) { @@ -533,25 +681,26 @@ xstudio::utility::scan_posix_path(const std::string &path, const int depth) { return items; } -std::string xstudio::utility::filemanager_show_uris(const std::vector &uris) { - int exit_code; - - std::vector cmd = { - "dbus-send", - "--session", - "--print-reply", - "--dest=org.freedesktop.FileManager1", - "--type=method_call", - "/org/freedesktop/FileManager1", - "org.freedesktop.FileManager1.ShowItems", - std::string("array:string:") + join_as_string(uris, ","), - "string:"}; - // dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call - // /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems - // array:string:"file://localhost//u/al/Desktop/itsdeadjim.jpg" string:"" - - return xstudio::utility::exec(cmd, exit_code); -} +// std::string xstudio::utility::filemanager_show_uris(const std::vector &uris) { +// int exit_code; + +// std::vector cmd = { +// "dbus-send", +// "--session", +// "--print-reply", +// "--dest=org.freedesktop.FileManager1", +// "--type=method_call", +// "/org/freedesktop/FileManager1", +// "org.freedesktop.FileManager1.ShowItems", +// std::string("array:string:") + join_as_string(uris, ","), +// "string:"}; +// // dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 +// --type=method_call +// // /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems +// // array:string:"file://localhost//u/al/Desktop/itsdeadjim.jpg" string:"" + +// return std::string(); // xstudio::utility::exec(cmd, exit_code); +// } std::string xstudio::utility::get_host_name() { std::array hostname{0}; @@ -561,7 +710,6 @@ std::string xstudio::utility::get_host_name() { std::string xstudio::utility::get_user_name() { std::string result; - #ifdef _WIN32 TCHAR username[MAX_PATH]; DWORD size = MAX_PATH; @@ -594,6 +742,18 @@ std::string xstudio::utility::expand_envvars( const std::string &src, const std::map &additional) { #ifdef _WIN32 + if (src.find("${USER}") != std::string::npos) { + std::string unix_home = utility::replace_once(src, "${USER}", "${USERNAME}"); + return expand_envvars(unix_home, additional); + } + if (src.find("${HOME}") != std::string::npos) { + std::string unix_home = utility::replace_once(src, "${HOME}", "${USERPROFILE}"); + return expand_envvars(unix_home, additional); + } + if (src.find("${TMPDIR}") != std::string::npos) { + std::string unix_home = utility::replace_once(src, "${TMPDIR}", "${TMP}"); + return expand_envvars(unix_home, additional); + } #else // some prefs have ${USERPROFILE} which is MS Windows only, on UNIX we want // ${HOME} @@ -622,6 +782,8 @@ std::string xstudio::utility::expand_envvars( env = additional.at(match_str); } else if (match_str == "USERFULLNAME") { env = utility::get_user_name(); + } else if (match_str == "XSTUDIO_ROOT") { + env = xstudio_root(); } else { spdlog::warn("Undefined envvar ${{{}}}", match_str); env = ""; @@ -641,7 +803,6 @@ std::string xstudio::utility::expand_envvars( std::string xstudio::utility::get_login_name() { std::string result; - #ifdef _WIN32 TCHAR username[MAX_PATH]; DWORD size = MAX_PATH; @@ -692,8 +853,66 @@ bool xstudio::utility::check_plugin_uri_request(const std::string &request) { const static std::regex re(R"((\w+)://.+)"); std::cmatch m; if (std::regex_match(request.c_str(), m, re)) { - if (m[1] != "xstudio" and m[1] != "file") + if (m[1] != "xstudio" and m[1] != "file" and m[1] != "https") return true; } return false; } + +std::string xstudio::utility::xstudio_root(const std::string &append_path) { + auto root = get_env("XSTUDIO_ROOT"); + std::string fallback_root; +#ifdef _WIN32 + char filename[MAX_PATH]; + DWORD nSize = _countof(filename); + DWORD result = GetModuleFileNameA(NULL, filename, nSize); + if (result == 0) { + spdlog::critical( + "Unable to determine executable path from Windows API, falling back " + "to standard methods"); + } else { + auto exePath = fs::path(filename); + + // The first parent path gets us to the bin directory, the second gets us to the + // level above bin. + auto xstudio_root = exePath.parent_path().parent_path(); + fallback_root = xstudio_root.string() + "/share/xstudio"; + } + +#elif defined(__apple__) + // Assuming binary is in MacOS folder in bundle + uint32_t sz = 4096; + std::array bin_path; + auto vv = _NSGetExecutablePath(bin_path.data(), &sz); + auto exePath = fs::path(bin_path.data()); + fallback_root = exePath.parent_path().parent_path(); +#else + + // TODO: This could inspect the current running process and look one directory up. + fallback_root = std::string(BINARY_DIR); + +#endif + + std::string path = (root ? (*root) + append_path : fallback_root + append_path); + const auto p = fs::path(path).string(); + return p; +} + +std::string xstudio::utility::xstudio_plugin_dir(const std::string &append_path) +{ +#ifdef __apple__ + return xstudio_root("/PlugIns/xstudio" + append_path); +#else + return xstudio_root("/plugin" + append_path); +#endif + +} + +std::string xstudio::utility::xstudio_resources_dir(const std::string &append_path) +{ +#ifdef __apple__ + return xstudio_root("/Resources/" + append_path); +#else + return xstudio_root("/" + append_path); +#endif +} diff --git a/src/utility/src/json_store.cpp b/src/utility/src/json_store.cpp index 8ebe62527..e17beb5e2 100644 --- a/src/utility/src/json_store.cpp +++ b/src/utility/src/json_store.cpp @@ -35,14 +35,17 @@ JsonStore xstudio::utility::open_session(const caf::uri &path) { JsonStore xstudio::utility::open_session(const std::string &path) { JsonStore js; - zstr::ifstream i(path); i >> js; - return js; } +void JsonStore::parse_string(const std::string &data) { + auto n = nlohmann::json::parse(data); + *this = nlohmann::json::parse(data); +} + // void JsonStore::merge(const JsonStore &json, const std::string &path) { // merge(json, path); // } diff --git a/src/utility/src/json_store_sync.cpp b/src/utility/src/json_store_sync.cpp new file mode 100644 index 000000000..31f15a9fd --- /dev/null +++ b/src/utility/src/json_store_sync.cpp @@ -0,0 +1,503 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/utility/json_store_sync.hpp" + +using namespace nlohmann; +using namespace xstudio::utility; + +JsonStoreSync::JsonStoreSync(nlohmann::json json) : JsonStore(std::move(json)) {} + +void JsonStoreSync::bind_send_event_func(SendEventFunc fs) { + event_send_func_ = [fs](auto &&PH1, auto &&PH2) { + return fs(std::forward(PH1), std::forward(PH2)); + }; +} + +void JsonStoreSync::process_event( + const nlohmann::json &event_pair, const bool redo, const bool undo_redo, const bool local) { + + const auto &event = (redo ? event_pair.at("redo") : event_pair.at("undo")); + + auto type = event.value("type", ""); + auto id = event.value("id", Uuid()); + + // spdlog::warn("JsonStoreSync::process_event {}", event.dump(2)); + + // ignore reflected events + if (not redo or id != id_) { + last_event_ = event_pair; + if (type == "insert_rows") { + insert_rows_base( + event.value("row", 0), + event.value("count", 0), + event.value("data", nlohmann::json::array()), + event.value("parent", ""), + local, + local ? id_ : id, + undo_redo); + } else if (type == "insert") { + insert_base( + event.value("key", ""), + event.value("data", nlohmann::json()), + event.value("parent", ""), + local, + local ? id_ : id, + undo_redo); + } else if (type == "remove_rows") { + remove_rows_base( + event.value("row", 0), + event.value("count", 0), + event.value("parent", ""), + local, + local ? id_ : id, + undo_redo); + } else if (type == "remove") { + remove_base( + event.value("key", ""), + event.value("parent", ""), + local, + local ? id_ : id, + undo_redo); + } else if (type == "set") { + set_base( + event.value("row", 0), + event.value("data", nlohmann::json::object()), + event.value("parent", ""), + local, + local ? id_ : id, + undo_redo); + } else if (type == "move") { + move_rows_base( + event.value("src_parent", ""), + event.value("src_row", 0), + event.value("count", 0), + event.value("dst_parent", ""), + event.value("dst_row", 0), + local, + local ? id_ : id, + undo_redo); + } else if (type == "reset") { + reset_data_base( + event.value("data", nlohmann::json::object()), + local, + local ? id_ : id, + undo_redo); + } + } +} + +void JsonStoreSync::insert_rows( + const int row, const int count, const nlohmann::json &data, const std::string &parent) { + insert_rows_base(row, count, data, parent, true, id_, false); +} + + +void JsonStoreSync::insert( + const std::string &key, const nlohmann::json &data, const std::string &parent) { + insert_base(key, data, parent, true, id_, false); +} + +void JsonStoreSync::remove(const std::string &key, const std::string &parent) { + remove_base(key, parent, true, id_, false); +} + +void JsonStoreSync::reset_data(const nlohmann::json &data) { + reset_data_base(data, true, id_, false); +} + +void JsonStoreSync::remove_base( + const std::string &key, + const std::string &parent, + const bool local, + const utility::Uuid &id, + const bool undo_redo) { + + // try doing it.. + auto ptr = json::json_pointer(parent); + auto &p = (*(this))[ptr]; + + // if local broadcast + if (local or origin_) { + auto event = xstudio::utility::SyncEvent; + event["redo"] = xstudio::utility::RemoveEvent; + event["redo"]["id"] = id; + event["redo"]["parent"] = parent; + event["redo"]["key"] = key; + + event["undo"] = xstudio::utility::InsertEvent; + event["undo"]["id"] = id; + event["undo"]["parent"] = parent; + event["undo"]["key"] = key; + event["undo"]["data"] = p.at(key); + + last_event_ = event; + if (event_send_func_) + event_send_func_(event, undo_redo); + } + + p.erase(key); +} + +void JsonStoreSync::insert_base( + const std::string &key, + const nlohmann::json &data, + const std::string &parent, + const bool local, + const utility::Uuid &id, + const bool undo_redo) { + + // try doing it.. + auto ptr = json::json_pointer(parent); + auto &p = (*(this))[ptr]; + + // if local broadcast + if (local or origin_) { + auto event = xstudio::utility::SyncEvent; + event["redo"] = xstudio::utility::InsertEvent; + event["redo"]["id"] = id_; + event["redo"]["parent"] = parent; + event["redo"]["key"] = key; + event["redo"]["data"] = data; + + if (p.count(key)) { + event["undo"] = xstudio::utility::InsertEvent; + event["undo"]["id"] = id; + event["undo"]["parent"] = parent; + event["undo"]["key"] = key; + event["undo"]["data"] = p.at(key); + } else { + event["undo"] = xstudio::utility::RemoveEvent; + event["undo"]["id"] = id; + event["undo"]["parent"] = parent; + event["undo"]["key"] = key; + } + + last_event_ = event; + if (event_send_func_) + event_send_func_(event, undo_redo); + } + + p[key] = data; +} + +void JsonStoreSync::insert_rows_base( + const int row, + const int count, + const nlohmann::json &data, + const std::string &parent, + const bool local, + const utility::Uuid &id, + const bool undo_redo) { + + // try doing it.. + auto ptr = json::json_pointer(parent); + auto &p = (*(this))[ptr]; + + if (p.at(children_).is_array()) { + if (data.empty()) { + p[children_].insert(std::next(p[children_].begin(), row), count, R"({})"_json); + } else { + p[children_].insert(std::next(p[children_].begin(), row), data.begin(), data.end()); + } + + // if local broadcast + if (local or origin_) { + auto event = xstudio::utility::SyncEvent; + event["redo"] = xstudio::utility::InsertRowsEvent; + event["redo"]["id"] = id; + event["redo"]["parent"] = parent; + event["redo"]["row"] = row; + event["redo"]["count"] = count; + event["redo"]["data"] = data; + + event["undo"] = xstudio::utility::RemoveRowsEvent; + event["undo"]["id"] = id; + event["undo"]["parent"] = parent; + event["undo"]["row"] = row; + event["undo"]["count"] = count; + + last_event_ = event; + if (event_send_func_) + event_send_func_(event, undo_redo); + } + } +} + +void JsonStoreSync::remove_rows(const int row, const int count, const std::string &parent) { + if (count) + remove_rows_base(row, count, parent, true, id_, false); +} + +void JsonStoreSync::remove_rows_base( + const int row, + const int count, + const std::string &parent, + const bool local, + const utility::Uuid &id, + const bool undo_redo) { + // try doing it.. + auto ptr = json::json_pointer(parent); + auto &p = (*(this))[ptr]; + + if (p.at(children_).is_array()) { + + if (local or origin_) { + auto event = xstudio::utility::SyncEvent; + event["redo"] = xstudio::utility::RemoveRowsEvent; + event["redo"]["id"] = id; + event["redo"]["parent"] = parent; + event["redo"]["row"] = row; + event["redo"]["count"] = count; + + event["undo"] = xstudio::utility::InsertRowsEvent; + event["undo"]["id"] = id; + event["undo"]["parent"] = parent; + event["undo"]["row"] = row; + event["undo"]["count"] = count; + + auto undo_data = R"([])"_json; + for (auto i = 0; i < count; i++) + undo_data.push_back(p.at(children_).at(row + i)); + event["undo"]["data"] = undo_data; + + last_event_ = event; + if (event_send_func_) + event_send_func_(event, undo_redo); + } + + p[children_].erase( + std::next(p[children_].begin(), row), std::next(p[children_].begin(), row + count)); + } +} + +void JsonStoreSync::set( + const int row, + const std::string &key, + const nlohmann::json &data, + const std::string &parent) { + auto tmp = R"({})"_json; + tmp[key] = data; + set(row, tmp, parent); +} + +void JsonStoreSync::set(const int row, const nlohmann::json &data, const std::string &parent) { + set_base(row, data, parent, true, id_, false); +} + +void JsonStoreSync::set_base( + const int row, + const nlohmann::json &data, + const std::string &parent, + const bool local, + const utility::Uuid &id, + const bool undo_redo) { + + // try doing it.. + auto ptr = json::json_pointer(parent); + auto &p = (*(this))[ptr]; + + if (p.at(children_).is_array()) { + + if (local or origin_) { + auto event = xstudio::utility::SyncEvent; + event["redo"] = xstudio::utility::SetEvent; + event["redo"]["id"] = id; + event["redo"]["parent"] = parent; + event["redo"]["row"] = row; + event["redo"]["data"] = data; + + event["undo"] = xstudio::utility::SetEvent; + event["undo"]["id"] = id; + event["undo"]["parent"] = parent; + event["undo"]["row"] = row; + + auto undo_data = R"({})"_json; + for (const auto &i : data.items()) + undo_data[i.key()] = p.at(children_).at(row).at(i.key()); + + event["undo"]["data"] = undo_data; + + last_event_ = event; + if (event_send_func_) + event_send_func_(event, undo_redo); + } + + p[children_][row].update(data); + } +} + +void JsonStoreSync::move_rows( + const std::string &src_parent, + const int src_row, + const int count, + const std::string &dst_parent, + const int dst_row) { + move_rows_base(src_parent, src_row, count, dst_parent, dst_row, true, id_, false); +} + +void JsonStoreSync::move_rows_base( + const std::string &src_parent, + const int src_row, + const int count, + const std::string &dst_parent, + const int dst_row, + const bool local, + const utility::Uuid &id, + const bool undo_redo) { + + auto sptr = json::json_pointer(src_parent); + auto &sp = (*(this))[sptr]; + auto tmp = json::array(); + + // entries are moved in to tmp, this make life much eaiser + // but dst locations need to take this into account. + + // spdlog::warn("{}", sp.dump(2)); + + + if (sp.at(children_).is_array()) { + // move entries to tmp buffer. + auto e = std::next(sp.at(children_).begin(), src_row); + + for (auto i = 0; i < count; i++) { + tmp.push_back(*e); + e = sp[children_].erase(e); + } + // now inject into dst. + + auto dptr = json::json_pointer(dst_parent); + auto &dp = (*(this))[dptr]; + + if (dp.at(children_).is_array()) { + dp[children_].insert( + std::next(dp.at(children_).begin(), dst_row), tmp.begin(), tmp.end()); + + // if local broadcast + if (local or origin_) { + auto event = SyncEvent; + + event["redo"] = xstudio::utility::MoveEvent; + event["redo"]["id"] = id; + event["redo"]["src_parent"] = src_parent; + event["redo"]["src_row"] = src_row; + event["redo"]["count"] = count; + event["redo"]["dst_parent"] = dst_parent; + event["redo"]["dst_row"] = dst_row; + + event["undo"] = xstudio::utility::MoveEvent; + event["undo"]["id"] = id; + event["undo"]["src_parent"] = dst_parent; + event["undo"]["src_row"] = dst_row; + event["undo"]["count"] = count; + event["undo"]["dst_parent"] = src_parent; + event["undo"]["dst_row"] = src_row; + + last_event_ = event; + if (event_send_func_) + event_send_func_(event, undo_redo); + } + } + } + // spdlog::warn("{}", sp.dump(2)); +} + +void JsonStoreSync::reset_data_base( + const nlohmann::json &data, + const bool local, + const utility::Uuid &id, + const bool undo_redo) { + auto &tmp = JsonStore::at(json::json_pointer("")); + + if (local or origin_) { + auto event = SyncEvent; + + event["redo"] = xstudio::utility::ResetEvent; + event["redo"]["id"] = id; + event["redo"]["data"] = data; + + event["undo"] = xstudio::utility::ResetEvent; + event["undo"]["id"] = id; + event["undo"]["data"] = tmp; + + last_event_ = event; + if (event_send_func_) + event_send_func_(event, undo_redo); + } + + tmp = data; +} + +const nlohmann::json &JsonStoreSync::as_json() const { return *this; } + +nlohmann::json::const_reference JsonStoreSync::at(size_type idx) const { + return JsonStore::at(idx); +} + +nlohmann::json::const_reference +JsonStoreSync::at(const nlohmann::json::json_pointer &ptr) const { + return JsonStore::at(ptr); +} + +nlohmann::json::const_reference +JsonStoreSync::at(const typename nlohmann::json::object_t::key_type &key) const { + return JsonStore::at(key); +} + +std::optional JsonStoreSync::find_first( + const std::string &key, + const std::optional value, + const int prune_depth, + const nlohmann::json::json_pointer &parent, + const int row) const { + auto result = find(key, value, 1, prune_depth, parent, row); + if (result.empty()) + return {}; + + return result[0]; +} + + +std::vector JsonStoreSync::find( + const std::string &key, + const std::optional value, + const int max_find_count, + const int prune_depth, + const nlohmann::json::json_pointer &parent, + const int row) const { + std::vector result; + + auto cptr = parent / children_; + + if (contains(cptr) and at(cptr).is_array()) { + const auto &children = at(cptr); + // check for matches + for (size_t i = row; i < children.size(); i++) { + if (children.at(i).is_object()) { + if (children.at(i).count(key) and + (not value or (*value) == children.at(i).at(key))) { + + result.emplace_back(cptr / std::to_string(i)); + if (max_find_count > 0 and + static_cast(result.size()) >= max_find_count) + break; + } + + if (prune_depth - 1) { + auto child_results = find( + key, + value, + max_find_count - result.size(), + prune_depth - 1, + cptr / std::to_string(i), + 0); + result.insert(result.end(), child_results.begin(), child_results.end()); + if (max_find_count > 0 and + static_cast(result.size()) >= max_find_count) + break; + } + } + } + } + + return result; +} diff --git a/src/utility/src/logging.cpp b/src/utility/src/logging.cpp index 4e8772bb5..027ef9195 100644 --- a/src/utility/src/logging.cpp +++ b/src/utility/src/logging.cpp @@ -10,6 +10,12 @@ using namespace xstudio::utility; void xstudio::utility::start_logger( const spdlog::level::level_enum lvl, const std::string &logfile) { + + static bool logging_started = false; + if (logging_started) + return; + logging_started = true; + // spdlog::init_thread_pool(8192, 1); auto stderr_sink = std::make_shared(); stderr_sink->set_level(lvl); @@ -27,7 +33,7 @@ void xstudio::utility::start_logger( // sinks.end(), spdlog::thread_pool(), spdlog::async_overflow_policy::block); auto logger = std::make_shared("xstudio", sinks.begin(), sinks.end()); spdlog::set_default_logger(logger); - // spdlog::set_level(spdlog::level::debug); + spdlog::set_level(spdlog::level::debug); // spdlog::set_error_handler([](const std::string &msg){ // spdlog::warn("{}", msg); diff --git a/src/utility/src/media_reference.cpp b/src/utility/src/media_reference.cpp index 14cbfeaf6..028a3d22a 100644 --- a/src/utility/src/media_reference.cpp +++ b/src/utility/src/media_reference.cpp @@ -58,7 +58,7 @@ MediaReference::MediaReference( MediaReference::MediaReference(const JsonStore &jsn) : container_(jsn.at("container")), duration_(jsn.at("duration")) { - auto uri = caf::make_uri(jsn.at("uri")); + auto uri = caf::make_uri(jsn.at("uri").get()); if (uri) uri_ = *uri; else { @@ -74,20 +74,22 @@ MediaReference::MediaReference(const JsonStore &jsn) throw XStudioError("Invalid URI " + jsn.at("uri").get()); } - frame_list_ = jsn.at("frame_list"); - timecode_ = jsn.at("timecode"); - offset_ = jsn.value("offset", 0); + frame_list_ = jsn.at("frame_list"); + timecode_ = jsn.at("timecode"); + offset_ = jsn.value("offset", 0); + start_frame_offset_ = jsn.value("start_frame_offset", 0); } JsonStore MediaReference::serialise() const { JsonStore jsn; - jsn["uri"] = to_string(uri_); - jsn["container"] = container_; - jsn["duration"] = duration_; - jsn["frame_list"] = static_cast(frame_list_); - jsn["timecode"] = static_cast(timecode_); - jsn["offset"] = offset_; + jsn["uri"] = to_string(uri_); + jsn["container"] = container_; + jsn["duration"] = duration_; + jsn["frame_list"] = static_cast(frame_list_); + jsn["timecode"] = static_cast(timecode_); + jsn["offset"] = offset_; + jsn["start_frame_offset"] = start_frame_offset_; return jsn; } @@ -111,7 +113,54 @@ void MediaReference::set_frame_list(const FrameList &fl) { void MediaReference::set_rate(const FrameRate &rate) { duration_.set_rate(rate, true); } -caf::uri MediaReference::uri() const { return uri_; } +caf::uri MediaReference::uri(const FramePadFormat fpf) const { + auto result = uri_; + + if (not container_) { + switch (fpf) { + case FramePadFormat::FPF_SHAKE: + // replace formet string with correct number of #/@ + { + auto str = uri_decode(to_string(uri_)); + std::cmatch m; + if (std::regex_match(str.c_str(), m, std::regex(".*\\{:(\\d+)d\\}.*"))) { + auto repstr = std::string(std::stoi(m[1].str()), '#'); + + result = *caf::make_uri(uri_encode(std::regex_replace( + str, + std::regex("(\\{:\\d+d\\})"), + repstr, + std::regex_constants::format_first_only))); + } + } + break; + + case FramePadFormat::FPF_NUKE: + // replace format string with %04d etc. + // file://localhost//tmp/test/test.{:04d}.exr + + { + result = *caf::make_uri(uri_encode(std::regex_replace( + uri_decode(to_string(uri_)), + std::regex("\\{:(\\d+d)\\}"), + "%$1", + std::regex_constants::format_first_only))); + } + break; + + case FramePadFormat::FPF_XSTUDIO: + default: + break; + } + } + + return result; +} + +// EXPECT_EQ(mr2.uri(), posix_path_to_uri("/tmp/test/test.{:03d}.exr")); +// EXPECT_EQ(mr2.uri(MediaReference::FramePadFormat::FPF_NUKE), +// posix_path_to_uri("/tmp/test/test.%03d.exr")); + void MediaReference::set_uri(const caf::uri &uri) { uri_ = uri; } @@ -150,8 +199,8 @@ std::optional MediaReference::uri_from_frame(const int sequence_frame) if (container_) return uri_; - auto _uri = - caf::make_uri(uri_encode(fmt::format(uri_decode(to_string(uri_)), sequence_frame))); + auto _uri = caf::make_uri( + uri_encode(fmt::format(fmt::runtime(uri_decode(to_string(uri_))), sequence_frame))); if (_uri) return *_uri; @@ -175,4 +224,13 @@ std::optional MediaReference::uri(const int logical_frame, int &file_f return {}; } + +void MediaReference::fill_partial_sequences() { + // any frame sequence that has gaps, steps etc. will have the frame list + // filled out so we have a contiguous set of frames. + if (!frame_list_.empty()) { + set_frame_list(FrameList(frame_list_.start(), frame_list_.frames().back())); + } +} + } // namespace xstudio::utility diff --git a/src/utility/src/notification_handler.cpp b/src/utility/src/notification_handler.cpp new file mode 100644 index 000000000..15998d06a --- /dev/null +++ b/src/utility/src/notification_handler.cpp @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "xstudio/atoms.hpp" +#include "xstudio/utility/notification_handler.hpp" + +namespace xstudio::utility { + +Notification::Notification(const JsonStore &jsn) { + uuid_ = jsn.value("uuid", Uuid()); + + auto type = jsn.value("type", "UNKNOWN"); + type_ = NT_UNKNOWN; + + if (type == "INFO") + type_ = NT_INFO; + else if (type == "WARN") + type_ = NT_WARN; + else if (type == "PROCESSING") + type_ = NT_PROCESSING; + else if (type == "PROGRESS_RANGE") + type_ = NT_PROGRESS_RANGE; + else if (type == "PROGRESS_PERCENTAGE") + type_ = NT_PROGRESS_PERCENTAGE; + + expires_ = jsn.value("expires", utility::sysclock::now()); + + if (type_ == NT_PROGRESS_RANGE) { + progress_minimum_ = jsn.value("min_progress", 0.f); + progress_maximum_ = jsn.value("max_progress", 0.f); + progress_ = jsn.value("progress", 0.f); + text_ = jsn.value("_text", ""); + } else if (type_ == NT_PROGRESS_PERCENTAGE) { + progress_minimum_ = jsn.value("min_progress", 0.f); + progress_maximum_ = jsn.value("max_progress", 0.f); + progress_ = jsn.value("progress", 0.f); + text_ = jsn.value("_text", ""); + } else { + text_ = jsn.value("_text", ""); + } +} + + +void to_json(nlohmann::json &j, const Notification &n) { + j["uuid"] = n.uuid_; + switch (n.type_) { + case NT_INFO: + j["type"] = "INFO"; + break; + case NT_WARN: + j["type"] = "WARN"; + break; + case NT_PROGRESS_RANGE: + j["type"] = "PROGRESS_RANGE"; + break; + case NT_PROGRESS_PERCENTAGE: + j["type"] = "PROGRESS_PERCENTAGE"; + break; + case NT_PROCESSING: + j["type"] = "PROCESSING"; + break; + default: + j["type"] = "UNKNOWN"; + break; + } + j["expires"] = n.expires_; + + if (n.type_ == NT_PROGRESS_RANGE) { + j["min_progress"] = n.progress_minimum_; + j["max_progress"] = n.progress_maximum_; + j["progress"] = n.progress_; + j["progress_percent"] = n.progress_percentage(); + j["text"] = n.progress_text_range(); + j["_text"] = n.text_; + } else if (n.type_ == NT_PROGRESS_PERCENTAGE) { + j["min_progress"] = n.progress_minimum_; + j["max_progress"] = n.progress_maximum_; + j["progress"] = n.progress_; + j["progress_percent"] = n.progress_percentage(); + j["text"] = n.progress_text_percentage(); + j["_text"] = n.text_; + } else { + j["text"] = n.text_; + j["_text"] = n.text_; + } +} + +caf::message_handler NotificationHandler::default_event_handler() { + return {[=](utility::event_atom, notification_atom, const JsonStore &) {}}; +} + +caf::message_handler +NotificationHandler::message_handler(caf::event_based_actor *act, caf::actor event_group) { + actor_ = act; + event_group_ = event_group; + + return caf::message_handler( + {[=](notification_atom) -> JsonStore { return digest(); }, + [=](notification_atom, notification_atom) -> std::vector { + auto result = std::vector(); + result.reserve(notifications_.size()); + result.insert(result.end(), notifications_.begin(), notifications_.end()); + return result; + }, + [=](notification_atom, const Uuid &uuid) -> bool { + auto result = remove_notification(uuid); + if (result) + actor_->mail(utility::event_atom_v, notification_atom_v, digest()) + .send(event_group_); + return result; + }, + [=](notification_atom, const bool check_expire) { + if (check_expired()) { + actor_->mail(utility::event_atom_v, notification_atom_v, digest()) + .send(event_group_); + utility::sys_time_point next_expires = next_expire(); + if (next_expires != utility::sys_time_point()) { + if (next_expires > utility::sysclock::now()) + anon_mail(notification_atom_v, true) + .delay(next_expires - utility::sysclock::now()) + .send(act, weak_ref); + else + anon_mail(notification_atom_v, true).send(act); + } + } + }, + [=](notification_atom, const Notification ¬ification) -> bool { + auto result = add_update_notification(notification); + if (result) { + actor_->mail(utility::event_atom_v, notification_atom_v, digest()) + .send(event_group_); + utility::sys_time_point next_expires = next_expire(); + if (next_expires != utility::sys_time_point()) { + if (next_expires > utility::sysclock::now()) + anon_mail(notification_atom_v, true) + .delay(next_expires - utility::sysclock::now()) + .send(act, weak_ref); + else + anon_mail(notification_atom_v, true).send(act); + } + } + return result; + }, + [=](notification_atom, const Uuid &uuid, const float progress) -> bool { + auto result = update_progress(uuid, progress); + if (result) + actor_->mail(utility::event_atom_v, notification_atom_v, digest()) + .send(event_group_); + return result; + }}); +} + +utility::sys_time_point NotificationHandler::next_expire() const { + utility::sys_time_point result; + + if (not notifications_.empty()) + result = notifications_.front().expires_; + + for (const auto &i : notifications_) { + if (i.expires_ < result) + result = i.expires_; + } + + return result; +} + + +bool NotificationHandler::add_update_notification(const Notification ¬ification) { + auto result = false; + + for (auto it = notifications_.begin(); not result and it != notifications_.end(); ++it) { + if (it->uuid_ == notification.uuid_) { + result = true; + *it = notification; + it->update_expires(); + } + } + + if (not result) { + result = true; + notifications_.push_back(notification); + notifications_.back().update_expires(); + } + + return result; +} + + +JsonStore NotificationHandler::digest() const { + auto result = R"([])"_json; + + for (const auto &i : notifications_) + result.push_back(i); + + return result; +} + +bool NotificationHandler::check_expired() { + auto now = utility::sysclock::now(); + auto result = false; + auto it = notifications_.begin(); + + while (it != notifications_.end()) { + if (it->expires_ < now) { + result = true; + it = notifications_.erase(it); + } else + it++; + } + + return result; +} + +bool NotificationHandler::remove_notification(const Uuid &uuid) { + auto result = false; + + for (auto it = notifications_.begin(); not result and it != notifications_.end(); ++it) { + if (it->uuid_ == uuid) { + result = true; + it = notifications_.erase(it); + } + } + + return result; +} + +bool NotificationHandler::update_progress(const Uuid &uuid, const float value) { + auto result = false; + + for (auto it = notifications_.begin(); not result and it != notifications_.end(); ++it) { + if (it->uuid_ == uuid) { + if (it->progress_ != value) { + result = true; + it->progress_ = value; + } + break; + } + } + + return result; +} + + +} // namespace xstudio::utility diff --git a/src/utility/src/remote_session_file.cpp b/src/utility/src/remote_session_file.cpp index 20ab3d800..756913f92 100644 --- a/src/utility/src/remote_session_file.cpp +++ b/src/utility/src/remote_session_file.cpp @@ -14,7 +14,7 @@ using namespace xstudio::utility; -static std::regex parse_re(R"((.+)_(.+)_(.+)_(.+))"); +static std::regex parse_re(R"((.+)_.+_(.+)_(.+))"); RemoteSessionFile::RemoteSessionFile(const std::string &file_path) { // build entry.. @@ -30,9 +30,8 @@ RemoteSessionFile::RemoteSessionFile(const std::string &file_path) { std::smatch match; if (std::regex_search(file_name, match, parse_re)) { session_name_ = match[1]; - sync_ = (match[2] == "sync" ? true : false); - host_ = match[3]; - port_ = std::stoi(match[4], nullptr, 0); + host_ = match[2]; + port_ = std::stoi(match[3], nullptr, 0); } else { throw std::runtime_error("Invalid remote session file. " + file_name); } @@ -53,8 +52,8 @@ RemoteSessionFile::RemoteSessionFile(const std::string &file_path) { last_write_ = fs::last_write_time(filepath()); } -RemoteSessionFile::RemoteSessionFile(const std::string path, const int port, const bool sync) - : path_(std::move(path)), port_(port), sync_(sync) { +RemoteSessionFile::RemoteSessionFile(const std::string path, const int port) + : path_(std::move(path)), port_(port) { pid_ = get_pid(); for (auto i = 0; i < 100; i++) { @@ -72,15 +71,13 @@ RemoteSessionFile::RemoteSessionFile(const std::string path, const int port, con RemoteSessionFile::RemoteSessionFile( const std::string path, const int port, - const bool sync, const std::string session_name, const std::string host, const bool force_cleanup) : path_(std::move(path)), session_name_(std::move(session_name)), host_(std::move(host)), - port_(port), - sync_(sync) { + port_(port) { // we can't test if remote is still valid. // so we should only use this to create a new connection ? @@ -176,16 +173,7 @@ RemoteSessionManager::~RemoteSessionManager() { std::optional RemoteSessionManager::first_api() const { for (const auto &i : sessions_) { - if (i.host() == "localhost" and not i.sync()) - return i; - } - - return {}; -} - -std::optional RemoteSessionManager::first_sync() const { - for (const auto &i : sessions_) { - if (i.host() == "localhost" and i.sync()) + if (i.host() == "localhost") return i; } @@ -207,8 +195,8 @@ void RemoteSessionManager::add_session_file(const RemoteSessionFile rsm) { } -std::string RemoteSessionManager::create_session_file(const int port, const bool sync) { - auto i = RemoteSessionFile(path_, port, sync); +std::string RemoteSessionManager::create_session_file(const int port) { + auto i = RemoteSessionFile(path_, port); std::string session_name = i.session_name(); sessions_.emplace_front(i); return session_name; @@ -216,12 +204,10 @@ std::string RemoteSessionManager::create_session_file(const int port, const bool void RemoteSessionManager::create_session_file( const int port, - const bool sync, const std::string session_name, const std::string host, const bool force_cleanup) { - sessions_.emplace_front( - RemoteSessionFile(path_, port, sync, session_name, host, force_cleanup)); + sessions_.emplace_front(RemoteSessionFile(path_, port, session_name, host, force_cleanup)); } void RemoteSessionManager::remove_session(const std::string &session_name) { diff --git a/src/utility/src/sequence.cpp b/src/utility/src/sequence.cpp index 53a1f1d75..afa6ef48d 100644 --- a/src/utility/src/sequence.cpp +++ b/src/utility/src/sequence.cpp @@ -74,6 +74,7 @@ std::vector uri_from_file(const std::string &path) { Entry::Entry(const std::string path) : name_(std::move(path)) { std::memset(&stat_, 0, sizeof stat_); } + #ifdef _WIN32 uint64_t get_block_size_windows(const xstudio::utility::Entry &entry) { const std::string &path = entry.name_; // Assuming 'name_' contains the path @@ -102,9 +103,12 @@ Sequence::Sequence(const Entry &entry) size_(entry.stat_.st_blocks * 512), #endif apparent_size_(entry.stat_.st_size), -#ifdef _WIN32 +#if defined(_WIN32) mtim_(get_mtim(entry.stat_)), ctim_(get_ctim(entry.stat_)), +#elif defined(__apple__) + mtim_(entry.stat_.st_mtimespec.tv_sec), + ctim_(entry.stat_.st_mtimespec.tv_sec), #else mtim_(entry.stat_.st_mtim.tv_sec), ctim_(entry.stat_.st_ctim.tv_sec), @@ -243,21 +247,15 @@ std::optional create_default_seq(const Entry &entry) { R"((.+\.)([-]?\d+)(\.[^.\d]{1,4}\.[^.]{1,3}))", std::regex::optimize); static const std::regex body_dot_number_and_ext( R"((.+\.)([-]?\d+)(\.[^.]+))", std::regex::optimize); - // static const std::regex body_number_and_ext( - // R"((.+?)([-]?\d+)(\.[^.]+))", std::regex::optimize); - // static const std::regex body_number_body("(.+)([-]?\\d{3,})(.+)$"); - // if (std::regex_match(entry.name_.c_str(), m, bare_number) or - // std::regex_match(entry.name_.c_str(), m, number_and_ext) or - // std::regex_match(entry.name_.c_str(), m, body_dot_number_and_double_ext) or - // std::regex_match(entry.name_.c_str(), m, body_dot_number_and_ext) or - // std::regex_match(entry.name_.c_str(), m, body_number_and_ext)) { + static const std::regex body_no_dot_number_and_ext( + R"((.+[^0-9])(\d+)(\.[^.]+))", std::regex::optimize); if (std::regex_match(entry.name_.c_str(), m, bare_number) or std::regex_match(entry.name_.c_str(), m, number_and_ext) or std::regex_match(entry.name_.c_str(), m, body_dot_number_and_double_ext) or - std::regex_match(entry.name_.c_str(), m, body_dot_number_and_ext) - // std::regex_match(entry.name_.c_str(), m, body_number_and_ext) - ) { + std::regex_match(entry.name_.c_str(), m, body_dot_number_and_ext) or + std::regex_match(entry.name_.c_str(), m, body_no_dot_number_and_ext)) { + // skip versions.. if (ends_with(m[1].str(), "_v")) return {}; @@ -420,4 +418,4 @@ std::vector sequences_from_entries( return sequences; } -} // namespace xstudio::utility +} // namespace xstudio::utility \ No newline at end of file diff --git a/src/utility/src/timecode.cpp b/src/utility/src/timecode.cpp index d9e317645..071d169dd 100644 --- a/src/utility/src/timecode.cpp +++ b/src/utility/src/timecode.cpp @@ -25,7 +25,16 @@ Timecode::Timecode( Timecode::Timecode(const unsigned int f, const double fr, const bool df) : frame_rate_(fr == 0.0 ? 24.0 : fr), drop_frame_(df) { + total_frames(f); +} + +Timecode::Timecode(const std::string &timecode, const double fr, const bool df) + : frame_rate_(fr == 0.0 ? 24.0 : fr), drop_frame_(df) { + set_timecode(timecode); + validate(); +} +void Timecode::total_frames(const unsigned int f) { const unsigned int nominal_fps = nominal_framerate(); unsigned int frame_input = f % max_frames(); @@ -45,11 +54,6 @@ Timecode::Timecode(const unsigned int f, const double fr, const bool df) hours_ = ((frame_input / nominal_fps) / 60 / 60); } -Timecode::Timecode(const std::string &timecode, const double fr, const bool df) - : frame_rate_(fr == 0.0 ? 24.0 : fr), drop_frame_(df) { - set_timecode(timecode); - validate(); -} void Timecode::set_timecode(const std::string &timecode) { unsigned int h, m, s, f; diff --git a/src/utility/test/CMakeLists.txt b/src/utility/test/CMakeLists.txt index 009754678..d9f5bf759 100644 --- a/src/utility/test/CMakeLists.txt +++ b/src/utility/test/CMakeLists.txt @@ -2,7 +2,8 @@ include(CTest) SET(LINK_DEPS xstudio::bookmark - caf::core + CAF::core + xstudio::utility ) create_tests("${LINK_DEPS}") diff --git a/src/utility/test/authentication_test.cpp b/src/utility/test/authentication_test.cpp new file mode 100644 index 000000000..69be0ba48 --- /dev/null +++ b/src/utility/test/authentication_test.cpp @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "xstudio/utility/authentication.hpp" +#include + +using namespace xstudio::utility; + +TEST(AuthenticationTest, Test) {} \ No newline at end of file diff --git a/src/utility/test/frame_time_test.cpp b/src/utility/test/frame_time_test.cpp index eb2493506..23c870c7b 100644 --- a/src/utility/test/frame_time_test.cpp +++ b/src/utility/test/frame_time_test.cpp @@ -1,10 +1,98 @@ // SPDX-License-Identifier: Apache-2.0 -#include "xstudio/utility/edit_list.hpp" #include "xstudio/utility/frame_range.hpp" #include using namespace xstudio::utility; +TEST(FrameRangeIntersect, Test) { + auto fr1 = FrameRange( + 24 * timebase::k_flicks_24fps, 24 * timebase::k_flicks_24fps, timebase::k_flicks_24fps); + + // auto fr2 = FrameRange(); + + EXPECT_EQ(fr1.intersect(fr1), fr1); + + // no overlap front + EXPECT_EQ( + fr1.intersect(FrameRange( + timebase::k_flicks_zero_seconds, + 24 * timebase::k_flicks_24fps, + timebase::k_flicks_24fps)) + .duration(), + timebase::k_flicks_zero_seconds); + + // no overlap end + EXPECT_EQ( + fr1.intersect(FrameRange( + 48 * timebase::k_flicks_24fps, + 24 * timebase::k_flicks_24fps, + timebase::k_flicks_24fps)) + .duration(), + timebase::k_flicks_zero_seconds); + + + // one frame + EXPECT_EQ( + fr1.intersect(FrameRange( + timebase::k_flicks_zero_seconds, + 25 * timebase::k_flicks_24fps, + timebase::k_flicks_24fps)) + .duration(), + timebase::k_flicks_24fps); + + // one frame + EXPECT_EQ( + fr1.intersect(FrameRange( + timebase::k_flicks_zero_seconds, + 25 * timebase::k_flicks_24fps, + timebase::k_flicks_24fps)) + .start(), + 24 * timebase::k_flicks_24fps); + + // check duration truncation. + + EXPECT_EQ( + fr1.intersect(FrameRange( + timebase::k_flicks_zero_seconds, + 48 * timebase::k_flicks_24fps, + timebase::k_flicks_24fps)), + fr1); + + EXPECT_EQ( + fr1.intersect(FrameRange( + timebase::k_flicks_zero_seconds, + 49 * timebase::k_flicks_24fps, + timebase::k_flicks_24fps)), + fr1); + + auto avail = FrameRange( + 24 * timebase::k_flicks_24fps, 24 * timebase::k_flicks_24fps, timebase::k_flicks_24fps); + auto active = FrameRange( + 0 * timebase::k_flicks_24fps, 48 * timebase::k_flicks_24fps, timebase::k_flicks_24fps); + + EXPECT_FALSE(avail.intersect(active) == active); + + auto fr_actjvn = FrameRange(); + from_json( + R"({ + "duration": 3057600000, + "rate": 29400000, + "start": 30723000000 + })"_json, + fr_actjvn); + + auto fr_avajvn = FrameRange(); + from_json( + R"({ + "duration": 3057600000, + "rate": 29400000, + "start": 30723000000 + })"_json, + fr_actjvn); + + EXPECT_FALSE(fr_avajvn.intersect(fr_actjvn) == fr_actjvn); +} + TEST(FrameRateTest, Test) { FrameRate ri24(24); @@ -18,7 +106,7 @@ TEST(FrameRateTest, Test) { EXPECT_EQ(r24.to_microseconds().count(), std::chrono::microseconds(41666).count()); - EXPECT_EQ((FrameRate(1.0) / r24), 24); + // EXPECT_EQ((FrameRate(1.0) / r24).count(), 24); FrameRate r0; @@ -158,295 +246,3 @@ TEST(FrameRateDurationTest3, Test) { // EXPECT_EQ(a.duration().seconds(), 0.0); // } - -TEST(EditListTest1, Test) { - FrameRateDuration r12_24(12l, 24.0); - FrameRateDuration r6_12(6l, 12.0); - FrameRateDuration r24_48(24l, 48.0); - FrameRate r24(timebase::k_flicks_24fps); - - EditList edit_list( - {EditListSection(Uuid(), r12_24, Timecode()), - EditListSection(Uuid(), r6_12, Timecode()), - EditListSection(Uuid(), r24_48, Timecode())}); - - EXPECT_EQ(edit_list.duration_frames(TimeSourceMode::FIXED, r24), unsigned(42)); - EXPECT_EQ(edit_list.duration_frames(TimeSourceMode::DYNAMIC, r24), unsigned(42)); - EXPECT_EQ(edit_list.duration_frames(TimeSourceMode::REMAPPED, r24), unsigned(36)); - - EXPECT_EQ(edit_list.duration_seconds(TimeSourceMode::FIXED, r24), 42.0 / 24.0); - EXPECT_EQ(edit_list.duration_seconds(TimeSourceMode::DYNAMIC, r24), 1.5f); - EXPECT_EQ(edit_list.duration_seconds(TimeSourceMode::REMAPPED, r24), 1.5f); - - EXPECT_EQ( - timebase::to_seconds(edit_list.duration_flicks(TimeSourceMode::FIXED, r24)), - 42.0 / 24.0); - EXPECT_EQ( - timebase::to_seconds(edit_list.duration_flicks(TimeSourceMode::DYNAMIC, r24)), 1.5f); - EXPECT_EQ( - timebase::to_seconds(edit_list.duration_flicks(TimeSourceMode::REMAPPED, r24)), 1.5f); -} - -TEST(EditListTest2, Test) { - FrameRateDuration r30_30(30l, 30.0); - FrameRateDuration r30_60(30l, 60.0); - FrameRateDuration r30_15(30l, 15.0); - FrameRate r24(timebase::k_flicks_24fps); - - EditList edit_list( - {EditListSection(Uuid(), r30_30, Timecode()), - EditListSection(Uuid(), r30_60, Timecode()), - EditListSection(Uuid(), r30_15, Timecode())}); - - FrameRate fr30(timebase::k_flicks_one_thirtieth_second); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr30, FrameRateDuration(0, 24.0)), 0); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr30, FrameRateDuration(1, 24.0)), 1); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr30, FrameRateDuration(29, 24.0)), - 29); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr30, FrameRateDuration(30, 24.0)), - 30); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr30, FrameRateDuration(31, 24.0)), - 32); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr30, FrameRateDuration(32, 24.0)), - 34); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr30, FrameRateDuration(45, 24.0)), - 60); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr30, FrameRateDuration(46, 24.0)), - 61); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr30, FrameRateDuration(47, 24.0)), - 61); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr30, FrameRateDuration(104, 24.0)), - 89); -} - -TEST(EditListTest3, Test) { - FrameRateDuration r30_30(30l, 30.0); - FrameRateDuration r30_60(30l, 60.0); - FrameRateDuration r30_15(30l, 15.0); - FrameRate r24(timebase::k_flicks_24fps); - - EditList edit_list( - {EditListSection(Uuid(), r30_30, Timecode()), - EditListSection(Uuid(), r30_60, Timecode()), - EditListSection(Uuid(), r30_15, Timecode())}); - - FrameRate fr15(timebase::k_flicks_one_fifteenth_second); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr15, FrameRateDuration(0, 24.0)), 0); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr15, FrameRateDuration(1, 24.0)), 2); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr15, FrameRateDuration(14, 24.0)), - 28); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr15, FrameRateDuration(15, 24.0)), - 30); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr15, FrameRateDuration(16, 24.0)), - 34); - // EXPECT_EQ(edit_list.logical_frame(TimeSourceMode::REMAPPED, fr15, - // FrameRateDuration(16, 24.0)), 34); - EXPECT_EQ(edit_list.duration_frames(TimeSourceMode::REMAPPED, fr15), unsigned(52)); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, fr15, FrameRateDuration(51, 24.0)), - 89); -} - -/*TEST(EditListTest4, Test) { - FrameRateDuration r30_30(static_cast(30), timebase::k_flicks_one_thirtieth_second); - FrameRateDuration r30_60(static_cast(30), timebase::k_flicks_one_sixtieth_second); - FrameRateDuration r30_15(static_cast(30), timebase::k_flicks_one_fifteenth_second); - FrameRate r24(timebase::k_flicks_24fps); - - EditList edit_list( - {EditListSection(Uuid(), r30_30, Timecode()), - EditListSection(Uuid(), r30_60, Timecode()), - EditListSection(Uuid(), r30_15, Timecode())}); - - FrameRate fr15(timebase::k_flicks_one_fifteenth_second); - - EXPECT_EQ( - edit_list.frame(TimeSourceMode::REMAPPED, fr30, r24, 30).frames(), - FrameRateDuration(static_cast(30), r24).frames()); - - // EXPECT_EQ(edit_list.logical_frame(TimeSourceMode::REMAPPED, fr30, - // FrameRateDuration(36, 24.0)), 42); - - // EXPECT_EQ(edit_list.frame(TimeSourceMode::REMAPPED, fr30, r24, 45).frames(), - // FrameRateDuration(static_cast(36), r24).frames()); - // EXPECT_EQ(edit_list.frame(TimeSourceMode::REMAPPED, fr30, r24, 30), - // FrameRateDuration(static_cast(30), r24)); - // EXPECT_EQ(edit_list.frame(TimeSourceMode::REMAPPED, fr30, r24, 31).frames(), - // FrameRateDuration(static_cast(31), r24).frames()); - // EXPECT_EQ(edit_list.frame(TimeSourceMode::REMAPPED, fr30, r24, 32).frames(), - // FrameRateDuration(static_cast(32), r24).frames()); - - // EXPECT_EQ(edit_list.frame(TimeSourceMode::FIXED, fr15, r24, 1), - // FrameRateDuration(static_cast(1), r24)); - // EXPECT_EQ(edit_list.frame(TimeSourceMode::DYNAMIC, fr15, r24, 1), - // FrameRateDuration(static_cast(1), r24)); - // EXPECT_EQ(edit_list.frame(TimeSourceMode::REMAPPED, fr15, r24, 4).seconds(), - // FrameRateDuration(static_cast(4), r24).seconds()); - - - // EXPECT_EQ(edit_list.frame(TimeSourceMode::FIXED, fr15, r24, 34), - // FrameRateDuration(static_cast(34), r24)); - // EXPECT_EQ(edit_list.frame(TimeSourceMode::DYNAMIC, fr15, r24, 34), - // FrameRateDuration(static_cast(34), r24)); EXPECT_EQ( - // edit_list.frame(TimeSourceMode::REMAPPED, fr15, r24, 29).frames(), - // FrameRateDuration(static_cast(15), r24).frames() - // ); - - // EXPECT_EQ(edit_list.frame(TimeSourceMode::REMAPPED, fr30, r24, 1), - // FrameRateDuration(static_cast(1), r24)); - // EXPECT_EQ(edit_list.frame(TimeSourceMode::REMAPPED, fr30, r24, 30), - // FrameRateDuration(static_cast(30), r24)); - // EXPECT_EQ(edit_list.frame(TimeSourceMode::REMAPPED, fr30, r24, 31), - // FrameRateDuration(static_cast(32), r24)); -}*/ - -TEST(EditListTest5, Test) { - - FrameRateDuration r30_24(30l, 24.0); - FrameRateDuration r30_60(30l, 60.0); - FrameRateDuration r30_30(30l, 30.0); - - FrameRate fr24(timebase::k_flicks_24fps); - FrameRate fr30(timebase::k_flicks_one_thirtieth_second); - FrameRate fr60(timebase::k_flicks_one_sixtieth_second); - - EditList edit_list( - {EditListSection(Uuid(), r30_24, Timecode()), - EditListSection(Uuid(), r30_60, Timecode()), - EditListSection(Uuid(), r30_30, Timecode())}); - - FrameRate fr15(timebase::k_flicks_one_fifteenth_second); - - EXPECT_EQ(edit_list.frame_rate_at_frame(15), fr24); - EXPECT_EQ(edit_list.frame_rate_at_frame(45), fr60); - EXPECT_EQ(edit_list.frame_rate_at_frame(75), fr30); - - try { - - static_cast(edit_list.frame_rate_at_frame(150)); - FAIL() << "frame_rate_at_frame() should throw an out of frames error\n"; - - } catch (...) { - } - - EXPECT_EQ(edit_list.flicks_from_frame(TimeSourceMode::FIXED, 0, fr24), timebase::flicks(0)); - EXPECT_EQ( - edit_list.flicks_from_frame(TimeSourceMode::REMAPPED, 0, fr24), timebase::flicks(0)); - EXPECT_EQ( - edit_list.flicks_from_frame(TimeSourceMode::DYNAMIC, 0, fr24), timebase::flicks(0)); - - EXPECT_EQ( - edit_list.flicks_from_frame(TimeSourceMode::FIXED, 10, fr24), - 10 * timebase::k_flicks_24fps); - EXPECT_EQ( - edit_list.flicks_from_frame(TimeSourceMode::REMAPPED, 10, fr24), - 10 * timebase::k_flicks_24fps); - EXPECT_EQ( - edit_list.flicks_from_frame(TimeSourceMode::DYNAMIC, 10, fr24), - 10 * timebase::k_flicks_24fps); - - EXPECT_EQ( - edit_list.flicks_from_frame(TimeSourceMode::FIXED, 40, fr24), - 40.0 * timebase::k_flicks_24fps); - EXPECT_EQ( - edit_list.flicks_from_frame(TimeSourceMode::REMAPPED, 40, fr24), - 40.0 * timebase::k_flicks_24fps); - EXPECT_EQ( - edit_list.flicks_from_frame(TimeSourceMode::DYNAMIC, 40, fr24), - 30.0 * timebase::k_flicks_24fps + 10.0 * timebase::k_flicks_one_sixtieth_second); - - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::FIXED, 10 * timebase::k_flicks_24fps, fr24), - 10); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::REMAPPED, 10 * timebase::k_flicks_24fps, fr24), - 10); - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::DYNAMIC, 10 * timebase::k_flicks_24fps, fr24), - 10); - - EXPECT_EQ( - edit_list.logical_frame(TimeSourceMode::FIXED, 40 * timebase::k_flicks_24fps, fr24), - 40); - EXPECT_EQ( - edit_list.logical_frame( - TimeSourceMode::REMAPPED, - 30 * timebase::k_flicks_24fps + 10 * timebase::k_flicks_24fps, - fr24), - 55); // 10 frames @ 24fps = (60/24)*10 @ 60fps = 25 @ 60fps - EXPECT_EQ( - edit_list.logical_frame( - TimeSourceMode::DYNAMIC, - 30 * timebase::k_flicks_24fps + 10 * timebase::k_flicks_one_sixtieth_second, - fr24), - 40); - - try { - - static_cast(edit_list.logical_frame( - TimeSourceMode::DYNAMIC, - 30 * timebase::k_flicks_24fps + 30 * timebase::k_flicks_one_sixtieth_second + - 30 * timebase::k_flicks_one_thirtieth_second + - 15 * timebase::k_flicks_one_thirtieth_second, - fr24)); - FAIL() << "logical_frame() should throw an out of frames error\n"; - - } catch (...) { - } - - auto frames_max = std::numeric_limits::max(); - auto frames_min = std::numeric_limits::min(); - - timebase::flicks step_period(0); - EXPECT_EQ( - edit_list.step( - TimeSourceMode::FIXED, fr24, true, 1.0, 10, frames_min, frames_max, step_period), - 11); - EXPECT_EQ(step_period, timebase::k_flicks_24fps); - - // step from last frame back to first - step_period = timebase::flicks(0); - EXPECT_EQ( - edit_list.step( - TimeSourceMode::FIXED, fr24, true, 1.0, 90, frames_min, frames_max, step_period), - 0); - EXPECT_EQ(step_period, timebase::k_flicks_24fps); - - // 30fps rate remapping should step two frames in 60fps source at frame 40 - step_period = timebase::flicks(0); - EXPECT_EQ( - edit_list.step( - TimeSourceMode::REMAPPED, fr30, true, 1.0, 40, frames_min, frames_max, step_period), - 42); - EXPECT_EQ(step_period, timebase::k_flicks_one_thirtieth_second); - - // - step_period = timebase::flicks(0); - EXPECT_EQ( - edit_list.step( - TimeSourceMode::DYNAMIC, fr30, true, 1.0, 40, frames_min, frames_max, step_period), - 41); - EXPECT_EQ(step_period, timebase::k_flicks_one_sixtieth_second); - - step_period = timebase::flicks(0); - EXPECT_EQ( - edit_list.step( - TimeSourceMode::DYNAMIC, fr30, true, 1.0, 60, frames_min, frames_max, step_period), - 61); - EXPECT_EQ(step_period, timebase::k_flicks_one_thirtieth_second); -} diff --git a/src/utility/test/json_store_sync_test.cpp b/src/utility/test/json_store_sync_test.cpp new file mode 100644 index 000000000..998b6bef1 --- /dev/null +++ b/src/utility/test/json_store_sync_test.cpp @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "xstudio/utility/json_store_sync.hpp" +#include "xstudio/utility/logging.hpp" + +#include + +using namespace xstudio::utility; +using namespace nlohmann; + +TEST(JsonStoreSyncTest, Test) { + + JsonStoreSync a(R"({"children":[]})"_json); + JsonStoreSync b(R"({"children":[]})"_json); + JsonStoreSync c(R"({"children":[]})"_json); + + a.bind_send_event_func( + [&](auto &&PH1, auto &&PH2) { b.process_event(std::forward(PH1)); }); + + b.bind_send_event_func( + [&](auto &&PH1, auto &&PH2) { a.process_event(std::forward(PH1)); }); + + a.insert_rows(0, 1, R"([{"hello":true}])"_json); + EXPECT_EQ(a.dump(), b.dump()); + + b.insert_rows(1, 1, R"([{"goodbye":true}])"_json); + EXPECT_EQ(a.dump(), b.dump()); + + a.set(0, "hello", R"(false)"_json); + EXPECT_EQ(a.dump(), b.dump()); + + b.set(1, "goodbye", R"(false)"_json); + EXPECT_EQ(a.dump(), b.dump()); + + a.remove_rows(1, 1); + EXPECT_EQ(a.dump(), b.dump()); + + b.remove_rows(0, 1); + EXPECT_EQ(a.dump(), b.dump()); + + + a.insert("test1", R"({"hellogoodbye":true})"_json); + EXPECT_EQ(a.dump(), b.dump()); + + b.insert("test2", R"({"goodbyehello":true})"_json); + EXPECT_EQ(a.dump(), b.dump()); + + a.remove("test1"); + EXPECT_EQ(a.dump(), b.dump()); + + b.remove("test2"); + EXPECT_EQ(a.dump(), b.dump()); + + + EXPECT_EQ(a.dump(), c.dump()); +} + +TEST(JsonStoreSyncTestMove, Test) { + JsonStoreSync a(R"({"children":[]})"_json); + JsonStoreSync b(R"({"children":[]})"_json); + a.bind_send_event_func( + [&](auto &&PH1, auto &&PH2) { b.process_event(std::forward(PH1)); }); + + b.bind_send_event_func( + [&](auto &&PH1, auto &&PH2) { a.process_event(std::forward(PH1)); }); + + a.insert_rows(0, 1, R"([{"hello":true, "children":[1,2,3]}])"_json); + EXPECT_EQ(a.dump(), b.dump()); + + b.insert_rows(1, 1, R"([{"goodbye":true, "children":[4,5,6]}])"_json); + EXPECT_EQ(a.dump(), b.dump()); + + a.move_rows("/children/0", 0, 1, "/children/0", 1); + EXPECT_EQ(a.dump(), b.dump()); + + b.move_rows("/children/0", 0, 1, "/children/1", 1); + EXPECT_EQ(a.dump(), b.dump()); +} + +TEST(JsonStoreSyncTestUndo, Test) { + JsonStoreSync a(R"({"children":[]})"_json); + JsonStoreSync b(R"({"children":[]})"_json); + + // insert data at row + a.insert_rows(0, 1, R"([{"hello":true}])"_json); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + a.unapply_event(a.last_event()); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + // set existing key + a.insert_rows(0, 1, R"([{"hello":true}])"_json); + b.process_event(a.last_event()); + + a.insert("hello", R"([{"hello":true}])"_json, "/children/0"); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + a.unapply_event(a.last_event()); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + // set new key + a.insert("hellome", R"([{"hello":true}])"_json, "/children/0"); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + a.unapply_event(a.last_event()); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + // remove key + a.remove("hello", "/children/0"); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + a.unapply_event(a.last_event()); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + // set row keys + a.set(0, R"({"hello":false})"_json, ""); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + a.unapply_event(a.last_event()); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + // remove rows + a.remove_rows(0, 1, ""); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + a.unapply_event(a.last_event()); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + a.reset_data( + R"({"children":[{"hello":true, "children":[1,2,3]}, {"goodbye":true, "children":[4,5,6]}]})"_json); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + a.unapply_event(a.last_event()); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + + // move rows + a.reset_data(R"({"children":[ + {"hello":true, "children":[1,2,3]}, + {"goodbye":true, "children":[4,5,6]} + ]})"_json); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + a.move_rows("/children/0", 0, 1, "/children/0", 1); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + a.unapply_event(a.last_event()); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + a.move_rows("/children/0", 0, 1, "/children/1", 1); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); + + a.unapply_event(a.last_event()); + b.process_event(a.last_event()); + EXPECT_EQ(a.dump(), b.dump()); +} + +TEST(JsonStoreSyncTestFind, Test) { + JsonStoreSync a(R"( + { + "children":[ + {"a": 1}, + {"b": 2}, + { + "c": 3, + "children":[ + {"a": 1}, + {"b": 2}, + {"c": 3} + ] + } + ] + } + )"_json); + + // start_logger(spdlog::level::debug); + + auto found = a.find("a"); + EXPECT_EQ(found.size(), 2); + EXPECT_EQ(found.at(0), "/children/0"); + EXPECT_EQ(found.at(1), "/children/2/children/0"); + + // test find count limit + found = a.find("a", {}, 1); + EXPECT_EQ(found.size(), 1); + EXPECT_EQ(found.at(0), "/children/0"); + + // test value + found = a.find("a", 1, 1); + EXPECT_EQ(found.size(), 1); + EXPECT_EQ(found.at(0), "/children/0"); + + // test value not match + found = a.find("a", 2, 1); + EXPECT_EQ(found.size(), 0); + + // test key not match + found = a.find("d"); + EXPECT_EQ(found.size(), 0); + + // test depth prune + found = a.find("a", {}, -1, 1); + EXPECT_EQ(found.size(), 1); + + // test depth prune + found = a.find("a", {}, -1, 2); + EXPECT_EQ(found.size(), 2); +} \ No newline at end of file diff --git a/src/utility/test/managed_dir_test.cpp b/src/utility/test/managed_dir_test.cpp index 28a0dda83..8d50c8dc4 100644 --- a/src/utility/test/managed_dir_test.cpp +++ b/src/utility/test/managed_dir_test.cpp @@ -1,4 +1,3 @@ -// SPDX-License-Identifier: Apache-2.0 #include #include diff --git a/src/utility/test/media_reference_test.cpp b/src/utility/test/media_reference_test.cpp index 43401af3d..32454ee75 100644 --- a/src/utility/test/media_reference_test.cpp +++ b/src/utility/test/media_reference_test.cpp @@ -3,6 +3,7 @@ #include "xstudio/utility/helpers.hpp" #include "xstudio/utility/media_reference.hpp" #include "xstudio/utility/caf_helpers.hpp" +#include "xstudio/utility/logging.hpp" #include @@ -44,3 +45,22 @@ TEST(MediaReferenceTest, Test) { EXPECT_EQ(mr5.uri(0, frame), posix_path_to_uri("/tmp/test/test.0002.exr")); EXPECT_EQ(mr5.uri(4, frame), posix_path_to_uri("/tmp/test/test.0010.exr")); } + +TEST(MediaReferenceURITest, Test) { + MediaReference mr1(posix_path_to_uri("/tmp/test/test.{:04d}.exr"), std::string("1-24")); + + EXPECT_EQ(uri_to_posix_path(mr1.uri()), "/tmp/test/test.{:04d}.exr"); + EXPECT_EQ( + uri_to_posix_path(mr1.uri(MediaReference::FramePadFormat::FPF_NUKE)), + "/tmp/test/test.%04d.exr"); + EXPECT_EQ( + uri_to_posix_path(mr1.uri(MediaReference::FramePadFormat::FPF_SHAKE)), + "/tmp/test/test.####.exr"); + + // MediaReference mr2(posix_path_to_uri("/tmp/test/test.{:03d}.exr"), std::string("1-24")); + + // EXPECT_EQ(mr2.uri(), posix_path_to_uri("/tmp/test/test.{:03d}.exr")); + // EXPECT_EQ(uri_to_posix_path(mr2.uri(MediaReference::FramePadFormat::FPF_NUKE)), + // "/tmp/test/test.%03d.exr"); EXPECT_EQ(mr2.uri(MediaReference::FramePadFormat::FPF_SHAKE), + // posix_path_to_uri("/tmp/test/test.###.exr")); +} \ No newline at end of file diff --git a/src/utility/test/remote_session_file_test.cpp b/src/utility/test/remote_session_file_test.cpp index c464a2044..265c6c48a 100644 --- a/src/utility/test/remote_session_file_test.cpp +++ b/src/utility/test/remote_session_file_test.cpp @@ -5,8 +5,8 @@ using namespace xstudio::utility; TEST(SessionFileTest, Test) { - RemoteSessionFile local(".", 12345, false); - RemoteSessionFile remote(".", 12346, false, "remotetest", "cookham", true); + RemoteSessionFile local(".", 12345); + RemoteSessionFile remote(".", 12346, "remotetest", "cookham", true); RemoteSessionFile remote2(remote.filepath()); remote2.set_remove_on_delete(); @@ -21,8 +21,8 @@ TEST(SessionFileTest, Test) { TEST(SessionFileManager, Test) { RemoteSessionManager rsm("."); rsm.create_session_file(1234); - rsm.create_session_file(12346, false, "remotetest", "cookham", true); - rsm.create_session_file(12346, false, "local", "localhost"); + rsm.create_session_file(12346, "remotetest", "cookham", true); + rsm.create_session_file(12346, "local", "localhost"); auto first_local = rsm.first_api(); auto find_remote = rsm.find("remotetest"); diff --git a/src/utility/test/serialize_test.cpp b/src/utility/test/serialize_test.cpp index e06f1968b..5385ec29e 100644 --- a/src/utility/test/serialize_test.cpp +++ b/src/utility/test/serialize_test.cpp @@ -207,26 +207,6 @@ TEST(JsonStoreSerializerTest, Test) { EXPECT_EQ(u1, u2) << "Creation from string should be equal"; } -TEST(EditListSerializerTest, Test) { - fixture f; - - binary_serializer::container_type buf; - binary_serializer bs{f.system, buf}; - EditList u1( - {EditListSection(Uuid(), FrameRateDuration(12l, 24.0), Timecode()), - EditListSection(Uuid(), FrameRateDuration(24l, 24.0), Timecode())}); - EditList u2; - - auto e = bs.apply(u1); - EXPECT_TRUE(e) << "unable to serialize" << to_string(bs.get_error()) << std::endl; - - binary_deserializer bd{f.system, buf}; - e = bd.apply(u2); - EXPECT_TRUE(e) << "unable to deserialize" << to_string(bd.get_error()) << std::endl; - - EXPECT_EQ(u1, u2) << "Creation from string should be equal"; -} - TEST(TimecodeSerializerTest, Test) { fixture f; diff --git a/src/utility/test/time_cache_test.cpp b/src/utility/test/time_cache_test.cpp index d4249f981..98279f2c1 100644 --- a/src/utility/test/time_cache_test.cpp +++ b/src/utility/test/time_cache_test.cpp @@ -7,8 +7,10 @@ #include "xstudio/media/media.hpp" #include "xstudio/utility/time_cache.hpp" #include "xstudio/utility/uuid.hpp" +#include "xstudio/utility/logging.hpp" using namespace xstudio::utility; +using namespace xstudio::media; TEST(TimeCacheTest, Test) { using namespace std::chrono_literals; @@ -179,3 +181,90 @@ TEST(TimeCacheTest2, Test) { // new EXPECT_TRUE(mc.store_check("test", 1000)); } + + +TEST(TimeCacheSpeedTest, Test) { + using namespace std::chrono_literals; + TimeCache> mc; + + spdlog::stopwatch sw; + start_logger(spdlog::level::info); + + auto buffer = std::make_shared("testing"); + auto futuretime = clock::now(); + + sw.reset(); + for (int i = 0; i < 100000; i++) + mc.store(i, buffer, futuretime); + spdlog::info("store {:.3} seconds.", sw); + + sw.reset(); + for (int i = 0; i < 100000; i++) + mc.retrieve(i); + spdlog::info("retrieve {:.3} seconds.", sw); +} + + +TEST(TimeCacheSpeedTestmk, Test) { + using namespace std::chrono_literals; + TimeCache> mc; + + spdlog::stopwatch sw; + start_logger(spdlog::level::info); + + auto buffer = std::make_shared("testing"); + auto futuretime = clock::now(); + + sw.reset(); + for (int i = 0; i < 100000; i++) + mc.store(MediaKey(std::to_string(i)), buffer, futuretime); + spdlog::info("store {:.3} seconds.", sw); + + sw.reset(); + for (int i = 0; i < 100000; i++) + mc.retrieve(MediaKey(std::to_string(i))); + spdlog::info("retrieve {:.3} seconds.", sw); +} + + +TEST(TimeCacheSpeedTest2, Test) { + using namespace std::chrono_literals; + TimeCache> mc; + + mc.set_max_count(5000); + + spdlog::stopwatch sw; + start_logger(spdlog::level::info); + + auto buffer = std::make_shared("testing"); + auto futuretime = clock::now(); + + sw.reset(); + for (int i = 0; i < 10000; i++) + mc.store(i, buffer, futuretime); + spdlog::info("store {:.3} seconds.", sw); + + sw.reset(); + for (int i = 0; i < 10000; i++) + mc.retrieve(i); + spdlog::info("retrieve {:.3} seconds.", sw); +} + +TEST(TimeCacheSpeedTest3, Test) { + using namespace std::chrono_literals; + TimeCache> mc; + + mc.set_max_count(5000); + + spdlog::stopwatch sw; + start_logger(spdlog::level::info); + + auto buffer = std::make_shared("testing"); + auto futuretime = clock::now(); + auto uuid = Uuid(); + + sw.reset(); + for (int i = 0; i < 10000; i++) + mc.store(i, buffer, futuretime, uuid, futuretime); + spdlog::info("store {:.3} seconds.", sw); +} diff --git a/ui/icons/xstudio_app.ico b/ui/icons/xstudio_app.ico new file mode 100644 index 000000000..e3282a168 Binary files /dev/null and b/ui/icons/xstudio_app.ico differ diff --git a/ui/icons/xstudio_file.ico b/ui/icons/xstudio_file.ico new file mode 100644 index 000000000..aed1466b7 Binary files /dev/null and b/ui/icons/xstudio_file.ico differ diff --git a/ui/qml/reskin/assets/icons/new/delete_w500.svg b/ui/qml/reskin/assets/icons/new/delete_w500.svg deleted file mode 100644 index 078b4395c..000000000 --- a/ui/qml/reskin/assets/icons/new/delete_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/search_w500.svg b/ui/qml/reskin/assets/icons/new/search_w500.svg deleted file mode 100644 index d72b70a07..000000000 --- a/ui/qml/reskin/assets/icons/new/search_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/splitscreen.svg b/ui/qml/reskin/assets/icons/new/splitscreen.svg deleted file mode 100644 index 56de447fb..000000000 --- a/ui/qml/reskin/assets/icons/new/splitscreen.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/splitscreen2.svg b/ui/qml/reskin/assets/icons/new/splitscreen2.svg deleted file mode 100644 index 8396226dc..000000000 --- a/ui/qml/reskin/assets/icons/new/splitscreen2.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/view_grid_w500.svg b/ui/qml/reskin/assets/icons/new/view_grid_w500.svg deleted file mode 100644 index b65f2c0ff..000000000 --- a/ui/qml/reskin/assets/icons/new/view_grid_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/view_w500.svg b/ui/qml/reskin/assets/icons/new/view_w500.svg deleted file mode 100644 index 61f465e4d..000000000 --- a/ui/qml/reskin/assets/icons/new/view_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/brush_w500.svg b/ui/qml/reskin/assets/icons/retired/brush_w500.svg deleted file mode 100644 index 5c99b7806..000000000 --- a/ui/qml/reskin/assets/icons/retired/brush_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/delete_w500.svg b/ui/qml/reskin/assets/icons/retired/delete_w500.svg deleted file mode 100644 index 078b4395c..000000000 --- a/ui/qml/reskin/assets/icons/retired/delete_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/fast_forward_w500.svg b/ui/qml/reskin/assets/icons/retired/fast_forward_w500.svg deleted file mode 100644 index a9360d96d..000000000 --- a/ui/qml/reskin/assets/icons/retired/fast_forward_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/fast_rewind_w500.svg b/ui/qml/reskin/assets/icons/retired/fast_rewind_w500.svg deleted file mode 100644 index f4353fe23..000000000 --- a/ui/qml/reskin/assets/icons/retired/fast_rewind_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/open_in_new_w500.svg b/ui/qml/reskin/assets/icons/retired/open_in_new_w500.svg deleted file mode 100644 index b5a7763d8..000000000 --- a/ui/qml/reskin/assets/icons/retired/open_in_new_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/open_with_w500.svg b/ui/qml/reskin/assets/icons/retired/open_with_w500.svg deleted file mode 100644 index 0b15ddf1b..000000000 --- a/ui/qml/reskin/assets/icons/retired/open_with_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/pan_w500.svg b/ui/qml/reskin/assets/icons/retired/pan_w500.svg deleted file mode 100644 index 63d4e92a0..000000000 --- a/ui/qml/reskin/assets/icons/retired/pan_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/pause_w500.svg b/ui/qml/reskin/assets/icons/retired/pause_w500.svg deleted file mode 100644 index 0e8b16d61..000000000 --- a/ui/qml/reskin/assets/icons/retired/pause_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/photo_camera_w500.svg b/ui/qml/reskin/assets/icons/retired/photo_camera_w500.svg deleted file mode 100644 index aeadccb5b..000000000 --- a/ui/qml/reskin/assets/icons/retired/photo_camera_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/play_arrow_w500.svg b/ui/qml/reskin/assets/icons/retired/play_arrow_w500.svg deleted file mode 100644 index 7be81f799..000000000 --- a/ui/qml/reskin/assets/icons/retired/play_arrow_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/repeat_w500.svg b/ui/qml/reskin/assets/icons/retired/repeat_w500.svg deleted file mode 100644 index cb61f567a..000000000 --- a/ui/qml/reskin/assets/icons/retired/repeat_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/restart_w500.svg b/ui/qml/reskin/assets/icons/retired/restart_w500.svg deleted file mode 100644 index 873173467..000000000 --- a/ui/qml/reskin/assets/icons/retired/restart_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/search_w500.svg b/ui/qml/reskin/assets/icons/retired/search_w500.svg deleted file mode 100644 index d72b70a07..000000000 --- a/ui/qml/reskin/assets/icons/retired/search_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/skip_next_w500.svg b/ui/qml/reskin/assets/icons/retired/skip_next_w500.svg deleted file mode 100644 index b832f2fb1..000000000 --- a/ui/qml/reskin/assets/icons/retired/skip_next_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/skip_previous_w500.svg b/ui/qml/reskin/assets/icons/retired/skip_previous_w500.svg deleted file mode 100644 index d8091a382..000000000 --- a/ui/qml/reskin/assets/icons/retired/skip_previous_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/sticky_note_w500.svg b/ui/qml/reskin/assets/icons/retired/sticky_note_w500.svg deleted file mode 100644 index 9ba94f04f..000000000 --- a/ui/qml/reskin/assets/icons/retired/sticky_note_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/sync_w500.svg b/ui/qml/reskin/assets/icons/retired/sync_w500.svg deleted file mode 100644 index ebb8d982a..000000000 --- a/ui/qml/reskin/assets/icons/retired/sync_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/trending_w500.svg b/ui/qml/reskin/assets/icons/retired/trending_w500.svg deleted file mode 100644 index c39338063..000000000 --- a/ui/qml/reskin/assets/icons/retired/trending_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/tune_w500.svg b/ui/qml/reskin/assets/icons/retired/tune_w500.svg deleted file mode 100644 index 94a3e5587..000000000 --- a/ui/qml/reskin/assets/icons/retired/tune_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/view_grid_w500.svg b/ui/qml/reskin/assets/icons/retired/view_grid_w500.svg deleted file mode 100644 index b65f2c0ff..000000000 --- a/ui/qml/reskin/assets/icons/retired/view_grid_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/view_w500.svg b/ui/qml/reskin/assets/icons/retired/view_w500.svg deleted file mode 100644 index 61f465e4d..000000000 --- a/ui/qml/reskin/assets/icons/retired/view_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/volume_down_w500.svg b/ui/qml/reskin/assets/icons/retired/volume_down_w500.svg deleted file mode 100644 index c81a7f968..000000000 --- a/ui/qml/reskin/assets/icons/retired/volume_down_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/volume_mute_w500.svg b/ui/qml/reskin/assets/icons/retired/volume_mute_w500.svg deleted file mode 100644 index 537de53f1..000000000 --- a/ui/qml/reskin/assets/icons/retired/volume_mute_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/volume_no_sound_w500.svg b/ui/qml/reskin/assets/icons/retired/volume_no_sound_w500.svg deleted file mode 100644 index 5f3970437..000000000 --- a/ui/qml/reskin/assets/icons/retired/volume_no_sound_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/volume_up_w500.svg b/ui/qml/reskin/assets/icons/retired/volume_up_w500.svg deleted file mode 100644 index 2e66df655..000000000 --- a/ui/qml/reskin/assets/icons/retired/volume_up_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/retired/zoom_in_w500.svg b/ui/qml/reskin/assets/icons/retired/zoom_in_w500.svg deleted file mode 100644 index e5a03f680..000000000 --- a/ui/qml/reskin/assets/icons/retired/zoom_in_w500.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/search.svg b/ui/qml/reskin/assets/icons/search.svg deleted file mode 100755 index 8710306dd..000000000 --- a/ui/qml/reskin/assets/icons/search.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/qml/reskin/fonts/Inter/OFL.txt b/ui/qml/reskin/fonts/Inter/OFL.txt deleted file mode 100644 index ad214842c..000000000 --- a/ui/qml/reskin/fonts/Inter/OFL.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/ui/qml/reskin/layout_framework/XsLayoutModeBar.qml b/ui/qml/reskin/layout_framework/XsLayoutModeBar.qml deleted file mode 100644 index bb34ed520..000000000 --- a/ui/qml/reskin/layout_framework/XsLayoutModeBar.qml +++ /dev/null @@ -1,210 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 1.4 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 - -Rectangle { id: modeBar - - height: XsStyleSheet.menuHeight - // color: XsStyleSheet.menuBarColor - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.lighter( XsStyleSheet.menuBarColor, 1.15) } - GradientStop { position: 1.0; color: Qt.darker( XsStyleSheet.menuBarColor, 1.15) } - } - - property string barId: "" - property real panelPadding: XsStyleSheet.panelPadding - property real buttonHeight: XsStyleSheet.widgetStdHeight-4 - - // Rectangle{anchors.fill: parent; color: "red"; opacity:.3} - - property var selected_layout_index - - XsSecondaryButton{ id: menuBtn - width: XsStyleSheet.menuIndicatorSize - height: XsStyleSheet.menuIndicatorSize - anchors.right: parent.right - anchors.rightMargin: panelPadding - anchors.verticalCenter: parent.verticalCenter - imgSrc: "qrc:/icons/menu.svg" - isActive: barMenu.visible - onClicked: { - barMenu.x = menuBtn.x-barMenu.width - barMenu.y = menuBtn.y //+ menuBtn.height - barMenu.visible = !barMenu.visible - } - } - XsMenuNew { - id: barMenu - visible: false - menu_model: barMenuModel - menu_model_index: barMenuModel.index(-1, -1) - menuWidth: 160 - } - XsMenusModel { - id: barMenuModel - modelDataName: "ModeBarMenu"+barId - onJsonChanged: { - barMenu.menu_model_index = index(-1, -1) - } - } - - - XsMenuModelItem { - text: "Remove Current Layout" - menuPath: "" - menuItemPosition: 3 - menuModelName: "ModeBarMenu"+barId - onActivated: { - } - } - XsMenuModelItem { - menuItemType: "divider" - menuPath: "" - menuItemPosition: 2 - menuModelName: "ModeBarMenu"+barId - } - XsMenuModelItem { - text: "Reset Default Layouts" - menuPath: "" - menuItemPosition: 3 - menuModelName: "ModeBarMenu"+barId - onActivated: { - } - } - XsMenuModelItem { - text: "Reset Current Layout" - menuPath: "" - menuItemPosition: 3 - menuModelName: "ModeBarMenu"+barId - onActivated: { - } - } - XsMenuModelItem { - menuItemType: "divider" - menuPath: "" - menuItemPosition: 2 - menuModelName: "ModeBarMenu"+barId - } - XsMenuModelItem { - text: "Save As New..." - menuPath: "" - menuItemPosition: 1 - menuModelName: "ModeBarMenu"+barId - onActivated: { - } - } - XsMenuModelItem { - text: "Duplicate..." - menuPath: "" - menuItemPosition: 1 - menuModelName: "ModeBarMenu"+barId - onActivated: { - layouts_model.duplicate_layout(current_layout_index) - } - } - XsMenuModelItem { - text: "Rename..." - menuPath: "" - menuItemPosition: 1 - menuModelName: "ModeBarMenu"+barId - property var idx: 1 - onActivated: { - // TODO: pop-up string query dialog to get new name - layouts_model.set(current_layout_index, "Renamed "+ idx, "layout_name") - idx = idx+1 - } - } - XsMenuModelItem { - text: "New Layout..." - menuPath: "" - menuItemPosition: 1 - menuModelName: "ModeBarMenu"+barId - onActivated: { - var rc = layouts_model.rowCount(layouts_model_root_index) - layouts_model.insertRowsSync(rc, 1, layouts_model_root_index) - layouts_model.set(layouts_model.index(rc, 0, layouts_model_root_index), - '{ - "children": [ - { - "child_dividers": [], - "children": [ - { - "children": [ - { - "tab_view": "Playlists" - } - ], - "current_tab": 0 - } - ], - "split_horizontal": false - } - ], - "enabled": true, - "layout_name": "New Layout" - }', "jsonRole") - } - } - - - property var layouts_model - property var layouts_model_root_index - property var current_layout_index: layouts_model.index(selected_layout, 0, layouts_model_root_index) - property int selected_layout: 0 - - DelegateModel { - - id: the_layouts - // this DelegateModel is set-up to iterate over the contents of the Media - // node (i.e. the MediaSource objects) - model: layouts_model - rootIndex: layouts_model_root_index - delegate: - XsNavButton { - property real btnWidth: 20+textWidth - - text: layout_name - width: btnView.width>(btnWidth*btnView.model.count)? btnWidth : btnView.width/btnView.model.count - height: buttonHeight - - isActive: index==selected_layout - onClicked:{ - selected_layout = index - } - - Rectangle{ id: btnDivider - visible: index != 0 - width:btnView.spacing - height: btnView.height/1.2 - color: palette.base - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.right - } - } - } - - ListView { - id: btnView - x: panelPadding - orientation: ListView.Horizontal - spacing: 1 - width: parent.width - menuBtn.width - panelPadding*3 - height: buttonHeight - contentHeight: contentItem.childrenRect.height - contentWidth: contentItem.childrenRect.width - snapMode: ListView.SnapToItem - interactive: false - layoutDirection: Qt.RightToLeft - anchors.verticalCenter: parent.verticalCenter - currentIndex: selected_layout - - model: the_layouts - - } - -} \ No newline at end of file diff --git a/ui/qml/reskin/layout_framework/XsPanelDivider.qml b/ui/qml/reskin/layout_framework/XsPanelDivider.qml deleted file mode 100644 index 1a3f76e3f..000000000 --- a/ui/qml/reskin/layout_framework/XsPanelDivider.qml +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 - -import xStudioReskin 1.0 - -Item { - - id: divider - property bool isVertical: true - property real thresholdSize: 10 - property real dividerSize: isHovered || dragging? 1.5*2.5 : 1.5 - property real minLimit: 0 - property real maxLimit: isVertical? parent.width : parent.height - property int id: -1 - - property bool dragging: mArea.drag.active - property bool isHovered: mArea.containsMouse - - - width: isVertical? dividerSize*2 : parent.width - height: isVertical? parent.height : dividerSize*2 - - Rectangle{ id: visualThumb - - width: isVertical? dividerSize : parent.width - height: isVertical? parent.height : dividerSize - color: isHovered || dragging? mArea.pressed? palette.highlight : XsStyleSheet.secondaryTextColor : "#AA000000" - - Component.onCompleted: { - if(isVertical) anchors.left = parent.left - else anchors.top = parent.top - } - - } - - property var fractional_position: child_dividers[index] - - property var computed_position: (isVertical ? parent.width : parent.height)*fractional_position - - onComputed_positionChanged: { - if (!dragging) { - if (isVertical) x = computed_position - else y = computed_position - } - } - - onYChanged: { - if (dragging && !isVertical) { - var v = child_dividers; - v[index] = y/parent.height - child_dividers= v; - } - } - - onXChanged: { - if (dragging && isVertical) { - var v = child_dividers; - v[index] = x/parent.width - child_dividers= v; - } - } - - MouseArea{ id: mArea - - width: divider.width - height: divider.height - anchors.centerIn: divider - preventStealing: true - - hoverEnabled: true - cursorShape: isVertical? Qt.SizeHorCursor : Qt.SizeVerCursor - - drag.target: divider - drag.axis: isVertical? Drag.XAxis : Drag.YAxis - - } - -} \ No newline at end of file diff --git a/ui/qml/reskin/layout_framework/XsPanelSplitter.qml b/ui/qml/reskin/layout_framework/XsPanelSplitter.qml deleted file mode 100644 index 4366ecd54..000000000 --- a/ui/qml/reskin/layout_framework/XsPanelSplitter.qml +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 1.4 -import Qt.labs.qmlmodels 1.0 -import QtQml.Models 2.14 - -import xStudioReskin 1.0 - -/* This widget is a custom layout that divides itself between one or more -children. The children can be XsPanelSplitters too, and as such we can -recursively subdivide a window into many resizable panels. It's all based -on the 'panels_layout_model' that is a tree like structure that drives the -recursion. The 'panels_layout_model' is itself backed by json data that comes -from the xstudio preferences files. Look for 'reskin_windows_and_panels_model' -in the preference files for more. In practice we problably don't need anything -this flexible but the capability is there in case we do need it one day. */ -Rectangle { - - id: topItem - color: "transparent" - - anchors.fill: parent - property var panels_layout_model - property var panels_layout_model_index - property bool isVertical: true - property var rowCount: panels_layout_model.rowCount(panels_layout_model_index) - - property var dividers: child_dividers !== undefined ? child_dividers : [] - - Repeater { - - // 'child_dividers' is data exposed by the model and should be - // a vector of floats- eachvalue saying where the split is in normalised - // width/height of the pane. For a pane split into three equally sized panels, - // say, you would have child_dividers = [0.333, 0.666] - model: dividers.length - - XsDivider { - - isVertical: topItem.isVertical - z:1000 // make sure the dividers are on top - - } - } - - Repeater { - - id: the_view - - model: DelegateModel{ - - model: panels_layout_model - rootIndex: panels_layout_model_index - delegate: - - Item { - - id: root - - // using the dividers positions to work out the size and position - // of this child panel - property var d: index == 0 ? 0.0 : topItem.dividers[index-1] - property var e: index >= topItem.dividers.length ? 1.0 : topItem.dividers[index] - property var frac_size: e-d - - width: topItem.isVertical ? topItem.width*frac_size : topItem.width - height: topItem.isVertical ? topItem.height : topItem.height*frac_size - x: topItem.isVertical ? topItem.width*d : 0 - y: topItem.isVertical ? 0 : topItem.height*d - - property var child - property var child_type - - function buildSubPanels() { - - // if 'split_horizontal' is defined (either true or fale), - // then we have hit another splitter - if (split_horizontal !== undefined) { - - if (child && child_type == "XsSplitPanel") { - child.buildSubPanels() - return - } - if (child) child.destroy() - - let component = Qt.createComponent("./XsPanelSplitter.qml") - let recurse_into_model_idx = topItem.panels_layout_model.index(index,0,panels_layout_model_index) - child_type = "XsSplitPanel" - - if (component.status == Component.Ready) { - - child = component.createObject( - root, - { - panels_layout_model: topItem.panels_layout_model, - panels_layout_model_index: recurse_into_model_idx, - isVertical: split_horizontal - }) - - } else { - console.log("component", component, component.errorString()) - } - - } else { - - // 'split_horizontal' is not defined, so we are at a - // 'leaf' node, as it were, and we need to create - // the container that holds an actual UI panel - if (child && child_type == "XsPanelContainer") { - return - } - if (child) child.destroy() - - child_type = "XsPanelContainer" - let component = Qt.createComponent("./XsViewContainer.qml") - let recurse_into_model_idx = topItem.panels_layout_model.index(index,0,panels_layout_model_index) - if (component.status == Component.Ready) { - - child = component.createObject( - root, - { - panels_layout_model: topItem.panels_layout_model, - panels_layout_model_index: recurse_into_model_idx, - panels_layout_model_row: index - }) - - } else { - console.log("component", component, component.errorString()) - } - } - } - - Component.onCompleted: { - buildSubPanels() - } - - } - } - } -} \ No newline at end of file diff --git a/ui/qml/reskin/layout_framework/XsViewContainer.qml b/ui/qml/reskin/layout_framework/XsViewContainer.qml deleted file mode 100644 index 47bf7786e..000000000 --- a/ui/qml/reskin/layout_framework/XsViewContainer.qml +++ /dev/null @@ -1,337 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 1.4 -import Qt.labs.qmlmodels 1.0 -import QtQml.Models 2.14 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 -import xstudio.qml.helpers 1.0 - -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import Qt.labs.qmlmodels 1.0 -import QtQml.Models 2.14 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 - -TabView { - - id: container - anchors.fill: parent - property string panelId: "" + container - - // This object instance has been build via a model. - // These properties point us into that node of the model that created us. - property var panels_layout_model - property var panels_layout_model_index - property var panels_layout_model_row - - property int modified_tab_index: -1 - - // update the currentIndex from the model - currentIndex: current_tab === undefined ? 0 : current_tab - - // when user changes tab, store in the model - onCurrentIndexChanged: { - if (currentIndex != current_tab) { - current_tab = currentIndex - } - } - - // Here we make the tabs by iterating over the panels_layout_model at - // the current 'panels_layout_model_index'. The current index is where - // we which is the level at which the panel isn't split, in other words - // where we need to insert a 'view'. However, we can have multiple 'views' - // within a panel thanks to the tabbing feature. The info on the tabs - // is within the child nodes of this node in the panels_layout_model. - Repeater { - model: DelegateModel { - id: tabs - model: panels_layout_model - rootIndex: panels_layout_model_index - delegate: XsTab { - id: defaultTab - title: tab_view ? tab_view : "" - } - } - } - - - XsModelProperty { - id: current_view - role: "tab_view" - index: panels_layout_model_index - } - - property real buttonSize: XsStyleSheet.menuIndicatorSize - property real panelPadding: XsStyleSheet.panelPadding - property real menuWidth: 170//panelMenu.menuWidth - property real tabWidth: 95 - - /*********************************************************************** - * - * Create the menu that appears when you click on the + tab button or on - * the chevron dropdown button on a particular tab - * - */ - - // This instance of the 'XsViewsModel' type gives us access to the global - // model that contains details of all 'views' that are available - XsViewsModel { - id: views_model - } - - // Declare a unique menu model for this instance of the XsViewContainer - XsMenusModel { - id: tabTypeModel - modelDataName: "TabMenu"+panelId - onJsonChanged: { - tabTypeMenu.menu_model_index = index(-1, -1) - } - } - - // Build a menu from the model immediately above - XsMenuNew { - id: tabTypeMenu - visible: false - menuWidth: 80 - menu_model: tabTypeModel - menu_model_index: tabTypeModel.index(-1, -1) - } - - // Add menu items for each view registered with the global model - // of views - Repeater { - model: views_model - Item { - XsMenuModelItem { - text: view_name - menuPath: "" //"Set Panel To|" - menuModelName: "TabMenu"+panelId - menuItemPosition: index - onActivated: { - if (modified_tab_index >= 0) - switch_tab_to(view_name) - else if (modified_tab_index == -10) - add_tab(view_name) - } - } - } - } - - // Add a divider - XsMenuModelItem { - text: "" - menuPath: "" - menuItemPosition: 99 - menuItemType: "divider" - menuModelName: "TabMenu"+panelId - } - - // Add a 'Close Tab' menu item - XsMenuModelItem { - text: "Close Tab" - menuPath: "" - menuItemPosition: 100 - menuItemType: "button" - menuModelName: "TabMenu"+panelId - onActivated: { - remove_tab(modified_tab_index) //#TODO: WIP - } - } - - /************************************************************************/ - - style: TabViewStyle{ - - tabsMovable: true - - tabBar: Rectangle{ - - color: XsStyleSheet.panelBgColor - - // For adding a new tab - XsSecondaryButton{ - - id: addBtn - // visible: false - width: buttonSize - height: buttonSize - z: 1 - x: tabWidth*count + panelPadding/2 - anchors.verticalCenter: menuBtn.verticalCenter - imgSrc: "qrc:/icons/add.svg" - - onClicked: { - modified_tab_index = -10 - tabTypeMenu.x = x - tabTypeMenu.y = y+height - tabTypeMenu.visible = !tabTypeMenu.visible - } - - } - - XsSecondaryButton{ id: menuBtn - width: buttonSize - height: buttonSize - anchors.right: parent.right - anchors.rightMargin: panelPadding - anchors.verticalCenter: parent.verticalCenter - imgSrc: "qrc:/icons/menu.svg" - isActive: panelMenu.visible - onClicked: { - panelMenu.x = menuBtn.x-panelMenu.width - panelMenu.y = menuBtn.y //+ menuBtn.height - panelMenu.visible = !panelMenu.visible - } - } - - } - - tab: Rectangle{ id: tabDiv - color: styleData.selected? "#5C5C5C":"#474747" //#TODO: to check with UX - implicitWidth: tabWidth //metrics.width + typeSelectorBtn.width + panelPadding*2 //Math.max(metrics.width + 2, 80) - implicitHeight: XsStyleSheet.widgetStdHeight - - - Item{ - anchors.centerIn: parent - width: textDiv.width + typeSelectorBtn.width - - Text{ id: textDiv - text: styleData.title - anchors.verticalCenter: parent.verticalCenter - color: palette.text - font.bold: styleData.selected - elide: Text.ElideRight - - TextMetrics { - id: metrics - text: textDiv.text - font: textDiv.font - } - - } - XsSecondaryButton{ - - id: typeSelectorBtn - width: buttonSize - height: width - anchors.left: textDiv.right - anchors.leftMargin: 1 - anchors.verticalCenter: parent.verticalCenter - imgSrc: "qrc:/icons/chevron_right.svg" - rotation: 90 - smooth: true - antialiasing: true - isActive: showingMenu && tabTypeMenu.visible - property bool showingMenu: false - - onClicked: { - // store which tab has been clicked on - modified_tab_index = index - tabTypeMenu.x = parent.x+typeSelectorBtn.x - tabTypeMenu.y = parent.y+typeSelectorBtn.y+typeSelectorBtn.height - tabTypeMenu.visible = !tabTypeMenu.visible - showingMenu = tabTypeMenu.visible - } - } - } - - } - - frame: Rectangle{ - visible: false - } - - } - - XsMenuModelItem { - text: "Close Panel" - menuPath: "" - menuItemPosition: 5 - menuModelName: "PanelMenu"+panelId - onActivated: { - close_panel() - } - } - - XsMenuNew { - id: panelMenu - visible: false - menu_model: panelMenuModel - menu_model_index: panelMenuModel.index(-1, -1) - } - - XsMenusModel { - id: panelMenuModel - modelDataName: "PanelMenu"+panelId - onJsonChanged: { - panelMenu.menu_model_index = index(-1, -1) - } - } - - XsMenuModelItem { - menuItemType: "divider" - menuPath: "" - menuItemPosition: 3 - menuModelName: "PanelMenu"+panelId - } - XsMenuModelItem { - text: "Split Panel Vertical" - menuPath: "" - menuItemPosition: 1 - menuModelName: "PanelMenu"+panelId - onActivated: { - split_panel(true) - } - } - XsMenuModelItem { - text: "Split Panel Horizontal" - menuPath: "" - menuItemPosition: 2 - menuModelName: "PanelMenu"+panelId - onActivated: { - split_panel(false) - } - } - - function undock_panel() { - } - - function split_panel(horizontal) { - panels_layout_model.split_panel(panels_layout_model_index, horizontal) - } - - function switch_tab_to(new_tab_view) { - panels_layout_model.set(panels_layout_model.index(modified_tab_index, 0, panels_layout_model_index), new_tab_view, "tab_view") - } - - function add_tab(new_tab_view) { - var r = panels_layout_model.rowCount(panels_layout_model_index) - panels_layout_model.insertRowsSync(r, 1, panels_layout_model_index) - panels_layout_model.set( - panels_layout_model.index( - r, - 0, - panels_layout_model_index - ), - new_tab_view, - "tab_view") - } - - function remove_tab(tab_index) { - panels_layout_model.removeRows(tab_index, 1, panels_layout_model_index) - } - - function close_panel() { - // our parent must be a 'splitter' - panels_layout_model.close_panel(panels_layout_model_index) - } - - -} \ No newline at end of file diff --git a/ui/qml/reskin/main_reskin.qml b/ui/qml/reskin/main_reskin.qml deleted file mode 100644 index 8e0ebce0c..000000000 --- a/ui/qml/reskin/main_reskin.qml +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -import xStudioReskin 1.0 - -XsSessionWindow { - - window_name: "main_window" - -} - diff --git a/ui/qml/reskin/qml_reskin.qrc b/ui/qml/reskin/qml_reskin.qrc deleted file mode 100644 index 22293994b..000000000 --- a/ui/qml/reskin/qml_reskin.qrc +++ /dev/null @@ -1,228 +0,0 @@ - - - - - - - main_reskin.qml - xStudioReskin/qmldir - xStudioReskin/XsStyleSheet.qml - - session_data/XsMediaListModelData.qml - session_data/XsSessionData.qml - - windows/XsSessionWindow.qml - - layout_framework/XsLayoutModeBar.qml - layout_framework/XsPanelDivider.qml - layout_framework/XsPanelSplitter.qml - layout_framework/XsTestWindow.qml - layout_framework/XsViewContainer.qml - layout_framework/XsPanelsMenuButton.qml - - views/media/XsMedialist.qml - views/media/XsMediaHeader.qml - views/media/XsMediaItems.qml - views/media/delegates/XsMediaItemDelegate.qml - views/media/delegates/XsMediaHeaderColumn.qml - views/media/delegates/XsMediaSourceSelector.qml - views/media/data_indicators/XsMediaFlagIndicator.qml - views/media/data_indicators/XsMediaNotesIndicator.qml - views/media/data_indicators/XsMediaTextItem.qml - views/media/data_indicators/XsMediaThumbnailImage.qml - - views/playlists/XsPlaylists.qml - views/playlists/XsPlaylistItems.qml - views/playlists/delegates/XsPlaylistItemDelegate.qml - views/playlists/delegates/XsPlaylistDividerDelegate.qml - views/playlists/delegates/XsSubsetItemDelegate.qml - views/playlists/delegates/XsTimelineItemDelegate.qml - - views/timeline/XsTimelinePanel.qml - views/timeline/XsTimeline.qml - views/timeline/XsTimelineEditTools.qml - views/timeline/XsTimelineMenu.qml - views/timeline/data/XsSortFilterModel.qml - views/timeline/delegates/XsTimelineEditToolItems.qml - views/timeline/delegates/XsDelegateAudioTrack.qml - views/timeline/delegates/XsDelegateClip.qml - views/timeline/delegates/XsDelegateGap.qml - views/timeline/delegates/XsDelegateStack.qml - views/timeline/delegates/XsDelegateVideoTrack.qml - views/timeline/widgets/XsClipItem.qml - views/timeline/widgets/XsDragBoth.qml - views/timeline/widgets/XsDragLeft.qml - views/timeline/widgets/XsDragRight.qml - views/timeline/widgets/XsGapItem.qml - views/timeline/widgets/XsMoveClip.qml - views/timeline/widgets/XsTrackHeader.qml - views/timeline/widgets/XsTimelineCursor.qml - views/timeline/widgets/XsTickWidget.qml - views/timeline/widgets/XsElideLabel.qml - - views/viewport/XsViewport.qml - views/viewport/XsViewportActionBar.qml - views/viewport/XsViewportTransportBar.qml - views/viewport/XsViewportToolBar.qml - views/viewport/XsViewportInfoBar.qml - views/viewport/widgets/XsViewerMenuButton.qml - views/viewport/widgets/XsViewerSeekEditButton.qml - views/viewport/widgets/XsViewerTextDisplay.qml - views/viewport/widgets/XsViewerToggleButton.qml - views/viewport/widgets/XsViewerVolumeButton.qml - - - - - - widgets/bars_and_tabs/XsSearchBar.qml - widgets/bars_and_tabs/XsTab.qml - widgets/bars_and_tabs/XsTabView.qml - - widgets/buttons/XsNavButton.qml - widgets/buttons/XsPrimaryButton.qml - widgets/buttons/XsSearchButton.qml - widgets/buttons/XsSecondaryButton.qml - - widgets/controls/XsSlider.qml - widgets/controls/XsScrollBar.qml - - widgets/dialogs/XsPopup.qml - widgets/dialogs/XsOpenSessionDialog.qml - - widgets/labels/XsText.qml - widgets/labels/XsTextField.qml - widgets/labels/XsToolTip.qml - - widgets/menus/XsMainMenuBar.qml - widgets/menus/XsMenu.qml - widgets/menus/XsMenuDivider.qml - widgets/menus/XsMenuItem.qml - widgets/menus/XsMenuMultiChoice.qml - widgets/menus/XsMenuItemToggle.qml - widgets/menus/XsMenuItemToggleWithSettings.qml - - widgets/outputs/XsGridView.qml - widgets/outputs/XsImage.qml - widgets/outputs/XsListView.qml - - - - - - fonts/Inter/OFL.txt - fonts/Inter/Inter-Black.ttf - fonts/Inter/Inter-Bold.ttf - fonts/Inter/Inter-ExtraBold.ttf - fonts/Inter/Inter-ExtraLight.ttf - fonts/Inter/Inter-Light.ttf - fonts/Inter/Inter-Medium.ttf - fonts/Inter/Inter-Regular.ttf - fonts/Inter/Inter-SemiBold.ttf - fonts/Inter/Inter-Thin.ttf - - - - - assets/images/sample.png - - - - - assets/icons/sort_flipped.png - - assets/icons/new/sort.svg - assets/icons/new/triangle.svg - assets/icons/new/add.svg - assets/icons/new/chevron_right.svg - assets/icons/new/close.svg - assets/icons/new/fast_forward.svg - assets/icons/new/play_arrow.svg - assets/icons/new/radio_button_checked.svg - assets/icons/new/radio_button_unchecked.svg - assets/icons/new/check_box_checked.svg - assets/icons/new/check_box_unchecked.svg - assets/icons/new/search.svg - assets/icons/new/skip_next.svg - assets/icons/new/skip_previous.svg - assets/icons/new/menu.svg - assets/icons/new/more_vert.svg - assets/icons/new/error.svg - assets/icons/new/filter_none.svg - assets/icons/new/draft.svg - assets/icons/new/more_horiz.svg - assets/icons/new/arrow_drop_down.svg - assets/icons/new/settings.svg - assets/icons/new/disabled.svg - assets/icons/new/splitscreen.svg - assets/icons/new/splitscreen2.svg - - assets/icons/new/list_default.svg - assets/icons/new/list_shotgun.svg - assets/icons/new/list_subset.svg - - assets/icons/new/search.svg - assets/icons/new/search_off.svg - assets/icons/new/delete.svg - assets/icons/new/view.svg - assets/icons/new/view_grid.svg - - assets/icons/new/fast_rewind.svg - assets/icons/new/fast_forward.svg - assets/icons/new/open_in_new.svg - assets/icons/new/photo_camera.svg - assets/icons/new/play_arrow.svg - assets/icons/new/pause.svg - assets/icons/new/repeat.svg - assets/icons/new/skip_previous.svg - assets/icons/new/skip_next.svg - assets/icons/new/sync.svg - assets/icons/new/trending.svg - assets/icons/new/volume_no_sound.svg - assets/icons/new/volume_down.svg - assets/icons/new/volume_mute.svg - assets/icons/new/volume_up.svg - - assets/icons/new/brush.svg - assets/icons/new/open_with.svg - assets/icons/new/sticky_note.svg - assets/icons/new/tune.svg - assets/icons/new/pan.svg - assets/icons/new/zoom_in.svg - assets/icons/new/restart.svg - assets/icons/new/reset_image.svg - assets/icons/new/movie.svg - assets/icons/new/theaters.svg - - assets/icons/new/list.svg - assets/icons/new/list_view.svg - - assets/icons/new/reset_tv.svg - assets/icons/new/undo.svg - assets/icons/new/redo.svg - assets/icons/new/filter.svg - assets/icons/new/ad_group.svg - assets/icons/new/arrow_selector_tool.svg - assets/icons/new/content_cut.svg - assets/icons/new/content_paste.svg - assets/icons/new/expand.svg - assets/icons/new/expand_all.svg - assets/icons/new/format_size.svg - assets/icons/new/ink_pen.svg - assets/icons/new/input.svg - assets/icons/new/laps.svg - assets/icons/new/library_music.svg - assets/icons/new/output.svg - assets/icons/new/rectangle.svg - assets/icons/new/upload.svg - assets/icons/new/arrows_outward.svg - assets/icons/new/repartition.svg - - - assets/icons/new/build_gang.svg - assets/icons/new/center_focus_strong.svg - assets/icons/new/variables_insert.svg - - - - diff --git a/ui/qml/reskin/session_data/XsMediaListModelData.qml b/ui/qml/reskin/session_data/XsMediaListModelData.qml deleted file mode 100644 index b3b9c2d26..000000000 --- a/ui/qml/reskin/session_data/XsMediaListModelData.qml +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 - -import xstudio.qml.session 1.0 -import xstudio.qml.helpers 1.0 -import xstudio.qml.models 1.0 - -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 - -/* This model gives us access to the data of media in a playlist, subset, timeline -etc. that we can iterate over with a Repeater, ListView etc. */ -DelegateModel { - - id: mediaList - - // our model is the main sessionData instance - model: theSessionData - - // we listen to the main selection model that selects stuff in the - // main sessionData - this thing decides which playlist, subset, timeline - // etc. is selected to be displayed in our media list - property var currentSelectedPlaylistIndex: sessionSelectionModel.currentIndex - onCurrentSelectedPlaylistIndexChanged : { - updateMedia() - } - - function updateMedia() { - if(currentSelectedPlaylistIndex.valid) { - // wait for valid index.. - let mind = currentSelectedPlaylistIndex.model.index(0, 0, currentSelectedPlaylistIndex) - if(mind.valid) { - mediaList.rootIndex = mind - } else { - // try again in 200 milliseconds - callback_timer.setTimeout(function() { return function() { - updateMedia() - }}(), 200); - } - } else { - mediaList.rootIndex = null - } - } - -} - diff --git a/ui/qml/reskin/session_data/XsPlaylistsModelData.qml b/ui/qml/reskin/session_data/XsPlaylistsModelData.qml deleted file mode 100644 index ebce36425..000000000 --- a/ui/qml/reskin/session_data/XsPlaylistsModelData.qml +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 - -import xstudio.qml.session 1.0 -import xstudio.qml.helpers 1.0 -import xstudio.qml.models 1.0 - -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 - diff --git a/ui/qml/reskin/session_data/XsSessionData.qml b/ui/qml/reskin/session_data/XsSessionData.qml deleted file mode 100644 index ad7252a2a..000000000 --- a/ui/qml/reskin/session_data/XsSessionData.qml +++ /dev/null @@ -1,206 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 - -import xstudio.qml.session 1.0 -import xstudio.qml.helpers 1.0 -import xstudio.qml.models 1.0 -import xstudio.qml.global_store_model 1.0 - -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 - -Item { - - id: collecion - property var sessionActorAddr - - XsSessionModel { - - id: sessionData - sessionActorAddr: collecion.sessionActorAddr - - } - property alias session: sessionData - - - XsGlobalStoreModel { - id: globalStoreModel - } - property alias globalStoreModel: globalStoreModel - - /* selectedMediaSetIndex is the index into the model that points to the 'selected' - playlist, subset, timeline etc. - the selected media set is the playlist, - subset or timeline is the last one to be single-clicked on in the playlists - panel. The selected media set is what is shown in the media list for example - but can and often is different to the 'viewedMediaSet' */ - property var selectedMediaSetIndex: session.index(-1, -1) - onSelectedMediaSetIndexChanged: { - - sessionSelectionModel.select(selectedMediaSetIndex, ItemSelectionModel.ClearAndSelect | ItemSelectionModel.setCurrentIndex) - sessionSelectionModel.setCurrentIndex(selectedMediaSetIndex, ItemSelectionModel.setCurrentIndex) - - } - - - /* viewedMediaSetIndex is the index into the model that points to the 'active' - playlist, subset, timeline etc. - the active media set is the playlist, - subset or timeline that is being viewed in the viewport and shows in the - timeline panel */ - property var viewedMediaSetIndex: session.index(-1, -1) - - onViewedMediaSetIndexChanged: { - - session.setPlayheadTo(viewedMediaSetIndex) - - // get the index of the PlayheadSelection node for this playlist - let ind = session.search_recursive("PlayheadSelection", "typeRole", viewedMediaSetIndex) - - if (ind.valid) { - // make the 'mediaSelectionModel' track the PlayheadSelection - playheadSelectionIndex = ind - mediaSelectionModel.updateSelection() - - } - - // get the index of the Playhead node for this playlist - let ind2 = session.search_recursive("Playhead", "typeRole", viewedMediaSetIndex) - if (ind2.valid) { - // make the 'mediaSelectionModel' track the PlayheadSelection - currentPlayheadProperties.index = ind2 - } - - } - - // This ItemSelectionModel manages playlist, subset, timeline etc. selection - // from the top-level session. Of the selection, the first selected item - // is the 'active' playlist/subset/timeline that is shown in the medialist - // and viewport - ItemSelectionModel { - id: sessionSelectionModel - model: sessionData - } - property alias sessionSelectionModel: sessionSelectionModel - - /* playheadSelectionIndex is the index into the model that points to the 'active' - playheadSelectionActor - Each playlist, subset, timeline has its own - playheadSelectionActor and this is the object that selectes media from the - playlist to be shown in the viewport (and compared with A/B, String compare - modes etc.) */ - property var playheadSelectionIndex - - /* Here we use XsModelPropertyMap to track the Uuid of the 'current' playhead. - Note that we set the index for this in onviewedMediaSetIndexChanged above */ - XsModelPropertyMap { - id: currentPlayheadProperties - property var playheadUuid: values.actorUuidRole - } - - /* This XsModuleData talks to a backend data model that contains all the - attribute data of the Playhead object and exposes it as data in QML as - a QAbstractItemModel. Every playhead instance in the app publishes its own - data model which is identified by the uuid of the playhead - by changing the - 'modelDataName' to the Uuid of the current playhead we get access to the - data of the current active playhead. - - If this seems confusing it's because it is! We have two different ways of - exposing the data of backend objects - the main Session model and then more - flexible 'XsModuleData' that can be set-up (in the backend) to include some - or all of the data from one or several backend objects. - - At some point we may rationalise this and build into the singe Session model*/ - XsModuleData { - - id: current_playhead_data - - // this is how we link up with the backend data model that gives - // access to all the playhead attributes data - modelDataName: currentPlayheadProperties.playheadUuid ? currentPlayheadProperties.playheadUuid : "" - } - property alias current_playhead_data: current_playhead_data - - // This ItemSelectionModel manages media selection within the current - // active playlist/subset/timeline etc. - ItemSelectionModel { - - id: mediaSelectionModel - model: session - - onSelectionChanged: { - if(selectedIndexes.length) { - session.updateSelection(playheadSelectionIndex, selectedIndexes) - } - } - - // This is pretty baffling..... Shouldn't the backend playhead - // selectin actor update the model for us instead of this gubbins? - function updateSelection() { - - // the playheadSelection item is a child of the playlist (or subset, - // timeline etc) so use this to get to the playlist - let playlistIndex = playheadSelectionIndex.parent - - // iterator over the playheadSelection rows ... - /*let count = sessionData.rowCount(playheadSelectionIndex) - for(let i =0; i mouseX) isExpanded = false - else isExpanded = true - - if(drag.active) { - - size = (dragThumbDiv.x + dragThumbDiv.width) // / titleBarTotalWidth - - - // if(isExpanded) { - // headerItemsModel.get(index+1).size += headerItemsModel.get(index).size - // } - // else{ - - // } - - } - - } - } - - } - -} \ No newline at end of file diff --git a/ui/qml/reskin/views/media/delegates/XsMediaItemDelegate.qml b/ui/qml/reskin/views/media/delegates/XsMediaItemDelegate.qml deleted file mode 100644 index 6f26ae477..000000000 --- a/ui/qml/reskin/views/media/delegates/XsMediaItemDelegate.qml +++ /dev/null @@ -1,278 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 -import QtQuick.Layouts 1.15 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 - -Rectangle { - - id: contentDiv - width: parent.width; - height: parent.height - - color: "transparent" - property color highlightColor: palette.highlight - property color bgColorPressed: XsStyleSheet.widgetBgNormalColor - property color bgColorNormal: "transparent" - property color forcedBgColorNormal: bgColorNormal - property color borderColorHovered: highlightColor - - property color hintColor: XsStyleSheet.hintColor - property color errorColor: XsStyleSheet.errorColor - - property bool isSelected: mediaSelectionModel.selectedIndexes.includes(media_item_model_index) - - property var selectionIndex: mediaSelectionModel.selectedIndexes.indexOf(media_item_model_index)+1 - - property bool isMissing: false - property bool isActive: false - property real panelPadding: XsStyleSheet.panelPadding - property real itemPadding: XsStyleSheet.panelPadding/2 - - property real headerThumbWidth: 1 - - // property real rowHeight: XsStyleSheet.widgetStdHeight - property real itemHeight: (rowHeight-8) //16 - - signal activated() //#TODO: for testing only - - //font.pixelSize: textSize - //font.family: textFont - //hoverEnabled: true - opacity: enabled ? 1.0 : 0.33 - - property var columns_model - - Item { - - anchors.fill: parent - - Rectangle{ id: rowDividerLine - width: parent.width; height: headerThumbWidth - color: bgColorPressed - anchors.bottom: parent.bottom - } - - RowLayout{ - - id: row - spacing: 0 - height: rowHeight - anchors.verticalCenter: parent.verticalCenter - - Repeater { - - // Note: columns_model is set-up in the ui_qml.json preference - // file. Look for 'media_list_columns_config' item in that - // file. It specifies the title, size, data_type and so-on for - // each column in the media list view. The DelegateChooser - // here creates graphics/text items that go into the media list - // table depedning on the 'data_type'. To add new ways to view - // data like traffic lights, icons and so-on create a new - // indicator class with a new correspondinf 'data_type' in the - // ui_qml.json - model: columns_model - delegate: chooser - - DelegateChooser { - - id: chooser - role: "data_type" - - DelegateChoice { - roleValue: "flag" - XsMediaFlagIndicator{ - Layout.preferredWidth: size - Layout.minimumHeight: itemHeight - } - } - - DelegateChoice { - roleValue: "metadata" - XsMediaTextItem { - raw_text: metadataFieldValues ? metadataFieldValues[index] : "" - Layout.preferredWidth: size - Layout.minimumHeight: itemHeight - } - } - - DelegateChoice { - roleValue: "role_data" - XsMediaTextItem { - Layout.preferredWidth: size - Layout.minimumHeight: itemHeight - raw_text: { - var result = "" - if (object == "MediaSource") { - let image_source_idx = media_item_model_index.model.search_recursive( - media_item_model_index.model.get(media_item_model_index, "imageActorUuidRole"), - "actorUuidRole", - media_item_model_index) - result = media_item_model_index.model.get(image_source_idx, role_name) - } else if (object == "Media") { - result = media_item_model_index.model.get(media_item_model_index, role_name) - } - return "" + result; - } - } - } - - DelegateChoice { - roleValue: "index" - XsMediaTextItem { - text: selectionIndex ? selectionIndex : "" - Layout.preferredWidth: size - Layout.minimumHeight: itemHeight - } - } - - DelegateChoice { - roleValue: "notes" - XsMediaNotesIndicator{ - Layout.preferredWidth: size - Layout.minimumHeight: itemHeight - } - } - - DelegateChoice { - roleValue: "thumbnail" - XsMediaThumbnailImage { - Layout.preferredWidth: size - Layout.minimumHeight: itemHeight - } - } - - } - - } - } - } - - //background: - Rectangle { - id: bgDiv - anchors.fill: parent - border.color: contentDiv.down || contentDiv.hovered ? borderColorHovered: borderColorNormal - border.width: borderWidth - color: contentDiv.down || isSelected ? bgColorPressed : forcedBgColorNormal - - Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: contentDiv.activeFocus - color: "transparent" - opacity: 0.33 - border.color: borderColorHovered - border.width: borderWidth - anchors.centerIn: parent - } - - // Rectangle{anchors.fill: parent; color: "grey"; opacity:(index%2==0?.2:0)} - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onPressed: { - if (mouse.modifiers == Qt.ControlModifier) { - - toggleSelection() - - } else if (mouse.modifiers == Qt.ShiftModifier) { - - inclusiveSelect() - - } else { - - exclusiveSelect() - - } - } - - } - - - function toggleSelection() { - - if (!(mediaSelectionModel.selection.count == 1 && - mediaSelectionModel.selection[0] == media_item_model_index)) { - mediaSelectionModel.select( - media_item_model_index, - ItemSelectionModel.Toggle - ) - } - - } - - function exclusiveSelect() { - - mediaSelectionModel.select( - media_item_model_index, - ItemSelectionModel.ClearAndSelect - | ItemSelectionModel.setCurrentIndex - ) - - } - - function inclusiveSelect() { - - // For Shift key and select find the nearest selected row, - // select items between that row and the row of THIS item - var row = media_item_model_index.row - var d = 10000 - var nearest_row = -1 - var selection = mediaSelectionModel.selectedIndexes - - for (var i = 0; i < selection.length; ++i) { - var delta = Math.abs(selection[i].row-row) - if (delta < d) { - d = delta - nearest_row = selection[i].row - } - } - - if (nearest_row!=-1) { - - var model = media_item_model_index.model - var first = Math.min(row, nearest_row) - var last = Math.max(row, nearest_row) - - for (var i = first; i <= last; ++i) { - - mediaSelectionModel.select( - model.index( - i, - media_item_model_index.column, - media_item_model_index.parent - ), - ItemSelectionModel.Select - ) - } - } - } - - - // onClicked: { - // isSelected = true - // } - /*onDoubleClicked: { - isSelected = true - activated() //#TODO - } - onPressed: { - mediaSelectionModel.select(media_item_model_index, ItemSelectionModel.ClearAndSelect | ItemSelectionModel.setCurrentIndex) - } - onReleased: { - focus = false - } - onPressAndHold: { - isMissing = !isMissing - }*/ - -} \ No newline at end of file diff --git a/ui/qml/reskin/views/media/delegates/XsMediaSourceSelector.qml b/ui/qml/reskin/views/media/delegates/XsMediaSourceSelector.qml deleted file mode 100644 index d6f270eb1..000000000 --- a/ui/qml/reskin/views/media/delegates/XsMediaSourceSelector.qml +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 - -Item { - - id: selector - - property var media_item_model_index: null - property var columns_model - property var media_index_in_playlist - - width: itemRowWidth - height: itemRowHeight - - // This Item is instanced for every Media object. Media objects contain - // one or more MediaSource objects. We want instance a XsMediaItemDelegate - // for the 'current' MediaSource. So here we track the UUID of the 'image actor', - // which is a property of the Media object and tells us the ID of the - // current MediaSource - property var imageActorUuid: imageActorUuidRole - - // here we follow the Media object metadata fields. They are accessed deeper - // down in the 'data_indicators' that are instanced by the 'XsMediaItemDelegate' - // created here. - property var metadataFieldValues: metadataSet0Role - - Repeater { - model: - DelegateModel { - - // this DelegateModel is set-up to iterate over the contents of the Media - // node (i.e. the MediaSource objects) - model: media_item_model_index.model - rootIndex: media_item_model_index - delegate: - DelegateChooser { - - id: chooser - - // Here we employ a chooser and check against the uuid of the - // MediaSource object - role: "actorUuidRole" - - DelegateChoice { - // we only instance the XsMediaItemDelegate when the - // MediaSource object uuid matches the imageActorUuid - - // Hence we have filtered for the active MediaSource - roleValue: imageActorUuid - XsMediaItemDelegate { - width: selector.width - height: selector.height - columns_model: selector.columns_model - } - } - } - } - } -} \ No newline at end of file diff --git a/ui/qml/reskin/views/playlists/XsPlaylistItems.qml b/ui/qml/reskin/views/playlists/XsPlaylistItems.qml deleted file mode 100644 index d547dfade..000000000 --- a/ui/qml/reskin/views/playlists/XsPlaylistItems.qml +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtQuick.Controls.Styles 1.4 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 -import QtQuick.Layouts 1.15 - -import xStudioReskin 1.0 - -XsListView { id: playlist - - model: playlistsModel - - property var itemsDataModel: null - - property real itemRowWidth: 200 - property real itemRowStdHeight: XsStyleSheet.widgetStdHeight -2 - - DelegateModel { - id: playlistsModel - - // this is required as "model" doesn't issue notifications on change - property var notifyModel: theSessionData - - // we use the main session data model - model: notifyModel - - // point at session 0,0, it's children are the playlists. - rootIndex: notifyModel.index(0, 0, notifyModel.index(-1, -1)) - delegate: chooser - } - - DelegateChooser { - id: chooser - role: "typeRole" - - DelegateChoice { - roleValue: "ContainerDivider"; - - XsPlaylistDividerDelegate{ - width: itemRowWidth - height: itemRowStdHeight +(4+1) - } - } - DelegateChoice { - roleValue: "Playlist"; - - XsPlaylistItemDelegate{ - width: itemRowWidth - modelIndex: playlistsModel.modelIndex(index) - } - } - - } - -} diff --git a/ui/qml/reskin/views/playlists/XsPlaylists.qml b/ui/qml/reskin/views/playlists/XsPlaylists.qml deleted file mode 100644 index 4cca1ace6..000000000 --- a/ui/qml/reskin/views/playlists/XsPlaylists.qml +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtQuick.Controls.Styles 1.4 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 -import QtQuick.Layouts 1.15 - -import xStudioReskin 1.0 - -Item{ - - id: panel - - anchors.fill: parent - - property color panelColor: XsStyleSheet.panelBgColor - property color bgColorPressed: palette.highlight - property color bgColorNormal: "transparent" - property color forcedBgColorNormal: bgColorNormal - property color borderColorHovered: bgColorPressed - property color borderColorNormal: "transparent" - property real borderWidth: 1 - property bool isSubDivider: false - - property real textSize: XsStyleSheet.fontSize - property var textFont: XsStyleSheet.fontFamily - property color textColorNormal: palette.text - property color hintColor: XsStyleSheet.hintColor - - property real btnWidth: XsStyleSheet.primaryButtonStdWidth - property real secBtnWidth: XsStyleSheet.secondaryButtonStdWidth - property real btnHeight: XsStyleSheet.widgetStdHeight+4 - property real panelPadding: XsStyleSheet.panelPadding - - // background - Rectangle{ - z: -1000 - anchors.fill: parent - gradient: Gradient { - GradientStop { position: 0.0; color: "#5C5C5C" } - GradientStop { position: 1.0; color: "#474747" } - } - } - - Item{id: actionDiv - width: parent.width; - height: btnHeight+(panelPadding*2) - - RowLayout{ - x: panelPadding - spacing: 1 - width: parent.width-(x*2) - height: btnHeight - anchors.verticalCenter: parent.verticalCenter - - XsPrimaryButton{ id: addPlaylistBtn - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/add.svg" - } - XsPrimaryButton{ id: deleteBtn - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/delete.svg" - } - XsSearchButton{ id: searchBtn - Layout.preferredWidth: isExpanded? btnWidth*6 : btnWidth - Layout.preferredHeight: parent.height - isExpanded: false - hint: "Search playlists..." - } - Item{ - Layout.fillWidth: true - Layout.preferredHeight: parent.height - } - XsPrimaryButton{ id: morePlaylistBtn - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/more_vert.svg" - } - } - - } - - Rectangle{ id: playlistDiv - x: panelPadding - y: actionDiv.height - width: panel.width-(x*2) - height: panel.height-y-panelPadding - color: panelColor - - Rectangle{ id: titleBar - color: XsStyleSheet.panelTitleBarColor - width: parent.width - height: XsStyleSheet.widgetStdHeight - - XsText{ - text: "Playlist ("+playlistItems.count+")" - anchors.left: parent.left - anchors.leftMargin: panelPadding - anchors.verticalCenter: parent.verticalCenter - horizontalAlignment: Text.AlignLeft - } - - XsSecondaryButton{ - width: secBtnWidth - height: secBtnWidth - imgSrc: "qrc:/icons/filter_none.svg" - anchors.right: errorBtn.left - anchors.rightMargin: panelPadding - anchors.verticalCenter: parent.verticalCenter - } - - XsSecondaryButton{ id: errorBtn - width: secBtnWidth - height: secBtnWidth - imgSrc: "qrc:/icons/error.svg" - anchors.right: parent.right - anchors.rightMargin: panelPadding + panelPadding/2 - anchors.verticalCenter: parent.verticalCenter - } - - } - - XsPlaylistItems{ - - id: playlistItems - y: titleBar.height - itemRowWidth: playlistDiv.width - - } - - - } - -} \ No newline at end of file diff --git a/ui/qml/reskin/views/playlists/delegates/XsPlaylistDividerDelegate.qml b/ui/qml/reskin/views/playlists/delegates/XsPlaylistDividerDelegate.qml deleted file mode 100644 index 22e79929a..000000000 --- a/ui/qml/reskin/views/playlists/delegates/XsPlaylistDividerDelegate.qml +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 - -import xStudioReskin 1.0 - -Item { - id: dividerDiv - width: parent.width - height: parent.height - opacity: enabled ? 1.0 : 0.33 - - property real itemRealHeight: XsStyleSheet.widgetStdHeight -2 - property real panelPadding: XsStyleSheet.panelPadding - property real buttonWidth: XsStyleSheet.secondaryButtonStdWidth - - property color panelColor: XsStyleSheet.panelBgColor - property color bgColorPressed: XsStyleSheet.widgetBgNormalColor - property color bgColorNormal: "transparent" - property color forcedBgColorNormal: bgColorNormal - - property color hintColor: XsStyleSheet.hintColor - property color dividerColor: XsStyleSheet.menuBarColor - - property bool isSelected: false - property bool isSubDivider: false - - Button{ id: visibleItemDiv - - y: panelPadding - width: parent.width - height: itemRealHeight - - text: nameRole - font.pixelSize: textSize - font.family: textFont - hoverEnabled: true - padding: 0 - - contentItem: - Item{ - width: parent.width - panelPadding //for scrollbar - height: parent.height //- 5 - anchors.top: parent.top - anchors.topMargin: panelPadding - anchors.bottom: parent.bottom - anchors.bottomMargin: panelPadding - - Rectangle{ id: leftLine; height: 1; color: isSelected?hintColor:dividerColor; anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left; anchors.leftMargin: isSubDivider? buttonWidth*2+panelPadding : buttonWidth - anchors.right: textDiv.left; anchors.rightMargin: panelPadding*2 - - } - Rectangle{ id: rightLine; height: 1; color: isSelected?hintColor:dividerColor; anchors.verticalCenter: parent.verticalCenter - anchors.left: textDiv.right; anchors.leftMargin: panelPadding*2 - anchors.right: moreBtn.visible? moreBtn.left : parent.right; anchors.rightMargin: moreBtn.visible? panelPadding : panelPadding*2 - } - XsText { - id: textDiv - property real titlePadding: leftLine.anchors.leftMargin - text: visibleItemDiv.text - font: visibleItemDiv.font - color: hintColor - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - width: (parent.width-textDiv.titlePadding*2) > textWidth? textWidth : (parent.width-textDiv.titlePadding*2) - height: parent.height - - // XsToolTip{ - // text: visibleItemDiv.text - // visible: visibleItemDiv.hovered && parent.truncated - // width: metricsDiv.width == 0? 0 : visibleItemDiv.width - // } - } - - XsSecondaryButton{ id: moreBtn - visible: visibleItemDiv.hovered - width: buttonWidth - height: buttonWidth - imgSrc: "qrc:/icons/more_horiz.svg" - anchors.right: parent.right - anchors.rightMargin: panelPadding - anchors.verticalCenter: parent.verticalCenter - imgOverlayColor: hintColor - } - } - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - border.color: visibleItemDiv.down || visibleItemDiv.hovered ? borderColorHovered: borderColorNormal - border.width: borderWidth - color: visibleItemDiv.down || isSelected? bgColorPressed : forcedBgColorNormal - - Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: visibleItemDiv.activeFocus - color: "transparent" - opacity: 0.33 - border.color: borderColorHovered - border.width: borderWidth - anchors.centerIn: parent - } - } - - onPressed: { - focus = true - isSelected = !isSelected - } - onReleased: focus = false - } - -} \ No newline at end of file diff --git a/ui/qml/reskin/views/playlists/delegates/XsPlaylistItemDelegate.qml b/ui/qml/reskin/views/playlists/delegates/XsPlaylistItemDelegate.qml deleted file mode 100644 index 83d2a2c1c..000000000 --- a/ui/qml/reskin/views/playlists/delegates/XsPlaylistItemDelegate.qml +++ /dev/null @@ -1,261 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 -import QtQuick.Layouts 1.15 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 - -import xStudioReskin 1.0 - -Item { - id: contentDiv - width: parent.width; - height: itemRowStdHeight + (isExpanded ? subItemsCount*itemRowStdHeight : 0) - opacity: enabled ? 1.0 : 0.33 - - property real itemRealHeight: XsStyleSheet.widgetStdHeight -2 - property real itemPadding: XsStyleSheet.panelPadding/2 - property real buttonWidth: XsStyleSheet.secondaryButtonStdWidth - - property color bgColorPressed: XsStyleSheet.widgetBgNormalColor - property color bgColorNormal: "transparent" - property color forcedBgColorNormal: bgColorNormal - - property color hintColor: XsStyleSheet.hintColor - property color errorColor: XsStyleSheet.errorColor - - /* modelIndex should be set to point into the session data model and get - to the playlist that we are representing */ - property var modelIndex - - /* first index in playlist is media ... */ - property var itemCount: mediaCountRole - - /* .... the third row gives us the data of the subsets/timelines etc. i.e. - the children lists of the playlist */ - property var subItemsModelIndex: modelIndex && modelIndex.valid ? theSessionData.index(2, 0, modelIndex) : undefined - property var subItemsCount: subItemsModel.count - - property bool isSelected: false - property bool isMissing: false - property bool isExpanded: false - - // Rectangle{anchors.fill: parent; color:(index%2==0)?"transparent":"yellow"; opacity:0.3} - - Button { id: visibleItemDiv - - width: parent.width - height: itemRealHeight - - text: isMissing? "This playlist no longer exists" : nameRole - font.pixelSize: textSize - font.family: textFont - hoverEnabled: true - - contentItem: - Item{ - anchors.fill: parent - - RowLayout { - - x: spacing - spacing: itemPadding - width: parent.width -x -spacing -(spacing*2) //for scrollbar - height: XsStyleSheet.widgetStdHeight - anchors.verticalCenter: parent.verticalCenter - - Item{ - Layout.preferredWidth: buttonWidth - Layout.preferredHeight: buttonWidth - - XsSecondaryButton{ - - id: subsetBtn - - imgSrc: "qrc:/icons/chevron_right.svg" - visible: subItemsCount != 0 - anchors.fill: parent - - isActive: isExpanded - scale: rotation==0 || rotation==-90? 1:0.85 - rotation: (isExpanded)? 90:0 - Behavior on rotation {NumberAnimation{duration: 150 }} - - onClicked:{ - isExpanded = !isExpanded - } - - } - } - XsImage{ - Layout.minimumWidth: buttonWidth - Layout.maximumWidth: buttonWidth - Layout.minimumHeight: buttonWidth - Layout.maximumHeight: buttonWidth - source: isMissing? "qrc:/icons/error.svg" : "qrc:/icons/list_default.svg" - // Math.floor(Math.random()*2)==0? "qrc:/icons/list_subset.svg" : - // Math.floor(Math.random()*2)==1? "qrc:/icons/list_shotgun.svg" : - imgOverlayColor: isMissing? errorColor : hintColor - } - XsText { - id: textDiv - text: visibleItemDiv.text //+"-"+index //#TODO - font: visibleItemDiv.font - color: isMissing? hintColor : textColorNormal - Layout.fillWidth: true - Layout.preferredHeight: buttonWidth - - leftPadding: itemPadding - horizontalAlignment: Text.AlignLeft - tooltipText: visibleItemDiv.text - tooltipVisibility: visibleItemDiv.hovered && truncated - toolTipWidth: visibleItemDiv.width+5 - } - XsSecondaryButton{ id: addBtn - imgSrc: "qrc:/icons/add.svg" - imgOverlayColor: hintColor - visible: visibleItemDiv.hovered - Layout.preferredWidth: buttonWidth - Layout.preferredHeight: buttonWidth - } - XsSecondaryButton{ id: moreBtn - imgSrc: "qrc:/icons/more_horiz.svg" - imgOverlayColor: hintColor - visible: visibleItemDiv.hovered - Layout.preferredWidth: buttonWidth - Layout.preferredHeight: buttonWidth - } - XsText{ id: countDiv - text: itemCount - Layout.minimumWidth: buttonWidth + 5 - Layout.preferredHeight: buttonWidth - color: hintColor - } - Item{ - Layout.preferredWidth: buttonWidth - Layout.preferredHeight: buttonWidth - - XsSecondaryButton{ id: errorIndicator - anchors.fill: parent - visible: errorRole != 0 - imgSrc: "qrc:/icons/error.svg" - imgOverlayColor: hintColor - - toolTip.text: errorRole +" errors" - toolTip.visible: hovered - } - } - } - } - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - border.color: visibleItemDiv.down || visibleItemDiv.hovered ? borderColorHovered: borderColorNormal - border.width: borderWidth - color: visibleItemDiv.down || isSelected? bgColorPressed : forcedBgColorNormal - - Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: visibleItemDiv.activeFocus - color: "transparent" - opacity: 0.33 - border.color: borderColorHovered - border.width: borderWidth - anchors.centerIn: parent - } - } - - onPressed: { - // here we set the index of the active playlist (stored by - // XsSessionData) to our own index on click - selectedMediaSetIndex = modelIndex - } - - onDoubleClicked: { - // here we set the index of the active playlist (stored by - // XsSessionData) to our own index on click - viewedMediaSetIndex = modelIndex - selectedMediaSetIndex = modelIndex - } - - } - - /* Here we have a model to iterate over the contents of the playlist (if - any) such as subsets, timelines, dividers etc */ - DelegateModel { - id: subItemsModel - - // we use the main session data model - // this is required as "model" doesn't issue notifications on change - property var notifyModel: theSessionData - - // we use the main session data model - model: notifyModel - - // playlists are one level in at row=0, column=0. - rootIndex: subItemsModelIndex - delegate: chooser - } - - DelegateChooser { - id: chooser - role: "typeRole" - - DelegateChoice { - - roleValue: "Subset" - XsSubsetItemDelegate{ - - width: itemRowWidth - height: itemRowStdHeight - modelIndex: theSessionData.index(index, 0, subItemsModelIndex) - } - } - - DelegateChoice { - - roleValue: "Timeline" - XsTimelineItemDelegate{ - width: itemRowWidth - height: itemRowStdHeight - modelIndex: theSessionData.index(index, 0, subItemsModelIndex) - } - } - - DelegateChoice { - - roleValue: "ContainerDivider" - XsPlaylistDividerDelegate{ - isSubDivider: true - width: itemRowWidth - height: itemRowStdHeight - } - - } - - } - - // The layout to show the playlist sub-items - ColumnLayout { - - id: subItems - anchors.left: parent.left - anchors.right: parent.right - anchors.top: visibleItemDiv.bottom - visible: isExpanded - spacing: 0 - - Repeater { - - model: subItemsModel - - } - } - -} \ No newline at end of file diff --git a/ui/qml/reskin/views/playlists/delegates/XsSubsetItemDelegate.qml b/ui/qml/reskin/views/playlists/delegates/XsSubsetItemDelegate.qml deleted file mode 100644 index 48dd9be41..000000000 --- a/ui/qml/reskin/views/playlists/delegates/XsSubsetItemDelegate.qml +++ /dev/null @@ -1,185 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 -import QtQuick.Layouts 1.15 - -import xStudioReskin 1.0 - -Item { - id: contentDiv - width: parent.width; - height: itemRowStdHeight - opacity: enabled ? 1.0 : 0.33 - - property real itemRealHeight: XsStyleSheet.widgetStdHeight -2 - property real itemPadding: XsStyleSheet.panelPadding/2 - property real buttonWidth: XsStyleSheet.secondaryButtonStdWidth - - property color bgColorPressed: XsStyleSheet.widgetBgNormalColor - property color bgColorNormal: "transparent" - property color forcedBgColorNormal: bgColorNormal - - property color hintColor: XsStyleSheet.hintColor - property color errorColor: XsStyleSheet.errorColor - property var itemCount: mediaCountRole - - /* modelIndex should be set to point into the session data model and get - to the playlist that we are representing */ - property var modelIndex - - property bool isSelected: false - property bool isMissing: false - property bool isSubList: true - property bool isExpanded: true - - // Rectangle{anchors.fill: parent; color:(index%2==0)?"transparent":"yellow"; opacity:0.3} - - Button{ - - id: visibleItemDiv - - width: parent.width - height: itemRealHeight - - text: isMissing? "This playlist no longer exists" : nameRole - font.pixelSize: textSize - font.family: textFont - hoverEnabled: true - - contentItem: - Item{ - anchors.fill: parent - - RowLayout{ - - x: subsetBtn.width+(spacing*2) - spacing: itemPadding - width: parent.width -x -spacing -(spacing*2) //for scrollbar - height: XsStyleSheet.widgetStdHeight - anchors.verticalCenter: parent.verticalCenter - - Item{ - Layout.preferredWidth: buttonWidth - Layout.preferredHeight: buttonWidth - - XsSecondaryButton{ - - id: subsetBtn - - imgSrc: "qrc:/icons/chevron_right.svg" - visible: subItemsCount != 0 - anchors.fill: parent - - isActive: isExpanded - scale: rotation==0 || rotation==-90? 1:0.85 - rotation: (isExpanded)? 90:0 - Behavior on rotation {NumberAnimation{duration: 150 }} - - onClicked:{ - isExpanded = !isExpanded - } - - } - } - XsImage{ - Layout.minimumWidth: buttonWidth - Layout.maximumWidth: buttonWidth - Layout.minimumHeight: buttonWidth - Layout.maximumHeight: buttonWidth - source: isMissing? "qrc:/icons/error.svg" : - Math.floor(Math.random()*2)==0? "qrc:/icons/list_subset.svg" : - Math.floor(Math.random()*2)==1? "qrc:/icons/list_shotgun.svg" : - "qrc:/icons/list_default.svg" - imgOverlayColor: isMissing? errorColor : hintColor - } - XsText { - id: textDiv - text: visibleItemDiv.text //+"-"+index //#TODO - font: visibleItemDiv.font - color: isMissing? hintColor : textColorNormal - Layout.fillWidth: true - Layout.preferredHeight: buttonWidth - - leftPadding: itemPadding - horizontalAlignment: Text.AlignLeft - tooltipText: visibleItemDiv.text - tooltipVisibility: visibleItemDiv.hovered && truncated - toolTipWidth: visibleItemDiv.width+5 - } - XsSecondaryButton{ id: addBtn - imgSrc: "qrc:/icons/add.svg" - imgOverlayColor: hintColor - visible: visibleItemDiv.hovered - Layout.preferredWidth: buttonWidth - Layout.preferredHeight: buttonWidth - } - XsSecondaryButton{ id: moreBtn - imgSrc: "qrc:/icons/more_horiz.svg" - imgOverlayColor: hintColor - visible: visibleItemDiv.hovered - Layout.preferredWidth: buttonWidth - Layout.preferredHeight: buttonWidth - } - XsText{ id: countDiv - text: itemCount - Layout.minimumWidth: buttonWidth + 5 - Layout.preferredHeight: buttonWidth - color: hintColor - } - Item{ - Layout.preferredWidth: buttonWidth - Layout.preferredHeight: buttonWidth - - XsSecondaryButton{ id: errorIndicator - anchors.fill: parent - visible: errorRole != 0 - imgSrc: "qrc:/icons/error.svg" - imgOverlayColor: hintColor - - toolTip.text: errorRole +" errors" - toolTip.visible: hovered - } - } - } - } - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - border.color: visibleItemDiv.down || visibleItemDiv.hovered ? borderColorHovered: borderColorNormal - border.width: borderWidth - color: visibleItemDiv.down || isSelected? bgColorPressed : forcedBgColorNormal - - Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: visibleItemDiv.activeFocus - color: "transparent" - opacity: 0.33 - border.color: borderColorHovered - border.width: borderWidth - anchors.centerIn: parent - } - } - - onPressed: { - // here we set the index of the active playlist (stored by - // XsSessionData) to our own index on click - selectedMediaSetIndex = modelIndex - } - - onDoubleClicked: { - // here we set the index of the active playlist (stored by - // XsSessionData) to our own index on click - viewedMediaSetIndex = modelIndex - selectedMediaSetIndex = modelIndex - } - - } - - -} \ No newline at end of file diff --git a/ui/qml/reskin/views/playlists/delegates/XsTimelineItemDelegate.qml b/ui/qml/reskin/views/playlists/delegates/XsTimelineItemDelegate.qml deleted file mode 100644 index 756b4857f..000000000 --- a/ui/qml/reskin/views/playlists/delegates/XsTimelineItemDelegate.qml +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 -import QtQuick.Layouts 1.15 - -import xStudioReskin 1.0 -import xstudio.qml.helpers 1.0 - -Item { - id: contentDiv - width: parent.width; - height: itemRowStdHeight - opacity: enabled ? 1.0 : 0.33 - - property real itemRealHeight: XsStyleSheet.widgetStdHeight -2 - property real itemPadding: XsStyleSheet.panelPadding/2 - property real buttonWidth: XsStyleSheet.secondaryButtonStdWidth - - property color bgColorPressed: XsStyleSheet.widgetBgNormalColor - property color bgColorNormal: "transparent" - property color forcedBgColorNormal: bgColorNormal - - property color hintColor: XsStyleSheet.hintColor - property color errorColor: XsStyleSheet.errorColor - property var itemCount: mediaCountRole - - /* modelIndex should be set to point into the session data model and get - to the playlist that we are representing */ - property var modelIndex - - property bool isSelected: false - property bool isMissing: false - property bool isSubList: true - property bool isExpanded: true - - XsModelRowCount { - id: mediaModelCount - index: mediaListModelIndex - } - - Button{ - - id: visibleItemDiv - - width: parent.width - height: itemRealHeight - - text: isMissing? "This playlist no longer exists" : nameRole - font.pixelSize: textSize - font.family: textFont - hoverEnabled: true - - contentItem: - Item{ - anchors.fill: parent - - RowLayout{ - - x: subsetBtn.width+(spacing*2) - spacing: itemPadding - width: parent.width -x -spacing -(spacing*2) //for scrollbar - height: XsStyleSheet.widgetStdHeight - anchors.verticalCenter: parent.verticalCenter - - Item{ - Layout.preferredWidth: buttonWidth - Layout.preferredHeight: buttonWidth - - XsSecondaryButton{ - - id: subsetBtn - - imgSrc: "qrc:/icons/chevron_right.svg" - visible: subItemsCount != 0 - anchors.fill: parent - - isActive: isExpanded - scale: rotation==0 || rotation==-90? 1:0.85 - rotation: (isExpanded)? 90:0 - Behavior on rotation {NumberAnimation{duration: 150 }} - - onClicked:{ - isExpanded = !isExpanded - } - - } - } - XsImage{ - Layout.minimumWidth: buttonWidth - Layout.maximumWidth: buttonWidth - Layout.minimumHeight: buttonWidth - Layout.maximumHeight: buttonWidth - source: isMissing? "qrc:/icons/error.svg" : - Math.floor(Math.random()*2)==0? "qrc:/icons/list_subset.svg" : - Math.floor(Math.random()*2)==1? "qrc:/icons/list_shotgun.svg" : - "qrc:/icons/list_default.svg" - imgOverlayColor: isMissing? errorColor : hintColor - } - XsText { - id: textDiv - text: visibleItemDiv.text //+"-"+index //#TODO - font: visibleItemDiv.font - color: isMissing? hintColor : textColorNormal - Layout.fillWidth: true - Layout.preferredHeight: buttonWidth - - leftPadding: itemPadding - horizontalAlignment: Text.AlignLeft - tooltipText: visibleItemDiv.text - tooltipVisibility: visibleItemDiv.hovered && truncated - toolTipWidth: visibleItemDiv.width+5 - } - XsSecondaryButton{ id: addBtn - imgSrc: "qrc:/icons/add.svg" - imgOverlayColor: hintColor - visible: visibleItemDiv.hovered - Layout.preferredWidth: buttonWidth - Layout.preferredHeight: buttonWidth - } - XsSecondaryButton{ id: moreBtn - imgSrc: "qrc:/icons/more_horiz.svg" - imgOverlayColor: hintColor - visible: visibleItemDiv.hovered - Layout.preferredWidth: buttonWidth - Layout.preferredHeight: buttonWidth - } - XsText{ id: countDiv - text: itemCount - Layout.minimumWidth: buttonWidth + 5 - Layout.preferredHeight: buttonWidth - color: hintColor - } - Item{ - Layout.preferredWidth: buttonWidth - Layout.preferredHeight: buttonWidth - - XsSecondaryButton{ id: errorIndicator - anchors.fill: parent - visible: errorRole != 0 - imgSrc: "qrc:/icons/error.svg" - imgOverlayColor: hintColor - - toolTip.text: errorRole +" errors" - toolTip.visible: hovered - } - } - } - } - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - border.color: visibleItemDiv.down || visibleItemDiv.hovered ? borderColorHovered: borderColorNormal - border.width: borderWidth - color: visibleItemDiv.down || isSelected? bgColorPressed : forcedBgColorNormal - - Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: visibleItemDiv.activeFocus - color: "transparent" - opacity: 0.33 - border.color: borderColorHovered - border.width: borderWidth - anchors.centerIn: parent - } - } - - onPressed: { - // here we set the index of the active playlist (stored by - // XsSessionData) to our own index on click - selectedMediaSetIndex = modelIndex - } - - onDoubleClicked: { - // here we set the index of the active playlist (stored by - // XsSessionData) to our own index on click - viewedMediaSetIndex = modelIndex - selectedMediaSetIndex = modelIndex - } - - } - - -} \ No newline at end of file diff --git a/ui/qml/reskin/views/timeline/XsTimeline.qml b/ui/qml/reskin/views/timeline/XsTimeline.qml deleted file mode 100644 index c4dda65ef..000000000 --- a/ui/qml/reskin/views/timeline/XsTimeline.qml +++ /dev/null @@ -1,278 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtQuick.Controls.Styles 1.4 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 -import QtQuick.Layouts 1.15 - -import xstudio.qml.helpers 1.0 -import xstudio.qml.models 1.0 - -import xStudioReskin 1.0 - -Item{ - - id: panel - anchors.fill: parent - - property color bgColorPressed: palette.highlight - property color bgColorNormal: "transparent" - property color forcedBgColorNormal: bgColorNormal - property color borderColorHovered: bgColorPressed - property color borderColorNormal: "transparent" - property real borderWidth: 1 - - property real textSize: XsStyleSheet.fontSize - property var textFont: XsStyleSheet.fontFamily - property color textColorNormal: palette.text - property color hintColor: XsStyleSheet.hintColor - - property real btnWidth: XsStyleSheet.primaryButtonStdWidth - property real btnHeight: XsStyleSheet.widgetStdHeight+4 - property real panelPadding: XsStyleSheet.panelPadding - - property bool isEditToolsExpanded: false - - //#TODO: test - property bool showIcons: false - - // background - Rectangle{ - z: -1000 - anchors.fill: parent - gradient: Gradient { - GradientStop { position: 0.0; color: "#5C5C5C" } - GradientStop { position: 1.0; color: "#474747" } - } - } - - Item{ - - id: actionDiv - width: parent.width; - height: btnHeight+(panelPadding*2) - - RowLayout{ - x: panelPadding - spacing: 1 - width: parent.width-(x*2) - height: btnHeight - anchors.verticalCenter: parent.verticalCenter - - XsText{ - - XsModelProperty { - id: playheadTimecode - role: "value" - index: currentPlayheadData.search_recursive("Current Source Timecode", "title") - } - - Connections { - target: currentPlayheadData // this bubbles up from XsSessionWindow - function onJsonChanged() { - playheadTimecode.index = currentPlayheadData.search_recursive("Current Source Timecode", "title") - } - } - - id: timestampDiv - Layout.preferredWidth: btnWidth*3 - Layout.preferredHeight: parent.height - text: playheadTimecode.value ? playheadTimecode.value : "00:00:00:00" - font.pixelSize: XsStyleSheet.fontSize +6 - font.weight: Font.Bold - horizontalAlignment: Text.AlignHCenter - - } - - XsPrimaryButton{ id: addPlaylistBtn - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/add.svg" - text: "Add" - onClicked: showIcons = !showIcons - } - XsPrimaryButton{ id: deleteBtn - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/delete.svg" - text: "Delete" - } - XsPrimaryButton{ - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/undo.svg" - text: "Undo" - } - XsPrimaryButton{ - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/redo.svg" - text: "Redo" - } - XsSearchButton{ id: searchBtn - Layout.preferredWidth: isExpanded? btnWidth*6 : btnWidth - Layout.preferredHeight: parent.height - isExpanded: false - hint: "Search..." - // isExpandedToLeft: true - } - XsText{ id: titleDiv - Layout.fillWidth: true - Layout.minimumWidth: 0 - Layout.preferredHeight: parent.height - text: viewedMediaSetProperties.values.nameRole - font.bold: true - - opacity: searchBtn.isExpanded? 0:1 - Behavior on opacity { NumberAnimation { duration: 150; easing.type: Easing.OutQuart } } - } - XsPrimaryButton{ - Layout.preferredWidth: btnWidth*1.8 - Layout.preferredHeight: parent.height - imgSrc: "" - text: "Loop IO" - onClicked:{ - isActive = !isActive - } - } - XsPrimaryButton{ - Layout.preferredWidth: btnWidth*2.6 - Layout.preferredHeight: parent.height - imgSrc: "" - text: "Loop Selection" - onClicked:{ - isActive = !isActive - } - } - Item{ - Layout.preferredWidth: panelPadding/2 - Layout.preferredHeight: parent.height - } - XsPrimaryButton{ - Layout.preferredWidth: showIcons? btnWidth : btnWidth*1.8 - Layout.preferredHeight: parent.height - imgSrc: showIcons? "qrc:/icons/center_focus_strong.svg":"" - text: "Focus" - onClicked:{ - isActive = !isActive - } - } - XsPrimaryButton{ - Layout.preferredWidth: btnWidth*1.8 - Layout.preferredHeight: parent.height - imgSrc: "" - text: "Ripple" - onClicked:{ - isActive = !isActive - } - } - XsPrimaryButton{ - Layout.preferredWidth: btnWidth*1.8 - Layout.preferredHeight: parent.height - imgSrc: "" - text: "Gang" - onClicked:{ - isActive = !isActive - } - } - XsPrimaryButton{ - Layout.preferredWidth: btnWidth*1.8 - Layout.preferredHeight: parent.height - imgSrc: "" - text: "Snap" - onClicked:{ - isActive = !isActive - } - } - Item{ - Layout.preferredWidth: panelPadding/2 - Layout.preferredHeight: parent.height - } - XsPrimaryButton{ - Layout.preferredWidth: btnWidth*1.8 - Layout.preferredHeight: parent.height - imgSrc: "" - text: "Overwrite" - } - XsPrimaryButton{ - Layout.preferredWidth: btnWidth*1.8 - Layout.preferredHeight: parent.height - imgSrc: "" - text: "Insert" - } - Item{ - Layout.preferredWidth: panelPadding/2 - Layout.preferredHeight: parent.height - } - XsPrimaryButton{ id: settingsBtn - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/settings.svg" - } - XsPrimaryButton{ id: filterBtn - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/filter.svg" - } - XsPrimaryButton{ id: morePlaylistBtn - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/more_vert.svg" - } - - } - - } - - Rectangle{ - - id: timelineDiv - x: panelPadding - y: actionDiv.height - width: panel.width-(x*2) - height: panel.height-y-panelPadding - color: XsStyleSheet.panelBgColor - - XsTimelineEditTools{ - - x: spacing - y: spacing - - width: isEditToolsExpanded? cellWidth*2 : cellWidth - height: parent.height= 0 && local_x < handle) { - let ppos = mapFromItem(item, 0, 0) - let item_row = item.modelIndex().row - if(item_row) { - dragBothLeft.x = ppos.x -dragBothLeft.width / 2 - dragBothLeft.y = ppos.y - show_dragBothLeft = true - } else { - dragLeft.x = ppos.x - dragLeft.y = ppos.y - show_dragLeft = true - } - modelIndex = item.modelIndex() - } - else if(local_x >= item.width - handle && local_x < item.width) { - let ppos = mapFromItem(item, item.width, 0) - let item_row = item.modelIndex().row - if(item_row == item.modelIndex().model.rowCount(item.modelIndex().parent)-1) { - dragRight.x = ppos.x - dragRight.width - dragRight.y = ppos.y - show_dragRight = true - modelIndex = item.modelIndex().parent - } else { - dragBothRight.x = ppos.x -dragBothRight.width / 2 - dragBothRight.y = ppos.y - show_dragBothRight = true - modelIndex = item.modelIndex().model.index(item_row+1,0,item.modelIndex().parent) - } - } - } else if(["Audio Track","Video Track"].includes(item_type)) { - let ppos = mapFromItem(item, trackHeaderWidth, 0) - dragRight.x = ppos.x - dragRight.width - dragRight.y = ppos.y - show_dragRight = true - modelIndex = item.modelIndex() - } - } - - if(show_dragLeft != dragLeft.visible) - dragLeft.visible = show_dragLeft - - if(show_dragRight != dragRight.visible) - dragRight.visible = show_dragRight - - if(show_dragBothLeft != dragBothLeft.visible) - dragBothLeft.visible = show_dragBothLeft - - if(show_dragBothRight != dragBothRight.visible) - dragBothRight.visible = show_dragBothRight - - if(show_dragAvailable != dragAvailable.visible) - dragAvailable.visible = show_dragAvailable - - if(show_moveClip != moveClip.visible) - moveClip.visible = show_moveClip - } - - onPositionChanged: { - processPosition(drag.x, drag.y) - } - - onDropped: { - processPosition(drop.x, drop.y) - if(modelIndex != null) { - handleDrop(modelIndex, drop) - modelIndex = null - } - dragAvailable.visible = false - dragBothLeft.visible = false - dragBothRight.visible = false - dragLeft.visible = false - moveClip.visible = false - dragRight.visible = false - } - } - - Keys.onReleased: { - if(event.key == Qt.Key_U && event.modifiers == Qt.ControlModifier) { - // UNDO - undo(viewedMediaSetProperties.index); - event.accepted = true - } else if(event.key == Qt.Key_Z && event.modifiers == Qt.ControlModifier) { - // REDO - redo(viewedMediaSetProperties.index); - event.accepted = true - } - } - - - Item { - id: dragContainer - anchors.fill: parent - // anchors.topMargin: 20 - - property alias dragged_items: dragged_items - - ItemSelectionModel { - id: dragged_items - } - - Drag.active: moveDragHandler.active - Drag.dragType: Drag.Automatic - Drag.supportedActions: Qt.CopyAction - - function startDrag(mode) { - dragContainer.Drag.supportedActions = mode - let indexs = timeline.timelineSelection.selectedIndexes - - dragged_items.model = timeline.timelineSelection.model - dragged_items.select( - helpers.createItemSelection(timeline.timelineSelection.selectedIndexes), - ItemSelectionModel.ClearAndSelect - ) - - let ids = [] - - // order by row not selection order.. - - for(let i=0;i a[0] - b[0] ) - for(let i=0;i 0) { - theSessionData.insertTimelineGap(mindex.row+1, mindex.parent, resizeItem.adjustAnteceedingGap, resizeItem.fps, "New Gap") - } - resizeItem.adjustAnteceedingGap = 0 - } - - } else if(dragLeft.visible) { - let mindex = resizeItem.modelIndex() - let src_model = mindex.model - src_model.set(mindex, resizeItem.startFrame, "activeStartRole") - src_model.set(mindex, resizeItem.durationFrame, "activeDurationRole") - resizeItem.isAdjustingStart = false - resizeItem.isAdjustingDuration = false - - if(resizePreceedingItem) { - if(resizePreceedingItem.durationFrame == 0) { - theSessionData.removeTimelineItems([resizePreceedingItem.modelIndex()]) - resizePreceedingItem = null - } else { - src_model.set(resizePreceedingItem.modelIndex(), resizePreceedingItem.durationFrame, "activeDurationRole") - src_model.set(resizePreceedingItem.modelIndex(), resizePreceedingItem.durationFrame, "availableDurationRole") - resizePreceedingItem.isAdjustingDuration = false - } - } else { - if(resizeItem.adjustPreceedingGap > 0) { - theSessionData.insertTimelineGap(mindex.row, mindex.parent, resizeItem.adjustPreceedingGap, resizeItem.fps, "New Gap") - } - resizeItem.adjustPreceedingGap = 0 - } - } else if(dragAvailable.visible) { - let src_model = resizeItem.modelIndex().model - src_model.set(resizeItem.modelIndex(), resizeItem.startFrame, "activeStartRole") - resizeItem.isAdjustingStart = false - } else if(dragBothLeft.visible) { - let mindex = resizeItem.modelIndex() - let src_model = mindex.model - src_model.set(mindex, resizeItem.startFrame, "activeStartRole") - src_model.set(mindex, resizeItem.durationFrame, "activeDurationRole") - - if(resizePreceedingItem) { - let pindex = src_model.index(mindex.row-1, 0, mindex.parent) - src_model.set(pindex, resizePreceedingItem.durationFrame, "activeDurationRole") - } - resizeItem.isAdjustingStart = false - resizeItem.isAdjustingDuration = false - } else if(dragBothRight.visible) { - let mindex = resizeItem.modelIndex() - let src_model = mindex.model - src_model.set(mindex, resizeItem.durationFrame, "activeDurationRole") - - let pindex = src_model.index(mindex.row + 1, 0, mindex.parent) - src_model.set(pindex, resizeAnteceedingItem.startFrame, "activeStartRole") - src_model.set(pindex, resizeAnteceedingItem.durationFrame, "activeDurationRole") - - resizeItem.isAdjustingDuration = false - } else if(moveClip.visible) { - let mindex = resizeItem.modelIndex() - let src_model = mindex.model - - if(resizePreceedingItem && resizePreceedingItem.durationFrame) { - src_model.set(resizePreceedingItem.modelIndex(), resizePreceedingItem.durationFrame, "activeDurationRole") - src_model.set(resizePreceedingItem.modelIndex(), resizePreceedingItem.durationFrame, "availableDurationRole") - } - - if(resizeAnteceedingItem && resizeAnteceedingItem.durationFrame) { - src_model.set(resizeAnteceedingItem.modelIndex(), resizeAnteceedingItem.durationFrame, "activeDurationRole") - src_model.set(resizeAnteceedingItem.modelIndex(), resizeAnteceedingItem.durationFrame, "availableDurationRole") - } - - let delete_preceeding = resizePreceedingItem && !resizePreceedingItem.durationFrame - let delete_anteceeding = resizeAnteceedingItem && !resizeAnteceedingItem.durationFrame - let insert_preceeding = resizeItem.isAdjustPreceeding && resizeItem.adjustPreceedingGap - let insert_anteceeding = resizeItem.isAdjustAnteceeding && resizeItem.adjustAnteceedingGap - - // some operations are moves - if(insert_preceeding && delete_anteceeding) { - // move clip left - moveItem(resizeItem.modelIndex(), 1) - } else if (delete_preceeding && insert_anteceeding) { - moveItem(resizeItem.modelIndex(), -1) - } else { - if(delete_preceeding) { - theSessionData.removeTimelineItems([resizePreceedingItem.modelIndex()]) - } - - if(delete_anteceeding) { - theSessionData.removeTimelineItems([resizeAnteceedingItem.modelIndex()]) - } - - if(insert_preceeding) { - theSessionData.insertTimelineGap(mindex.row, mindex.parent, resizeItem.adjustPreceedingGap, resizeItem.fps, "New Gap") - } - - if(insert_anteceeding) { - theSessionData.insertTimelineGap(mindex.row + 1, mindex.parent, resizeItem.adjustAnteceedingGap, resizeItem.fps, "New Gap") - } - } - - resizeItem.adjustPreceedingGap = 0 - resizeItem.isAdjustPreceeding = false - resizeItem.adjustAnteceedingGap = 0 - resizeItem.isAdjustAnteceeding = false - - } - - if(resizePreceedingItem) { - resizePreceedingItem.isAdjustingStart = false - resizePreceedingItem.isAdjustingDuration = false - } - - if(resizeAnteceedingItem) { - resizeAnteceedingItem.isAdjustingStart = false - resizeAnteceedingItem.isAdjustingDuration = false - } - - resizeItem = null - } - - resizeAnteceedingItem = null - resizePreceedingItem = null - isResizing = false - dragLeft.visible = false - dragRight.visible = false - dragBothLeft.visible = false - moveClip.visible = false - dragBothRight.visible = false - dragAvailable.visible = false - } else { - moveDragHandler.enabled = false - } - } - - onPressed: { - if(mouse.button == Qt.RightButton) { - adjustSelection(mouse) - timelineMenu.x = mouse.x//*2 - timelineMenu.y = mouse.y-timelineMenu.height - timelineMenu.visible = true - - } else if(mouse.button == Qt.LeftButton) { - adjustSelection(mouse) - } - - if(dragLeft.visible || dragRight.visible || dragBothLeft.visible || dragBothRight.visible || dragAvailable.visible || moveClip.visible) { - let [item, item_type, local_x, local_y] = resolveItem(mouse.x, mouse.y) - resizeItem = item - resizeItemStartX = mouse.x - resizeItemType = item_type - isResizing = true - if(dragLeft.visible) { - resizeItem.adjustDuration = 0 - resizeItem.adjustStart = 0 - resizeItem.isAdjustingDuration = true - resizeItem.isAdjustingStart = true - // is there a gap to our left.. - let mi = resizeItem.modelIndex() - let pre_index = preceedingIndex(mi) - if(pre_index.valid) { - let preceeding_type = pre_index.model.get(pre_index, "typeRole") - - if(preceeding_type == "Gap") { - resizePreceedingItem = resizeItem.parentLV.itemAtIndex(mi.row - 1) - resizePreceedingItem.adjustDuration = 0 - resizePreceedingItem.isAdjustingDuration = true - } - } - } else if(dragRight.visible) { - resizeItem.adjustDuration = 0 - resizeItem.isAdjustingDuration = true - - let mi = resizeItem.modelIndex() - let ante_index = anteceedingIndex(mi) - if(ante_index.valid) { - let anteceeding_type = ante_index.model.get(ante_index, "typeRole") - - if(anteceeding_type == "Gap") { - resizeAnteceedingItem = resizeItem.parentLV.itemAtIndex(mi.row + 1) - resizeAnteceedingItem.adjustDuration = 0 - resizeAnteceedingItem.isAdjustingDuration = true - } - } - } else if(dragAvailable.visible) { - resizeItem.adjustStart = 0 - resizeItem.isAdjustingStart = true - } else if(dragBothLeft.visible) { - // both at front or end..? - let mi = resizeItem.modelIndex() - resizeItem.adjustDuration = 0 - resizeItem.adjustStart = 0 - resizeItem.isAdjustingStart = true - resizeItem.isAdjustingDuration = true - - resizePreceedingItem = resizeItem.parentLV.itemAtIndex(mi.row - 1) - resizePreceedingItem.adjustDuration = 0 - resizePreceedingItem.isAdjustingDuration = true - } else if(dragBothRight.visible) { - // both at front or end..? - let mi = resizeItem.modelIndex() - resizeItem.adjustDuration = 0 - resizeItem.isAdjustingDuration = true - - resizeAnteceedingItem = resizeItem.parentLV.itemAtIndex(mi.row + 1) - resizeAnteceedingItem.adjustStart = 0 - resizeAnteceedingItem.adjustDuration = 0 - resizeAnteceedingItem.isAdjustingStart = true - resizeAnteceedingItem.isAdjustingDuration = true - } else if(moveClip.visible) { - // we adjust material either side of us.. - let mi = resizeItem.modelIndex() - let prec_index = preceedingIndex(mi) - let ante_index = anteceedingIndex(mi) - - let preceeding_type = prec_index.valid ? prec_index.model.get(prec_index, "typeRole") : "Track" - let anteceeding_type = ante_index.valid ? ante_index.model.get(ante_index, "typeRole") : "Track" - - if(preceeding_type == "Gap") { - resizePreceedingItem = resizeItem.parentLV.itemAtIndex(mi.row - 1) - resizePreceedingItem.adjustDuration = 0 - resizePreceedingItem.isAdjustingDuration = true - } else { - resizeItem.adjustPreceedingGap = 0 - resizeItem.isAdjustPreceeding = true - } - - if(anteceeding_type == "Gap") { - resizeAnteceedingItem = resizeItem.parentLV.itemAtIndex(mi.row + 1) - resizeAnteceedingItem.adjustDuration = 0 - resizeAnteceedingItem.isAdjustingDuration = true - } else if(anteceeding_type != "Track") { - resizeItem.adjustAnteceedingGap = 0 - resizeItem.isAdjustAnteceeding = true - } - } - } else { - let [item, item_type, local_x, local_y] = resolveItem(mouse.x, mouse.y) - if(item_type != null && item_type != "Stack" && timelineSelection.isSelected(item.modelIndex())) { - moveDragHandler.enabled = true - } - } - } - - onPositionChanged: { - if(isResizing) { - let frame_change = -((resizeItemStartX - mouse.x) / scaleX) - - if(dragRight.visible) { - - frame_change = resizeItem.checkAdjust(frame_change, true) - if(resizeAnteceedingItem) { - frame_change = -resizeAnteceedingItem.checkAdjust(-frame_change, false) - resizeAnteceedingItem.adjust(-frame_change) - } else { - resizeItem.adjustAnteceedingGap = -frame_change - } - - resizeItem.adjust(frame_change) - - let ppos = mapFromItem(resizeItem, resizeItem.width - (resizeItem.adjustAnteceedingGap * scaleX) - dragRight.width, 0) - dragRight.x = ppos.x - } else if(dragLeft.visible) { - // must inject / resize gap. - // make sure last frame doesn't change.. - frame_change = resizeItem.checkAdjust(frame_change, false, true) - if(resizePreceedingItem) { - frame_change = resizePreceedingItem.checkAdjust(frame_change, false) - resizePreceedingItem.adjust(frame_change) - } else { - resizeItem.adjustPreceedingGap = frame_change - } - - resizeItem.adjust(frame_change) - - let ppos = mapFromItem(resizeItem, resizeItem.adjustPreceedingGap * scaleX, 0) - dragLeft.x = ppos.x - } else if(dragBothLeft.visible) { - frame_change = resizeItem.checkAdjust(frame_change, true) - frame_change = resizePreceedingItem.checkAdjust(frame_change, true) - - resizeItem.adjust(frame_change) - resizePreceedingItem.adjust(frame_change) - - let ppos = mapFromItem(resizeItem, -dragBothLeft.width / 2, 0) - dragBothLeft.x = ppos.x - } else if(dragBothRight.visible) { - frame_change = resizeItem.checkAdjust(frame_change, true) - frame_change = resizeAnteceedingItem.checkAdjust(frame_change, true) - - resizeItem.adjust(frame_change) - resizeAnteceedingItem.adjust(frame_change) - - let ppos = mapFromItem(resizeItem, resizeItem.width - dragBothRight.width / 2, 0) - dragBothRight.x = ppos.x - } else if(dragAvailable.visible) { - resizeItem.updateStart(resizeItemStartX, mouse.x) - } else if(moveClip.visible) { - if(resizePreceedingItem) - frame_change = resizePreceedingItem.checkAdjust(frame_change, false) - else - frame_change = Math.max(0, frame_change) - - if(resizeAnteceedingItem) - frame_change = -resizeAnteceedingItem.checkAdjust(-frame_change, false) - // else - // frame_change = Math.max(0, frame_change) - - if(resizePreceedingItem) - resizePreceedingItem.adjust(frame_change) - else if(resizeItem.isAdjustPreceeding) - resizeItem.adjustPreceedingGap = frame_change - - if(resizeAnteceedingItem) - resizeAnteceedingItem.adjust(-frame_change) - else if(resizeItem.isAdjustAnteceeding) - resizeItem.adjustAnteceedingGap = -frame_change - - let ppos = mapFromItem(resizeItem, resizeItem.width / 2 - moveClip.width / 2, 0) - moveClip.x = ppos.x - } - } else { - let [item, item_type, local_x, local_y] = resolveItem(mouse.x, mouse.y) - - if(hovered != item) { - // console.log(item,item.modelIndex(), item_type, local_x, local_y) - hovered = item - } - - let show_dragLeft = false - let show_dragRight = false - let show_dragBothLeft = false - let show_moveClip = false - let show_dragBothRight = false - let show_dragAvailable = false - let handle = 32 - - if(hovered) { - if("Clip" == item_type) { - - let preceeding_type = "Track" - let anteceeding_type = "Track" - - let mi = item.modelIndex() - - let ante_index = anteceedingIndex(mi) - let pre_index = preceedingIndex(mi) - - if(ante_index.valid) - anteceeding_type = ante_index.model.get(ante_index, "typeRole") - - if(pre_index.valid) - preceeding_type = pre_index.model.get(pre_index, "typeRole") - - // expand left - let left = local_x <= (handle * 1.5) && local_x >= 0 - let left_edge = left && local_x < (handle / 2) - let right = local_x >= hovered.width - (1.5 * handle) && local_x < hovered.width - let right_edge = right && local_x > hovered.width - (handle / 2) - let middle = local_x >= (hovered.width/2) - (handle / 2) && local_x <= (hovered.width/2) + (handle / 2) - - if(preceeding_type == "Clip" && left_edge) { - let ppos = mapFromItem(item, -dragBothLeft.width / 2, 0) - dragBothLeft.x = ppos.x - dragBothLeft.y = ppos.y - show_dragBothLeft = true - item.parentLV.itemAtIndex(mi.row - 1).isBothHovered = true - } else if(left) { - let ppos = mapFromItem(item, 0, 0) - dragLeft.x = ppos.x - dragLeft.y = ppos.y - show_dragLeft = true - if(preceeding_type == "Clip") - item.parentLV.itemAtIndex(mi.row - 1).isBothHovered = false - } else if(anteceeding_type == "Clip" && right_edge) { - let ppos = mapFromItem(item, hovered.width - dragBothRight.width/2, 0) - dragBothRight.x = ppos.x - dragBothRight.y = ppos.y - show_dragBothRight = true - item.parentLV.itemAtIndex(mi.row + 1).isBothHovered = true - } else if(right) { - let ppos = mapFromItem(item, hovered.width - dragRight.width, 0) - dragRight.x = ppos.x - dragRight.y = ppos.y - show_dragRight = true - if(anteceeding_type == "Clip") - item.parentLV.itemAtIndex(mi.row + 1).isBothHovered = false - } else if(middle && (preceeding_type != "Clip" || anteceeding_type != "Clip") && !(preceeding_type == "Track" && anteceeding_type == "Clip")) { - let ppos = mapFromItem(item, hovered.width / 2, hovered.height / 2) - moveClip.x = ppos.x - moveClip.width / 2 - moveClip.y = ppos.y - moveClip.height / 2 - show_moveClip = true - } else if("Clip" == item_type && local_y >= 0 && local_y <= 8) { - // available range.. - let ppos = mapFromItem(item, hovered.width / 2, 0) - dragAvailable.x = ppos.x -dragAvailable.width / 2 - dragAvailable.y = ppos.y - dragAvailable.height / 2 - show_dragAvailable = true - } - } - } - - if(show_dragLeft != dragLeft.visible) - dragLeft.visible = show_dragLeft - - if(show_dragRight != dragRight.visible) - dragRight.visible = show_dragRight - - if(show_dragBothLeft != dragBothLeft.visible) - dragBothLeft.visible = show_dragBothLeft - - if(show_moveClip != moveClip.visible) - moveClip.visible = show_moveClip - - if(show_dragBothRight != dragBothRight.visible) - dragBothRight.visible = show_dragBothRight - - if(show_dragAvailable != dragAvailable.visible) - dragAvailable.visible = show_dragAvailable - } - } - - onWheel: { - // maintain position as we zoom.. - if(wheel.modifiers == Qt.ShiftModifier) { - if(wheel.angleDelta.y > 1) { - scaleX += 0.2 - scaleY += 0.2 - } else { - scaleX -= 0.2 - scaleY -= 0.2 - } - wheel.accepted = true - // console.log(wheel.x, wheel.y) - } else if(wheel.modifiers == Qt.ControlModifier) { - if(wheel.angleDelta.y > 1) { - scaleX += 0.2 - } else { - scaleX -= 0.2 - } - wheel.accepted = true - } else if(wheel.modifiers == (Qt.ControlModifier | Qt.ShiftModifier)) { - if(wheel.angleDelta.y > 1) { - scaleY += 0.2 - } else { - scaleY -= 0.2 - } - wheel.accepted = true - } else { - wheel.accepted = false - } - - - if(wheel.accepted) { - list_view.itemAtIndex(0).jumpToFrame(viewport.playhead.frame, ListView.Center) - // let current_frame = list_view.itemAtIndex(0).currentFrame() - // jumpToFrame(viewport.playhead.frame, false) - } - } - - Connections { - target: timeline - function onJumpToStart() { - list_view.itemAtIndex(0).jumpToStart() - } - function onJumpToEnd() { - list_view.itemAtIndex(0).jumpToEnd() - } - } - - ListView { - anchors.fill: parent - interactive: false - id:list_view - model: timeline_items - orientation: ListView.Horizontal - - property var timelineItem: timeline - property var hoveredItem: hovered - property real scaleX: timeline.scaleX - property real scaleY: timeline.scaleY - property real itemHeight: timeline.itemHeight - property real trackHeaderWidth: timeline.trackHeaderWidth - property var setTrackHeaderWidth: timeline.setTrackHeaderWidth - property var timelineSelection: timeline.timelineSelection - property var timelineFocusSelection: timeline.timelineFocusSelection - property int playheadFrame: playheadLogicalFrame ? playheadLogicalFrame : 0 - property string itemFlag: "" - - } - } - } -} \ No newline at end of file diff --git a/ui/qml/reskin/views/timeline/data/XsSortFilterModel.qml b/ui/qml/reskin/views/timeline/data/XsSortFilterModel.qml deleted file mode 100644 index e73e76dba..000000000 --- a/ui/qml/reskin/views/timeline/data/XsSortFilterModel.qml +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.9 -import QtQml.Models 2.14 - -import xStudio 1.0 - -DelegateModel { - id: delegateModel - - property var srcModel: null - property var lessThan: function(left, right) { return true; } - property var filterAcceptsItem: function(item) { return true; } - - onSrcModelChanged: model = srcModel - - signal updated() - - function update() { - hiddenItems.setGroups(0, hiddenItems.count, "unsorted") - items.setGroups(0, items.count, "unsorted") - } - - function insertPosition(lessThan, item) { - let lower = 0 - let upper = items.count - while (lower < upper) { - const middle = Math.floor(lower + (upper - lower) / 2) - const result = lessThan(item.model, - items.get(middle).model) - if (result) { - upper = middle - } else { - lower = middle + 1 - } - } - return lower - } - - function sort(lessThan) { - while (unsortedItems.count > 0) { - const item = unsortedItems.get(0) - - if(!filterAcceptsItem(item.model)) { - item.groups = "hidden" - } else { - const index = insertPosition(lessThan, item) - item.groups = "items" - items.move(item.itemsIndex, index) - } - } - } - - items.includeByDefault: false - groups: [ - DelegateModelGroup { - id: unsortedItems - name: "unsorted" - - includeByDefault: true - - onChanged: { - delegateModel.sort(delegateModel.lessThan) - updated() - } - }, - DelegateModelGroup { - id: hiddenItems - name: "hidden" - - includeByDefault: false - } - ] -} - - -// // SPDX-License-Identifier: Apache-2.0 -// import QtQuick 2.9 -// import QtQml.Models 2.14 - -// import xStudio 1.0 - -// DelegateModel { -// id: delegateModel - -// property var srcModel: null -// property var lessThan: function(left, right) { return true; } -// property var filterAcceptsItem: function(item) { return true; } - -// onSrcModelChanged: model = srcModel - -// signal updated() - -// function update() { -// if (items.count > 0) { -// items.setGroups(0, items.count, "items"); -// } - -// // Step 1: Filter items -// var ivisible = []; -// for (var i = 0; i < items.count; ++i) { -// var item = items.get(i); -// if (filterAcceptsItem(item.model)) { -// ivisible.push(item); -// } -// } - -// // Step 2: Sort the list of visible items -// ivisible.sort(function(a, b) { -// return lessThan(a.model, b.model) ? -1 : 1; -// }); - - -// // Step 3: Add all items to the visible group: -// for (i = 0; i < ivisible.length; ++i) { -// item = ivisible[i]; -// item.inIvisible = true; -// if (item.ivisibleIndex !== i) { -// visibleItems.move(item.ivisibleIndex, i, 1); -// } -// } -// updated() -// } - -// items.onChanged: update() -// onLessThanChanged: update() -// onFilterAcceptsItemChanged: update() - -// groups: DelegateModelGroup { -// id: visibleItems - -// name: "ivisible" -// includeByDefault: false -// } - -// filterOnGroup: "ivisible" -// } \ No newline at end of file diff --git a/ui/qml/reskin/views/timeline/delegates/XsDelegateAudioTrack.qml b/ui/qml/reskin/views/timeline/delegates/XsDelegateAudioTrack.qml deleted file mode 100644 index 5993475b9..000000000 --- a/ui/qml/reskin/views/timeline/delegates/XsDelegateAudioTrack.qml +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 -import QtGraphicalEffects 1.0 -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudioReskin 1.0 -import xstudio.qml.module 1.0 -import xstudio.qml.helpers 1.0 - -DelegateChoice { - roleValue: "Audio Track" - - Component { - Rectangle { - id: control - - color: timelineBackground - property real scaleX: ListView.view.scaleX - property real scaleY: ListView.view.scaleY - property real itemHeight: ListView.view.itemHeight - property real trackHeaderWidth: ListView.view.trackHeaderWidth - property real cX: ListView.view.cX - property real parentWidth: ListView.view.parentWidth - property var timelineItem: ListView.view.timelineItem - property string itemFlag: flagColourRole != "" ? flagColourRole : ListView.view.itemFlag - property var parentLV: ListView.view - readonly property bool extraDetail: height > 60 - property var setTrackHeaderWidth: ListView.view.setTrackHeaderWidth - - width: ListView.view.width - height: itemHeight * scaleY - - opacity: enabledRole ? 1.0 : 0.2 - - property bool isHovered: hoveredItem == control - property bool isSelected: false - property var timelineSelection: ListView.view.timelineSelection - property var timelineFocusSelection: ListView.view.timelineFocusSelection - property var hoveredItem: ListView.view.hoveredItem - property var itemTypeRole: typeRole - property alias list_view: list_view - - function modelIndex() { - return control.DelegateModel.model.srcModel.index( - index, 0, control.DelegateModel.model.rootIndex - ) - } - - Connections { - target: timelineSelection - function onSelectionChanged(selected, deselected) { - if(isSelected && helpers.itemSelectionContains(deselected, modelIndex())) - isSelected = false - else if(!isSelected && helpers.itemSelectionContains(selected, modelIndex())) - isSelected = true - } - } - - DelegateChooser { - id: chooser - role: "typeRole" - - XsDelegateClip {} - XsDelegateGap {} - } - - DelegateModel { - id: track_items - property var srcModel: theSessionData - model: srcModel - rootIndex: helpers.makePersistent(control.DelegateModel.model.srcModel.index( - index, 0, control.DelegateModel.model.rootIndex - )) - delegate: chooser - } - - XsTrackHeader { - id: track_header - z: 2 - width: trackHeaderWidth - height: Math.ceil(control.itemHeight * control.scaleY) - anchors.top: parent.top - anchors.left: parent.left - - isHovered: control.isHovered - itemFlag: control.itemFlag - trackIndex: trackIndexRole - setTrackHeaderWidth: control.setTrackHeaderWidth - text: nameRole - title: "Audio Track" - isEnabled: enabledRole - onEnabledClicked: enabledRole = !enabledRole - } - - Flickable { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: track_header.right - anchors.right: parent.right - - contentWidth: Math.ceil(trimmedDurationRole * control.scaleX) - contentHeight: Math.ceil(control.itemHeight * control.scaleY) - contentX: control.cX - - interactive: false - - Row { - id:list_view - - property real scaleX: control.scaleX - property real scaleY: control.scaleY - property real itemHeight: control.itemHeight - property var timelineSelection: control.timelineSelection - property var timelineFocusSelection: control.timelineFocusSelection - property var timelineItem: control.timelineItem - property var hoveredItem: control.hoveredItem - property real trackHeaderWidth: control.trackHeaderWidth - property string itemFlag: control.itemFlag - property var itemAtIndex: item_repeater.itemAt - - Repeater { - id: item_repeater - model: track_items - } - } - } - } - } -} diff --git a/ui/qml/reskin/views/timeline/delegates/XsDelegateClip.qml b/ui/qml/reskin/views/timeline/delegates/XsDelegateClip.qml deleted file mode 100644 index 642d43e1e..000000000 --- a/ui/qml/reskin/views/timeline/delegates/XsDelegateClip.qml +++ /dev/null @@ -1,217 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 -import QtGraphicalEffects 1.0 -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudioReskin 1.0 -import xstudio.qml.module 1.0 -import xstudio.qml.helpers 1.0 - -DelegateChoice { - roleValue: "Clip" - - Component { - RowLayout { - id: control - spacing: 0 - - property var config: ListView.view || control.parent - - width: (durationFrame + adjustPreceedingGap + adjustAnteceedingGap) * config.scaleX - height: config.scaleY * config.itemHeight - - property bool isAdjustPreceeding: false - property bool isAdjustAnteceeding: false - - property int adjustPreceedingGap: 0 - property int adjustAnteceedingGap: 0 - - property bool isBothHovered: false - - property bool isAdjustingStart: false - property int adjustStart: 0 - property int startFrame: isAdjustingStart ? trimmedStartRole + adjustStart : trimmedStartRole - - property bool isAdjustingDuration: false - property int adjustDuration: 0 - property int durationFrame: isAdjustingDuration ? trimmedDurationRole + adjustDuration : trimmedDurationRole - property int currentStartRole: trimmedStartRole - property real fps: rateFPSRole - - property var timelineFocusSelection: config.timelineFocusSelection - property var timelineSelection: config.timelineSelection - property var timelineItem: config.timelineItem - property var itemTypeRole: typeRole - property var hoveredItem: config.hoveredItem - property var scaleX: config.scaleX - property var parentLV: config - property string itemFlag: flagColourRole != "" ? flagColourRole : config.itemFlag - - property bool hasMedia: mediaIndex.valid - property var mediaIndex: control.DelegateModel.model.srcModel.index(-1,-1, control.DelegateModel.model.rootIndex) - - onHoveredItemChanged: isBothHovered = false - - function modelIndex() { - return control.DelegateModel.model.srcModel.index( - index, 0, control.DelegateModel.model.rootIndex - ) - } - - function adjust(offset) { - let doffset = offset - if(isAdjustingStart) { - adjustStart = offset - doffset = -doffset - } - if(isAdjustingDuration) { - adjustDuration = doffset - } - } - - function checkAdjust(offset, lock_duration=false, lock_end=false) { - let doffset = offset - - if(isAdjustingStart) { - let tmp = Math.min( - availableStartRole+availableDurationRole-1, - Math.max(trimmedStartRole + offset, availableStartRole) - ) - - if(lock_end && tmp > trimmedStartRole+trimmedDurationRole) { - tmp = trimmedStartRole+trimmedDurationRole-1 - } - - if(trimmedStartRole != tmp-offset) { - return checkAdjust(tmp-trimmedStartRole) - } - - // if adjusting duration as well - doffset = -doffset - } - - if(isAdjustingDuration && lock_duration) { - let tmp = Math.max( - 1, - Math.min(trimmedDurationRole + doffset, availableDurationRole - (startFrame-availableStartRole) ) - ) - - if(trimmedDurationRole != tmp-doffset) { - if(isAdjustingStart) - return checkAdjust(-(tmp-trimmedDurationRole)) - else - return checkAdjust(tmp-trimmedDurationRole) - } - } - - return offset - } - - - function updateStart(startX, x) { - let tmp = - (startX - x) * ((availableDurationRole - activeDurationRole) / width) - adjustStart = Math.floor(Math.min( - Math.max(trimmedStartRole + tmp, availableStartRole), - availableStartRole + availableDurationRole - trimmedDurationRole - ) - trimmedStartRole) - } - - - - XsGapItem { - visible: adjustPreceedingGap != 0 - Layout.preferredWidth: adjustPreceedingGap * scaleX - Layout.fillHeight: true - start: 0 - duration: adjustPreceedingGap - } - - XsClipItem { - id: clip - - Layout.preferredWidth: durationFrame * scaleX - Layout.fillHeight: true - - isHovered: hoveredItem == control || isAdjustingStart || isAdjustingDuration || isBothHovered - start: startFrame - duration: durationFrame - isEnabled: enabledRole && hasMedia - fps: control.fps - name: nameRole - parentStart: parentStartRole - availableStart: availableStartRole - availableDuration: availableDurationRole - primaryColor: itemFlag != "" ? itemFlag : defaultClip - mediaFlagColour: mediaFlag.value == undefined || mediaFlag.value == "" ? "transparent" : mediaFlag.value - - - XsModelProperty { - id: mediaFlag - role: "flagColourRole" - index: mediaIndex - } - - Component.onCompleted: { - checkMedia() - } - - function checkMedia() { - let model = control.DelegateModel.model.srcModel - let tindex = model.getTimelineIndex(control.DelegateModel.model.rootIndex) - let mlist = model.index(0, 0, tindex) - mediaIndex = model.search(clipMediaUuidRole, "actorUuidRole", mlist) - } - - Connections { - target: dragContainer.dragged_items - function onSelectionChanged() { - if(dragContainer.dragged_items.selectedIndexes.length) { - if(dragContainer.dragged_items.isSelected(modelIndex())) { - if(dragContainer.Drag.supportedActions == Qt.CopyAction) - clip.isCopying = true - else - clip.isMoving = true - } - } else { - clip.isMoving = false - clip.isCopying = false - } - } - } - - Connections { - target: control.timelineSelection - function onSelectionChanged(selected, deselected) { - if(clip.isSelected && helpers.itemSelectionContains(deselected, modelIndex())) - clip.isSelected = false - else if(!clip.isSelected && helpers.itemSelectionContains(selected, modelIndex())) - clip.isSelected = true - } - } - - Connections { - target: control.timelineFocusSelection - function onSelectionChanged(selected, deselected) { - if(clip.isFocused && helpers.itemSelectionContains(deselected, modelIndex())) - clip.isFocused = false - else if(!clip.isFocused && helpers.itemSelectionContains(selected, modelIndex())) - clip.isFocused = true - } - } - } - - XsGapItem { - visible: adjustAnteceedingGap != 0 - Layout.preferredWidth: adjustAnteceedingGap * scaleX - Layout.fillHeight: true - start: 0 - duration: adjustAnteceedingGap - } - } - } -} diff --git a/ui/qml/reskin/views/timeline/delegates/XsDelegateGap.qml b/ui/qml/reskin/views/timeline/delegates/XsDelegateGap.qml deleted file mode 100644 index d50f867bc..000000000 --- a/ui/qml/reskin/views/timeline/delegates/XsDelegateGap.qml +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 -import QtGraphicalEffects 1.0 -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudioReskin 1.0 -import xstudio.qml.module 1.0 -import xstudio.qml.helpers 1.0 - -DelegateChoice { - roleValue: "Gap" - - Component { - XsGapItem { - id: control - - property var config: ListView.view || control.parent - - width: durationFrame * config.scaleX - height: config.scaleY * config.itemHeight - - isHovered: hoveredItem == control - - start: startFrame - duration: durationFrame - fps: rateFPSRole - name: nameRole - parentStart: parentStartRole - isEnabled: enabledRole - - property int adjustDuration: 0 - property bool isAdjustingDuration: false - property int adjustStart: 0 - property bool isAdjustingStart: false - property int durationFrame: isAdjustingDuration ? trimmedDurationRole + adjustDuration : trimmedDurationRole - property int startFrame: isAdjustingStart ? trimmedStartRole + adjustStart : trimmedStartRole - property var itemTypeRole: typeRole - - property var timelineSelection: config.timelineSelection - property var timelineFocusSelection: config.timelineFocusSelection - property var timelineItem: config.timelineItem - property var parentLV: config - property var hoveredItem: config.hoveredItem - - function adjust(offset) { - adjustDuration = offset - } - - // we only ever adjust duration - function checkAdjust(offset) { - let tmp = Math.max(0, trimmedDurationRole + offset) - - if(trimmedDurationRole != tmp-offset) { - // console.log("duration limited", trimmedDurationRole, tmp-doffset) - return checkAdjust(tmp-trimmedDurationRole) - } - - return offset - } - - function modelIndex() { - return control.DelegateModel.model.srcModel.index( - index, 0, control.DelegateModel.model.rootIndex - ) - } - - Connections { - target: timelineSelection - function onSelectionChanged(selected, deselected) { - if(isSelected && helpers.itemSelectionContains(deselected, modelIndex())) - isSelected = false - else if(!isSelected && helpers.itemSelectionContains(selected, modelIndex())) - isSelected = true - } - } - } - } -} diff --git a/ui/qml/reskin/views/timeline/delegates/XsDelegateStack.qml b/ui/qml/reskin/views/timeline/delegates/XsDelegateStack.qml deleted file mode 100644 index 612263713..000000000 --- a/ui/qml/reskin/views/timeline/delegates/XsDelegateStack.qml +++ /dev/null @@ -1,421 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 -import QtGraphicalEffects 1.0 -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudioReskin 1.0 -import xstudio.qml.module 1.0 -import xstudio.qml.helpers 1.0 - -DelegateChoice { - roleValue: "Stack" - - Component { - Rectangle { - id: control - - width: ListView.view.width - height: ListView.view.height - - property real myWidth: ((duration.value ? duration.value : 0) * scaleX) //+ trackHeaderWidth// + 10 - property real parentWidth: Math.max(ListView.view.width, myWidth + trackHeaderWidth) - - color: timelineBackground - - // needs to dynamicy resize badsed on listview.. - // in the mean time hack.. - - property real scaleX: ListView.view.scaleX - property real scaleY: ListView.view.scaleY - property real itemHeight: ListView.view.itemHeight - property real timelineHeaderHeight: itemHeight - property real trackHeaderWidth: ListView.view.trackHeaderWidth - property var setTrackHeaderWidth: ListView.view.setTrackHeaderWidth - - property string itemFlag: flagColourRole != "" ? flagColourRole : ListView.view.itemFlag - - opacity: enabledRole ? 1.0 : 0.2 - - property bool isSelected: false - property bool isHovered: hoveredItem == control - - property var timelineSelection: ListView.view.timelineSelection - property var timelineFocusSelection: ListView.view.timelineFocusSelection - property int playheadFrame: ListView.view.playheadFrame - property var timelineItem: ListView.view.timelineItem - property var hoveredItem: ListView.view.hoveredItem - - property var itemTypeRole: typeRole - property alias list_view_video: list_view_video - property alias list_view_audio: list_view_audio - - function modelIndex() { - return control.DelegateModel.model.srcModel.index( - index, 0, control.DelegateModel.model.rootIndex - ) - } - - // function viewStartFrame() { - // return trimmedStartRole + ((myWidth * hbar.position)/scaleX); - // } - - // function viewEndFrame() { - // return trimmedStartRole + ((myWidth * (hbar.position+hbar.size))/scaleX); - // } - - function jumpToStart() { - if(hbar.size<1.0) - hbar.position = 0.0 - } - - function jumpToEnd() { - if(hbar.size<1.0) - hbar.position = 1.0 - hbar.size - } - - - // ListView.Center - // ListView.Beginning - // ListView.End - // ListView.Visible - // ListView.Contain - // ListView.SnapPosition - - function jumpToFrame(frame, mode) { - if(hbar.size<1.0) { - let new_position = hbar.position - let first = ((frame - trimmedStartRole) * scaleX) / myWidth - - if(mode == ListView.Center) { - new_position = first - (hbar.size / 2) - } else if(mode == ListView.Beginning) { - new_position = first - } else if(mode == ListView.End) { - new_position = (first - hbar.size) - (2 * (1.0 / (trimmedDurationRole * scaleX))) - } else if(mode == ListView.Visible) { - // calculate frame as position. - if(first < new_position) { - new_position -= (hbar.size / 2) - } else if(first > (new_position + hbar.size)) { - // reposition - new_position += (hbar.size / 2) - } - } - - return hbar.position = Math.max(0, Math.min(new_position, 1.0 - hbar.size)) - } - return hbar.position - } - - Connections { - target: timelineSelection - function onSelectionChanged(selected, deselected) { - if(isSelected && helpers.itemSelectionContains(deselected, modelIndex())) - isSelected = false - else if(!isSelected && helpers.itemSelectionContains(selected, modelIndex())) - isSelected = true - } - } - - DelegateChooser { - id: chooser - role: "typeRole" - - XsDelegateClip {} - XsDelegateGap {} - XsDelegateAudioTrack {} - XsDelegateVideoTrack {} - } - - - XsSortFilterModel { - id: video_items - srcModel: theSessionData - rootIndex: helpers.makePersistent(control.DelegateModel.model.srcModel.index( - index, 0, control.DelegateModel.model.rootIndex - )) - delegate: chooser - - filterAcceptsItem: function(item) { - return item.typeRole == "Video Track" - } - - lessThan: function(left, right) { - return left.index > right.index - } - // onUpdated: console.log("video_items updated") - } - - XsSortFilterModel { - id: audio_items - srcModel: theSessionData - rootIndex: helpers.makePersistent(control.DelegateModel.model.srcModel.index( - index, 0, control.DelegateModel.model.rootIndex - )) - delegate: chooser - - filterAcceptsItem: function(item) { - return item.typeRole == "Audio Track" - } - - lessThan: function(left, right) { - return left.index < right.index - } - // onUpdated: console.log("audio_items updated") - } - - Connections { - target: theSessionData - - function onRowsMoved(parent, first, count, target, first) { - Qt.callLater(video_items.update) - Qt.callLater(audio_items.update) - } - } - - - // capture pointer to stack, so we can watch it's available size - XsModelProperty { - id: duration - role: "trimmedDurationRole" - index: control.DelegateModel.model.rootIndex - } - - XsTimelineCursor { - z:10 - anchors.left: parent.left - anchors.leftMargin: trackHeaderWidth - anchors.right: parent.right - anchors.top: parent.top - height: control.height - - tickWidth: tickWidget.tickWidth - secondOffset: tickWidget.secondOffset - fractionOffset: tickWidget.fractionOffset - start: tickWidget.start - duration: tickWidget.duration - fps: tickWidget.fps - position: playheadFrame - } - - ScrollBar { - id: hbar - hoverEnabled: true - active: hovered || pressed - orientation: Qt.Horizontal - - size: width / myWidth //(myWidth - trackHeaderWidth) - - // onSizeChanged: { - // console.log("size", size, "position", position, ) - // } - - anchors.left: parent.left - anchors.leftMargin: trackHeaderWidth - anchors.right: parent.right - anchors.bottom: parent.bottom - policy: size < 1.0 ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff - z:11 - } - - ColumnLayout { - id: splitView - anchors.fill: parent - spacing: 0 - - ColumnLayout { - id: topView - Layout.minimumWidth: parent.width - Layout.minimumHeight: (itemHeight * control.scaleY) * 2 - Layout.preferredHeight: parent.height*0.7 - spacing: 0 - - RowLayout { - spacing: 0 - Layout.preferredHeight: timelineHeaderHeight - Layout.fillWidth: true - - Rectangle { - color: trackBackground - Layout.preferredHeight: timelineHeaderHeight - Layout.preferredWidth: trackHeaderWidth - } - - Rectangle { - id: frameTrack - Layout.preferredHeight: timelineHeaderHeight - Layout.fillWidth: true - - // border.color: "black" - // border.width: 1 - color: trackBackground - - property real offset: hbar.position * myWidth - - XsTickWidget { - id: tickWidget - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - height: parent.height-4 - tickWidth: control.scaleX - secondOffset: (frameTrack.offset / control.scaleX) % rateFPSRole - fractionOffset: frameTrack.offset % control.scaleX - start: trimmedStartRole + (frameTrack.offset / control.scaleX) - duration: Math.ceil(width / control.scaleX) - fps: rateFPSRole - - onFramePressed: { - playheadLogicalFrame = frame - } - onFrameDragging:{ - playheadLogicalFrame = frame - } - } - } - } - - Rectangle { - color: trackEdge - Layout.fillHeight: true - Layout.fillWidth: true - - ListView { - id: list_view_video - anchors.fill: parent - - - spacing: 1 - - model: video_items - clip: true - interactive: false - // header: stack_header - // headerPositioning: ListView.OverlayHeader - verticalLayoutDirection: ListView.BottomToTop - - property real scaleX: control.scaleX - property real scaleY: control.scaleY - property real itemHeight: control.itemHeight - property var timelineSelection: control.timelineSelection - property var timelineFocusSelection: control.timelineFocusSelection - - property real cX: hbar.position * myWidth - property real parentWidth: control.parentWidth - property int playheadFrame: control.playheadFrame - property var timelineItem: control.timelineItem - property var hoveredItem: control.hoveredItem - property real trackHeaderWidth: control.trackHeaderWidth - property string itemFlag: control.itemFlag - property var setTrackHeaderWidth: control.setTrackHeaderWidth - - footerPositioning: ListView.InlineFooter - footer: Rectangle { - color: timelineBackground - width: parent.width - height: Math.max(0,list_view_video.parent.height - ((((itemHeight*control.scaleY)+1) * list_view_video.count))) - } - - displaced: Transition { - NumberAnimation { - properties: "x,y" - duration: 100 - } - } - - ScrollBar.vertical: ScrollBar { - policy: list_view_video.visibleArea.heightRatio < 1.0 ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff - } - } - } - } - - Rectangle { - id: sizer - color: "transparent" - border.color: trackEdge - Layout.minimumWidth: parent.width - Layout.preferredHeight: handleSize - Layout.minimumHeight: handleSize - Layout.maximumHeight: handleSize - property real handleSize: 8 - - MouseArea { - id: ma - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton - - cursorShape: Qt.SizeVerCursor - - onPositionChanged: { - if(pressed) { - let ppos = mapToItem(splitView, 0, mouse.y) - topView.Layout.preferredHeight = ppos.y - (sizer.handleSize/2) - bottomView.Layout.preferredHeight = splitView.height - (ppos.y - (sizer.handleSize/2)) - sizer.handleSize - } - } - } - } - - Item { - id: bottomView - Layout.minimumWidth: parent.width - Layout.minimumHeight: itemHeight*control.scaleY - Layout.preferredHeight: parent.height*0.3 - Rectangle { - anchors.fill: parent - color: trackEdge - ListView { - id: list_view_audio - spacing: 1 - - anchors.fill: parent - - model: audio_items - clip: true - interactive: false - - property real scaleX: control.scaleX - property real scaleY: control.scaleY - property real itemHeight: control.itemHeight - property var timelineSelection: control.timelineSelection - property var timelineFocusSelection: control.timelineFocusSelection - property real cX: hbar.position * myWidth - property real parentWidth: control.parentWidth - property int playheadFrame: control.playheadFrame - property var timelineItem: control.timelineItem - property var hoveredItem: control.hoveredItem - property real trackHeaderWidth: control.trackHeaderWidth - property var setTrackHeaderWidth: control.setTrackHeaderWidth - property string itemFlag: control.itemFlag - - displaced: Transition { - NumberAnimation { - properties: "x,y" - duration: 100 - } - } - - footerPositioning: ListView.InlineFooter - footer: Rectangle { - color: timelineBackground - width: parent.width - height: Math.max(0,bottomView.height - ((((itemHeight*control.scaleY)+1) * list_view_audio.count))) - } - - ScrollBar.vertical: ScrollBar { - policy: list_view_audio.visibleArea.heightRatio < 1.0 ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/ui/qml/reskin/views/timeline/delegates/XsDelegateVideoTrack.qml b/ui/qml/reskin/views/timeline/delegates/XsDelegateVideoTrack.qml deleted file mode 100644 index c9d4d65e9..000000000 --- a/ui/qml/reskin/views/timeline/delegates/XsDelegateVideoTrack.qml +++ /dev/null @@ -1,147 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 -import QtGraphicalEffects 1.0 -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudioReskin 1.0 -import xstudio.qml.module 1.0 -import xstudio.qml.helpers 1.0 - -DelegateChoice { - roleValue: "Video Track" - - Component { - Rectangle { - id: control - - color: timelineBackground - property real scaleX: ListView.view.scaleX - property real scaleY: ListView.view.scaleY - property real itemHeight: ListView.view.itemHeight - property real trackHeaderWidth: ListView.view.trackHeaderWidth - property real cX: ListView.view.cX - property real parentWidth: ListView.view.parentWidth - property var timelineItem: ListView.view.timelineItem - property string itemFlag: flagColourRole != "" ? flagColourRole : ListView.view.itemFlag - property var parentLV: ListView.view - readonly property bool extraDetail: height > 60 - property var setTrackHeaderWidth: ListView.view.setTrackHeaderWidth - - width: ListView.view.width - height: itemHeight * scaleY - - opacity: enabledRole ? 1.0 : 0.2 - - property bool isHovered: hoveredItem == control - property bool isSelected: false - property bool isFocused: false - property var timelineSelection: ListView.view.timelineSelection - property var timelineFocusSelection: ListView.view.timelineFocusSelection - property var hoveredItem: ListView.view.hoveredItem - property var itemTypeRole: typeRole - - property alias list_view: list_view - - function modelIndex() { - return control.DelegateModel.model.srcModel.index( - index, 0, control.DelegateModel.model.rootIndex - ) - } - - Connections { - target: timelineSelection - function onSelectionChanged(selected, deselected) { - if(isSelected && helpers.itemSelectionContains(deselected, modelIndex())) - isSelected = false - else if(!isSelected && helpers.itemSelectionContains(selected, modelIndex())) - isSelected = true - } - } - - Connections { - target: timelineFocusSelection - function onSelectionChanged(selected, deselected) { - if(isFocused && helpers.itemSelectionContains(deselected, modelIndex())) - isFocused = false - else if(!isFocused && helpers.itemSelectionContains(selected, modelIndex())) - isFocused = true - } - } - - DelegateChooser { - id: chooser - role: "typeRole" - - XsDelegateClip {} - XsDelegateGap {} - } - - DelegateModel { - id: track_items - property var srcModel: theSessionData - model: srcModel - rootIndex: helpers.makePersistent(control.DelegateModel.model.srcModel.index( - index, 0, control.DelegateModel.model.rootIndex - )) - delegate: chooser - } - - XsTrackHeader { - id: track_header - z: 2 - anchors.top: parent.top - anchors.left: parent.left - width: trackHeaderWidth - height: Math.ceil(control.itemHeight * control.scaleY) - isHovered: control.isHovered - itemFlag: control.itemFlag - trackIndex: trackIndexRole - setTrackHeaderWidth: control.setTrackHeaderWidth - text: nameRole - isEnabled: enabledRole - isFocused: control.isFocused - onFocusClicked: timelineFocusSelection.select(modelIndex(), ItemSelectionModel.Toggle) - onEnabledClicked: enabledRole = !enabledRole - } - - Flickable { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: track_header.right - anchors.right: parent.right - - interactive: false - - contentWidth: Math.ceil(trimmedDurationRole * control.scaleX) - contentHeight: Math.ceil(control.itemHeight * control.scaleY) - contentX: control.cX - - Row { - id:list_view - - property real scaleX: control.scaleX - property real scaleY: control.scaleY - property real itemHeight: control.itemHeight - property var timelineSelection: control.timelineSelection - property var timelineFocusSelection: control.timelineFocusSelection - property var timelineItem: control.timelineItem - property var hoveredItem: control.hoveredItem - property real trackHeaderWidth: control.trackHeaderWidth - property string itemFlag: control.itemFlag - - property var itemAtIndex: item_repeater.itemAt - - Repeater { - id: item_repeater - model: track_items - } - } - } - } - } -} diff --git a/ui/qml/reskin/views/timeline/delegates/XsTimelineEditToolItems.qml b/ui/qml/reskin/views/timeline/delegates/XsTimelineEditToolItems.qml deleted file mode 100644 index dcc41c9ef..000000000 --- a/ui/qml/reskin/views/timeline/delegates/XsTimelineEditToolItems.qml +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 -import QtQuick.Layouts 1.15 - -import xStudioReskin 1.0 - - -XsPrimaryButton{ id: btnDiv - - isActiveIndicatorAtLeft: true - - imageDiv.rotation: _name=="Move UD" || _name=="Roll"? 90 : 0 - -} diff --git a/ui/qml/reskin/views/timeline/widgets/XsClipItem.qml b/ui/qml/reskin/views/timeline/widgets/XsClipItem.qml deleted file mode 100644 index a6eb55f44..000000000 --- a/ui/qml/reskin/views/timeline/widgets/XsClipItem.qml +++ /dev/null @@ -1,181 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 - -import xStudioReskin 1.0 - -Rectangle { - id: control - - // clip:true - property bool isHovered: false - property bool isEnabled: true - property bool isFocused: false - property bool isSelected: false - property int parentStart: 0 - property int start: 0 - property int duration: 0 - property int availableStart: 0 - property int availableDuration: 1 - property real fps: 24.0 - property string name - property color primaryColor: defaultClip - property bool isMoving: false - property bool isCopying: false - property color mediaFlagColour: "transparent" - - readonly property bool extraDetail: isHovered && height > 60 - - property color mainColor: Qt.lighter( primaryColor, isSelected ? 1.4 : 1.0) - - color: Qt.tint(timelineBackground, helpers.saturate(helpers.alphate(mainColor, 0.3), 0.3)) - - opacity: isEnabled ? 1.0 : 0.2 - - // XsTickWidget { - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.top: parent.top - // height: Math.min(parent.height/5, 20) - // start: control.start - // duration: control.duration - // fps: control.fps - // endTicks: false - // } - - Rectangle { - color: "transparent" - z:5 - anchors.fill: parent - border.width: isHovered ? 3 : 2 - border.color: isMoving || isCopying || isFocused ? "red" : isHovered ? palette.highlight : Qt.lighter( - Qt.tint(timelineBackground, helpers.saturate(helpers.alphate(mainColor, 0.4), 0.4)), - 1.2) - } - - Rectangle { - anchors.left: parent.left - anchors.leftMargin: 2 - anchors.top: parent.top - anchors.bottom: parent.bottom - width: 2 - color: mediaFlagColour - // z: 6 - } - - XsElideLabel { - anchors.fill: parent - anchors.leftMargin: 5 - anchors.rightMargin: 5 - elide: Qt.ElideMiddle - text: name - opacity: 0.8 - font.pixelSize: 14 - z:1 - clip: true - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - Label { - anchors.verticalCenter: parent.verticalCenter - text: parentStart - anchors.left: parent.left - anchors.leftMargin: 10 - visible: isHovered - z:2 - } - - Label { - anchors.verticalCenter: parent.verticalCenter - text: parentStart + duration -1 - anchors.right: parent.right - anchors.rightMargin: 10 - visible: isHovered - z:2 - } - - Label { - text: duration - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: 5 - visible: extraDetail - z:2 - } - Label { - anchors.left: parent.left - anchors.leftMargin: 10 - anchors.topMargin: 5 - text: start - visible: extraDetail - z:2 - } - Label { - anchors.top: parent.top - anchors.right: parent.right - anchors.rightMargin: 10 - anchors.topMargin: 5 - text: start + duration - 1 - visible: extraDetail - z:2 - } - - Label { - text: availableDuration - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: 5 - visible: extraDetail - opacity: 0.5 - z:2 - } - Label { - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.leftMargin: 10 - anchors.bottomMargin: 5 - text: availableStart - visible: extraDetail - opacity: 0.5 - z:2 - } - Label { - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.rightMargin: 10 - anchors.bottomMargin: 5 - opacity: 0.5 - text: availableStart + availableDuration - 1 - visible: extraDetail - z:2 - } - - - // position of clip in media - Rectangle { - - visible: isHovered - - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left - - color: Qt.darker( control.color, 1.2) - - width: (parent.width / availableDuration) * (start - availableStart) - } - - Rectangle { - visible: isHovered - - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - - color: Qt.darker( control.color, 1.2) - - width: parent.width - ((parent.width / availableDuration) * duration) - ((parent.width / availableDuration) * (start - availableStart)) - } -} diff --git a/ui/qml/reskin/views/timeline/widgets/XsDragBoth.qml b/ui/qml/reskin/views/timeline/widgets/XsDragBoth.qml deleted file mode 100644 index 9ec2a8452..000000000 --- a/ui/qml/reskin/views/timeline/widgets/XsDragBoth.qml +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Shapes 1.12 -import xStudioReskin 1.0 - - -Shape { - id: control - property real thickness: 2 - property color color: palette.highlight - - ShapePath { - strokeWidth: control.thickness - strokeColor: control.color - fillColor: "transparent" - - startX: control.width/2 - startY: 0 - - // to bottom right - PathLine {x: control.width/2; y: control.height} - } - - ShapePath { - strokeWidth: control.thickness - fillColor: control.color - strokeColor: control.color - - startX: control.width/2 - startY: control.height / 3 - - // to bottom right - PathLine {x: control.width; y: control.height / 2} - PathLine {x: control.width/2; y: (control.height / 3) * 2} - PathLine {x: 0; y: control.height / 2} - PathLine {x: control.width/2; y: control.height / 3} - } -} diff --git a/ui/qml/reskin/views/timeline/widgets/XsDragLeft.qml b/ui/qml/reskin/views/timeline/widgets/XsDragLeft.qml deleted file mode 100644 index 996bbb134..000000000 --- a/ui/qml/reskin/views/timeline/widgets/XsDragLeft.qml +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Shapes 1.12 -import xStudioReskin 1.0 - - -Shape { - id: control - property real thickness: 2 - property color color: palette.highlight - - ShapePath { - strokeWidth: control.thickness - strokeColor: control.color - fillColor: "transparent" - - startX: 0 - startY: 0 - - // to bottom right - PathLine {x: 0; y: control.height} - } - - ShapePath { - strokeWidth: control.thickness - fillColor: control.color - strokeColor: control.color - - startX: 0 - startY: control.height / 3 - - // to bottom right - PathLine {x: 0; y: (control.height / 3) * 2} - PathLine {x: control.width; y: control.height / 2} - PathLine {x: 0; y: control.height / 3} - } -} diff --git a/ui/qml/reskin/views/timeline/widgets/XsDragRight.qml b/ui/qml/reskin/views/timeline/widgets/XsDragRight.qml deleted file mode 100644 index 6c5e6329c..000000000 --- a/ui/qml/reskin/views/timeline/widgets/XsDragRight.qml +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import xStudioReskin 1.0 - -XsDragLeft { - rotation: 180.0 -} \ No newline at end of file diff --git a/ui/qml/reskin/views/timeline/widgets/XsElideLabel.qml b/ui/qml/reskin/views/timeline/widgets/XsElideLabel.qml deleted file mode 100644 index a03542ecb..000000000 --- a/ui/qml/reskin/views/timeline/widgets/XsElideLabel.qml +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 - -// Qt.ElideLeft -// Qt.ElideMiddle -// Qt.ElideNone -// Qt.ElideRight - -Item { - id: item - - height: label.height - - property string text - property int elideWidth: width - property int elide: Qt.ElideRight - - property alias color: label.color - property alias font: label.font - property alias horizontalAlignment: label.horizontalAlignment - property alias verticalAlignment: label.verticalAlignment - - Label { - id: label - text: textMetrics.elidedText - anchors.fill: parent - - TextMetrics { - id: textMetrics - text: item.text - - font: label.font - - elide: item.elide - elideWidth: item.elideWidth - } - } -} diff --git a/ui/qml/reskin/views/timeline/widgets/XsGapItem.qml b/ui/qml/reskin/views/timeline/widgets/XsGapItem.qml deleted file mode 100644 index f6ae4534c..000000000 --- a/ui/qml/reskin/views/timeline/widgets/XsGapItem.qml +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 - -import xStudioReskin 1.0 - -Rectangle { - id: control - - property bool isHovered: false - property bool isEnabled: true - property bool isSelected: false - property int start: 0 - property int parentStart: 0 - property int duration: 0 - property real fps: 24.0 - property string name - readonly property bool extraDetail: isSelected && height > 60 - - color: timelineBackground - - // XsTickWidget { - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.top: parent.top - // height: Math.min(parent.height/5, 20) - // start: control.start - // duration: control.duration - // fps: fps - // endTicks: false - // } - - XsElideLabel { - anchors.fill: parent - anchors.leftMargin: 5 - anchors.rightMargin: 5 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: name - opacity: 0.4 - elide: Qt.ElideMiddle - font.pixelSize: 14 - clip: true - visible: isHovered - z:1 - } - Label { - anchors.horizontalCenter: parent.horizontalCenter - text: duration - anchors.top: parent.top - anchors.topMargin: 5 - z:2 - visible: extraDetail - } - Label { - anchors.top: parent.top - anchors.left: parent.left - anchors.topMargin: 5 - anchors.leftMargin: 10 - text: start - visible: extraDetail - z:2 - } - Label { - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 10 - text: parentStart - visible: isHovered - z:2 - } - Label { - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: 10 - text: parentStart + duration - 1 - visible: isHovered - z:2 - } - Label { - anchors.top: parent.top - anchors.topMargin: 5 - anchors.right: parent.right - anchors.rightMargin: 10 - text: start + duration - 1 - z:2 - visible: extraDetail - } -} diff --git a/ui/qml/reskin/views/timeline/widgets/XsMoveClip.qml b/ui/qml/reskin/views/timeline/widgets/XsMoveClip.qml deleted file mode 100644 index 9ec2a8452..000000000 --- a/ui/qml/reskin/views/timeline/widgets/XsMoveClip.qml +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Shapes 1.12 -import xStudioReskin 1.0 - - -Shape { - id: control - property real thickness: 2 - property color color: palette.highlight - - ShapePath { - strokeWidth: control.thickness - strokeColor: control.color - fillColor: "transparent" - - startX: control.width/2 - startY: 0 - - // to bottom right - PathLine {x: control.width/2; y: control.height} - } - - ShapePath { - strokeWidth: control.thickness - fillColor: control.color - strokeColor: control.color - - startX: control.width/2 - startY: control.height / 3 - - // to bottom right - PathLine {x: control.width; y: control.height / 2} - PathLine {x: control.width/2; y: (control.height / 3) * 2} - PathLine {x: 0; y: control.height / 2} - PathLine {x: control.width/2; y: control.height / 3} - } -} diff --git a/ui/qml/reskin/views/timeline/widgets/XsTickWidget.qml b/ui/qml/reskin/views/timeline/widgets/XsTickWidget.qml deleted file mode 100644 index ecd17e363..000000000 --- a/ui/qml/reskin/views/timeline/widgets/XsTickWidget.qml +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import QtGraphicalEffects 1.0 - -import xStudio 1.1 - -Rectangle { - id: control - property int start: 0 - property int duration: 0 - property int secondOffset: 0 - property real fractionOffset: 0 - property real fps: 24 - property real tickWidth: (control.width / duration) - property color tickColor: "black" - property bool renderFrames: duration > 2 && tickWidth > 5 - property bool renderSeconds: duration > fps && tickWidth * fps > 5 - property bool endTicks: true - - color: "transparent" - - signal frameClicked(int frame) - signal framePressed(int frame) - signal frameDragging(int frame) - - MouseArea{ - id: mArea - anchors.fill: parent - hoverEnabled: true - property bool dragging: false - onClicked: { - if (mouse.button == Qt.LeftButton) { - frameClicked(start + ((mouse.x + fractionOffset)/ tickWidth)) - } - } - onReleased: { - dragging = false - } - onPressed: { - if (mouse.button == Qt.LeftButton) { - framePressed(start + ((mouse.x + fractionOffset)/ tickWidth)) - dragging = true - } - } - - onPositionChanged: { - if (dragging) { - frameDragging(start + ((mouse.x + fractionOffset)/ tickWidth)) - } - } - } - - - // frame repeater - Repeater { - model: control.height > 8 && renderFrames ? duration-(endTicks ? 0 : 1) : null - Rectangle { - height: control.height / 2 - color: tickColor - - x: ((index+(endTicks ? 0 : 1)) * tickWidth) - fractionOffset - visible: x >=0 - width: 1 - } - } - - Repeater { - model: control.height > 4 && renderSeconds ? Math.ceil(duration / fps) - (endTicks ? 0 : 1) : null - Rectangle { - height: control.height - color: tickColor - - x: (((index + (endTicks ? 0 : 1)) * (tickWidth * fps)) - (secondOffset * tickWidth)) - fractionOffset - visible: x >=0 - width: 1 - } - } -} \ No newline at end of file diff --git a/ui/qml/reskin/views/timeline/widgets/XsTimelineCursor.qml b/ui/qml/reskin/views/timeline/widgets/XsTimelineCursor.qml deleted file mode 100644 index 1b76b4da4..000000000 --- a/ui/qml/reskin/views/timeline/widgets/XsTimelineCursor.qml +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Shapes 1.12 -import xStudio 1.1 - -Shape { - id: control - - property real thickness: 2 - property color color: palette.highlight - - property int position: start - property int start: 0 - property int duration: 0 - property int secondOffset: 0 - property real fractionOffset: 0 - property real fps: 24 - property real tickWidth: (control.width / duration) - - readonly property real cursorX: ((position-start) * tickWidth) - fractionOffset - property int cursorSize: 20 - - visible: position >= start - - ShapePath { - id: line - strokeWidth: control.thickness - strokeColor: control.color - fillColor: "transparent" - - startX: cursorX - startY: 0 - - // to bottom right - PathLine {x: cursorX; y: control.height} - } - - ShapePath { - strokeWidth: control.thickness - fillColor: control.color - strokeColor: control.color - - startX: cursorX-(cursorSize/2) - startY: 0 - - // to bottom right - PathLine {x: cursorX+(cursorSize/2); y: 0} - PathLine {x: cursorX; y: cursorSize} - // PathLine {x: cursorX-(cursorSize/2); y: 0} - } -} - - // // frame repeater - // Rectangle { - // anchors.top: parent.top - // height: control.height - // color: cursorColor - // visible: position >= start - // x: ((position-start) * tickWidth) - fractionOffset - // width: 2 - // } diff --git a/ui/qml/reskin/views/timeline/widgets/XsTrackHeader.qml b/ui/qml/reskin/views/timeline/widgets/XsTrackHeader.qml deleted file mode 100644 index 66400560c..000000000 --- a/ui/qml/reskin/views/timeline/widgets/XsTrackHeader.qml +++ /dev/null @@ -1,184 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 - -import xStudioReskin 1.0 - -Item { - id: control - - property bool isHovered: false - property string itemFlag: "" - property string text: "" - property int trackIndex: 0 - property var setTrackHeaderWidth: function(val) {} - property string title: "Video Track" - - property bool isEnabled: false - signal enabledClicked() - - property bool isFocused: false - signal focusClicked() - - Rectangle { - id: control_background - - color: Qt.darker( trackBackground, isSelected ? 0.6 : 1.0) - - anchors.fill: parent - - RowLayout { - clip: true - spacing: 10 - anchors.fill: parent - anchors.leftMargin: 10 - anchors.rightMargin: 10 - anchors.topMargin: 5 - anchors.bottomMargin: 5 - - Rectangle { - Layout.preferredHeight: parent.height/3 - Layout.preferredWidth: Layout.preferredHeight - color: itemFlag != "" ? helpers.saturate(itemFlag, 0.4) : control_background.color - border.width: 2 - border.color: Qt.lighter(color, 1.2) - - MouseArea { - - anchors.fill: parent - onPressed: trackFlag.popup() - cursorShape: Qt.PointingHandCursor - - /*XsFlagMenu { - id:trackFlag - onFlagSet: flagColourRole = (hex == "#00000000" ? "" : hex) - }*/ - } - } - - Label { - // Layout.preferredWidth: 20 - Layout.fillHeight: true - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - text: control.title[0] + trackIndex - } - - XsElideLabel { - Layout.fillHeight: true - Layout.fillWidth: true - Layout.minimumWidth: 30 - Layout.alignment: Qt.AlignLeft - elide: Qt.ElideRight - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - text: control.text == "" ? control.title : control.text - } - - GridLayout { - Layout.fillHeight: true - Layout.alignment: Qt.AlignRight - - Rectangle { - Layout.preferredHeight: Math.min(Math.min(control.height - 20, control.width/3/4), 40) - Layout.preferredWidth: Layout.preferredHeight - - color: control.isEnabled ? trackEdge : Qt.darker(trackEdge, 1.4) - - Label { - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - anchors.fill: parent - text: "E" - } - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - - onClicked: { - control.enabledClicked() - } - } - } - - Rectangle { - Layout.preferredHeight: Math.min(Math.min(control.height - 20, control.width/3/4), 40) - Layout.preferredWidth: Layout.preferredHeight - - color: control.isFocused ? trackEdge : Qt.darker(trackEdge, 1.4) - - Label { - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - anchors.fill: parent - text: "F" - } - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - - onClicked: { - control.focusClicked() - } - } - } - } - - - // Label { - // anchors.top: parent.top - // anchors.left: parent.left - // anchors.leftMargin: 10 - // anchors.topMargin: 5 - // text: trimmedStartRole - // visible: extraDetail - // z:4 - // } - // Label { - // anchors.top: parent.top - // anchors.left: parent.left - // anchors.leftMargin: 40 - // anchors.topMargin: 5 - // text: trimmedDurationRole - // visible: extraDetail - // z:4 - // } - // Label { - // anchors.top: parent.top - // anchors.left: parent.left - // anchors.leftMargin: 70 - // anchors.topMargin: 5 - // text: trimmedDurationRole ? trimmedStartRole + trimmedDurationRole - 1 : 0 - // visible: extraDetail - // z:4 - // } - } - } - - Rectangle { - width: 4 - height: parent.height - - anchors.right: parent.right - anchors.top: parent.top - color: timelineBackground - - MouseArea { - id: ma - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton - - cursorShape: Qt.SizeHorCursor - - onPositionChanged: { - if(pressed) { - let ppos = mapToItem(control, mouse.x, 0) - setTrackHeaderWidth(ppos.x + 4) - } - } - } - } -} - diff --git a/ui/qml/reskin/views/viewport/XsViewport.qml b/ui/qml/reskin/views/viewport/XsViewport.qml deleted file mode 100644 index a4a6a55e8..000000000 --- a/ui/qml/reskin/views/viewport/XsViewport.qml +++ /dev/null @@ -1,151 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.15 - -import xStudioReskin 1.0 -import xstudio.qml.viewport 1.0 - -//Viewport { -Rectangle{ color: "transparent" - - id: viewport - anchors.fill: parent - property color gradient_colour_top: "#5C5C5C" - property color gradient_colour_bottom: "#474747" - - property alias view: view - focus: true - - Item { - anchors.fill: parent - // Keys.forwardTo: viewport //#TODO: To check with Ted - focus: true - Keys.forwardTo: view - } - - property real panelPadding: XsStyleSheet.panelPadding - - Rectangle{ - id: r - gradient: Gradient { - GradientStop { position: r.alpha; color: gradient_colour_top } - GradientStop { position: r.beta; color: gradient_colour_bottom } - } - anchors.fill: actionBar - property real alpha: -y/height - property real beta: parent.height/height + alpha - - } - - XsViewportActionBar{ - id: actionBar - anchors.top: parent.top - actionbar_model_data_name: view.name + "_actionbar" - } - - - Rectangle{ - id: r2 - gradient: Gradient { - GradientStop { position: r2.alpha; color: gradient_colour_top } - GradientStop { position: r2.beta; color: gradient_colour_bottom } - } - anchors.top: infoBar.top - anchors.bottom: infoBar.bottom - anchors.left: parent.left - anchors.right: parent.right - property real alpha: -y/height - property real beta: parent.height/height + alpha - - } - - XsViewportInfoBar{ - id: infoBar - anchors.top: actionBar.bottom - } - - property color gradient_dark: "black" - property color gradient_light: "white" - - Viewport { - id: view - x: panelPadding - y: (actionBar.height + infoBar.height) - width: parent.width-(x*2) - height: parent.height-(toolBar.height + transportBar.height) - (y) - - onPointerEntered: { - focus = true; - forceActiveFocus() - } - - } - - Rectangle{ - // couple of pixels down the left of the viewport - id: left_side - gradient: Gradient { - GradientStop { position: left_side.alpha; color: gradient_colour_top } - GradientStop { position: left_side.beta; color: gradient_colour_bottom } - } - anchors.left: parent.left - anchors.right: view.left - anchors.top: view.top - anchors.bottom: view.bottom - property real alpha: -y/height - property real beta: parent.height/height + alpha - - } - - Rectangle{ - // couple of pixels down the right of the viewport - id: right_side - gradient: Gradient { - GradientStop { position: right_side.alpha; color: gradient_colour_top } - GradientStop { position: right_side.beta; color: gradient_colour_bottom } - } - anchors.left: view.right - anchors.right: parent.right - anchors.top: view.top - anchors.bottom: view.bottom - property real alpha: -y/height - property real beta: parent.height/height + alpha - - } - - Rectangle{ - id: r3 - gradient: Gradient { - GradientStop { position: r3.alpha; color: gradient_colour_top } - GradientStop { position: r3.beta; color: gradient_colour_bottom } - } - anchors.fill: toolBar - property real alpha: -y/height - property real beta: parent.height/height + alpha - - } - - XsViewportToolBar{ - id: toolBar - anchors.bottom: transportBar.top - toolbar_model_data_name: view.name + "_toolbar" - } - - Rectangle{ - id: r4 - gradient: Gradient { - GradientStop { position: r4.alpha; color: gradient_colour_top } - GradientStop { position: r4.beta; color: gradient_colour_bottom } - } - anchors.fill: transportBar - property real alpha: -y/height - property real beta: parent.height/height + alpha - - } - XsViewportTransportBar{ - id: transportBar - anchors.bottom: parent.bottom - } - - -} \ No newline at end of file diff --git a/ui/qml/reskin/views/viewport/XsViewportActionBar.qml b/ui/qml/reskin/views/viewport/XsViewportActionBar.qml deleted file mode 100644 index bcd5a473b..000000000 --- a/ui/qml/reskin/views/viewport/XsViewportActionBar.qml +++ /dev/null @@ -1,196 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.15 -// import QtQml.Models 2.14 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 -import xstudio.qml.helpers 1.0 -import "./widgets" - -Item{id: actionDiv - width: parent.width; - height: btnHeight+(panelPadding*2) - - property real btnHeight: XsStyleSheet.widgetStdHeight+4 - property real panelPadding: XsStyleSheet.panelPadding - - property string actionbar_model_data_name - - /************************************************************************* - - Access Playhead data - - **************************************************************************/ - - // Get the UUID of the current onscreen media from the playhead - XsModelProperty { - id: __playheadSourceUuid - role: "value" - index: currentPlayheadData.search_recursive("Current Media Uuid", "title") - } - XsModelProperty { - id: __playheadMediaSourceUuid - role: "value" - index: currentPlayheadData.search_recursive("Current Media Source Uuid", "title") - } - - Connections { - target: currentPlayheadData // this bubbles up from XsSessionWindow - function onJsonChanged() { - __playheadSourceUuid.index = currentPlayheadData.search_recursive("Current Media Uuid", "title") - __playheadMediaSourceUuid.index = currentPlayheadData.search_recursive("Current Media Source Uuid", "title") - } - } - property alias mediaUuid: __playheadSourceUuid.value - property alias mediaSourceUuid: __playheadMediaSourceUuid.value - - // When the current onscreen media changes, search for the corresponding - // node in the main session data model - onMediaUuidChanged: { - - // TODO - current this gets us to media actor, not media source actor, - // so we can't get to the file name yet - mediaData.index = theSessionData.search_recursive( - mediaUuid, - "actorUuidRole", - viewedMediaSetIndex - ) - } - - onMediaSourceUuidChanged: { - mediaSourceData.index = theSessionData.search_recursive( - mediaSourceUuid, - "actorUuidRole", - viewedMediaSetIndex - ) - } - - // this gives us access to the 'role' data of the entry in the session model - // for the current on-screen media - XsModelPropertyMap { - id: mediaData - index: theSessionData.invalidIndex() - } - - // this gives us access to the 'role' data of the entry in the session model - // for the current on-screen media SOURCE - XsModelPropertyMap { - id: mediaSourceData - property var fileName: { - let result = "TBD" - if(index.valid && values.pathRole != undefined) { - result = helpers.fileFromURL(values.pathRole) - } - return result - } - } - - /*************************************************************************/ - - RowLayout{ - x: panelPadding - spacing: 1 - width: parent.width-(x*2) - height: btnHeight - anchors.verticalCenter: parent.verticalCenter - - XsPrimaryButton{ id: transformBtn - Layout.preferredWidth: 40 - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/open_with.svg" - onClicked:{ - isActive = !isActive - } - } - XsPrimaryButton{ id: colourBtn - Layout.preferredWidth: 40 - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/tune.svg" - onClicked:{ - isActive = !isActive - } - } - XsPrimaryButton{ id: drawBtn - Layout.preferredWidth: 40 - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/brush.svg" - onClicked:{ - isActive = !isActive - } - } - XsPrimaryButton{ id: notesBtn - Layout.preferredWidth: 40 - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/sticky_note.svg" - onClicked:{ - isActive = !isActive - } - } - XsText{ - Layout.fillWidth: true - Layout.preferredHeight: parent.height - text: mediaSourceData.fileName - font.bold: true - } - XsPrimaryButton{ id: resetBtn - Layout.preferredWidth: 40 - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/reset_tv.svg" - onClicked:{ - zoomBtn.isZoomMode = false - panBtn.isActive = false - } - } - - XsModuleData { - id: actionbar_model_data - modelDataName: actionbar_model_data_name - } - - Repeater { - - id: the_view - model: actionbar_model_data - - delegate: XsPrimaryButton{ - id: zoomBtn - Layout.preferredWidth: 40 - Layout.preferredHeight: parent.height - imgSrc: title == "Zoom (Z)" ? "qrc:/icons/zoom_in.svg" : "qrc:/icons/pan.svg" - isActive: value - onClicked:{ - value = !value - } - } - } - - /*XsPrimaryButton{ id: zoomBtn - Layout.preferredWidth: 40 - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/zoom_in.svg" - isActive: isZoomMode - property bool isZoomMode: false - onClicked:{ - isZoomMode = !isZoomMode - panBtn.isActive = false - } - } - XsPrimaryButton{ id: panBtn - Layout.preferredWidth: 40 - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/pan.svg" - onClicked:{ - isActive = !isActive - zoomBtn.isZoomMode = false - } - }*/ - - XsPrimaryButton{ id: moreBtn - Layout.preferredWidth: 40 - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/more_vert.svg" - } - } - -} \ No newline at end of file diff --git a/ui/qml/reskin/views/viewport/XsViewportInfoBar.qml b/ui/qml/reskin/views/viewport/XsViewportInfoBar.qml deleted file mode 100644 index 6a8523b7c..000000000 --- a/ui/qml/reskin/views/viewport/XsViewportInfoBar.qml +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.15 -// import QtQml.Models 2.14 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 -import "./widgets" - -Rectangle { - id: toolBar - x: barPadding - width: parent.width-(x*2) //parent.width - height: btnHeight //+(barPadding*2) - color: XsStyleSheet.panelTitleBarColor - - property string panelIdForMenu: panelId - - property real barPadding: XsStyleSheet.panelPadding - property real btnWidth: XsStyleSheet.primaryButtonStdWidth - property real btnHeight: XsStyleSheet.widgetStdHeight - - Item { - id: bgDivLeft; - anchors.left: parent.left - anchors.right: rowDiv.left - anchors.rightMargin: rowDiv.spacing - height: btnHeight - } - Item { - id: bgDivRight; - anchors.left: rowDiv.right - anchors.leftMargin: rowDiv.spacing - anchors.right: parent.right - height: btnHeight - } - - - // onWidthChanged: { //#TODO: incomplete) for centered buttons - // if(parent.width < rowDiv.width) { - - // rowDiv.preferredBtnWidth = (rowDiv.width/rowDiv.btnCount) - // rowDiv.width = toolBar.width - // } - // } - - - RowLayout{ - id: rowDiv - spacing: 0 - - -/* - // //for center buttons - // width = (preferredBtnWidth+spacing)*btnCount - // preferredBtnWidth = (maxBtnWidth*btnCount)>toolBar.width? (toolBar.width/btnCount) : maxBtnWidth - - // //for fullWidth buttons - // width = parent.width - (spacing*(btnCount)) - // preferredBtnWidth = (width/btnCount) -*/ - - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - - width: (preferredBtnWidth+spacing)*btnCount //parent.width - (spacing*(btnCount)) - height: btnHeight - - property int btnCount: 5 - property real maxBtnWidth: 110 - property real preferredBtnWidth: (maxBtnWidth*btnCount)>toolBar.width? (toolBar.width/btnCount) : maxBtnWidth - property real preferredMenuWidth: preferredBtnWidth<100? 100 : preferredBtnWidth - - // dummy 'value' property for offsetButton - property var value - - XsViewerSeekEditButton{ id: offsetButton - Layout.preferredWidth: rowDiv.preferredBtnWidth - Layout.preferredHeight: parent.height - text: "Offset" - shortText: "Oft" //#TODO - fromValue: -10 - defaultValue: 0 - toValue: 10 - valueText: 0 - stepSize: 1 - decimalDigits: 0 - showValueWhenShortened: true - isBgGradientVisible: false - } - - XsViewerMenuButton{ id: formatBtn - Layout.preferredWidth: rowDiv.preferredBtnWidth - Layout.preferredHeight: parent.height - text: "Format" - shortText: "Fmt" //#TODO - valueText: "dnxhd" - clickDisabled: true - showValueWhenShortened: true - isBgGradientVisible: false - } - XsViewerMenuButton{ id: bitBtn - Layout.preferredWidth: rowDiv.preferredBtnWidth - Layout.preferredHeight: parent.height - text: "Bit Depth" - shortText: "Bit" //#TODO - valueText: "8 bits" - clickDisabled: true - showValueWhenShortened: true - isBgGradientVisible: false - } - XsViewerMenuButton{ id: fpsBtn - Layout.preferredWidth: rowDiv.preferredBtnWidth - Layout.preferredHeight: parent.height - text: "FPS" - shortText: "FPS" //#TODO - valueText: "24.0" - clickDisabled: true - showValueWhenShortened: true - isBgGradientVisible: false - } - XsViewerMenuButton{ id: resBtn - Layout.preferredWidth: rowDiv.preferredBtnWidth - Layout.preferredHeight: parent.height - text: "Res" - shortText: "Res" //#TODO - valueText: "1920x1080" - clickDisabled: true - showValueWhenShortened: true - isBgGradientVisible: false - shortThresholdWidth: 99+10 //60+30 - } - - - - - - } -} \ No newline at end of file diff --git a/ui/qml/reskin/views/viewport/XsViewportToolBar.qml b/ui/qml/reskin/views/viewport/XsViewportToolBar.qml deleted file mode 100644 index d58b161ed..000000000 --- a/ui/qml/reskin/views/viewport/XsViewportToolBar.qml +++ /dev/null @@ -1,122 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.15 -import Qt.labs.qmlmodels 1.0 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 -import "./widgets" - -Item { - id: toolBar - width: parent.width - height: btnHeight //+(barPadding*2) - - property string panelIdForMenu: panelId - - property real barPadding: XsStyleSheet.panelPadding - property real btnWidth: XsStyleSheet.primaryButtonStdWidth - property real btnHeight: XsStyleSheet.widgetStdHeight - property string toolbar_model_data_name - - // Here is where we get all the data about toolbar items that is broadcast - // from the backend. Note that each entry in the model (which is a simple - // 1-dimensional list) - // - // Each item can (but doesn't have to) provide 'role' data with the following - // names, which are 'visible' in delegates of Repeater(s) etc: - // - // type, attr_enabled, activated, title, abbr_title, combo_box_options, - // combo_box_abbr_options, combo_box_options_enabled, tooltip, - // custom_message, integer_min, integer_max, float_scrub_min, - // float_scrub_max, float_scrub_step, float_scrub_sensitivity, - // float_display_decimals, value, default_value, short_value, - // disabled_value, attr_uuid, groups, menu_paths, toolbar_position, - // override_value, serialize_key, qml_code, preference_path, - // init_only_preference_path, font_size, font_family, text_alignment, - // text_alignment_box, attr_colour, hotkey_uuid - // - // Some important ones are: - // 'title' (the name of the corresponding backend attribute) - // 'value' (the actual data value of the attribute) - // 'type' (the attribute type, e.g. float, bool, multichoice) - // 'combo_box_options' (for multichoice attrs, this is a list of strings) - - XsModuleData { - id: toolbar_model_data - modelDataName: toolbar_model_data_name - } - - RowLayout{ - - id: rowDiv - x: barPadding - spacing: 1 - width: parent.width-(x*2)-(spacing*(btnCount)) - height: btnHeight - anchors.verticalCenter: parent.verticalCenter - - property int btnCount: toolbar_model_data.length-3 //-3 because Source button not working yet and hiding zoom and pan for now - property real preferredBtnWidth: (width/btnCount) //- (spacing) - - Repeater { - - id: the_view - model: toolbar_model_data - - delegate: chooser - - DelegateChooser { - id: chooser - role: "type" - - DelegateChoice { - roleValue: "FloatScrubber" - - XsViewerSeekEditButton{ - Layout.preferredWidth: rowDiv.preferredBtnWidth - Layout.preferredHeight: parent.height - text: title - shortText: abbr_title - fromValue: float_scrub_min - toValue: float_scrub_max - stepSize: float_scrub_step - decimalDigits: 2 - } - - } - - - DelegateChoice { - roleValue: "ComboBox" - - XsViewerMenuButton - { - Layout.preferredWidth: rowDiv.preferredBtnWidth - Layout.preferredHeight: parent.height - text: title - shortText: abbr_title - valueText: value - } - - } - - DelegateChoice { - roleValue: "OnOffToggle" - - XsViewerToggleButton - { - property bool isZmPan: title == "Zoom (Z)" || title == "Pan (X)" - Layout.preferredWidth: isZmPan ? 0 : rowDiv.preferredBtnWidth - Layout.preferredHeight: parent.height - text: title - shortText: abbr_title - visible: !isZmPan - } - - } - } - } - - } -} \ No newline at end of file diff --git a/ui/qml/reskin/views/viewport/XsViewportTransportBar.qml b/ui/qml/reskin/views/viewport/XsViewportTransportBar.qml deleted file mode 100644 index 772694618..000000000 --- a/ui/qml/reskin/views/viewport/XsViewportTransportBar.qml +++ /dev/null @@ -1,231 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Layouts 1.15 -import QtQml.Models 2.14 -// import Qt.labs.qmlmodels 1.0 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 -import xstudio.qml.helpers 1.0 -import "./widgets" - -Item { - id: transportBar - width: parent.width - height: btnHeight+(barPadding*2) - - property string panelIdForMenu: panelId - - property real barPadding: XsStyleSheet.panelPadding - property real btnWidth: XsStyleSheet.primaryButtonStdWidth - property real btnHeight: XsStyleSheet.widgetStdHeight+(2*2) - - /************************************************************************* - - Access Playhead data - - **************************************************************************/ - XsModelProperty { - id: __playheadLogicalFrame - role: "value" - index: currentPlayheadData.search_recursive("Logical Frame", "title") - } - XsModelProperty { - id: __playheadPlaying - role: "value" - index: currentPlayheadData.search_recursive("playing", "title") - } - Connections { - target: currentPlayheadData // this bubbles up from XsSessionWindow - function onJsonChanged() { - __playheadLogicalFrame.index = currentPlayheadData.search_recursive("Logical Frame", "title") - __playheadPlaying.index = currentPlayheadData.search_recursive("playing", "title") - } - } - property alias playheadLogicalFrame: __playheadLogicalFrame.value - property alias playheadPlaying: __playheadPlaying.value - /*************************************************************************/ - - RowLayout{ - x: barPadding - spacing: barPadding - width: parent.width-(x*2) - height: btnHeight - anchors.verticalCenter: parent.verticalCenter - - RowLayout{ - spacing: 1 - Layout.preferredWidth: btnWidth*5 - Layout.maximumHeight: parent.height - - XsPrimaryButton{ id: rewindButton - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/fast_rewind.svg" - } - XsPrimaryButton{ id: previousButton - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/skip_previous.svg" - } - XsPrimaryButton{ id: playButton - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: playheadPlaying ? "qrc:/icons/pause.svg" : "qrc:/icons/play_arrow.svg" - onClicked: playheadPlaying = !playheadPlaying - } - XsPrimaryButton{ id: nextButton - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/skip_next.svg" - } - XsPrimaryButton{ id: forwardButton - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/fast_forward.svg" - } - } - - XsViewerTextDisplay{ - - id: playheadPosition - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - text: playheadLogicalFrame !== undefined ? playheadLogicalFrame : "-" - modelDataName: playheadPosition.text+"_ButtonMenu"+panelIdForMenu - menuWidth: 175 - - XsMenuModelItem { - text: "Time Display" - menuPath: "" - menuItemType: "multichoice" - menuItemPosition: 1 - choices: ["Frames", "Time", "Timecode", "Frames From Timecode"] - currentChoice: "Frames" - menuModelName: playheadPosition.text+"_ButtonMenu"+panelIdForMenu - } - } - - Rectangle{ id: timeFrame - Layout.fillWidth: true - Layout.preferredHeight: parent.height - color: "black" - } - XsViewerTextDisplay{ id: duration - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - text: "24.0" - modelDataName: duration.text+"_ButtonMenu"+panelIdForMenu - menuWidth: 105 - - - XsMenuModelItem { - text: "Duration" - menuPath: "" - menuItemPosition: 1 - menuItemType: "toggle" - menuModelName: duration.text+"_ButtonMenu"+panelIdForMenu - onActivated: { - } - } - XsMenuModelItem { - text: "Remaining" - menuPath: "" - menuItemPosition: 2 - menuItemType: "toggle" - menuModelName: duration.text+"_ButtonMenu"+panelIdForMenu - onActivated: { - } - } - XsMenuModelItem { - text: "FPS" - menuPath: "" - menuItemPosition: 3 - menuItemType: "toggle" - menuModelName: duration.text+"_ButtonMenu"+panelIdForMenu - onActivated: { - } - } - } - - RowLayout{ - spacing: 1 - Layout.preferredWidth: (btnWidth*4)+spacing*4 - Layout.preferredHeight: parent.height - - XsViewerVolumeButton{ id: volumeButton - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - volume: 4 - } - XsPrimaryButton{ id: loopModeButton - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/repeat.svg" - isActive: loopModeBtnMenu.visible - - onClicked: { - loopModeBtnMenu.x = x-width//*2 - loopModeBtnMenu.y = y-loopModeBtnMenu.height - loopModeBtnMenu.visible = !loopModeBtnMenu.visible - } - - XsMenuNew { - id: loopModeBtnMenu - // visible: false - menu_model: loopModeBtnMenuModel - menu_model_index: loopModeBtnMenuModel.index(-1, -1) - menuWidth: 100 - } - XsMenusModel { - id: loopModeBtnMenuModel - modelDataName: "LoopModeMenu-"+panelIdForMenu - onJsonChanged: { - loopModeBtnMenu.menu_model_index = index(-1, -1) - } - } - XsMenuModelItem { - text: "Play Once" - menuPath: "" - menuItemPosition: 1 - menuItemType: "toggle" - menuModelName: "LoopModeMenu-"+panelIdForMenu - onActivated: { - } - } - XsMenuModelItem { - text: "Loop" - menuPath: "" - menuItemPosition: 2 - menuItemType: "toggle" - menuModelName: "LoopModeMenu-"+panelIdForMenu - onActivated: { - } - } - XsMenuModelItem { - text: "Ping Pong" - menuPath: "" - menuItemPosition: 3 - menuItemType: "toggle" - menuModelName: "LoopModeMenu-"+panelIdForMenu - onActivated: { - } - } - } - XsPrimaryButton{ id: snapshotButton - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/photo_camera.svg" - enabled: false - } - XsPrimaryButton{ id: popoutButton - Layout.preferredWidth: btnWidth - Layout.preferredHeight: parent.height - imgSrc: "qrc:/icons/open_in_new.svg" - enabled: false - } - - } - } - -} \ No newline at end of file diff --git a/ui/qml/reskin/views/viewport/widgets/XsViewerMenuButton.qml b/ui/qml/reskin/views/viewport/widgets/XsViewerMenuButton.qml deleted file mode 100644 index 0e06b0e3d..000000000 --- a/ui/qml/reskin/views/viewport/widgets/XsViewerMenuButton.qml +++ /dev/null @@ -1,206 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.15 -import QtQml.Models 2.12 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 - -Item{ - id: widget - - property alias buttonWidget: buttonWidget - - property color bgColorPressed: palette.highlight //"#D17000" - property color bgColorNormal: "#1AFFFFFF" - property color forcedBgColorNormal: bgColorNormal - property color borderColorNormal: "transparent" - property real borderWidth: 1 - property bool isBgGradientVisible: true - property color textColor: XsStyleSheet.secondaryTextColor //"#F1F1F1" - property var tooltip: "" - property var tooltipTitle: "" - property alias bgDiv: bgDiv - // property var textElide: textDiv.elide - // property alias textDiv: textDiv - property bool clickDisabled: false //enabled - - property string text: "" - property string hotkeyText: "" - property string shortText: "" - property string valueText: "" - property bool isShortened: false - property real shortThresholdWidth: 100 - property bool isShortTextOnly: false - property bool showValueWhenShortened: false - property real shortOnlyThresholdWidth: shortThresholdWidth-40 - //Math.max(60 , statusDiv.textWidth) - //textDiv.textWidth +statusDiv.textWidth - - // property alias menuWidth: btnMenu.menuWidth - property real menuWidth: width - // property alias menu: menuOptions - // property string menuValue: "" //menuOptions.menuAt(menuOptions.currentIndex) - property bool isActive: btnMenu.visible - property bool subtleActive: false - property bool isMultiSelectable: false - - property var menuModel: "" - - function closeMenu() - { - btnMenu.visible = false - } - - function menuTriggered(value){ - valueText = value - closeMenu() - } - - onWidthChanged: { - if(width < shortThresholdWidth) { - isShortened = true - if(width < shortOnlyThresholdWidth) { - if(showValueWhenShortened) isShortTextOnly = false - else isShortTextOnly = true - } - else isShortTextOnly = false - } - else { - isShortened = false - isShortTextOnly = false - } - } - - Button { - id: buttonWidget - - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - anchors.fill: parent - hoverEnabled: !clickDisabled - focusPolicy: Qt.NoFocus - - contentItem: - Item{ id: contentDiv - anchors.fill: parent - opacity: enabled ? 1.0 : 0.33 - - Item{ - width: parent.width>itemsWidth? itemsWidth+2 : parent.width - height: parent.height - anchors.centerIn: parent - clip: true - - property real itemsWidth: textDiv.textWidth +statusDiv.textWidth - - XsText { - id: textDiv - text: isShortened? - showValueWhenShortened? "" : widget.shortText - : hotkeyText==""? - widget.text : - widget.text+" ("+hotkeyText+")" - color: textColor - anchors.verticalCenter: parent.verticalCenter - clip: true - elide: Text.ElideMiddle - // font: buttonWidget.font - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - } - XsText { - id: statusDiv - text: isShortTextOnly? - showValueWhenShortened? valueText : "" - : " "+valueText - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - font.bold: true - anchors.verticalCenter: parent.verticalCenter - anchors.left: textDiv.left - anchors.leftMargin: textDiv.textWidth - clip: true - elide: Text.ElideMiddle - - // Rectangle{anchors.fill: parent; color: "blue"; opacity:.3} - - // onTextWidthChanged:{ - // if(textWidth>textDiv.textWidth) textWidth = textDiv.textWidth - // } - - // width: contentDiv.width - textDiv.textWidth - 2 - // Rectangle{anchors.fill: parent; color: "red"; opacity:.3} - } - } - } - - // XsToolTip{ //.#TODO: - // text: parent.text - // visible: buttonWidget.hovered && parent.truncated - // width: buttonWidget.width == 0? 0 : 150 - // x: 0 - // } - // ToolTip.text: buttonWidget.text - // ToolTip.visible: buttonWidget.hovered && textDiv.truncated - - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - border.color: buttonWidget.down || buttonWidget.hovered ? bgColorPressed: borderColorNormal - border.width: borderWidth - color: "transparent" - - Rectangle{ - visible: isBgGradientVisible - anchors.fill: parent - gradient: Gradient { - GradientStop { position: 0.0; color: buttonWidget.down || (isActive && !subtleActive)? bgColorPressed: "#33FFFFFF" } - GradientStop { position: 1.0; color: buttonWidget.down || (isActive && !subtleActive)? bgColorPressed: forcedBgColorNormal } - } - } - - Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: buttonWidget.activeFocus - color: "transparent" - opacity: 0.33 - border.color: bgColorPressed - border.width: borderWidth - anchors.centerIn: parent - } - } - - //onPressed: focus = true - //onReleased: focus = false - - - onClicked: { - btnMenu.x = x//-btnMenu.width - btnMenu.y = y-btnMenu.height - btnMenu.visible = !btnMenu.visible - } - } - MouseArea{ id: clickBlocker - anchors.centerIn: parent - enabled: clickDisabled - width: enabled? parent.width : 0 - height: enabled? parent.height : 0 - } - - - // This menu works by picking up the 'value' and 'combo_box_options' role - // data that is exposed via the model that instantiated this XsViewerMenuButton - // instance - XsMenuMultiChoice { - id: btnMenu - visible: false - } - -} diff --git a/ui/qml/reskin/views/viewport/widgets/XsViewerSeekEditButton.qml b/ui/qml/reskin/views/viewport/widgets/XsViewerSeekEditButton.qml deleted file mode 100644 index d579b190d..000000000 --- a/ui/qml/reskin/views/viewport/widgets/XsViewerSeekEditButton.qml +++ /dev/null @@ -1,259 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.14 -import QtQuick.Controls 2.14 -import QtQuick.Controls 1.4 -import QtGraphicalEffects 1.15 - -import xStudioReskin 1.0 - -Control{ id: widget - enabled: true - property bool isPressed: false //mouseArea.containsPress - property bool isMouseHovered: mouseArea.containsMouse - property string text: "" - property string shortText: "" - property real fromValue: 1 - property real toValue: 100 - property real defaultValue: toValue - property real prevValue: defaultValue/2 - property real valueText: value !== undefined ? value : 0 - property alias stepSize: mouseArea.stepSize - property int decimalDigits: 2 - - property bool isShortened: false - property real shortThresholdWidth: 99 - property bool isShortTextOnly: false - property bool showValueWhenShortened: false - property real shortOnlyThresholdWidth: 60 - - property color textColor: XsStyleSheet.secondaryTextColor - property color bgColorPressed: palette.highlight - property color bgColorNormal: "#1AFFFFFF" - property color forcedBgColorNormal: bgColorNormal - property color borderColorNormal: "transparent" - property real borderWidth: 1 - property bool isBgGradientVisible: true - - property bool isActive: false - property bool subtleActive: false - - signal editingCompleted() - focusPolicy: Qt.NoFocus - - onWidthChanged: { - if(width < shortThresholdWidth) { - isShortened = true - if(width < shortOnlyThresholdWidth) { - if(showValueWhenShortened) isShortTextOnly = false - else isShortTextOnly = true - } - else isShortTextOnly = false - } - else { - isShortened = false - isShortTextOnly = false - } - } - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - border.color: widget.isPressed || widget.hovered ? bgColorPressed: borderColorNormal - border.width: borderWidth - color: "transparent" - - Rectangle{ - visible: isBgGradientVisible - anchors.fill: parent - gradient: Gradient { - GradientStop { position: 0.0; color: isPressed || (isActive && !subtleActive)? bgColorPressed: "#33FFFFFF" } - GradientStop { position: 1.0; color: isPressed || (isActive && !subtleActive)? bgColorPressed: forcedBgColorNormal } - } - } - - Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: widget.activeFocus - color: "transparent" - opacity: 0.33 - border.color: bgColorPressed - border.width: borderWidth - anchors.centerIn: parent - } - } - - - Rectangle{id: midPoint; width:0; height:1; color:"transparent"; x:parent.width/1.5 } //anchors.centerIn: parent} - Item{ - anchors.centerIn: parent - width: valueDiv.visible? textDiv.width+valueDiv.width : textDiv.width - height: textDiv.height - - XsText{ id: textDiv - text: isShortened? - showValueWhenShortened? "" : widget.shortText - : widget.text - color: textColor - horizontalAlignment: Text.AlignRight - anchors.verticalCenter: parent.verticalCenter - clip: true - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - } - - Rectangle{ - visible: !isBgGradientVisible - width: valueDiv.width - height: valueDiv.height - color: palette.base - - anchors.verticalCenter: valueDiv.verticalCenter - anchors.left: valueDiv.left - anchors.leftMargin: 2.8 - } - XsTextField{ id: valueDiv - visible: text - text: isShortTextOnly? - showValueWhenShortened ? valueText : "" - : " " + valueText.toFixed(decimalDigits) - bgColorNormal: "transparent" - borderColor: bgColorNormal - //focus: isMouseHovered && !isPressed - onFocusChanged:{ - if(focus) { - // drawDialog.requestActivate() - selectAll() - forceActiveFocus() - } - else{ - deselect() - } - } - maximumLength: 5 - // inputMask: "900" - inputMethodHints: Qt.ImhDigitsOnly - // // validator: IntValidator {bottom: 0; top: 100;} - selectByMouse: false - width: textWidth - - horizontalAlignment: Text.AlignHCenter - anchors.left: textDiv.right - // topPadding: (widget.height-height)/2 - anchors.verticalCenter: parent.verticalCenter - - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - - // Rectangle{anchors.fill: parent; color: "yellow"; opacity:.3} - - onEditingFinished:{ - // console.log(widget.text,"onEd_F: ", text) - } - onEditingCompleted:{ - // console.log(widget.text,"onEd_C: ", text) - // accepted() - - // console.log("OnAcc: ", text) - // // if(currentTool != "Erase"){ //#todo - // if(parseFloat(text) >= toValue) { - // value = toValue - // } - // else if(parseFloat(text) <= fromValue) { - // value = fromValue - // } - // else { - // value = parseFloat(text) - // } - // text = "" + value - // selectAll() - // // } - } - - onAccepted:{ - console.log(widget.text,"OnAccepted: ", text) - // if(currentTool != "Erase"){ //#todo - if(parseFloat(text) >= toValue) { - valueText = toValue - } - else if(parseFloat(text) <= fromValue) { - valueText = fromValue - } - else { - valueText = parseFloat(text) - } - text = "" + valueText - selectAll() - // } - } - } - - } - - MouseArea{ - id: mouseArea - anchors.fill: parent - cursorShape: Qt.SizeHorCursor - hoverEnabled: true - propagateComposedEvents: true - - property real prevMX: 0 - property real deltaMX: 0.0 - property real stepSize: 0.25 - property int valueOnPress: 0 - - onMouseXChanged: { - if(isPressed) - { - deltaMX = mouseX - prevMX - let deltaValue = parseFloat(deltaMX*stepSize) - let valueToApply = valueOnPress + deltaValue //Math.round(valueOnPress + deltaValue) - - if(deltaMX>0) - { - if(valueToApply >= toValue) { - value = toValue - valueOnPress = toValue - prevMX = mouseX - } - else { - value = valueToApply - } - } - else { - if(valueToApply < fromValue){ - value = fromValue - valueOnPress = fromValue - prevMX = mouseX - } - else { - value = valueToApply - } - } - } - } - onPressed: { - prevMX = mouseX - valueOnPress = value - - isPressed = true - //focus = true - } - onReleased: { - isPressed = false - //focus = false - } - onDoubleClicked: { - if(value == defaultValue){ - value = prevValue - } - else{ - prevValue = value - value = defaultValue - } - } - } -} \ No newline at end of file diff --git a/ui/qml/reskin/views/viewport/widgets/XsViewerTextDisplay.qml b/ui/qml/reskin/views/viewport/widgets/XsViewerTextDisplay.qml deleted file mode 100644 index 621e22112..000000000 --- a/ui/qml/reskin/views/viewport/widgets/XsViewerTextDisplay.qml +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 -import Qt.labs.qmlmodels 1.0 -// import QtQml.Models 2.14 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 - -Rectangle { - id: widget - color: isActive? Qt.darker(palette.highlight,2) : palette.base - border.color: isHovered? palette.highlight : "transparent" - - property alias text: textDiv.text - property color textColor: palette.highlight - property bool isHovered: mArea.containsMouse - property bool isActive: btnMenu.visible - - property alias modelDataName: btnMenuModel.modelDataName - property alias menuWidth: btnMenu.menuWidth - - XsText { - id: textDiv - text: "" - color: textColor - width: parent.width - anchors.centerIn: parent - tooltipVisibility: isHovered && textDiv.truncated - } - - MouseArea{ - id: mArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - btnMenu.x = x //-btnMenu.width - btnMenu.y = y-btnMenu.height - btnMenu.visible = !btnMenu.visible - } - } - - XsMenuNew { - id: btnMenu - visible: false - menu_model: btnMenuModel - menu_model_index: btnMenuModel.index(0, 0, btnMenuModel.index(-1, -1)) - } - XsMenusModel { - id: btnMenuModel - modelDataName: "" - onJsonChanged: { - btnMenu.menu_model_index = btnMenuModel.index(0, 0, btnMenuModel.index(-1, -1)) - } - } -} \ No newline at end of file diff --git a/ui/qml/reskin/views/viewport/widgets/XsViewerToggleButton.qml b/ui/qml/reskin/views/viewport/widgets/XsViewerToggleButton.qml deleted file mode 100644 index 97b12936a..000000000 --- a/ui/qml/reskin/views/viewport/widgets/XsViewerToggleButton.qml +++ /dev/null @@ -1,122 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 - -import xStudioReskin 1.0 - -Button { - id: widget - - text: "" - width: 10 - height: 10 - - property string hotkeyText: "" - property string shortText: text - property bool isShortened: false - property real shortThresholdWidth: 99 - property bool isShortTextOnly: false - property real shortOnlyThresholdWidth: 60 - - onWidthChanged: { - if(width < shortThresholdWidth) { - isShortened = true - if(width < shortOnlyThresholdWidth) isShortTextOnly = true - else isShortTextOnly = false - } - else { - isShortened = false - isShortTextOnly = false - } - } - - property bool isActive: false - - property color bgColorPressed: palette.highlight - property color bgColorNormal: XsStyleSheet.widgetBgNormalColor - property color forcedBgColorNormal: bgColorNormal - property color borderColorHovered: bgColorPressed - property color borderColorNormal: "transparent" - property real borderWidth: 1 - - property color textColor: XsStyleSheet.secondaryTextColor - property var textElide: textDiv.elide - property alias textDiv: textDiv - - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - hoverEnabled: true - - contentItem: - Item{ - anchors.fill: parent - opacity: enabled ? 1.0 : 0.33 - - Item{ - width: parent.width>itemsWidth? itemsWidth : parent.width - height: parent.height - anchors.centerIn: parent - clip: true - - property real itemsWidth: textDiv.textWidth +statusDiv.textWidth - - XsText { - id: textDiv - text: isShortened? - widget.shortText : - hotkeyText==""? - widget.text : - widget.text+" ("+hotkeyText+")" - color: textColor - - anchors.verticalCenter: parent.verticalCenter - } - XsText { - id: statusDiv - text: isShortTextOnly? "" : value ? " ON":" OFF" - font.bold: true - - anchors.verticalCenter: parent.verticalCenter - anchors.left: textDiv.left - anchors.leftMargin: textDiv.textWidth - } - } - } - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - border.color: widget.down || widget.hovered ? borderColorHovered: borderColorNormal - border.width: borderWidth - gradient: Gradient { - GradientStop { position: 0.0; color: widget.down || isActive? bgColorPressed: "#33FFFFFF" } - GradientStop { position: 1.0; color: widget.down || isActive? bgColorPressed: forcedBgColorNormal } - } - - Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: widget.activeFocus - color: "transparent" - opacity: 0.33 - border.color: borderColorHovered - border.width: borderWidth - anchors.centerIn: parent - } - } - - /*onPressed: focus = true - onReleased: focus = false*/ - focusPolicy: Qt.NoFocus - - onFocusChanged: { - console.log("Button focus", focus) - } - - onClicked: value = !value -} - diff --git a/ui/qml/reskin/views/viewport/widgets/XsViewerVolumeButton.qml b/ui/qml/reskin/views/viewport/widgets/XsViewerVolumeButton.qml deleted file mode 100644 index 832885703..000000000 --- a/ui/qml/reskin/views/viewport/widgets/XsViewerVolumeButton.qml +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 -import QtQuick.Layouts 1.3 - -import xStudioReskin 1.0 - -XsPrimaryButton{ id: volumeButton - imgSrc: isMute? "qrc:/icons/volume_mute.svg": - volume==0? "qrc:/icons/volume_no_sound.svg": - volume<=5? "qrc:/icons/volume_down.svg": - "qrc:/icons/volume_up.svg" - - isActive: popup.visible - - property color bgColorPressed: palette.highlight - property color bgColorNormal: "#1AFFFFFF" - property color forcedBgColorNormal: "#E6676767" //bgColorNormal - - property alias volume: volumeSlider.value - property alias btnIcon: muteButton.imgSrc - property bool isMute: false - - onClicked:{ - popup.open() - } - - XsPopup { id: popup - width: parent.width - height: parent.width*5 - x: 0 - y: -height //+(width/1.25) - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - - XsText{ id: valueDisplay - Layout.preferredHeight: XsStyleSheet.widgetStdHeight+(2*2) - Layout.preferredWidth: parent.width - text: parseInt(volume) - // opacity: isMute? 0.7:1 - font.bold: true - } - - XsSlider{ id: volumeSlider - Layout.fillHeight: true - Layout.preferredWidth: parent.width - orientation: Qt.Vertical - fillColor: isMute? Qt.darker(palette.highlight,2) : palette.highlight - handleColor: isMute? Qt.darker(palette.text,1.2) : palette.text - onValueChanged: isMute = false - - onReleased:{ - popup.close() - } - } - Item{ - Layout.preferredHeight: XsStyleSheet.widgetStdHeight //+(2*2) - Layout.preferredWidth: parent.width - - XsSecondaryButton{ id: muteButton - anchors.centerIn: parent - width: 20 //XsStyleSheet.secondaryButtonStdWidth - height: 20 - imgSrc: "qrc:/icons/volume_mute.svg" - isActive: isMute - property color bgColorPressed: palette.highlight - property color bgColorNormal: "#1AFFFFFF" - property color forcedBgColorNormal: "#E6676767" //bgColorNormal - - - onClicked:{ - isMute = !isMute - popup.close() - } - } - - } - - - } - - } - -} diff --git a/ui/qml/reskin/widgets/bars_and_tabs/XsTab.qml b/ui/qml/reskin/widgets/bars_and_tabs/XsTab.qml deleted file mode 100644 index 9da51682e..000000000 --- a/ui/qml/reskin/widgets/bars_and_tabs/XsTab.qml +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 1.4 - -import xstudio.qml.models 1.0 - -import xStudioReskin 1.0 - -Tab { - id: widget - title: "" - property var viewSource - property var currentViewSource - property var the_panel - - onTitleChanged: { - viewSource = views_model.view_qml_source(title) - } - - // this model lists the 'Views' (e.g. Playlists, Media List, Timeline plus - // and 'views' registered by an xstudio plugin) - XsViewsModel { - id: views_model - } - - Connections { - target: views_model - function onJsonChanged() { - viewSource = views_model.view_qml_source(title) - } - } - - onViewSourceChanged: { - loadPanel() - } - - function loadPanel() { - - if (viewSource == currentViewSource) return; - - let component = Qt.createComponent(viewSource) - currentViewSource = viewSource - - let tab_bg_visible = viewSource != "Viewport" - - if (component.status == Component.Ready) { - - if (the_panel != undefined) the_panel.destroy() - the_panel = component.createObject( - widget, - { - }) - } else { - console.log("Error loading panel:", component, component.errorString()) - } - } -} \ No newline at end of file diff --git a/ui/qml/reskin/widgets/bars_and_tabs/XsTabView.qml b/ui/qml/reskin/widgets/bars_and_tabs/XsTabView.qml deleted file mode 100644 index f91e667f8..000000000 --- a/ui/qml/reskin/widgets/bars_and_tabs/XsTabView.qml +++ /dev/null @@ -1,248 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import Qt.labs.qmlmodels 1.0 -import QtQml.Models 2.14 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 - -TabView{ - - id: widget - - currentIndex: 0 - onCurrentIndexChanged:{ - currentTab = widget.getTab(widget.currentIndex) - } - - property string panelId: "" - property var currentTab: defaultTab - property real buttonSize: XsStyleSheet.menuIndicatorSize - property real panelPadding: XsStyleSheet.panelPadding - property real menuWidth: 160//panelMenu.menuWidth - property real tabWidth: 95 - property bool tab_bg_visible: true - - function addNewTab(title){ - addTab(title, emptyComp) - widget.currentIndex = widget.count-1 - } - - style: TabViewStyle{ - tabsMovable: true - - tabBar: Rectangle{ - color: XsStyleSheet.panelBgColor - - // XsSecondaryButton{ id: addBtn0 - // // visible: false - // width: buttonSize - // height: width - // anchors.right: menuBtn.left - // anchors.rightMargin: 8/2 - // anchors.verticalCenter: menuBtn.verticalCenter - // imgSrc: "qrc:/icons/add.svg" - // smooth: true - // antialiasing: true - - // onClicked: { - // addTab("New", emptyComp) - // widget.currentIndex = widget.count-1 - // // currentTab = widget.getTab(widget.currentIndex) - // } - // Component{ id: emptyComp0 - // Rectangle { - // anchors.fill: parent - // color: "#5C5C5C" - // Text { - // anchors.centerIn: parent - // text: "Empty" - // } - // } - // } - // } - - // For adding a new tab - XsSecondaryButton{ - - id: addBtn - // visible: false - width: buttonSize - height: buttonSize - z: 1 - x: tabWidth*count + panelPadding/2 - anchors.verticalCenter: menuBtn.verticalCenter - imgSrc: "qrc:/icons/add.svg" - - // Rectangle{anchors.fill: parent; color: "red"; opacity:.3} - - onClicked: { - tabTypeMenu.x = x - tabTypeMenu.y = y+height - tabTypeMenu.visible = !tabTypeMenu.visible - } - - } - - XsMenuNew { - id: tabTypeMenu - visible: false - menuWidth: 80 - menu_model: tabTypeModel - menu_model_index: tabTypeModel.index(-1, -1) - } - XsMenusModel { - id: tabTypeModel - modelDataName: "TabMenu"+panelId - onJsonChanged: { - tabTypeMenu.menu_model_index = index(-1, -1) - } - } - - XsSecondaryButton{ id: menuBtn - width: buttonSize - height: buttonSize - anchors.right: parent.right - anchors.rightMargin: panelPadding - anchors.verticalCenter: parent.verticalCenter - imgSrc: "qrc:/icons/menu.svg" - isActive: panelMenu.visible - onClicked: { - panelMenu.x = menuBtn.x-panelMenu.width - panelMenu.y = menuBtn.y //+ menuBtn.height - panelMenu.visible = !panelMenu.visible - } - } - XsMenuNew { - id: panelMenu - visible: false - menu_model: panelMenuModel - menu_model_index: panelMenuModel.index(-1, -1) - menuWidth: widget.menuWidth - } - XsMenusModel { - id: panelMenuModel - modelDataName: "PanelMenu"+panelId - onJsonChanged: { - panelMenu.menu_model_index = index(-1, -1) - } - } - } - - tab: Rectangle{ id: tabDiv - color: styleData.selected? "#5C5C5C":"#474747" //#TODO: to check with UX - implicitWidth: tabWidth //metrics.width + typeSelectorBtn.width + panelPadding*2 //Math.max(metrics.width + 2, 80) - implicitHeight: XsStyleSheet.widgetStdHeight - - - // Rectangle{id: topline - // color: XsStyleSheet.panelTabColor - // width: parent.width - // height: 1 - // } - // Rectangle{id: rightline - // color: XsStyleSheet.panelTabColor - // width: 1 - // height: parent.height - // } - Item{ - anchors.centerIn: parent - width: textDiv.width + typeSelectorBtn.width - - Text{ id: textDiv - text: styleData.title - // width: metrics.width - // width: parent.width - typeSelectorBtn.width-typeSelectorBtn.anchors.rightMargin - // anchors.left: parent.left - // anchors.leftMargin: panelPadding - // anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - color: palette.text - font.bold: styleData.selected - elide: Text.ElideRight - - TextMetrics { - id: metrics - text: textDiv.text - font: textDiv.font - } - - // XsToolTip{ - // text: textDiv.text - // // visible: tabMArea.hovered && parent.truncated - // width: metrics.width == 0? 0 : textDiv.width - // // x: 0 //#TODO: flex/pointer - // } - } - XsSecondaryButton{ id: typeSelectorBtn - width: buttonSize - height: width - anchors.left: textDiv.right - anchors.leftMargin: 1 - // anchors.right: parent.right - // anchors.rightMargin: panelPadding - anchors.verticalCenter: parent.verticalCenter - imgSrc: "qrc:/icons/chevron_right.svg" - rotation: 90 - smooth: true - antialiasing: true - isActive: typeMenu.visible - - onClicked: { - typeMenu.x = typeSelectorBtn.x - typeMenu.y = typeSelectorBtn.y+typeSelectorBtn.height - typeMenu.visible = !typeMenu.visible - } - } - } - - XsMenuNew { - id: typeMenu - visible: false - menuWidth: 80 - menu_model: typeModel - menu_model_index: typeModel.index(-1, -1) - } - - XsMenusModel { - id: typeModel - modelDataName: "TabMenu"+panelId - onJsonChanged: { - typeMenu.menu_model_index = index(-1, -1) - } - } - - XsMenuModelItem { - text: "" - menuPath: "" - // menuItemPosition: 1 - menuItemType: "divider" - menuModelName: "TabMenu"+panelId - onActivated: { - } - } - XsMenuModelItem { - text: "Close Tab" - menuPath: "" - // menuItemPosition: 1 - menuItemType: "button" - menuModelName: "TabMenu"+panelId - onActivated: { - removeTab(getTab(index)) //#TODO: WIP - } - } - } - - frame: Rectangle{ - gradient: Gradient { - GradientStop { position: 0.0; color: "#5C5C5C" } - GradientStop { position: 1.0; color: "#474747" } - } - visible: tab_bg_visible - } - - } - -} \ No newline at end of file diff --git a/ui/qml/reskin/widgets/buttons/XsNavButton.qml b/ui/qml/reskin/widgets/buttons/XsNavButton.qml deleted file mode 100644 index e70fbe7fa..000000000 --- a/ui/qml/reskin/widgets/buttons/XsNavButton.qml +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 - -import xStudioReskin 1.0 - -Button { - id: widget - - text: "" - property bool isActive: false - - property color bgColorPressed: palette.highlight - property color bgColorNormal: "transparent" - property color forcedBgColorNormal: bgColorNormal - property color borderColorHovered: bgColorPressed - property color borderColorNormal: "transparent" - property real borderWidth: 1 - - property color textColorNormal: palette.text - property var textElide: textDiv.elide - property alias textDiv: textDiv - property real textWidth: textDiv.textWidth - - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - hoverEnabled: true - opacity: enabled ? 1.0 : 0.33 - - property bool isShort: false - signal shortened() - onShortened:{ - isShort = true - console.log("NAV_btn_",text,": shortened") - } - signal expanded() - onExpanded:{ - isShort = false - console.log("NAV_btn_",text,": expanded") - } - - contentItem: - Item{ - anchors.fill: parent - XsText { - id: textDiv - text: isShort? widget.shortTerm : widget.text - font: widget.font - color: textColorNormal - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - topPadding: 2 - bottomPadding: 2 - leftPadding: 5 //20 - rightPadding: 5 //20 - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - height: parent.height - - XsToolTip{ - text: widget.text - visible: widget.hovered && parent.truncated - width: metricsDiv.width == 0? 0 : textWidth+22 - // x: 0 //#TODO: flex/pointer - } - } - } - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - border.color: widget.hovered ? borderColorHovered: borderColorNormal - border.width: widget.hovered ? borderWidth : 0 - color: widget.down? bgColorPressed : forcedBgColorNormal - - Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: false //widget.activeFocus //#TODO - color: "transparent" - opacity: 0.33 - border.color: borderColorHovered - border.width: borderWidth - anchors.centerIn: parent - } - Rectangle{ - id: activeIndicator - anchors.bottom: parent.bottom - width: widget.width-(7*2) //textWidth+(7*2); - height: 2 - anchors.horizontalCenter: parent.horizontalCenter - color: isActive? palette.highlight : "transparent" - } - } - - /*onPressed: focus = true - onReleased: focus = false*/ - -} - diff --git a/ui/qml/reskin/widgets/buttons/XsPrimaryButton.qml b/ui/qml/reskin/widgets/buttons/XsPrimaryButton.qml deleted file mode 100644 index c563e5168..000000000 --- a/ui/qml/reskin/widgets/buttons/XsPrimaryButton.qml +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 - -import xStudioReskin 1.0 - -Button { - id: widget - - property alias imgSrc: imageDiv.source - property bool isActive: false - property bool isActiveViaIndicator: true - property bool isActiveIndicatorAtLeft: false - - property alias imageDiv: imageDiv - property color imgOverlayColor: palette.text - property color bgColorPressed: palette.highlight - property color bgColorNormal: XsStyleSheet.widgetBgNormalColor - property color forcedBgColorNormal: bgColorNormal - property color borderColorHovered: bgColorPressed - property color borderColorNormal: "transparent" - property real borderWidth: 1 - focusPolicy: Qt.NoFocus - - hoverEnabled: true - - contentItem: - Item{ - anchors.fill: parent - opacity: enabled ? 1.0 : 0.33 - - XsImage { - id: imageDiv - sourceSize.height: 20 //24 - sourceSize.width: 20 //24 - anchors.centerIn: parent - imgOverlayColor: !pressed && (isActive && !isActiveViaIndicator)? palette.highlight : palette.text - } - - //#TODO: just for timeline-test - XsText { - id: textDiv - visible: imgSrc=="" - text: widget.text - font: widget.font - color: textColorNormal - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - height: parent.height - } - XsToolTip{ - text: widget.text - visible: textDiv.visible? widget.hovered && textDiv.truncated : widget.hovered && widget.text!="" - width: metricsDiv.width == 0? 0 : textDiv.textWidth +10 - // height: widget.height - x: widget.width //#TODO: flex/pointer - y: widget.height - } - } - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - border.color: widget.down || widget.hovered ? borderColorHovered: borderColorNormal - border.width: borderWidth - - gradient: Gradient { - GradientStop { position: 0.0; color: widget.down || (isActive && !isActiveViaIndicator)? bgColorPressed: "#33FFFFFF" } - GradientStop { position: 1.0; color: widget.down || (isActive && !isActiveViaIndicator)? bgColorPressed: forcedBgColorNormal } - } - - Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: widget.activeFocus - color: "transparent" - opacity: 0.33 - border.color: borderColorHovered - border.width: borderWidth - anchors.centerIn: parent - } - Rectangle{ - anchors.bottom: parent.bottom - width: isActiveIndicatorAtLeft? borderWidth*3 : parent.width; - height: isActiveIndicatorAtLeft? parent.height : borderWidth*3 - color: isActiveViaIndicator && isActive? bgColorPressed : "transparent" - } - } - - /*onPressed: focus = true - onReleased: focus = false*/ - -} - diff --git a/ui/qml/reskin/widgets/buttons/XsSearchButton.qml b/ui/qml/reskin/widgets/buttons/XsSearchButton.qml deleted file mode 100644 index 94de70cca..000000000 --- a/ui/qml/reskin/widgets/buttons/XsSearchButton.qml +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 - -import xStudioReskin 1.0 - -Item { - id: widget - - property bool isExpanded: false - property bool isExpandedToLeft: false - property alias imgSrc: searchBtn.imgSrc - property string hint: "" - - width: XsStyleSheet.primaryButtonStdWidth - height: XsStyleSheet.widgetStdHeight + 4 - - XsPrimaryButton{ id: searchBtn - x: isExpandedToLeft? searchBar.width : 0 - width: XsStyleSheet.primaryButtonStdWidth - height: parent.height - imgSrc: "qrc:/icons/search.svg" - text: "Search" - isActive: isExpanded - - onClicked: { - isExpanded = !isExpanded - - if(isExpanded) searchBar.forceActiveFocus() - else { - searchBar.clearSearch() - searchBar.focus = false - } - } - } - - XsSearchBar{ id: searchBar - - Behavior on width { NumberAnimation { duration: 150; easing.type: Easing.OutQuart } } - width: isExpanded? XsStyleSheet.primaryButtonStdWidth * 5 : 0 - - height: parent.height - // anchors.left: searchBtn.right - - placeholderText: isExpanded? hint : "" //activeFocus? "" : hint - - Component.onCompleted: { - if(isExpandedToLeft) anchors.right = searchBtn.left - else anchors.left = searchBtn.right - } - } - -} - diff --git a/ui/qml/reskin/widgets/buttons/XsSecondaryButton.qml b/ui/qml/reskin/widgets/buttons/XsSecondaryButton.qml deleted file mode 100644 index 621af1140..000000000 --- a/ui/qml/reskin/widgets/buttons/XsSecondaryButton.qml +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 - -import xStudioReskin 1.0 - -Button { - id: widget - - property alias imgSrc: imageDiv.source - property bool isActive: false - property bool onlyVisualyEnabled: false - - property color imgOverlayColor: "#C1C1C1" - property color bgColorPressed: palette.highlight - property color bgColorNormal: "transparent" - property color forcedBgColorNormal: bgColorNormal - property color borderColorHovered: bgColorPressed - property color borderColorNormal: "transparent" - property real borderWidth: 1 - - property alias toolTip: toolTip - - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - hoverEnabled: true - smooth: true - antialiasing: true - - contentItem: - Item{ - anchors.fill: parent - opacity: enabled || onlyVisualyEnabled ? 1.0 : 0.33 - XsImage { - id: imageDiv - sourceSize.height: 16 - sourceSize.width: 16 - imgOverlayColor: widget.imgOverlayColor - anchors.centerIn: parent - } - } - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - border.color: widget.down || widget.hovered ? borderColorHovered: borderColorNormal - border.width: borderWidth - color: widget.down || isActive? bgColorPressed : forcedBgColorNormal - - Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: widget.activeFocus - color: "transparent" - opacity: 0.33 - border.color: borderColorHovered - border.width: borderWidth - anchors.centerIn: parent - } - } - - - XsToolTip{ - id: toolTip - text: widget.text - visible: false - width: visible? text.width : 0 //widget.width - x: 0 //#TODO: flex/pointer - } - - onPressed: focus = true - onReleased: focus = false - -} - diff --git a/ui/qml/reskin/widgets/controls/XsScrollBar.qml b/ui/qml/reskin/widgets/controls/XsScrollBar.qml deleted file mode 100644 index 0783a4936..000000000 --- a/ui/qml/reskin/widgets/controls/XsScrollBar.qml +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 - -import xStudioReskin 1.0 - -ScrollBar { id: widget - property color thumbColorPressed: palette.highlight - property color thumbColorHovered: palette.text - property color thumbColorNormal: XsStyleSheet.hintColor - - property real thumbWidth: thumb.implicitWidth - - padding: 0 //.5 - minimumSize: 0.1 - // size: 0.95 - - contentItem: - Rectangle { id: thumb - implicitWidth: 5 - implicitHeight: 5 - radius: width/1.1 - color: widget.pressed ? thumbColorPressed: thumbColorHovered //widget.hovered? thumbColorHovered: thumbColorNormal - opacity: hovered||active? .8:0.4 - } -} \ No newline at end of file diff --git a/ui/qml/reskin/widgets/dialogs/XsOpenSessionDialog.qml b/ui/qml/reskin/widgets/dialogs/XsOpenSessionDialog.qml deleted file mode 100644 index ffac45461..000000000 --- a/ui/qml/reskin/widgets/dialogs/XsOpenSessionDialog.qml +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Dialogs 1.0 - -import QuickFuture 1.0 -import QuickPromise 1.0 - -import xStudio 1.0 - -FileDialog { - title: "Open Session" - //folder: app_window.sessionFunction.defaultSessionFolder() || shortcuts.home - defaultSuffix: "xst" - - nameFilters: ["xStudio (*.xst *.xsz)"] - selectExisting: true - selectMultiple: false - onAccepted: { - console.log("fileUrl", fileUrl) - Future.promise(studio.loadSessionFuture(fileUrl)).then( - function(result){ - // console.log(result) - } - ) - /*app_window.sessionFunction.newRecentPath(fileUrl) - app_window.sessionFunction.defaultSessionFolder(path.slice(0, path.lastIndexOf("/") + 1))*/ - } - onRejected: { - } -} diff --git a/ui/qml/reskin/widgets/dialogs/XsPopup.qml b/ui/qml/reskin/widgets/dialogs/XsPopup.qml deleted file mode 100644 index 0e4f24b45..000000000 --- a/ui/qml/reskin/widgets/dialogs/XsPopup.qml +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 - -import xStudioReskin 1.0 - -Popup { - - id: widget - topPadding: XsStyleSheet.menuPadding - bottomPadding: XsStyleSheet.menuPadding - leftPadding: 0 - rightPadding: 0 - - // parent: Overlay.overlay //#TODO - - property color bgColorPressed: palette.highlight - property color bgColorNormal: "#4B4B4B" //"#5C5C5C" - property color forcedBgColorNormal: bgColorNormal //"#E6676767" - - background: Rectangle{ - implicitWidth: 100 - implicitHeight: 200 - border.width: forcedBgColorNormal==bgColorNormal? 0:1 - border.color: XsStyleSheet.baseColor - gradient: Gradient { - GradientStop { position: 0.0; color: forcedBgColorNormal==bgColorNormal?"#707070":"#F2676767" } - GradientStop { position: 1.0; color: forcedBgColorNormal } - } - } - -} - - diff --git a/ui/qml/reskin/widgets/labels/XsText.qml b/ui/qml/reskin/widgets/labels/XsText.qml deleted file mode 100644 index 35e6b353d..000000000 --- a/ui/qml/reskin/widgets/labels/XsText.qml +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 - -import xStudioReskin 1.0 - -Text { - id: widget - - property color textColorNormal: palette.text - property var textElide: widget.elide - property real textWidth: metrics.width - property alias toolTip: toolTip - property alias tooltipText: toolTip.text - property alias tooltipVisibility: toolTip.visible - property real toolTipWidth: widget.width+5 //150 - - text: "" - color: textColorNormal - - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - // topPadding: 2 - // bottomPadding: 2 - // leftPadding: 20 - // rightPadding: 20 - elide: Text.ElideRight - // width: parent.width - // height: parent.height - - TextMetrics { - id: metrics - font: widget.font - text: widget.text - } - // MouseArea{ - // id: mArea - // anchors.fill: parent - // hoverEnabled: true - // propagateComposedEvents: true - // } - - XsToolTip{ - id: toolTip - text: widget.text - visible: false //mArea.containsMouse && parent.truncated - width: metrics.width == 0? 0 : toolTipWidth - x: 0 //#TODO: flex/pointer - } -} \ No newline at end of file diff --git a/ui/qml/reskin/widgets/labels/XsTextField.qml b/ui/qml/reskin/widgets/labels/XsTextField.qml deleted file mode 100644 index ef59a2098..000000000 --- a/ui/qml/reskin/widgets/labels/XsTextField.qml +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.14 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.12 - -import xStudioReskin 1.0 - -TextField { id: widget - - property color bgColorEditing: palette.highlight - property color bgColorNormal: palette.base - - property color textColorSelection: palette.text - property color textColorEditing: palette.text - property color textColorNormal: "light grey" - property color textColor: palette.text - property real textWidth: text==""? 0 : metrics.width+3 - - property color borderColor: palette.base - property real borderWidth: 1 - - property bool bgVisibility: true - property bool forcedBg: false - property bool forcedHover: false - - signal editingCompleted() - - font.bold: true - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - color: textColor //enabled? focus || hovered? textColorEditing: textColorNormal: Qt.darker(textColorNormal, 1.75) - selectedTextColor: "white" - selectionColor: palette.highlight - - hoverEnabled: true - horizontalAlignment: TextInput.AlignHCenter - - padding: 0 - selectByMouse: true - activeFocusOnTab: true - onEditingFinished: { - editingCompleted() - } - - TextMetrics { - id: metrics - font: widget.font - text: widget.text - } - - background: - Rectangle { - visible: bgVisibility - implicitWidth: width - implicitHeight: height - color: "transparent" //enabled || forcedBg? widget.focus? Qt.darker(bgColorEditing, 2.75): bgColorNormal: Qt.darker(bgColorNormal, 1.75) - border.color: "transparent" //widget.focus || widget.hovered || forcedHover? bgColorEditing: borderColor - } - -} diff --git a/ui/qml/reskin/widgets/labels/XsToolTip.qml b/ui/qml/reskin/widgets/labels/XsToolTip.qml deleted file mode 100644 index d66981593..000000000 --- a/ui/qml/reskin/widgets/labels/XsToolTip.qml +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 - -import xStudioReskin 1.0 - -ToolTip { - id: widget - - property alias textDiv: textDiv - property alias metricsDiv: metricsDiv - - property color bgColor: palette.text - property color textColor: palette.base - property real panelPadding: XsStyleSheet.panelPadding - - delay: 100 - timeout: 1000 - - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - - rightPadding: 0 - leftPadding: 0 - - TextMetrics { - id: metricsDiv - font: textDiv.font - text: textDiv.text - } - - contentItem: Text { - id: textDiv - text: widget.text - font: widget.font - color: textColor - // width: widget.width - leftPadding: panelPadding - rightPadding: panelPadding - wrapMode: Text.Wrap //WrapAnywhere - } - - background: Rectangle { - color: bgColor - - Rectangle { - id: shadowDiv - color: "#000000" - opacity: 0.2 - x: 2 - y: -2 - z: -1 - } - } - -} \ No newline at end of file diff --git a/ui/qml/reskin/widgets/menus/XsMainMenuBar.qml b/ui/qml/reskin/widgets/menus/XsMainMenuBar.qml deleted file mode 100644 index 2528dde14..000000000 --- a/ui/qml/reskin/widgets/menus/XsMainMenuBar.qml +++ /dev/null @@ -1,381 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 1.4 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 - -Rectangle { - - id: menu_bar - height: XsStyleSheet.menuHeight - // color: XsStyleSheet.menuBarColor - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.lighter( XsStyleSheet.menuBarColor, 1.15) } - GradientStop { position: 1.0; color: Qt.darker( XsStyleSheet.menuBarColor, 1.15) } - } - - // this gives us access to the global tree model that defines menus, - // sub-menus and menu items - XsMenusModel { - id: menus_model - modelDataName: "main menu bar" - onJsonChanged: { - root_index = index(-1, -1) - } - } - - // This index points us to the 'main menu bar' branch of - // the global tree model - property var root_index: menus_model.index(-1, -1) - - // XsMenuModelItem { - // text: "Save Session" - // menuPath: "Session|Current Session" - // menuItemPosition: 1 - // menuModelName: "main menu bar" - // hotkey: "Ctrl+Z" - // onActivated: { - // } - // } - - // XsMenuModelItem { - // menuItemType: "divider" - // menuPath: "" - // menuItemPosition: 3 - // menuModelName: "main menu bar" - // } - - XsMenuModelItem { - text: "New Session" - menuPath: "File" - menuItemPosition: 1 - menuModelName: "main menu bar" - onActivated: { - } - } - XsMenuModelItem { - text: "Open Session" - menuPath: "File" - menuItemPosition: 2 - menuModelName: "main menu bar" - onActivated: { - var component = Qt.createComponent("qrc:/widgets/dialogs/XsOpenSessionDialog.qml"); - if (component.status == Component.Ready) { - var dialog = component.createObject(parent) - dialog.open() - } else { - console.log("Error loading component:", component.errorString()); - } - } - } - XsMenuModelItem { - text: "Save Session" - menuPath: "File" - menuItemPosition: 3 - menuModelName: "main menu bar" - onActivated: { - } - } - XsMenuModelItem { - menuItemType: "divider" - menuPath: "File" - menuItemPosition: 4 - menuModelName: "main menu bar" - } - XsMenuModelItem { - text: "Quit" - menuPath: "File" - menuItemPosition: 5 - menuModelName: "main menu bar" - onActivated: { - Qt.quit() - } - } - - XsMenuModelItem { - text: "Cut" - menuPath: "Edit" - menuItemPosition: 1 - menuModelName: "main menu bar" - onActivated: { - } - } - - XsMenuModelItem { - text: "New" - menuPath: "Playlists" - menuItemPosition: 1 - menuModelName: "main menu bar" - onActivated: { - } - } - - XsMenuModelItem { - text: "Flag Media" - menuPath: "Media" - menuItemPosition: 1 - menuModelName: "main menu bar" - onActivated: { - } - } - - XsMenuModelItem { - text: "New Sequence" - menuPath: "Timeline" - menuItemPosition: 1 - menuModelName: "main menu bar" - onActivated: { - } - } - - XsMenuModelItem { - text: "Play/Pause" - menuPath: "Playback" - menuItemPosition: 1 - menuModelName: "main menu bar" - onActivated: { - } - } - - XsMenuModelItem { - text: "Hide UI" - menuPath: "Viewer" - menuItemPosition: 1 - menuModelName: "main menu bar" - onActivated: { - } - } - - XsMenuModelItem { - text: "Save Layout.." - menuPath: "Layout" - menuItemPosition: 1 - menuModelName: "main menu bar" - onActivated: { - } - } - - XsMenuModelItem { - text: "Drawing Tools" - menuPath: "Panels" - menuItemPosition: 1 - menuModelName: "main menu bar" - onActivated: { - } - } - - XsMenuModelItem { - menuItemType: "divider" - menuPath: "Panels" - menuModelName: "main menu bar" - } - XsMenuModelItem { - text: "Red" - menuPath: "Panels|Settings|UI Accent Colour" - menuModelName: "main menu bar" - onActivated: { - XsStyleSheet.accentColor = accentColorModel.get(3).value - } - } - XsMenuModelItem { - text: "Orange" - menuPath: "Panels|Settings|UI Accent Colour" - menuModelName: "main menu bar" - onActivated: { - XsStyleSheet.accentColor = accentColorModel.get(4).value - } - } - XsMenuModelItem { - text: "Yellow" - menuPath: "Panels|Settings|UI Accent Colour" - menuModelName: "main menu bar" - onActivated: { - XsStyleSheet.accentColor = accentColorModel.get(5).value - } - } - XsMenuModelItem { - text: "Green" - menuPath: "Panels|Settings|UI Accent Colour" - menuModelName: "main menu bar" - onActivated: { - XsStyleSheet.accentColor = accentColorModel.get(6).value - } - } - XsMenuModelItem { - text: "Blue" - menuPath: "Panels|Settings|UI Accent Colour" - menuModelName: "main menu bar" - onActivated: { - XsStyleSheet.accentColor = accentColorModel.get(0).value - } - } - XsMenuModelItem { - text: "Purple" - menuPath: "Panels|Settings|UI Accent Colour" - menuModelName: "main menu bar" - onActivated: { - XsStyleSheet.accentColor = accentColorModel.get(1).value - } - } - XsMenuModelItem { - text: "Pink" - menuPath: "Panels|Settings|UI Accent Colour" - menuModelName: "main menu bar" - onActivated: { - XsStyleSheet.accentColor = accentColorModel.get(2).value - } - } - XsMenuModelItem { - text: "Graphite" - menuPath: "Panels|Settings|UI Accent Colour" - menuModelName: "main menu bar" - onActivated: { - XsStyleSheet.accentColor = accentColorModel.get(7).value - } - } - - - XsMenuModelItem { - text: "ShotGrid" - menuPath: "Publish" - menuItemPosition: 1 - menuModelName: "main menu bar" - onActivated: { - } - } - - XsMenuModelItem { - text: "Publish All" - menuPath: "Playlists|Publish" - menuItemPosition: 1 - hotkey: "Ctrl+P" - menuModelName: "main menu bar" - onActivated: { - } - } - - - XsMenuModelItem { - text: "Bypass Colour Management" - menuItemType: "toggle" - menuPath: "Colour" - menuItemPosition: 1 - isChecked: false - menuModelName: "main menu bar" - onIsCheckedChanged: { - console.log("isChecked", isChecked) - } - } - - XsMenuModelItem { - text: "Channels" - menuPath: "" - menuItemType: "multichoice" - menuItemPosition: 1 - choices: ["RGB", "R", "G", "B", "A", "Luminance"] - currentChoice: "Luminance" - menuModelName: "main menu bar" - onCurrentChoiceChanged: { - console.log("currentChoice", currentChoice) - } - } - - // XsMenuModelItem { - // text: "UI Accent Colour(WIP)" - // menuPath: "Colour" - // menuItemType: "multichoice" - // menuItemPosition: 1 - // choices: ["Blue", "Purple", "Pink", "Red", "Orange", "Yellow", "Green", "Graphite"] - // currentChoice: "Orange" - // menuModelName: "main menu bar" - // onCurrentChoiceChanged: { - // console.log("currentChoice", currentChoice) - // } - // } - - - XsMenuModelItem { - text: "About" - menuPath: "Help" - menuItemPosition: 1 - menuModelName: "main menu bar" - onActivated: { - } - } - - - ListModel { id: accentColorModel - - ListElement { - name: qsTr("Blue") - value: "#307bf6" - } - ListElement { - name: qsTr("Purple") - value: "#9b56a3" - } - ListElement { - name: qsTr("Pink") - value: "#e65d9c" - } - ListElement { - name: qsTr("Red") - value: "#ed5f5d" - } - ListElement { - name: qsTr("Orange") - value: "#e9883a" - } - ListElement { - name: qsTr("Yellow") - value: "#f3ba4b" - } - ListElement { - name: qsTr("Green") - value: "#77b756" - } - ListElement { - name: qsTr("Graphite") - value: "#999999"//"#666666" - } - - } - - - - XsListView { - - anchors.fill: parent - orientation: ListView.Horizontal - isScrollbarVisibile: false - - model: DelegateModel { - - model: menus_model - rootIndex: root_index - - delegate: XsMenuItemNew { - - menu_model: menus_model - - // As we loop over the top level items in the 'main menu bar' - // here, we set the index to row=index, column=0. This takes - // us one step deeper into the tree on each iteration - menu_model_index: menus_model.index(index, 0, root_index) - - // This var simply tells the pop-up to appear below the menu - // item rather than to the right of it. - is_in_bar: true - - parent_menu: menu_bar - - } - - } - - } - -} \ No newline at end of file diff --git a/ui/qml/reskin/widgets/menus/XsMenu.qml b/ui/qml/reskin/widgets/menus/XsMenu.qml deleted file mode 100644 index f847086de..000000000 --- a/ui/qml/reskin/widgets/menus/XsMenu.qml +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 - -XsPopup { - - id: the_popup - // x: 30 - height: view.height+ (topPadding+bottomPadding) - width: view.width - - property var menu_model - property var menu_model_index - - property alias menuWidth: view.width - - ListView { - - id: view - orientation: ListView.Vertical - spacing: 0 - width: 160 //contentWidth - height: contentHeight - contentHeight: contentItem.childrenRect.height - contentWidth: contentItem.childrenRect.width - snapMode: ListView.SnapToItem - - model: DelegateModel { - - // setting up the model and rootIndex like this causes us to - // iterate over the children of the node in the 'menu_model' that - // is found at 'menu_model_index'. Note that the delegate will - // be assigned a value 'index' which is its index in the list of - // children. - model: the_popup.menu_model - rootIndex: the_popup.menu_model_index - delegate: chooser - - DelegateChooser { - id: chooser - role: "menu_item_type" - - DelegateChoice { - roleValue: "button" - - XsMenuItemNew { - // again, we pass in the model to the menu item and - // step one level deeper into the tree by useing row=index - menu_model: the_popup.menu_model - menu_model_index: the_popup.menu_model.index( - index, // row = child index - 0, // column = 0 (always, we don't use columns) - the_popup.menu_model_index // the parent index into the model - ) - parent_menu: the_popup - parentWidth: view.width - - // icon: "qrc:/icons/filter_none.svg" - } - - } - - DelegateChoice { - roleValue: "menu" - - XsMenuItemNew { - // again, we pass in the model to the menu item and - // step one level deeper into the tree by useing row=index - menu_model: the_popup.menu_model - menu_model_index: the_popup.menu_model.index( - index, // row = child index - 0, // column = 0 (always, we don't use columns) - the_popup.menu_model_index // the parent index into the model - ) - parent_menu: the_popup - parentWidth: view.width - } - } - - DelegateChoice { - roleValue: "divider" - - XsMenuDivider { - parentWidth: view.width - } - - } - - DelegateChoice { - roleValue: "multichoice" - - XsMenuItemNew { - menu_model: the_popup.menu_model - menu_model_index: the_popup.menu_model.index(index, 0, the_popup.menu_model_index) - - parent_menu: the_popup - parentWidth: view.width - } - - } - - DelegateChoice { - roleValue: "toggle" - - XsMenuItemToggle { - menu_model: the_popup.menu_model - menu_model_index: the_popup.menu_model.index(index, 0, the_popup.menu_model_index) - - parent_menu: the_popup - parentWidth: view.width - - onClicked: { - isChecked = !isChecked - } - } - - } - - DelegateChoice { - roleValue: "toggle_settings" - - XsMenuItemToggleWithSettings { - menu_model: the_popup.menu_model - menu_model_index: the_popup.menu_model.index(index, 0, the_popup.menu_model_index) - - parent_menu: the_popup - parentWidth: view.width - - onChecked:{ - isChecked = !isChecked - } - } - - } - - DelegateChoice { - roleValue: "toggle_checkbox" - - XsMenuItemToggle { - menu_model: the_popup.menu_model - menu_model_index: the_popup.menu_model.index(index, 0, the_popup.menu_model_index) - - isRadioButton: true - parent_menu: the_popup - parentWidth: view.width - - onClicked:{ - isChecked = !isChecked - } - } - - } - - - } - } - - } -} - - diff --git a/ui/qml/reskin/widgets/menus/XsMenuChoice.qml b/ui/qml/reskin/widgets/menus/XsMenuChoice.qml deleted file mode 100644 index 4bdb2a5a7..000000000 --- a/ui/qml/reskin/widgets/menus/XsMenuChoice.qml +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 - -Popup { - - // Note that the model gives use a string 'current_choice', plus - // a list of strings 'choices' - - id: the_popup - height: view.height+ (topPadding+bottomPadding) - width: view.width - topPadding: XsStyleSheet.menuPadding - bottomPadding: XsStyleSheet.menuPadding - leftPadding: 0 - rightPadding: 0 - - property var menu_model - property var menu_model_index - - property color bgColorNormal: "#1AFFFFFF" - property color forcedBgColorNormal: "#EE444444" //bgColorNormal - - background: Rectangle{ - implicitWidth: 100 - implicitHeight: 200 - gradient: Gradient { - GradientStop { position: 0.0; color: forcedBgColorNormal==bgColorNormal?"#33FFFFFF":"#EE222222" } - GradientStop { position: 1.0; color: forcedBgColorNormal } - } - } - - ListView { - - id: view - orientation: ListView.Vertical - spacing: 0 - width: contentWidth - height: contentHeight - contentHeight: contentItem.childrenRect.height - contentWidth: contentItem.childrenRect.width - snapMode: ListView.SnapToItem - currentIndex: -1 - - model: DelegateModel { - - model: choices - - delegate: XsMenuItemToggle{ - isRadioButton: true - radioSelectedChoice: current_choice - onChecked:{ - current_choice = label - } - - Component.onCompleted: { - label = choices[index] - // is_checked = current_choice == label - } - } - - } - - } -} - - diff --git a/ui/qml/reskin/widgets/menus/XsMenuDivider.qml b/ui/qml/reskin/widgets/menus/XsMenuDivider.qml deleted file mode 100644 index 901350266..000000000 --- a/ui/qml/reskin/widgets/menus/XsMenuDivider.qml +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 - -import xStudioReskin 1.0 - -Item { - width: parentWidth - height: XsStyleSheet.menuPadding*2 + XsStyleSheet.menuDividerHeight - - property real parentWidth: 0 - - Rectangle { - width: parent.width - height: XsStyleSheet.menuDividerHeight - anchors.verticalCenter: parent.verticalCenter - color: XsStyleSheet.menuDividerColor - } -} \ No newline at end of file diff --git a/ui/qml/reskin/widgets/menus/XsMenuItem.qml b/ui/qml/reskin/widgets/menus/XsMenuItem.qml deleted file mode 100644 index 174f07d25..000000000 --- a/ui/qml/reskin/widgets/menus/XsMenuItem.qml +++ /dev/null @@ -1,245 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 -import QtGraphicalEffects 1.15 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 - -Item { - - id: widget - // width: is_in_bar? menuWidth:parentWidth //( (menuWidth > menuStdWidth) || is_in_bar )? menuWidth : menuStdWidth - // width: ( (menuWidth > menuStdWidth) || is_in_bar )? menuWidth : menuStdWidth - width: menuWidth //( (menuWidth > menuStdWidth) || is_in_bar )? menuWidth : menuStdWidth - height: XsStyleSheet.menuHeight - - property var menu_model - property var menu_model_index - property var sub_menu: null - property bool is_in_bar: false - property var parent_menu - property alias icon: iconDiv.source - property alias colourIndicatorValue: colourIndicatorDiv.color - - property bool isHovered: menuMouseArea.containsMouse - property bool isActive: menuMouseArea.pressed || isSubMenuActive - property bool isFocused: menuMouseArea.activeFocus - property bool isSubMenuActive: sub_menu? sub_menu.visible : false - - property real parentWidth: 0 - property real menuWidth: parentWidth>" : "") - height: parent.height - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - color: labelColor - anchors.left: iconDiv.visible || colourIndicatorDiv.visible? iconDiv.right : parent.left - anchors.leftMargin: labelPadding - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - - width: parent.width - clip: true - } - Text { id: hotKeyDiv - text: hotkey ? " " + hotkey : "" - height: parent.height - font: labelDiv.font - color: hotKeyColor - anchors.right: parent.right - anchors.rightMargin: labelPadding - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - } - TextMetrics { - id: labelMetrics - font: labelDiv.font - text: labelDiv.text + hotKeyDiv.text - // Component.onCompleted: { - // console.log("matrix:", width, text, menuRealWidth) - // } - } - - Image { id: subMenuIndicatorDiv - visible: sub_menu? !is_in_bar: false - source: "qrc:/icons/chevron_right.svg" - sourceSize.height: 16 - sourceSize.width: 16 - horizontalAlignment: Image.AlignHCenter - verticalAlignment: Image.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: labelPadding - smooth: true - antialiasing: true - layer { - enabled: true - effect: ColorOverlay { color: hotKeyColor } - } - } - - Component.onCompleted: { - make_submenus() - } - - function make_submenus() { - - if (!menu_model_index.valid) return; - if (menu_model.rowCount(menu_model_index)) { - let component = Qt.createComponent("./XsMenu.qml") - if (component.status == Component.Ready) { - sub_menu = component.createObject( - widget, - { - menu_model: widget.menu_model, - menu_model_index: widget.menu_model_index - }) - - } else { - console.log("Failed to create menu component: ", component, component.errorString()) - } - } else if (menu_model.get(menu_model_index,"menu_item_type") == "multichoice") { - let component = Qt.createComponent("./XsMenuMultiChoice.qml") - if (component.status == Component.Ready) { - sub_menu = component.createObject( - widget, - { - menu_model: widget.menu_model, - menu_model_index: widget.menu_model_index, - - parent_menu: widget, - parentWidth: widget.parentWidth - }) - - } else { - console.log("Failed to create menu component: ", component, component.errorString()) - } - } - - } - - -} \ No newline at end of file diff --git a/ui/qml/reskin/widgets/menus/XsMenuItemToggle.qml b/ui/qml/reskin/widgets/menus/XsMenuItemToggle.qml deleted file mode 100644 index 29e5a897c..000000000 --- a/ui/qml/reskin/widgets/menus/XsMenuItemToggle.qml +++ /dev/null @@ -1,169 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQml.Models 2.14 -import Qt.labs.qmlmodels 1.0 -import QtGraphicalEffects 1.12 - -import xStudioReskin 1.0 -import xstudio.qml.models 1.0 - -Item { - - // Note that the model gives use a boolean 'is_checked', plus 'name' and - // 'hotkey' strings. - - id: widget - width: parentWidth //(menuWidth > menuStdWidth)? menuWidth : menuStdWidth - height: XsStyleSheet.menuHeight - - property var menu_model - property var menu_model_index - property var parent_menu - - property string label: name ? name : "" - property bool isChecked: isRadioButton? radioSelectedChoice==label : is_checked - - signal clicked() - - property bool isHovered: menuMouseArea.containsMouse - property bool isActive: menuMouseArea.pressed - property bool isFocused: menuMouseArea.activeFocus - - property real parentWidth: 0 - property real menuWidth: parentWidth menuStdWidth)? menuWidth : menuStdWidth - height: XsStyleSheet.menuHeight - - property var menu_model - property var menu_model_index - property var parent_menu - - property string label: name ? name : "" - property bool isChecked: isRadioButton? - radioSelectedChoice==label : - is_checked? is_checked : false //#TODO - - signal checked() - - property bool isHovered: menuMouseArea.containsMouse - property bool isActive: menuMouseArea.pressed - property bool isFocused: menuMouseArea.activeFocus - - property real parentWidth: 0 - property real menuWidth: parentWidth"+widget.menuValue+"" - font: buttonWidget.font - color: textColorPressed - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - topPadding: 4 - bottomPadding: 4 - leftPadding: 20 - rightPadding: 20 - anchors.horizontalCenter: parent.horizontalCenter - elide: Text.ElideRight - width: parent.width - height: parent.height - } - } - - - ToolTip.text: buttonWidget.text - ToolTip.visible: buttonWidget.hovered && textDiv.truncated - - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - border.color: buttonWidget.down || buttonWidget.hovered ? bgColorPressed: borderColorNormal - border.width: borderWidth - - gradient: Gradient { - GradientStop { position: 0.0; color: buttonWidget.down || (isActive && !subtleActive)? bgColorPressed: "#33FFFFFF" } - GradientStop { position: 1.0; color: buttonWidget.down || (isActive && !subtleActive)? bgColorPressed: forcedBgColorNormal } - } - - Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: buttonWidget.activeFocus - color: "transparent" - opacity: 0.33 - border.color: bgColorPressed - border.width: borderWidth - anchors.centerIn: parent - } - } - - /*onPressed: focus = true - onReleased: focus = false*/ - onClicked: menuOptions.toggleShow() - - } - - XsViewerMenu { id: menuOptions - width: parent.width*2 - x: buttonWidget.x - y: buttonWidget.y + buttonWidget.height - - forcedBgColorNormal: widget.forcedBgColorNormal - - ActionGroup{id: actionGroup} - - Instantiator { id: dynamicMenu - active: true - model: menuModel - - delegate: XsMenuItem { - mytext: menuText - actiongroup: isSelectable && !isMultiSelectable? actionGroup : null - hasSubMenu: hasSub - shortcut: key2===""? key1 : key1+"+"+key2 - // isMultiCheckable: isMultiSelectable - isCheckable: isSelectable - isChecked: isSelected - - onTriggered: { - menuValue= menuText //dynamicMenu.objectAt(index). - } - } - - onObjectAdded: menuOptions.insertItem(index, object) - onObjectRemoved: menuOptions.removeItem(index, object) - } - - // XsViewerMenuItem { - // bgColorPressed: palette.highlight - // mytext: "Default_0"; onTriggered: menuValue= mytext - // shortcut: "Key" - // } - - } - -} diff --git a/ui/qml/reskin/widgets/prototypes/new/XsViewerMenu.qml b/ui/qml/reskin/widgets/prototypes/new/XsViewerMenu.qml deleted file mode 100644 index 60f1a73db..000000000 --- a/ui/qml/reskin/widgets/prototypes/new/XsViewerMenu.qml +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.12 - -import xStudioReskin 1.0 - -Menu { - id: widget - - dim: false - topPadding: 4 - bottomPadding: 4 - - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - - property bool hasCheckable: false - property color bgColorNormal: "#1AFFFFFF" - property color forcedBgColorNormal: bgColorNormal - - background: Rectangle { - id: bgrect - gradient: Gradient { - GradientStop { position: 0.0; color: "#33FFFFFF" } - GradientStop { position: 1.0; color: forcedBgColorNormal } - } - } - - delegate: XsMenuItem{} - - function toggleShow() { - if(visible) close() - else open() - } - - - - - - Component.onCompleted: { - var curritem; - for (var i=0; i"+widget.menuValue+"" : widget.text - font: buttonWidget.font - color: textColorPressed - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - topPadding: 4 - bottomPadding: 4 - leftPadding: 20 - rightPadding: 20 - anchors.horizontalCenter: parent.horizontalCenter - elide: Text.ElideRight - width: parent.width - height: parent.height - - XsToolTip{ - text: parent.text - visible: buttonWidget.hovered && parent.truncated - width: buttonWidget.width == 0? 0 : 150 - x: 0 - } - } - } - - - // ToolTip.text: buttonWidget.text - // ToolTip.visible: buttonWidget.hovered && textDiv.truncated - - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - border.color: buttonWidget.down || buttonWidget.hovered ? bgColorPressed: borderColorNormal - border.width: borderWidth - - gradient: Gradient { - GradientStop { position: 0.0; color: buttonWidget.down || (isActive && !subtleActive)? bgColorPressed: "#33FFFFFF" } - GradientStop { position: 1.0; color: buttonWidget.down || (isActive && !subtleActive)? bgColorPressed: forcedBgColorNormal } - } - - Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: buttonWidget.activeFocus - color: "transparent" - opacity: 0.33 - border.color: bgColorPressed - border.width: borderWidth - anchors.centerIn: parent - } - } - - onPressed: focus = true - onReleased: focus = false - - onClicked: menuOptions.toggleShow() - } - - - XsViewerMenu { id: menuOptions - - title: "" - width: parent.width - x: buttonWidget.x - y: buttonWidget.y + buttonWidget.height - - forcedBgColorNormal: widget.forcedBgColorNormal - - ActionGroup{id: actionGroup} - - Instantiator { id: dynamicMenu - active: true - model: menuModel - - delegate: XsMenuItem { - mytext: menuText - actiongroup: isSelectable && !isMultiSelectable? actionGroup : null - - // hasSubMenu: hasSub - // shortcut: key2!==""? key1+"+"+key2:key - isMultiCheckable: isMultiSelectable - isCheckable: isSelectable - isChecked: isSelected - - onTriggered: { - menuValue= menuText //dynamicMenu.objectAt(index). - menuTriggered() - } - } - - onObjectAdded: menuOptions.insertItem(index, object) - onObjectRemoved: menuOptions.removeItem(index, object) - } - - // XsViewerMenuItem { - // mytext: "Default"; onTriggered: menuValue= mytext - // isMultiCheckable: isMultiSelectable - // isCheckable: isSelectable - // // subMenu: true - // } - } - - -} diff --git a/ui/qml/reskin/widgets/prototypes/new/XsViewerMenuItem.qml b/ui/qml/reskin/widgets/prototypes/new/XsViewerMenuItem.qml deleted file mode 100644 index b24a56910..000000000 --- a/ui/qml/reskin/widgets/prototypes/new/XsViewerMenuItem.qml +++ /dev/null @@ -1,219 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.5 -import QtGraphicalEffects 1.12 - -import xStudioReskin 1.0 - -MenuItem { - id: widget - - implicitHeight: visible ? 22 : 0 - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - - property color bgColorPressed: palette.highlight //XsStyle.hoverColor - property string iconbg: "" - property real iconsize: 16 //XsStyle.menuItemHeight *.66 - - property alias mytext: myAction.text - property bool isMultiCheckable: false - property alias isCheckable: myAction.checkable - property alias isChecked: myAction.checked - property string shortcut: "" - property var actiongroup - property string imgSrc: "" - - property bool hasSubMenu: false - - action: Action { - id: myAction - ActionGroup.group: typeof(actiongroup)!= "undefined" ? actiongroup : null - onTriggered: { - if(widget.checkable && actiongroup) { - if (!myAction.checked) { - myAction.checked = true - } - } - } - } - - arrow: Image { - id:myIcon - source:"qrc:/icons/chevron_right.svg" - sourceSize.height: 16//parent.height - sourceSize.width: 16//parent.width - x: parent.width - width - 5 - visible: widget.subMenu || widget.hasSubMenu - anchors.verticalCenter: parent.verticalCenter - - ColorOverlay{ - id: myIconOverlay - anchors.fill: myIcon - source: myIcon - color: textPart.color - antialiasing: true - } - - } - - indicator: Item { - id:checkBoxItem - // Icon (radio or checkbox) - implicitWidth: widget.checkable? XsStyle.menuItemHeight : 0 - implicitHeight: XsStyle.menuItemHeight - visible: widget.checkable - x: 3 - - Rectangle { - width: iconsize - height: iconsize - Gradient { - id:indGrad - orientation: Gradient.Horizontal - } - - color: "transparent" // widget.checked?XsStyle.primaryColor:"transparent" - // gradient:iconbg===""?widget.checked?styleGradient.accent_gradient:Gradient.gradient:getBGGrad() - function getBGGrad() { - preferences.assignColorStringToGradient(iconbg, indGrad) - return indGrad - } - - anchors.centerIn: parent - // border.width: 1 // checkableDecorated?1:0 - // border.color: actiongroup? - // widget.checked?bgColorPressed:widget.highlighted?bgColorPressed:XsStyle.mainColor - // :widget.highlighted?bgColorPressed:XsStyle.mainColor - // radius: actiongroup?iconsize / 2:0 - - Image { - id: checkIcon - visible: true - source: widget.multiCheckable? widget.checked? "qrc:/icons/check_box_checked.svg" : "qrc:/icons/check_box_unchecked.svg" - : widget.checked? "qrc:/icons/radio_button_checked.svg" : "qrc:/icons/radio_button_unchecked.svg" - width: iconsize // 2 - height: iconsize // 2 - anchors.centerIn: parent - - layer { - enabled: true - effect: ColorOverlay { color: widget.checked? widget.highlighted?"#F1F1F1":bgColorPressed:"#C1C1C1" } - } - } - - } - } - - // icon - // icon.source:imgSrc//typeof(widget.imgSrc) === "undefined"?"":widget.imgSrc - // Item { - // Image { - // id: name - // source: widget.imgSrc - // } - // } - - property alias textPart: contentPart.textPart - contentItem: Rectangle { - id: contentPart - color: "transparent" - anchors.fill: parent - implicitHeight: XsStyle.menuItemHeight - implicitWidth: textPart.implicitWidth + shortcutPart.implicitWidth - - Image { - id: iconPart - source: imgSrc - anchors.right: contentPart.right - anchors.rightMargin: 5 - anchors.verticalCenter: contentPart.verticalCenter - visible: imgSrc !== "" - width: iconsize - height: iconsize - - ColorOverlay{ - anchors.fill: iconPart - source:iconPart - visible: iconPart.visible - color: bgColorPressed - antialiasing: true - } - } - - property alias textPart:textPart - Text { - id: textPart - anchors.fill: parent - // anchors.top: parent.top - // anchors.bottom: parent.bottom - // anchors.right: parent.right - // anchors.left: iconPart.visible?iconPart.right:parent.left - leftPadding: widget.menu === null?iconsize: - widget.menu.hasCheckable?widget.indicator.width + 6: - iconPart.visible?iconsize + 10:iconsize - rightPadding: widget.arrow.width - text: widget.mytext //?XsUtils.replaceMenuText(widget.mytext):widget.text - // font: widget.font - font.pixelSize: XsStyle.menuFontSize - font.family: XsStyle.menuFontFamily - font.hintingPreference: Font.PreferNoHinting - opacity: enabled ? 1.0 : 0.5 - color: "#F1F1F1" //widget.subMenu?widget.subMenu.fakeDisabled?"#88ffffff":XsStyle.hoverColor:XsStyle.hoverColor - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - Text { - id: shortcutPart - padding: 5 - anchors.right: parent.right - anchors.rightMargin: widget.subMenu?20:5 - anchors.verticalCenter: parent.verticalCenter - height: 18 - text: widget.shortcut?widget.shortcut:"" //XsUtils.getPlatformShortcut(widget.shortcut):"" - font.pixelSize: XsStyle.menuFontSize - font.family: XsStyle.menuFontFamily - font.hintingPreference: Font.PreferNoHinting - - opacity: enabled ? 1.0 : 0.3 - color: widget.highlighted?textPart.color:XsStyleSheet.accentColor - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - } - // Rectangle { - // visible: shortcutPart.text !== "" && - // !widget.shortcut.includes('Ctrl') && - // !widget.shortcut.includes('Alt') - // color: "transparent" - // anchors.fill: shortcutPart - // border { - // width: 1 - // color: shortcutPart.color - // } - // radius: 5 - // } - } - - background: Rectangle { - - anchors { - margins: 1 - fill: parent - } - - implicitHeight: XsStyle.menuItemHeight - opacity: enabled ? 1 : 0.3 - // color: widget.highlighted ? - // XsStyle.highlightColor: "transparent" - // color: widget.highlighted? palette.highlight : "transparent" - // gradient:widget.highlighted? styleGradient.accent_gradient : Gradient.gradient - - gradient: Gradient { - GradientStop { position: 0.0; color: widget.highlighted? bgColorPressed: "transparent" } //"#33FFFFFF" } - GradientStop { position: 1.0; color: widget.highlighted? bgColorPressed: "transparent" } //"#11FFFFFF" } - } - - } - -} diff --git a/ui/qml/reskin/widgets/prototypes/new/XsViewerToggleButton.qml b/ui/qml/reskin/widgets/prototypes/new/XsViewerToggleButton.qml deleted file mode 100644 index 1ff052fb4..000000000 --- a/ui/qml/reskin/widgets/prototypes/new/XsViewerToggleButton.qml +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 - -import xStudioReskin 1.0 - -Button { - id: widget - - //#TODO: shortform support on rezise (text-model), via signal - - property bool isActive: false - - property color bgColorPressed: palette.highlight - property color bgColorNormal: XsStyleSheet.widgetBgNormalColor - property color forcedBgColorNormal: bgColorNormal - property color borderColorHovered: bgColorPressed - property color borderColorNormal: "transparent" - property real borderWidth: 1 - - property color textColorPressed: "#F1F1F1" - property color textColorNormal: "light grey" - property var textElide: textDiv.elide - property alias textDiv: textDiv - property bool toggleState: false - - font.pixelSize: XsStyleSheet.fontSize - font.family: XsStyleSheet.fontFamily - hoverEnabled: true - opacity: enabled ? 1.0 : 0.33 - - contentItem: - Item{ - anchors.fill: parent - - Text { - id: textDiv - text: widget.toggleState? widget.text+" ON" : widget.text+" OFF" - font: widget.font - color: textColorPressed - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - topPadding: 4 - bottomPadding: 4 - leftPadding: 20 - rightPadding: 20 - elide: Text.ElideRight - width: parent.width - height: parent.height - - XsToolTip{ - text: parent.text - visible: widget.hovered && parent.truncated - width: widget.width == 0? 0 : 150 - x: 0 - } - } - } - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - border.color: widget.down || widget.hovered ? borderColorHovered: borderColorNormal - border.width: borderWidth - - gradient: Gradient { - GradientStop { position: 0.0; color: widget.down || isActive? bgColorPressed: "#33FFFFFF" } - GradientStop { position: 1.0; color: widget.down || isActive? bgColorPressed: forcedBgColorNormal } - } - - Rectangle { - id: bgFocusDiv - implicitWidth: parent.width+borderWidth - implicitHeight: parent.height+borderWidth - visible: widget.activeFocus - color: "transparent" - opacity: 0.33 - border.color: borderColorHovered - border.width: borderWidth - anchors.centerIn: parent - } - } - - onPressed: focus = true - onReleased: focus = false - - onClicked: widget.toggleState = !widget.toggleState -} - diff --git a/ui/qml/reskin/widgets/prototypes/old/XsButton.qml b/ui/qml/reskin/widgets/prototypes/old/XsButton.qml deleted file mode 100644 index 3035ef791..000000000 --- a/ui/qml/reskin/widgets/prototypes/old/XsButton.qml +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 //for ColorOverlay - -import xStudioReskin 1.0 - -Button { - id: widget - - // font.pixelSize: XsStyle.menuFontSize - // font.family: XsStyle.fontFamily - - property alias bgDiv: bgDiv - property alias image: image - property color bgColorPressed: palette.highlight - property color bgColorNormal: palette.base - property color borderColorNormal: palette.base - property real borderWidth: 1 - property real borderRadius: 2 - - property color textColorPressed: palette.text - property color textColorNormal: "light grey" - property var textElide: textDiv.elide - property alias textDiv: textDiv - - property string imgSrc: "" - - property bool isActive: false - property bool subtleActive: false - - property var tooltip: "" - property var tooltipTitle: "" - hoverEnabled: true - - contentItem: - Item{ - anchors.fill: parent - opacity: enabled ? 1.0: bgColorNormal!=palette.base?1: 0.3 - Image { - id: image - visible: imgSrc!="" - source: imgSrc - sourceSize.height: parent.height/1.5 - sourceSize.width: parent.width/1.5 - horizontalAlignment: Image.AlignHCenter - verticalAlignment: Image.AlignVCenter - anchors.centerIn: parent - smooth: true - antialiasing: true - layer { - enabled: true - effect: - ColorOverlay { - color: widget.down || widget.hovered? ((subtleActive && isActive) ? textColorNormal : textColorPressed) : (subtleActive && isActive) ? bgColorPressed : textColorNormal - } - } - } - Text { - id: textDiv - text: widget.text - font: widget.font - color: widget.down || widget.hovered? textColorPressed: textColorNormal - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - topPadding: 1 - anchors.horizontalCenter: parent.horizontalCenter - elide: Text.ElideRight - width: parent.width - height: parent.height - } - } - - ToolTip.text: widget.text - ToolTip.visible: widget.hovered && textDiv.truncated - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - // opacity: enabled ? 1: 0.3 - color: enabled && widget.hoverEnabled? widget.down || (isActive && !subtleActive)? bgColorPressed: bgColorNormal : Qt.darker(bgColorNormal,1.5) - border.color: widget.down || widget.hovered ? bgColorPressed: borderColorNormal - border.width: borderWidth - radius: borderRadius - } - - /*onPressed: focus = true - onReleased: focus = false*/ -} - diff --git a/ui/qml/reskin/widgets/prototypes/old/XsMenu.qml b/ui/qml/reskin/widgets/prototypes/old/XsMenu.qml deleted file mode 100644 index 604e04c67..000000000 --- a/ui/qml/reskin/widgets/prototypes/old/XsMenu.qml +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.12 - -import xStudioReskin 1.0 - -Menu { - id:myMenu - - - property color bgColorPressed: palette.highlight - property color bgColorNormal: XsStyleSheet.widgetBgNormalColor - property color forcedBgColorNormal: bgColorNormal - property color borderColorHovered: bgColorPressed - property color borderColorNormal: "transparent" - - // property alias topLeftCorner: bgrect.topLeftCorner - // property alias topRightCorner: bgrect.topRightCorner - // property alias bottomRightCorner: bgrect.bottomRightCorner - // property alias bottomLeftCorner: bgrect.bottomLeftCorner - - // background: RoundedRectangle { - property alias radius: bgrect.radius - property bool hasCheckable: false - property bool fakeDisabled: false - topPadding: Math.max(3, XsStyle.menuRadius) - bottomPadding: topPadding - dim: false - - background: Rectangle { - id: bgrect - border { - color: XsStyle.menuBorderColor - width: XsStyle.menuBorderWidth - } - - gradient: Gradient { - GradientStop { position: 0.0; color: myMenu.down || myMenu.hovered? bgColorPressed: "#33FFFFFF" } - GradientStop { position: 1.0; color: myMenu.down || myMenu.hovered? bgColorPressed: forcedBgColorNormal } - } - // color: XsStyle.mainBackground - // radius: XsStyle.menuRadius - } - width: { - var result = 0; - var padding = 0; - for (var i = 0; i < count; ++i) { - var item = itemAt(i); - if (item.contentItem != undefined) { - result = Math.max(item.contentItem.implicitWidth, result); - padding = Math.max(item.padding, padding); - } - } - return Math.max(100, result + padding * 2); - } - delegate: XsMenuItem {} - Component.onCompleted: { - var curritem; - for (var i=0; i 1) - { - interval = 1.00 / (spl.length - 1) - } - var currStop = 0.0 - var stop - for (var index in spl) - { - // console.debug("index: "+index+"-"+spl[index]) - stop = gradiantStopComponent.createObject(appWindow, {"position": currStop, "color": spl[index]}); - stops.push(stop) - currStop = currStop + interval - } - gradobj.stops = stops - } -} diff --git a/ui/qml/reskin/widgets/prototypes/old/misc/utils.js b/ui/qml/reskin/widgets/prototypes/old/misc/utils.js deleted file mode 100644 index 7df041bc3..000000000 --- a/ui/qml/reskin/widgets/prototypes/old/misc/utils.js +++ /dev/null @@ -1,270 +0,0 @@ - -// function getMethods(obj) { -// let properties = new Set() -// let currentObj = obj -// do { -// Object.getOwnPropertyNames(currentObj).map(item => properties.add(item)) -// } while ((currentObj = Object.getPrototypeOf(currentObj))) -// return [...properties.keys()].filter(item => typeof obj[item] === 'function') -// } - - -// function getControlByKeyword(trays, keyword) { -// // var trays = [leftTray, rightTray, mediaToolsTray, transportTray, sessionSettingsTray]; -// var control = null; -// for (var index in trays) { -// control = trays[index].getControlByKeyword(keyword); -// if (control !== null) { -// return control; -// } -// } -// return null; -// } - -function getTimeCodeStr(displayValue) -{ - // e.g. minus 1 so that frame 30 for 30fps should still be 00:00 - // displayValue = displayValue - if(playhead) { - var fps = playhead.media.mediaSource? parseFloat(playhead.media.mediaSource.fpsString):24 - var frames = displayValue % Math.round(fps) - var seconds = Math.floor(displayValue / fps) - var minutes = Math.floor(seconds / 60) - var hours = Math.floor(minutes / 60) - - var FF = pad(frames, 2) - var SS = pad(seconds % 60, 2) - var MM = pad(minutes % 60, 2) - var HH = pad(hours, 2) - return HH + ':' + MM + ':' + SS + ':' + FF - } - return "--" -} - -function getTimeStr(displayValue) { - // e.g. minus 1 so that frame 30 for 30fps should still be 00:00 - if(playhead) { - displayValue = displayValue - 1 - var fps = playhead.media.mediaSource ? playhead.media.mediaSource.fps:24 - var seconds = Math.floor(displayValue / fps) - var minutes = Math.floor(seconds / 60) - var hours = Math.floor(minutes / 60) - - var SS = pad(seconds % 60, 2) - var MM = pad(minutes % 60, 2) - var str - if (hours > 0) { - var HH = hours % 24 - str = HH + ':' + MM + ':' + SS - } else { - str = MM + ':' + SS - } - return str - } - return "--" -} - -function pad(n, width, z) { - z = z || '0'; - n = n + ''; - return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; -} - -function replaceMenuText(txt) -{ - // there is a bug in Qt where a MenuItem delegate will - // not correctly display the ampersand text; This is a - // workaround. - var index = txt.indexOf("&"); - if(index >= 0) - txt = txt.replace(txt.substr(index, 2), ("" + txt.substr(index + 1, 1) +"")); - return txt; -} - -function getPlatformShortcut(txt) { - // console.log(Qt.platform.os) - //if (Qt.platform.os === "linux" || - // Qt.platform.os === "windows" || - // Qt.platform.os === "osx" || - // Qt.platform.os === "unix") { - if (Qt.platform.os === "osx") { - txt = txt.replace(/Ctrl\+/g, "⌘"); - txt = txt.replace(/Alt\+/g, "⌥"); - } - return txt; -} - -function calcMax(arr){ - return Math.max(...arr); -} - -function calcMin(arr){ - return Math.min(...arr); -} - -function calcRange(arr){ - mx = calcMax(arr); - mn = calcMin(arr); - return mx-mn; -} - -function nearest(val, inc) { - return (Math.round(val / inc)+(val % inc ? 1:0)) * inc -} - - -function sumArr(arr){ - var a = arr.slice(); - return a.reduce(function(a, b) { return parseFloat(a) + parseFloat(b); }); -} - -function sortArr(arr){ - var ary = arr.slice(); - ary.sort(function(a,b){ return parseFloat(a) - parseFloat(b);}); - return ary; -} -// qml: [0,0,0,2,0,0,0,1,2,3,3,0] [7,1,2,2] 3 - -function calcPreferred(arr){ - return calcMax(arr) -} - -function openDialogCenter(qml_path, parent=app_window) { - return openDialog(qml_path, parent, {"anchors.centerIn": parent}) -} - -function openDialogPlaylist(qml_path, parent=app_window) { - return openDialog(qml_path, parent, {"anchors.centerIn": parent}) -} - -function openDialog(qml_path, parent=app_window, params={}) { - var dialog = null - var component = Qt.createComponent(qml_path); - if (component.status == Component.Ready) { - dialog = component.createObject(parent, params) - } else { - console.log("Error loading component:", component.errorString()); - } - return dialog -} - -function centerYInParent(holder, parent, height) { - var oy = (parent.height/2)-(height/2); - var py = mapToItem(holder,0,oy).y; - if(py<0){ - py = mapFromItem(holder, 0, 0).y; - } else if(py+height > holder.height){ - py = mapFromItem(holder, 0, holder.height-height).y; - } else { - py = oy; - } - return py; -} - -function centerXInParent(holder, parent, width) { - var ox = (parent.width/2)-(width/2); - var px = mapToItem(holder, ox,0).x; - if(px<0){ - px = mapFromItem(holder, 0, 0).x; - } else if(px+width > holder.width){ - px = mapFromItem(holder, holder.width-width, 0).x; - } else { - px = ox; - } - return px; -} - -//only top level -function getSelectedCuuids(obj, recursive=false, func=null) { - var cuuids = [] - - if(obj) { - var model = obj.itemModel - if(model) { - for(var i=0;i 1) - { - interval = 1.00 / (spl.length - 1) - } - var currStop = 0.0 - var stop - for (var index in spl) - { - // console.debug("index: "+index+"-"+spl[index]) - stop = gradiantStopComponent.createObject(app_window, {"position": currStop, "color": spl[index]}); - stops.push(stop) - currStop = currStop + interval - } - gradobj.stops = stops - } -} diff --git a/ui/qml/xstudio/cursors/magnifier_cursor.svg b/ui/qml/xstudio/assets/cursors/magnifier_cursor.svg similarity index 100% rename from ui/qml/xstudio/cursors/magnifier_cursor.svg rename to ui/qml/xstudio/assets/cursors/magnifier_cursor.svg diff --git a/ui/qml/reskin/assets/icons/new/ad_group.svg b/ui/qml/xstudio/assets/icons/ad_group.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/ad_group.svg rename to ui/qml/xstudio/assets/icons/ad_group.svg diff --git a/ui/qml/reskin/assets/icons/new/add.svg b/ui/qml/xstudio/assets/icons/add.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/add.svg rename to ui/qml/xstudio/assets/icons/add.svg diff --git a/ui/qml/xstudio/assets/icons/arrow_circle_up.svg b/ui/qml/xstudio/assets/icons/arrow_circle_up.svg new file mode 100644 index 000000000..7ed9c083e --- /dev/null +++ b/ui/qml/xstudio/assets/icons/arrow_circle_up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/arrow_drop_down.svg b/ui/qml/xstudio/assets/icons/arrow_drop_down.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/arrow_drop_down.svg rename to ui/qml/xstudio/assets/icons/arrow_drop_down.svg diff --git a/ui/qml/xstudio/assets/icons/arrow_range.svg b/ui/qml/xstudio/assets/icons/arrow_range.svg new file mode 100644 index 000000000..33e35f890 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/arrow_range.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/arrow_selector_tool.svg b/ui/qml/xstudio/assets/icons/arrow_selector_tool.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/arrow_selector_tool.svg rename to ui/qml/xstudio/assets/icons/arrow_selector_tool.svg diff --git a/ui/qml/xstudio/assets/icons/arrow_upward.svg b/ui/qml/xstudio/assets/icons/arrow_upward.svg new file mode 100644 index 000000000..0e51754fa --- /dev/null +++ b/ui/qml/xstudio/assets/icons/arrow_upward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/arrows_outward.svg b/ui/qml/xstudio/assets/icons/arrows_outward.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/arrows_outward.svg rename to ui/qml/xstudio/assets/icons/arrows_outward.svg diff --git a/ui/qml/xstudio/feather_icons/book.svg b/ui/qml/xstudio/assets/icons/book.svg old mode 100755 new mode 100644 similarity index 100% rename from ui/qml/xstudio/feather_icons/book.svg rename to ui/qml/xstudio/assets/icons/book.svg diff --git a/ui/qml/xstudio/assets/icons/brightness_high.svg b/ui/qml/xstudio/assets/icons/brightness_high.svg new file mode 100644 index 000000000..5aa286a32 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/brightness_high.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/brightness_low.svg b/ui/qml/xstudio/assets/icons/brightness_low.svg new file mode 100644 index 000000000..ddbe68844 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/brightness_low.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/brush.svg b/ui/qml/xstudio/assets/icons/brush.svg new file mode 100644 index 000000000..d583fce54 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/brush.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/build_gang.svg b/ui/qml/xstudio/assets/icons/build_gang.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/build_gang.svg rename to ui/qml/xstudio/assets/icons/build_gang.svg diff --git a/ui/qml/reskin/assets/icons/new/center_focus_strong.svg b/ui/qml/xstudio/assets/icons/center_focus_strong.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/center_focus_strong.svg rename to ui/qml/xstudio/assets/icons/center_focus_strong.svg diff --git a/ui/qml/xstudio/assets/icons/center_focus_weak.svg b/ui/qml/xstudio/assets/icons/center_focus_weak.svg new file mode 100644 index 000000000..d99473397 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/center_focus_weak.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/check.svg b/ui/qml/xstudio/assets/icons/check.svg new file mode 100644 index 000000000..1655d12bf --- /dev/null +++ b/ui/qml/xstudio/assets/icons/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/check_box_checked.svg b/ui/qml/xstudio/assets/icons/check_box_checked.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/check_box_checked.svg rename to ui/qml/xstudio/assets/icons/check_box_checked.svg diff --git a/ui/qml/reskin/assets/icons/new/check_box_unchecked.svg b/ui/qml/xstudio/assets/icons/check_box_unchecked.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/check_box_unchecked.svg rename to ui/qml/xstudio/assets/icons/check_box_unchecked.svg diff --git a/ui/qml/xstudio/assets/icons/check_circle.svg b/ui/qml/xstudio/assets/icons/check_circle.svg new file mode 100644 index 000000000..fc9a8160d --- /dev/null +++ b/ui/qml/xstudio/assets/icons/check_circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/chevron_right.svg b/ui/qml/xstudio/assets/icons/chevron_right.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/chevron_right.svg rename to ui/qml/xstudio/assets/icons/chevron_right.svg diff --git a/ui/qml/reskin/assets/icons/circle.svg b/ui/qml/xstudio/assets/icons/circle.svg old mode 100755 new mode 100644 similarity index 100% rename from ui/qml/reskin/assets/icons/circle.svg rename to ui/qml/xstudio/assets/icons/circle.svg diff --git a/ui/qml/reskin/assets/icons/new/close.svg b/ui/qml/xstudio/assets/icons/close.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/close.svg rename to ui/qml/xstudio/assets/icons/close.svg diff --git a/ui/qml/xstudio/assets/icons/cloud-download.svg b/ui/qml/xstudio/assets/icons/cloud-download.svg new file mode 100644 index 000000000..479908880 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/cloud-download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/communities.svg b/ui/qml/xstudio/assets/icons/communities.svg new file mode 100644 index 000000000..655e68d5b --- /dev/null +++ b/ui/qml/xstudio/assets/icons/communities.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/content_copy.svg b/ui/qml/xstudio/assets/icons/content_copy.svg new file mode 100644 index 000000000..d5784eefc --- /dev/null +++ b/ui/qml/xstudio/assets/icons/content_copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/content_cut.svg b/ui/qml/xstudio/assets/icons/content_cut.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/content_cut.svg rename to ui/qml/xstudio/assets/icons/content_cut.svg diff --git a/ui/qml/reskin/assets/icons/new/content_paste.svg b/ui/qml/xstudio/assets/icons/content_paste.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/content_paste.svg rename to ui/qml/xstudio/assets/icons/content_paste.svg diff --git a/ui/qml/xstudio/assets/icons/crop_free.svg b/ui/qml/xstudio/assets/icons/crop_free.svg new file mode 100644 index 000000000..7482ec0d7 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/crop_free.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/delete.svg b/ui/qml/xstudio/assets/icons/delete.svg new file mode 100644 index 000000000..560d174b9 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/deployed_cube.svg b/ui/qml/xstudio/assets/icons/deployed_cube.svg new file mode 100644 index 000000000..3cc4ce7b6 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/deployed_cube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/desktop_windows.svg b/ui/qml/xstudio/assets/icons/desktop_windows.svg new file mode 100644 index 000000000..29ee5b6ad --- /dev/null +++ b/ui/qml/xstudio/assets/icons/desktop_windows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/disabled.svg b/ui/qml/xstudio/assets/icons/disabled.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/disabled.svg rename to ui/qml/xstudio/assets/icons/disabled.svg diff --git a/ui/qml/xstudio/assets/icons/dock_bottom.svg b/ui/qml/xstudio/assets/icons/dock_bottom.svg new file mode 100644 index 000000000..064961eb2 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/dock_bottom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/dock_bottom_flipped.png b/ui/qml/xstudio/assets/icons/dock_bottom_flipped.png new file mode 100644 index 000000000..e040ecafa Binary files /dev/null and b/ui/qml/xstudio/assets/icons/dock_bottom_flipped.png differ diff --git a/ui/qml/xstudio/assets/icons/dock_left.svg b/ui/qml/xstudio/assets/icons/dock_left.svg new file mode 100644 index 000000000..f0bb5b0b1 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/dock_left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/dock_right.svg b/ui/qml/xstudio/assets/icons/dock_right.svg new file mode 100644 index 000000000..5093d539b --- /dev/null +++ b/ui/qml/xstudio/assets/icons/dock_right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/draft.svg b/ui/qml/xstudio/assets/icons/draft.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/draft.svg rename to ui/qml/xstudio/assets/icons/draft.svg diff --git a/ui/qml/xstudio/assets/icons/edit.svg b/ui/qml/xstudio/assets/icons/edit.svg new file mode 100644 index 000000000..cb81b1130 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/eject.svg b/ui/qml/xstudio/assets/icons/eject.svg new file mode 100644 index 000000000..811f0640b --- /dev/null +++ b/ui/qml/xstudio/assets/icons/eject.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/error.svg b/ui/qml/xstudio/assets/icons/error.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/error.svg rename to ui/qml/xstudio/assets/icons/error.svg diff --git a/ui/qml/reskin/assets/icons/new/expand.svg b/ui/qml/xstudio/assets/icons/expand.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/expand.svg rename to ui/qml/xstudio/assets/icons/expand.svg diff --git a/ui/qml/reskin/assets/icons/new/expand_all.svg b/ui/qml/xstudio/assets/icons/expand_all.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/expand_all.svg rename to ui/qml/xstudio/assets/icons/expand_all.svg diff --git a/ui/qml/reskin/assets/icons/new/fast_forward.svg b/ui/qml/xstudio/assets/icons/fast_forward.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/fast_forward.svg rename to ui/qml/xstudio/assets/icons/fast_forward.svg diff --git a/ui/qml/reskin/assets/icons/new/fast_rewind.svg b/ui/qml/xstudio/assets/icons/fast_rewind.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/fast_rewind.svg rename to ui/qml/xstudio/assets/icons/fast_rewind.svg diff --git a/ui/qml/xstudio/assets/icons/featured_play_list.svg b/ui/qml/xstudio/assets/icons/featured_play_list.svg new file mode 100644 index 000000000..b1fe3181d --- /dev/null +++ b/ui/qml/xstudio/assets/icons/featured_play_list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/filter.svg b/ui/qml/xstudio/assets/icons/filter.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/filter.svg rename to ui/qml/xstudio/assets/icons/filter.svg diff --git a/ui/qml/reskin/assets/icons/new/filter_none.svg b/ui/qml/xstudio/assets/icons/filter_none.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/filter_none.svg rename to ui/qml/xstudio/assets/icons/filter_none.svg diff --git a/ui/qml/xstudio/assets/icons/fit_screen.svg b/ui/qml/xstudio/assets/icons/fit_screen.svg new file mode 100644 index 000000000..40882ec4d --- /dev/null +++ b/ui/qml/xstudio/assets/icons/fit_screen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/fit_select.svg b/ui/qml/xstudio/assets/icons/fit_select.svg new file mode 100644 index 000000000..71ef9cb99 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/fit_select.svg @@ -0,0 +1,56 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/ui/qml/reskin/assets/icons/new/format_size.svg b/ui/qml/xstudio/assets/icons/format_size.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/format_size.svg rename to ui/qml/xstudio/assets/icons/format_size.svg diff --git a/ui/qml/xstudio/assets/icons/frame_inspect.svg b/ui/qml/xstudio/assets/icons/frame_inspect.svg new file mode 100644 index 000000000..e84230a94 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/frame_inspect.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/grid_view.svg b/ui/qml/xstudio/assets/icons/grid_view.svg new file mode 100644 index 000000000..964c3f85f --- /dev/null +++ b/ui/qml/xstudio/assets/icons/grid_view.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/help.svg b/ui/qml/xstudio/assets/icons/help.svg new file mode 100644 index 000000000..a20737f59 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/hexagon.svg b/ui/qml/xstudio/assets/icons/hexagon.svg new file mode 100644 index 000000000..fe2f9d2f3 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/hexagon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/ink_pen.svg b/ui/qml/xstudio/assets/icons/ink_pen.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/ink_pen.svg rename to ui/qml/xstudio/assets/icons/ink_pen.svg diff --git a/ui/qml/reskin/assets/icons/new/input.svg b/ui/qml/xstudio/assets/icons/input.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/input.svg rename to ui/qml/xstudio/assets/icons/input.svg diff --git a/ui/qml/xstudio/assets/icons/instant_mix.svg b/ui/qml/xstudio/assets/icons/instant_mix.svg new file mode 100644 index 000000000..246de5d6b --- /dev/null +++ b/ui/qml/xstudio/assets/icons/instant_mix.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/keyboard_arrow_up.svg b/ui/qml/xstudio/assets/icons/keyboard_arrow_up.svg new file mode 100644 index 000000000..2d2f2f154 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/keyboard_arrow_up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/keyboard_tab.svg b/ui/qml/xstudio/assets/icons/keyboard_tab.svg new file mode 100644 index 000000000..c0fde6eb8 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/keyboard_tab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/laps.svg b/ui/qml/xstudio/assets/icons/laps.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/laps.svg rename to ui/qml/xstudio/assets/icons/laps.svg diff --git a/ui/qml/xstudio/assets/icons/left_drag.svg b/ui/qml/xstudio/assets/icons/left_drag.svg new file mode 100644 index 000000000..4c2f320eb --- /dev/null +++ b/ui/qml/xstudio/assets/icons/left_drag.svg @@ -0,0 +1,55 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/ui/qml/xstudio/assets/icons/left_right_drag.svg b/ui/qml/xstudio/assets/icons/left_right_drag.svg new file mode 100644 index 000000000..3f539d0d5 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/left_right_drag.svg @@ -0,0 +1,58 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/ui/qml/xstudio/assets/icons/library_add.svg b/ui/qml/xstudio/assets/icons/library_add.svg new file mode 100644 index 000000000..b53c3c708 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/library_add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/library_music.svg b/ui/qml/xstudio/assets/icons/library_music.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/library_music.svg rename to ui/qml/xstudio/assets/icons/library_music.svg diff --git a/ui/qml/xstudio/assets/icons/link.svg b/ui/qml/xstudio/assets/icons/link.svg new file mode 100644 index 000000000..319a0681c --- /dev/null +++ b/ui/qml/xstudio/assets/icons/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/list.svg b/ui/qml/xstudio/assets/icons/list.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/list.svg rename to ui/qml/xstudio/assets/icons/list.svg diff --git a/ui/qml/xstudio/assets/icons/list_alt.svg b/ui/qml/xstudio/assets/icons/list_alt.svg new file mode 100644 index 000000000..70519ce97 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/list_alt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/list_default.svg b/ui/qml/xstudio/assets/icons/list_default.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/list_default.svg rename to ui/qml/xstudio/assets/icons/list_default.svg diff --git a/ui/qml/reskin/assets/icons/new/list_shotgun.svg b/ui/qml/xstudio/assets/icons/list_shotgun.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/list_shotgun.svg rename to ui/qml/xstudio/assets/icons/list_shotgun.svg diff --git a/ui/qml/reskin/assets/icons/new/list_subset.svg b/ui/qml/xstudio/assets/icons/list_subset.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/list_subset.svg rename to ui/qml/xstudio/assets/icons/list_subset.svg diff --git a/ui/qml/reskin/assets/icons/new/list_view.svg b/ui/qml/xstudio/assets/icons/list_view.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/list_view.svg rename to ui/qml/xstudio/assets/icons/list_view.svg diff --git a/ui/qml/xstudio/assets/icons/lock.svg b/ui/qml/xstudio/assets/icons/lock.svg new file mode 100644 index 000000000..20b9e3984 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/lock_open.svg b/ui/qml/xstudio/assets/icons/lock_open.svg new file mode 100644 index 000000000..824c70b7c --- /dev/null +++ b/ui/qml/xstudio/assets/icons/lock_open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/mask_domino.svg b/ui/qml/xstudio/assets/icons/mask_domino.svg new file mode 100644 index 000000000..52136f729 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/mask_domino.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/menu.svg b/ui/qml/xstudio/assets/icons/menu.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/menu.svg rename to ui/qml/xstudio/assets/icons/menu.svg diff --git a/ui/qml/reskin/assets/icons/new/more_horiz.svg b/ui/qml/xstudio/assets/icons/more_horiz.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/more_horiz.svg rename to ui/qml/xstudio/assets/icons/more_horiz.svg diff --git a/ui/qml/reskin/assets/icons/new/more_vert.svg b/ui/qml/xstudio/assets/icons/more_vert.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/more_vert.svg rename to ui/qml/xstudio/assets/icons/more_vert.svg diff --git a/ui/qml/reskin/assets/icons/new/more_vert_500.svg b/ui/qml/xstudio/assets/icons/more_vert_500.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/more_vert_500.svg rename to ui/qml/xstudio/assets/icons/more_vert_500.svg diff --git a/ui/qml/reskin/assets/icons/new/movie.svg b/ui/qml/xstudio/assets/icons/movie.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/movie.svg rename to ui/qml/xstudio/assets/icons/movie.svg diff --git a/ui/qml/reskin/assets/icons/new/open_in.svg b/ui/qml/xstudio/assets/icons/open_in.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/open_in.svg rename to ui/qml/xstudio/assets/icons/open_in.svg diff --git a/ui/qml/reskin/assets/icons/new/open_in_new.svg b/ui/qml/xstudio/assets/icons/open_in_new.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/open_in_new.svg rename to ui/qml/xstudio/assets/icons/open_in_new.svg diff --git a/ui/qml/reskin/assets/icons/new/open_with.svg b/ui/qml/xstudio/assets/icons/open_with.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/open_with.svg rename to ui/qml/xstudio/assets/icons/open_with.svg diff --git a/ui/qml/reskin/assets/icons/new/output.svg b/ui/qml/xstudio/assets/icons/output.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/output.svg rename to ui/qml/xstudio/assets/icons/output.svg diff --git a/ui/qml/reskin/assets/icons/new/pan.svg b/ui/qml/xstudio/assets/icons/pan.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/pan.svg rename to ui/qml/xstudio/assets/icons/pan.svg diff --git a/ui/qml/reskin/assets/icons/new/pause.svg b/ui/qml/xstudio/assets/icons/pause.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/pause.svg rename to ui/qml/xstudio/assets/icons/pause.svg diff --git a/ui/qml/reskin/assets/icons/new/photo_camera.svg b/ui/qml/xstudio/assets/icons/photo_camera.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/photo_camera.svg rename to ui/qml/xstudio/assets/icons/photo_camera.svg diff --git a/ui/qml/reskin/assets/icons/new/play_arrow.svg b/ui/qml/xstudio/assets/icons/play_arrow.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/play_arrow.svg rename to ui/qml/xstudio/assets/icons/play_arrow.svg diff --git a/ui/qml/xstudio/assets/icons/play_circle.svg b/ui/qml/xstudio/assets/icons/play_circle.svg new file mode 100644 index 000000000..0940d04de --- /dev/null +++ b/ui/qml/xstudio/assets/icons/play_circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/feather_icons/plus-circle.svg b/ui/qml/xstudio/assets/icons/plus-circle.svg old mode 100755 new mode 100644 similarity index 100% rename from ui/qml/xstudio/feather_icons/plus-circle.svg rename to ui/qml/xstudio/assets/icons/plus-circle.svg diff --git a/ui/qml/reskin/assets/icons/plus.svg b/ui/qml/xstudio/assets/icons/plus.svg old mode 100755 new mode 100644 similarity index 100% rename from ui/qml/reskin/assets/icons/plus.svg rename to ui/qml/xstudio/assets/icons/plus.svg diff --git a/ui/qml/xstudio/assets/icons/quick_reference_all.svg b/ui/qml/xstudio/assets/icons/quick_reference_all.svg new file mode 100644 index 000000000..5985112a3 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/quick_reference_all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/radio_button_checked.svg b/ui/qml/xstudio/assets/icons/radio_button_checked.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/radio_button_checked.svg rename to ui/qml/xstudio/assets/icons/radio_button_checked.svg diff --git a/ui/qml/xstudio/assets/icons/radio_button_unchecked.svg b/ui/qml/xstudio/assets/icons/radio_button_unchecked.svg new file mode 100644 index 000000000..483e418b7 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/radio_button_unchecked.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/rectangle.svg b/ui/qml/xstudio/assets/icons/rectangle.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/rectangle.svg rename to ui/qml/xstudio/assets/icons/rectangle.svg diff --git a/ui/qml/xstudio/assets/icons/redo.svg b/ui/qml/xstudio/assets/icons/redo.svg new file mode 100644 index 000000000..94d65b4c8 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/redo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/repartition.svg b/ui/qml/xstudio/assets/icons/repartition.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/repartition.svg rename to ui/qml/xstudio/assets/icons/repartition.svg diff --git a/ui/qml/reskin/assets/icons/new/repeat.svg b/ui/qml/xstudio/assets/icons/repeat.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/repeat.svg rename to ui/qml/xstudio/assets/icons/repeat.svg diff --git a/ui/qml/reskin/assets/icons/new/reset_image.svg b/ui/qml/xstudio/assets/icons/reset_image.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/reset_image.svg rename to ui/qml/xstudio/assets/icons/reset_image.svg diff --git a/ui/qml/reskin/assets/icons/new/reset_tv.svg b/ui/qml/xstudio/assets/icons/reset_tv.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/reset_tv.svg rename to ui/qml/xstudio/assets/icons/reset_tv.svg diff --git a/ui/qml/reskin/assets/icons/new/restart.svg b/ui/qml/xstudio/assets/icons/restart.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/restart.svg rename to ui/qml/xstudio/assets/icons/restart.svg diff --git a/ui/qml/xstudio/assets/icons/right_drag.svg b/ui/qml/xstudio/assets/icons/right_drag.svg new file mode 100644 index 000000000..dfa591826 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/right_drag.svg @@ -0,0 +1,55 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/ui/qml/xstudio/feather_icons/rotate-ccw.svg b/ui/qml/xstudio/assets/icons/rotate-ccw.svg old mode 100755 new mode 100644 similarity index 100% rename from ui/qml/xstudio/feather_icons/rotate-ccw.svg rename to ui/qml/xstudio/assets/icons/rotate-ccw.svg diff --git a/ui/qml/reskin/assets/icons/new/search.svg b/ui/qml/xstudio/assets/icons/search.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/search.svg rename to ui/qml/xstudio/assets/icons/search.svg diff --git a/ui/qml/reskin/assets/icons/new/search_off.svg b/ui/qml/xstudio/assets/icons/search_off.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/search_off.svg rename to ui/qml/xstudio/assets/icons/search_off.svg diff --git a/ui/qml/xstudio/assets/icons/select.svg b/ui/qml/xstudio/assets/icons/select.svg new file mode 100644 index 000000000..1a65f0cb1 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/select.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/settings.svg b/ui/qml/xstudio/assets/icons/settings.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/settings.svg rename to ui/qml/xstudio/assets/icons/settings.svg diff --git a/ui/qml/reskin/assets/icons/new/skip.svg b/ui/qml/xstudio/assets/icons/skip.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/skip.svg rename to ui/qml/xstudio/assets/icons/skip.svg diff --git a/ui/qml/reskin/assets/icons/new/skip_next.svg b/ui/qml/xstudio/assets/icons/skip_next.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/skip_next.svg rename to ui/qml/xstudio/assets/icons/skip_next.svg diff --git a/ui/qml/reskin/assets/icons/new/skip_previous.svg b/ui/qml/xstudio/assets/icons/skip_previous.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/skip_previous.svg rename to ui/qml/xstudio/assets/icons/skip_previous.svg diff --git a/ui/qml/reskin/assets/icons/new/sort.svg b/ui/qml/xstudio/assets/icons/sort.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/sort.svg rename to ui/qml/xstudio/assets/icons/sort.svg diff --git a/ui/qml/reskin/assets/icons/sort_flipped.png b/ui/qml/xstudio/assets/icons/sort_flipped.png similarity index 100% rename from ui/qml/reskin/assets/icons/sort_flipped.png rename to ui/qml/xstudio/assets/icons/sort_flipped.png diff --git a/ui/qml/xstudio/assets/icons/split_scene.svg b/ui/qml/xstudio/assets/icons/split_scene.svg new file mode 100644 index 000000000..676550da5 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/split_scene.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/splitscreen.svg b/ui/qml/xstudio/assets/icons/splitscreen.svg new file mode 100644 index 000000000..fb5287abc --- /dev/null +++ b/ui/qml/xstudio/assets/icons/splitscreen.svg @@ -0,0 +1,55 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/ui/qml/xstudio/assets/icons/splitscreen2.svg b/ui/qml/xstudio/assets/icons/splitscreen2.svg new file mode 100644 index 000000000..2b82a41f3 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/splitscreen2.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/ui/qml/xstudio/assets/icons/splitscreen_add.svg b/ui/qml/xstudio/assets/icons/splitscreen_add.svg new file mode 100644 index 000000000..7ab4b5b3b --- /dev/null +++ b/ui/qml/xstudio/assets/icons/splitscreen_add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/feather_icons/square.svg b/ui/qml/xstudio/assets/icons/square.svg old mode 100755 new mode 100644 similarity index 100% rename from ui/qml/xstudio/feather_icons/square.svg rename to ui/qml/xstudio/assets/icons/square.svg diff --git a/ui/qml/xstudio/assets/icons/stacks.svg b/ui/qml/xstudio/assets/icons/stacks.svg new file mode 100644 index 000000000..58e6f5daa --- /dev/null +++ b/ui/qml/xstudio/assets/icons/stacks.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/sticky_note.svg b/ui/qml/xstudio/assets/icons/sticky_note.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/sticky_note.svg rename to ui/qml/xstudio/assets/icons/sticky_note.svg diff --git a/ui/qml/xstudio/assets/icons/stylus_note.svg b/ui/qml/xstudio/assets/icons/stylus_note.svg new file mode 100644 index 000000000..00c71b688 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/stylus_note.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/swap_horiz.svg b/ui/qml/xstudio/assets/icons/swap_horiz.svg new file mode 100644 index 000000000..a0d96ade1 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/swap_horiz.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/swap_vert.svg b/ui/qml/xstudio/assets/icons/swap_vert.svg new file mode 100644 index 000000000..12e017482 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/swap_vert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/sync.svg b/ui/qml/xstudio/assets/icons/sync.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/sync.svg rename to ui/qml/xstudio/assets/icons/sync.svg diff --git a/ui/qml/reskin/assets/icons/new/theaters.svg b/ui/qml/xstudio/assets/icons/theaters.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/theaters.svg rename to ui/qml/xstudio/assets/icons/theaters.svg diff --git a/ui/qml/xstudio/assets/icons/toolbar_top.svg b/ui/qml/xstudio/assets/icons/toolbar_top.svg new file mode 100644 index 000000000..bcf430bc0 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/toolbar_top.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/trending.svg b/ui/qml/xstudio/assets/icons/trending.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/trending.svg rename to ui/qml/xstudio/assets/icons/trending.svg diff --git a/ui/qml/reskin/assets/icons/new/triangle.svg b/ui/qml/xstudio/assets/icons/triangle.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/triangle.svg rename to ui/qml/xstudio/assets/icons/triangle.svg diff --git a/ui/qml/reskin/assets/icons/new/tune.svg b/ui/qml/xstudio/assets/icons/tune.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/tune.svg rename to ui/qml/xstudio/assets/icons/tune.svg diff --git a/ui/qml/xstudio/assets/icons/tv_displays.svg b/ui/qml/xstudio/assets/icons/tv_displays.svg new file mode 100644 index 000000000..429bf7084 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/tv_displays.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/undo.svg b/ui/qml/xstudio/assets/icons/undo.svg new file mode 100644 index 000000000..c451e1adc --- /dev/null +++ b/ui/qml/xstudio/assets/icons/undo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/menu.svg b/ui/qml/xstudio/assets/icons/unused/menu.svg old mode 100755 new mode 100644 similarity index 100% rename from ui/qml/reskin/assets/icons/menu.svg rename to ui/qml/xstudio/assets/icons/unused/menu.svg diff --git a/ui/qml/reskin/assets/icons/play.svg b/ui/qml/xstudio/assets/icons/unused/play.svg old mode 100755 new mode 100644 similarity index 100% rename from ui/qml/reskin/assets/icons/play.svg rename to ui/qml/xstudio/assets/icons/unused/play.svg diff --git a/src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/search.svg b/ui/qml/xstudio/assets/icons/unused/search.svg similarity index 100% rename from src/plugin/data_source/dneg/shotgun/src/qml/Shotgun.1/search.svg rename to ui/qml/xstudio/assets/icons/unused/search.svg diff --git a/ui/qml/xstudio/assets/icons/upgrade.svg b/ui/qml/xstudio/assets/icons/upgrade.svg new file mode 100644 index 000000000..52cd3b48e --- /dev/null +++ b/ui/qml/xstudio/assets/icons/upgrade.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/upload.svg b/ui/qml/xstudio/assets/icons/upload.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/upload.svg rename to ui/qml/xstudio/assets/icons/upload.svg diff --git a/ui/qml/reskin/assets/icons/new/variables_insert.svg b/ui/qml/xstudio/assets/icons/variables_insert.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/variables_insert.svg rename to ui/qml/xstudio/assets/icons/variables_insert.svg diff --git a/ui/qml/xstudio/assets/icons/vertical_align_center.svg b/ui/qml/xstudio/assets/icons/vertical_align_center.svg new file mode 100644 index 000000000..761de687e --- /dev/null +++ b/ui/qml/xstudio/assets/icons/vertical_align_center.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/video_cam.svg b/ui/qml/xstudio/assets/icons/video_cam.svg new file mode 100644 index 000000000..6f57999ca --- /dev/null +++ b/ui/qml/xstudio/assets/icons/video_cam.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/view.svg b/ui/qml/xstudio/assets/icons/view.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/view.svg rename to ui/qml/xstudio/assets/icons/view.svg diff --git a/ui/qml/reskin/assets/icons/new/view_grid.svg b/ui/qml/xstudio/assets/icons/view_grid.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/view_grid.svg rename to ui/qml/xstudio/assets/icons/view_grid.svg diff --git a/ui/qml/xstudio/assets/icons/view_object_track.svg b/ui/qml/xstudio/assets/icons/view_object_track.svg new file mode 100644 index 000000000..3651c31f8 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/view_object_track.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/view_timeline.svg b/ui/qml/xstudio/assets/icons/view_timeline.svg new file mode 100644 index 000000000..8fcf68fd3 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/view_timeline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/visibility.svg b/ui/qml/xstudio/assets/icons/visibility.svg new file mode 100644 index 000000000..8fe45d09a --- /dev/null +++ b/ui/qml/xstudio/assets/icons/visibility.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/visibility_off.svg b/ui/qml/xstudio/assets/icons/visibility_off.svg new file mode 100644 index 000000000..d98cf8d94 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/visibility_off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/volume_down.svg b/ui/qml/xstudio/assets/icons/volume_down.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/volume_down.svg rename to ui/qml/xstudio/assets/icons/volume_down.svg diff --git a/ui/qml/reskin/assets/icons/new/volume_mute.svg b/ui/qml/xstudio/assets/icons/volume_mute.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/volume_mute.svg rename to ui/qml/xstudio/assets/icons/volume_mute.svg diff --git a/ui/qml/reskin/assets/icons/new/volume_no_sound.svg b/ui/qml/xstudio/assets/icons/volume_no_sound.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/volume_no_sound.svg rename to ui/qml/xstudio/assets/icons/volume_no_sound.svg diff --git a/ui/qml/xstudio/assets/icons/volume_off.svg b/ui/qml/xstudio/assets/icons/volume_off.svg new file mode 100644 index 000000000..11c325584 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/volume_off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/new/volume_up.svg b/ui/qml/xstudio/assets/icons/volume_up.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/volume_up.svg rename to ui/qml/xstudio/assets/icons/volume_up.svg diff --git a/ui/qml/xstudio/assets/icons/warning.svg b/ui/qml/xstudio/assets/icons/warning.svg new file mode 100644 index 000000000..0c2da12d0 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/waves.svg b/ui/qml/xstudio/assets/icons/waves.svg new file mode 100644 index 000000000..9b213dc58 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/waves.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/assets/icons/width.svg b/ui/qml/xstudio/assets/icons/width.svg new file mode 100644 index 000000000..63b182052 --- /dev/null +++ b/ui/qml/xstudio/assets/icons/width.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/reskin/assets/icons/x.svg b/ui/qml/xstudio/assets/icons/x.svg old mode 100755 new mode 100644 similarity index 100% rename from ui/qml/reskin/assets/icons/x.svg rename to ui/qml/xstudio/assets/icons/x.svg diff --git a/ui/qml/xstudio/assets/icons/xstudio_logo_256_v1.png b/ui/qml/xstudio/assets/icons/xstudio_logo_256_v1.png new file mode 100644 index 000000000..e1698d37a Binary files /dev/null and b/ui/qml/xstudio/assets/icons/xstudio_logo_256_v1.png differ diff --git a/ui/qml/xstudio/images/xstudio_logo_256_v1.svg b/ui/qml/xstudio/assets/icons/xstudio_logo_256_v1.svg similarity index 100% rename from ui/qml/xstudio/images/xstudio_logo_256_v1.svg rename to ui/qml/xstudio/assets/icons/xstudio_logo_256_v1.svg diff --git a/ui/qml/reskin/assets/icons/new/zoom_in.svg b/ui/qml/xstudio/assets/icons/zoom_in.svg similarity index 100% rename from ui/qml/reskin/assets/icons/new/zoom_in.svg rename to ui/qml/xstudio/assets/icons/zoom_in.svg diff --git a/ui/qml/reskin/assets/images/sample.png b/ui/qml/xstudio/assets/images/sample.png similarity index 100% rename from ui/qml/reskin/assets/images/sample.png rename to ui/qml/xstudio/assets/images/sample.png diff --git a/ui/qml/xstudio/assets/images/xstudio_backdrop_v004_03.svg b/ui/qml/xstudio/assets/images/xstudio_backdrop_v004_03.svg new file mode 100644 index 000000000..7d3ba18f6 --- /dev/null +++ b/ui/qml/xstudio/assets/images/xstudio_backdrop_v004_03.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/qml/xstudio/bars/XsLayoutsBar.qml b/ui/qml/xstudio/bars/XsLayoutsBar.qml deleted file mode 100644 index a81c3894b..000000000 --- a/ui/qml/xstudio/bars/XsLayoutsBar.qml +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.3 - -import xStudio 1.0 - -Rectangle { - - property int currentIndex: 0 - border.color: Qt.darker(XsStyle.mainBackground,1.2) - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.lighter(XsStyle.mainBackground,1.2) } - GradientStop { position: 1.0; color: XsStyle.mainBackground } - } - property var current_layout: sessionWidget.layout_name - - ListModel{ - - id: layoutsModel - - // ListElement{ - // name: "Browse" - // layout_name: "browse_layout" - // } - ListElement{ - name: "Edit" - layout_name: "edit_layout" - } - ListElement{ - name: "Review" - layout_name: "review_layout" - } - ListElement{ - name: "Present" - layout_name: "presentation_layout" - } - } - - RowLayout { - - id: buttons_layout - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.topMargin: 4 - anchors.rightMargin: 12 - - spacing: 5 - - Repeater { - - id: repeater - model: layoutsModel - - Rectangle { - - id: plus_button - width: text_metrics.width+10 - color: "transparent" - Layout.fillHeight: true - property var hovered: mouse_area.containsMouse - property bool is_active: current_layout === layout_name - - Rectangle { - anchors.fill: parent - color: "white" - opacity: 0.15 - visible: hovered - } - - Text { - id: label - anchors.centerIn: parent - text: name - verticalAlignment: Text.AlignVCenter - color: is_active ? XsStyle.controlColor : XsStyle.controlTitleColor - font.family: XsStyle.controlTitleFontFamily - font.pixelSize: XsStyle.layoutsBarFontSize - - } - - TextMetrics { - id: text_metrics - font: label.font - text: label.text - } - - MouseArea { - id: mouse_area - anchors.fill: parent - hoverEnabled: true - onClicked: { - sessionWidget.layout_name = layout_name - } - } - - - } - } - } - -} diff --git a/ui/qml/xstudio/bars/XsMainControls.qml b/ui/qml/xstudio/bars/XsMainControls.qml deleted file mode 100644 index 093670492..000000000 --- a/ui/qml/xstudio/bars/XsMainControls.qml +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.3 - -import xStudio 1.0 - - -Rectangle { - id: mainControls - - color: XsStyle.mainBackground - - implicitHeight: XsStyle.transportHeight * opacity - - property int buttonPadding: 7 - - visible: opacity !== 0 - opacity: 1 - Behavior on opacity { - NumberAnimation { duration: playerWidget.doTrayAnim?200:0 } - } - - property var playheadFrame: playhead ? playhead.frame + 1 : 1 - property var playheadFrames: playhead ? playhead.frames : 1 - - RowLayout { - - anchors.fill: parent - anchors.topMargin: 2 - anchors.bottomMargin: 2 - - anchors.leftMargin: spacing - anchors.rightMargin: spacing - - // The top timeline row. - id: toolBar - - spacing: 4 - - - /*XsTray { - id: leftTray - anchors.left: parent.left - color: XsStyle.controlBackground - - XsTrayDivider {} - }*/ - XsTransportTray { - id: transportTray - Layout.fillHeight: true - pad: mainControls.buttonPadding - } - - - XsTimelinePositionWidget { - id:currentTimeText - value: playheadFrame - Layout.fillHeight: true - } - - XsTimelineSlider { - Layout.fillWidth: true - Layout.fillHeight: true - } - - XsTimelineDurationWidget { - id: durationTimeText - value: {preferences.timeline_indicator.value === "Duration" ? playheadFrames : playheadFrames-playheadFrame } - Layout.fillHeight: true - } - - XsVolumeWidget { - Layout.fillHeight: true - //icon.color: hovered ? XsStyle.controlColor : XsStyle.controlTitleColor - buttonPadding: mainControls.buttonPadding - } - - XsLoopWidget { - Layout.fillHeight: true - //icon.color: hovered ? XsStyle.controlColor : XsStyle.controlTitleColor - buttonPadding: mainControls.buttonPadding - } - - XsSnapshotWidget { - Layout.fillHeight: true - //icon.color: hovered ? XsStyle.controlColor : XsStyle.controlTitleColor - buttonPadding: mainControls.buttonPadding - } - - XsTrayButton { - - Layout.fillHeight: true - text: "Pop Out" - icon.source: "qrc:/icons/popout_viewer.png" - tooltip: "Open the pop-out interface." - buttonPadding: mainControls.buttonPadding - visible: sessionWidget.window_name == "main_window" - onClicked: { - app_window.togglePopoutViewer() - } - toggled_on: app_window ? app_window.popout_window ? app_window.popout_window.visible : false : false - - } - } - -} diff --git a/ui/qml/xstudio/bars/XsMediaInfoBar.qml b/ui/qml/xstudio/bars/XsMediaInfoBar.qml deleted file mode 100644 index b2f9ee509..000000000 --- a/ui/qml/xstudio/bars/XsMediaInfoBar.qml +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.5 - -import xStudio 1.0 -import xstudio.qml.module 1.0 -import xstudio.qml.helpers 1.0 - -Rectangle { - - id: mediaInfoBar - implicitHeight: XsStyle.mediaInfoBarFontSize*2.2*opacity//filenameLabel.height + XsStyle.mediaInfoBarMargins*2 - color: XsStyle.mediaInfoBarBackground - opacity: 1 - visible: opacity !== 0 - z: 1000 - - property var playhead: viewport.playhead - property var format: mediaImageSource.values.formatRole ? mediaImageSource.values.formatRole : "TBD" - property var bitDepth: mediaImageSource.values.bitDepthRole ? mediaImageSource.values.bitDepthRole : "TBD" - property var fpsString: mediaImageSource.values.rateFPSRole ? mediaImageSource.values.rateFPSRole : "TBD" - property var resolution: mediaImageSource.values.resolutionRole ? mediaImageSource.values.resolutionRole : "TBD" - - function sourceOffsetFramesChanged() { - offsetInput.text = (playhead.sourceOffsetFrames > 0 ? '+' : '') + playhead.sourceOffsetFrames - } - - property bool offset_enabled: true//{!playhead ? false : (playhead.compareMode != 0 && playhead.compareMode != 5)} - - XsModuleAttributes { - id: playhead_attrs - attributesGroupNames: "playhead" - } - - property bool auto_align_enabled: playhead_attrs.compare !== undefined ? playhead_attrs.compare == "A/B" : false - - Behavior on opacity { - NumberAnimation { duration: playerWidget.doTrayAnim?200:0 } - } - - RowLayout { - - anchors.fill: parent - - XsMediaInfoBarAutoAlign{ - id: auto_align_option - label_text: "Auto Align" - visible: auto_align_enabled - tooltip_text: "Select option 'On' to auto align and extend range to include frames from all sources. Select 'On (Trim)' to auto align and reduce to frame range common to all sources." - Layout.fillWidth: true - Layout.fillHeight: true - } - - XsMediaInfoBarOffset { - id: offset_group - visible: offset_enabled - Layout.fillWidth: true - Layout.fillHeight: true - } - - XsMediaInfoBarItem { - id: format_display - label_text: "Format" - value_text: format - tooltip_text: "The image format or video codec of the current source" - Layout.fillWidth: true - Layout.fillHeight: true - } - - XsMediaInfoBarItem { - id: bitdepth_display - label_text: "Bit Depth" - value_text: bitDepth - tooltip_text: "The image bitdepth of the current source" - Layout.fillWidth: true - Layout.fillHeight: true - } - - XsMediaInfoBarItem { - id: fps_display - label_text: "FPS" - value_text: fpsString - tooltip_text: "The playback rate of the current source" - Layout.fillWidth: true - Layout.fillHeight: true - } - - XsMediaInfoBarItem { - id: res_display - label_text: "Res" - value_text: resolution - tooltip_text: "The image resolution in pixels of the current source" - Layout.fillWidth: true - Layout.fillHeight: true - } - - } -} \ No newline at end of file diff --git a/ui/qml/xstudio/bars/XsMediaListPanelHeader.qml b/ui/qml/xstudio/bars/XsMediaListPanelHeader.qml deleted file mode 100644 index e947a985c..000000000 --- a/ui/qml/xstudio/bars/XsMediaListPanelHeader.qml +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.5 -import QtGraphicalEffects 1.12 -import QtQml.Models 2.15 -import xstudio.qml.helpers 1.0 - -import xStudio 1.0 - -Rectangle { - - id: media_list_header - color: "transparent" - property bool expanded: true - anchors.fill: parent - - Label { - anchors.leftMargin: 7 - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "Media" - color: XsStyle.controlColor - font.pixelSize: XsStyle.sessionBarFontSize - font.family: XsStyle.controlTitleFontFamily - font.hintingPreference: Font.PreferNoHinting - font.weight: Font.DemiBold - verticalAlignment: Qt.AlignVCenter - } - - Label { - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - text: app_window.currentSource.fullName - color: XsStyle.controlColor - font.pixelSize: XsStyle.sessionBarFontSize - font.family: XsStyle.controlTitleFontFamily - font.hintingPreference: Font.PreferNoHinting - font.weight: Font.DemiBold - verticalAlignment: Qt.AlignVCenter - } - - Label { - anchors.rightMargin: 7 - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - text: app_window.currentSource.values.mediaCountRole ? app_window.currentSource.values.mediaCountRole : 0 - color: XsStyle.controlColor - font.pixelSize: XsStyle.sessionBarFontSize - font.family: XsStyle.controlTitleFontFamily - font.hintingPreference: Font.PreferNoHinting - font.weight: Font.DemiBold - verticalAlignment: Qt.AlignVCenter - } - -} diff --git a/ui/qml/xstudio/bars/XsMenuBar.qml b/ui/qml/xstudio/bars/XsMenuBar.qml deleted file mode 100644 index 40bee2caa..000000000 --- a/ui/qml/xstudio/bars/XsMenuBar.qml +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.5 -import QtGraphicalEffects 1.12 - -import xStudio 1.0 - -MenuBar { - id:myMenuBar - - height: XsStyle.menuBarHeight*opacity - implicitHeight: XsStyle.menuBarHeight*opacity - visible: opacity != 0.0 - background: Rectangle {color: XsStyle.mainBackground} - property var playerWidget: undefined - property var playhead: viewport.playhead - - property alias playerWidget: myMenuBar.playerWidget - property alias playhead: myMenuBar.playhead - - property alias panelMenu: panel_menu - property alias mediaMenu: media_menu - - // property alias panelMenu: myMenuBar.panel_menu - - delegate: MenuBarItem { - id: menuBarItem - height: XsStyle.menuBarHeight - contentItem: Text { - text: XsUtils.replaceMenuText(menuBarItem.text) - font.pixelSize: XsStyle.menuFontSize - font.family: XsStyle.menuFontFamily - font.hintingPreference: Font.PreferNoHinting - opacity: enabled ? 1.0 : 0.3 - color: XsStyle.hoverColor//menuBarItem.highlighted ? XsStyle.hoverColor : XsStyle.mainColor - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - x: 30 - } - background: Rectangle { -// color: menuBarItem.menu.opened?XsStyle.highlightColor : "transparent" - color: menuBarItem.menu.opened?XsStyle.primaryColor: "transparent" - gradient:menuBarItem.menu.opened?styleGradient.accent_gradient:Gradient.gradient - } - onFocusChanged: { - // this prevents stealing keypresses that might be needed elsewhere. - // (like space for play/pause-- not opening a menu) - //focus = false - } - - } - XsFileMenu {} - XsEditMenu {} - XsPlaylistMenu {} - XsMediaMenu { - id: media_menu - } - XsTimelineMenu {} - XsPlaybackMenu { - id: playback_menu - } - XsViewerContextMenu { - } - XsLayoutMenu {} - XsSnapshotMenu {} - XsPanelMenu { - id: panel_menu - } - XsMenu { - title: qsTr("Publish") - id: publish_menu_toplevel - XsModuleMenuBuilder { - id: publish_menu - parent_menu: publish_menu_toplevel - root_menu_name: "publish_menu" - } - } - - XsMenu { - title: qsTr("Colour") - id: colour_menu_toplevel - XsModuleMenuBuilder { - parent_menu: colour_menu_toplevel - root_menu_name: "Colour" - } - } - - XsHelpMenu {} - - -} - diff --git a/ui/qml/xstudio/bars/XsSessionControls.qml b/ui/qml/xstudio/bars/XsSessionControls.qml deleted file mode 100644 index ef8cacb0f..000000000 --- a/ui/qml/xstudio/bars/XsSessionControls.qml +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.3 - -import xStudio 1.0 - -Rectangle { - - // The main row with play controls. - height: XsStyle.sessionBarHeight*opacity - visible: opacity !== 0 - opacity: 1 - - Behavior on opacity { - NumberAnimation { duration: XsStyle.presentationModeAnimLength } - } - - color: XsStyle.mainBackground - - gradient: Gradient { - orientation: Gradient.Horizontal - GradientStop { position: 0.2; color: XsStyle.controlBackground} - GradientStop { position: 0.5; color: XsStyle.mainBackground} - GradientStop { position: 0.8; color: XsStyle.controlBackground} - } - - function toggleNotes(show=undefined) { - mediaToolsTray.toggleNotesDialog(show) - } - function toggleDrawDialog(show=undefined) { - mediaToolsTray.toggleDrawDialog(show) - } - - Rectangle { - - id: myRowLayout - anchors.fill: parent - color: "transparent" - - /*onWidthChanged: { - if(width < (mediaToolsTray.layout.implicitWidth + sessionSettingsTray.layout.implicitWidth)) { - mediaToolsTray.collapsed = true - sessionSettingsTray.collapsed = true - } else { - mediaToolsTray.collapsed = false - sessionSettingsTray.collapsed = false - } - }*/ - - XsMediaToolsTray { - id: mediaToolsTray - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - } - - XsLayoutTray { - id: layoutTray - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.bottom: parent.bottom - } - - XsPaneVisibilityTray { - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - //width:300 - id: paneVisibilityTray - } - } -} diff --git a/ui/qml/xstudio/bars/XsShortcuts.qml b/ui/qml/xstudio/bars/XsShortcuts.qml deleted file mode 100644 index 2afb8614f..000000000 --- a/ui/qml/xstudio/bars/XsShortcuts.qml +++ /dev/null @@ -1,510 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Window 2.15 - -import xStudio 1.0 - -import xstudio.qml.module 1.0 -import xstudio.qml.viewport 1.0 - -Item { - - id: shortcuts - property var playlist_panel: sessionWidget.playlist_panel - property var viewport: sessionWidget.viewport - property var playhead: viewport.playhead - property var context - - property var topLevelWindow - property bool shortcutsWithDigits: true - - function play_faster(forwards) { - if (!playhead.playing) { - playhead.forward = forwards - playhead.velocityMultiplier = 1 - playhead.playing = true - } else if (playhead.forward != forwards) { - playhead.forward = forwards - playhead.velocityMultiplier = 1 - } else if (playhead.velocityMultiplier < 16) { - playhead.velocityMultiplier = playhead.velocityMultiplier*2 - } - } - - // Keys.forwardTo: viewport - // For now, define shortcuts for controlling playback as I haven't worked - // out how to capture keystrokes in the whole UI and forwarding to the - // viewport to handle - that would be a better solution if we can make - // it work! - /*XsHotkey { - context: shortcuts.context - sequence: ["Space"] - onActivated: { - playhead.playing = !playhead.playing - } - }*/ - - XsHotkey { - context: shortcuts.context - sequence: "Alt+E" - name: "Debug dump session" - description: "Dumps session data into the terminal for debug purposes only." - onActivated: { - app_window.sessionModel.dump() - } - } - - - // Other shortcuts ... - XsHotkey { - context: shortcuts.context - sequence: "Escape" - name: "Toggle controls visible, exists fullscreen" - description: "Hit this key to show the x-studio controls and/or exit full screen mode" - onActivated: { - if (!playerWidget.controlsVisible) { - playerWidget.toggleControlsVisible() - } else { - playerWidget.normalScreen() - } - } - } - XsHotkey { - context: shortcuts.context - sequence: "Left" - name: "Step backwards one frame" - description: "Halts playback (if playing) and steps playhead back one frame" - onActivated: { - if (playhead.playing) { - playhead.playing = false - } - playhead.step(-1) - } - autoRepeat: true - } - XsHotkey { - context: shortcuts.context - sequence: "Right" - name: "Step forwards one frame" - description: "Halts playback (if playing) and steps playhead forwards one frame" - onActivated: { - if (playhead.playing) { - playhead.playing = false - } - playhead.step(1) - } - autoRepeat: true - } - XsHotkey { - context: shortcuts.context - sequence: "Up" - name: "Jump to previous source" - description: "Cycles current on-screen media backwards through selected media sources" - onActivated: app_window.sessionFunction.jumpToPreviousSource() - } - XsHotkey { - context: shortcuts.context - sequence: "Down" - name: "Jump to next source" - description: "Cycles current on-screen media forwards through selected media sources" - onActivated: app_window.sessionFunction.jumpToNextSource() - } - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+a" - name: "Select All" - description: "Selects all media in the playlist for viewing" - onActivated: app_window.sessionFunction.selectAllMedia() - } - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+d" - name: "De-select All" - description: "De-selects all media in the playlist for viewing, then selects the first media item." - onActivated: app_window.sessionFunction.deselectAllMedia() - } - - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+n" - name: "New Session" - description: "Create a new session, asks if you want to save current session." - onActivated: { - app_window.sessionFunction.saveBeforeNewSession() - } - } - XsHotkey { - context: shortcuts.context - sequence: "." - name: "Next Bookmark" - description: "Jump through the timeline to the next bookmarked frame." - onActivated: playhead.frame = playhead.nextBookmark(playhead.frame) - } - XsHotkey { - context: shortcuts.context - sequence: "," - name: "Previous Bookmark" - description: "Jump through the timeline to the previous bookmarked frame." - onActivated: playhead.frame = playhead.previousBookmark(playhead.frame) - } - - XsHotkey { - context: shortcuts.context - sequence: "Shift+l" - name: "Nearest Bookmark" - description: "Jump through the timeline to the nearest bookmarked frame." - onActivated: { - let pos = playhead.getNearestBookmark(playhead.frame) - if(pos.length) { - playhead.setLoopStart(pos[0] + playhead.sourceOffsetFrames) - playhead.setLoopEnd(pos[1] + playhead.sourceOffsetFrames) - playhead.useLoopRange = true - } - } - } - - XsHotkey { - context: shortcuts.context - sequence: ";" - name: "Create Bookmark Silently" - description: "Create a new bookmark on the current frame." - onActivated: app_window.sessionFunction.addNote() - } - - XsHotkey { - context: shortcuts.context - sequence: ":" - name: "Create Bookmark" - description: "Create a new bookmark on the current frame and pops-up the bookmarks interface." - onActivated: { - app_window.sessionFunction.addNote() - sessionWidget.toggleNotes(true) - } - } - - XsHotkey { - context: shortcuts.context - sequence: "'" - name: "Extend Bookmark Duration" - description: "Extend the duration of the nearest bookmark on the timeline so that it reaches to the current frame." - onActivated: { - let pos = playhead.getNearestBookmark(playhead.frame) - if(pos.length) { - let ind = app_window.bookmarkModel.search(pos[2], "uuidRole") - if(ind.valid) { - app_window.bookmarkModel.set(ind, playhead.frame - pos[0], "durationFrameRole") - } - } - } - } - - // Temp fix to show annotations tool (not strictly keeping it as plugin - // controlled behaviour) - XsModuleAttributes { - id: anno_tool_backend_settings - attributesGroupNames: "annotations_tool_settings_0" - } - - XsHotkey { - context: shortcuts.context - sequence: "n" - name: "Show or Hide Notes" - description: "Either shows and activates or hides the notes (bookmarks) window." - onActivated: sessionWidget.toggleNotes() - } - - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+Shift+p" - name: "Create New Playlist" - description: "Creates a new playlist." - onActivated: sessionFunction.newPlaylist( - app_window.sessionModel.index(0, 0), null, playlist_panel - ) - } - - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+Shift+d" - name: "Create Divider" - description: "Creates a divider in the session playlist view." - onActivated: sessionFunction.newDivider( - app_window.sessionModel.index(0, 0), null, playlist_panel - ) - } - - XsHotkey { - context: shortcuts.context - sequence: "Alt+d" - name: "Create Divider With Date" - description: "Creates a divider in the session playlist view and titles it with today's date." - onActivated: sessionFunction.newDivider( - app_window.sessionModel.index(0, 0), XsUtils.yyyymmdd("-"), playlist_panel - ) - } - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+Shift+s" - name: "Create Subset" - description: "Creates a playlsit subset under the current playlist." - onActivated: { - let ind = app_window.sessionFunction.firstSelected("Playlist") - if(ind != null) { - sessionFunction.newSubset( - ind.model.index(2,0,ind), null, playlist_panel - ) - } - } - } - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+Shift+t" - name: "Create Timeline" - description: "Creates a timeline under the current playlist." - } - - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+Shift+c" - name: "Create Contact Sheet" - description: "Creates a contact sheet under the current playlist." - } - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+Shift+i" - name: "Create Playlist Divider" - description: "Creates a divider within the subsets of the current playlist." - onActivated: { - let ind = app_window.sessionFunction.firstSelected("Playlist") - if(ind != null) { - sessionFunction.newDivider( - ind.model.index(2,0,ind), null, playlist_panel - ) - } - } - } - - XsHotkey { - context: shortcuts.context - sequence: "i" - name: "Set Loop In Point" - description: "Sets the playhead looping in point at the current frame." - onActivated: { - playhead.setLoopStart(playhead.frame) - playhead.useLoopRange = true - } - } - XsHotkey { - context: shortcuts.context - sequence: "o" - name: "Set Loop Sout Point" - description: "Sets the playhead looping out point at the current frame." - onActivated: { - playhead.setLoopEnd(playhead.frame) - playhead.useLoopRange = true - } - } - XsHotkey { - context: shortcuts.context - sequence: "p" - name: "Toggle loop in/out range" - description: "Turns on or turns off the in/out points for looping" - onActivated: playhead.useLoopRange = !playhead.useLoopRange - } - - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+h" - name: "Toggle Controls Visible" - description: "Toggles the main UI controls visible to give minimal interface" - onActivated: playerWidget.toggleControlsVisible() - } - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+f" - name: "Toggle Full Screen" - description: "Toggles the xStudio UI in/out of full-screen mode" - onActivated: playerWidget.toggleFullscreen() - } - - XsHotkey { - context: shortcuts.context - sequence: "1" - name: "View 1st selected source" - onActivated: playhead.keyPlayheadIndex = 0 - } - XsHotkey { - context: shortcuts.context - sequence: "2" - name: "View 2nd selected source" - onActivated: playhead.keyPlayheadIndex = 1 - } - XsHotkey { - context: shortcuts.context - sequence: "3" - name: "View 3rd selected source" - onActivated: playhead.keyPlayheadIndex = 2 - } - XsHotkey { - context: shortcuts.context - sequence: "4" - name: "View 4th selected source" - onActivated: playhead.keyPlayheadIndex = 3 - } - XsHotkey { - context: shortcuts.context - sequence: "5" - name: "View 5th selected source" - onActivated: playhead.keyPlayheadIndex = 4 - } - XsHotkey { - context: shortcuts.context - sequence: "6" - name: "View 6th selected source" - onActivated: playhead.keyPlayheadIndex = 5 - } - XsHotkey { - context: shortcuts.context - sequence: "7" - name: "View 7th selected source" - onActivated: playhead.keyPlayheadIndex = 6 - } - XsHotkey { - context: shortcuts.context - sequence: "8" - name: "View 8th selected source" - onActivated: playhead.keyPlayheadIndex = 7 - } - XsHotkey { - context: shortcuts.context - sequence: "9" - name: "View 9th selected source" - onActivated: playhead.keyPlayheadIndex = 8 - } - - XsHotkey { - context: shortcuts.context - sequence: "Delete" - name: "Remove media" - description: "Removes current selected media from playlist" - onActivated: app_window.sessionFunction.requestRemoveSelectedMedia() - } - - XsHotkey { - context: shortcuts.context - sequence: "Backspace" - name: "Remove media (backspace)" - description: "Removes current selected media from playlist" - onActivated: app_window.sessionFunction.requestRemoveSelectedMedia() - } - - Repeater { - model: app_window.mediaFlags - Item { - property var myName: name - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+" + (index == app_window.mediaFlags.count-1 ? 0: index+1) - name: "Flag media with color " + (index == app_window.mediaFlags.count-1 ? 0: index+1) - description: "Flags media with the associated colour code" - //onActivated: app_window.sessionFunction.flagSelectedMedia(colour, control.name) - onActivated: { - let model = app_window.mediaFlags - app_window.sessionFunction.flagSelectedMedia(colour, myName) - } - } - } - } - - XsHotkey { - context: shortcuts.context - sequence: "F1" - name: "Show User Docs" - description: "Loads the xstudio user docs into your default web browser" - onActivated: { - var url = studio.userDocsUrl() - if (url == "") { - errorDialog.text = "Unable to resolve location of user docs." - errorDialog.visible = true - } else { - Qt.openUrlExternally(url) - } - } - } - - XsHotkey { - context: shortcuts.context - sequence: "Tab" - name: "Toggle Presentation Mode" - description: "Toggles xStudio in/out of presentation mode" - onActivated: { - sessionWidget.togglePresentationLayout() - } - } - - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+s" - name: "Save Session" - description: "Saves current session under current file path." - onActivated: app_window.sessionFunction.saveSession() - } - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+Shift+s" - name: "Save Session As" - description: "Saves current session under a new file path." - onActivated: app_window.sessionFunction.saveSessionAs() - - } - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+o" - name: "Open new session" - description: "Open new session" - onActivated: app_window.sessionFunction.saveBeforeOpen() - } - - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+/" - name: "Show about info" - description: "Shows the xStudio About panel." - onActivated: openDialog("qrc:/dialogs/XsAboutDialog.qml") - } - XsHotkey { - context: shortcuts.context - sequence: "Ctrl+Q" - name: "Quit" - description: "Quits xStudio." - onActivated: app_window.close() - } - XsHotkey { - context: shortcuts.context - sequence: "Home" - name: "To Start" - description: "Jumps to the start frame" - onActivated: playhead.frame = playhead.useLoopRange ? playhead.loopStart : 0 - } - XsHotkey { - context: shortcuts.context - sequence: "End" - name: "To End" - description: "Jumps to the end frame" - onActivated: playhead.frame = playhead.useLoopRange ? playhead.loopEnd : playhead.frames-1 - } - - property var thedialog: undefined - function openDialog(qml_path) { - var component = Qt.createComponent(qml_path); - if (component.status == Component.Ready) { - thedialog = component.createObject(app_window); - thedialog.visible = true - } else { - // Error Handling - console.log("Error loading component:", component.errorString()); - } - } - - -} \ No newline at end of file diff --git a/ui/qml/xstudio/bars/XsStatusBar.qml b/ui/qml/xstudio/bars/XsStatusBar.qml deleted file mode 100644 index c08badbd5..000000000 --- a/ui/qml/xstudio/bars/XsStatusBar.qml +++ /dev/null @@ -1,198 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.0 -import QtQuick.Controls 2.5 - -import xStudio 1.0 - -Rectangle { - id: statusRect - color: XsStyle.statusBarBackground -// property alias title: myTitle.text -// property alias text: myMessage.text - property bool textVisible: true - property color myTitleBG: "transparent" - property string myTitleFG: "" - - property string lastTitle: "" - property string lastMessage: "" - - height: XsStyle.statusBarHeight*opacity - - visible: opacity != 0 - - Behavior on opacity { - NumberAnimation { duration: XsStyle.presentationModeAnimLength } - } - - Behavior on color { - ColorAnimation {duration:XsStyle.colorDuration} - } - Rectangle { - height: 1 - color: XsStyle.controlBackground - anchors.left: parent.left - anchors.right: parent.right - } - function restoreColors() { - myTitleBG = 'transparent' - myTitleFG = '' - myMessage.color = XsStyle.hoverColor - if (lastTitle) { - myTitle.text = lastTitle - myTitle.visible = true - } else { - myTitle.visible = false - } - if (lastMessage) { - myMessage.text = lastMessage - myMessage.visible = true - } else { - myMessage.visible = false - } - } - Timer { - id: restoreColorsTimer - running: false - repeat: false - interval: 3500 - onTriggered: restoreColors() - } - Timer { - id: clearMessageTimer - interval: 2000 - onTriggered: clearMessage() - } - - function _statusMessage(title, message, ttlbgcolor) { - myDev.opacity = 0 - restoreColorsTimer.stop() - lastTitle = myTitle.text - lastMessage = myMessage.text - myTitle.text = title - myTitle.visible = true - myTitleFG = ttlbgcolor //'#fff' -// myTitleBG = ttlbgcolor - - myMessage.text = message - myMessage.visible = true - myMessage.color = '#fff' - restoreColorsTimer.restart() - clearMessageTimer.restart() - } - function normalMessage(message, title, isdev) { - if(restoreColorsTimer.running) { - lastTitle = title - lastMessage = message - } else { - if(title) { - myTitle.text = title - myTitle.visible = true - } else { - myTitle.visible = false - } - myMessage.visible = true - myMessage.text = message - if (typeof(isdev) === "undefined") { - isdev = false - } - if (isdev) { - myDev.opacity = 1 - } else { - myDev.opacity = 0 - } - } - } - function clearMessage() { - myDev.opacity = 0 - if(restoreColorsTimer.running) { - lastTitle = "" - lastMessage = "" - } else { - textVisible = false - myTitle.text = "" - myMessage.text = "" - } - } - function infoMessage(message) { - _statusMessage('INFO', message, XsStyle.highlightColor) - } - function warningMessage(message) { - _statusMessage('WARNING', message, "#c80") - } - function errorMessage(message) { - _statusMessage('ERROR', message, "#800") - } - Label { - id: myDev - visible: opacity === 1 - padding: 5 - opacity: 0 -// visible: false - text: "Coming Soon" - color: '#fff' - background: Rectangle { - color: XsStyle.indevColor -// gradient:styleGradient.accent_gradient - radius: 10 - } - anchors.left: statusRect.left - anchors.leftMargin: 3 - anchors.top: statusRect.top - anchors.bottom: statusRect.bottom - anchors.topMargin: 3 - anchors.bottomMargin: 3 - verticalAlignment: Label.AlignVCenter - font.pixelSize: XsStyle.statusBarFontSize - font.family: XsStyle.fontFamily - font.hintingPreference: Font.PreferNoHinting - } - - Label { - id: myTitle - padding: 5 - visible: textVisible - text: "Welcome to " + Qt.application.name + '.' - color: myTitleFG?myTitleFG:XsStyle.highlightColor - Behavior on color { - ColorAnimation {duration:XsStyle.colorDuration} - } - anchors.left: myDev.visible?myDev.right:statusRect.left -// anchors.leftMargin: myDev.visible?0:10 - anchors.top: statusRect.top - anchors.bottom: statusRect.bottom - anchors.topMargin: 3 - anchors.bottomMargin: 3 - verticalAlignment: Label.AlignVCenter - font.bold: true - font.pixelSize: XsStyle.statusBarFontSize - font.family: XsStyle.fontFamily - font.hintingPreference: Font.PreferNoHinting - background: Rectangle { - id: myTitleRect -// border { -// width: 1 -// color: XsStyle.highlightColor -// } - - color: "transparent" - radius: 5 -// Behavior on color { -// ColorAnimation {duration:XsStyle.colorDuration} -// } - } - } - Label { - id: myMessage - visible: textVisible - anchors.top: statusRect.top - anchors.bottom: statusRect.bottom - anchors.left: myTitle.right - anchors.leftMargin: 5 - color: XsStyle.hoverColor - verticalAlignment: Label.AlignVCenter - font.pixelSize: XsStyle.statusBarFontSize - font.family: XsStyle.fontFamily - font.hintingPreference: Font.PreferNoHinting - - } -} diff --git a/ui/qml/xstudio/bars/XsTimelinePanelHeader.qml b/ui/qml/xstudio/bars/XsTimelinePanelHeader.qml deleted file mode 100644 index 6a0581013..000000000 --- a/ui/qml/xstudio/bars/XsTimelinePanelHeader.qml +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.5 -import QtGraphicalEffects 1.12 - -import xStudio 1.0 - -Rectangle { - - id: timeline_panel_header - color: "transparent" - property bool expanded: true - anchors.fill: parent - - Label { - anchors.leftMargin: 7 - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "Timeline" - color: XsStyle.controlColor - font.pixelSize: XsStyle.sessionBarFontSize - font.family: XsStyle.controlTitleFontFamily - font.hintingPreference: Font.PreferNoHinting - font.weight: Font.DemiBold - verticalAlignment: Qt.AlignVCenter - } - - Label { - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - text: app_window.currentSource.fullName - color: XsStyle.controlColor - font.pixelSize: XsStyle.sessionBarFontSize - font.family: XsStyle.controlTitleFontFamily - font.hintingPreference: Font.PreferNoHinting - font.weight: Font.DemiBold - verticalAlignment: Qt.AlignVCenter - } - - Label { - anchors.rightMargin: 7 - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - text: app_window.currentSource.mediaCount != undefined ? app_window.currentSource.mediaCount : 0 - color: XsStyle.controlColor - font.pixelSize: XsStyle.sessionBarFontSize - font.family: XsStyle.controlTitleFontFamily - font.hintingPreference: Font.PreferNoHinting - font.weight: Font.DemiBold - verticalAlignment: Qt.AlignVCenter - } - -} diff --git a/ui/qml/xstudio/bars/XsToolBar.qml b/ui/qml/xstudio/bars/XsToolBar.qml deleted file mode 100644 index 186f518b7..000000000 --- a/ui/qml/xstudio/bars/XsToolBar.qml +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.14 -import QtQuick.Extras 1.4 - -import xStudio 1.0 - -import xstudio.qml.module 1.0 -import xstudio.qml.models 1.0 - -import BasicViewportMask 1.0 - -Rectangle { - id: myBar - property int barHeight: 21 - property int topPadding: XsStyle.lrMargin - height: barHeight - property int separator_width: 1 - - color: XsStyle.mainBackground - implicitHeight: { - return (barHeight + topPadding) * opacity - } - - XsModuleData { - id: tester - modelDataName: viewport.name + "_toolbar" - } - - opacity: 1 - visible: opacity !== 0 - - Behavior on opacity { - NumberAnimation { duration: playerWidget.doTrayAnim?200:0 } - } - - property var collapse_all: false; - - function checkCollapseMode() { - - // check if any of the widgets need to minimise, if so minimise - // all widgets (looks much nicer this way) or un-minimize - var c = false - for (var index = 0; index < the_view.count; index++) { - var the_item = the_view.itemAt(index) - if (the_item) { - var curr_but = the_item.item - if (curr_but && curr_but.suggestedCollapseMode == 3) { - c = true; - break; - } - } - } - if (c != collapse_all) collapse_all = c - - } - - onCollapse_allChanged: { - - for (var index = 0; index < the_view.count; index++) { - var obj = the_view.itemAt(index) - if(obj && obj.item) { - obj.item.collapse = collapse_all - } - } - } - - onWidthChanged: { - checkCollapseMode() - } - - Repeater { - - id: the_view - anchors.fill: parent - model: tester - - delegate: Item { - - id: parent_item - anchors.bottom: parent.bottom - anchors.top: parent.top - width: (myBar.width-separator_width*(the_view.count-1))/the_view.count - property var ordered_x_position: 0 - x: index*(width+separator_width) - property var dynamic_widget - - // 'title', 'type', 'qml_code' attributes may or may not be provided by the Repeater model, - // which connects with attributes in the backend via the Module class. We need to map - // them to local variables like this so we get signals when they are set or changed - property var title_: title ? title : "nought" - property var type_: type ? type : null - property var qml_code_: qml_code ? qml_code : null - - onQml_code_Changed: { - if (qml_code_) { - dynamic_widget = Qt.createQmlObject(qml_code_, parent_item) - } - } - - // attributes can have a floating point 'toolbar_position' data value ... this is - // used to position the attribute widgets in the toolbar, with the one with the - // lowest toolbar_position value left most and the highest rightmost. See arrange_widgets() - property var order_value: dynamic_widget ? dynamic_widget.toolbar_position_ ? dynamic_widget.toolbar_position_ : 10000.0 : 0 - - // for standard attribute types, we have standard toolbar widgets which - // we load dynamically here - onType_Changed: { - - if (qml_code_) { - // skip, handled above - } else if (type == "FloatScrubber") { - dynamic_widget = Qt.createQmlObject('import "../base/widgets"; XsToolbarFloatScrubber { anchors.fill: parent}', parent_item) - } else if (type == "ComboBox") { - dynamic_widget = Qt.createQmlObject('import "../base/widgets"; XsToolbarComboBox { anchors.fill: parent}', parent_item) - } else if (type == "OnOffToggle") { - dynamic_widget = Qt.createQmlObject('import "../base/widgets"; XsToolbarOnOffToggle { anchors.fill: parent}', parent_item) - } else { - console.log("XsToolBar failed to instance widget, unknown type:", type) - } - } - } - } -} diff --git a/ui/qml/xstudio/bars/XsViewportTitleBar.qml b/ui/qml/xstudio/bars/XsViewportTitleBar.qml deleted file mode 100644 index 1a72eb2f9..000000000 --- a/ui/qml/xstudio/bars/XsViewportTitleBar.qml +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Layouts 1.3 -import QtQuick.Controls 2.5 - -import xStudio 1.0 - -Rectangle { - - color: XsStyle.mainBackground - height: XsStyle.viewerTitleBarHeight*opacity - implicitHeight: XsStyle.viewerTitleBarHeight*opacity - - property alias mediaToolsTray: mediaToolsTray - visible: opacity != 0.0 - - Behavior on opacity { - NumberAnimation {duration: 200} - } - - XsMediaToolsTray { - id: mediaToolsTray - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: XsStyle.viewerTitleBarButtonsMargin - } - - Rectangle { - - x: Math.max(parent.width/2-width/2, mediaToolsTray.x+mediaToolsTray.width) - anchors.top: parent.top - anchors.bottom: parent.bottom - width: label_metrics.width - color: "transparent" - - Text { - - id: label - text: app_window.mediaImageSource.fileName - color: XsStyle.controlColor - verticalAlignment: Qt.AlignVCenter - horizontalAlignment: Qt.AlignLeft - anchors.fill: parent - - font { - pixelSize: XsStyle.mediaInfoBarFontSize+6 - family: XsStyle.controlTitleFontFamily - hintingPreference: Font.PreferNoHinting - } - } - - TextMetrics { - id: label_metrics - font: label.font - text: label.text - } - - } - - Rectangle { - - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - width: 30 - gradient: Gradient { - orientation: Gradient.Horizontal - GradientStop { position: 0.0; color: "#00bb7700"} - GradientStop { position: 1.0; color: XsStyle.mainBackground} - } - z: 1 - } - -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/core/XsDraggableItem.qml b/ui/qml/xstudio/base/core/XsDraggableItem.qml deleted file mode 100644 index 16b604b0e..000000000 --- a/ui/qml/xstudio/base/core/XsDraggableItem.qml +++ /dev/null @@ -1,254 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -/* - * listviewdragitem - * - * An example of reordering items in a ListView via drag'n'drop. - * - * Author: Aurélien Gâteau - * License: BSD - */ -import QtQuick 2.0 - -import xStudio 1.0 - -Item { - id: root - - default property Item contentItem - - // This item will become the parent of the dragged item during the drag operation - property Item draggedItemParent - - signal moveItemRequested(int from, int to) - - // Size of the area at the top and bottom of the list where drag-scrolling happens - property int scrollEdgeSize: 6 - - // Internal: set to -1 when drag-scrolling up and 1 when drag-scrolling down - property int _scrollingDirection: 0 - - // Internal: shortcut to access the attached ListView from everywhere. Shorter than root.ListView.view - property ListView _listView: ListView.view - - width: contentItem.width - height: topPlaceholder.height + wrapperParent.height + bottomPlaceholder.height - - // Make contentItem a child of contentItemWrapper - onContentItemChanged: { - contentItem.parent = contentItemWrapper; - } - - Rectangle { - id: topPlaceholder - anchors { - left: parent.left - right: parent.right - top: parent.top - } - height: 0 - color: "transparent" - } - - Item { - id: wrapperParent - anchors { - left: parent.left - right: parent.right - top: topPlaceholder.bottom - } - height: contentItem.height - - Rectangle { - id: contentItemWrapper - anchors.fill: parent - property var listview_model: _listView.model - // property var listview_view: _listView.view - - Drag.active: dragArea.drag.active - // Drag.hotSpot { - // x: contentItem.width / 2 - // y: contentItem.height / 2 - // } - - MouseArea { - id: dragArea - anchors.fill: parent - - // anchors { - // left: parent.left - // top: parent.top - // bottom: parent.bottom - // } - // width: 20 - drag.target: parent - // Disable smoothed so that the Item pixel from where we started the drag remains under the mouse cursor - drag.smoothed: false - drag.axis: Drag.YAxis - drag.threshold: 0 - - onReleased: { - if (drag.active) { - emitMoveItemRequested(); - } - } - } - } - } - - Rectangle { - id: bottomPlaceholder - anchors { - left: parent.left - right: parent.right - top: wrapperParent.bottom - } - height: 0 - color: "transparent" - } - - SmoothedAnimation { - id: upAnimation - target: _listView - property: "contentY" - to: 0 - running: _scrollingDirection == -1 - } - - SmoothedAnimation { - id: downAnimation - target: _listView - property: "contentY" - to: _listView.contentHeight - _listView.height - running: _scrollingDirection == 1 - } - - Loader { - id: topDropAreaLoader - active: model.index === 0 - anchors { - left: parent.left - right: parent.right - bottom: wrapperParent.verticalCenter - } - height: contentItem.height - sourceComponent: Component { - DropArea { - property int dropIndex: 0 - onEntered: { - if(! drag.source || drag.source.listview_model != _listView.model) - drag.accepted = false - } - } - } - } - - DropArea { - id: bottomDropArea - anchors { - left: parent.left - right: parent.right - top: wrapperParent.verticalCenter - } - property bool isLast: model.index === _listView.count - height: isLast ? _listView.contentHeight - y : contentItem.height - - property int dropIndex: model.index + 1 - - onEntered: { - if(! drag.source || drag.source.listview_model != _listView.model) - drag.accepted = false - } - } - - states: [ - State { - when: dragArea.drag.active - name: "dragging" - - ParentChange { - target: contentItemWrapper - parent: draggedItemParent - } - PropertyChanges { - target: contentItemWrapper - opacity: 0.7 - anchors.fill: undefined - width: contentItem.width - height: contentItem.height - } - PropertyChanges { - target: wrapperParent - height: 0 - } - PropertyChanges { - target: root - _scrollingDirection: { - var yCoord = _listView.mapFromItem(dragArea, 0, dragArea.mouseY).y; - if (yCoord < scrollEdgeSize) { - -1; - } else if (yCoord > _listView.height - scrollEdgeSize) { - 1; - } else { - 0; - } - } - } - } - , - State { - when: bottomDropArea.containsDrag - name: "droppingBelow" - PropertyChanges { - target: bottomPlaceholder - height: contentItem.height - } - PropertyChanges { - target: bottomDropArea - height: contentItem.height *2 - } - }, - State { - when: topDropAreaLoader.item && topDropAreaLoader.item.containsDrag - name: "droppingAbove" - PropertyChanges { - target: topPlaceholder - height: contentItem.height - } - PropertyChanges { - target: topDropAreaLoader - height: contentItem.height *2 - } - } - ] - - function emitMoveItemRequested() { - var dropArea = contentItemWrapper.Drag.target; - if (!dropArea) { - return; - } - var dropIndex = dropArea.dropIndex; - - // If the target item is below us, then decrement dropIndex because the target item is going to move up when - // our item leaves its place - // if (model.index < dropIndex) { - // dropIndex--; - // } - if (model.index === dropIndex) { - return; - } - root.moveItemRequested(model.index, dropIndex); - - // Scroll the ListView to ensure the dropped item is visible. This is required when dropping an item after the - // last item of the view. Delay the scroll using a Timer because we have to wait until the view has moved the - // item before we can scroll to it. - makeDroppedItemVisibleTimer.start(); - } - - Timer { - id: makeDroppedItemVisibleTimer - interval: 0 - onTriggered: { - _listView.positionViewAtIndex(model.index, ListView.Contain); - } - } -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/core/XsModuleMenuBuilder.qml b/ui/qml/xstudio/base/core/XsModuleMenuBuilder.qml deleted file mode 100644 index 090df945b..000000000 --- a/ui/qml/xstudio/base/core/XsModuleMenuBuilder.qml +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 - -import xstudio.qml.module 1.0 - -import xStudio 1.0 - -Item { - - id: parent_item - property var parent_menu - property var root_menu_name - property var insert_after - property int insert_after_index: -1 - - property var ct: parent_menu.count - onCtChanged: set_insert_index() - property var empty: module_menu_shim.empty - - onInsert_afterChanged: set_insert_index() - onParent_menuChanged: set_insert_index() - - function set_insert_index() { - if (parent_menu && insert_after) { - for (var idx = 0; idx < parent_menu.count; idx++) { - if (itemAt(idx) == insert_after) { - insert_after_index = idx+1; - break; - } - } - } - } - - XsModuleMenu { - id: module_menu_shim - root_menu_name: parent_item.root_menu_name - } - - Instantiator { - - model: module_menu_shim - - delegate: XsModuleMenuItem { - } - onObjectAdded: { - if (insert_after_index != -1) { - parent_menu.insertItem(insert_after_index, object) - } else { - parent_menu.addItem(object) - } - } - onObjectRemoved: parent_menu.removeItem(object) - - } - - Instantiator { - - model: module_menu_shim.num_submenus - - delegate: XsModuleSubMenu { - root_menu_name : module_menu_shim.submenu_names[index] - } - - // The trick is on those two lines - onObjectAdded: { - if (insert_after_index != -1) { - parent_menu.insertMenu(insert_after_index, object) - } else { - parent_menu.addMenu(object) - } - } - onObjectRemoved: { - parent_menu.removeMenu(object) - } - } -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/core/XsSortFilterModel.qml b/ui/qml/xstudio/base/core/XsSortFilterModel.qml deleted file mode 100644 index e73e76dba..000000000 --- a/ui/qml/xstudio/base/core/XsSortFilterModel.qml +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.9 -import QtQml.Models 2.14 - -import xStudio 1.0 - -DelegateModel { - id: delegateModel - - property var srcModel: null - property var lessThan: function(left, right) { return true; } - property var filterAcceptsItem: function(item) { return true; } - - onSrcModelChanged: model = srcModel - - signal updated() - - function update() { - hiddenItems.setGroups(0, hiddenItems.count, "unsorted") - items.setGroups(0, items.count, "unsorted") - } - - function insertPosition(lessThan, item) { - let lower = 0 - let upper = items.count - while (lower < upper) { - const middle = Math.floor(lower + (upper - lower) / 2) - const result = lessThan(item.model, - items.get(middle).model) - if (result) { - upper = middle - } else { - lower = middle + 1 - } - } - return lower - } - - function sort(lessThan) { - while (unsortedItems.count > 0) { - const item = unsortedItems.get(0) - - if(!filterAcceptsItem(item.model)) { - item.groups = "hidden" - } else { - const index = insertPosition(lessThan, item) - item.groups = "items" - items.move(item.itemsIndex, index) - } - } - } - - items.includeByDefault: false - groups: [ - DelegateModelGroup { - id: unsortedItems - name: "unsorted" - - includeByDefault: true - - onChanged: { - delegateModel.sort(delegateModel.lessThan) - updated() - } - }, - DelegateModelGroup { - id: hiddenItems - name: "hidden" - - includeByDefault: false - } - ] -} - - -// // SPDX-License-Identifier: Apache-2.0 -// import QtQuick 2.9 -// import QtQml.Models 2.14 - -// import xStudio 1.0 - -// DelegateModel { -// id: delegateModel - -// property var srcModel: null -// property var lessThan: function(left, right) { return true; } -// property var filterAcceptsItem: function(item) { return true; } - -// onSrcModelChanged: model = srcModel - -// signal updated() - -// function update() { -// if (items.count > 0) { -// items.setGroups(0, items.count, "items"); -// } - -// // Step 1: Filter items -// var ivisible = []; -// for (var i = 0; i < items.count; ++i) { -// var item = items.get(i); -// if (filterAcceptsItem(item.model)) { -// ivisible.push(item); -// } -// } - -// // Step 2: Sort the list of visible items -// ivisible.sort(function(a, b) { -// return lessThan(a.model, b.model) ? -1 : 1; -// }); - - -// // Step 3: Add all items to the visible group: -// for (i = 0; i < ivisible.length; ++i) { -// item = ivisible[i]; -// item.inIvisible = true; -// if (item.ivisibleIndex !== i) { -// visibleItems.move(item.ivisibleIndex, i, 1); -// } -// } -// updated() -// } - -// items.onChanged: update() -// onLessThanChanged: update() -// onFilterAcceptsItemChanged: update() - -// groups: DelegateModelGroup { -// id: visibleItems - -// name: "ivisible" -// includeByDefault: false -// } - -// filterOnGroup: "ivisible" -// } \ No newline at end of file diff --git a/ui/qml/xstudio/base/core/XsWindowStateSaver.qml b/ui/qml/xstudio/base/core/XsWindowStateSaver.qml deleted file mode 100644 index 33b52de39..000000000 --- a/ui/qml/xstudio/base/core/XsWindowStateSaver.qml +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.10 -import QtQuick.Window 2.10 -import QtQuick.Controls 2.3 -import Qt.labs.settings 1.0 - -import xStudio 1.0 - -import xstudio.qml.helpers 1.0 - -Item -{ - property Window windowObj - property string windowName: "" - property var override_pref_path: undefined - - property alias window_settings: window_settings - - XsModelNestedPropertyMap { - id: window_settings - index: app_window.globalStoreModel.search_recursive(override_pref_path ? override_pref_path : "/ui/qml/" + windowName + "_settings", "pathRole") - property alias properties: window_settings.values - } - - Component.onCompleted: - { - try { - windowObj.x = window_settings.properties.x - windowObj.y = window_settings.properties.y - - if(windowObj.centerOnOpen != undefined) - windowObj.centerOnOpen = false - - windowObj.width = window_settings.properties.width - windowObj.height = window_settings.properties.height - } catch(err) {} - //windowObj.visibility = window_settings.properties.visibility - } - - Connections - { - target: windowObj - function onXChanged(x) { saveSettingsTimer.restart() } - function onYChanged(y) { saveSettingsTimer.restart() } - function onWidthChanged(width) { saveSettingsTimer.restart() } - function onHeightChanged(height) { saveSettingsTimer.restart() } - function onVisibilityChanged(v) { saveSettingsTimer.restart() } - } - - Timer - { - id: saveSettingsTimer - interval: 1000 - repeat: false - onTriggered: saveSettings() - } - - function saveSettings() - { - switch(windowObj.visibility) - { - case ApplicationWindow.Windowed: - window_settings.properties.x = windowObj.x; - window_settings.properties.y = windowObj.y; - window_settings.properties.width = windowObj.width; - window_settings.properties.height = windowObj.height; - window_settings.properties.visibility = windowObj.visibility; - break; - case ApplicationWindow.FullScreen: - window_settings.properties.visibility = windowObj.visibility; - break; - case ApplicationWindow.Maximized: - window_settings.properties.visibility = windowObj.visibility; - break; - case ApplicationWindow.Hidden: - window_settings.properties.visibility = windowObj.visibility; - break - } - } -} diff --git a/ui/qml/xstudio/base/dialogs/XsButtonDialog.qml b/ui/qml/xstudio/base/dialogs/XsButtonDialog.qml deleted file mode 100644 index 17bf8664a..000000000 --- a/ui/qml/xstudio/base/dialogs/XsButtonDialog.qml +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtQuick.Layouts 1.3 - -import xStudio 1.0 - -XsDialogModal { - property alias text: text_control.text - property int rejectIndex: -1 - property int acceptIndex: buttonModel ? buttonModel.length-1 : -1 - property var buttonModel: ['Cancel', 'Maybe', 'Okay'] - - minimumHeight: 85 - minimumWidth: 300 - - keepCentered: true - centerOnOpen: true - - // height: (footer ? footer.height : 0) + layout.implicitHeight + (header? header.height : 0) - - signal selected(int button_index) - - ColumnLayout { - id: content - anchors.fill: parent - anchors.margins: 10 - spacing: 10 - - XsLabel { - Layout.fillWidth: true - Layout.fillHeight: visible ? true : false - Layout.minimumHeight: 20 - Layout.alignment: Qt.AlignVCenter|Qt.AlignHCenter - - id: text_control - font.pixelSize: XsStyle.popupControlFontSize * 1.3 - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - text: "" - visible: text ? true : false - } - - RowLayout { - Layout.minimumHeight: 25 - Layout.minimumWidth: 100 - // Layout.maximumHeight: 25 - Layout.fillWidth: true - Layout.fillHeight: false - Layout.alignment: Qt.AlignVBottom|Qt.AlignHCenter - - spacing:10 - - Keys.onReturnPressed: { - selected(acceptIndex) - accept() - } - Keys.onEscapePressed: { - selected(rejectIndex) - reject() - } - - Repeater { - model: buttonModel - delegate: XsRoundButton { - text: modelData - Layout.fillWidth: true - Layout.fillHeight: true - highlighted: index == acceptIndex - onClicked: { - selected(index) - if(index == rejectIndex) { - reject() - } else { - accept() - } - } - } - } - } - } -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/dialogs/XsComingSoonDialog.qml b/ui/qml/xstudio/base/dialogs/XsComingSoonDialog.qml deleted file mode 100644 index caef883c2..000000000 --- a/ui/qml/xstudio/base/dialogs/XsComingSoonDialog.qml +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Window 2.2 - -import xStudio 1.0 - -XsWindow { - property alias image: myImage.source - property alias icon: myIcon.source - property alias comment: myText.text - - keepCentered: true - centerOnOpen: true - - Component.onCompleted: { - if(myImage.sourceSize.height !== 0) { - myIconRect.visible = false - myText.visible = false - height = myImage.sourceSize.height - width = myImage.sourceSize.width - } else { - imagerect.visible = false - width = myText.width + 40 - } - } - - Rectangle { - id: imagerect - color: "transparent" - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - - Image { - id: myImage - anchors.fill: parent - } - } - - Rectangle { - id: myIconRect - anchors.top: parent.top - anchors.bottom: myText.top - anchors.topMargin: 20 - anchors.horizontalCenter: parent.horizontalCenter - color: "transparent" - - XsColoredImage { - anchors.fill:parent - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - id: myIcon - height: 140 - width: 140 - } - } - Text { - id: myText - color: XsStyle.mainColor - anchors.bottomMargin: 20 - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - } -} diff --git a/ui/qml/xstudio/base/dialogs/XsDialog.qml b/ui/qml/xstudio/base/dialogs/XsDialog.qml deleted file mode 100644 index 6ba43c509..000000000 --- a/ui/qml/xstudio/base/dialogs/XsDialog.qml +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQml 2.15 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Window 2.15 - -import xStudio 1.0 - -XsWindow { - asDialog: true - - Shortcut { - sequence: "Esc" - onActivated: reject() - } - - signal accepted() - signal rejected() - - function accept() { - accepted() - close() - } - - function reject() { - rejected() - close() - } - - function open() { - show() - } -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/dialogs/XsDialogModal.qml b/ui/qml/xstudio/base/dialogs/XsDialogModal.qml deleted file mode 100644 index 13c3d911b..000000000 --- a/ui/qml/xstudio/base/dialogs/XsDialogModal.qml +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQml 2.15 -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Window 2.15 - -import xStudio 1.0 - -XsDialog { - id: control - modality: Qt.WindowModal - - Connections { - target: control - function onVisibleChanged() { - app_window.dimmer = control.visible - } - } -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/dialogs/XsModuleAttributesDialog.qml b/ui/qml/xstudio/base/dialogs/XsModuleAttributesDialog.qml deleted file mode 100644 index 0693e0ebf..000000000 --- a/ui/qml/xstudio/base/dialogs/XsModuleAttributesDialog.qml +++ /dev/null @@ -1,194 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtQuick.Layouts 1.3 -import QtQuick.Window 2.2 -import Qt.labs.qmlmodels 1.0 - -import xStudio 1.1 -import xstudio.qml.module 1.0 -import xstudio.qml.models 1.0 - -XsWindow { - - id: dialog - width: 300 - height: r1.count*20 + 100 - property var attributesGroupNames - - centerOnOpen: true - - XsModuleAttributesModel { - id: attribute_set - attributesGroupNames: dialog.attributesGroupNames - } - - /*XsModuleData { - id: attribute_set - modelDataName: dialog.attributesGroupNames - }*/ - - RowLayout { - - anchors.fill: parent - anchors.margins: 10 - - ColumnLayout { - - Layout.margins: 5 - - Repeater { - - // N.B the 'combo_box_options' attribute is propagated via the - // XsToolBox which instantiates the BasicViewportMaskButton - model: attribute_set - y: 5 - id: r1 - - Rectangle { - - height: 20 - color: "transparent" - width: label_metrics.width - Layout.alignment: Qt.AlignRight - - Text { - id: thetext - anchors.fill: parent - text: title ? title : "" - color: XsStyle.controlColor - font.family: XsStyle.controlTitleFontFamily - font.pixelSize: XsStyle.popupControlFontSize - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - y: index*20 - } - - TextMetrics { - id: label_metrics - font: thetext.font - text: thetext.text - } - } - - } - } - - ColumnLayout { - - Layout.fillWidth: true - Layout.margins: 5 - - Repeater { - - model: attribute_set - y: 5 - delegate: chooser - - DelegateChooser { - id: chooser - role: "type" - - DelegateChoice { - roleValue: "FloatScrubber"; - XsFloatAttrSlider { - height: 20 - Layout.fillWidth: true - } - } - - DelegateChoice { - roleValue: "IntegerValue"; - XsIntAttrSlider { - height: 20 - Layout.fillWidth: true - } - } - - DelegateChoice { - roleValue: "ColourAttribute"; - XsColourChooser { - height: 20 - Layout.fillWidth: true - } - } - - - DelegateChoice { - roleValue: "OnOffToggle"; - Rectangle { - color: "transparent" - height: 20 - Layout.fillWidth: true - XsBoolAttrCheckBox { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.topMargin: 2 - anchors.bottomMargin: 2 - } - } - } - - DelegateChoice { - roleValue: "ComboBox"; - Rectangle { - height: 20 - Layout.fillWidth: true - color: "transparent" - XsComboBox { - model: combo_box_options - anchors.fill: parent - property var value_: value ? value : null - onValue_Changed: { - currentIndex = indexOfValue(value_) - } - Component.onCompleted: currentIndex = indexOfValue(value_) - onCurrentValueChanged: { - value = currentValue; - } - } - } - } - - } - - } - } - } - - RoundButton { - id: btnOK - text: qsTr("Close") - width: 60 - height: 24 - radius: 5 - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.margins: 5 - DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole - background: Rectangle { - radius: 5 -// color: XsStyle.highlightColor//mouseArea.containsMouse?:XsStyle.controlBackground - color: mouseArea.containsMouse?XsStyle.primaryColor:XsStyle.controlBackground - gradient:mouseArea.containsMouse?styleGradient.accent_gradient:Gradient.gradient - anchors.fill: parent - } - contentItem: Text { - text: btnOK.text - color: XsStyle.hoverColor//:XsStyle.mainColor - font.family: XsStyle.fontFamily - font.hintingPreference: Font.PreferNoHinting - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - MouseArea { - id: mouseArea - hoverEnabled: true - anchors.fill: btnOK - cursorShape: Qt.PointingHandCursor - onClicked: dialog.close() - } - } - -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/dialogs/XsStringRequestDialog.qml b/ui/qml/xstudio/base/dialogs/XsStringRequestDialog.qml deleted file mode 100644 index 473316982..000000000 --- a/ui/qml/xstudio/base/dialogs/XsStringRequestDialog.qml +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtQuick.Layouts 1.3 - -import xStudio 1.0 - -XsDialogModal { - id: control - - minimumHeight: 85 - minimumWidth: 300 - - keepCentered: true - centerOnOpen: true - - property alias okay_text: okay.text - property alias secondary_okay_text: secondary_okay.text - property alias cancel_text: cancel.text - property alias text: text_control.text - property alias echoMode: text_control.echoMode - property alias input: text_control - - signal cancelled() - signal okayed() - signal secondary_okayed() - - - function okaying() { - okayed() - accept() - } - function secondary_okaying() { - secondary_okayed() - accept() - } - function cancelling() { - cancelled() - reject() - } - - Connections { - target: control - function onVisibleChanged() { - if(visible){ - text_control.selectAll() - text_control.forceActiveFocus() - } - } - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - - TextField { - id: text_control - text: "" - - Layout.fillWidth: true - Layout.fillHeight: true - - selectByMouse: true - font.family: XsStyle.fontFamily - font.hintingPreference: Font.PreferNoHinting - font.pixelSize: XsStyle.sessionBarFontSize - color: XsStyle.hoverColor - selectionColor: XsStyle.highlightColor - onAccepted: okaying() - background: Rectangle { - anchors.fill: parent - color: XsStyle.popupBackground - radius: 5 - } - } - - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: false - Layout.topMargin: 10 - Layout.minimumHeight: 20 - - //focus: true - Keys.onReturnPressed: okayed() - Keys.onEscapePressed: cancelled() - - XsRoundButton { - id: cancel - text: qsTr("Cancel") - - Layout.fillWidth: true - Layout.fillHeight: true - Layout.minimumWidth: control.width / 5 - - onClicked: { - cancelling() - } - } - XsHSpacer{} - XsRoundButton { - id: secondary_okay - text: "" - - visible: text != "" - Layout.fillWidth: true - Layout.minimumWidth: control.width / 5 - Layout.fillHeight: true - - onClicked:{ - secondary_okaying() - } - } - XsHSpacer{} - XsRoundButton { - id: okay - text: "Okay" - highlighted: true - - Layout.minimumWidth: control.width / 5 - Layout.fillWidth: true - Layout.fillHeight: true - - onClicked: { - okaying() - } - } - } - } -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/dialogs/XsWindow.qml b/ui/qml/xstudio/base/dialogs/XsWindow.qml deleted file mode 100644 index d517fb83e..000000000 --- a/ui/qml/xstudio/base/dialogs/XsWindow.qml +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Window 2.15 -import QtQuick.Controls 2.15 - -import xStudio 1.0 - -ApplicationWindow { - id: control - property bool onTop: true - property bool frameLess: false - property bool asDialog: false - property bool asWindow: false - property var centerOn: app_window - property bool keepCentered: false - property bool centerOnOpen: false - - // override default palette - palette.base: XsStyle.controlBackground - palette.text: XsStyle.hoverColor - palette.button: XsStyle.controlTitleColor - palette.highlight: XsStyle.highlightColor - palette.light: XsStyle.highlightColor - palette.highlightedText: XsStyle.mainBackground - - palette.brightText: XsStyle.highlightColor - palette.buttonText: XsStyle.hoverColor - palette.windowText: XsStyle.hoverColor - - function toggle() { - if(visible) { - hide() - } else { - show() - } - } - - Connections { - target: control - function onVisibleChanged() { - if(!visible) { - app_window.requestActivate() - sessionWidget.playerWidget.forceActiveFocus() - } else { - control.requestActivate() - if(keepCentered || centerOnOpen) { - centerOnOpen = false - if(centerOn) { - let xx = centerOn.x - if(typeof centerOn.mapToGlobal !== "undefined"){ - xx = centerOn.mapToGlobal(0, 0).x - } - x = Math.max(xx, xx + (centerOn.width/2) - (width / 2)) - - let yy = centerOn.y - if(typeof centerOn.mapToGlobal !== "undefined"){ - yy = centerOn.mapToGlobal(0, 0).y - } - y = Math.max(yy, yy + (centerOn.height/2) - (height / 2)) - } else { - x = (Screen.width / 2) - (width / 2) - y = (Screen.height / 2) - (height / 2) - } - } - } - } - } - - flags: (asDialog ? Qt.Dialog : asWindow ? Qt.WindowSystemMenuHint : Qt.SubWindow) |(frameLess ? Qt.FramelessWindowHint : 0) | (onTop ? Qt.WindowStaysOnTopHint : 0) - color: "#222" -} diff --git a/ui/qml/xstudio/base/widgets/XsBoolAttrCheckBox.qml b/ui/qml/xstudio/base/widgets/XsBoolAttrCheckBox.qml deleted file mode 100644 index 4f513c32d..000000000 --- a/ui/qml/xstudio/base/widgets/XsBoolAttrCheckBox.qml +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Templates 2.12 as T -import QtQuick.Controls 2.12 -import QtQuick.Controls.impl 2.12 -import QtGraphicalEffects 1.12 - -import xStudio 1.0 - - // color: XsStyle.controlTitleColor - // font { - // pixelSize: XsStyle.popupControlFontSize - // family: XsStyle.controlContentFontFamily - // hintingPreference: Font.PreferNoHinting - // } - -Rectangle { - - id: menu_item - width: height - height: iconsize - color: mouseArea.containsMouse ? XsStyle.highlightColor : "transparent" - property string text - property bool checked: value ? value : false - property bool checkable: true - property int iconsize: XsStyle.menuItemHeight *.66 - gradient: mouseArea.containsMouse ? styleGradient.accent_gradient : null - z: 1000 - - - Rectangle { - - anchors.fill: parent - gradient: checked ? styleGradient.accent_gradient : null - color: checked ? XsStyle.highlightColor : "transparent" - border.width: 1 // mycheckableDecorated?1:0 - border.color: mouseArea.containsMouse ? XsStyle.hoverColor : XsStyle.mainColor - visible: checkable - - Image { - id: checkIcon - visible: menu_item.checked - sourceSize.height: XsStyle.menuItemHeight - sourceSize.width: XsStyle.menuItemHeight - source: "qrc:///feather_icons/check.svg" - width: iconsize // 2 - height: iconsize // 2 - anchors.centerIn: parent - } - - ColorOverlay{ - id: colorolay - anchors.fill: checkIcon - source:checkIcon - visible: menu_item.checked - color: XsStyle.hoverColor//menuItem.highlighted?XsStyle.hoverColor:XsStyle.menuBackground - antialiasing: true - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - console.log("clicked", value) - value = !value - } - } - - property string tooltip_text: "" - - ToolTip.delay: 500 - ToolTip.visible: mouseArea.containsMouse && tooltip_text != "" - ToolTip.text: tooltip_text - - -} diff --git a/ui/qml/xstudio/base/widgets/XsBorder.qml b/ui/qml/xstudio/base/widgets/XsBorder.qml deleted file mode 100644 index 562dda692..000000000 --- a/ui/qml/xstudio/base/widgets/XsBorder.qml +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Shapes 1.12 - -Shape { - id: control - property color color: "black" - property real thickness: 1.0 - - property bool topBorder: true - property bool bottomBorder: true - property bool leftBorder: true - property bool rightBorder: true - - readonly property real halfThick: thickness / 2 - - - ShapePath { - strokeWidth: control.thickness - strokeColor: topBorder ? control.color : "transparent" - fillColor: "transparent" - - startX: halfThick - startY: halfThick - - PathLine {x: control.width - 1 - halfThick ; y: halfThick } - } - - ShapePath { - strokeWidth: control.thickness - strokeColor: bottomBorder ? control.color : "transparent" - fillColor: "transparent" - - startX: halfThick - startY: control.height-1 - halfThick - - PathLine {x: control.width-1-halfThick; y: control.height-1-halfThick} - } - - ShapePath { - strokeWidth: control.thickness - strokeColor: leftBorder ? control.color : "transparent" - fillColor: "transparent" - - startX: halfThick - startY: halfThick - - PathLine {x: halfThick; y: control.height-1-halfThick} - } - - ShapePath { - strokeWidth: control.thickness - strokeColor: rightBorder ? control.color : "transparent" - fillColor: "transparent" - - startX: control.width-1-halfThick - startY: halfThick - - PathLine {x: control.width-1-halfThick; y: control.height-1-halfThick} - } - -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/widgets/XsButton.qml b/ui/qml/xstudio/base/widgets/XsButton.qml deleted file mode 100644 index 1cbb5d60e..000000000 --- a/ui/qml/xstudio/base/widgets/XsButton.qml +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.12 - -import xStudio 1.0 - -Button { - property var tooltip: "" - property var tooltipTitle: "" - implicitHeight: control.implicitHeight - implicitWidth: control.implicitWidth - hoverEnabled: true - - onHoveredChanged: { - if(hovered) { - status_bar.normalMessage(tooltip, tooltipTitle) - } else { - status_bar.clearMessage() - } - } - - background: Rectangle { - color: hovered ? palette.highlight : (highlighted ? palette.highlight : palette.button) - gradient: hovered ? styleGradient.accent_gradient : Gradient.gradient - anchors.fill: parent - } - - contentItem: Label { - id: control - anchors.centerIn: parent - text: parent.text - color: palette.text - font.family: XsStyle.fontFamily - font.pixelSize: 12 - font.hintingPreference: Font.PreferNoHinting - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } -} diff --git a/ui/qml/xstudio/base/widgets/XsButtonNew.qml b/ui/qml/xstudio/base/widgets/XsButtonNew.qml deleted file mode 100644 index 844761b64..000000000 --- a/ui/qml/xstudio/base/widgets/XsButtonNew.qml +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.15 //for ColorOverlay - -import xStudio 1.1 - -Button { - id: widget - - font.pixelSize: XsStyle.menuFontSize - font.family: XsStyle.fontFamily - - property alias bgDiv: bgDiv - property alias image: image - property color bgColorPressed: palette.highlight - property color bgColorNormal: palette.base - property color borderColorNormal: palette.base - property real borderWidth: 1 - property real borderRadius: 2 - - property color textColorPressed: palette.text - property color textColorNormal: "light grey" - property var textElide: textDiv.elide - property alias textDiv: textDiv - - property string imgSrc: "" - - property bool isActive: false - property bool subtleActive: false - - property var tooltip: "" - property var tooltipTitle: "" - hoverEnabled: true - - onHoveredChanged: { - if(hovered) { - status_bar.normalMessage(tooltip, tooltipTitle) - } else { - status_bar.clearMessage() - } - } - - contentItem: - Item{ - anchors.fill: parent - opacity: enabled ? 1.0: bgColorNormal!=palette.base?1: 0.3 - Image { - id: image - visible: imgSrc!="" - source: imgSrc - sourceSize.height: parent.height/1.5 - sourceSize.width: parent.width/1.5 - horizontalAlignment: Image.AlignHCenter - verticalAlignment: Image.AlignVCenter - anchors.centerIn: parent - smooth: true - antialiasing: true - layer { - enabled: true - effect: - ColorOverlay { - color: widget.down || widget.hovered? ((subtleActive && isActive) ? textColorNormal : textColorPressed) : (subtleActive && isActive) ? bgColorPressed : textColorNormal - } - } - } - Text { - id: textDiv - text: widget.text - font: widget.font - color: widget.down || widget.hovered? textColorPressed: textColorNormal - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - topPadding: 1 - anchors.horizontalCenter: parent.horizontalCenter - elide: Text.ElideRight - width: parent.width - height: parent.height - } - } - - ToolTip.text: widget.text - ToolTip.visible: widget.hovered && textDiv.truncated - - background: - Rectangle { - id: bgDiv - implicitWidth: 100 - implicitHeight: 40 - // opacity: enabled ? 1: 0.3 - color: enabled && widget.hoverEnabled? widget.down || (isActive && !subtleActive)? bgColorPressed: bgColorNormal : Qt.darker(bgColorNormal,1.5) - border.color: widget.down || widget.hovered ? bgColorPressed: borderColorNormal - border.width: borderWidth - radius: borderRadius - } - - onPressed: focus = true - onReleased: focus = false -} - diff --git a/ui/qml/xstudio/base/widgets/XsCheckBoxNew.qml b/ui/qml/xstudio/base/widgets/XsCheckBoxNew.qml deleted file mode 100644 index 211818bdf..000000000 --- a/ui/qml/xstudio/base/widgets/XsCheckBoxNew.qml +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtGraphicalEffects 1.12 - -import xStudio 1.1 - -CheckBox { id: widget - - property var indicatorType: "tick" //"box" - property alias imgIndicator: imgIndicator - property alias indicatorItem: indicatorItem - property alias textItem: textItem - - property color bgColorChecked: palette.highlight - property color bgColorNormal: palette.base - - property color indicatorColor: palette.text - property color textColorChecked: palette.text - property color textColorNormal: "light grey" - - property color borderColor: palette.base - property real borderWidth: 1 - - property bool forcedTextHover: false - property bool forcedHover: false - - font.pixelSize: XsStyle.menuFontSize - font.family: XsStyle.fontFamily - checked: false - activeFocusOnTab: true - - - indicator: - Rectangle { id: indicatorItem - implicitWidth: 22 - implicitHeight: 22 - x: widget.leftPadding - y: widget.height/2 - height/2 - radius: 3 //indicatorType=="box" ? 3: implicitHeight/2 - color: indicatorType=="box" ?bgColorNormal: widget.pressed? bgColorChecked: bgColorNormal - - border.color: widget.hovered || forcedHover? bgColorChecked: borderColor - border.width: borderWidth - - Rectangle { - width: parent.width/2 - height: width - anchors.centerIn: parent - radius: 2 - color: widget.hovered || forcedHover? indicatorColor: Qt.darker(indicatorColor, 1.2) - visible: widget.checked && indicatorType=="box" - } - Image { id: imgIndicator - visible: widget.checked && indicatorType=="tick" - source: "qrc:///feather_icons/check.svg" - sourceSize.height: widget.indicator.width*0.8 - sourceSize.width: widget.indicator.height*0.8 - anchors.centerIn: parent - layer { - textureSize: Qt.size(imgIndicator.width, imgIndicator.height) - enabled: true - effect: - ColorOverlay { - anchors.fill: imgIndicator; color: widget.hovered || forcedHover? indicatorColor: Qt.darker(indicatorColor, 1.2) - } - } - } - } - // layer { - // enabled: true - // effect: - // ColorOverlay { - // color: widget.hovered? indicatorColor: Qt.darker(indicatorColor, 1.2) - - contentItem: - Text { id: textItem - text: widget.text - font: widget.font - opacity: enabled ? 1.0: 0.3 - color: widget.hovered || forcedTextHover || forcedHover? textColorChecked: textColorNormal - verticalAlignment: Text.AlignVCenter - leftPadding: widget.indicator.width + widget.spacing - elide: Text.ElideRight - - XsToolTip{ - x: indicatorItem.width+2 - text: parent.text - visible: (widget.hovered) && parent.truncated - width: textItem.contentWidth == 0? 0 : parent.width-indicatorItem.width - } - } - - onCheckedChanged: focus=!focus -} diff --git a/ui/qml/xstudio/base/widgets/XsCheckBoxWithComboBox.qml b/ui/qml/xstudio/base/widgets/XsCheckBoxWithComboBox.qml deleted file mode 100644 index 104ffae15..000000000 --- a/ui/qml/xstudio/base/widgets/XsCheckBoxWithComboBox.qml +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.15 - -import xStudio 1.1 - -Control { - id: widget - - property alias text: checkBox.text - property color textColorPressed: palette.text - property color textColorNormal: "light grey" - - // property alias model: editable? activeComboBox.model: comboBox.model - // property alias activeComboBox: editable? editableComboBox: comboBox - // property alias editable: editableComboBox.editable - property alias model: editableComboBox.model - property alias textRole: editableComboBox.textRole - property alias editable: editableComboBox.editable - property alias checked: checkBox.checked - property alias currentIndex: editableComboBox.currentIndex - - XsCheckbox{ id: checkBox - forcedTextHover: parent.hovered - // width: 80 - height: parent.height - enabled: widget.enabled - } - - // XsComboBox{ id: comboBox - // visible: !editable - // height: parent.height - // anchors.left: checkBox.right - // anchors.right: parent.right - // anchors.rightMargin: 12 - // } - - XsComboBox{ id: editableComboBox - enabled: checkBox.checked - height: parent.height - anchors.left: checkBox.right - anchors.right: parent.right - } - -} - - - diff --git a/ui/qml/xstudio/base/widgets/XsCheckBoxWithMultiComboBox.qml b/ui/qml/xstudio/base/widgets/XsCheckBoxWithMultiComboBox.qml deleted file mode 100644 index 0a69eba8a..000000000 --- a/ui/qml/xstudio/base/widgets/XsCheckBoxWithMultiComboBox.qml +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.15 - -import xStudio 1.1 - -Control { - id: widget - - property alias text: checkBox.text - property color textColorPressed: palette.text - property color textColorNormal: "light grey" - - property alias model: multiComboBox.valuesModel - property alias hint: multiComboBox.hint - property alias checked: checkBox.checked - property alias popup: multiComboBox.popup - property alias checkedIndexes: multiComboBox.checkedIndexes - - signal hide() - onHide:{ - multiComboBox.close() - } - - XsCheckbox{ id: checkBox - forcedTextHover: parent.hovered - // width: 80 - height: parent.height - enabled: widget.enabled - onCheckedChanged:{ - hide() - } - } - - XsComboBoxMultiSelect{ id: multiComboBox - enabled: checkBox.checked - forcedBg: checkBox.checked - height: parent.height - anchors.left: checkBox.right - anchors.right: parent.right - hint: "multi-input" - anchors.verticalCenter: parent.verticalCenter - width: parent.width - // height: itemHeight - } -} - - - diff --git a/ui/qml/xstudio/base/widgets/XsCheckBoxWithTextField.qml b/ui/qml/xstudio/base/widgets/XsCheckBoxWithTextField.qml deleted file mode 100644 index b1b13e1cd..000000000 --- a/ui/qml/xstudio/base/widgets/XsCheckBoxWithTextField.qml +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 - -import xStudio 1.1 - -Control { - id: widget - - property alias text: checkBox.text - property color textColorPressed: palette.text - property color textColorNormal: "light grey" - - property alias model: textField.text - property alias placeholderText: textField.placeholderText - property alias inputMethodHints: textField.inputMethodHints - - XsCheckbox{ id: checkBox - forcedTextHover: parent.hovered - width: 100 - height: parent.height - leftPadding: 12 - } - - XsTextField{id: textField - height: parent.height - anchors.left: checkBox.right - anchors.right: parent.right - anchors.rightMargin: 12 - } - -} - - - diff --git a/ui/qml/xstudio/base/widgets/XsCheckbox.qml b/ui/qml/xstudio/base/widgets/XsCheckbox.qml deleted file mode 100644 index 1f6f046d2..000000000 --- a/ui/qml/xstudio/base/widgets/XsCheckbox.qml +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Templates 2.12 as T -import QtQuick.Controls 2.12 -import QtQuick.Controls.impl 2.12 -import QtGraphicalEffects 1.12 - -import xStudio 1.0 - - // color: XsStyle.controlTitleColor - // font { - // pixelSize: XsStyle.popupControlFontSize - // family: XsStyle.controlContentFontFamily - // hintingPreference: Font.PreferNoHinting - // } - -Rectangle { - - id: menu_item - width: iconsize - height: iconsize - color: mouseArea.containsMouse ? XsStyle.highlightColor : "transparent" - property string text - property bool checked: false - property bool checkable: true - property int iconsize: XsStyle.menuItemHeight *.66 - gradient: mouseArea.containsMouse ? styleGradient.accent_gradient : null - z: 1000 - signal triggered - - Rectangle { - - anchors.fill: parent - gradient: checked ? styleGradient.accent_gradient : null - color: checked ? XsStyle.highlightColor : "transparent" - border.width: 1 // mycheckableDecorated?1:0 - border.color: mouseArea.containsMouse ? XsStyle.hoverColor : XsStyle.mainColor - visible: checkable - - Image { - id: checkIcon - visible: menu_item.checked - sourceSize.height: XsStyle.menuItemHeight - sourceSize.width: XsStyle.menuItemHeight - source: "qrc:///feather_icons/check.svg" - width: iconsize // 2 - height: iconsize // 2 - anchors.centerIn: parent - } - - ColorOverlay{ - id: colorolay - anchors.fill: checkIcon - source:checkIcon - visible: menu_item.checked - color: XsStyle.hoverColor//menuItem.highlighted?XsStyle.hoverColor:XsStyle.menuBackground - antialiasing: true - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - menu_item.triggered() - } - } -} diff --git a/ui/qml/xstudio/base/widgets/XsCollapsibleLabel.qml b/ui/qml/xstudio/base/widgets/XsCollapsibleLabel.qml deleted file mode 100644 index c7226b903..000000000 --- a/ui/qml/xstudio/base/widgets/XsCollapsibleLabel.qml +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.3 -import QtQuick 2.14 -import QtGraphicalEffects 1.12 -import QtQml 2.14 - -import xStudio 1.0 - -RowLayout { - id: collapsed_widget - property alias collapsed: expand_button.collapsed - property string text: "" - - XsExpandButton { - Layout.alignment: Qt.AlignVCenter|Qt.AlignHCenter - width: 32 - height: width - id: expand_button - } - - XsLabel { - id: label - font.pixelSize: XsStyle.popupControlFontSize - verticalAlignment: Text.AlignVCenter - color: "white" - text: collapsed_widget.text - } -} diff --git a/ui/qml/xstudio/base/widgets/XsColoredImage.qml b/ui/qml/xstudio/base/widgets/XsColoredImage.qml deleted file mode 100644 index f86ae5101..000000000 --- a/ui/qml/xstudio/base/widgets/XsColoredImage.qml +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtGraphicalEffects 1.12 - -import xStudio 1.0 - -Item { - property alias source: myIcon.source - property alias iconColor: myIconOverlay.color - property alias sourceSize: myIcon.sourceSize - property alias myIcon:myIcon - - Image { - id: myIcon - // visible: playerWidget.controlsVisible - sourceSize.height: parent.height - sourceSize.width: parent.width - width: parent.height - height: parent.height - antialiasing: true - anchors.centerIn: parent - } - ColorOverlay{ - id: myIconOverlay - // visible: playerWidget.controlsVisible - anchors.fill: myIcon - source:myIcon - color: XsStyle.hoverColor - antialiasing: true - } -} diff --git a/ui/qml/xstudio/base/widgets/XsColourChooser.qml b/ui/qml/xstudio/base/widgets/XsColourChooser.qml deleted file mode 100644 index 03a50d4bc..000000000 --- a/ui/qml/xstudio/base/widgets/XsColourChooser.qml +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtQuick.Layouts 1.3 -import QtQuick.Window 2.2 -import QtQuick.Dialogs 1.0 - -import xStudio 1.0 -import xstudio.qml.module 1.0 - -Rectangle { - - id: control - property var value_: value ? value : 0 - color: "transparent" - - Rectangle { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left - - width: height*1.66 - color: value - - border.color: ma.containsMouse ? XsStyle.hoverColor : XsStyle.mainColor - - MouseArea { - id: ma - anchors.fill: parent - hoverEnabled: true - onClicked: { - colorDialog.color = value - colorDialog.open() - } - } - - } - - ColorDialog { - id: colorDialog - title: "Choose a colour" - onAccepted: value = colorDialog.color - } - -} diff --git a/ui/qml/xstudio/base/widgets/XsComboBoxMultiSelect.qml b/ui/qml/xstudio/base/widgets/XsComboBoxMultiSelect.qml deleted file mode 100644 index 67919fd2d..000000000 --- a/ui/qml/xstudio/base/widgets/XsComboBoxMultiSelect.qml +++ /dev/null @@ -1,319 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Window 2.15 -import QtQuick.Controls 2.15 -import QtQml.Models 2.14 - -import xStudio 1.1 - -Item{ id: widget - - property color bgColorEditable: "light grey" - property color bgColorActive: palette.highlight - property color bgColorNormal: palette.base - - property color textColorActive: palette.text - property color textColorNormal: "light grey" - property color textColorDisabled: Qt.darker(textColorNormal, 1.6) - property color indicatorColorNormal: "light grey" - - property color borderColor: XsStyle.menuBorderColor - property real borderWidth: 1 - property real borderRadius: 6 - property real framePadding: 6 - property real itemSpacing: 1 - - property real fontSize: XsStyle.menuFontSize - property int displayItemCount: 10 - - property alias forcedBg: searchTextField.forcedBg - property string hint: "multi-select" - - property bool isFiltered: false - onIsFilteredChanged: { - if(isFiltered) { - valuesModel.selectionFilter = sourceSelectionModel.selection - } else { - valuesModel.selectionFilter = empty.selection - } - } - property var valuesModel - - property int valuesCount: valuesModel ? valuesModel.length: 0 - onValuesCountChanged:{ - valuesPopup.currentIndex=-1 - } - property int checkedCount: sourceSelectionModel.selectedIndexes.length - property alias checkedIndexes: sourceSelectionModel.selectedIndexes - property alias theSelection: sourceSelectionModel.selection - - property alias popup: valuesPopup - signal close() - signal clear() - - Connections { - target: valuesModel ? valuesModel.sourceModel : null - function onModelReset() { - clear() - checkedCount = sourceSelectionModel.selectedIndexes.length - } - } - - - ItemSelectionModel { - id: sourceSelectionModel - model: valuesModel ? valuesModel.sourceModel : null - onSelectionChanged: { - // this is mank.. - checkedCount = sourceSelectionModel.selectedIndexes.length - if(isFiltered) { - valuesModel.selectionFilter = sourceSelectionModel.selection - } - } - } - ItemSelectionModel { - id: empty - model: valuesModel - } - - // width: parent.width - // height: itemHeight - - Rectangle{ id: searchField - width: parent.width - height: parent.height - color: "transparent" - - border.color: bgColorNormal - border.width: borderWidth - - XsTextField { id: searchTextField - width: parent.width - height: widget.height - font.pixelSize: fontSize*1.2 - placeholderText: hint - forcedHover: arrowButton.hovered - - onAccepted: { - focus = false - if(valuesModel.length>0) - { - if(valuesPopup.currentIndex==-1) valuesPopup.currentIndex=0 - valuesPopup.focus = true - } - } - onTextEdited: { - valuesModel.setFilterWildcard(text) - valuesPopup.visible = true - searchTextField.focus = true - } - - onFocusChanged: { - if(focus) - { - // valuesPopup.currentIndex=-1 - if(isFiltered) countDisplay.isCountBtnClicked = true - else arrowButton.isArrowBtnClicked = true - valuesPopup.visible = true - searchTextField.focus = true - } - } - Keys.onPressed: (event)=> { - if (event.key == Qt.Key_Down) { - if(valuesModel.length>0) { - if(valuesPopup.currentIndex===-1) valuesPopup.currentIndex=0 - else if(valuesPopup.currentIndex!==-1) valuesPopup.currentIndex+=1 - valuesPopup.focus = true - } - } - else if (event.key == Qt.Key_Up) { - if(valuesModel.length>0) { - if(valuesPopup.currentIndex!==-1) { - valuesPopup.currentIndex-=1 - valuesPopup.focus = true - } - } - } - } - - } - XsButton{ id: arrowButton - property bool isArrowBtnClicked: false - text: "" - imgSrc: isActive?"qrc:/feather_icons/chevron-up.svg": "qrc:/feather_icons/chevron-down.svg" - width: height - height: widget.height - framePadding - anchors.verticalCenter: parent.verticalCenter - anchors.right: searchTextField.right - anchors.rightMargin: framePadding/2 - borderColorNormal: Qt.lighter(bgColorNormal, 0.3) - isActive: arrowButton.isArrowBtnClicked //valuesPopup.visible - - onClicked: { - isFiltered = false - - if(arrowButton.isArrowBtnClicked){ - valuesPopup.visible = false - arrowButton.isArrowBtnClicked = false - } - else{ - valuesPopup.visible = true - arrowButton.isArrowBtnClicked = true - } - - countDisplay.isCountBtnClicked = false - } - } - XsButton{ id: clearButton - text: "" - imgSrc: "qrc:/feather_icons/x.svg" - visible: searchTextField.length!=0 - width: height - height: widget.height - framePadding - anchors.verticalCenter: parent.verticalCenter - anchors.right: checkedCount>0 && countDisplay.visible? countDisplay.left: arrowButton.left - anchors.rightMargin: framePadding/2 - borderColorNormal: Qt.lighter(bgColorNormal, 0.3) - onClicked: { - searchTextField.clear() - searchTextField.textEdited() - } - } - XsButton{ id: countDisplay - property bool isCountBtnClicked: false - text: checkedCount - font.pixelSize: text.length==1? 10 : 9 - visible: checkedCount>0 - width: height - height: widget.height - framePadding*1.10 - borderRadius: width/1.2 - isActive: isCountBtnClicked //isFiltered - textColorNormal: isActive? "light grey": palette.highlight - borderColorNormal: isActive || hovered || !enabled? palette.base : palette.highlight - // opacity: 1:0.7 - anchors.verticalCenter: parent.verticalCenter - anchors.right: arrowButton.left - anchors.rightMargin: framePadding - onClicked: { - - if(countDisplay.isCountBtnClicked){ - valuesPopup.visible = false - isFiltered = true - - countDisplay.isCountBtnClicked = false - } - else{ - isFiltered = true - valuesPopup.visible = true - - countDisplay.isCountBtnClicked = true - } - - arrowButton.isArrowBtnClicked = false - } - } - } - - Rectangle{ - anchors.fill: valuesPopup; color: Qt.darker(bgColorNormal, 1.5); visible: valuesPopup.visible - } - ListView{ id: valuesPopup - z: 10 - property real valuesItemHeight: widget.height/1.3 - model: valuesModel - - Rectangle{ anchors.fill: parent; color: "transparent"; - border.color: Qt.lighter(bgColorNormal, 1.3); border.width: borderWidth - } - - visible: false - onVisibleChanged: { - if(visible) focus=true - } - clip: true - orientation: ListView.Vertical - snapMode: ListView.SnapToItem - currentIndex: -1 - - x: searchField.x - y: searchField.y + searchField.height - width: searchTextField.width - height: valuesCount>=displayItemCount? valuesItemHeight*displayItemCount: valuesItemHeight*valuesCount - ScrollBar.vertical: XsScrollBar { id: valuesScrollBar; padding: 2} - onContentHeightChanged: { - if(height < contentHeight) valuesScrollBar.policy = ScrollBar.AlwaysOn - else { - valuesScrollBar.policy = ScrollBar.AsNeeded - } - } - - Keys.onUpPressed: { - if(currentIndex!=-1) currentIndex-=1 - - if(currentIndex==-1) searchTextField.focus=true - } - Keys.onDownPressed: { - if(currentIndex!=model.length-1) currentIndex+=1 - } - Keys.onPressed: (event)=> { - if (event.key == Qt.Key_Enter || event.key == Qt.Key_Return) { - updateField(currentIndex) - } - } - - delegate: - Rectangle{ - width: valuesPopup.width - height: valuesPopup.valuesItemHeight - color: checkBox.checked ? Qt.darker(bgColorNormal, 1.1): Qt.darker(bgColorNormal, 1.5) - - XsCheckbox{ id: checkBox - text: nameRole - width: parent.width - height: parent.height - anchors.verticalCenter: parent.verticalCenter - checked: sourceSelectionModel.selectedIndexes.includes(valuesModel.mapToSource(valuesModel.index(index, 0))) - onClicked: updateField(index) - - onHoveredChanged:{ - if(hovered) valuesPopup.currentIndex=index - } - - forcedHover: valuesPopup.currentIndex==index - indicatorItem.implicitWidth: parent.height/1.2 - indicatorItem.implicitHeight: parent.height/1.2 - } - } - } - - function updateField(index) - { - valuesPopup.currentIndex=index - - if(index!==-1) sourceSelectionModel.select(valuesModel.mapToSource(valuesModel.index(index, 0)), ItemSelectionModel.Toggle) - if(!checkedCount) { - countDisplay.isCountBtnClicked = false - arrowButton.isArrowBtnClicked = true - isFiltered=false - } - } - - onClose: { - valuesPopup.visible = false - searchTextField.focus = false - - countDisplay.isCountBtnClicked = false - arrowButton.isArrowBtnClicked = false - isFiltered = false - } - - onClear: { - sourceSelectionModel.clearSelection() - - valuesPopup.visible = false - - searchTextField.clear() - } - -} - diff --git a/ui/qml/xstudio/base/widgets/XsComboBoxNew.qml b/ui/qml/xstudio/base/widgets/XsComboBoxNew.qml deleted file mode 100644 index 911b1e409..000000000 --- a/ui/qml/xstudio/base/widgets/XsComboBoxNew.qml +++ /dev/null @@ -1,247 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Window 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Templates 2.15 as T - -import xStudio 1.1 - -T.ComboBox { id: widget - - property color bgColorEditable: "light grey" - property color bgColorActive: palette.highlight - property color bgColorNormal: palette.base - - property color textColorActive: palette.text - property color textColorNormal: "light grey" - property color textColorDisabled: Qt.darker(textColorNormal, 1.6) - property color indicatorColorNormal: "light grey" - - property color borderColor: XsStyle.menuBorderColor - property real borderWidth: 1 - property real borderRadius: 6 - property real framePadding: 6 - - property real fontSize: XsStyle.menuFontSize - property int displayItemCount: 10 - property bool wasEditable: false - property alias popupOptions: popupOptions - property bool downArrowVisible: true - - property string tooltip_text: "" - - ToolTip.delay: 500 - ToolTip.visible: hovered && tooltip_text != "" - ToolTip.text: tooltip_text - - onHoveredChanged: { - downArrow.requestPaint() - } - - rightPadding: editable? (padding + indicator.width*2 + spacing): (padding) - - focusPolicy: Qt.StrongFocus - activeFocusOnTab: true - wheelEnabled: false - selectTextByMouse: true - - background: - Rectangle{ - implicitWidth: 100 - implicitHeight: 22 - radius: borderRadius - color: widget.popup.opened ? Qt.darker(bgColorActive, 2.75): bgColorNormal - border.color: widget.hovered || widget.popup.opened? bgColorActive: bgColorNormal - border.width: widget.pressed? 2: 1 - } - - contentItem: - T.TextField{ id: textField - // rightPadding: indicator.width - // text: widget.editable ? widget.editText: widget.displayText - text: activeFocus && widget.editable ? widget.displayText: textMetrics.elidedText - - onFocusChanged: { - if(focus) { - selectAll() - forceActiveFocus() - } - else{ - deselect() - } - } - onReleased: focus = false - onAccepted: focus = false - - enabled: widget.editable - autoScroll: widget.editable - readOnly: widget.down - inputMethodHints: widget.inputMethodHints - validator: widget.validator - selectByMouse: widget.selectTextByMouse - - selectionColor: widget.palette.highlight - selectedTextColor: textColorActive //widget.palette.highlightedText - - font.pixelSize: fontSize - font.family: XsStyle.fontFamily - color: widget.enabled? widget.hovered || widget.activeFocus? textColorActive: textColorNormal: textColorDisabled - width: widget.width //- indicatorMArea.width - horizontalAlignment: Text.AlignHCenter - height: widget.height - verticalAlignment: Text.AlignVCenter - topPadding: (widget.height-textMetrics.height)/4 - - TextMetrics { id: textMetrics - font: textField.font - text: widget.displayText //widget.displayText - elideWidth: textField.width - 8 - elide: Text.ElideRight - } - - background: - Rectangle{ - radius: borderRadius - color: widget.editable ? widget.popup.opened || widget.activeFocus? Qt.darker(bgColorActive, 1.5): widget.hovered?Qt.darker(bgColorEditable, 1.2):Qt.darker(bgColorEditable, 1.5): "transparent"; - opacity: widget.enabled? 0.7 : 0.3 - // width: widget.width - // height: widget.height - scale: 0.99 - z:-1 - } - - } - - indicator: - Canvas { - id: downArrow - x: editable? (widget.width - width - widget.rightPadding/3): (widget.width - width*1.5 - widget.rightPadding/3) - y: widget.topPadding + (widget.availableHeight - height) / 2 - z: 10 - width: 12 - height: 8 - contextType: "2d" - opacity: widget.enabled ? 1: 0.3 - visible: downArrowVisible - - Connections { - target: widget - function onPressedChanged() - { - downArrow.requestPaint(); - } - } - - onPaint: { - if(context !== undefined) { - context.reset(); - context.moveTo(0, 0); - context.lineTo(width, 0); - context.lineTo(width / 2, height); - context.closePath(); - context.lineWidth= borderWidth - context.strokeStyle = (!editable && widget.hovered) || (editable && indicatorMArea.containsMouse) || widget.popup.opened? bgColorActive: indicatorColorNormal - context.stroke(); - } - } - - MouseArea{ id: indicatorMArea - anchors.centerIn: parent - - width: widget.width - downArrow.x +4 - height: widget.availableHeight - hoverEnabled: true - propagateComposedEvents: true - onHoveredChanged: { - downArrow.requestPaint() - } - onClicked: { - if(popupOptions.opened) popupOptions.close() - else popupOptions.open() - } - } - } - - popup: - T.Popup{ - id: popupOptions - width: widget.width - height: contentItem.implicitHeight - y: widget.height - padding: 0 - - contentItem: - ListView { id: listView - clip: true - implicitHeight: contentHeight>widget.height*displayItemCount? widget.height*displayItemCount: contentHeight - model: widget.popup.visible ? widget.delegateModel: null - currentIndex: widget.highlightedIndex - snapMode: ListView.SnapToItem - - ScrollBar.vertical: - XsScrollBar {id: control - padding: 2 - policy: listView.model && (listView.height< (widget.height*listView.model.count))? ScrollBar.AlwaysOn: ScrollBar.AlwaysOff - } - } - - background: - Rectangle{ - radius: borderRadius - color: bgColorNormal - border.color: borderColor - border.width: 1 - } - onOpened: { - widget.wasEditable = widget.editable - widget.editable = false - forceActiveFocus() - focus = true - downArrow.requestPaint() - } - onClosed: { - widget.focus = false - focus = false - downArrow.requestPaint() - widget.editable = widget.wasEditable - // ensure active focus is passed back to viewport - sessionWidget.playerWidget.viewport.forceActiveFocus() - } - } - - delegate: //Popup-ListView's delegate - ItemDelegate { - id: itemDelegate - width: widget.width - height: widget.height - padding: 0 - - contentItem: - Text{ - text: widget.textRole ? (Array.isArray(widget.model) ? modelData[widget.textRole]: model[widget.textRole]): modelData - font.pixelSize: fontSize - font.family: XsStyle.fontFamily - font.weight: widget.currentIndex === index? Font.ExtraBold: Font.Normal - color: widget.highlightedIndex === index || widget.currentIndex === index? textColorActive: textColorNormal - elide: Text.ElideRight - width: widget.width - // height: widget.height - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - highlighted: widget.highlightedIndex === index - hoverEnabled: widget.hoverEnabled - palette.text: widget.palette.text - palette.highlightedText: widget.palette.highlightedText - - background: - Rectangle{ - radius: borderRadius - color: widget.highlightedIndex === index? bgColorActive: "transparent" - width: widget.width - height: widget.height - } - } - -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/widgets/XsComboBoxWithText.qml b/ui/qml/xstudio/base/widgets/XsComboBoxWithText.qml deleted file mode 100644 index b1bcb2cb8..000000000 --- a/ui/qml/xstudio/base/widgets/XsComboBoxWithText.qml +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.15 - -import xStudio 1.1 - -Control { - id: widget - - property alias text: textHolder.text - property color textColorPressed: palette.text - property color textColorNormal: "light grey" - - property alias model: comboBox.model - property alias textRole: comboBox.textRole - property alias editable: comboBox.editable - property alias currentIndex: comboBox.currentIndex - property real comboBoxLeftPadding: 6 //similar to checkbox - - Text{ id: textHolder - text: "" - font.pixelSize: XsStyle.menuFontSize - font.family: XsStyle.fontFamily - color: widget.hovered? textColorPressed: textColorNormal - width: parent.width - height: parent.height/2 - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - } - - XsComboBox{ id: comboBox - enabled: parent.enabled - height: parent.height/2 - anchors.left: parent.left - anchors.leftMargin: comboBoxLeftPadding - anchors.right: parent.right - anchors.top: textHolder.bottom - } -} - - - diff --git a/ui/qml/xstudio/base/widgets/XsDecoratorWidget.qml b/ui/qml/xstudio/base/widgets/XsDecoratorWidget.qml deleted file mode 100644 index 5109eee59..000000000 --- a/ui/qml/xstudio/base/widgets/XsDecoratorWidget.qml +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 2.3 -import QtQuick 2.14 -import xStudio 1.0 - -Item { - id: container - - property string widgetString - property var widget_ui - // implicitHeight: widget_ui.height - - function loadUI() { - widget_ui = Qt.createQmlObject(widgetString, - container, - "test"); - // widget_ui.implicitWidth = container.width - } - - Component.onCompleted: loadUI() -} diff --git a/ui/qml/xstudio/base/widgets/XsElideLabel.qml b/ui/qml/xstudio/base/widgets/XsElideLabel.qml deleted file mode 100644 index a03542ecb..000000000 --- a/ui/qml/xstudio/base/widgets/XsElideLabel.qml +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.15 -import QtQuick.Controls 2.15 - -// Qt.ElideLeft -// Qt.ElideMiddle -// Qt.ElideNone -// Qt.ElideRight - -Item { - id: item - - height: label.height - - property string text - property int elideWidth: width - property int elide: Qt.ElideRight - - property alias color: label.color - property alias font: label.font - property alias horizontalAlignment: label.horizontalAlignment - property alias verticalAlignment: label.verticalAlignment - - Label { - id: label - text: textMetrics.elidedText - anchors.fill: parent - - TextMetrics { - id: textMetrics - text: item.text - - font: label.font - - elide: item.elide - elideWidth: item.elideWidth - } - } -} diff --git a/ui/qml/xstudio/base/widgets/XsExpandButton.qml b/ui/qml/xstudio/base/widgets/XsExpandButton.qml deleted file mode 100644 index be70a0b9a..000000000 --- a/ui/qml/xstudio/base/widgets/XsExpandButton.qml +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.3 -import QtQuick 2.14 -import QtGraphicalEffects 1.12 -import QtQml 2.14 - -import xStudio 1.0 - -Image { - property bool expanded: false - - source: "qrc:///feather_icons/chevron-right.svg" - sourceSize.height: height - sourceSize.width: width - smooth: true - transformOrigin: Item.Center - rotation: expanded ? 90 : 0 - MouseArea { - anchors.fill: parent - onClicked: expanded = !expanded - } - - layer { - enabled: true - effect: ColorOverlay { - color: XsStyle.controlColor - } - } -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/widgets/XsFloatAttrSlider.qml b/ui/qml/xstudio/base/widgets/XsFloatAttrSlider.qml deleted file mode 100644 index 8b58b1152..000000000 --- a/ui/qml/xstudio/base/widgets/XsFloatAttrSlider.qml +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtQuick.Layouts 1.3 -import QtQuick.Window 2.2 - -import xStudio 1.0 -import xstudio.qml.module 1.0 - -Rectangle { - - id: control - property var value_: value ? value : 0.0 - property var min_: float_scrub_min ? float_scrub_min : 0.0 - property var max_: float_scrub_max ? float_scrub_max : 1.0 - property var step_: float_scrub_step ? float_scrub_step : 1.0 - color: "transparent" - - function setValue(v) { - value = v - } - - onValue_Changed: { - // need this check to stop nasty feedback, as ui event changes the - // value in the backend, that is broacast back to the front end that - // changes the value here, which changes the backend value ad infinitum - if (!slider.pressed) { - slider.value = value_; - } - } - - Rectangle { - - id: text_value - width: 40 - color: XsStyle.mediaInfoBarOffsetBgColor - border.width: 1 - border.color: ma3.containsMouse ? XsStyle.hoverColor : XsStyle.mainColor - height: valueInput.font.pixelSize*1.4 - - MouseArea { - id: ma3 - anchors.fill: parent - hoverEnabled: true - } - - TextInput { - - id: valueInput - text: value_.toFixed(2) // 'value' property provided via the xStudio attributes model - - color: enabled ? XsStyle.controlColor : XsStyle.controlColorDisabled - selectByMouse: true - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - anchors.fill: parent - - font { - family: XsStyle.fontFamily - } - - onEditingFinished: { - focus = false - setValue(parseFloat(text)) - } - } - } - - Slider { - - id: slider - to: max_ - from: min_ - stepSize: step_ - orientation: Qt.Horizontal - value: 0.0 - - anchors.left: text_value.right - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.leftMargin: 5 - - handle: Rectangle { - id: sliderHandle - y: (slider.height - height) / 2 - x: slider.visualPosition * (slider.width - width) - width:10 - height:10 - radius:10 - color:XsStyle.hoverColor - } - - onValueChanged : { - if (pressed) { - control.setValue(value) - } - } - - background: Rectangle { - // bar right of handle - color: XsStyle.transportBackground - border.width: 1 - y: (slider.height - height) / 2 - height: 6 - radius: width / 2 - border.color: XsStyle.transportBorderColor - Rectangle { - // bar left of handle - width: slider.visualPosition * parent.width - x: 0 - height: parent.height - color: XsStyle.primaryColor//XsStyle.highlightColor - gradient:styleGradient.accent_gradient - radius: parent.radius - border.width: 1 - border.color: XsStyle.transportBorderColor - } - } - } -} diff --git a/ui/qml/xstudio/base/widgets/XsFrame.qml b/ui/qml/xstudio/base/widgets/XsFrame.qml deleted file mode 100644 index 85e8cc6f7..000000000 --- a/ui/qml/xstudio/base/widgets/XsFrame.qml +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 - -import xStudio 1.1 - -Rectangle{ id: frame - - - property real framePadding: 6 - property real frameWidth: 1 - property real frameRadius: 2 - property color frameColor: palette.base - - - MouseArea{ - anchors.fill: parent; onPressed: focus=true - } - - x: framePadding - y: framePadding - - width: parent.width - framePadding*2 - height: parent.height - framePadding*2 - - color: "transparent" - radius: frameRadius - border.color: frameColor - border.width: frameWidth - -} - - - diff --git a/ui/qml/xstudio/base/widgets/XsFramedButton.qml b/ui/qml/xstudio/base/widgets/XsFramedButton.qml deleted file mode 100644 index 18b557b40..000000000 --- a/ui/qml/xstudio/base/widgets/XsFramedButton.qml +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 2 -import QtQuick 2.14 -import QtGraphicalEffects 1.12 -import QtQml 2.14 - -import xStudio 1.0 - -Rectangle { - id: control - property alias source: image.source - property string toolTip: "" - property string toolTipTitle: "" - - signal clicked(variant mouse) - signal pressed(variant mouse) - signal released(variant mouse) - - gradient: mouse_area.containsMouse ? styleGradient.accent_gradient : null - color: mouse_area.containsMouse ? XsStyle.highlightColor : XsStyle.mainBackground - - border.color : XsStyle.menuBorderColor - border.width : 1 - Image { - id: image - anchors.margins: 2 - anchors.fill: parent - source: "qrc:///feather_icons/more-horizontal.svg" - sourceSize.height: height - sourceSize.width: width - smooth: true - layer { - enabled: true - effect: ColorOverlay { - color: XsStyle.controlColor - } - } - } - MouseArea { - id: mouse_area - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - anchors.fill: parent - - onClicked: control.clicked(mouse) - onReleased: control.released(mouse) - onPressed: control.pressed(mouse) - - onHoveredChanged: { - if(toolTip) { - if (containsMouse) { - status_bar.normalMessage(toolTip, toolTipTitle) - } else { - status_bar.clearMessage() - } - } - } - } -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/widgets/XsHSlider.qml b/ui/qml/xstudio/base/widgets/XsHSlider.qml deleted file mode 100644 index e52e64647..000000000 --- a/ui/qml/xstudio/base/widgets/XsHSlider.qml +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Controls 2.5 -import QtGraphicalEffects 1.12 -import QtQuick.Layouts 1.3 - -import xStudio 1.0 - -Rectangle { - color: "transparent" - id: control - - property int value - property int to: 100 - property int from: 0 - property int factor: 1 - property int stepSize: 1 - property int orientation: Qt.Horizontal - property alias slider: slider - property alias pressed: slider.pressed - - onValueChanged : { - slider.value = value - } - - GridLayout { - - anchors.fill: parent - - signal unPressed() - - columns: 5 - rowSpacing: 1 - - Label { - id: from - text: {'' + Math.round(control.value/control.factor)} - font.family: XsStyle.fontFamily - font.hintingPreference: Font.PreferNoHinting - color: XsStyle.hoverColor - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - } - - - Slider { - id: slider - to: control.to - from: control.from - stepSize: control.stepSize - orientation: Qt.Horizontal - value: control.value - - snapMode: Slider.SnapAlways - Layout.fillWidth: true - Layout.fillHeight: true - //Layout.preferredWidth: 300 - Layout.preferredHeight: 10 - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - - handle: Rectangle { - id: sliderHandle - x: slider.visualPosition * (slider.width - width) - y: (slider.height - height) / 2 - width:10 - height:10 - radius:10 - color:XsStyle.hoverColor - } - - onValueChanged : { - control.value = value - } - - background: Rectangle { - // bar right of handle - color: XsStyle.transportBackground - border.width: 1 - y: (slider.height - height) / 2 - width: parent.width - height: 6 - radius: height / 2 - border.color: XsStyle.transportBorderColor - Rectangle { - // bar left of handle - //width: (1.0-slider.visualPosition) * parent.width - x: slider.visualPosition * parent.width - //width: parent.width - color: "#414141"//preferences.accent_color - gradient:preferences.accent_gradient - radius: parent.radius - border.width: 1 - border.color: XsStyle.transportBorderColor - } - } - - onPressedChanged : { - if (pressed === false) { - unPressed() - } - } - } - } -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/widgets/XsHSpacer.qml b/ui/qml/xstudio/base/widgets/XsHSpacer.qml deleted file mode 100644 index 3114ca0ad..000000000 --- a/ui/qml/xstudio/base/widgets/XsHSpacer.qml +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.0 -import QtQuick.Layouts 1.3 - -Rectangle { - Layout.fillWidth: true -} diff --git a/ui/qml/xstudio/base/widgets/XsHotkeyView.qml b/ui/qml/xstudio/base/widgets/XsHotkeyView.qml deleted file mode 100644 index b6432036b..000000000 --- a/ui/qml/xstudio/base/widgets/XsHotkeyView.qml +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick.Controls 2 -import QtQuick 2.14 -import QtGraphicalEffects 1.12 -import QtQml 2.14 - -import xStudio 1.0 - -Rectangle { - - id: control - color: "transparent" - - border.color : XsStyle.menuBorderColor - border.width : 1 - - width: 72+128+256+8 - - Text { - id: seq - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 7 - width: 72 - text: sequence - color: XsStyle.controlColor - font.family: XsStyle.controlTitleFontFamily - font.pixelSize: XsStyle.popupControlFontSize - } - - Rectangle { - id: divider - color: XsStyle.menuBorderColor - anchors.left: seq.right - y: 4 - height: parent.height-8 - width: 2 - } - - Text { - id: cmp - anchors.left: divider.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 7 - width: 128 - text: component - color: XsStyle.controlColor - font.family: XsStyle.controlTitleFontFamily - font.pixelSize: XsStyle.popupControlFontSize - } - - Rectangle { - id: divider2 - color: XsStyle.menuBorderColor - anchors.left: cmp.right - y: 4 - height: parent.height-8 - width: 2 - } - - - Text { - id: thetext - anchors.left: divider2.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 7 - width: 256 - text: hotkey_name - color: XsStyle.controlColor - font.family: XsStyle.controlTitleFontFamily - font.pixelSize: XsStyle.popupControlFontSize - } - -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/widgets/XsIntAttrSlider.qml b/ui/qml/xstudio/base/widgets/XsIntAttrSlider.qml deleted file mode 100644 index db45a9509..000000000 --- a/ui/qml/xstudio/base/widgets/XsIntAttrSlider.qml +++ /dev/null @@ -1,120 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 -import QtQuick.Layouts 1.3 -import QtQuick.Window 2.2 - -import xStudio 1.0 -import xstudio.qml.module 1.0 - -Rectangle { - - id: control - property var value_: value ? value : 0 - property var min_: integer_min ? integer_min : 0 - property var max_: integer_max ? integer_max : 1 - color: "transparent" - - function setValue(v) { - value = v - } - - onValue_Changed: { - // need this check to stop nasty feedback, as ui event changes the - // value in the backend, that is broacast back to the front end that - // changes the value here, which changes the backend value ad infinitum - if (!slider.pressed) { - slider.value = value_; - } - } - - Rectangle { - - id: text_value - width: 40 - color: XsStyle.mediaInfoBarOffsetBgColor - border.width: 1 - border.color: ma3.containsMouse ? XsStyle.hoverColor : XsStyle.mainColor - height: valueInput.font.pixelSize*1.4 - - MouseArea { - id: ma3 - anchors.fill: parent - hoverEnabled: true - } - - TextInput { - - id: valueInput - text: value_.toFixed(0) // 'value' property provided via the xStudio attributes model - - color: enabled ? XsStyle.controlColor : XsStyle.controlColorDisabled - selectByMouse: true - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - anchors.fill: parent - - font { - family: XsStyle.fontFamily - } - - onEditingFinished: { - focus = false - setValue(parseInt(text)) - } - } - } - - Slider { - - id: slider - to: max_ - from: min_ - stepSize: 1 - orientation: Qt.Horizontal - value: 0.0 - - anchors.left: text_value.right - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.leftMargin: 5 - - handle: Rectangle { - id: sliderHandle - y: (slider.height - height) / 2 - x: slider.visualPosition * (slider.width - width) - width:10 - height:10 - radius:10 - color:XsStyle.hoverColor - } - - onValueChanged : { - if (pressed) { - control.setValue(value) - } - } - - background: Rectangle { - // bar right of handle - color: XsStyle.transportBackground - border.width: 1 - y: (slider.height - height) / 2 - height: 6 - radius: width / 2 - border.color: XsStyle.transportBorderColor - Rectangle { - // bar left of handle - width: slider.visualPosition * parent.width - x: 0 - height: parent.height - color: XsStyle.primaryColor//XsStyle.highlightColor - gradient:styleGradient.accent_gradient - radius: parent.radius - border.width: 1 - border.color: XsStyle.transportBorderColor - } - } - } -} diff --git a/ui/qml/xstudio/base/widgets/XsIntSlider.qml b/ui/qml/xstudio/base/widgets/XsIntSlider.qml deleted file mode 100644 index eb2c01e40..000000000 --- a/ui/qml/xstudio/base/widgets/XsIntSlider.qml +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Controls 2.5 -import QtGraphicalEffects 1.12 -import QtQuick.Layouts 1.3 - -import xStudio 1.0 - -GridLayout { - id: control - - property int value - property int to: 100 - property int from: 0 - property int factor: 1 - property int stepSize: 1 - property int orientation: Qt.Vertical - property alias slider: slider - property alias pressed: slider.pressed - - signal unPressed() - - columns: 1 - rowSpacing: 5 - - Label { - id: from - text: {'' + Math.round(control.value/control.factor)} - font.family: XsStyle.fontFamily - font.hintingPreference: Font.PreferNoHinting - color: XsStyle.hoverColor - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - } - - onValueChanged : { - slider.value = value - } - - Slider { - id: slider - to: control.to - from: control.from - stepSize: control.stepSize - orientation: Qt.Vertical - value: control.value - - snapMode: Slider.SnapAlways - Layout.fillHeight: true - Layout.preferredWidth: 10 - Layout.preferredHeight: 300 - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - - handle: Rectangle { - id: sliderHandle - x: (slider.width - width) / 2 - y: slider.visualPosition * (slider.height - height) - width:10 - height:10 - radius:10 - color:XsStyle.hoverColor - } - - onValueChanged : { - control.value = value - } - - background: Rectangle { - // bar right of handle - color: XsStyle.transportBackground - border.width: 1 - x: (slider.width - width) / 2 - width: 6 - radius: width / 2 - border.color: XsStyle.transportBorderColor - Rectangle { - // bar left of handle - height: (1.0-slider.visualPosition) * parent.height - y: slider.visualPosition * parent.height - width: parent.width - color: XsStyle.primaryColor//XsStyle.highlightColor - gradient:styleGradient.accent_gradient - radius: parent.radius - border.width: 1 - border.color: XsStyle.transportBorderColor - } - } - - onPressedChanged : { - if (pressed === false) { - unPressed() - } - } - } -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/widgets/XsIntSliderPopupSimple.qml b/ui/qml/xstudio/base/widgets/XsIntSliderPopupSimple.qml deleted file mode 100644 index 1c9e88a48..000000000 --- a/ui/qml/xstudio/base/widgets/XsIntSliderPopupSimple.qml +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Controls 2.5 -import QtGraphicalEffects 1.12 -import QtQuick.Layouts 1.3 - -import xStudio 1.0 - -XsPopup { - id: popup - - property alias value: control.value - property alias to: control.to - property alias from: control.from - property alias factor: control.factor - property alias stepSize: control.stepSize - property alias orientation: control.orientation - property alias slider: control.slider - - XsIntSlider { - id: control - anchors.fill: parent - - onUnPressed: close() - } -} diff --git a/ui/qml/xstudio/base/widgets/XsLabel.qml b/ui/qml/xstudio/base/widgets/XsLabel.qml deleted file mode 100644 index d1607540c..000000000 --- a/ui/qml/xstudio/base/widgets/XsLabel.qml +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.14 - -import xStudio 1.0 - -Label { - // color: XsStyle.controlTitleColor - font { - pixelSize: XsStyle.popupControlFontSize - family: XsStyle.controlContentFontFamily - hintingPreference: Font.PreferNoHinting - } -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/widgets/XsLabelInput.qml b/ui/qml/xstudio/base/widgets/XsLabelInput.qml deleted file mode 100644 index b9d22f9d1..000000000 --- a/ui/qml/xstudio/base/widgets/XsLabelInput.qml +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Comprised of 2 pieces: -// 1. HEADER (Rect + Text) -// 2. ListView (driven by Models) that uses Delegates to display a row of buttons - -import QtQuick 2.12 -import QtQml.Models 2.12 -import QtQuick.Controls 2.5 -import QtQuick.Layouts 1.3 - -import xStudio 1.0 - -XsTextInput { - id: control - - leftPadding: 6 - rightPadding: 6 - topPadding: 2 - bottomPadding:2 - - Rectangle { - id: background - anchors.fill: parent - color: palette.base - z: parent.z - 1 - border.width: parent && parent.activeFocus ? 2 : 1 - border.color: parent && parent.activeFocus ? palette.highlight : palette.button - } - Connections { - // function onActiveFocusChanged() { - // // background.color = (activeFocus ? XsStyle.highlightColor : palette.base) - // // control.color = (activeFocus ? "black" : XsStyle.controlTitleColor) - // } - function onEditingFinished() { - parent.forceActiveFocus() - } - } -} \ No newline at end of file diff --git a/ui/qml/xstudio/base/widgets/XsMenu.qml b/ui/qml/xstudio/base/widgets/XsMenu.qml deleted file mode 100644 index b5af3a24c..000000000 --- a/ui/qml/xstudio/base/widgets/XsMenu.qml +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtGraphicalEffects 1.12 - -import xStudio 1.0 - -Menu { - id:myMenu - - // property alias topLeftCorner: bgrect.topLeftCorner - // property alias topRightCorner: bgrect.topRightCorner - // property alias bottomRightCorner: bgrect.bottomRightCorner - // property alias bottomLeftCorner: bgrect.bottomLeftCorner - - // background: RoundedRectangle { - property alias radius: bgrect.radius - property bool hasCheckable: false - property bool fakeDisabled: false - topPadding: Math.max(3, XsStyle.menuRadius) - bottomPadding: topPadding - dim: false - - background: Rectangle { - id: bgrect - border { - color: XsStyle.menuBorderColor - width: XsStyle.menuBorderWidth - } - color: XsStyle.mainBackground - radius: XsStyle.menuRadius - } - width: { - var result = 0; - var padding = 0; - for (var i = 0; i < count; ++i) { - var item = itemAt(i); - if (item.contentItem != undefined) { - result = Math.max(item.contentItem.implicitWidth, result); - padding = Math.max(item.padding, padding); - } - } - return Math.max(100, result + padding * 2); - } - delegate: XsMenuItem {} - Component.onCompleted: { - var curritem; - for (var i=0; i